diff --git a/.clang-tidy b/.clang-tidy index 41df5470ff..02e90d9148 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -85,6 +85,8 @@ CheckOptions: readability-braces-around-statements.ShortStatementLines: 2 readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.InlineNamespaceCase: lower_case readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.StructCase: CamelCase readability-identifier-naming.UnionCase: CamelCase diff --git a/.envrc b/.envrc index 3550a30f2d..cecf4b4767 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ +watch_file nix/*.nix + use flake diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 60f3da09f1..9510212344 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -2,12 +2,21 @@ "image_tag": "sha-fecfc0c", "configs": { "ubuntu": [ + { + "compiler": ["gcc"], + "build_type": ["Debug"], + "arch": ["amd64"], + "minimal": true, + "suffix": "coverage", + "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" + }, { "compiler": ["clang"], "build_type": ["Release"], "arch": ["amd64"], "minimal": true }, + { "compiler": ["gcc"], "build_type": ["Release"], @@ -29,14 +38,6 @@ "sanitizers": ["address", "undefinedbehavior"] }, - { - "compiler": ["gcc"], - "build_type": ["Debug"], - "arch": ["amd64"], - "minimal": true, - "suffix": "coverage", - "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" - }, { "compiler": ["clang"], "build_type": ["Debug"], diff --git a/BUILD.md b/BUILD.md index a15c94edc9..edc52fe3b7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,6 +42,8 @@ Our Linux CI tooling is distro-independent and uses a Nix-based environment, so ### macOS Many `xrpld` engineers use macOS for development. +The minimum supported version is macOS 15 (Sequoia). +CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15. ### Windows diff --git a/CMakeLists.txt b/CMakeLists.txt index a115304ddb..cd0a6216fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,23 @@ if(DEFINED CMAKE_MODULE_PATH) endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# Must be set before project() because project() consumes it when configuring the compiler and SDK. +# A user-provided -DCMAKE_OSX_DEPLOYMENT_TARGET still takes precedence. +# +# CMAKE_SYSTEM_NAME can't be used before project(), so CMAKE_HOST_SYSTEM_NAME is used instead. +# +# When CMAKE_OSX_DEPLOYMENT_TARGET is bumped to >=26.0, FastFloat dependency won't be needed anymore +if( + CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" + AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET +) + set(CMAKE_OSX_DEPLOYMENT_TARGET + "15.0" + CACHE STRING + "Minimum macOS deployment version" + ) +endif() + project(xrpl) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 23) @@ -87,6 +104,7 @@ include(deps/Boost) add_subdirectory(external/antithesis-sdk) find_package(date REQUIRED) find_package(ed25519 REQUIRED) +find_package(FastFloat REQUIRED) find_package(gRPC REQUIRED) find_package(LibArchive REQUIRED) find_package(lz4 REQUIRED) @@ -102,6 +120,7 @@ target_link_libraries( xrpl_libs INTERFACE ed25519::ed25519 + FastFloat::fast_float lz4::lz4 mpt-crypto::mpt-crypto OpenSSL::Crypto diff --git a/cmake/XrplAddBenchmark.cmake b/cmake/XrplAddBenchmark.cmake index 1dd875dd61..921deb0658 100644 --- a/cmake/XrplAddBenchmark.cmake +++ b/cmake/XrplAddBenchmark.cmake @@ -1,3 +1,5 @@ +include_guard() + include(isolate_headers) # Define a benchmark executable for the module `name`. diff --git a/conan.lock b/conan.lock index 5b6fa9620f..d0a97e5c15 100644 --- a/conan.lock +++ b/conan.lock @@ -17,12 +17,13 @@ "mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188", "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744", - "libcurl/8.20.0#c90b0c91a33d9a79b519c1c70bafc823%1780907438.587", + "libcurl/8.21.0#8c26e59c04891ba3373ea3552e18f67f%1783067699.863", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732", "libarchive/3.8.7#c446109bd1f1d8ba7936c94189bc50e6%1782392403.066892", "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979", "grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135", + "fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1785888854.601666", "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562", "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492", "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654", @@ -41,7 +42,7 @@ "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162", "msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649", "meson/1.10.2#9d2d10681fe7fe61c788c58626c89b25%1775558003.754", - "m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259", + "m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659", "libtool/2.4.7#14e7739cc128bc1623d2ed318008e47e%1755679003.847", "gnu-config/cci.20210814#466e9d4d7779e1c142443f7ea44b4284%1762363589.329", "cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091", diff --git a/conan/profiles/default b/conan/profiles/default index 40e95dc42d..8ad723a9af 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -5,6 +5,13 @@ {% 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 #} +{# dependency objects target a newer macOS than the binary and the linker warns. #} +{# Keep at or below CMAKE_OSX_DEPLOYMENT_TARGET in CMakeLists.txt. #} +{% set min_macos_version = "15.0" %} +{% endif %} [settings] os={{ os }} @@ -18,6 +25,9 @@ compiler.runtime=static {% else %} compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }} {% endif %} +{% if os == "Macos" %} +os.version={{ min_macos_version }} +{% endif %} [conf] {# The Boost recipe builds with b2, which doesn't use Conan's toolchain files. #} @@ -41,6 +51,16 @@ tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'} {# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #} user.package:cppstd_version=23 tools.info.package_id:confs+=["user.package:cppstd_version"] + +{% if os == "Macos" %} +[buildenv] +{# os.version adds -mmacosx-version-min to compiler command lines, #} +{# but Boost.Context's b2 assembly (.S) rule ignores it, #} +{# so those objects keep the host SDK version and still warn at link time. #} +{# clang's assembler honors this env var regardless, pinning them. #} +{# Scoped to boost/* since it is the only gap. #} +boost/*:MACOSX_DEPLOYMENT_TARGET={{ min_macos_version }} +{% endif %} {% if compiler == "gcc" and compiler_version < 13 %} tools.build:cxxflags+=['-Wno-restrict'] {% endif %} diff --git a/conanfile.py b/conanfile.py index fdd7426440..9f85e4cfa1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -30,6 +30,7 @@ class Xrpl(ConanFile): requires = [ "ed25519/2015.03", + "fast_float/8.2.10", "grpc/1.81.1", "libarchive/3.8.7", "nudb/2.0.9", @@ -218,6 +219,7 @@ class Xrpl(ConanFile): "boost::thread", "date::date", "ed25519::ed25519", + "fast_float::fast_float", "grpc::grpc++", "libarchive::libarchive", "lz4::lz4", diff --git a/docs/build/nix_troubleshooting.md b/docs/build/nix_troubleshooting.md index ae5cb8059a..fa766c0ee9 100644 --- a/docs/build/nix_troubleshooting.md +++ b/docs/build/nix_troubleshooting.md @@ -3,6 +3,78 @@ Common issues encountered when using the [Nix development shell](./nix.md), and how to resolve them. +## `command not found: nix` after a macOS update + +If a shell suddenly can't find `nix` at all: + +``` +$ nix develop +zsh: command not found: nix +``` + +then Nix is almost certainly still installed — only the shell hook that puts it +on your `PATH` is gone. Confirm that first: + +```bash +ls -l /nix/var/nix/profiles/default/bin/nix +``` + +If that exists, the installation is fine and this is purely a `PATH` problem. + +### Why it happens + +The installer does not touch your dotfiles. Instead it sources a setup script +from the Nix store by editing **system-wide** rc files: + +| Shell | File the installer edits | +| ----- | ------------------------------------- | +| bash | `/etc/bashrc`, `/etc/bash.bashrc` | +| zsh | `/etc/zshrc` | +| fish | `$__fish_sysconf_dir/conf.d/nix.fish` | + +macOS manages `/etc/zshrc`, so an OS update can replace it with the vendor copy +and silently drop the Nix block. `/etc/bashrc` and the fish file usually survive, +which is why the breakage often shows up in zsh only. You can verify this by +diffing against the backup the installer left behind: + +```bash +diff /etc/zshrc /etc/zshrc.backup-before-nix +``` + +If they are identical, the Nix snippet was wiped. This is upstream issue +[NixOS/nix#3616](https://github.com/NixOS/nix/issues/3616). + +### Fix + +To unblock the current shell: + +```bash +. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh +``` + +For a permanent fix, add the snippet to your **user** rc file rather than +restoring `/etc/zshrc` — user dotfiles are not clobbered by OS updates: + +```bash +cat >>~/.zshrc <<'EOF' + +# Nix +if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' +fi +# End Nix +EOF +``` + +The scripts guard against double-sourcing via `__ETC_PROFILE_NIX_SOURCED`, so +this is safe even if a system-wide hook is later restored. + +> [!NOTE] +> `/etc/zshrc` and `~/.zshrc` are only read by **interactive** zsh. If the +> snippet is present but `zsh -c '…'`, a script, or an IDE terminal still can't +> find `nix`, that shell is non-interactive — put the snippet in `~/.zshenv` +> instead. + ## Git worktrees If `nix develop` fails with an error like: diff --git a/include/xrpl/basics/Resolver.h b/include/xrpl/basics/Resolver.h index 239eb9630e..a159619517 100644 --- a/include/xrpl/basics/Resolver.h +++ b/include/xrpl/basics/Resolver.h @@ -11,7 +11,7 @@ namespace xrpl { class Resolver { public: - using HandlerType = std::function)>; + using HandlerType = std::function)>; virtual ~Resolver() = 0; diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 36e7615c3a..75c9b8c7bd 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -251,4 +252,11 @@ makeSlice(std::basic_string const& s) return Slice(s.data(), s.size()); } +template +Slice +makeSlice(std::basic_string_view s) +{ + return Slice(s.data(), s.size()); +} + } // namespace xrpl diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 7cf21892bd..288c5d6673 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -58,7 +58,7 @@ struct LexicalCast "beast::LexicalCast can only be used with integral types"); template - bool + constexpr bool operator()(Integral& out, std::string_view in) const requires(std::is_integral_v && !std::is_same_v) { @@ -110,7 +110,7 @@ struct LexicalCast> { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, boost::core::basic_string_view in) const { return LexicalCast()(out, in); @@ -123,7 +123,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, std::string in) const { return LexicalCast()(out, in); @@ -136,7 +136,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char const* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char const*) : non-null input"); @@ -151,7 +151,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char*) : non-null input"); @@ -177,7 +177,7 @@ struct BadLexicalCast : public std::bad_cast * @return `false` if there was a parsing or range error */ template -bool +constexpr bool lexicalCastChecked(Out& out, In in) { return detail::LexicalCast()(out, in); @@ -191,7 +191,7 @@ lexicalCastChecked(Out& out, In in) * @return The new type. */ template -Out +constexpr Out lexicalCastThrow(In in) { if (Out out; lexicalCastChecked(out, in)) @@ -207,7 +207,7 @@ lexicalCastThrow(In in) * @return The new type. */ template -Out +constexpr Out lexicalCast(In in, Out defaultValue = Out()) { if (Out out; lexicalCastChecked(out, in)) diff --git a/include/xrpl/beast/insight/StatsDCollector.h b/include/xrpl/beast/insight/StatsDCollector.h index e14d3a27ff..0b44f345ba 100644 --- a/include/xrpl/beast/insight/StatsDCollector.h +++ b/include/xrpl/beast/insight/StatsDCollector.h @@ -26,7 +26,7 @@ public: * @param journal Destination for logging output. */ static std::shared_ptr - make(IP::Endpoint const& address, std::string const& prefix, Journal journal); + make(ip::Endpoint const& address, std::string const& prefix, Journal journal); }; } // namespace beast::insight diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index 4f4fb189a6..7422778ea2 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -15,7 +15,7 @@ //------------------------------------------------------------------------------ namespace beast { -namespace IP { +namespace ip { using Address = boost::asio::ip::address; @@ -73,13 +73,13 @@ isPublic(Address const& addr) return (addr.is_v4()) ? isPublic(addr.to_v4()) : isPublic(addr.to_v6()); } -} // namespace IP +} // namespace ip //------------------------------------------------------------------------------ template void -hash_append(Hasher& h, beast::IP::Address const& addr) noexcept +hash_append(Hasher& h, beast::ip::Address const& addr) noexcept { using beast::hash_append; if (addr.is_v4()) @@ -101,12 +101,12 @@ hash_append(Hasher& h, beast::IP::Address const& addr) noexcept namespace boost { template <> -struct hash<::beast::IP::Address> +struct hash<::beast::ip::Address> { explicit hash() = default; std::size_t - operator()(::beast::IP::Address const& addr) const + operator()(::beast::ip::Address const& addr) const { return ::beast::Uhash<>{}(addr); } diff --git a/include/xrpl/beast/net/IPAddressConversion.h b/include/xrpl/beast/net/IPAddressConversion.h index 73777cf841..e2d485642f 100644 --- a/include/xrpl/beast/net/IPAddressConversion.h +++ b/include/xrpl/beast/net/IPAddressConversion.h @@ -4,7 +4,7 @@ #include -namespace beast::IP { +namespace beast::ip { /** * Convert to Endpoint. @@ -32,7 +32,7 @@ toAsioAddress(Endpoint const& endpoint); boost::asio::ip::tcp::endpoint toAsioEndpoint(Endpoint const& endpoint); -} // namespace beast::IP +} // namespace beast::ip namespace beast { @@ -41,25 +41,25 @@ struct IPAddressConversion { explicit IPAddressConversion() = default; - static IP::Endpoint + static ip::Endpoint fromAsio(boost::asio::ip::address const& address) { - return IP::fromAsio(address); + return ip::fromAsio(address); } - static IP::Endpoint + static ip::Endpoint fromAsio(boost::asio::ip::tcp::endpoint const& endpoint) { - return IP::fromAsio(endpoint); + return ip::fromAsio(endpoint); } static boost::asio::ip::address - toAsioAddress(IP::Endpoint const& address) + toAsioAddress(ip::Endpoint const& address) { - return IP::toAsioAddress(address); + return ip::toAsioAddress(address); } static boost::asio::ip::tcp::endpoint - toAsioEndpoint(IP::Endpoint const& address) + toAsioEndpoint(ip::Endpoint const& address) { - return IP::toAsioEndpoint(address); + return ip::toAsioEndpoint(address); } }; diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h index 94943af3ea..280c2c791c 100644 --- a/include/xrpl/beast/net/IPAddressV4.h +++ b/include/xrpl/beast/net/IPAddressV4.h @@ -2,7 +2,7 @@ #include -namespace beast::IP { +namespace beast::ip { using AddressV4 = boost::asio::ip::address_v4; @@ -25,4 +25,4 @@ isPublic(AddressV4 const& addr); char getClass(AddressV4 const& address); -} // namespace beast::IP +} // namespace beast::ip diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h index b51cb62532..0659e7e405 100644 --- a/include/xrpl/beast/net/IPAddressV6.h +++ b/include/xrpl/beast/net/IPAddressV6.h @@ -2,7 +2,7 @@ #include -namespace beast::IP { +namespace beast::ip { using AddressV6 = boost::asio::ip::address_v6; @@ -18,4 +18,4 @@ isPrivate(AddressV6 const& addr); bool isPublic(AddressV6 const& addr); -} // namespace beast::IP +} // namespace beast::ip diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index c4b269e9c3..d4d3b2ab12 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -13,7 +13,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { using Port = std::uint16_t; @@ -223,7 +223,7 @@ operator<<(OutputStream& os, Endpoint const& endpoint) std::istream& operator>>(std::istream& is, Endpoint& endpoint); -} // namespace beast::IP +} // namespace beast::ip //------------------------------------------------------------------------------ @@ -232,12 +232,12 @@ namespace std { * std::hash support. */ template <> -struct hash<::beast::IP::Endpoint> +struct hash<::beast::ip::Endpoint> { hash() = default; std::size_t - operator()(::beast::IP::Endpoint const& endpoint) const + operator()(::beast::ip::Endpoint const& endpoint) const { return ::beast::Uhash<>{}(endpoint); } @@ -249,12 +249,12 @@ namespace boost { * boost::hash support. */ template <> -struct hash<::beast::IP::Endpoint> +struct hash<::beast::ip::Endpoint> { hash() = default; std::size_t - operator()(::beast::IP::Endpoint const& endpoint) const + operator()(::beast::ip::Endpoint const& endpoint) const { return ::beast::Uhash<>{}(endpoint); } diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index c20fe2522c..e24904a87b 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -295,6 +295,20 @@ public: return runner_->arg(); } +protected: + /** + * Lets a suite compose other suites (e.g. an aggregator that reruns a + * group of related suites under its own name) via `SuiteInfo::run`. + * + * @return The runner this suite is executing under. + */ + Runner& + runner() const + { + return *runner_; + } + +public: /** * DEPRECATED * @return `true` if the test condition indicates success(a false value) diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 414dff896d..cb7a9d00ad 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -18,9 +18,9 @@ namespace xrpl { namespace node_store { class Database; } // namespace node_store -namespace Resource { +namespace resource { class Manager; -} // namespace Resource +} // namespace resource namespace perf { class PerfLog; } // namespace perf @@ -163,7 +163,7 @@ public: virtual PeerReservationTable& getPeerReservations() = 0; - virtual Resource::Manager& + virtual resource::Manager& getResourceManager() = 0; // Storage services diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 47ad3ac1e0..260917face 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -623,6 +623,7 @@ class ValueConstIterator : public ValueIteratorBase public: using size_t = unsigned int; using difference_type = int; + using value_type = Value const; using reference = Value const&; using pointer = Value const*; using SelfType = ValueConstIterator; @@ -687,6 +688,7 @@ class ValueIterator : public ValueIteratorBase public: using size_t = unsigned int; using difference_type = int; + using value_type = Value; using reference = Value&; using pointer = Value*; using SelfType = ValueIterator; diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index 8e0d11cccb..c69efff964 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -286,6 +286,77 @@ computeFullPaymentInterest( std::uint32_t startDate, TenthBips32 closeInterestRate); +// Deltas applied to Vault.AssetsTotal and LoanBroker.DebtTotal at a single +// accounting touch point (origination, payment, impair/unimpair/default). +struct AccountingDeltas +{ + Number assetsTotalDelta; + Number debtTotalDelta; +}; + +// Whole-life (pre-LendingProtocolV1_1) recognition model: interest is +// recognized into AssetsTotal/DebtTotal up front, at origination. +namespace accrual { + +// LoanSet origination: what's added to Vault.AssetsTotal and LoanBroker.DebtTotal +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested, Number const& interestDue); + +// LoanSet origination: would recognizing this loan's interest push +// Vault.AssetsTotal past Vault.AssetsMaximum? +bool +loanOriginationExceedsVaultMaximum( + Number const& vaultMaximum, + Number const& vaultTotal, + Number const& interestDue); + +// LoanManage impair/unimpair/default: the vault's exposure to this loan +Number +loanVaultExposure(SLE::const_ref loanSle); + +// LoanPay: what's added to Vault.AssetsTotal and subtracted from LoanBroker.DebtTotal for a payment +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts); + +} // namespace accrual + +// Cash-basis (LendingProtocolV1_1) recognition model: AssetsTotal/DebtTotal +// are principal-only, interest is recognized only as it's actually paid. +namespace cash_basis { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested); + +Number +loanVaultExposure(SLE::const_ref loanSle); + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts); + +} // namespace cash_basis + +// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is +// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is +// VaultVersion::CashBasis, else accrual::. These are the only entry points +// transactors call. +AccountingDeltas +loanOriginationDeltas( + SLE::const_ref vaultSle, + Number const& principalRequested, + Number const& interestDue); + +bool +loanOriginationExceedsVaultMaximum( + SLE::const_ref vaultSle, + Number const& vaultTotal, + Number const& interestDue); + +Number +loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle); + +AccountingDeltas +loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts); + namespace detail { // These classes and functions should only be accessed by LendingHelper // functions and unit tests diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 1bd1663314..5681cc57e8 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -107,4 +108,19 @@ sharesToAssetsWithdraw( [[nodiscard]] bool isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance); +/** + * Resolves a Vault's LEVersion, the single point every accounting touch + * point should call to determine which recognition model (accrual vs. + * cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1 + * activated never have sfLEVersion set, which resolves here to + * VaultVersion::Legacy. + * + * @param vault The vault SLE. + * + * @return The Vault's LEVersion, or VaultVersion::Legacy if the field is + * absent. + */ +[[nodiscard]] VaultVersion +getVaultVersion(SLE::const_ref vault); + } // namespace xrpl diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h index b98885959d..d090247388 100644 --- a/include/xrpl/net/AutoSocket.h +++ b/include/xrpl/net/AutoSocket.h @@ -67,16 +67,16 @@ public: return socket_->next_layer(); } - beast::IP::Endpoint + beast::ip::Endpoint localEndpoint() { - return beast::IP::fromAsio(lowestLayer().local_endpoint()); + return beast::ip::fromAsio(lowestLayer().local_endpoint()); } - beast::IP::Endpoint + beast::ip::Endpoint remoteEndpoint() { - return beast::IP::fromAsio(lowestLayer().remote_endpoint()); + return beast::ip::fromAsio(lowestLayer().remote_endpoint()); } lowest_layer_type& diff --git a/include/xrpl/peerfinder/Config.h b/include/xrpl/peerfinder/Config.h index 9ff0d342c3..3326ae8a97 100644 --- a/include/xrpl/peerfinder/Config.h +++ b/include/xrpl/peerfinder/Config.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { struct PeerLimitConfig { @@ -28,7 +28,7 @@ struct Config * This includes both inbound and outbound, but does not include * fixed peers. */ - std::size_t maxPeers{Tuning::kDefaultMaxPeers}; + std::size_t maxPeers{tuning::kDefaultMaxPeers}; /** * The number of automatic outbound connections to maintain. @@ -100,7 +100,7 @@ struct Config onWrite(beast::PropertyStream::Map& map) const; /** - * Make PeerFinder::Config from peer limit and server mode parameters. + * Make peer_finder::Config from peer limit and server mode parameters. */ static Config makeConfig( @@ -160,4 +160,4 @@ to_string(Result result) noexcept return "unknown"; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/PeerfinderManager.h b/include/xrpl/peerfinder/PeerfinderManager.h index ed683520c1..bb03d85537 100644 --- a/include/xrpl/peerfinder/PeerfinderManager.h +++ b/include/xrpl/peerfinder/PeerfinderManager.h @@ -15,7 +15,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Maintains a set of IP addresses used for getting into the network. @@ -68,17 +68,17 @@ public: * file, along with the set of corresponding IP addresses. */ virtual void - addFixedPeer(std::string_view name, std::vector const& addresses) = 0; + addFixedPeer(std::string_view name, std::vector const& addresses) = 0; /** - * Add a set of strings as fallback IP::Endpoint sources. + * Add a set of strings as fallback ip::Endpoint sources. * @param name A label used for diagnostics. */ virtual void addFallbackStrings(std::string const& name, std::vector const& strings) = 0; /** - * Add a URL as a fallback location to obtain IP::Endpoint sources. + * Add a URL as a fallback location to obtain ip::Endpoint sources. * @param name A label used for diagnostics. */ /* VFALCO NOTE Unimplemented @@ -95,8 +95,8 @@ public: */ virtual std::pair, Result> newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) = 0; + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) = 0; /** * Create a new outbound slot with the specified remote endpoint. @@ -104,7 +104,7 @@ public: * Usually this is because of a duplicate connection. */ virtual std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) = 0; /** * Called when mtENDPOINTS is received. @@ -145,7 +145,7 @@ public: * @return `true` if the connection should be kept */ virtual bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; + onConnected(std::shared_ptr const& slot, beast::ip::Endpoint const& localEndpoint) = 0; /** * Request an active slot type. @@ -162,7 +162,7 @@ public: /** * Return a set of addresses we should connect to. */ - virtual std::vector + virtual std::vector autoconnect() = 0; virtual std::vector, std::vector>> @@ -176,4 +176,4 @@ public: oncePerSecond() = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/Slot.h b/include/xrpl/peerfinder/Slot.h index 9db39ac94c..58e094afc4 100644 --- a/include/xrpl/peerfinder/Slot.h +++ b/include/xrpl/peerfinder/Slot.h @@ -7,7 +7,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Properties and state associated with a peer to peer overlay connection. @@ -52,13 +52,13 @@ public: /** * The remote endpoint of socket. */ - [[nodiscard]] virtual beast::IP::Endpoint const& + [[nodiscard]] virtual beast::ip::Endpoint const& remoteEndpoint() const = 0; /** * The local endpoint of the socket, when known. */ - [[nodiscard]] virtual std::optional const& + [[nodiscard]] virtual std::optional const& localEndpoint() const = 0; [[nodiscard]] virtual std::optional @@ -72,4 +72,4 @@ public: publicKey() const = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/Types.h b/include/xrpl/peerfinder/Types.h index 9e82d9d65c..1327f2564f 100644 --- a/include/xrpl/peerfinder/Types.h +++ b/include/xrpl/peerfinder/Types.h @@ -8,14 +8,14 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { using clock_type = beast::AbstractClock; /** * Represents a set of addresses. */ -using IPAddresses = std::vector; +using IPAddresses = std::vector; //------------------------------------------------------------------------------ @@ -26,10 +26,10 @@ struct Endpoint { Endpoint() = default; - Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); + Endpoint(beast::ip::Endpoint ep, std::uint32_t hops); std::uint32_t hops = 0; - beast::IP::Endpoint address; + beast::ip::Endpoint address; }; inline bool @@ -43,4 +43,4 @@ operator<(Endpoint const& lhs, Endpoint const& rhs) */ using Endpoints = std::vector; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Bootcache.h b/include/xrpl/peerfinder/detail/Bootcache.h index 2141a374fa..453d9c22d2 100644 --- a/include/xrpl/peerfinder/detail/Bootcache.h +++ b/include/xrpl/peerfinder/detail/Bootcache.h @@ -14,7 +14,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Stores IP addresses useful for gaining initial connections. @@ -65,7 +65,7 @@ private: }; using left_t = boost::bimaps:: - unordered_set_of, std::equal_to<>>; + unordered_set_of, std::equal_to<>>; using right_t = boost::bimaps::multiset_of>; using map_type = boost::bimap; using value_type = map_type::value_type; @@ -73,11 +73,11 @@ private: struct Transform { using first_argument_type = map_type::right_map::const_iterator::value_type const&; - using result_type = beast::IP::Endpoint const&; + using result_type = beast::ip::Endpoint const&; explicit Transform() = default; - beast::IP::Endpoint const& + beast::ip::Endpoint const& operator()(map_type::right_map::const_iterator::value_type const& v) const { return v.get_left(); @@ -121,7 +121,7 @@ public: size() const; /** - * IP::Endpoint iterators that traverse in decreasing valence. + * ip::Endpoint iterators that traverse in decreasing valence. */ /** @{ */ [[nodiscard]] const_iterator @@ -146,25 +146,25 @@ public: * Add a newly-learned address to the cache. */ bool - insert(beast::IP::Endpoint const& endpoint); + insert(beast::ip::Endpoint const& endpoint); /** * Add a staticallyconfigured address to the cache. */ bool - insertStatic(beast::IP::Endpoint const& endpoint); + insertStatic(beast::ip::Endpoint const& endpoint); /** * Called when an outbound connection handshake completes. */ void - onSuccess(beast::IP::Endpoint const& endpoint); + onSuccess(beast::ip::Endpoint const& endpoint); /** * Called when an outbound connection attempt fails to handshake. */ void - onFailure(beast::IP::Endpoint const& endpoint); + onFailure(beast::ip::Endpoint const& endpoint); /** * Stores the cache in the persistent database on a timer. @@ -189,4 +189,4 @@ private: flagForUpdate(); }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Checker.h b/include/xrpl/peerfinder/detail/Checker.h index 28ec83adb1..e1ac1d44e0 100644 --- a/include/xrpl/peerfinder/detail/Checker.h +++ b/include/xrpl/peerfinder/detail/Checker.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Tests remote listening sockets to make sure they are connectable. @@ -104,7 +104,7 @@ public: */ template void - asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& handler); + asyncConnect(beast::ip::Endpoint const& endpoint, Handler&& handler); private: void @@ -179,7 +179,7 @@ Checker::wait() template template void -Checker::asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& handler) +Checker::asyncConnect(beast::ip::Endpoint const& endpoint, Handler&& handler) { auto const op = std::make_shared>(*this, ioContext_, std::forward(handler)); @@ -202,4 +202,4 @@ Checker::remove(BasicAsyncOp& op) cond_.notify_all(); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Counts.h b/include/xrpl/peerfinder/detail/Counts.h index ce78eadce4..035103463e 100644 --- a/include/xrpl/peerfinder/detail/Counts.h +++ b/include/xrpl/peerfinder/detail/Counts.h @@ -10,7 +10,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Direction of a slot count adjustment. @@ -50,7 +50,7 @@ public: // Must be handshaked and in the right state XRPL_ASSERT( s.state() == Slot::State::Connected || s.state() == Slot::State::Accept, - "xrpl::PeerFinder::Counts::can_activate : valid input state"); + "xrpl::peer_finder::Counts::can_activate : valid input state"); if (s.fixed() || s.reserved()) return true; @@ -67,9 +67,9 @@ public: [[nodiscard]] std::size_t attemptsNeeded() const { - if (attempts_ >= Tuning::kMaxConnectAttempts) + if (attempts_ >= tuning::kMaxConnectAttempts) return 0; - return Tuning::kMaxConnectAttempts - attempts_; + return tuning::kMaxConnectAttempts - attempts_; } /** @@ -295,7 +295,7 @@ private: switch (s.state()) { case Slot::State::Accept: - XRPL_ASSERT(s.inbound(), "xrpl::PeerFinder::Counts::adjust : input is inbound"); + XRPL_ASSERT(s.inbound(), "xrpl::peer_finder::Counts::adjust : input is inbound"); acceptCount_ += n; break; @@ -303,7 +303,7 @@ private: case Slot::State::Connected: XRPL_ASSERT( !s.inbound(), - "xrpl::PeerFinder::Counts::adjust : input is not " + "xrpl::peer_finder::Counts::adjust : input is not " "inbound"); attempts_ += n; break; @@ -331,7 +331,7 @@ private: // LCOV_EXCL_START default: - UNREACHABLE("xrpl::PeerFinder::Counts::adjust : invalid input state"); + UNREACHABLE("xrpl::peer_finder::Counts::adjust : invalid input state"); break; // LCOV_EXCL_STOP }; @@ -391,4 +391,4 @@ private: int closingCount_{0}; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Fixed.h b/include/xrpl/peerfinder/detail/Fixed.h index 6754ec6dbd..5a52fd7c3e 100644 --- a/include/xrpl/peerfinder/detail/Fixed.h +++ b/include/xrpl/peerfinder/detail/Fixed.h @@ -7,7 +7,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Metadata for a Fixed slot. @@ -36,8 +36,8 @@ public: void failure(clock_type::time_point const& now) { - failures_ = std::min(failures_ + 1, Tuning::kConnectionBackoff.size() - 1); - when_ = now + std::chrono::minutes(Tuning::kConnectionBackoff[failures_]); + failures_ = std::min(failures_ + 1, tuning::kConnectionBackoff.size() - 1); + when_ = now + std::chrono::minutes(tuning::kConnectionBackoff[failures_]); } /** @@ -55,4 +55,4 @@ private: std::size_t failures_{0}; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Handouts.h b/include/xrpl/peerfinder/detail/Handouts.h index cb5fd7f850..c20d4b2139 100644 --- a/include/xrpl/peerfinder/detail/Handouts.h +++ b/include/xrpl/peerfinder/detail/Handouts.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace detail { @@ -28,7 +28,7 @@ template std::size_t handoutOne(Target& t, HopContainer& h) { - XRPL_ASSERT(!t.full(), "xrpl::PeerFinder::detail::handoutOne : target is not full"); + XRPL_ASSERT(!t.full(), "xrpl::peer_finder::detail::handoutOne : target is not full"); for (auto it = h.begin(); it != h.end(); ++it) { auto const& e = *it; @@ -95,7 +95,7 @@ public: [[nodiscard]] bool full() const { - return list_.size() >= Tuning::kRedirectEndpointCount; + return list_.size() >= tuning::kRedirectEndpointCount; } [[nodiscard]] SlotImp::ptr const& @@ -124,7 +124,7 @@ private: template RedirectHandouts::RedirectHandouts(SlotImp::ptr slot) : slot_(std::move(slot)) { - list_.reserve(Tuning::kRedirectEndpointCount); + list_.reserve(tuning::kRedirectEndpointCount); } template @@ -138,7 +138,7 @@ RedirectHandouts::tryInsert(Endpoint const& ep) // addresses in a peer HTTP handshake instead of // the tmENDPOINTS message. // - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) return false; // Don't send them our address @@ -181,7 +181,7 @@ public: [[nodiscard]] bool full() const { - return list_.size() >= Tuning::kNumberOfEndpoints; + return list_.size() >= tuning::kNumberOfEndpoints; } void @@ -210,7 +210,7 @@ private: template SlotHandouts::SlotHandouts(SlotImp::ptr slot) : slot_(std::move(slot)) { - list_.reserve(Tuning::kNumberOfEndpoints); + list_.reserve(tuning::kNumberOfEndpoints); } template @@ -220,7 +220,7 @@ SlotHandouts::tryInsert(Endpoint const& ep) if (full()) return false; - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) return false; if (slot_->recent.filter(ep.address, ep.hops)) @@ -259,9 +259,9 @@ class ConnectHandouts public: // Keeps track of addresses we have made outgoing connections // to, for the purposes of not connecting to them too frequently. - using Squelches = beast::aged_set; + using Squelches = beast::aged_set; - using list_type = std::vector; + using list_type = std::vector; private: std::size_t needed_; @@ -274,7 +274,7 @@ public: template bool - tryInsert(beast::IP::Endpoint const& endpoint); + tryInsert(beast::ip::Endpoint const& endpoint); [[nodiscard]] bool empty() const @@ -316,13 +316,13 @@ ConnectHandouts::ConnectHandouts(std::size_t needed, Squelches& squelches) template bool -ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint) +ConnectHandouts::tryInsert(beast::ip::Endpoint const& endpoint) { if (full()) return false; // Make sure the address isn't already in our list - if (std::ranges::any_of(list_, [&endpoint](beast::IP::Endpoint const& other) { + if (std::ranges::any_of(list_, [&endpoint](beast::ip::Endpoint const& other) { // Ignore port for security reasons return other.address() == endpoint.address(); })) @@ -341,4 +341,4 @@ ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint) return true; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Livecache.h b/include/xrpl/peerfinder/detail/Livecache.h index cac284d1cc..ec797065e5 100644 --- a/include/xrpl/peerfinder/detail/Livecache.h +++ b/include/xrpl/peerfinder/detail/Livecache.h @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { template class Livecache; @@ -188,10 +188,10 @@ class Livecache : protected detail::LivecacheBase { private: using cache_type = beast::aged_map< - beast::IP::Endpoint, + beast::ip::Endpoint, Element, std::chrono::steady_clock, - std::less, + std::less, Allocator>; beast::Journal journal_; @@ -220,8 +220,8 @@ public: // but not given out (since they would exceed maxHops). They // are used for automatic connection attempts. // - using Histogram = std::array; - using lists_type = std::array; + using Histogram = std::array; + using lists_type = std::array; template struct Transform @@ -400,7 +400,7 @@ Livecache::expire() { std::size_t n(0); typename cache_type::time_point const expired( - cache_.clock().now() - Tuning::kLiveCacheSecondsToLive); + cache_.clock().now() - tuning::kLiveCacheSecondsToLive); for (auto iter(cache_.chronological.begin()); iter != cache_.chronological.end() && iter.when() <= expired;) { @@ -427,8 +427,8 @@ Livecache::insert(Endpoint const& ep) // when redirecting. // XRPL_ASSERT( - ep.hops <= (Tuning::kMaxHops + 1), - "xrpl::PeerFinder::Livecache::insert : maximum input hops"); + ep.hops <= (tuning::kMaxHops + 1), + "xrpl::peer_finder::Livecache::insert : maximum input hops"); auto result = cache_.emplace(ep.address, ep); Element& e(result.first->second); if (result.second) @@ -468,7 +468,7 @@ void Livecache::onWrite(beast::PropertyStream::Map& map) { typename cache_type::time_point const expired( - cache_.clock().now() - Tuning::kLiveCacheSecondsToLive); + cache_.clock().now() - tuning::kLiveCacheSecondsToLive); map["size"] = size(); map["hist"] = hops.histogram(); beast::PropertyStream::Set set("entries", map); @@ -527,8 +527,8 @@ void Livecache::HopsT::insert(Element& e) { XRPL_ASSERT( - e.endpoint.hops <= Tuning::kMaxHops + 1, - "xrpl::PeerFinder::Livecache::HopsT::insert : maximum input hops"); + e.endpoint.hops <= tuning::kMaxHops + 1, + "xrpl::peer_finder::Livecache::HopsT::insert : maximum input hops"); // This has security implications without a shuffle lists_[e.endpoint.hops].push_front(e); ++hist_[e.endpoint.hops]; @@ -539,8 +539,8 @@ void Livecache::HopsT::reinsert(Element& e, std::uint32_t numHops) { XRPL_ASSERT( - numHops <= Tuning::kMaxHops + 1, - "xrpl::PeerFinder::Livecache::HopsT::reinsert : maximum hops input"); + numHops <= tuning::kMaxHops + 1, + "xrpl::peer_finder::Livecache::HopsT::reinsert : maximum hops input"); auto& list = lists_[e.endpoint.hops]; list.erase(list.iterator_to(e)); @@ -561,4 +561,4 @@ Livecache::HopsT::remove(Element& e) list.erase(list.iterator_to(e)); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Logic.h b/include/xrpl/peerfinder/detail/Logic.h index c623263884..4821054280 100644 --- a/include/xrpl/peerfinder/detail/Logic.h +++ b/include/xrpl/peerfinder/detail/Logic.h @@ -43,7 +43,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * The Logic for maintaining the list of Slot addresses. @@ -57,7 +57,7 @@ public: // Maps remote endpoints to slots. Since a slot has a // remote endpoint upon construction, this holds all counts_. // - using Slots = std::map>; + using Slots = std::map>; beast::Journal journal; clock_type& clock; @@ -81,7 +81,7 @@ private: Counts counts_; // A list of slots that should always be connected - std::map fixed_; + std::map fixed_; public: // Live livecache from mtENDPOINTS messages @@ -96,7 +96,7 @@ public: // The addresses (but not port) we are connected to. This includes // outgoing connection attempts. Note that this set can contain // duplicates (since the port is not set) - std::multiset connectedAddresses; + std::multiset connectedAddresses; // Set of public keys belonging to active peers std::set keys; @@ -170,13 +170,13 @@ public: } void - addFixedPeer(std::string_view name, beast::IP::Endpoint const& ep) + addFixedPeer(std::string_view name, beast::ip::Endpoint const& ep) { - addFixedPeer(name, std::vector{ep}); + addFixedPeer(name, std::vector{ep}); } void - addFixedPeer(std::string_view name, std::vector const& addresses) + addFixedPeer(std::string_view name, std::vector const& addresses) { std::scoped_lock const _(lock); @@ -213,8 +213,8 @@ public: // Called when the Checker completes a connectivity test void checkComplete( - beast::IP::Endpoint const& remoteAddress, - beast::IP::Endpoint const& checkedAddress, + beast::ip::Endpoint const& remoteAddress, + beast::ip::Endpoint const& checkedAddress, boost::system::error_code ec) { if (ec == boost::asio::error::operation_aborted) @@ -256,8 +256,8 @@ public: std::pair newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) { JLOG(journal.debug()) << std::left << std::setw(18) << "Logic accept" << remoteEndpoint << " on local " << localEndpoint; @@ -293,7 +293,7 @@ public: // Remote address must not already exist XRPL_ASSERT( result.second, - "xrpl::PeerFinder::Logic::new_inbound_slot : remote endpoint " + "xrpl::peer_finder::Logic::new_inbound_slot : remote endpoint " "inserted"); // Add to the connected address list connectedAddresses.emplace(remoteEndpoint.address()); @@ -306,7 +306,7 @@ public: // Can't check for self-connect because we don't know the local endpoint std::pair - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) { JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " << remoteEndpoint; @@ -329,7 +329,7 @@ public: // Remote address must not already exist XRPL_ASSERT( result.second, - "xrpl::PeerFinder::Logic::new_outbound_slot : remote endpoint " + "xrpl::peer_finder::Logic::new_outbound_slot : remote endpoint " "inserted"); // Add to the connected address list @@ -342,7 +342,7 @@ public: } bool - onConnected(SlotImp::ptr const& slot, beast::IP::Endpoint const& localEndpoint) + onConnected(SlotImp::ptr const& slot, beast::ip::Endpoint const& localEndpoint) { beast::WrappedSink sink{journal.sink(), slot->prefix()}; beast::Journal const journal{sink}; @@ -354,7 +354,7 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::onConnected : valid slot input"); + "xrpl::peer_finder::Logic::onConnected : valid slot input"); // Assign the local endpoint now that it's known slot->localEndpoint(localEndpoint); @@ -365,7 +365,7 @@ public: { XRPL_ASSERT( iter->second->localEndpoint() == slot->remoteEndpoint(), - "xrpl::PeerFinder::Logic::onConnected : local and remote " + "xrpl::peer_finder::Logic::onConnected : local and remote " "endpoints do match"); JLOG(journal.warn()) << "Logic dropping as self connect"; return false; @@ -393,11 +393,11 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::activate : valid slot input"); + "xrpl::peer_finder::Logic::activate : valid slot input"); // Must be accepted or connected XRPL_ASSERT( slot->state() == Slot::State::Accept || slot->state() == Slot::State::Connected, - "xrpl::PeerFinder::Logic::activate : valid slot state"); + "xrpl::peer_finder::Logic::activate : valid slot state"); // Check for duplicate connection by key if (keys.contains(key)) @@ -425,7 +425,7 @@ public: { [[maybe_unused]] bool const inserted = keys.insert(key).second; // Public key must not already exist - XRPL_ASSERT(inserted, "xrpl::PeerFinder::Logic::activate : public key inserted"); + XRPL_ASSERT(inserted, "xrpl::peer_finder::Logic::activate : public key inserted"); } // Change state and update counts @@ -443,7 +443,7 @@ public: if (iter == fixed_.end()) { logicError( - "PeerFinder::Logic::activate(): remote_endpoint " + "peer_finder::Logic::activate(): remote_endpoint " "missing from fixed_"); } @@ -476,10 +476,10 @@ public: // VFALCO TODO This should add the returned addresses to the // squelch list in one go once the list is built, // rather than having each module add to the squelch list. - std::vector + std::vector autoconnect() { - std::vector none; + std::vector none; std::scoped_lock const _(lock); @@ -635,7 +635,7 @@ public: // either. ipv6 has a slightly more compact string // representation of 0, so use that for self entries. ep.address = - beast::IP::Endpoint(beast::IP::AddressV6()).atPort(config_.listeningPort); + beast::ip::Endpoint(beast::ip::AddressV6()).atPort(config_.listeningPort); for (auto& t : targets) t.insert(ep); } @@ -656,7 +656,7 @@ public: result.emplace_back(slot, list); } - whenBroadcast = now + Tuning::kSecondsPerMessage; + whenBroadcast = now + tuning::kSecondsPerMessage; } return result; @@ -675,7 +675,7 @@ public: entry.second->expire(); // Expire the recent attempts table - beast::expire(squelches, Tuning::kRecentAttemptDuration); + beast::expire(squelches, tuning::kRecentAttemptDuration); bootcache.periodicActivity(); } @@ -692,7 +692,7 @@ public: Endpoint& ep(*iter); // Enforce hop limit - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) { JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " << ep.address << " for excess hops " << ep.hops; @@ -754,10 +754,10 @@ public: beast::Journal const journal{sink}; // If we're sent too many endpoints, sample them at random: - if (list.size() > Tuning::kNumberOfEndpointsMax) + if (list.size() > tuning::kNumberOfEndpointsMax) { std::shuffle(list.begin(), list.end(), defaultPrng()); - list.resize(Tuning::kNumberOfEndpointsMax); + list.resize(tuning::kNumberOfEndpointsMax); } JLOG(journal.trace()) << "Endpoints contained " << list.size() @@ -768,12 +768,12 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::onEndpoints : valid slot input"); + "xrpl::peer_finder::Logic::onEndpoints : valid slot input"); // Must be handshaked! XRPL_ASSERT( slot->state() == Slot::State::Active, - "xrpl::PeerFinder::Logic::onEndpoints : valid slot state"); + "xrpl::peer_finder::Logic::onEndpoints : valid slot state"); clock_type::time_point const now(clock.now()); @@ -785,7 +785,7 @@ public: for (auto const& ep : list) { - XRPL_ASSERT(ep.hops, "xrpl::PeerFinder::Logic::onEndpoints : nonzero hops"); + XRPL_ASSERT(ep.hops, "xrpl::peer_finder::Logic::onEndpoints : nonzero hops"); slot->recent.insert(ep.address, ep.hops); @@ -837,7 +837,7 @@ public: bootcache.insert(ep.address); } - slot->whenAcceptEndpoints = now + Tuning::kSecondsPerMessage; + slot->whenAcceptEndpoints = now + tuning::kSecondsPerMessage; } //-------------------------------------------------------------------------- @@ -851,7 +851,7 @@ public: if (iter == slots.end()) { logicError( - "PeerFinder::Logic::remove(): remote_endpoint " + "peer_finder::Logic::remove(): remote_endpoint " "missing from slots_"); } @@ -866,7 +866,7 @@ public: if (iter == keys.end()) { logicError( - "PeerFinder::Logic::remove(): public_key missing " + "peer_finder::Logic::remove(): public_key missing " "from keys_"); } @@ -879,7 +879,7 @@ public: if (iter == connectedAddresses.end()) { logicError( - "PeerFinder::Logic::remove(): remote_endpoint " + "peer_finder::Logic::remove(): remote_endpoint " "address missing from connectedAddresses_"); } @@ -907,7 +907,7 @@ public: if (iter == fixed_.end()) { logicError( - "PeerFinder::Logic::on_closed(): remote_endpoint " + "peer_finder::Logic::on_closed(): remote_endpoint " "missing from fixed_"); } @@ -943,7 +943,7 @@ public: // LCOV_EXCL_START default: UNREACHABLE( - "xrpl::PeerFinder::Logic::on_closed : invalid slot " + "xrpl::peer_finder::Logic::on_closed : invalid slot " "state"); break; // LCOV_EXCL_STOP @@ -968,17 +968,17 @@ public: // Returns `true` if the address matches a fixed slot address // Must have the lock held bool - fixed(beast::IP::Endpoint const& endpoint) const + fixed(beast::ip::Endpoint const& endpoint) const { return std::ranges::any_of( fixed_, [&endpoint](auto const& entry) { return entry.first == endpoint; }); } // Returns `true` if the address matches a fixed slot address - // Note that this does not use the port information in the IP::Endpoint + // Note that this does not use the port information in the ip::Endpoint // Must have the lock held bool - fixed(beast::IP::Address const& address) const + fixed(beast::ip::Address const& address) const { return std::ranges::any_of( fixed_, [&address](auto const& entry) { return entry.first.address() == address; }); @@ -1097,9 +1097,9 @@ public: // //-------------------------------------------------------------------------- - // Returns true if the IP::Endpoint contains no invalid data. + // Returns true if the ip::Endpoint contains no invalid data. bool - isValidAddress(beast::IP::Endpoint const& address) + isValidAddress(beast::ip::Endpoint const& address) { if (isUnspecified(address)) return false; @@ -1220,7 +1220,7 @@ Logic::onRedirects( { std::scoped_lock const _(lock); std::size_t n = 0; - for (; first != last && n < Tuning::kMaxRedirects; ++first, ++n) + for (; first != last && n < tuning::kMaxRedirects; ++first, ++n) bootcache.insert(beast::IPAddressConversion::fromAsio(*first)); if (n > 0) { @@ -1229,4 +1229,4 @@ Logic::onRedirects( } } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/SlotImp.h b/include/xrpl/peerfinder/detail/SlotImp.h index 35c61b13cf..db86183f64 100644 --- a/include/xrpl/peerfinder/detail/SlotImp.h +++ b/include/xrpl/peerfinder/detail/SlotImp.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class SlotImp : public Slot { @@ -21,13 +21,13 @@ public: // inbound SlotImp( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint remoteEndpoint, + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock); // outbound - SlotImp(beast::IP::Endpoint remoteEndpoint, bool fixed, clock_type& clock); + SlotImp(beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock); bool inbound() const override @@ -53,13 +53,13 @@ public: return state_; } - beast::IP::Endpoint const& + beast::ip::Endpoint const& remoteEndpoint() const override { return remoteEndpoint_; } - std::optional const& + std::optional const& localEndpoint() const override { return localEndpoint_; @@ -93,13 +93,13 @@ public: } void - localEndpoint(beast::IP::Endpoint const& endpoint) + localEndpoint(beast::ip::Endpoint const& endpoint) { localEndpoint_ = endpoint; } void - remoteEndpoint(beast::IP::Endpoint const& endpoint) + remoteEndpoint(beast::ip::Endpoint const& endpoint) { remoteEndpoint_ = endpoint; } @@ -140,20 +140,20 @@ public: * sending a slot the same address too frequently. */ void - insert(beast::IP::Endpoint const& ep, std::uint32_t hops); + insert(beast::ip::Endpoint const& ep, std::uint32_t hops); /** * Returns `true` if we should not send endpoint to the slot. */ bool - filter(beast::IP::Endpoint const& ep, std::uint32_t hops); + filter(beast::ip::Endpoint const& ep, std::uint32_t hops); private: void expire(); friend class SlotImp; - beast::aged_unordered_map cache_; + beast::aged_unordered_map cache_; } recent; void @@ -167,8 +167,8 @@ private: bool const fixed_; bool reserved_; State state_; - beast::IP::Endpoint remoteEndpoint_; - std::optional localEndpoint_; + beast::ip::Endpoint remoteEndpoint_; + std::optional localEndpoint_; std::optional publicKey_; static std::int32_t constexpr kUnknownPort = -1; @@ -196,4 +196,4 @@ public: clock_type::time_point whenAcceptEndpoints; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Source.h b/include/xrpl/peerfinder/detail/Source.h index 5cdb535bdd..09aa4e216a 100644 --- a/include/xrpl/peerfinder/detail/Source.h +++ b/include/xrpl/peerfinder/detail/Source.h @@ -7,7 +7,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * A static or dynamic source of peer addresses. @@ -46,4 +46,4 @@ public: fetch(Results& results, beast::Journal journal) = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/SourceStrings.h b/include/xrpl/peerfinder/detail/SourceStrings.h index 325a024764..e9783c775f 100644 --- a/include/xrpl/peerfinder/detail/SourceStrings.h +++ b/include/xrpl/peerfinder/detail/SourceStrings.h @@ -6,7 +6,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Provides addresses from a static set of strings. @@ -22,4 +22,4 @@ public: make(std::string const& name, Strings const& strings); }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Store.h b/include/xrpl/peerfinder/detail/Store.h index 9393ef6c2b..1f9352c6ec 100644 --- a/include/xrpl/peerfinder/detail/Store.h +++ b/include/xrpl/peerfinder/detail/Store.h @@ -6,7 +6,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Abstract persistence for PeerFinder data. @@ -17,7 +17,7 @@ public: virtual ~Store() = default; // load the bootstrap cache - using load_callback = std::function; + using load_callback = std::function; virtual std::size_t load(load_callback const& cb) = 0; @@ -26,11 +26,11 @@ public: { explicit Entry() = default; - beast::IP::Endpoint endpoint; + beast::ip::Endpoint endpoint; int valence{}; }; virtual void save(std::vector const& v) = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Tuning.h b/include/xrpl/peerfinder/detail/Tuning.h index ea4637dd9d..b4ccfae167 100644 --- a/include/xrpl/peerfinder/detail/Tuning.h +++ b/include/xrpl/peerfinder/detail/Tuning.h @@ -9,7 +9,7 @@ * Heuristically tuned constants. */ /** @{ */ -namespace xrpl::PeerFinder::Tuning { +namespace xrpl::peer_finder::tuning { //--------------------------------------------------------- // @@ -111,5 +111,5 @@ constexpr std::chrono::seconds kLiveCacheSecondsToLive(30); // Note that we ignore the port for purposes of comparison. constexpr std::chrono::seconds kRecentAttemptDuration(60); -} // namespace xrpl::PeerFinder::Tuning +} // namespace xrpl::peer_finder::tuning /** @} */ diff --git a/include/xrpl/peerfinder/make_Manager.h b/include/xrpl/peerfinder/make_Manager.h index 5da372e588..e514734d8b 100644 --- a/include/xrpl/peerfinder/make_Manager.h +++ b/include/xrpl/peerfinder/make_Manager.h @@ -10,7 +10,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * @brief Create a new Manager. @@ -33,4 +33,4 @@ makeManager( Store& store, beast::insight::Collector::ptr const& collector); -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/proto/xrpl.proto b/include/xrpl/proto/xrpl.proto index d49920201e..bef5ec1d76 100644 --- a/include/xrpl/proto/xrpl.proto +++ b/include/xrpl/proto/xrpl.proto @@ -246,7 +246,15 @@ message TMGetObjectByHash { message TMLedgerNode { required bytes nodedata = 1; - optional bytes nodeid = 2; // missing for ledger base data + + // Used when protocol version <2.3. Not set for ledger base data. + optional bytes nodeid = 2; + + // Used when protocol version >=2.3. Neither value is set for ledger base data. + oneof reference { + bytes id = 3; // Set for inner nodes. + uint32 depth = 4; // Set for leaf nodes. + } } enum TMLedgerInfoType { diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index c3292e6074..b52e705b38 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -33,7 +33,7 @@ namespace xrpl { * Command line Requests use apiCommandLineVersion. */ -namespace RPC { +namespace rpc { template static constexpr std::integral_constant kApiVersion = {}; @@ -60,7 +60,7 @@ static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion); inline void setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) { - XRPL_ASSERT(apiVersion != kApiInvalidVersion, "xrpl::RPC::setVersion : input is valid"); + XRPL_ASSERT(apiVersion != kApiInvalidVersion, "xrpl::rpc::setVersion : input is valid"); auto& retObj = parent[jss::version] = json::ValueType::Object; @@ -99,12 +99,12 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) inline unsigned int getAPIVersionNumber(json::Value const& jv, bool betaEnabled) { - static json::Value const kMinVersion(RPC::kApiMinimumSupportedVersion); + static json::Value const kMinVersion(rpc::kApiMinimumSupportedVersion); json::Value const maxVersion( - betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion); + betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion); if (!jv.isObject() || !jv.isMember(jss::api_version)) - return RPC::kApiVersionIfUnspecified; + return rpc::kApiVersionIfUnspecified; try { @@ -113,33 +113,33 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled) { case json::ValueType::Int: if (rawVersion.asInt() < 0) - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; [[fallthrough]]; case json::ValueType::UInt: { auto const apiVersion = rawVersion.asUInt(); if (apiVersion < kMinVersion || apiVersion > maxVersion) - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; return apiVersion; } default: - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; } } catch (...) { - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; } } -} // namespace RPC +} // namespace rpc template void forApiVersions(Fn const& fn, Args&&... args) requires // (MaxVer >= MinVer) && // - (MinVer >= RPC::kApiMinimumSupportedVersion) && // - (RPC::kApiMaximumValidVersion >= MaxVer) && requires { + (MinVer >= rpc::kApiMinimumSupportedVersion) && // + (rpc::kApiMaximumValidVersion >= MaxVer) && requires { fn(std::integral_constant{}, std::forward(args)...); fn(std::integral_constant{}, std::forward(args)...); } @@ -158,11 +158,11 @@ template void forAllApiVersions(Fn const& fn, Args&&... args) requires requires { - forApiVersions( + forApiVersions( fn, std::forward(args)...); } { - forApiVersions( + forApiVersions( fn, std::forward(args)...); } diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h index 18ba20f23c..c3f90d8f9f 100644 --- a/include/xrpl/protocol/BuildInfo.h +++ b/include/xrpl/protocol/BuildInfo.h @@ -8,7 +8,7 @@ * Versioning information for this build. */ // VFALCO The namespace is deprecated -namespace xrpl::BuildInfo { +namespace xrpl::build_info { /** * Server version. @@ -84,4 +84,4 @@ isXrpldVersion(std::uint64_t version); bool isNewerVersion(std::uint64_t version); -} // namespace xrpl::BuildInfo +} // namespace xrpl::build_info diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 8ac7c8c58f..465b6d711f 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -167,7 +167,7 @@ enum WarningCodeI { // VFALCO NOTE these should probably not be in the RPC namespace. -namespace RPC { +namespace rpc { /** * Maps an rpc error code to its token, default message, and HTTP status. @@ -337,7 +337,7 @@ containsError(json::Value const& json); int errorCodeHttpStatus(ErrorCodeI code); -} // namespace RPC +} // namespace rpc /** * Returns a single string with the contents of an RPC error. diff --git a/include/xrpl/protocol/MultiApiJson.h b/include/xrpl/protocol/MultiApiJson.h index 9a4882ec55..a0029fa491 100644 --- a/include/xrpl/protocol/MultiApiJson.h +++ b/include/xrpl/protocol/MultiApiJson.h @@ -188,6 +188,6 @@ struct MultiApiJson // Wrapper for Json for all supported API versions. using MultiApiJson = - detail::MultiApiJson; + detail::MultiApiJson; } // namespace xrpl diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index bef05b9a8f..df4fedb707 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -6,7 +6,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Adds common synthetic fields to transaction-related JSON responses @@ -16,4 +16,4 @@ void insertNFTSyntheticInJson(json::Value&, std::shared_ptr const&, TxMeta const&); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index e83e1c97b6..567f66d339 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -139,7 +139,7 @@ tenthBipsOfValue(T value, TenthBips bips) return value * bips.value() / kTenthBipsPerUnity.value(); } -namespace Lending { +namespace lending { /** * The maximum management fee rate allowed by a loan broker in 1/10 bips. * @@ -236,7 +236,7 @@ static constexpr int kLoanPaymentsPerFeeIncrement = 5; * without an amendment */ static constexpr int kLoanMaximumPaymentsPerTransaction = 100; -} // namespace Lending +} // namespace lending /** * The maximum length of a URI inside an NFT @@ -316,6 +316,17 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6; */ constexpr std::uint8_t kVaultMaximumIouScale = 18; +/** + * Vault ledger-entry schema versions. Assigned to newly created + * Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before + * activation are left without LEVersion (implicit legacy version 0, + * accrual-basis accounting). + */ +enum class VaultVersion : uint8_t { + Legacy = 0, + CashBasis, +}; + /** * Maximum recursion depth for vault shares being put as an asset inside * another vault; counted from 0 diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 98301af487..833078d741 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -260,7 +260,7 @@ calcAccountID(PublicKey const& pk); inline std::string getFingerprint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, std::optional const& publicKey = std::nullopt, std::optional const& id = std::nullopt) { diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 8f1c7a4ce3..ed8ffeb88e 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -20,7 +20,7 @@ namespace xrpl { -namespace Attestations { +namespace attestations { struct AttestationBase { @@ -227,7 +227,7 @@ struct CmpByCreateCount } }; -}; // namespace Attestations +}; // namespace attestations // Result when checking when two attestation match. enum class AttestationMatch { @@ -241,7 +241,7 @@ enum class AttestationMatch { struct XChainClaimAttestation { - using TSignedAttestation = Attestations::AttestationClaim; + using TSignedAttestation = attestations::AttestationClaim; static SField const& arrayFieldName; AccountID keyAccount; @@ -297,7 +297,7 @@ struct XChainClaimAttestation struct XChainCreateAccountAttestation { - using TSignedAttestation = Attestations::AttestationCreateAccount; + using TSignedAttestation = attestations::AttestationCreateAccount; static SField const& arrayFieldName; AccountID keyAccount; diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 90810e06d2..b6408581a9 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -505,6 +505,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfShareMPTID, SoeRequired}, {sfWithdrawalPolicy, SoeRequired}, {sfScale, SoeDefault}, + {sfLEVersion, SoeDefault}, // 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 4ef76c8b75..16defe3ba3 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -18,6 +18,7 @@ TYPED_SFIELD(sfMethod, UINT8, 2) TYPED_SFIELD(sfTransactionResult, UINT8, 3) TYPED_SFIELD(sfScale, UINT8, 4) TYPED_SFIELD(sfAssetScale, UINT8, 5) +TYPED_SFIELD(sfLEVersion, UINT8, 6) // 8-bit integers (uncommon) TYPED_SFIELD(sfTickSize, UINT8, 16) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h b/include/xrpl/protocol_autogen/ledger_entries/Vault.h index 2bf92b4f5d..a6ab54cb0a 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Vault.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h @@ -287,6 +287,30 @@ public: { return this->sle_->isFieldPresent(sfScale); } + + /** + * @brief Get sfLEVersion (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLEVersion() const + { + if (hasLEVersion()) + return this->sle_->at(sfLEVersion); + return std::nullopt; + } + + /** + * @brief Check if sfLEVersion is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLEVersion() const + { + return this->sle_->isFieldPresent(sfLEVersion); + } }; /** @@ -508,6 +532,17 @@ public: return *this; } + /** + * @brief Set sfLEVersion (SoeDefault) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setLEVersion(std::decay_t const& value) + { + object_[sfLEVersion] = value; + return *this; + } + /** * @brief Build and return the completed Vault wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h index 12ea548fd2..b5bb8dd52e 100644 --- a/include/xrpl/resource/Charge.h +++ b/include/xrpl/resource/Charge.h @@ -4,7 +4,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * A consumption charge. @@ -32,7 +32,7 @@ public: label() const; /** - * Return the cost of the charge in Resource::Manager units. + * Return the cost of the charge in resource::Manager units. */ [[nodiscard]] value_type cost() const; @@ -60,4 +60,4 @@ private: std::ostream& operator<<(std::ostream& os, Charge const& v); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h index 9abcbffc82..01539e3a39 100644 --- a/include/xrpl/resource/Consumer.h +++ b/include/xrpl/resource/Consumer.h @@ -8,7 +8,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { struct Entry; class Logic; @@ -96,4 +96,4 @@ private: std::ostream& operator<<(std::ostream& os, Consumer const& v); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Disposition.h b/include/xrpl/resource/Disposition.h index cd5bceafa5..28dd4edf62 100644 --- a/include/xrpl/resource/Disposition.h +++ b/include/xrpl/resource/Disposition.h @@ -1,6 +1,6 @@ #pragma once -namespace xrpl::Resource { +namespace xrpl::resource { /** * The disposition of a consumer after applying a load charge. @@ -24,4 +24,4 @@ enum class Disposition { Drop }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Fees.h b/include/xrpl/resource/Fees.h index 5001b504d6..411169253d 100644 --- a/include/xrpl/resource/Fees.h +++ b/include/xrpl/resource/Fees.h @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Schedule of fees charged for imposing load on the server. @@ -31,4 +31,4 @@ extern Charge const kFeeWarning; // The cost of receiving a warning. extern Charge const kFeeDrop; // The cost of being dropped for excess load. /** @} */ -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Gossip.h b/include/xrpl/resource/Gossip.h index 4ad5852de0..0d8ccb100c 100644 --- a/include/xrpl/resource/Gossip.h +++ b/include/xrpl/resource/Gossip.h @@ -4,7 +4,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Data format for exchanging consumption information across peers. @@ -21,10 +21,10 @@ struct Gossip explicit Item() = default; int balance{}; - beast::IP::Endpoint address; + beast::ip::Endpoint address; }; std::vector items; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/README.md b/include/xrpl/resource/README.md index 545d4e9ca0..96b6c3d603 100644 --- a/include/xrpl/resource/README.md +++ b/include/xrpl/resource/README.md @@ -1,4 +1,4 @@ -# Resource::Manager +# resource::Manager The ResourceManager module has these responsibilities: @@ -36,7 +36,7 @@ to the general public. ## Consumer Types Consumers are placed into three classifications (as identified by the -Resource::Kind enumeration): +resource::Kind enumeration): - InBound, - OutBound, and @@ -72,6 +72,6 @@ drop connections to those IP addresses that occur commonly in the gossip. ## Access -In xrpld, the Application holds a unique instance of Resource::Manager, +In xrpld, the Application holds a unique instance of resource::Manager, which may be retrieved by calling the method `Application::getResourceManager()`. diff --git a/include/xrpl/resource/ResourceManager.h b/include/xrpl/resource/ResourceManager.h index 03aab60c75..267cfb16e3 100644 --- a/include/xrpl/resource/ResourceManager.h +++ b/include/xrpl/resource/ResourceManager.h @@ -14,7 +14,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Tracks load and resource consumption. @@ -32,10 +32,10 @@ public: * IP if proxied. */ virtual Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) = 0; + newInboundEndpoint(beast::ip::Endpoint const& address) = 0; virtual Consumer newInboundEndpoint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, bool const proxy, std::string_view forwardedFor) = 0; @@ -43,13 +43,13 @@ public: * Create a new endpoint keyed by outbound IP address and port. */ virtual Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) = 0; + newOutboundEndpoint(beast::ip::Endpoint const& address) = 0; /** * Create a new unlimited endpoint keyed by forwarded IP. */ virtual Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) = 0; + newUnlimitedEndpoint(beast::ip::Endpoint const& address) = 0; /** * Extract packaged consumer information for export. @@ -78,4 +78,4 @@ public: std::unique_ptr makeManager(beast::insight::Collector::ptr const& collector, beast::Journal journal); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h index 1336bda6ab..ec5a328b8b 100644 --- a/include/xrpl/resource/detail/Entry.h +++ b/include/xrpl/resource/detail/Entry.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { using clock_type = beast::AbstractClock; @@ -91,4 +91,4 @@ operator<<(std::ostream& os, Entry const& v) return os; } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Import.h b/include/xrpl/resource/detail/Import.h index b19dbc4d1a..c5366146c1 100644 --- a/include/xrpl/resource/detail/Import.h +++ b/include/xrpl/resource/detail/Import.h @@ -5,7 +5,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * A set of imported consumer data from a gossip origin. @@ -32,4 +32,4 @@ struct Import std::vector items; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h index a0f11422a7..180e868319 100644 --- a/include/xrpl/resource/detail/Key.h +++ b/include/xrpl/resource/detail/Key.h @@ -7,17 +7,17 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { // The consumer key struct Key { Kind kind; - beast::IP::Endpoint address; + beast::ip::Endpoint address; Key() = delete; - Key(Kind k, beast::IP::Endpoint addr) : kind(k), address(std::move(addr)) + Key(Kind k, beast::ip::Endpoint addr) : kind(k), address(std::move(addr)) { } @@ -47,4 +47,4 @@ struct Key }; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Kind.h b/include/xrpl/resource/detail/Kind.h index ce2e0773cf..9af760d252 100644 --- a/include/xrpl/resource/detail/Kind.h +++ b/include/xrpl/resource/detail/Kind.h @@ -1,6 +1,6 @@ #pragma once -namespace xrpl::Resource { +namespace xrpl::resource { /** * Kind of consumer. @@ -12,4 +12,4 @@ namespace xrpl::Resource { */ enum class Kind { Inbound, Outbound, Unlimited }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index 3f36ad84a3..aaaeb4fdd1 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -24,7 +24,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { class Logic { @@ -96,7 +96,7 @@ public: } Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) + newInboundEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -126,7 +126,7 @@ public: } Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) + newOutboundEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -159,7 +159,7 @@ public: * enabled. */ Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) + newUnlimitedEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -387,7 +387,7 @@ public: { std::scoped_lock const _(lock_); Entry& entry(iter->second); - XRPL_ASSERT(entry.refcount == 0, "xrpl::Resource::Logic::erase : entry not used"); + XRPL_ASSERT(entry.refcount == 0, "xrpl::resource::Logic::erase : entry not used"); inactive_.erase(inactive_.iteratorTo(entry)); table_.erase(iter); } @@ -421,7 +421,7 @@ public: default: // LCOV_EXCL_START UNREACHABLE( - "xrpl::Resource::Logic::release : invalid entry " + "xrpl::resource::Logic::release : invalid entry " "kind"); break; // LCOV_EXCL_STOP @@ -440,7 +440,7 @@ public: static_assert( kFeeLogAsWarn > kFeeLogAsInfo && kFeeLogAsInfo > kFeeLogAsDebug && kFeeLogAsDebug > 10); - static auto kGetStream = [](Resource::Charge::value_type cost, beast::Journal& journal) { + static auto kGetStream = [](resource::Charge::value_type cost, beast::Journal& journal) { if (cost >= kFeeLogAsWarn) return journal.warn(); if (cost >= kFeeLogAsInfo) @@ -564,4 +564,4 @@ public: } }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Tuning.h b/include/xrpl/resource/detail/Tuning.h index 62f7fa3f9d..d631aaddba 100644 --- a/include/xrpl/resource/detail/Tuning.h +++ b/include/xrpl/resource/detail/Tuning.h @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Tunable constants. @@ -26,4 +26,4 @@ static constexpr std::chrono::seconds kSecondsUntilExpiration{300}; // Number of seconds until imported gossip expires static constexpr std::chrono::seconds kGossipExpirationSeconds{30}; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index 2e9bd857c7..4bf88cd53b 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -62,7 +62,7 @@ public: using ref = std::shared_ptr const&; - using Consumer = Resource::Consumer; + using Consumer = resource::Consumer; public: /** diff --git a/include/xrpl/server/Session.h b/include/xrpl/server/Session.h index be8d9a497c..03ac767c25 100644 --- a/include/xrpl/server/Session.h +++ b/include/xrpl/server/Session.h @@ -52,7 +52,7 @@ public: /** * Returns the remote address of the connection. */ - virtual beast::IP::Endpoint + virtual beast::ip::Endpoint remoteAddress() = 0; /** diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index c7553c1da3..6020d3cc65 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -157,7 +157,7 @@ protected: return port_; } - beast::IP::Endpoint + beast::ip::Endpoint remoteAddress() override { return beast::IPAddressConversion::fromAsio(remoteAddress_); diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index 59a866ab8c..b1670865bd 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -196,7 +196,7 @@ BaseWSPeer::run() startTimer(); closeOnTimer_ = true; impl().ws_.set_option(boost::beast::websocket::stream_base::decorator([](auto& res) { - res.set(boost::beast::http::field::server, BuildInfo::getFullVersionString()); + res.set(boost::beast::http::field::server, build_info::getFullVersionString()); })); impl().ws_.async_accept( request_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) { diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index a1194ccfd3..e198c472fa 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -95,6 +94,21 @@ enum class SHAMapState { * * See https://en.wikipedia.org/wiki/Merkle_tree */ + +/** + * Holds a SHAMap node's identity, leaf status, and serialized data. Used by + * getNodeFat to return node data for peer synchronization. + */ +struct SHAMapNodeData +{ + SHAMapNodeID nodeID; + // The `data` field (a Blob, 8-byte aligned) needs 4 bytes of padding after the `nodeID` field + // (36 bytes, 4-byte aligned) regardless of what comes between them, so `isLeaf` costs nothing + // extra here. Moving it after `data` would add 8 bytes to the size of this struct instead. + bool isLeaf; + Blob data; +}; + class SHAMap { private: @@ -289,10 +303,10 @@ public: std::vector> getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter); - bool + [[nodiscard]] bool getNodeFat( SHAMapNodeID const& wanted, - std::vector>& data, + std::vector& data, bool fatLeaves, std::uint32_t depth) const; @@ -321,10 +335,45 @@ public: void serializeRoot(Serializer& s) const; + /** + * Add a root node to the SHAMap during synchronization. + * + * This function is used when receiving the root node of a SHAMap from a peer during ledger + * synchronization. The node must already have been deserialized. + * + * @param hash The expected hash of the root node. + * @param rootNode A deserialized root node to add. + * @param filter Optional sync filter to track received nodes. + * @return Status indicating whether the node was useful, duplicate, or invalid. + * + * @note This function expects the rootNode to be a valid, deserialized SHAMapTreeNode. The + * caller is responsible for deserialization and basic validation before calling this + * function. + */ SHAMapAddNode - addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter); + addRootNode(SHAMapHash const& hash, SHAMapTreeNodePtr rootNode, SHAMapSyncFilter const* filter); + + /** + * Add a known node at a specific position in the SHAMap during synchronization. + * + * This function is used when receiving nodes from peers during ledger synchronization. The node + * is inserted at the position specified by nodeID. The node must already have been + * deserialized. + * + * @param nodeID The position in the tree where this node belongs. + * @param treeNode A deserialized tree node to add. + * @param filter Optional sync filter to track received nodes. + * @return Status indicating whether the node was useful, duplicate, or invalid. + * + * @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The + * caller is responsible for deserialization and basic validation before calling this + * function. This also means that the nodeID must be consistent with the node's content. + */ SHAMapAddNode - addKnownNode(SHAMapNodeID const& nodeID, Slice const& rawNode, SHAMapSyncFilter const* filter); + addKnownNode( + SHAMapNodeID const& nodeID, + SHAMapTreeNodePtr treeNode, + SHAMapSyncFilter const* filter); // status functions void diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h index 26cfde9fe8..ab5bd574ed 100644 --- a/include/xrpl/shamap/SHAMapLeafNode.h +++ b/include/xrpl/shamap/SHAMapLeafNode.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -60,4 +63,16 @@ public: getString(SHAMapNodeID const&) const final; }; +/** + * Return the key of the item held by a SHAMap leaf node. + * + * @param node a node known to be a leaf (see SHAMapTreeNode::isLeaf). + */ +inline uint256 const& +leafKey(SHAMapTreeNode const& node) +{ + XRPL_ASSERT(node.isLeaf(), "xrpl::leafKey : node is a leaf"); + return safeDowncast(node).peekItem()->key(); +} + } // namespace xrpl diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp index 7e3e02f855..56f2c77204 100644 --- a/sanitizers/suppressions/ubsan.supp +++ b/sanitizers/suppressions/ubsan.supp @@ -102,6 +102,10 @@ undefined:nudb # Snappy compression library intentional overflows unsigned-integer-overflow:snappy.cc +# fast_float parses floats with a SWAR trick (parse_eight_digits_unrolled) that +# multiplies eight packed digits modulo 2^64; the wraparound is by design. +unsigned-integer-overflow:fast_float + # Abseil intentional overflows in hashing, RNG and time arithmetic. # Matched at library scope (like boost above): the wraparound is by design # across many absl files (hash mixing, raw_hash_set probing, duration math, @@ -192,8 +196,9 @@ unsigned-integer-overflow:rpc/handlers/orderbook/GetAggregatePrice.cpp # Test-only intentional overflow/underflow in fixture and unit-test arithmetic. unsigned-integer-overflow:tests/libxrpl/basics/RangeSet.cpp unsigned-integer-overflow:test/app/Batch_test.cpp +unsigned-integer-overflow:test/app/ConfidentialTransfer_test.cpp unsigned-integer-overflow:test/app/Invariants_test.cpp -unsigned-integer-overflow:test/app/Loan_test.cpp +unsigned-integer-overflow:test/app/lending/LoanSecurity_test.cpp unsigned-integer-overflow:test/app/NFToken_test.cpp unsigned-integer-overflow:test/app/OfferMPT_test.cpp unsigned-integer-overflow:test/app/Offer_test.cpp diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp index f854dbd3a7..cd3e15bd65 100644 --- a/src/benchmarks/libxrpl/nodestore/Backend.cpp +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -20,21 +20,22 @@ namespace xrpl::node_store { namespace { -constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000}; -constexpr int kThreadCounts[] = {1, 4, 8}; +constexpr auto kPoolSizes = std::to_array({1000, 10000, 100000}); +constexpr auto kThreadCounts = std::to_array({1, 4, 8}); constexpr std::size_t kBatchSize = 256; +constexpr std::size_t kMissRatio = 5; constexpr std::string_view kNamePrefix = "BM_Backend_"; constexpr std::string_view kNameSeparator = "/"; struct RunState { - std::unique_ptr harness; - Batch present; // prefix-1 objects, eligible to be stored - Batch recent; // prefix-1 objects in the "future" key space - std::vector missing; // prefix-2 keys that are never stored - std::vector shuffle; // [0, poolSize) permutation for random-like access - std::size_t avgPayload = 0; // mean getData().size() over `present` + std::unique_ptr harness; ///< backend under test, rebuilt per run + Batch present; ///< prefix-1 objects, eligible to be stored + Batch recent; ///< prefix-1 objects in the "future" key space + std::vector missing; ///< prefix-2 keys that are never stored + std::vector shuffle; ///< [0, poolSize) permutation for random-like access + std::size_t avgPayload = 0; ///< mean getData().size() over `present` void release() @@ -85,7 +86,7 @@ Workload const kInsert{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; backend.store(rs.present[index % poolSize]); }, .reportBytes = true, @@ -104,7 +105,7 @@ Workload const kFetch{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; backend.fetch(rs.present[index % poolSize]->getHash(), &result); benchmark::DoNotOptimize(result); @@ -118,7 +119,7 @@ Workload const kMissing{ .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; backend.fetch(rs.missing[index % poolSize], &result); benchmark::DoNotOptimize(result); @@ -139,10 +140,10 @@ Workload const kMixed{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; auto const pick = rs.shuffle[index % poolSize]; - if (index % 5 == 0) + if (index % kMissRatio == 0) { backend.fetch(rs.missing[pick], &result); } @@ -170,7 +171,7 @@ Workload const kWork{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; auto const slot = index % poolSize; auto const pick = rs.shuffle[slot]; @@ -239,7 +240,7 @@ registerWorkload(BackendConfig const& bc, Workload const& w) { auto rs = std::make_shared(); auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)); - b->RangeMultiplier(10)->Range(kPoolSizes[0], kPoolSizes[std::size(kPoolSizes) - 1]); + b->RangeMultiplier(10)->Range(kPoolSizes.front(), kPoolSizes.back()); b->Threads(1)->Threads(4)->Threads(8)->UseRealTime(); return; @@ -249,14 +250,14 @@ registerWorkload(BackendConfig const& bc, Workload const& w) { for (auto const threads : kThreadCounts) { - if (poolSize % static_cast(threads) != 0) + if (poolSize % threads != 0) continue; auto rs = std::make_shared(); benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)) ->Arg(poolSize) - ->Iterations(poolSize / static_cast(threads)) - ->Threads(threads) + ->Iterations(poolSize / threads) + ->Threads(static_cast(threads)) ->UseRealTime(); } } @@ -289,7 +290,7 @@ registerStoreBatch(BackendConfig const& bc) rs->harness = std::make_unique(cfg); rs->present = makePool(1, poolSize); rs->avgPayload = averagePayload(rs->present); - std::vector const batches = sliceBatches(rs->present, kBatchSize); + std::vector const batches = sliceFixedBatches(rs->present, kBatchSize); if (batches.empty()) { state.SkipWithError("pool smaller than one batch"); diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index 57abf42e89..debdc5d47a 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -40,18 +41,13 @@ inline void rngcpy(void* buffer, std::size_t bytes, Generator& g) { using result_type = typename Generator::result_type; - while (bytes >= sizeof(result_type)) + while (bytes > 0) { auto const v = g(); - std::memcpy(buffer, &v, sizeof(v)); - buffer = reinterpret_cast(buffer) + sizeof(v); - bytes -= sizeof(v); - } - - if (bytes > 0) - { - auto const v = g(); - std::memcpy(buffer, &v, bytes); + auto const chunk = std::min(bytes, sizeof(result_type)); + std::memcpy(buffer, &v, chunk); + buffer = reinterpret_cast(buffer) + chunk; + bytes -= chunk; } } @@ -145,7 +141,7 @@ makePool(std::uint8_t prefix, std::size_t count, std::size_t start = 0) Sequence seq(prefix); Batch pool; pool.reserve(count); - for (std::size_t i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) pool.push_back(seq.obj(start + i)); return pool; } @@ -158,7 +154,7 @@ makeMissingKeys(std::size_t count) Sequence seq(2); std::vector keys; keys.reserve(count); - for (std::size_t i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) keys.push_back(seq.key(i)); return keys; } @@ -206,16 +202,16 @@ inline std::vector makeShuffle(std::size_t size, std::uint64_t seed) { std::vector v(size); - std::iota(v.begin(), v.end(), std::size_t{0}); + std::ranges::iota(v, 0uz); beast::xor_shift_engine gen(seed); - std::shuffle(v.begin(), v.end(), gen); + std::ranges::shuffle(v, gen); return v; } // Partition a pool into fixed-size batches. Any trailing remainder shorter than // `batchSize` is dropped, so every returned batch has exactly `batchSize`. inline std::vector -sliceBatches(Batch const& pool, std::size_t batchSize) +sliceFixedBatches(Batch const& pool, std::size_t batchSize) { std::vector batches; if (batchSize == 0) @@ -228,13 +224,10 @@ sliceBatches(Batch const& pool, std::size_t batchSize) /** * @brief RAII owner of a NodeStore Backend opened on a private temporary directory. - * - * Member declaration order matters: `tempDir` is declared first so it is - * destroyed last, after the backend has closed and released its files. */ struct BackendHarness { - beast::TempDir tempDir; + beast::TempDir tempDir; ///< Declared first so it is destroyed last DummyScheduler scheduler; beast::Journal journal{beast::Journal::getNullSink()}; std::unique_ptr backend; diff --git a/src/libxrpl/basics/ResolverAsio.cpp b/src/libxrpl/basics/ResolverAsio.cpp index 25e95b7fc5..53739fed8a 100644 --- a/src/libxrpl/basics/ResolverAsio.cpp +++ b/src/libxrpl/basics/ResolverAsio.cpp @@ -255,7 +255,7 @@ public: if (ec == boost::asio::error::operation_aborted) return; - std::vector addresses; + std::vector addresses; auto iter = results.begin(); // If we get an error message back, we don't return any @@ -283,7 +283,7 @@ public: // first attempt to parse as an endpoint (IP addr + port). // If that doesn't succeed, fall back to generic name + port parsing - if (auto const result = beast::IP::Endpoint::fromStringChecked(str)) + if (auto const result = beast::ip::Endpoint::fromStringChecked(str)) { return make_pair(result->address().to_string(), std::to_string(result->port())); } diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp index f4edaf5aca..2b7deecb8e 100644 --- a/src/libxrpl/basics/StringUtilities.cpp +++ b/src/libxrpl/basics/StringUtilities.cpp @@ -74,7 +74,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) // We need to use Endpoint to parse the domain to // strip surrounding brackets from IPv6 addresses, // e.g. [::1] => ::1. - auto const result = beast::IP::Endpoint::fromStringChecked(domain); + auto const result = beast::ip::Endpoint::fromStringChecked(domain); pUrl.domain = result ? result->address().to_string() : domain; std::string const port = smMatch[5]; if (!port.empty()) diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 3cff5d93b5..72fe6189a5 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -207,7 +207,7 @@ private: static constexpr auto kMaxPacketSize = 1472; Journal journal_; - IP::Endpoint address_; + ip::Endpoint address_; std::string prefix_; boost::asio::io_context ioContext_; std::optional> work_; @@ -222,13 +222,13 @@ private: std::thread thread_; static boost::asio::ip::udp::endpoint - toEndpoint(IP::Endpoint const& ep) + toEndpoint(ip::Endpoint const& ep) { return boost::asio::ip::udp::endpoint(ep.address(), ep.port()); } public: - StatsDCollectorImp(IP::Endpoint address, std::string prefix, Journal journal) + StatsDCollectorImp(ip::Endpoint address, std::string prefix, Journal journal) : journal_(journal) , address_(std::move(address)) , prefix_(std::move(prefix)) @@ -707,7 +707,7 @@ StatsDMeterImpl::doProcess() //------------------------------------------------------------------------------ std::shared_ptr -StatsDCollector::make(IP::Endpoint const& address, std::string const& prefix, Journal journal) +StatsDCollector::make(ip::Endpoint const& address, std::string const& prefix, Journal journal) { return std::make_shared(address, prefix, journal); } diff --git a/src/libxrpl/beast/net/IPAddressConversion.cpp b/src/libxrpl/beast/net/IPAddressConversion.cpp index c0a37d234e..bf24ef75c1 100644 --- a/src/libxrpl/beast/net/IPAddressConversion.cpp +++ b/src/libxrpl/beast/net/IPAddressConversion.cpp @@ -5,7 +5,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { Endpoint fromAsio(boost::asio::ip::address const& address) @@ -31,4 +31,4 @@ toAsioEndpoint(Endpoint const& endpoint) return boost::asio::ip::tcp::endpoint{endpoint.address(), endpoint.port()}; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPAddressV4.cpp b/src/libxrpl/beast/net/IPAddressV4.cpp index f9b0c96022..2a59fe1cc4 100644 --- a/src/libxrpl/beast/net/IPAddressV4.cpp +++ b/src/libxrpl/beast/net/IPAddressV4.cpp @@ -1,6 +1,6 @@ #include -namespace beast::IP { +namespace beast::ip { bool isPrivate(AddressV4 const& addr) @@ -62,4 +62,4 @@ getClass(AddressV4 const& addr) return kTable[(addr.to_uint() & 0xE0000000) >> 29]; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPAddressV6.cpp b/src/libxrpl/beast/net/IPAddressV6.cpp index c75ccaf1cc..e5ef55065f 100644 --- a/src/libxrpl/beast/net/IPAddressV6.cpp +++ b/src/libxrpl/beast/net/IPAddressV6.cpp @@ -4,7 +4,7 @@ #include -namespace beast::IP { +namespace beast::ip { bool isPrivate(AddressV6 const& addr) @@ -58,4 +58,4 @@ isPublic(AddressV6 const& addr) return true; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPEndpoint.cpp b/src/libxrpl/beast/net/IPEndpoint.cpp index 5877151187..02ed5e37c5 100644 --- a/src/libxrpl/beast/net/IPEndpoint.cpp +++ b/src/libxrpl/beast/net/IPEndpoint.cpp @@ -14,7 +14,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { Endpoint::Endpoint() : port_(0) { @@ -176,4 +176,4 @@ operator>>(std::istream& is, Endpoint& endpoint) return is; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp index f9134e6629..8598f94491 100644 --- a/src/libxrpl/json/json_reader.cpp +++ b/src/libxrpl/json/json_reader.cpp @@ -3,9 +3,11 @@ #include #include +#include // IWYU pragma: keep +#include + #include #include -#include #include #include #include @@ -605,8 +607,14 @@ Reader::decodeNumber(Token& token) bool Reader::decodeDouble(Token& token) { + // Sanity check to avoid buffer overflow exploits. + if (token.end < token.start) + { + return addError("Unable to parse token length", token); + } + double value = 0; - auto const [ptr, ec] = std::from_chars(token.start, token.end, value); + auto const [ptr, ec] = fast_float::from_chars(token.start, token.end, value); // Reject anything from_chars could not turn into a finite double: // - ec != std::errc{}: no valid conversion, or an out-of-range magnitude diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index e6c3d632c1..89b03a03a7 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -130,6 +131,127 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale) roundToAsset(asset, value, scale, Number::RoundingMode::Upward); } +namespace accrual { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested, Number const& interestDue) +{ + return {.assetsTotalDelta = interestDue, .debtTotalDelta = principalRequested + interestDue}; +} + +bool +loanOriginationExceedsVaultMaximum( + Number const& vaultMaximum, + Number const& vaultTotal, + Number const& interestDue) +{ + return vaultMaximum != 0 && interestDue > vaultMaximum - vaultTotal; +} + +/* +XLS-66 section 3.2.3.2, defines the default amount as + +DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) + +Which is equivalent to (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) +*/ +Number +loanVaultExposure(SLE::const_ref loanSle) +{ + return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding); +} + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts) +{ + return { + .assetsTotalDelta = parts.valueChange, + .debtTotalDelta = (parts.principalPaid + parts.interestPaid) - parts.valueChange}; +} + +} // namespace accrual + +namespace cash_basis { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested) +{ + return {.assetsTotalDelta = kNumZero, .debtTotalDelta = principalRequested}; +} + +/* + * Under CashBasis accounting, Loan default amount is: + * + * DefaultAmount = Loan.PrincipalOutstanding + */ +Number +loanVaultExposure(SLE::const_ref loanSle) +{ + return loanSle->at(sfPrincipalOutstanding); +} + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts) +{ + return {.assetsTotalDelta = parts.interestPaid, .debtTotalDelta = parts.principalPaid}; +} + +} // namespace cash_basis + +namespace { + +// Cash-basis accounting applies only when featureLendingProtocolV1_1 is +// enabled AND the specific Vault was created under it (LEVersion == +// VaultVersion::CashBasis). Vaults created before activation keep accrual-basis +// accounting forever, even after the amendment later turns on. +bool +cashBasisEnabled(SLE::const_ref vaultSle) +{ + return getVaultVersion(vaultSle) == VaultVersion::CashBasis; +} + +} // namespace + +AccountingDeltas +loanOriginationDeltas( + SLE::const_ref vaultSle, + Number const& principalRequested, + Number const& interestDue) +{ + return cashBasisEnabled(vaultSle) + ? cash_basis::loanOriginationDeltas(principalRequested) + : accrual::loanOriginationDeltas(principalRequested, interestDue); +} + +bool +loanOriginationExceedsVaultMaximum( + SLE::const_ref vaultSle, + Number const& vaultTotal, + Number const& interestDue) +{ + // Cash-basis origination doesn't recognize interest into AssetsTotal, so + // interest due can never push the vault past AssetsMaximum at origination. + if (cashBasisEnabled(vaultSle)) + return false; + + auto const vaultMaximum = vaultSle->at(sfAssetsMaximum); + return accrual::loanOriginationExceedsVaultMaximum(vaultMaximum, vaultTotal, interestDue); +} + +Number +loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle) +{ + return cashBasisEnabled(vaultSle) ? cash_basis::loanVaultExposure(loanSle) + : accrual::loanVaultExposure(loanSle); +} + +AccountingDeltas +loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts) +{ + return cashBasisEnabled(vaultSle) ? cash_basis::loanPaymentDeltas(parts) + : accrual::loanPaymentDeltas(parts); +} + namespace detail { void @@ -1495,7 +1617,7 @@ makeRegularPayment( LoanPaymentType const paymentType, beast::Journal j) { - using namespace Lending; + using namespace lending; XRPL_ASSERT_PARTS( paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment, diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index b5b076d1cb..78f64d2077 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -6,6 +6,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include @@ -13,6 +14,7 @@ #include #include +#include namespace xrpl { @@ -137,4 +139,22 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref return sleToken->getFieldU64(sfMPTAmount) == outstanding; } +[[nodiscard]] VaultVersion +getVaultVersion(SLE::const_ref vault) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle"); + if (!vault->isFieldPresent(sfLEVersion)) + return VaultVersion::Legacy; + + auto const version = vault->at(sfLEVersion); + if (version > std::to_underlying(VaultVersion::CashBasis)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::getVaultVersion : invalid vault version"); + return VaultVersion::Legacy; + // LCOV_EXCL_STOP + } + return static_cast(version); +} + } // namespace xrpl diff --git a/src/libxrpl/peerfinder/Bootcache.cpp b/src/libxrpl/peerfinder/Bootcache.cpp index a2a56b4d01..4f9b5fc816 100644 --- a/src/libxrpl/peerfinder/Bootcache.cpp +++ b/src/libxrpl/peerfinder/Bootcache.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Bootcache::Bootcache(Store& store, clock_type& clock, beast::Journal journal) : store_(store), clock_(clock), journal_(journal), whenUpdate_(clock_.now()) @@ -78,7 +78,7 @@ void Bootcache::load() { clear(); - auto const n(store_.load([this](beast::IP::Endpoint const& endpoint, int valence) { + auto const n(store_.load([this](beast::ip::Endpoint const& endpoint, int valence) { auto const result(this->map_.insert(value_type(endpoint, valence))); if (!result.second) { @@ -96,7 +96,7 @@ Bootcache::load() } bool -Bootcache::insert(beast::IP::Endpoint const& endpoint) +Bootcache::insert(beast::ip::Endpoint const& endpoint) { auto const result(map_.insert(value_type(endpoint, 0))); if (result.second) @@ -109,7 +109,7 @@ Bootcache::insert(beast::IP::Endpoint const& endpoint) } bool -Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) +Bootcache::insertStatic(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, kStaticValence))); @@ -130,7 +130,7 @@ Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) } void -Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) +Bootcache::onSuccess(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, 1))); if (result.second) @@ -144,7 +144,7 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) ++entry.valence(); map_.erase(result.first); result = map_.insert(value_type(endpoint, entry)); - XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onSuccess : endpoint inserted"); + XRPL_ASSERT(result.second, "xrpl::peer_finder::Bootcache::onSuccess : endpoint inserted"); } Entry const& entry(result.first->right); JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache connect " << endpoint @@ -154,7 +154,7 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) } void -Bootcache::onFailure(beast::IP::Endpoint const& endpoint) +Bootcache::onFailure(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, -1))); if (result.second) @@ -168,7 +168,7 @@ Bootcache::onFailure(beast::IP::Endpoint const& endpoint) --entry.valence(); map_.erase(result.first); result = map_.insert(value_type(endpoint, entry)); - XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onFailure : endpoint inserted"); + XRPL_ASSERT(result.second, "xrpl::peer_finder::Bootcache::onFailure : endpoint inserted"); } Entry const& entry(result.first->right); auto const n(std::abs(entry.valence())); @@ -201,11 +201,11 @@ Bootcache::onWrite(beast::PropertyStream::Map& map) void Bootcache::prune() { - if (size() <= Tuning::kBootcacheSize) + if (size() <= tuning::kBootcacheSize) return; // Calculate the amount to remove - auto count((size() * Tuning::kBootcachePrunePercent) / 100); + auto count((size() * tuning::kBootcachePrunePercent) / 100); decltype(count) pruned(0); // Work backwards because bimap doesn't handle @@ -215,7 +215,7 @@ Bootcache::prune() { --count; --iter; - beast::IP::Endpoint const& endpoint(iter->get_left()); + beast::ip::Endpoint const& endpoint(iter->get_left()); Entry const& entry(iter->get_right()); JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache pruned" << endpoint << " at valence " << entry.valence(); @@ -244,7 +244,7 @@ Bootcache::update() store_.save(list); // Reset the flag and cooldown timer needsUpdate_ = false; - whenUpdate_ = clock_.now() + Tuning::kBootcacheCooldownTime; + whenUpdate_ = clock_.now() + tuning::kBootcacheCooldownTime; } // Checks the clock and calls update if we are off the cooldown. @@ -263,4 +263,4 @@ Bootcache::flagForUpdate() checkUpdate(); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/Config.cpp b/src/libxrpl/peerfinder/Config.cpp index 3e2f74f42b..60ac0ca547 100644 --- a/src/libxrpl/peerfinder/Config.cpp +++ b/src/libxrpl/peerfinder/Config.cpp @@ -7,13 +7,13 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { std::size_t Config::calcOutPeers() const { return std::max( - ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); + ((maxPeers * tuning::kOutPercent) + 50) / 100, std::size_t(tuning::kMinOutCount)); } void @@ -26,8 +26,8 @@ Config::applyTuning() // IP addresses. ipLimit = 2; - if (inPeers > Tuning::kDefaultMaxPeers) - ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); + if (inPeers > tuning::kDefaultMaxPeers) + ipLimit += std::min(5, static_cast(inPeers / tuning::kDefaultMaxPeers)); } // We don't allow a single IP to consume all incoming slots, @@ -58,7 +58,7 @@ Config::makeConfig( int ipLimit, bool verifyEndpoints) { - PeerFinder::Config config; + peer_finder::Config config; if (!limits.maxPeers) { @@ -85,7 +85,7 @@ Config::makeConfig( if (limits.maxPeers && *limits.maxPeers != 0) config.maxPeers = *limits.maxPeers; - config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); + config.maxPeers = std::max(config.maxPeers, tuning::kMinOutCount); config.outPeers = config.calcOutPeers(); // Calculate the number of outbound peers we want. If we dont want @@ -132,4 +132,4 @@ Config::makeConfig( return config; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/Endpoint.cpp b/src/libxrpl/peerfinder/Endpoint.cpp index 12f2725ea5..6f3e2289f9 100644 --- a/src/libxrpl/peerfinder/Endpoint.cpp +++ b/src/libxrpl/peerfinder/Endpoint.cpp @@ -5,11 +5,11 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { -Endpoint::Endpoint(beast::IP::Endpoint ep, std::uint32_t hops) - : hops(std::min(hops, Tuning::kMaxHops + 1)), address(std::move(ep)) +Endpoint::Endpoint(beast::ip::Endpoint ep, std::uint32_t hops) + : hops(std::min(hops, tuning::kMaxHops + 1)), address(std::move(ep)) { } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/PeerfinderManager.cpp b/src/libxrpl/peerfinder/PeerfinderManager.cpp index 2219627f09..0cce2389ec 100644 --- a/src/libxrpl/peerfinder/PeerfinderManager.cpp +++ b/src/libxrpl/peerfinder/PeerfinderManager.cpp @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class ManagerImp : public Manager { @@ -98,7 +98,7 @@ public: } void - addFixedPeer(std::string_view name, std::vector const& addresses) override + addFixedPeer(std::string_view name, std::vector const& addresses) override { logic_.addFixedPeer(name, addresses); } @@ -119,14 +119,14 @@ public: std::pair, Result> newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) override + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) override { return logic_.newInboundSlot(localEndpoint, remoteEndpoint); } std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) override + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) override { return logic_.newOutboundSlot(remoteEndpoint); } @@ -163,7 +163,7 @@ public: //-------------------------------------------------------------------------- bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) + onConnected(std::shared_ptr const& slot, beast::ip::Endpoint const& localEndpoint) override { SlotImp::ptr const impl(std::dynamic_pointer_cast(slot)); @@ -184,7 +184,7 @@ public: return logic_.redirect(impl); } - std::vector + std::vector autoconnect() override { return logic_.autoconnect(); @@ -265,4 +265,4 @@ makeManager( return std::make_unique(ioContext, clock, journal, store, collector); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/SlotImp.cpp b/src/libxrpl/peerfinder/SlotImp.cpp index 0a4f32fd62..5209bd51ab 100644 --- a/src/libxrpl/peerfinder/SlotImp.cpp +++ b/src/libxrpl/peerfinder/SlotImp.cpp @@ -9,11 +9,11 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { SlotImp::SlotImp( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint remoteEndpoint, + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock) : recent(clock) @@ -30,7 +30,7 @@ SlotImp::SlotImp( { } -SlotImp::SlotImp(beast::IP::Endpoint remoteEndpoint, bool fixed, clock_type& clock) +SlotImp::SlotImp(beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock) : recent(clock) , inbound_(false) , fixed_(fixed) @@ -49,29 +49,29 @@ SlotImp::state(State state) { // Must go through activate() to set active state XRPL_ASSERT( - state != State::Active, "xrpl::PeerFinder::SlotImp::state : input state is not active"); + state != State::Active, "xrpl::peer_finder::SlotImp::state : input state is not active"); // The state must be different XRPL_ASSERT( state_ != state, - "xrpl::PeerFinder::SlotImp::state : input state is different from " + "xrpl::peer_finder::SlotImp::state : input state is different from " "current"); // You can't transition into the initial states XRPL_ASSERT( state != State::Accept && state != State::Connect, - "xrpl::PeerFinder::SlotImp::state : input state is not an initial"); + "xrpl::peer_finder::SlotImp::state : input state is not an initial"); // Can only become connected from outbound connect state XRPL_ASSERT( state != State::Connected || (!inbound_ && state_ == State::Connect), - "xrpl::PeerFinder::SlotImp::state : input state is not connected an " + "xrpl::peer_finder::SlotImp::state : input state is not connected an " "invalid state"); // Can't gracefully close on an outbound connection attempt XRPL_ASSERT( state != State::Closing || state_ != State::Connect, - "xrpl::PeerFinder::SlotImp::state : input state is not closing an " + "xrpl::peer_finder::SlotImp::state : input state is not closing an " "invalid state"); state_ = state; @@ -83,7 +83,7 @@ SlotImp::activate(clock_type::time_point const& now) // Can only become active from the accept or connected state XRPL_ASSERT( state_ == State::Accept || state_ == State::Connected, - "xrpl::PeerFinder::SlotImp::activate : valid state"); + "xrpl::peer_finder::SlotImp::activate : valid state"); state_ = State::Active; whenAcceptEndpoints = now; @@ -100,7 +100,7 @@ SlotImp::RecentT::RecentT(clock_type& clock) : cache_(clock) } void -SlotImp::RecentT::insert(beast::IP::Endpoint const& ep, std::uint32_t hops) +SlotImp::RecentT::insert(beast::ip::Endpoint const& ep, std::uint32_t hops) { auto const result(cache_.emplace(ep, hops)); if (!result.second) @@ -115,7 +115,7 @@ SlotImp::RecentT::insert(beast::IP::Endpoint const& ep, std::uint32_t hops) } bool -SlotImp::RecentT::filter(beast::IP::Endpoint const& ep, std::uint32_t hops) +SlotImp::RecentT::filter(beast::ip::Endpoint const& ep, std::uint32_t hops) { auto const iter(cache_.find(ep)); if (iter == cache_.end()) @@ -129,7 +129,7 @@ SlotImp::RecentT::filter(beast::IP::Endpoint const& ep, std::uint32_t hops) void SlotImp::RecentT::expire() { - beast::expire(cache_, Tuning::kLiveCacheSecondsToLive); + beast::expire(cache_, tuning::kLiveCacheSecondsToLive); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/SourceStrings.cpp b/src/libxrpl/peerfinder/SourceStrings.cpp index f47e0cd51d..ca6ff07cba 100644 --- a/src/libxrpl/peerfinder/SourceStrings.cpp +++ b/src/libxrpl/peerfinder/SourceStrings.cpp @@ -8,7 +8,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class SourceStringsImp : public SourceStrings { @@ -33,9 +33,9 @@ public: results.addresses.reserve(strings_.size()); for (auto const& str : strings_) { - beast::IP::Endpoint ep(beast::IP::Endpoint::fromString(str)); + beast::ip::Endpoint ep(beast::ip::Endpoint::fromString(str)); if (isUnspecified(ep)) - ep = beast::IP::Endpoint::fromString(str); + ep = beast::ip::Endpoint::fromString(str); if (!isUnspecified(ep)) results.addresses.push_back(ep); } @@ -54,4 +54,4 @@ SourceStrings::make(std::string const& name, Strings const& strings) return std::make_shared(name, strings); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 6ac352f3e1..8a18b3f228 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -13,7 +13,7 @@ #include #include -namespace xrpl::BuildInfo { +namespace xrpl::build_info { namespace { @@ -173,4 +173,4 @@ isNewerVersion(std::uint64_t version) return false; } -} // namespace xrpl::BuildInfo +} // namespace xrpl::build_info diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 87761ce13e..e81f975844 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -9,7 +9,7 @@ #include namespace xrpl { -namespace RPC { +namespace rpc { namespace detail { @@ -215,12 +215,12 @@ errorCodeHttpStatus(ErrorCodeI code) return getErrorInfo(code).httpStatus; } -} // namespace RPC +} // namespace rpc std::string rpcErrorString(json::Value const& jv) { - XRPL_ASSERT(RPC::containsError(jv), "xrpl::RPC::rpcErrorString : input contains an error"); + XRPL_ASSERT(rpc::containsError(jv), "xrpl::rpc::rpcErrorString : input contains an error"); return jv[jss::error].asString() + jv[jss::error_message].asString(); } diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp index 4f0a2d5071..fd44ae1f33 100644 --- a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { void insertNFTSyntheticInJson( @@ -21,4 +21,4 @@ insertNFTSyntheticInJson( insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/libxrpl/protocol/RPCErr.cpp b/src/libxrpl/protocol/RPCErr.cpp index ec9a3dee9d..c172a1898d 100644 --- a/src/libxrpl/protocol/RPCErr.cpp +++ b/src/libxrpl/protocol/RPCErr.cpp @@ -13,7 +13,7 @@ json::Value rpcError(ErrorCodeI iError) { json::Value jvResult(json::ValueType::Object); - RPC::injectError(iError, jvResult); + rpc::injectError(iError, jvResult); return jvResult; } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 33ca5424d1..6ab272b3d1 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -48,7 +48,7 @@ namespace xrpl { -namespace STParsedJSONDetail { +namespace st_parsed_json_detail { template constexpr U toUnsigned(S value) @@ -93,7 +93,7 @@ makeName(std::string const& object, std::string const& field) static inline json::Value notAnObject(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' is not a JSON object."); } @@ -106,33 +106,33 @@ notAnObject(std::string const& object) static inline json::Value notAnArray(std::string const& object) { - return RPC::makeError(RpcInvalidParams, "Field '" + object + "' is not a JSON array."); + return rpc::makeError(RpcInvalidParams, "Field '" + object + "' is not a JSON array."); } static inline json::Value unknownField(std::string const& object, std::string const& field) { - return RPC::makeError(RpcInvalidParams, "Field '" + makeName(object, field) + "' is unknown."); + return rpc::makeError(RpcInvalidParams, "Field '" + makeName(object, field) + "' is unknown."); } static inline json::Value outOfRange(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' is out of range."); } static inline json::Value badType(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' has bad type."); } static inline json::Value invalidData(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' has invalid data."); } @@ -145,14 +145,14 @@ invalidData(std::string const& object) static inline json::Value arrayExpected(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' must be a JSON array."); } static inline json::Value arrayTooBig(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' exceeds allowed JSON array size of " + std::to_string(kMaxParsedJsonArraySize) + " elements per field."); @@ -161,20 +161,20 @@ arrayTooBig(std::string const& object, std::string const& field) static inline json::Value stringExpected(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' must be a string."); } static inline json::Value tooDeep(std::string const& object) { - return RPC::makeError(RpcInvalidParams, "Field '" + object + "' exceeds nesting depth limit."); + return rpc::makeError(RpcInvalidParams, "Field '" + object + "' exceeds nesting depth limit."); } static inline json::Value singletonExpected(std::string const& object, unsigned int index) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + object + "[" + std::to_string(index) + "]' must be an object with a single key/object value."); @@ -183,7 +183,7 @@ singletonExpected(std::string const& object, unsigned int index) static inline json::Value templateMismatch(SField const& sField) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Object '" + sField.getName() + "' contents did not meet requirements for that type."); } @@ -191,7 +191,7 @@ templateMismatch(SField const& sField) static inline json::Value nonObjectInArray(std::string const& item, json::UInt index) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Item '" + item + "' at index " + std::to_string(index) + " is not an object. Arrays may only contain objects."); @@ -791,7 +791,7 @@ parseLeaf( if (pathEl.isMember(jss::currency) && pathEl.isMember(jss::mpt_issuance_id)) { - error = RPC::makeError(RpcInvalidParams, "Invalid Asset."); + error = rpc::makeError(RpcInvalidParams, "Invalid Asset."); return ret; } @@ -1195,13 +1195,13 @@ parseArray( } } -} // namespace STParsedJSONDetail +} // namespace st_parsed_json_detail //------------------------------------------------------------------------------ STParsedJSONObject::STParsedJSONObject(std::string const& name, json::Value const& json) { - using namespace STParsedJSONDetail; + using namespace st_parsed_json_detail; object = parseObject(name, json, sfGeneric, 0, error); } diff --git a/src/libxrpl/protocol/XChainAttestations.cpp b/src/libxrpl/protocol/XChainAttestations.cpp index 792fe5da9d..7c887e785b 100644 --- a/src/libxrpl/protocol/XChainAttestations.cpp +++ b/src/libxrpl/protocol/XChainAttestations.cpp @@ -24,7 +24,7 @@ #include namespace xrpl { -namespace Attestations { +namespace attestations { AttestationBase::AttestationBase( AccountID attestationSignerAccount, @@ -385,7 +385,7 @@ operator==(AttestationCreateAccount const& lhs, AttestationCreateAccount const& std::tie(rhs.createCount, rhs.toCreate, rhs.rewardAmount); } -} // namespace Attestations +} // namespace attestations SField const& XChainClaimAttestation::arrayFieldName{sfXChainClaimAttestations}; SField const& XChainCreateAccountAttestation::arrayFieldName{sfXChainCreateAccountAttestations}; diff --git a/src/libxrpl/resource/Charge.cpp b/src/libxrpl/resource/Charge.cpp index e174c13522..f80588b143 100644 --- a/src/libxrpl/resource/Charge.cpp +++ b/src/libxrpl/resource/Charge.cpp @@ -6,7 +6,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { Charge::Charge(value_type cost, std::string label) : cost_(cost), label_(std::move(label)) { @@ -57,4 +57,4 @@ Charge::operator*(value_type m) const return Charge(cost_ * m, label_); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/Consumer.cpp b/src/libxrpl/resource/Consumer.cpp index 58d5775a31..934aaddbf5 100644 --- a/src/libxrpl/resource/Consumer.cpp +++ b/src/libxrpl/resource/Consumer.cpp @@ -12,7 +12,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { Consumer::Consumer(Logic& logic, Entry& entry) : logic_(&logic), entry_(&entry) { @@ -99,14 +99,14 @@ Consumer::charge(Charge const& what, std::string const& context) bool Consumer::warn() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::warn : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::warn : non-null entry"); return logic_->warn(*entry_); } bool Consumer::disconnect(beast::Journal const& j) { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::disconnect : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::disconnect : non-null entry"); bool const d = logic_->disconnect(*entry_); if (d) { @@ -118,14 +118,14 @@ Consumer::disconnect(beast::Journal const& j) int Consumer::balance() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::balance : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::balance : non-null entry"); return logic_->balance(*entry_); } Entry& Consumer::entry() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::entry : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::entry : non-null entry"); return *entry_; } @@ -142,4 +142,4 @@ operator<<(std::ostream& os, Consumer const& v) return os; } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/Fees.cpp b/src/libxrpl/resource/Fees.cpp index bb825fa3c7..037f60051e 100644 --- a/src/libxrpl/resource/Fees.cpp +++ b/src/libxrpl/resource/Fees.cpp @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { Charge const kFeeMalformedRequest(200, "malformed request"); Charge const kFeeRequestNoReply(10, "unsatisfiable request"); @@ -23,6 +23,6 @@ Charge const kFeeHeavyBurdenPeer(2000, "heavy peer request"); Charge const kFeeWarning(4000, "received warning"); Charge const kFeeDrop(6000, "dropped"); -// See also Resource::Logic::charge for log level cutoff values +// See also resource::Logic::charge for log level cutoff values -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/ResourceManager.cpp b/src/libxrpl/resource/ResourceManager.cpp index e3b4d9cc5c..cdfa95facd 100644 --- a/src/libxrpl/resource/ResourceManager.cpp +++ b/src/libxrpl/resource/ResourceManager.cpp @@ -23,7 +23,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { class ManagerImp : public Manager { @@ -58,14 +58,14 @@ public: } Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) override + newInboundEndpoint(beast::ip::Endpoint const& address) override { return logic_.newInboundEndpoint(address); } Consumer newInboundEndpoint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, bool const proxy, std::string_view forwardedFor) override { @@ -85,13 +85,13 @@ public: } Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) override + newOutboundEndpoint(beast::ip::Endpoint const& address) override { return logic_.newOutboundEndpoint(address); } Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) override + newUnlimitedEndpoint(beast::ip::Endpoint const& address) override { return logic_.newUnlimitedEndpoint(address); } @@ -136,7 +136,7 @@ private: void run() { - beast::setCurrentThreadName("Resource::Mngr"); + beast::setCurrentThreadName("resource::Mngr"); for (;;) { logic_.periodicActivity(); @@ -164,4 +164,4 @@ makeManager(beast::insight::Collector::ptr const& collector, beast::Journal jour return std::make_unique(collector, journal); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index 353c295856..39883873fb 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -104,7 +104,7 @@ InfoSub::~InfoSub() } } -Resource::Consumer& +resource::Consumer& InfoSub::getConsumer() { return consumer_; diff --git a/src/libxrpl/server/JSONRPCUtil.cpp b/src/libxrpl/server/JSONRPCUtil.cpp index f38ff280ac..d59582fc1a 100644 --- a/src/libxrpl/server/JSONRPCUtil.cpp +++ b/src/libxrpl/server/JSONRPCUtil.cpp @@ -42,7 +42,7 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b // CHECKME this returns a different version than the replies below. Is // this by design or an accident or should it be using - // BuildInfo::getFullVersionString () as well? + // build_info::getFullVersionString () as well? output("Server: " + systemName() + "-json-rpc/v1"); output("\r\n"); @@ -123,7 +123,7 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b "Content-Type: application/json; charset=UTF-8\r\n"); output("Server: " + systemName() + "-json-rpc/"); - output(BuildInfo::getFullVersionString()); + output(build_info::getFullVersionString()); output( "\r\n" "\r\n"); diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp index c1a79019af..694d4448d5 100644 --- a/src/libxrpl/server/Port.cpp +++ b/src/libxrpl/server/Port.cpp @@ -107,7 +107,7 @@ populate( { // First, check to see if 0.0.0.0 or ipv6 equivalent was configured, // which means all IP addresses. - auto const addr = beast::IP::Endpoint::fromStringChecked(ip); + auto const addr = beast::ip::Endpoint::fromStringChecked(ip); if (addr) { if (isUnspecified(*addr)) diff --git a/src/libxrpl/shamap/SHAMapNodeID.cpp b/src/libxrpl/shamap/SHAMapNodeID.cpp index 16aaafe709..a511fc038c 100644 --- a/src/libxrpl/shamap/SHAMapNodeID.cpp +++ b/src/libxrpl/shamap/SHAMapNodeID.cpp @@ -129,7 +129,8 @@ selectBranch(SHAMapNodeID const& id, uint256 const& hash) SHAMapNodeID SHAMapNodeID::createID(int depth, uint256 const& key) { - XRPL_ASSERT((depth >= 0) && (depth < 65), "xrpl::SHAMapNodeID::createID : valid branch input"); + XRPL_ASSERT( + depth >= 0 && depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth"); return SHAMapNodeID(depth, key & depthMask(depth)); } diff --git a/src/libxrpl/shamap/SHAMapSync.cpp b/src/libxrpl/shamap/SHAMapSync.cpp index cc30426f9d..cbed6885c9 100644 --- a/src/libxrpl/shamap/SHAMapSync.cpp +++ b/src/libxrpl/shamap/SHAMapSync.cpp @@ -107,7 +107,7 @@ SHAMap::visitNodes(std::function const& function) const void SHAMap::visitDifferences( - SHAMap const* have, + SHAMap const* map, std::function const& function) const { // Visit every node in this SHAMap that is not present @@ -118,13 +118,13 @@ SHAMap::visitDifferences( if (root_->getHash().isZero()) return; - if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) + if ((map != nullptr) && (root_->getHash() == map->root_->getHash())) return; if (root_->isLeaf()) { auto leaf = intr_ptr::staticPointerCast(root_); - if ((have == nullptr) || !have->hasLeafNode(leaf->peekItem()->key(), leaf->getHash())) + if ((map == nullptr) || !map->hasLeafNode(leaf->peekItem()->key(), leaf->getHash())) function(*root_); return; } @@ -149,18 +149,15 @@ SHAMap::visitDifferences( if (!node->isEmptyBranch(i)) { auto const& childHash = node->getChildHash(i); - SHAMapNodeID const childID = nodeID.getChildNodeID(i); + auto const childID = nodeID.getChildNodeID(i); auto next = descendThrow(node, i); if (next->isInner()) { - if ((have == nullptr) || !have->hasInnerNode(childID, childHash)) + if ((map == nullptr) || !map->hasInnerNode(childID, childHash)) stack.emplace(safeDowncast(next), childID); } - else if ( - (have == nullptr) || - !have->hasLeafNode( - safeDowncast(next)->peekItem()->key(), childHash)) + else if ((map == nullptr) || !map->hasLeafNode(leafKey(*next), childHash)) { if (!function(*next)) return; @@ -414,7 +411,7 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter) bool SHAMap::getNodeFat( SHAMapNodeID const& wanted, - std::vector>& data, + std::vector& data, bool fatLeaves, std::uint32_t depth) const { @@ -460,7 +457,7 @@ SHAMap::getNodeFat( // Add this node to the reply s.erase(); node->serializeForWire(s); - data.emplace_back(nodeID, s.getData()); + data.emplace_back(nodeID, node->isLeaf(), s.getData()); if (node->isInner()) { @@ -490,7 +487,7 @@ SHAMap::getNodeFat( // Just include this node s.erase(); childNode->serializeForWire(s); - data.emplace_back(childID, s.getData()); + data.emplace_back(childID, childNode->isLeaf(), s.getData()); } } } @@ -508,25 +505,33 @@ SHAMap::serializeRoot(Serializer& s) const } SHAMapAddNode -SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter) +SHAMap::addRootNode( + SHAMapHash const& hash, + SHAMapTreeNodePtr rootNode, + SHAMapSyncFilter const* filter) { + XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid"); + XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node"); + // we already have a root_ node if (root_->getHash().isNonZero()) { - JLOG(journal_.trace()) << "got root node, already have one"; - XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash input"); + JLOG(journal_.trace()) << "Got root node, already have one"; + XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash"); return SHAMapAddNode::duplicate(); } - XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid"); - auto node = SHAMapTreeNode::makeFromWire(rootNode); - if (!node || node->getHash() != hash) + if (rootNode->getHash() != hash) + { + JLOG(journal_.warn()) << "Corrupt root node received: expected hash " << hash << ", got " + << rootNode->getHash(); return SHAMapAddNode::invalid(); + } if (backed_) - canonicalize(hash, node); + canonicalize(hash, rootNode); - root_ = node; + root_ = std::move(rootNode); if (root_->isLeaf()) clearSynching(); @@ -543,9 +548,18 @@ SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFil } SHAMapAddNode -SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncFilter const* filter) +SHAMap::addKnownNode( + SHAMapNodeID const& nodeID, + SHAMapTreeNodePtr treeNode, + SHAMapSyncFilter const* filter) { - XRPL_ASSERT(!node.isRoot(), "xrpl::SHAMap::addKnownNode : valid node input"); + XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node"); + XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node"); + XRPL_ASSERT( + !treeNode->isLeaf() || + SHAMapNodeID::createID(nodeID.getDepth(), leafKey(*treeNode)).getNodeID() == + nodeID.getNodeID(), + "xrpl::SHAMap::addKnownNode : leaf position consistent with node ID"); if (!isSynching()) { @@ -559,14 +573,15 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF while (currNode->isInner() && !safeDowncast(currNode)->isFullBelow(generation) && - (currNodeID.getDepth() < node.getDepth())) + (currNodeID.getDepth() < nodeID.getDepth())) { - int const branch = selectBranch(currNodeID, node.getNodeID()); + int const branch = selectBranch(currNodeID, nodeID.getNodeID()); XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::addKnownNode : valid branch"); auto inner = safeDowncast(currNode); if (inner->isEmptyBranch(branch)) { - JLOG(journal_.warn()) << "Add known node for empty branch" << node; + JLOG(journal_.warn()) << "Add known node " << nodeID << " for empty branch " << branch + << " at " << currNodeID; return SHAMapAddNode::invalid(); } @@ -582,67 +597,45 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF if (currNode != nullptr) continue; - auto newNode = SHAMapTreeNode::makeFromWire(rawNode); - - if (!newNode || childHash != newNode->getHash()) + if (childHash != treeNode->getHash()) { - JLOG(journal_.warn()) << "Corrupt node received"; + JLOG(journal_.warn()) << "Corrupt node " << nodeID << " received: expected hash " + << childHash << ", got " << treeNode->getHash(); return SHAMapAddNode::invalid(); } - // In rare cases, a node can still be corrupt even after hash - // validation. For leaf nodes, we perform an additional check to - // ensure the node's position in the tree is consistent with its - // content to prevent inconsistencies that could - // propagate further down the line. - if (newNode->isLeaf()) - { - auto const& actualKey = - safeDowncast(newNode.get())->peekItem()->key(); - - // Validate that this leaf belongs at the target position - auto const expectedNodeID = SHAMapNodeID::createID(node.getDepth(), actualKey); - if (expectedNodeID.getNodeID() != node.getNodeID()) - { - JLOG(journal_.debug()) - << "Leaf node position mismatch: " - << "expected=" << expectedNodeID.getNodeID() << ", actual=" << node.getNodeID(); - return SHAMapAddNode::invalid(); - } - } - // Inner nodes must be at a level strictly less than 64 // but leaf nodes (while notionally at level 64) can be // at any depth up to and including 64: if ((currNodeID.getDepth() > kLeafDepth) || - (newNode->isInner() && currNodeID.getDepth() == kLeafDepth)) + (treeNode->isInner() && currNodeID.getDepth() == kLeafDepth)) { // Map is provably invalid state_ = SHAMapState::Invalid; return SHAMapAddNode::useful(); } - if (currNodeID != node) + if (currNodeID != nodeID) { // Either this node is broken or we didn't request it (yet) - JLOG(journal_.warn()) << "unable to hook node " << node; + JLOG(journal_.warn()) << "unable to hook node " << nodeID; JLOG(journal_.info()) << " stuck at " << currNodeID; - JLOG(journal_.info()) << "got depth=" << node.getDepth() + JLOG(journal_.info()) << "got depth=" << nodeID.getDepth() << ", walked to= " << currNodeID.getDepth(); return SHAMapAddNode::useful(); } if (backed_) - canonicalize(childHash, newNode); + canonicalize(childHash, treeNode); - newNode = prevNode->canonicalizeChild(branch, std::move(newNode)); + treeNode = prevNode->canonicalizeChild(branch, std::move(treeNode)); if (filter != nullptr) { Serializer s; - newNode->serializeWithPrefix(s); + treeNode->serializeWithPrefix(s); filter->gotNode( - false, childHash, ledgerSeq_, std::move(s.modData()), newNode->getType()); + false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType()); } return SHAMapAddNode::useful(); diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index a9ba0ec874..eca50eb809 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -483,6 +483,12 @@ ValidVault::finalize( result = false; } + if (view.rules().enabled(fixCleanup3_4_0) && afterVault.lossUnrealized < kZero) + { + JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative"; + result = false; + } + if (afterVault.assetsTotal < kZero) { JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index e03cb56fd5..cbfb93c386 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -864,7 +864,7 @@ applyClaimAttestations( return std::unexpected(tecXCHAIN_NO_CLAIM_ID); // Add claims that are part of the signer's list to the "claims" vector - std::vector atts; + std::vector atts; atts.reserve(std::distance(attBegin, attEnd)); for (auto att = attBegin; att != attEnd; ++att) { @@ -1042,7 +1042,7 @@ applyCreateAccountAttestations( return std::unexpected(tecINSUFFICIENT_RESERVE); } - std::vector atts; + std::vector atts; atts.reserve(std::distance(attBegin, attEnd)); for (auto att = attBegin; att != attEnd; ++att) { @@ -1160,8 +1160,8 @@ std::optional toClaim(STTx const& tx) { static_assert( - std::is_same_v || - std::is_same_v); + std::is_same_v || + std::is_same_v); try { @@ -1301,10 +1301,10 @@ attestationDoApply(ApplyContext& ctx) auto const& [srcChain, signersList, quorum, thisDoor, bridgeK] = scopeResult.value(); static_assert( - std::is_same_v || - std::is_same_v); + std::is_same_v || + std::is_same_v); - if constexpr (std::is_same_v) + if constexpr (std::is_same_v) { return applyClaimAttestations( ctx.view(), @@ -1317,7 +1317,7 @@ attestationDoApply(ApplyContext& ctx) quorum, ctx.journal); } - else if constexpr (std::is_same_v) + else if constexpr (std::is_same_v) { return applyCreateAccountAttestations( ctx.view(), @@ -2067,19 +2067,19 @@ XChainCreateClaimID::doApply() NotTEC XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationPreflight(ctx); } TER XChainAddClaimAttestation::preclaim(PreclaimContext const& ctx) { - return attestationPreclaim(ctx); + return attestationPreclaim(ctx); } TER XChainAddClaimAttestation::doApply() { - return attestationDoApply(ctx_); + return attestationDoApply(ctx_); } //------------------------------------------------------------------------------ @@ -2087,19 +2087,19 @@ XChainAddClaimAttestation::doApply() NotTEC XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationPreflight(ctx); } TER XChainAddAccountCreateAttestation::preclaim(PreclaimContext const& ctx) { - return attestationPreclaim(ctx); + return attestationPreclaim(ctx); } TER XChainAddAccountCreateAttestation::doApply() { - return attestationDoApply(ctx_); + return attestationDoApply(ctx_); } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index fb47cf0f97..b95d1001e1 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -283,10 +283,23 @@ OfferCreate::checkAcceptAsset( return asset.visit( [&](Issue const& issue) -> TER { auto const& issuer = issue.getIssuer(); + auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); + + // Check if the issuer has lsfDisallowIncomingTrustline set. + // If so, the account must already have a trustline to receive tokens. + if (view.rules().enabled(fixCleanup3_4_0) && + issuerAccount->isFlag(lsfDisallowIncomingTrustline)) + { + if (!trustLine) + { + JLOG(j.debug()) << "delay: can't receive IOUs from issuer with " + "DisallowIncomingTrustline set"; + return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; + } + } + if (issuerAccount->isFlag(lsfRequireAuth)) { - auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); - if (!trustLine) { return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; @@ -309,8 +322,6 @@ OfferCreate::checkAcceptAsset( } } - auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency)); - if (!trustLine) { return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index e9c153404c..b12cfb692f 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -34,7 +34,7 @@ LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx) NotTEC LoanBrokerSet::preflight(PreflightContext const& ctx) { - using namespace Lending; + using namespace lending; auto const& tx = ctx.tx; if (auto const data = tx[~sfData]; diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index a0aa948876..a312dba3b3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -127,23 +127,6 @@ LoanManage::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -static Number -owedToVault(SLE::ref loanSle) -{ - // Spec section 3.2.3.2, defines the default amount as - // - // DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - // - // Loan.InterestOutstanding is not stored directly on ledger. - // It is computed as - // - // Loan.TotalValueOutstanding - Loan.PrincipalOutstanding - - // Loan.ManagementFeeOutstanding - // - // Add that to the original formula, and you get this: - return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding); -} - TER LoanManage::defaultLoan( ApplyView& view, @@ -158,7 +141,7 @@ LoanManage::defaultLoan( std::int32_t const loanScale = loanSle->at(sfLoanScale); auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - Number const totalDefaultAmount = owedToVault(loanSle); + Number const totalDefaultAmount = loanVaultExposure(vaultSle, loanSle); // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; @@ -304,7 +287,7 @@ LoanManage::impairLoan( Asset const& vaultAsset, beast::Journal j) { - Number const lossUnrealized = owedToVault(loanSle); + Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle); // The vault may be at a different scale than the loan. Reduce rounding // errors during the accounting by rounding some of the values to that @@ -353,7 +336,7 @@ LoanManage::unimpairLoan( // Update the Vault object(clear "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - Number const lossReversed = owedToVault(loanSle); + Number const lossReversed = loanVaultExposure(vaultSle, loanSle); if (vaultLossUnrealizedProxy < lossReversed) { // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index 54ee85b186..74e8efeda2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -73,7 +73,7 @@ LoanPay::preflight(PreflightContext const& ctx) XRPAmount LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) { - using namespace Lending; + using namespace lending; auto const normalCost = Transactor::calculateBaseFee(view, tx); @@ -420,10 +420,13 @@ LoanPay::doApply() // LCOV_EXCL_STOP } + auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts); + JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid << ", interest paid: " << paymentParts->interestPaid << ", fee paid: " << paymentParts->feePaid - << ", value change: " << paymentParts->valueChange; + << ", assets total delta: " << assetsTotalDelta + << ", debt total delta: " << debtTotalDelta; //------------------------------------------------------ // LoanBroker object state changes @@ -439,13 +442,6 @@ LoanPay::doApply() !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded, "xrpl::LoanPay::doApply", "rounding does nothing for integral asset"); - // Account for value changes when reducing the broker's debt: - // - Positive value change (from full/late/overpayments): Subtract from the - // amount credited toward debt to avoid over-reducing the debt. - // - Negative value change (from full/overpayments): Add to the amount - // credited toward debt,effectively increasing the debt reduction. - auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange; - auto const totalPaidToBroker = paymentParts->feePaid; XRPL_ASSERT_PARTS( @@ -455,16 +451,16 @@ LoanPay::doApply() "payments add up"); // Decrease LoanBroker Debt by the amount paid, add the Loan value change - // (which might be negative). totalPaidToVaultForDebt may be negative, - // increasing the debt + // (which might be negative). debtTotalDelta may be negative, increasing the + // debt XRPL_ASSERT_PARTS( - isRounded(asset, totalPaidToVaultForDebt, loanScale), + isRounded(asset, debtTotalDelta, loanScale), "xrpl::LoanPay::doApply", - "totalPaidToVaultForDebt rounding good"); + "debtTotalDelta rounding good"); // Despite our best efforts, it's possible for rounding errors to accumulate // in the loan broker's debt total. This is because the broker may have more // than one loan with significantly different scales. - adjustImpreciseNumber(debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale); + adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale); //------------------------------------------------------ // Vault object state changes @@ -490,7 +486,7 @@ LoanPay::doApply() #endif assetsAvailableProxy += totalPaidToVaultRounded; - assetsTotalProxy += paymentParts->valueChange; + assetsTotalProxy += assetsTotalDelta; XRPL_ASSERT_PARTS( *assetsAvailableProxy <= *assetsTotalProxy, @@ -543,11 +539,11 @@ LoanPay::doApply() return tecPRECISION_LOSS; // LCOV_EXCL_STOP } - if (paymentParts->valueChange != beast::kZero && assetsTotalAfter == assetsTotalBefore) + if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore) { - // Non-zero valueChange with an unchanged assetsTotal indicates that the - // actual value change rounded to zero. That should be impossible, but I - // can't rule it out for extreme edge cases, so fail gracefully if it + // Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that + // the actual value change rounded to zero. That should be impossible, but + // I can't rule it out for extreme edge cases, so fail gracefully if it // happens. // // LCOV_EXCL_START @@ -555,20 +551,21 @@ LoanPay::doApply() << "LoanPay: Vault assets expected change, but unchanged after rounding: " // << "Before: " << assetsTotalBefore // << ", After: " << assetsTotalAfter // - << ", ValueChange: " << paymentParts->valueChange; + << ", AssetsTotalDelta: " << assetsTotalDelta; return tecPRECISION_LOSS; // LCOV_EXCL_STOP } - if (paymentParts->valueChange == beast::kZero && assetsTotalAfter != assetsTotalBefore) + if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore) { - // A change in assetsTotal when there was no valueChange indicates that - // something really weird happened. That should be flat out impossible. + // A change in assetsTotal when there was no assetsTotalDelta indicates + // that something really weird happened. That should be flat out + // impossible. // // LCOV_EXCL_START JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " // << "Before: " << assetsTotalBefore // << ", After: " << assetsTotalAfter // - << ", ValueChange: " << paymentParts->valueChange; + << ", AssetsTotalDelta: " << assetsTotalDelta; return tecINTERNAL; // LCOV_EXCL_STOP } diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 694d01c69f..95a9581dd3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -53,7 +53,7 @@ LoanSet::getFlagsMask(PreflightContext const& ctx) NotTEC LoanSet::preflight(PreflightContext const& ctx) { - using namespace Lending; + using namespace lending; auto const& tx = ctx.tx; @@ -439,12 +439,12 @@ LoanSet::doApply() principalRequested, properties.loanState.managementFeeDue); - auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum); XRPL_ASSERT_PARTS( - vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy, + *vaultSle->at(sfAssetsMaximum) == 0 || *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy, "xrpl::LoanSet::doApply", "Vault is below maximum limit"); - if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy) + + if (loanOriginationExceedsVaultMaximum(vaultSle, vaultTotalProxy, state.interestDue)) { JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault"; return tecLIMIT_EXCEEDED; @@ -490,8 +490,9 @@ LoanSet::doApply() auto const loanAssetsToBorrower = principalRequested - originationFee; - auto const newDebtDelta = principalRequested + state.interestDue; - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; + auto const [assetsTotalDelta, debtTotalDelta] = + loanOriginationDeltas(vaultSle, principalRequested, state.interestDue); + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + debtTotalDelta; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) { @@ -634,7 +635,7 @@ LoanSet::doApply() // Update the balances in the vault vaultAvailableProxy -= principalRequested; - vaultTotalProxy += state.interestDue; + vaultTotalProxy += assetsTotalDelta; XRPL_ASSERT_PARTS( *vaultAvailableProxy <= *vaultTotalProxy, "xrpl::LoanSet::doApply", @@ -642,7 +643,7 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); + adjustImpreciseNumber(brokerSle->at(sfDebtTotal), debtTotalDelta, vaultAsset, vaultScale); adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_); loanSequenceProxy += 1; // The sequence should be extremely unlikely to roll over, but fail if it diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index e1f5873a89..a522f62788 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace xrpl { @@ -241,6 +242,8 @@ VaultCreate::doApply() } if (scale != 0u) vault->at(sfScale) = scale; + if (view().rules().enabled(featureLendingProtocolV1_1)) + vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis); view().insert(vault); // Explicitly create MPToken for the vault owner diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index ffaf26b5a7..5230a1f7dd 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -3179,7 +3179,7 @@ class Batch_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); { - using namespace loanBroker; + using namespace loan_broker; env(set(lender, vaultKeylet.key), kManagementFeeRate(TenthBips16(100)), kDebtMaximum(debtMaximumValue), diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index d3e0182db5..0fc5d6f845 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -5469,6 +5469,429 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase } } + void + testSendOverdraftBulletproof(FeatureBitset features) + { + uint64_t const balance = 100; + testSendOverdraftBulletproofImpl(features, balance, balance); // SUCCEED + testSendOverdraftBulletproofImpl(features, balance, balance + 1); // FAIL + } + + void + testSendOverdraftBulletproofImpl(FeatureBitset features, unsigned balance, unsigned amt) + { + testcase("Send: overdraft prevention via bulletproof"); + using namespace test::jtx; + + // Attack scenario: Alice has 100 tokens, tries to send 101 to Bob. + // The client-side check in mpt-crypto:mpt_utility.cpp:743 prevents honest + // clients from creating this proof. We bypass it by manually + // constructing a forged proof to demonstrate that the ledger's + // range proof verification catches the overdraft. + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), issuer("issuer"); + + uint64_t const aliceBalance = balance; + uint64_t const aliceAmount = amt; + uint64_t const aliceRemaining = aliceBalance - aliceAmount; + + // Setup: Alice has 100 tokens converted to confidential + ConfidentialEnv confEnv{ + env, + issuer, + {{.account = alice, .payAmount = 1000, .convertAmount = aliceBalance}, + {.account = bob, .payAmount = 1000, .convertAmount = 30}}}; + auto& mptIssuer = confEnv.mpt; + + std::pair errors = aliceAmount > aliceBalance + ? std::make_pair(-1, TER(tecBAD_PROOF)) + : std::make_pair(0, TER(tesSUCCESS)); + + unsigned const numParticipants = 3; + + // Verify Alice's actual balance before attack + { + auto const balance = requireOptional( + mptIssuer.getDecryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing Alice's balance"); + BEAST_EXPECT(balance == aliceBalance); + } + + // We cannot use ConfidentialSendSetup directly because it would + // call mpt_get_confidential_send_proof which has a client-side + // check (amount > balance) at line 743 in mpt_utility.cpp. + // Instead, we manually construct the transaction components. + + Buffer const randomElgamal = generateBlindingFactor(); + Buffer const randomBalance = generateBlindingFactor(); + + // Create encrypted amounts (using the OVERDRAFT amount) + Buffer const aliceEncAmt = mptIssuer.encryptAmount(alice, aliceAmount, randomElgamal); + Buffer const bobEncAmt = mptIssuer.encryptAmount(bob, aliceAmount, randomElgamal); + Buffer const issuerEncAmt = mptIssuer.encryptAmount(issuer, aliceAmount, randomElgamal); + + // Create commitments + // IMPORTANT: Amount commitment uses same randomness as ElGamal encryption! + Buffer const amtCommit = mptIssuer.getPedersenCommitment(aliceAmount, randomElgamal); + Buffer const balanceCommit = mptIssuer.getPedersenCommitment(aliceBalance, randomBalance); + + // Get Alice's current encrypted spending balance + Buffer const aliceEncBalance = requireOptional( + mptIssuer.getEncryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing Alice's encrypted spending balance"); + + uint32_t const version = mptIssuer.getMPTokenVersion(alice); + auto const ctxHash = getSendContextHash( + alice.id(), mptIssuer.issuanceID(), env.seq(alice), bob.id(), version); + + // Now we need to manually generate the sigma proof part. + // The sigma proof verifies ciphertext consistency and commitments, + // but doesn't check the range. We'll construct it with the overdraft + // amount to bypass the client-side check. + + // Generate the sigma proof manually using the lower-level secp256k1 API + auto* ctx = mpt_secp256k1_context(); + Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + + // Parse all public keys and ciphertexts + secp256k1_pubkey c1, c2Alice, c2Bob, c2Issuer; + // Parse sender's ciphertext C1 (first 33(kCompressedEcPointLength) bytes) + auto x = secp256k1_ec_pubkey_parse(ctx, &c1, aliceEncAmt.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse C1")) + return; + // Parse C2 components for all recipients + x = secp256k1_ec_pubkey_parse( + ctx, &c2Alice, aliceEncAmt.data() + kCompressedEcPointLength, kCompressedEcPointLength); + auto y = secp256k1_ec_pubkey_parse( + ctx, &c2Bob, bobEncAmt.data() + kCompressedEcPointLength, kCompressedEcPointLength); + auto z = secp256k1_ec_pubkey_parse( + ctx, + &c2Issuer, + issuerEncAmt.data() + kCompressedEcPointLength, + kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1 && z == 1, "Failed to parse C2 components")) + return; + secp256k1_pubkey c2Vec[] = {c2Alice, c2Bob, c2Issuer}; + + // Parse public keys + secp256k1_pubkey pkAlice, pkBob, pkIssuer; + auto alicePubKey = requireOptional(mptIssuer.getPubKey(alice), "Missing alice pubkey"); + auto bobPubKey = requireOptional(mptIssuer.getPubKey(bob), "Missing bob pubkey"); + auto issuerPubKey = requireOptional(mptIssuer.getPubKey(issuer), "Missing issuer pubkey"); + x = secp256k1_ec_pubkey_parse(ctx, &pkAlice, alicePubKey.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse(ctx, &pkBob, bobPubKey.data(), kCompressedEcPointLength); + z = secp256k1_ec_pubkey_parse( + ctx, &pkIssuer, issuerPubKey.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1 && z == 1, "Failed to parse public keys")) + return; + secp256k1_pubkey pkVec[] = {pkAlice, pkBob, pkIssuer}; + + // Parse commitments + secp256k1_pubkey pcAmount, pcBalance, b1, b2; + x = secp256k1_ec_pubkey_parse(ctx, &pcAmount, amtCommit.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse( + ctx, &pcBalance, balanceCommit.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse commitments")) + return; + // Parse balance ciphertext + x = secp256k1_ec_pubkey_parse(ctx, &b1, aliceEncBalance.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse( + ctx, &b2, aliceEncBalance.data() + kCompressedEcPointLength, kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse balance ciphertext")) + return; + + // Get Alice's private key + auto alicePrivKey = requireOptional(mptIssuer.getPrivKey(alice), "Missing alice privkey"); + + // Generate the compact sigma proof (part of mpt_get_confidential_send_proof) + // This will succeed because sigma proof doesn't check amount vs balance + x = secp256k1_compact_standard_prove( + ctx, + sigmaProof.data(), + aliceAmount, + aliceBalance, + randomElgamal.data(), + alicePrivKey.data(), + randomBalance.data(), + numParticipants, + &c1, + c2Vec, + pkVec, + &pcAmount, + &pkAlice, + &pcBalance, + &b1, + &b2, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Failed to generate sigma proof")) + return; + + // Direct verification + x = secp256k1_compact_standard_verify( + ctx, + sigmaProof.data(), + numParticipants, + &c1, + c2Vec, + pkVec, + &pcAmount, + &pkAlice, + &pcBalance, + &b1, + &b2, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Sigma verification failed")) + return; + + // Compute the remaining blinding factor: r_remaining = r_balance - r_amount + // This is required because the ledger homomorphically computes: + // C_remaining = C_balance - C_amount = Commit(remaining, r_balance - r_amount) + Buffer randomRemaining(kEcBlindingFactorLength); + Buffer negRandomElgamal(kEcBlindingFactorLength); + secp256k1_mpt_scalar_negate(negRandomElgamal.data(), randomElgamal.data()); + secp256k1_mpt_scalar_add( + randomRemaining.data(), randomBalance.data(), negRandomElgamal.data()); + + // Now forge the bulletproof claiming + auto const forgedBulletproof = getForgedBulletproof( + {aliceAmount, aliceRemaining}, {randomElgamal, randomRemaining}, ctxHash); + + // Combine sigma proof + forged bulletproof + Buffer combinedProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE + kEcDoubleBulletproofLength); + std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + forgedBulletproof.data(), + kEcDoubleBulletproofLength); + + // Direct verification + x = mpt_verify_send_range_proof( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + amtCommit.data(), + balanceCommit.data(), + ctxHash.data()); + if (!BEAST_EXPECTS(x == errors.first, "Forged proof passed validation")) + return; + + // Attempt the transaction with forged proof + // Expected to FAIL with tecBAD_PROOF + mptIssuer.send({ + .account = alice, + .dest = bob, + .amt = aliceAmount, + .proof = strHex(combinedProof), + .senderEncryptedAmt = aliceEncAmt, + .destEncryptedAmt = bobEncAmt, + .issuerEncryptedAmt = issuerEncAmt, + .amountCommitment = amtCommit, + .balanceCommitment = balanceCommit, + .err = errors.second, + }); + + // Verify Alice's balance unchanged (attack prevented!) + { + auto const balance = requireOptional( + mptIssuer.getDecryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing post-attack balance"); + if (aliceAmount > aliceBalance) + { + BEAST_EXPECT(balance == aliceBalance); + } + else + { + BEAST_EXPECT(balance < aliceBalance); + } + } + } + + void + testConvertBackOverdraftBulletproof(FeatureBitset features) + { + uint64_t const balance = 100; + testConvertBackOverdraftBulletproofImpl(features, balance, balance); // SUCCEED + testConvertBackOverdraftBulletproofImpl(features, balance, balance + 1); // FAIL + } + + void + testConvertBackOverdraftBulletproofImpl(FeatureBitset features, uint64_t balance, uint64_t amt) + { + testcase("Convert back: overdraft prevention via bulletproof"); + using namespace test::jtx; + + // Attack scenario: Bob has 100 confidential tokens, tries to convert back 101. + // The client-side check in mpt_get_convert_back_proof would prevent honest + // clients from creating this proof. We bypass it by manually constructing + // a forged proof to demonstrate that the ledger's bulletproof verification + // catches the overdraft. + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + + uint64_t const bobBalance = balance; + uint64_t const convertAmount = amt; + uint64_t const bobRemaining = bobBalance - convertAmount; + + // Setup: Bob and Carol both have confidential balance + // Carol ensures outstanding amount >= convertAmount (bypass preclaim check) + // This allows us to test the bulletproof specifically + ConfidentialEnv confEnv{ + env, + alice, + { + {.account = bob, .payAmount = 1000, .convertAmount = bobBalance}, + {.account = carol, + .payAmount = 1000, + .convertAmount = std::max(convertAmount, bobBalance + 1)}, + }}; + auto& mptAlice = confEnv.mpt; + + std::pair errors = convertAmount > bobBalance + ? std::make_pair(-1, TER(tecBAD_PROOF)) + : std::make_pair(0, TER(tesSUCCESS)); + + // Verify Bob's actual balance before attack + { + auto const balance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing Bob's balance"); + BEAST_EXPECT(balance == bobBalance); + } + + // We cannot use the standard getConvertBackProof because it calls + // mpt_get_convert_back_proof which has client-side validation. + // Instead, we manually construct the sigma proof and forge the bulletproof. + + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + + // Create encrypted amounts for the conversion + Buffer const bobEncAmt = mptAlice.encryptAmount(bob, convertAmount, blindingFactor); + Buffer const issuerEncAmt = mptAlice.encryptAmount(alice, convertAmount, blindingFactor); + + // Create Pedersen commitment to the current balance + Buffer const balanceCommit = mptAlice.getPedersenCommitment(bobBalance, pcBlindingFactor); + + // Get Bob's current encrypted spending balance + Buffer const bobEncBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing Bob's encrypted spending balance"); + + uint32_t const version = mptAlice.getMPTokenVersion(bob); + auto const ctxHash = + getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); + + // Now manually generate the compact sigma proof for ConvertBack + auto* ctx = mpt_secp256k1_context(); + Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + + // Parse the holder's public key + secp256k1_pubkey pkBob; + auto bobPubKey = requireOptional(mptAlice.getPubKey(bob), "Missing bob pubkey"); + auto x = secp256k1_ec_pubkey_parse(ctx, &pkBob, bobPubKey.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse Bob's public key")) + return; + + // Parse balance commitment + secp256k1_pubkey pcBalance; + x = secp256k1_ec_pubkey_parse( + ctx, &pcBalance, balanceCommit.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse balance commitment")) + return; + + // Parse balance ciphertext (B1, B2) + secp256k1_pubkey b1, b2; + x = secp256k1_ec_pubkey_parse(ctx, &b1, bobEncBalance.data(), kCompressedEcPointLength); + auto y = secp256k1_ec_pubkey_parse( + ctx, &b2, bobEncBalance.data() + kCompressedEcPointLength, kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse balance ciphertext")) + return; + + // Get Bob's private key + auto bobPrivKey = requireOptional(mptAlice.getPrivKey(bob), "Missing bob privkey"); + + // Generate the compact sigma proof for ConvertBack + // This verifies balance ownership and commitment linkage + x = secp256k1_compact_convertback_prove( + ctx, + sigmaProof.data(), + bobBalance, + bobPrivKey.data(), + pcBlindingFactor.data(), + &pkBob, + &b1, + &b2, + &pcBalance, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Failed to generate convertback sigma proof")) + return; + + // Verify the sigma proof passes (it doesn't check range) + x = secp256k1_compact_convertback_verify( + ctx, sigmaProof.data(), &pkBob, &b1, &b2, &pcBalance, ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Sigma verification failed")) + return; + + // Now forge the single bulletproof claiming the remaining balance is valid + // For ConvertBack, we need to prove: (balance - convertAmount) >= 0 + // We create a commitment to the remainder and generate a bulletproof for it + + // The bulletproof needs the blinding factor for the remainder commitment + // The ledger computes: C_remainder = C_balance - convertAmount*G + // So the blinding factor is just pcBlindingFactor (no randomness in convertAmount*G) + + auto const forgedBulletproof = + getForgedSingleBulletproof(bobRemaining, pcBlindingFactor, ctxHash); + + // Combine sigma proof + forged bulletproof + Buffer combinedProof(kEcConvertBackProofLength); + std::memcpy( + combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, + forgedBulletproof.data(), + kEcSingleBulletproofLength); + + // Direct verification of the full proof + x = mpt_verify_convert_back_proof( + combinedProof.data(), + bobPubKey.data(), + bobEncBalance.data(), + balanceCommit.data(), + convertAmount, + ctxHash.data()); + if (!BEAST_EXPECTS(x == errors.first, "Forged proof verification mismatch")) + return; + + // Attempt the transaction with forged proof + // Expected to FAIL with tecBAD_PROOF when convertAmount > bobBalance + mptAlice.convertBack({ + .account = bob, + .amt = convertAmount, + .proof = combinedProof, + .holderEncryptedAmt = bobEncAmt, + .issuerEncryptedAmt = issuerEncAmt, + .blindingFactor = blindingFactor, + .pedersenCommitment = balanceCommit, + .err = errors.second, + }); + + // Verify Bob's balance unchanged (attack prevented!) + { + auto const postBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing post-attack balance"); + if (convertAmount > bobBalance) + { + BEAST_EXPECT(postBalance == bobBalance); + } + else + { + BEAST_EXPECT(postBalance < bobBalance); + } + } + } + void testConvertBackBulletproof(FeatureBitset features) { @@ -8143,6 +8566,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase testConvertBackWithAuditor(features); testConvertBackPedersenProof(features); testConvertBackBulletproof(features); + testConvertBackOverdraftBulletproof(features); // Homomorphic operation tests testSendHomomorphicOverflow(features); @@ -8177,6 +8601,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase testSendInvalidProofContextBinding(features); testSendForgedEqualityProof(features); testSendForgedRangeProof(features); + testSendOverdraftBulletproof(features); testSendNegativeValueMalleability(features); testSendFiatShamirBinding(features); testSendProofComponentReuse(features); diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 257ed33619..788514e284 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2823,15 +2823,15 @@ class Delegate_test : public beast::unit_test::Suite auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); env(createTx); - env(loanBroker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverDeposit(alice, keylet.key, XRP(1)), + env(loan_broker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::coverDeposit(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverWithdraw(alice, keylet.key, XRP(1)), + env(loan_broker::coverWithdraw(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID)); env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID)); diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index 7b13fc060b..9be01b2abe 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -139,7 +139,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env.fund(XRP(1000), alice); auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(temDISABLED)); + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(temDISABLED)); } Env env{*this, testableAmendments()}; @@ -151,38 +151,38 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite { // Fail preflight1. Can't combine AccountTxnID and ticket. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfAccountTxnID.jsonName] = "00000000000000000000000000000000" "00000000000000000000000000000000"; env(tx, ticket::Use(ticketSeq), Ter(temINVALID)); } // Fee too low. - env(ledgerStateFix::nftPageLinks(alice, alice), Ter(telINSUF_FEE_P)); + env(ledger_state_fix::nftPageLinks(alice, alice), Ter(telINSUF_FEE_P)); // Invalid flags. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(linkFixFee), Txflags(tfPassive), Ter(temINVALID_FLAG)); { - // ledgerStateFix::nftPageLinks requires an Owner field. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + // ledger_state_fix::nftPageLinks requires an Owner field. + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx.removeMember(sfOwner.jsonName); env(tx, Fee(linkFixFee), Ter(temINVALID)); } { // NFTokenPageLink fixes require sfOwner and reject fields that // belong to other LedgerStateFix types. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfBookDirectory.jsonName] = to_string(uint256{1}); env(tx, Fee(linkFixFee), Ter(temINVALID)); } { // Invalid LedgerFixType codes. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfLedgerFixType.jsonName] = 0; env(tx, Fee(linkFixFee), Ter(tefINVALID_LEDGER_FIX_TYPE)); @@ -193,7 +193,9 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite // Preclaim Account const carol("carol"); env.memoize(carol); - env(ledgerStateFix::nftPageLinks(alice, carol), Fee(linkFixFee), Ter(tecOBJECT_NOT_FOUND)); + env(ledger_state_fix::nftPageLinks(alice, carol), + Fee(linkFixFee), + Ter(tecOBJECT_NOT_FOUND)); } void @@ -214,13 +216,17 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite // Owner has no pages to fix. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); // Alice has only one page. env(token::mint(alice), Txflags(tfTransferable)); env.close(); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); // Alice has at least three pages. for (std::uint32_t i = 0; i < 64; ++i) @@ -229,7 +235,9 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env.close(); } - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); } void @@ -439,7 +447,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite //********************************************************************** // Verify that the LedgerStateFix transaction is not enabled. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(daria, alice), Fee(linkFixFee), Ter(temDISABLED)); + env(ledger_state_fix::nftPageLinks(daria, alice), Fee(linkFixFee), Ter(temDISABLED)); // Wait 15 ledgers so the LedgerStateFix transaction is no longer // retried. @@ -475,7 +483,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env(noop(daria)); // daria fixes the links in alice's NFToken directory. - env(ledgerStateFix::nftPageLinks(daria, alice), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(daria, alice), Fee(linkFixFee)); env.close(); // alice's last page should now be present and include no links. @@ -516,7 +524,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite } // daria fixes the links in bob's NFToken directory. - env(ledgerStateFix::nftPageLinks(daria, bob), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(daria, bob), Fee(linkFixFee)); env.close(); // bob's last page should now be present and include a previous @@ -574,7 +582,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite } // carol fixes the links in their own NFToken directory. - env(ledgerStateFix::nftPageLinks(carol, carol), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(carol, carol), Fee(linkFixFee)); env.close(); { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index eaf1f2704c..9bc9524f80 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -2412,7 +2412,7 @@ class Invariants_test : public beast::unit_test::Suite vaultID = vKeylet.key; // Create Loan Broker - using namespace loanBroker; + using namespace loan_broker; auto const loanBrokerKeylet = keylet::loanBroker(a.id(), env.seq(a)); // Create a Loan Broker with all default values. @@ -2721,7 +2721,7 @@ class Invariants_test : public beast::unit_test::Suite brokerKeylet = this->createLoanBroker(alice, env, asset); if (!BEAST_EXPECT(env.le(brokerKeylet))) return false; - env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(10))); + env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10))); env.close(); return BEAST_EXPECT(env.le(brokerKeylet)); }; @@ -3374,6 +3374,41 @@ class Invariants_test : public beast::unit_test::Suite precloseXrp, TxAccount::A2); + // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is + // allowed to change loss unrealized, so it isolates this check from the + // "must not change loss unrealized" invariant. Gated behind + // fixCleanup3_4_0 (see below). + doInvariantCheck( + {"loss unrealized must not be negative"}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { + sample.lossUnrealized = -1; + })); + }, + XRPAmount{}, + STTx{ttLOAN_MANAGE, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + // Without fixCleanup3_4_0 the same state must NOT trip the invariant, + // preserving pre-amendment behavior (no fork risk). + doInvariantCheck( + makeEnv(defaultAmendments() - fixCleanup3_4_0), + {}, + [&](Account const& a1, Account const& a2, ApplyContext& ac) { + auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { + sample.lossUnrealized = -1; + })); + }, + XRPAmount{}, + STTx{ttLOAN_MANAGE, [](STObject& tx) {}}, + {tesSUCCESS, tesSUCCESS}, + precloseXrp, + TxAccount::A2); + doInvariantCheck( {"set assets outstanding must not exceed assets maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { diff --git a/src/test/app/LedgerNodeHelpers_test.cpp b/src/test/app/LedgerNodeHelpers_test.cpp new file mode 100644 index 0000000000..a9e4e3ebfc --- /dev/null +++ b/src/test/app/LedgerNodeHelpers_test.cpp @@ -0,0 +1,260 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +namespace xrpl::tests { + +class LedgerNodeHelpers_test : public beast::unit_test::Suite +{ + static boost::intrusive_ptr + makeTestItem(std::uint32_t seed) + { + Serializer s; + s.add32(seed); + s.add32(seed + 1); + s.add32(seed + 2); + return makeShamapitem(s.getSHA512Half(), s.slice()); + } + + static std::string + serializeNode(SHAMapTreeNodePtr const& node) + { + Serializer s; + node->serializeForWire(s); + auto const slice = s.slice(); + return std::string(slice.begin(), slice.end()); + } + + void + testGetTreeNode() + { + testcase("getTreeNode"); + + // Valid: inner node. It must have at least one child for `serializeNode` to work. + { + auto const innerNode = intr_ptr::makeShared(1); + auto const childNode = intr_ptr::makeShared(1); + innerNode->setChild(0, childNode); + auto const innerData = serializeNode(innerNode); + auto const result = getTreeNode(innerData); + BEAST_EXPECT(result && result->isInner()); + } + + // Valid: leaf node. + { + auto const leafItem = makeTestItem(12345); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + auto const leafData = serializeNode(leafNode); + auto const result = getTreeNode(leafData); + BEAST_EXPECT(result && result->isLeaf()); + } + + // Invalid: empty data. + { + auto const result = getTreeNode(""); + BEAST_EXPECT(!result); + } + + // Invalid: garbage data. + { + auto const result = getTreeNode("invalid"); + BEAST_EXPECT(!result); + } + + // Invalid: truncated data. + { + auto const leafItem = makeTestItem(54321); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + // Truncate the data to trigger an exception in SHAMapTreeNode::makeAccountState when + // the data is used to deserialize the node. + uint256 const tag; + auto const leafData = serializeNode(leafNode).substr(0, tag.kBytes - 1); + auto const result = getTreeNode(leafData); + BEAST_EXPECT(!result); + } + } + + void + testGetSHAMapNodeID() + { + testcase("getSHAMapNodeID"); + + { + // Tests using inner nodes at various depths. + auto const innerNode = intr_ptr::makeShared(1); + auto const childNode = intr_ptr::makeShared(1); + innerNode->setChild(0, childNode); + auto const innerData = serializeNode(innerNode); + + // Valid: legacy `nodeid` field at arbitrary depth. + { + auto const innerDepth = 3; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_nodeid(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(result == innerID); + } + + // Valid: new `id` field at minimum depth. + { + auto const innerDepth = 0; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_id(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(result == innerID); + } + + // Invalid: new `depth` field should not be used for inner nodes. + { + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_depth(10); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + + // Invalid: both legacy `nodeid` and new `id` fields set for an inner node. + { + auto const innerDepth = 9; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_nodeid(innerID.getRawString()); + ledgerNode.set_id(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + } + + { + // Tests using leaf nodes at various depths. + auto const leafItem = makeTestItem(12345); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + auto const leafData = serializeNode(leafNode); + auto const leafKey = leafItem->key(); + + // Valid: legacy `nodeid` field at arbitrary depth. + { + auto const kLeafDepth = 5; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_nodeid(leafID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Invalid: new `id` field should not be used for leaf nodes. + { + auto const kLeafDepth = 5; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_id(leafID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(!result); + } + + // Valid: new `depth` field at minimum depth. + { + auto const kLeafDepth = 0; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Valid: new `depth` field at arbitrary depth between minimum and maximum. + { + auto const kLeafDepth = 10; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Valid: new `depth` field at maximum depth. + // Note that we do not test a depth greater than the maximum depth, because the proto + // message is assumed to have been validated by the time the getSHAMapNodeID function is + // called. + { + auto const kLeafDepth = SHAMap::kLeafDepth; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Invalid: legacy `nodeid` field where the node ID is inconsistent with the key. + { + auto const otherItem = makeTestItem(54321); + auto const otherNode = + intr_ptr::makeShared(otherItem, 1); + auto const otherData = serializeNode(otherNode); + auto const otherKey = otherItem->key(); + auto const otherDepth = 1; + auto const otherID = SHAMapNodeID::createID(otherDepth, otherKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(otherData); + ledgerNode.set_nodeid(otherID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(!result); + } + } + + // Invalid: no field set. + { + auto const innerNode = intr_ptr::makeShared(1); + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata("test_data"); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + } + +public: + void + run() override + { + testGetTreeNode(); + testGetSHAMapNodeID(); + } +}; + +BEAST_DEFINE_TESTSUITE(LedgerNodeHelpers, app, xrpl); + +} // namespace xrpl::tests diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 4cc83608d6..7b521402a4 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -280,13 +280,13 @@ public: send(std::shared_ptr const& m) override { } - [[nodiscard]] beast::IP::Endpoint + [[nodiscard]] beast::ip::Endpoint getRemoteAddress() const override { return {}; } void - charge(Resource::Charge const& fee, std::string const& context = {}) override + charge(resource::Charge const& fee, std::string const& context = {}) override { } [[nodiscard]] id_t @@ -1206,7 +1206,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite if (serverResult != expecting) return false; - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); jtx::Env serverEnv(*this); serverEnv.app().config().ledgerReplay = server; auto httpResp = xrpl::makeResponse( diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp deleted file mode 100644 index 231a3b405a..0000000000 --- a/src/test/app/Loan_test.cpp +++ /dev/null @@ -1,8767 +0,0 @@ -#include -// -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -class Loan_test : public beast::unit_test::Suite -{ -protected: - // Ensure that all the features needed for Lending Protocol are included, - // even if they are set to unsupported. - - FeatureBitset const all_{jtx::testableAmendments()}; - std::string const iouCurrency_{"IOU"}; - - void - testDisabled() - { - testcase("Disabled"); - // Lending Protocol depends on Single Asset Vault (SAV). Test - // combinations of the two amendments. - // Single Asset Vault depends on MPTokensV1, but don't test every combo - // of that. - using namespace jtx; - auto failAll = [this](FeatureBitset features) { - Env env(*this, features); - - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10000), alice, bob); - - auto const keylet = keylet::loanBroker(alice, env.seq(alice)); - - using namespace std::chrono_literals; - using namespace loan; - - // counter party signature is optional on LoanSet. Confirm that by - // sending transaction without one. - auto setTx = env.jt(set(alice, keylet.key, Number(10000)), Ter(temDISABLED)); - env(setTx); - - // All loan transactions are disabled. - // 1. LoanSet - setTx = env.jt(setTx, Sig(sfCounterpartySignature, bob), Ter(temDISABLED)); - env(setTx); - // Actual sequence will be based off the loan broker, but we - // obviously don't have one of those if the amendment is disabled - auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice)); - // Other Loan transactions are disabled, too. - // 2. LoanDelete - env(del(alice, loanKeylet.key), Ter(temDISABLED)); - // 3. LoanManage - env(manage(alice, loanKeylet.key, tfLoanImpair), Ter(temDISABLED)); - // 4. LoanPay - env(pay(alice, loanKeylet.key, XRP(500)), Ter(temDISABLED)); - }; - failAll(all_ - featureMPTokensV1); - failAll(all_ - featureSingleAssetVault - featureLendingProtocol); - failAll(all_ - featureSingleAssetVault); - failAll(all_ - featureLendingProtocol); - } - - struct BrokerParameters - { - Number vaultDeposit = 1'000'000; - Number debtMax = 25'000; - TenthBips32 coverRateMin = percentageToTenthBips(10); - int coverDeposit = 1000; - TenthBips16 managementFeeRate{100}; - TenthBips32 coverRateLiquidation = percentageToTenthBips(25); - std::string data = {}; // NOLINT(readability-redundant-member-init) - std::uint32_t flags = 0; - // If set, the vault is created with this sfScale value. Useful for - // tests that need finer loanScale to exercise rounding edge cases. - std::optional vaultScale = - std::nullopt; // NOLINT(readability-redundant-member-init) - - [[nodiscard]] Number - maxCoveredLoanValue(Number const& currentDebt) const - { - NumberRoundModeGuard const mg(Number::RoundingMode::Downward); - auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value(); - - return debtLimit - currentDebt; - } - - static BrokerParameters const& - defaults() - { - static BrokerParameters const kResult{}; - return kResult; - } - - // TODO: create an operator() which returns a transaction similar to - // LoanParameters - }; - - struct BrokerInfo - { - jtx::PrettyAsset asset; - uint256 brokerID; - uint256 vaultID; - BrokerParameters params; - BrokerInfo( - jtx::PrettyAsset const& asset, - Keylet const& brokerKeylet, - Keylet const& vaultKeylet, - BrokerParameters p) - : asset(asset) - , brokerID(brokerKeylet.key) - , vaultID(vaultKeylet.key) - , params(std::move(p)) - { - } - - [[nodiscard]] Keylet - brokerKeylet() const - { - return keylet::loanBroker(brokerID); - } - [[nodiscard]] Keylet - vaultKeylet() const - { - return keylet::vault(vaultID); - } - - [[nodiscard]] int - vaultScale(jtx::Env const& env) const - { - using namespace jtx; - - auto const vaultSle = env.le(keylet::vault(vaultID)); - return getAssetsTotalScale(vaultSle); - } - }; - - struct LoanParameters - { - // The account submitting the transaction. May be borrower or broker. - jtx::Account account; - // The counterparty. Should be the other of borrower or broker. - jtx::Account counter; - // Whether the counterparty is specified in the `counterparty` field, or - // only signs. - bool counterpartyExplicit = true; - Number principalRequest; - // NOLINTBEGIN(readability-redundant-member-init) - std::optional setFee = std::nullopt; - std::optional originationFee = std::nullopt; - std::optional serviceFee = std::nullopt; - std::optional lateFee = std::nullopt; - std::optional closeFee = std::nullopt; - std::optional overFee = std::nullopt; - std::optional interest = std::nullopt; - std::optional lateInterest = std::nullopt; - std::optional closeInterest = std::nullopt; - std::optional overpaymentInterest = std::nullopt; - std::optional payTotal = std::nullopt; - std::optional payInterval = std::nullopt; - std::optional gracePd = std::nullopt; - std::optional flags = std::nullopt; - // NOLINTEND(readability-redundant-member-init) - - template - jtx::JTx - operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const - { - using namespace jtx; - using namespace jtx::loan; - - JTx jt{loan::set( - account, - broker.brokerID, - broker.asset(principalRequest).number(), - flags.value_or(0))}; - - Sig(sfCounterpartySignature, counter)(env, jt); - - Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); - - if (counterpartyExplicit) - kCounterparty(counter)(env, jt); - if (originationFee) - kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt); - if (serviceFee) - kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt); - if (lateFee) - kLatePaymentFee(broker.asset(*lateFee).number())(env, jt); - if (closeFee) - kClosePaymentFee(broker.asset(*closeFee).number())(env, jt); - if (overFee) - kOverpaymentFee (*overFee)(env, jt); - if (interest) - kInterestRate (*interest)(env, jt); - if (lateInterest) - kLateInterestRate (*lateInterest)(env, jt); - if (closeInterest) - kCloseInterestRate (*closeInterest)(env, jt); - if (overpaymentInterest) - kOverpaymentInterestRate (*overpaymentInterest)(env, jt); - if (payTotal) - kPaymentTotal (*payTotal)(env, jt); - if (payInterval) - kPaymentInterval (*payInterval)(env, jt); - if (gracePd) - kGracePeriod (*gracePd)(env, jt); - - return env.jt(jt, fN...); - } - }; - - struct PaymentParameters - { - Number overpaymentFactor = Number{1}; - std::optional overpaymentExtra = std::nullopt; - std::uint32_t flags = 0; - bool showStepBalances = false; - bool validateBalances = true; - - static PaymentParameters const& - defaults() - { - static PaymentParameters const kResult{}; - return kResult; - } - }; - - struct LoanState - { - std::uint32_t previousPaymentDate = 0; - NetClock::time_point startDate; - std::uint32_t nextPaymentDate = 0; - std::uint32_t paymentRemaining = 0; - std::int32_t const loanScale = 0; - Number totalValue = 0; - Number principalOutstanding = 0; - Number managementFeeOutstanding = 0; - Number periodicPayment = 0; - std::uint32_t flags = 0; - std::uint32_t const paymentInterval = 0; - TenthBips32 const interestRate{}; - }; - - /** - * Helper class to compare the expected state of a loan and loan broker - * against the data in the ledger. - */ - struct VerifyLoanStatus - { - public: - jtx::Env const& env; - BrokerInfo const& broker; - jtx::Account const& pseudoAccount; - Keylet const& loanKeylet; - - VerifyLoanStatus( - jtx::Env const& env, - BrokerInfo const& broker, - jtx::Account const& pseudo, - Keylet const& keylet) - : env(env), broker(broker), pseudoAccount(pseudo), loanKeylet(keylet) - { - } - - /** - * Checks the expected broker state against the ledger - */ - void - checkBroker( - Number const& principalOutstanding, - Number const& interestOwed, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - std::uint32_t ownerCount) const - { - using namespace jtx; - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const brokerDebt = brokerSle->at(sfDebtTotal); - auto const expectedDebt = principalOutstanding + interestOwed; - env.test.BEAST_EXPECT(brokerDebt == expectedDebt); - env.test.BEAST_EXPECT( - env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable)); - env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); - - if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)}; - env.test.BEAST_EXPECT( - vaultSle->at(sfAssetsAvailable) == - env.balance(vaultPseudo, broker.asset).number()); - if (ownerCount == 0) - { - // The Vault must be perfectly balanced if there - // are no loans outstanding - auto const total = vaultSle->at(sfAssetsTotal); - auto const available = vaultSle->at(sfAssetsAvailable); - env.test.BEAST_EXPECT(total == available); - env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - - void - checkPayment( - std::int32_t loanScale, - jtx::Account const& account, - jtx::PrettyAmount const& balanceBefore, - STAmount const& expectedPayment, - jtx::PrettyAmount const& adjustment) const - { - auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent()); - - STAmount const balanceChangeAmount{ - broker.asset, - roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)}; - { - auto const difference = roundToScale( - env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), - borrowerScale); - env.test.expect( - roundToScale(difference, loanScale) >= beast::kZero, - "Balance before: " + to_string(balanceBefore.value()) + - ", expected change: " + to_string(balanceChangeAmount) + - ", difference (balance after - expected): " + to_string(difference), - __FILE__, - __LINE__); - } - } - - /** - * Checks both the loan and broker expect states against the ledger - */ - void - operator()( - std::uint32_t previousPaymentDate, - std::uint32_t nextPaymentDate, - std::uint32_t paymentRemaining, - Number const& loanScale, - Number const& totalValue, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - std::uint32_t flags) const - { - using namespace jtx; - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate); - env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate); - env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); - env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue); - env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding); - env.test.BEAST_EXPECT( - loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding); - env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment); - env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); - - auto const ls = constructLoanState(loan); - - auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; - auto const paymentInterval = loan->at(sfPaymentInterval); - checkBroker( - principalOutstanding, - ls.interestDue, - interestRate, - paymentInterval, - paymentRemaining, - 1); - - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u)) - { - env.test.BEAST_EXPECT( - vaultSle->at(sfLossUnrealized) == - totalValue - managementFeeOutstanding); - } - else - { - env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - } - - /** - * Checks both the loan and broker expect states against the ledger - */ - void - operator()(LoanState const& state) const - { - operator()( - state.previousPaymentDate, - state.nextPaymentDate, - state.paymentRemaining, - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - state.flags); - }; - }; - - BrokerInfo - createVaultAndBroker( - jtx::Env& env, - jtx::PrettyAsset const& asset, - jtx::Account const& lender, - BrokerParameters const& params = BrokerParameters::defaults()) - { - using namespace jtx; - - Vault const vault{env}; - - auto const deposit = asset(params.vaultDeposit); - auto const debtMaximumValue = asset(params.debtMax).value(); - auto const coverDepositValue = asset(params.coverDeposit).value(); - - auto const coverRateMinValue = params.coverRateMin; - - auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); - if (params.vaultScale) - tx[sfScale] = *params.vaultScale; - env(tx); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); - env.close(); - if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault)) - { - BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); - } - - auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); - - using namespace loanBroker; - env(set(lender, vaultKeylet.key, params.flags), - kData(params.data), - kManagementFeeRate(params.managementFeeRate), - kDebtMaximum(debtMaximumValue), - kCoverRateMinimum(coverRateMinValue), - kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation))); - - if (coverDepositValue != beast::kZero) - env(coverDeposit(lender, keylet.key, coverDepositValue)); - - env.close(); - - return {asset, keylet, vaultKeylet, params}; - } - - /** - * Get the state without checking anything - */ - LoanState - getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet) - { - using d = NetClock::duration; - using tp = NetClock::time_point; - - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - return LoanState{ - .previousPaymentDate = loan->at(sfPreviousPaymentDueDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .loanScale = loan->at(sfLoanScale), - .totalValue = loan->at(sfTotalValueOutstanding), - .principalOutstanding = loan->at(sfPrincipalOutstanding), - .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding), - .periodicPayment = loan->at(sfPeriodicPayment), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - .interestRate = TenthBips32{loan->at(sfInterestRate)}, - }; - } - return LoanState{}; - } - - /** - * Get the state and check the values against the parameters used in - * `lifecycle` - */ - LoanState - getCurrentState( - jtx::Env const& env, - BrokerInfo const& broker, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) - { - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - auto const state = getCurrentState(env, broker, loanKeylet); - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max(broker.vaultScale(env), state.principalOutstanding.exponent()))); - BEAST_EXPECT(state.paymentInterval == 600); - { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - BEAST_EXPECT( - state.totalValue == - roundToAsset( - broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale)); - } - BEAST_EXPECT( - state.managementFeeOutstanding == - computeManagementFee( - broker.asset, - state.totalValue - state.principalOutstanding, - broker.params.managementFeeRate, - state.loanScale)); - - verifyLoanStatus(state); - - return state; - } - - bool - canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state) - { - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - BEAST_EXPECT(vaultSle)) - { - // log << vaultSle->getJson() << std::endl; - auto const assetsUnavailable = - vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); - auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + state.totalValue - - state.managementFeeOutstanding; - - if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) - { - return false; - } - } - } - return true; - } - - enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; - - // Specify the accounts as params to allow other accounts to be used - jtx::PrettyAsset - createAsset( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - switch (assetType) - { - case AssetType::XRP: - // TODO: remove the factor, and set up loans in drops - return PrettyAsset{xrpIssue(), 1'000'000}; - - case AssetType::IOU: { - PrettyAsset const asset{issuer[iouCurrency_]}; - - auto const limit = - asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit)); - if (lender != issuer) - env(trust(lender, limit)); - if (borrower != issuer) - env(trust(borrower, limit)); - - return asset; - } - - case AssetType::MPT: { - // Enough to cover initial fees - if (!env.le(keylet::account(issuer))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); - if (!env.le(keylet::account(lender))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); - if (!env.le(keylet::account(borrower))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset so interest is interesting - PrettyAsset const asset{mptt.issuanceID(), 10'000}; - // Need to do the authorization here because mptt isn't - // accessible outside - if (lender != issuer) - mptt.authorize({.account = lender}); - if (borrower != issuer) - mptt.authorize({.account = borrower}); - - env.close(); - - return asset; - } - - default: - throw std::runtime_error("Unknown asset type"); - } - } - - void - describeLoan( - jtx::Env& env, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - AssetType assetType, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); - auto const principal = asset(loanParams.principalRequest).number(); - auto const interest = loanParams.interest.value_or(TenthBips32{}); - auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval); - auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal); - auto const feeRate = brokerParams.managementFeeRate; - auto const props = computeLoanProperties( - env.current()->rules(), - asset, - principal, - interest, - interval, - total, - feeRate, - asset(brokerParams.vaultDeposit).number().exponent()); - log << "Loan properties:\n" - << "\tPrincipal: " << principal << std::endl - << "\tInterest rate: " << interest << std::endl - << "\tPayment interval: " << interval << std::endl - << "\tManagement Fee Rate: " << feeRate << std::endl - << "\tTotal Payments: " << total << std::endl - << "\tPeriodic Payment: " << props.periodicPayment << std::endl - << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl - << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl - << "\tLoan Scale: " << props.loanScale << std::endl - << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl; - - // checkGuards returns a TER, so success is 0 - BEAST_EXPECT(!checkLoanGuards( - asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}) != beast::kZero, - loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal), - props, - env.journal)); - } - - std::optional> - createLoan( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); - if (lender != issuer) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); - if (borrower != issuer && borrower != lender) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); - - describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); - - // Make the asset - auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); - - env.close(); - if (asset.native() || lender != issuer) - { - env( - pay((asset.native() ? env.master : issuer), - lender, - asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); - } - // Fund the borrower later once we know the total loan - // size - - BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams); - - auto const pseudoAcctOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - if (!pseudoAcctOpt) - return std::nullopt; - Account const& pseudoAcct = *pseudoAcctOpt; - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the - // _LOAN_BROKER_ object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); - }(); - if (!loanKeyletOpt) - return std::nullopt; - Keylet const& loanKeylet = *loanKeyletOpt; - - env(loanParams(env, broker)); - - env.close(); - - return std::make_tuple(broker, loanKeylet, pseudoAcct); - } - - static void - topUpBorrower( - jtx::Env& env, - BrokerInfo const& broker, - jtx::Account const& issuer, - jtx::Account const& borrower, - LoanState const& state, - std::optional const& servFee) - { - using namespace jtx; - - STAmount const serviceFee = broker.asset(servFee.value_or(0)); - - // Ensure the borrower has enough funds to make the payments - // (including tx fees, if necessary) - auto const borrowerBalance = env.balance(borrower, broker.asset); - - auto const baseFee = env.current()->fees().base; - - // Add extra for transaction fees and reserves, if appropriate, or a - // tiny amount for the extra paid in each transaction - auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + - (broker.asset.native() ? Number( - baseFee * state.paymentRemaining + - accountReserve(*env.current(), borrower.id(), env.journal)) - : broker.asset(15).number()); - - auto const shortage = totalNeeded - borrowerBalance.number(); - - if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower)) - { - env( - pay((broker.asset.native() ? env.master : issuer), - borrower, - STAmount{broker.asset, shortage})); - } - } - - void - makeLoanPayments( - jtx::Env& env, - BrokerInfo const& broker, - LoanParameters const& loanParams, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower, - PaymentParameters const& paymentParams = PaymentParameters::defaults()) - { - // Make all the individual payments - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - - bool const showStepBalances = paymentParams.showStepBalances; - - auto const currencyLabel = getCurrencyLabel(broker.asset); - - auto const baseFee = env.current()->fees().base; - - env.close(); - auto state = getCurrentState(env, broker, loanKeylet); - - verifyLoanStatus(state); - - STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0)); - - topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; - - if (!showStepBalances) - { - log << currencyLabel << " Payment components: " - << "Payments remaining, " - << "rawInterest, rawPrincipal, " - "rawMFee, " - << "trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special" - << std::endl; - } - - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward); - - auto currentRoundedState = constructLoanState( - state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); - { - auto const raw = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - if (showStepBalances) - { - log << currencyLabel << " Starting loan balances: " - << "\n\tTotal value: " << currentRoundedState.valueOutstanding - << "\n\tPrincipal: " << currentRoundedState.principalOutstanding - << "\n\tInterest: " << currentRoundedState.interestDue - << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue - << "\n\tPayments remaining " << state.paymentRemaining << std::endl; - } - else - { - log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", " - << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", " - << currentRoundedState.principalOutstanding << ", " - << currentRoundedState.interestDue << ", " - << currentRoundedState.managementFeeDue << std::endl; - } - } - - // Try to pay a little extra to show that it's _not_ - // taken - auto const extraAmount = paymentParams.overpaymentExtra - ? broker.asset(*paymentParams.overpaymentExtra).value() - : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20}); - - STAmount const transactionAmount = - STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount; - - auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number(); - auto const initialState = state; - xrpl::detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - Number totalFeesPaid = 0; - std::size_t totalPaymentsMade = 0; - - xrpl::LoanState currentTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - auto validateBorrowerBalance = [&]() { - if (borrower == issuer || !paymentParams.validateBalances) - return; - auto const totalSpent = - (totalPaid.trackedValueDelta + totalFeesPaid + - (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero)); - BEAST_EXPECT( - env.balance(borrower, broker.asset).number() == - borrowerInitialBalance - totalSpent); - }; - - auto const defaultRound = broker.asset.integral() ? 3 : 0; - auto truncate = [defaultRound](Number const& n, std::optional places = std::nullopt) { - auto const p = places.value_or(defaultRound); - if (p == 0) - return n; - auto const factor = Number{1, p}; - return (n * factor).truncate() / factor; - }; - while (state.paymentRemaining > 0) - { - validateBorrowerBalance(); - // Compute the expected principal amount - auto const paymentComponents = xrpl::detail::computePaymentComponents( - env.current()->rules(), - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= roundedPeriodicPayment || - (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final && - paymentComponents.trackedValueDelta >= roundedPeriodicPayment)); - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - - xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; - BEAST_EXPECT( - deltas.total() == deltas.principal + deltas.interest + deltas.managementFee); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - deltas.total() == state.periodicPayment || - (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14); - - if (!showStepBalances) - { - log << currencyLabel << " Payment components: " << state.paymentRemaining << ", " - - << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee - << ", " << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* { - if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final) - return "final"; - if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra) - return "extra"; - return "none"; - }() << std::endl; - } - - auto const totalDueAmount = - STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee}; - - if (paymentParams.validateBalances) - { - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be dust. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - diff == beast::kZero || - (diff > beast::kZero && - ((broker.asset.integral() && (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::kZero && - paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedPrincipalDelta == state.principalOutstanding); - } - - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags)); - - env.close(d{state.paymentInterval / 2}); - - if (paymentParams.validateBalances) - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - { - // No reason for this not to exist - return; - } - auto const current = constructLoanState(loanSle); - auto const errors = nextTrueState - current; - log << currencyLabel << " Loan balances: " - << "\n\tAmount taken: " << paymentComponents.trackedValueDelta - << "\n\tTotal value: " << current.valueOutstanding - << " (true: " << truncate(nextTrueState.valueOutstanding) - << ", error: " << truncate(errors.total()) - << ")\n\tPrincipal: " << current.principalOutstanding - << " (true: " << truncate(nextTrueState.principalOutstanding) - << ", error: " << truncate(errors.principal) - << ")\n\tInterest: " << current.interestDue - << " (true: " << truncate(nextTrueState.interestDue) - << ", error: " << truncate(errors.interest) - << ")\n\tMgmt fee: " << current.managementFeeDue - << " (true: " << truncate(nextTrueState.managementFeeDue) - << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " - << loanSle->at(sfPaymentRemaining) << std::endl; - - currentRoundedState = current; - } - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - if (paymentParams.validateBalances) - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - totalFeesPaid += serviceFee; - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - validateBorrowerBalance(); - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - auto const initialInterestDue = initialState.totalValue - - (initialState.principalOutstanding + initialState.managementFeeOutstanding); - if (paymentParams.validateBalances) - { - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT(totalInterestPaid == initialInterestDue); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - { - // No reason for this not to exist - return; - } - log << currencyLabel << " Total amounts paid: " - << "\n\tTotal value: " << totalPaid.trackedValueDelta - << " (initial: " << truncate(initialState.totalValue) - << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta) - << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta - << " (initial: " << truncate(initialState.principalOutstanding) << ", error: " - << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta) - << ")\n\tInterest: " << totalInterestPaid - << " (initial: " << truncate(initialInterestDue) - << ", error: " << truncate(initialInterestDue - totalInterestPaid) - << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta - << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: " - << truncate( - initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta) - << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl; - } - } - - void - runLoan( - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - FeatureBitset features) - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - Env env(*this, features); - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - } - - /** - * Runs through the complete lifecycle of a loan - * - * 1. Create a loan. - * 2. Test a bunch of transaction failure conditions. - * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done - * depends on the callback. e.g. Default, Early payoff, make all the - * normal payments, etc. - * 4. Delete the loan. The loan will alternate between being deleted by the - * lender and the borrower. - */ - void - lifecycle( - std::string const& caseLabel, - char const* label, - jtx::Env& env, - Number const& loanAmount, - int interestExponent, - jtx::Account const& lender, - jtx::Account const& borrower, - jtx::Account const& evan, - BrokerInfo const& broker, - jtx::Account const& pseudoAcct, - std::uint32_t flags, - // The end of life callback is expected to take the loan to 0 payments - // remaining, one way or another - std::function - toEndOfLife) - { - auto const [keylet, loanSequence] = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - { - // will be invalid - return std::make_pair(keylet::loan(broker.brokerID), std::uint32_t(0)); - } - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_ - // object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return std::make_pair(keylet::loan(broker.brokerID, loanSequence), loanSequence); - }(); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); - - // No loans yet - verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); - - if (!BEAST_EXPECT(loanSequence != 0)) - return; - - testcase << caseLabel << " " << label; - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - auto applyExponent = [interestExponent, this](TenthBips32 value) mutable { - BEAST_EXPECT(value > TenthBips32(0)); - while (interestExponent > 0) - { - auto const oldValue = value; - value *= 10; - --interestExponent; - BEAST_EXPECT(value / 10 == oldValue); - } - while (interestExponent < 0) - { - auto const oldValue = value; - value /= 10; - ++interestExponent; - BEAST_EXPECT(value * 10 == oldValue); - } - return value; - }; - - auto const borrowerOwnerCount = env.ownerCount(borrower); - - auto const loanSetFee = env.current()->fees().base * 2; - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .counterpartyExplicit = false, - .principalRequest = loanAmount, - .setFee = loanSetFee, - .originationFee = 1, - .serviceFee = 2, - .lateFee = 3, - .closeFee = 4, - .overFee = applyExponent(percentageToTenthBips(5) / 10), - .interest = applyExponent(percentageToTenthBips(12)), - // 2.4% - .lateInterest = applyExponent(percentageToTenthBips(24) / 10), - .closeInterest = applyExponent(percentageToTenthBips(36) / 10), - .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10), - .payTotal = 12, - .payInterval = 600, - .gracePd = 60, - .flags = flags, - }; - Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value(); - auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value(); - auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value(); - auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); - auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); - - auto const borrowerStartbalance = env.balance(borrower, broker.asset); - - auto createJtx = loanParams(env, broker); - // Successfully create a Loan - env(createJtx); - - env.close(); - - auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count(); - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); - } - - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = 2 * env.current()->fees().base; - } - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount - - adjustment.value()); - } - - auto const loanFlags = - createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); - - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // log << "loan after create: " << to_string(loan->getJson()) - // << std::endl; - BEAST_EXPECT( - loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment)); - BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); - BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); - BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); - BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount); - BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); - BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); - BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); - BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); - BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); - BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest); - BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest); - BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest); - BEAST_EXPECT(loan->at(sfStartDate) == startDate); - BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); - BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0); - BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); - BEAST_EXPECT( - loan->at(sfLoanScale) >= - (broker.asset.integral() - ? 0 - : std::max(broker.vaultScale(env), principalRequestAmount.exponent()))); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount); - } - - auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); - - auto const loanProperties = computeLoanProperties( - env.current()->rules(), - broker.asset.raw(), - state.principalOutstanding, - state.interestRate, - state.paymentInterval, - state.paymentRemaining, - broker.params.managementFeeRate, - state.loanScale); - - verifyLoanStatus( - 0, - startDate + *loanParams.payInterval, - *loanParams.payTotal, - state.loanScale, - loanProperties.loanState.valueOutstanding, - principalRequestAmount, - loanProperties.loanState.managementFeeDue, - loanProperties.periodicPayment, - loanFlags | 0); - - // Manage the loan - // no-op - env(manage(lender, keylet.key, 0)); - { - // no flags - auto jt = manage(lender, keylet.key, 0); - jt.removeMember(sfFlags.getName()); - env(jt); - } - // Only the lender can manage - env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION)); - // unknown flags - env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG)); - // combinations of flags are not allowed - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault), - Ter(temINVALID_FLAG)); - // invalid loan ID - env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY)); - // Loan is unimpaired, can't unimpair it again - env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); - // Loan is unimpaired, it can go into default, but only after it's past - // due - env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON)); - - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, keylet.key, tfLoanImpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); - // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); - - auto const nextDueDate = startDate + *loanParams.payInterval; - - env.close(); - - verifyLoanStatus( - 0, - nextDueDate, - *loanParams.payTotal, - loanProperties.loanScale, - loanProperties.loanState.valueOutstanding, - principalRequestAmount, - loanProperties.loanState.managementFeeDue, - loanProperties.periodicPayment, - loanFlags | 0); - - // Can't delete the loan yet. It has payments remaining. - env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS)); - - if (BEAST_EXPECT(toEndOfLife)) - toEndOfLife(keylet, verifyLoanStatus); - env.close(); - - // Verify the loan is at EOL - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); - } - auto const borrowerStartingBalance = env.balance(borrower, broker.asset); - - // Try to delete the loan broker with an active loan - env(loanBroker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); - // Ensure the above tx doesn't get ordered after the LoanDelete and - // delete our broker! - env.close(); - - // Test failure cases - env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG)); - env(del(evan, keylet.key), Ter(tecNO_PERMISSION)); - env(del(lender, broker.brokerID), Ter(tecNO_ENTRY)); - - // Delete the loan - // Either the borrower or the lender can delete the loan. Alternate - // between who does it across tests. - static unsigned kDeleteCounter = 0; - auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower; - env(del(deleter, keylet.key)); - env.close(); - - PrettyAmount adjustment = broker.asset(0); - if (deleter == borrower) - { - // Need to account for fees if the loan is in XRP - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - } - - // No loans left - verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartingBalance.value() - adjustment); - BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - } - } - - static std::string - getCurrencyLabel(Asset const& asset) - { - if (asset.native()) - return "XRP"; - if (asset.holds()) - return "IOU"; - if (asset.holds()) - return "MPT"; - return "Unknown"; - } - - /** - * Wrapper to run a series of lifecycle tests for a given asset and loan - * amount - * - * Will be used in the future to vary the loan parameters. For now, it is - * only called once. - * - * Tests a bunch of LoanSet failure conditions before lifecycle. - */ - template - void - testCaseWrapper( - jtx::Env& env, - jtx::MPTTester& mptt, - std::array const& assets, - BrokerInfo const& broker, - Number const& loanAmount, - int interestExponent) - { - using namespace jtx; - using namespace Lending; - - auto const& asset = broker.asset.raw(); - auto const currencyLabel = getCurrencyLabel(asset); - auto const caseLabel = [&]() { - std::stringstream ss; - ss << "Lifecycle: " << loanAmount << " " << currencyLabel - << " Scale interest to: " << interestExponent << " "; - return ss.str(); - }(); - testcase << caseLabel; - - using namespace loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - Number const principalRequest = broker.asset(loanAmount).value(); - Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); - BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); - Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value(); - Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value(); - Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value(); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return Account{lender}; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - auto const baseFee = env.current()->fees().base; - - auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); - // Try some failure cases - // flags are checked first - env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(temINVALID_FLAG)); - - // field length validation - // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kData(std::string(kMaxDataPayloadLength, 'X')), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfData: too long - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kData(std::string(kMaxDataPayloadLength + 1, 'Y')), - loanSetFee, - Ter(temINVALID)); - - // field range validation - // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentFee(kMaxOverpaymentFee), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentFee(kMaxOverpaymentFee + 1), - loanSetFee, - Ter(temINVALID)); - - // sfInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kInterestRate(kMaxInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(kMaxInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - // sfInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kLateInterestRate(kMaxLateInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kLateInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kLateInterestRate(kMaxLateInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - // sfLateInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kLateInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kCloseInterestRate(kMaxCloseInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kCloseInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kCloseInterestRate(kMaxCloseInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kCloseInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentInterestRate(kMaxOverpaymentInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentTotal(LoanSet::kMinPaymentTotal), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentTotal(LoanSet::kMinPaymentTotal - 1), - loanSetFee, - Ter(temINVALID)); - - // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentInterval(LoanSet::kMinPaymentInterval), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(LoanSet::kMinPaymentInterval - 1), - loanSetFee, - Ter(temINVALID)); - - // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentInterval(LoanSet::kMinPaymentInterval * 2), - kGracePeriod(LoanSet::kMinPaymentInterval * 2), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(LoanSet::kMinPaymentInterval * 2), - kGracePeriod(LoanSet::kMinPaymentInterval * 3), - loanSetFee, - Ter(temINVALID)); - - // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - Ter(telINSUF_FEE_P)); - // insufficient fee - multisign - env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); - env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5 - 1), - Ter(telINSUF_FEE_P)); - // Bad multisign signatures for borrower (Account) - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(alice, issuer), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5), - Ter(tefBAD_SIGNATURE)); - // Bad multisign signatures for issuer (Counterparty) - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, alice, issuer), - Fee(env.current()->fees().base * 5 - 1), - Ter(tefBAD_SIGNATURE)); - env(signers(lender, kNone)); - env(signers(borrower, kNone)); - // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5), - Ter(tefNOT_MULTI_SIGNING)); - // not the broker owner, no counterparty, not signed by broker - // owner - env(set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, evan), - loanSetFee, - Ter(tefBAD_AUTH)); - // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - loanSetFee, - Ter(tecNO_PERMISSION)); - // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest), - Sig(sfCounterpartySignature, evan), - loanSetFee, - Ter(temBAD_SIGNER)); - // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - loanSetFee, - Ter(tecNO_ENTRY)); - // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(alice), - Sig(sfCounterpartySignature, alice), - loanSetFee, - Ter(terNO_ACCOUNT)); - - // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_FUNDS)); - - // Request more funds than the broker's first-loss capital can - // cover. - env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_FUNDS)); - - // Frozen trust line / locked MPT issuance - // XRP can not be frozen, but run through the loop anyway to test - // the tecLIMIT_EXCEEDED case - { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return; - - auto const vaultPseudo = [&]() { - auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - if (!BEAST_EXPECT(vaultSle)) - { - // This will be wrong, but the test has failed anyway. - return Account{lender}; - } - auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount)); - return vaultPseudo; - }(); - - auto const [freeze, deepfreeze, unfreeze, expectedResult] = - [&]() -> std::tuple< - std::function, - std::function, - std::function, - TER> { - // Freeze / lock the asset - std::function const empty; - if (broker.asset.native()) - { - // XRP can't be frozen - return std::make_tuple(empty, empty, empty, tesSUCCESS); - } - if (broker.asset.holds()) - { - auto freeze = [&](Account const& holder) { - env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze)); - }; - auto deepfreeze = [&](Account const& holder) { - env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze)); - }; - auto unfreeze = [&](Account const& holder) { - env(trust( - issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze)); - }; - return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN); - } - - auto freeze = [&](Account const& holder) { - mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock}); - }; - auto unfreeze = [&](Account const& holder) { - mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock}); - }; - return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); - }(); - - // Try freezing the accounts that can't be frozen - if (freeze) - { - for (auto const& account : {vaultPseudo, evan}) - { - // Freeze the account - freeze(account); - - // Try to create a loan with a frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(expectedResult)); - - // Unfreeze the account - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecLIMIT_EXCEEDED)); - } - } - - // Deep freeze the borrower, which prevents them from receiving - // funds - if (deepfreeze) - { - // Make sure evan has a trust line that so the issuer can - // freeze it. (Don't need to do this for the borrower, - // because LoanSet will create a line to the borrower - // automatically.) - env(trust(evan, issuer[iouCurrency_](100'000))); - - for (auto const& account : {// these accounts can't be frozen, which deep freeze - // implies - vaultPseudo, - evan, - // these accounts can't be deep frozen - lender}) - { - // Freeze evan - deepfreeze(account); - - // Try to create a loan with a deep frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(expectedResult)); - - // Unfreeze evan - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecLIMIT_EXCEEDED)); - } - } - } - - // Finally! Create a loan - - auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) { - if (auto const brokerSle = env.le(keylet::loanBroker(brokerID)); - BEAST_EXPECT(brokerSle)) - { - auto const available = brokerSle->at(sfCoverAvailable); - BEAST_EXPECT(available == expected); - return available; - } - return Number{}; - }; - auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) { - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max( - broker.vaultScale(env), state.principalOutstanding.exponent()))); - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - auto const defaultAmount = roundToAsset( - broker.asset, - std::min( - tenthBipsOfValue( - tenthBipsOfValue( - brokerSle->at(sfDebtTotal), broker.params.coverRateMin), - broker.params.coverRateLiquidation), - state.totalValue - state.managementFeeOutstanding), - state.loanScale); - return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); - } - return std::make_pair(Number{}, AccountID{}); - }; - auto replenishCover = [&env, &coverAvailable]( - BrokerInfo const& broker, - AccountID const& brokerAcct, - Number const& startingCoverAvailable, - Number const& amountToBeCovered) { - coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered); - env(loanBroker::coverDeposit( - brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered})); - coverAvailable(broker.brokerID, startingCoverAvailable); - env.close(); - }; - - auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) { - return [&, impair, baseFlag]( - Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Default the loan - - // Initialize values with the current state - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - - auto const& broker = verifyLoanStatus.broker; - auto const startingCoverAvailable = coverAvailable( - broker.brokerID, broker.asset(broker.params.coverDeposit).number()); - - if (impair) - { - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, loanKeylet.key, tfLoanImpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); - - if (canImpair) - { - state.flags |= tfLoanImpair; - state.nextPaymentDate = env.now().time_since_epoch().count(); - - // Once the loan is impaired, it can't be impaired again - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - } - verifyLoanStatus(state); - } - - auto const nextDueDate = tp{d{state.nextPaymentDate}}; - - // Can't default the loan yet. The grace period hasn't - // expired - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON)); - - // Let some time pass so that the loan can be - // defaulted - env.close(nextDueDate + 60s); - - auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); - - // Default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // The LoanBroker just lost some of it's first-loss capital. - // Replenish it. - replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered); - - state.flags |= tfLoanDefault; - state.paymentRemaining = 0; - state.totalValue = 0; - state.principalOutstanding = 0; - state.managementFeeOutstanding = 0; - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - // Once a loan is defaulted, it can't be managed - env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - // Can't make a payment on it either - env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED)); - }; - }; - - auto singlePayment = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - LoanState& state, - STAmount const& payoffAmount, - std::uint32_t numPayments, - std::uint32_t baseFlag, - std::uint32_t txFlags) { - // toEndOfLife - // - verifyLoanStatus(state); - - // Send some bogus pay transactions - env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags), - Ter(temINVALID)); - // broker.asset(80) is less than a single payment, but all these - // checks fail before that matters - env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION)); - - // TODO: Write a general "isFlag" function? See STObject::isFlag. - // Maybe add a static overloaded member? - if (!(state.flags & lsfLoanOverpayment)) - { - // If the loan does not allow overpayments, send a payment that - // tries to make an overpayment. Do not include `txFlags`, so we - // don't end up duplicating the next test transaction. - // - // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag: - // with fix on → preflight passes, apply returns tecNO_PERMISSION; - // with fix off → preflight rejects the flag, returns temINVALID_FLAG. - bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3); - STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}}; - XRPAmount const overpayFee{ - baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)}; - env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), - Fee(overpayFee), - Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG})); - - if (hasFix313) - { - env.disableFeature(fixCleanup3_1_3); - env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), - Fee(overpayFee), - Ter(temINVALID_FLAG)); - env.enableFeature(fixCleanup3_1_3); - } - } - // Try to send a payment marked as multiple mutually exclusive - // payment types. Do not include `txFlags`, so we don't duplicate - // the prior test transaction. - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanOverpayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET)); - } - - // Amount doesn't cover a single payment - env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags), - Ter(tecINSUFFICIENT_PAYMENT)); - - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - - // Send a transaction that tries to pay more than the borrowers's - // balance - XRPAmount const badFee{ - baseFee * - (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment / - kLoanPaymentsPerFeeIncrement + - 1)}; - env(pay(borrower, - loanKeylet.key, - STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2}, - txFlags), - Fee(badFee), - Ter(tecINSUFFICIENT_FUNDS)); - - XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; - env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee)); - - env.close(); - - // log << env.meta()->getJson() << std::endl; - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = badFee + goodFee; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.totalValue = 0; - state.managementFeeOutstanding = 0; - state.previousPaymentDate = - state.nextPaymentDate + (state.paymentInterval * (numPayments - 1)); - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - verifyLoanStatus.checkPayment( - state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); - }; - - auto fullPayment = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( - Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Full payoff amount will consist of - // 1. principal outstanding (1000) - // 2. accrued interest (at 12%) - // 3. prepayment penalty (closeInterest at 3.6%) - // 4. close payment fee (4) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; - BEAST_EXPECT( - periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{})); - STAmount const principalOutstanding{broker.asset, state.principalOutstanding}; - STAmount const accruedInterest{ - broker.asset, state.principalOutstanding * periodicRate * loanAge / interval}; - BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19))); - STAmount const prepaymentPenalty{ - broker.asset, state.principalOutstanding * Number(36, -3)}; - BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); - STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = roundToScale( - principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee, - state.loanScale); - BEAST_EXPECT( - payoffAmount == - roundToAsset( - broker.asset, - broker.asset(Number(1040000114155251, -12)).number(), - state.loanScale)); - - // The terms of this loan actually make the early payoff - // more expensive than just making payments - BEAST_EXPECT( - payoffAmount > - state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - 1, - baseFlag, - tfLoanFullPayment); - }; - }; - - auto combineAllPayments = [&](std::uint32_t baseFlag) { - return - [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(); - - BEAST_EXPECT( - STAmount(broker.asset, state.periodicPayment) == - broker.asset(Number(8333457002039338267, -17))); - - // Make all the payments in one transaction - // service fee is 2 - auto const startingPayments = state.paymentRemaining; - STAmount const payoffAmount = [&]() { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - auto const rawPayoff = - startingPayments * (state.periodicPayment + broker.asset(2).value()); - STAmount payoffAmount{broker.asset, rawPayoff}; - BEAST_EXPECTS( - payoffAmount == broker.asset(Number(1024014840244721, -12)), - to_string(payoffAmount)); - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - - payoffAmount = roundToScale(payoffAmount, state.loanScale); - - return payoffAmount; - }(); - - auto const totalPayoffValue = - state.totalValue + startingPayments * broker.asset(2).value(); - STAmount const totalPayoffAmount{broker.asset, totalPayoffValue}; - - BEAST_EXPECTS( - totalPayoffAmount == payoffAmount, - "Payoff amount: " + to_string(payoffAmount) + - ". Total Value: " + to_string(totalPayoffAmount)); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - state.paymentRemaining, - baseFlag, - 0); - }; - }; - - // There are a lot of fields that can be set on a loan, but most - // of them only affect the "math" when a payment is made. The - // only one that really affects behavior is the - // `tfLoanOverpayment` flag. - lifecycle( - caseLabel, - "Loan overpayment allowed - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - fullPayment(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - fullPayment(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - combineAllPayments(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - combineAllPayments(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Make payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Draw and make multiple payments - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == 0); - env.close(); - - verifyLoanStatus(state); - - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; - BEAST_EXPECT( - periodicRate == Number(2283105022831050228, -24, Number::Normalized{})); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; - - testcase << currencyLabel << " Payment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special"; - - auto const serviceFee = broker.asset(2); - - BEAST_EXPECT( - roundedPeriodicPayment == - roundToScale( - broker.asset( - Number(8333457002039338267, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - // 83334570.01162141 - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::RoundingMode::Upward); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - totalDue == - roundToScale( - broker.asset( - Number(8533457002039338267, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - - { - auto const raw = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - auto const rounded = constructLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining - << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " - << rounded.principalOutstanding << ", " << rounded.interestDue << ", " - << rounded.managementFeeDue; - } - - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = - STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - transactionAmount == - roundToScale( - broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - - auto const initialState = state; - xrpl::detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, - .trackedPrincipalDelta = 0, - .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - std::size_t totalPaymentsMade = 0; - - xrpl::LoanState currentTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - while (state.paymentRemaining > 0) - { - // Compute the expected principal amount - auto const paymentComponents = xrpl::detail::computePaymentComponents( - env.current()->rules(), - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECTS( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedValueDelta <= roundedPeriodicPayment, - "Delta: " + to_string(paymentComponents.trackedValueDelta) + - ", periodic payment: " + to_string(roundedPeriodicPayment)); - - xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; - - testcase << currencyLabel << " Payment components: " << state.paymentRemaining - << ", " << deltas.interest << ", " << deltas.principal << ", " - << deltas.managementFee << ", " << paymentComponents.trackedValueDelta - << ", " << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << [&]() -> char const* { - if (paymentComponents.specialCase == - ::xrpl::detail::PaymentSpecialCase::Final) - return "final"; - if (paymentComponents.specialCase == - ::xrpl::detail::PaymentSpecialCase::Extra) - return "extra"; - return "none"; - }(); - - auto const totalDueAmount = STAmount{ - broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()}; - - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be after the 8th digit. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - diff == beast::kZero || - (diff > beast::kZero && - ((broker.asset.integral() && (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + - paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedValueDelta <= roundedPeriodicPayment); - - BEAST_EXPECT( - state.paymentRemaining < 12 || - roundToAsset( - broker.asset, - deltas.principal, - state.loanScale, - Number::RoundingMode::Upward) == - roundToScale( - broker.asset( - Number(8333228691531218890, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::kZero && - paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedPrincipalDelta == state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - (state.periodicPayment.exponent() - - (deltas.principal + deltas.interest + deltas.managementFee - - state.periodicPayment) - .exponent()) > 14); - - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - if (canImpairLoan(env, broker, state)) - { - // Making a payment will unimpair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - } - - env.close(); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += - paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT( - totalInterestPaid == - initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding)); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); - }); - -#if LOAN_TODO - // TODO - - /* - LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also - borrower) tries to do the payment. Here's the scenario: Create a XRP - loan with loan broker as borrower, loan origination fee and loan service - fee. Loan broker makes the first payment with periodic payment and loan - service fee. - */ - - auto time = [&](std::string label, std::function timed) { - if (!BEAST_EXPECT(timed)) - return; - - using clock_type = std::chrono::steady_clock; - using duration_type = std::chrono::milliseconds; - - auto const start = clock_type::now(); - timed(); - auto const duration = - std::chrono::duration_cast(clock_type::now() - start); - - log << label << " took " << duration.count() << "ms" << std::endl; - - return duration; - }; - - lifecycle( - caseLabel, - "timing", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // Estimate optimal values for kLoanPaymentsPerFeeIncrement and - // kLoanMaximumPaymentsPerTransaction. - using namespace loan; - - auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet); - auto const serviceFee = broker.asset(2).value(); - - STAmount const totalDue{ - broker.asset, - roundPeriodicPayment( - broker.asset, state.periodicPayment + serviceFee, state.loanScale)}; - - // Make a single payment - time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); }); - env.close(); - - // Make all but the final payment - auto const numPayments = (state.paymentRemaining - 2); - STAmount const bigPayment{broker.asset, totalDue * numPayments}; - XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; - time("ten payments", [&]() { - env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee)); - }); - env.close(); - - time("final payment", [&]() { - // Make the final payment - env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1})); - }); - env.close(); - }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Explicit overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment and overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - -#endif - } - - void - testLoanSet(FeatureBitset features) - { - using namespace jtx; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - struct CaseArgs - { - bool requireAuth = false; - bool authorizeBorrower = false; - int initialXRP = 1'000'000; - }; - - auto const testCase = [&, this]( - std::function mptTest, - std::function iouTest, - CaseArgs args = {}) { - Env env(*this, features); - env.fund(XRP(args.initialXRP), issuer, lender, borrower); - env.close(); - if (args.requireAuth) - { - env(fset(issuer, asfRequireAuth)); - env.close(); - } - - // We need two different asset types, MPT and IOU. Prepare MPT - // first - MPTTester mptt{env, issuer, kMptInitNoFund}; - - auto const kNone = LedgerSpecificFlags(0); - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock | - (args.requireAuth ? tfMPTRequireAuth : kNone)}); - env.close(); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env.close(); - if (args.requireAuth) - { - mptt.authorize({.account = issuer, .holder = lender}); - if (args.authorizeBorrower) - mptt.authorize({.account = issuer, .holder = borrower}); - env.close(); - } - - env(pay(issuer, lender, mptAsset(10'000'000))); - env.close(); - - // Prepare IOU - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env.close(); - if (args.requireAuth) - { - env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); - env(pay(issuer, lender, iouAsset(10'000'000))); - if (args.authorizeBorrower) - { - env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); - env(pay(issuer, borrower, iouAsset(10'000))); - } - } - else - { - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(10'000))); - } - env.close(); - - // Create vaults and loan brokers - std::array const assets{mptAsset, iouAsset}; - std::vector brokers; - brokers.reserve(assets.size()); - for (auto const& asset : assets) - { - brokers.emplace_back(createVaultAndBroker(env, asset, lender)); - } - - if (mptTest) - mptTest(env, brokers[0], mptt); - if (iouTest) - iouTest(env, brokers[1]); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - - testcase("MPT issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(issuer), - Sig(sfCounterpartySignature, issuer), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - - testcase("IOU issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(issuer), - Sig(sfCounterpartySignature, issuer), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - - testcase("MPT unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - - testcase("IOU unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - }, - CaseArgs{.requireAuth = true}); - - auto const [acctReserve, incReserve] = [this]() -> std::pair { - Env const env{*this, testableAmendments()}; - return { - env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), - env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; - }(); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, borrower has " - "no reserve"); - mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize}); - env.close(); - - auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 == nullptr); - - // Burn some XRP - env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 != nullptr); - }, - {}, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, borrower has " - "no reserve"); - // Remove trust line from borrower to issuer - env.trust(broker.asset(0), borrower); - env.close(); - - env(pay(borrower, issuer, broker.asset(10'000))); - env.close(); - auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 == nullptr); - - // Burn some XRP - env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 != nullptr); - }, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender has " - "no reserve"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Burn some XRP - env(noop(lender), Fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 != nullptr); - }, - {}, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender has no " - "reserve"); - // Remove trust line from lender to issuer - env.trust(broker.asset(0), lender); - env.close(); - - auto const trustline = keylet::trustLine(lender, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 != nullptr); - - env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value())))); - env.close(); - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 == nullptr); - - // Burn some XRP - env(noop(lender), Fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine3 = env.le(trustline); - BEAST_EXPECT(sleLine3 != nullptr); - }, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, unauthorized lender"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Cannot create loan, lender not authorized to receive fee - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - env.close(); - - // Cannot create loan, even without an origination fee - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - env.close(); - - // No MPToken for lender - no authorization and no payment - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 == nullptr); - }, - {}, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - jtx::Account const alice{"alice"}; - jtx::Account const bella{"bella"}; - auto const msigSetup = [&](Env& env, Account const& account) { - json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); - env(tx1); - env.close(); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - Vault const vault{env}; - auto tx = vault.set({.owner = lender, .id = broker.vaultID}); - tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit; - env(tx); - env.close(); - - testcase("Vault at maximum value"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - kInterestRate(TenthBips32(10'000)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter(tecLIMIT_EXCEEDED)); - }, - nullptr); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - Vault const vault{env}; - auto tx = vault.set({.owner = lender, .id = broker.vaultID}); - tx[sfAssetsMaximum] = - BrokerParameters::defaults().vaultDeposit + broker.asset(1).number(); - env(tx); - env.close(); - - testcase("Vault maximum value exceeded"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - kInterestRate(TenthBips32(100'000)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - kPaymentTotal(2), - kPaymentInterval(3600 * 24), - Ter(tecLIMIT_EXCEEDED)); - }, - nullptr); - } - - void - testLifecycle(FeatureBitset features) - { - testcase("Lifecycle"); - using namespace jtx; - - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, features); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); - env.close(); - - // Create assets - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env(trust(evan, iouAsset(10'000'000))); - env(pay(issuer, evan, iouAsset(1'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, iouAsset(10'000))); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset a little bit so we can get some interest - PrettyAsset const mptAsset{mptt.issuanceID(), 100}; - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - mptt.authorize({.account = evan}); - env(pay(issuer, lender, mptAsset(10'000'000))); - env(pay(issuer, evan, mptAsset(1'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, mptAsset(10'000))); - env.close(); - - std::array const assets{iouAsset, xrpAsset, mptAsset}; - - // Create vaults and loan brokers - std::vector brokers; - brokers.reserve(assets.size()); - for (auto const& asset : assets) - { - brokers.emplace_back(createVaultAndBroker( - env, asset, lender, BrokerParameters{.data = "spam spam spam spam"})); - } - - // Create and update Loans - for (auto const& broker : brokers) - { - for (int amountExponent = 3; amountExponent >= 3; --amountExponent) - { - Number const loanAmount{1, amountExponent}; - for (int interestExponent = 0; interestExponent >= 0; --interestExponent) - { - testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent); - } - } - - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); - - auto const coverAvailable = brokerSle->at(sfCoverAvailable); - env(loanBroker::coverWithdraw( - lender, broker.brokerID, STAmount(broker.asset, coverAvailable))); - env.close(); - - brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); - } - // Verify we can delete the loan broker - env(loanBroker::del(lender, broker.brokerID)); - env.close(); - } - } - - void - testSelfLoan(FeatureBitset features) - { - testcase << "Self Loan"; - - using namespace jtx; - using namespace std::chrono_literals; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, features); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender)); - env.close(); - - // Use an XRP asset for simplicity - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - // Create vaults and loan brokers - BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - // The LoanSet json can be created without a counterparty signature, - // but it will not pass preflight - auto createJson = env.json( - set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee)); - env(createJson, Ter(temBAD_SIGNER)); - - // Adding an empty counterparty signature object also fails, but - // at the RPC level. - createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object)); - env(createJson, Ter(telENV_RPC_FAILED)); - - if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) - { - Serializer s; - jt.stx->add(s); - auto const jr = env.rpc("submit", strHex(s.slice())); - - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); - BEAST_EXPECT( - jResult[jss::error_exception] == - "fails local checks: Transaction has bad signature."); - } - - // Copy the transaction signature into the counterparty signature. - json::Value counterpartyJson{json::ValueType::Object}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - // The duplicated signature works - createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - - auto const startDate = env.current()->header().parentCloseTime; - - // Loan is successfully created - { - auto const res = env.rpc("account_objects", lender.human()); - auto const objects = res[jss::result][jss::account_objects]; - - std::map types; - BEAST_EXPECT(objects.size() == 4); - for (auto const& object : objects) - { - ++types[object[sfLedgerEntryType].asString()]; - } - BEAST_EXPECT(types.size() == 4); - for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"}) - { - BEAST_EXPECT(types[type] == 1); - } - } - auto const loanID = [&]() { - json::Value params(json::ValueType::Object); - params[jss::account] = lender.human(); - params[jss::type] = "Loan"; - auto const res = env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - - BEAST_EXPECT(objects.size() == 1); - - auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfBorrower] == lender.human()); - // soeDEFAULT fields are not returned if they're in the default - // state - BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); - BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); - BEAST_EXPECT(loan[sfFlags] == 0); - BEAST_EXPECT(loan[sfGracePeriod] == 60); - BEAST_EXPECT(!loan.isMember(sfInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); - BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); - BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); - BEAST_EXPECT(loan[sfLoanSequence] == 1); - BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); - BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); - BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); - BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); - BEAST_EXPECT(loan[sfPaymentInterval] == 60); - BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); - BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate)); - BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); - BEAST_EXPECT(!loan.isMember(sfLoanScale)); - BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); - - return loan["index"].asString(); - }(); - auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; - - env.close(startDate); - - // Make a payment - env(pay(lender, loanKeylet.key, broker.asset(1000))); - } - - void - testBatchBypassCounterparty(FeatureBitset features) - { - // From FIND-001 - testcase << "Batch Bypass Counterparty"; - - bool const lendingBatchEnabled = !std::ranges::any_of( - Batch::kDisabledTxTypes, - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters const brokerParams; - env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0); - - json::Value randomData{json::ValueType::Object}; - randomData[jss::SigningPubKey] = json::StaticString{"2600"}; - json::Value sigObject{json::ValueType::Object}; - sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); - Serializer ss; - ss.add32(HashPrefix::TxSign); - parse(randomData).addWithoutSigningFields(ss); - auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice()); - sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); - - forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject; - - // ? Fails because the lender hasn't signed the tx - env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED)); - - auto const seq = env.seq(borrower); - auto const batchFee = batch::calcBatchFee(env, 1, 2); - // ! Should fail because the lender hasn't signed the tx - env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), - batch::Inner(forgedLoanSet, seq + 1), - batch::Inner(pay(borrower, lender, XRP(1)), seq + 2), - Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH)); - env.close(); - - // ? Check that the loan was NOT created - { - json::Value params(json::ValueType::Object); - params[jss::account] = borrower.human(); - params[jss::type] = "Loan"; - auto const res = env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - BEAST_EXPECT(objects.size() == 0); - } - } - - void - testWrongMaxDebtBehavior(FeatureBitset features) - { - // From FIND-003 - testcase << "Wrong Max Debt Behavior"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - - BrokerParameters const brokerParams{.debtMax = 0}; - env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); - } - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee)); - - json::Value counterpartyJson{json::ValueType::Object}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - } - - void - testLoanPayComputePeriodicPaymentValidRateInvariant(FeatureBitset features) - { - // From FIND-012 - testcase << "LoanPay xrpl::detail::computePeriodicPayment : " - "valid rate"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters const brokerParams; - env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{640562, -5}; - - Number const serviceFee{2462611968}; - std::uint32_t const numPayments{4294967295 / 800}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - kLoanServiceFee(serviceFee), - kPaymentTotal(numPayments), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 55374; - createJson["ClosePaymentFee"] = "3825205248"; - createJson["LatePaymentFee"] = "237"; - createJson["LoanOriginationFee"] = "0"; - createJson["OverpaymentFee"] = 35167; - createJson["OverpaymentInterestRate"] = 1360; - createJson["PaymentInterval"] = 727; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - // Fails in preclaim because principal requested can't be - // represented as XRP - env(createJson, Ter(tecPRECISION_LOSS)); - env.close(); - - BEAST_EXPECT(!env.le(keylet)); - - Number const actualPrincipal{6}; - - createJson[sfPrincipalRequested] = actualPrincipal; - createJson.removeMember(sfSequence.jsonName); - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - // Fails in doApply because the payment is too small to be - // represented as XRP. - env(createJson, Ter(tecPRECISION_LOSS)); - env.close(); - } - - void - testRPC(FeatureBitset features) - { - // This will expand as more test cases are added. Some functionality - // is tested in other test functions. - testcase("RPC"); - - using namespace jtx; - - Env env(*this, features); - - auto lowerFee = [&]() { - // Run the local fee back down. - while (env.app().getFeeTrack().lowerLocalFee()) - ; - }; - - auto const baseFee = env.current()->fees().base; - - Account const alice{"alice"}; - std::string const borrowerPass = "borrower"; - Account const borrower{borrowerPass, KeyType::Ed25519}; - auto const lenderPass = "lender"; - Account const lender{lenderPass, KeyType::Ed25519}; - - env.fund(XRP(1'000'000), alice, lender, borrower); - env.close(); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env.close(); - - { - testcase("RPC AccountSet"); - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const signParams = [&]() { - json::Value signParams{json::ValueType::Object}; - signParams[jss::passphrase] = borrowerPass; - signParams[jss::key_type] = "ed25519"; - signParams[jss::tx_json] = txJson; - return signParams; - }(); - auto const jSign = env.rpc("json", "sign", to_string(signParams)); - BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json)); - auto txSignResult = jSign[jss::result][jss::tx_json]; - auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); - txSignResult.removeMember(jss::hash); - - auto const jtx = env.jt(txJson, Sig(borrower)); - BEAST_EXPECT(txSignResult == jtx.jv); - - lowerFee(); - auto const jSubmit = env.rpc("submit", txSignBlob); - BEAST_EXPECT( - jSubmit.isMember(jss::result) && - jSubmit[jss::result].isMember(jss::engine_result) && - jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); - - lowerFee(); - env(jtx.jv, Sig(kNone), Seq(kNone), Fee(kNone), Ter(tefPAST_SEQ)); - } - - { - testcase("RPC LoanSet - illegal signature_target"); - - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "Destination"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::error) && - jSignBorrower[jss::result][jss::error] == "invalidParams" && - jSignBorrower[jss::result].isMember(jss::error_message) && - jSignBorrower[jss::result][jss::error_message] == "Destination"); - } - { - testcase("RPC LoanSet - sign and submit borrower initiated"); - // 1. Borrower creates the transaction - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = borrower.human(); - txJson[sfCounterparty] = lender.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECTS( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json), - to_string(jSignBorrower)); - auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 2a. Borrower attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); - } - - // 3. Borrower sends the signed transaction to the lender - // 4. Lender signs the transaction - auto const lenderSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txBorrowerSignResult; - return params; - }(); - auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - - // 5. Lender submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Lender submits the transaction json originally - // received from the Borrower. It gets signed, but is now a - // duplicate, so fails. Borrower could done this instead of - // steps 4 and 5. - lowerFee(); - auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - - { - testcase("RPC LoanSet - sign and submit lender initiated"); - // 1. Lender creates the transaction - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = lender.human(); - txJson[sfCounterparty] = borrower.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Lender signs the transaction - auto const lenderSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - - // 2a. Lender attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); - } - - // 3. Lender sends the signed transaction to the Borrower - // 4. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txLenderSignResult; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json)); - auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 5. Borrower submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Borrower submits the transaction json originally - // received from the Lender. It gets signed, but is now a - // duplicate, so fails. Lender could done this instead of steps - // 4 and 5. - lowerFee(); - auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - } - - void - testServiceFeeOnBrokerDeepFreeze() - { - testcase << "Service Fee On Broker Deep Freeze"; - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - auto const iou = issuer["IOU"]; - - for (bool const deepFreeze : {true, false}) - { - Env env(*this); - - auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) { - if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(le)) - { - auto const account = le->at(accountField); - if (auto const sleLine = env.le(keylet::trustLine(account, iou)); - BEAST_EXPECT(sleLine)) - { - STAmount balance = sleLine->at(sfBalance); - if (account > issuer.id()) - balance.negate(); - return balance; - } - } - return STAmount{iou}; - }; - - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - env(trust(broker, iou(20'000'000))); - env(pay(issuer, broker, iou(10'000'000))); - env.close(); - - auto const brokerInfo = createVaultAndBroker(env, iou, broker); - - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); - - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(iou(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - - env(trust(borrower, iou(20'000'000))); - // The borrower increases their limit and acquires some IOU so - // they can pay interest - env(pay(issuer, borrower, iou(500))); - env.close(); - - if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) - { - if (deepFreeze) - { - env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze)); - env.close(); - } - - env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100))); - env.close(); - - if (deepFreeze) - { - // The fee goes to the broker pseudo-account - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100)); - BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000)); - } - else - { - // The fee goes to the broker account - BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100)); - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); - } - } - }; - } - - void - testIssuerLoan() - { - testcase << "Issuer Loan"; - - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower = issuer; - Account const lender("lender"); - Env env(*this); - - env.fund(XRP(1'000), issuer, lender); - - static constexpr std::int64_t kIssuerBalance = 10'000'000; - MPTTester const asset( - {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance}); - - BrokerParameters const brokerParams{ - .debtMax = 200, - }; - auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); - auto const loanSetFee = Fee(env.current()->fees().base * 2); - // Create Loan - env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee); - env.close(); - // Issuer should not create MPToken - BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); - // Issuer "borrowed" 200, OutstandingAmount decreased by 200 - BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance + 200)); - // Pay Loan - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - env(pay(borrower, loanKeylet.key, asset(200))); - env.close(); - // Issuer "re-payed" 200, OutstandingAmount increased by 200 - BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance)); - } - - void - testInvalidLoanDelete() - { - testcase("Invalid LoanDelete"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(del(alice, beast::kZero), Ter(temINVALID)); - } - } - - void - testInvalidLoanManage() - { - testcase("Invalid LoanManage"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(manage(alice, beast::kZero, tfLoanDefault), Ter(temINVALID)); - } - } - - void - testInvalidLoanPay() - { - testcase("Invalid LoanPay"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - auto const iou = issuer["IOU"]; - - // preclaim - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); - env(trust(lender, iou(10'000'000))); - env(pay(issuer, lender, iou(5'000'000))); - BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee); - - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); - - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - // preclaim: tecFROZEN - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); - env.close(); - - env(fclear(issuer, asfGlobalFreeze)); - env.close(); - - auto const pseudoBroker = [&]() -> std::optional { - if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - return Account{"pseudo", brokerSle->at(sfAccount)}; - } - - return std::nullopt; - }(); - if (!pseudoBroker) - return; - - // Lender and pseudoaccount must both be frozen - env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze)); - env(trust( - issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze)); - env.close(); - - // preclaim: tecFROZEN due to deep frozen - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); - env.close(); - - // Only one needs to be unfrozen - env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); - env.close(); - - // The payment is late by this point - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED)); - env.close(); - env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); - env.close(); - - // preclaim: tecKILLED - // note that tecKILLED in loanMakePayment() - // doesn't happen because of the preclaim check. - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecKILLED)); - } - - void - testInvalidLoanSet() - { - testcase("Invalid LoanSet"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const sponsor{"sponsor"}; - auto const iou = issuer["IOU"]; - - auto testWrapper = [&](auto&& test) { - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower, sponsor); - env(trust(lender, iou(10'000'000))); - env(pay(issuer, lender, iou(5'000'000))); - BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); - test(env, brokerInfo, loanSetFee, debtMaximumRequest); - }; - - // preflight: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) - { - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sponsor::As(sponsor, sponsorFlags), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(temINVALID_FLAG)); - } - - // first temBAD_SIGNER: TODO - // invalid grace period - { - // zero grace period - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kGracePeriod(0), - loanSetFee, - Ter(temINVALID)); - - // grace period less than default minimum - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kGracePeriod(LoanSet::kDefaultGracePeriod - 1), - loanSetFee, - Ter(temINVALID)); - - // grace period greater than payment interval - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(120), - kGracePeriod(121), - loanSetFee, - Ter(temINVALID)); - } - // empty/zero broker ID - { - auto jv = set(borrower, uint256{}, debtMaximumRequest); - - auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) { - // empty broker ID - jv[sfLoanBrokerID] = id; - env(jv, - Sig(sfCounterpartySignature, lender), - loanSetFee, - Txflags(flags), - Ter(temINVALID)); - }; - // empty broker ID - testZeroBrokerID(std::string("")); - // zero broker ID - // needs a flag to distinguish the parsed STTx from the prior - // test - testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); - } - - // preflightCheckSigningKey() failure: - // can it happen? the signature is checked before transactor - // executes - - JTx const tx = env.jt( - set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee); - STTx local = *(tx.stx); - auto counterpartySig = local.getFieldObject(sfCounterpartySignature); - auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); - badPubKey[20] ^= 0xAA; - counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); - local.setFieldObject(sfCounterpartySignature, counterpartySig); - json::Value jvResult; - jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); - auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; - BEAST_EXPECT( - res[jss::error] == "invalidTransaction" && - res[jss::error_exception] == - "fails local checks: Counterparty: Invalid signature."); - }); - - // preclaim: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - // canAddHoldingFailure (IOU only, if MPT doesn't have - // MPTCanTransfer set, then can't create Vault/LoanBroker, - // and LoanSet will fail with different error - env(fclear(issuer, asfDefaultRipple)); - env.close(); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(terNO_RIPPLE)); - }); - - // doApply: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - auto const amt = - env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); - env(pay(borrower, issuer, amt)); - - // tecINSUFFICIENT_RESERVE - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_RESERVE)); - - // addEmptyHolding failure - env(pay(issuer, borrower, amt)); - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecFROZEN)); - }); - } - - void - testAccountSendMptMinAmountInvariant(FeatureBitset features) - { - // (From FIND-006) - testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount " - "and MPT"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env(pay(issuer, lender, mptAsset(2'000'000))); - env(pay(issuer, borrower, mptAsset(1'000))); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 76671; - createJson["ClosePaymentFee"] = "2061925410"; - createJson["GracePeriod"] = 434; - createJson["InterestRate"] = 50302; - createJson["LateInterestRate"] = 30322; - createJson["LatePaymentFee"] = "294427911"; - createJson["LoanOriginationFee"] = "3250635102"; - createJson["LoanServiceFee"] = "9557386"; - createJson["OverpaymentFee"] = 51249; - createJson["OverpaymentInterestRate"] = 14304; - createJson["PaymentInterval"] = 434; - createJson["PaymentTotal"] = "2891743748"; - createJson["PrincipalRequested"] = "8516.98"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(temINVALID)); - env.close(); - } - - void - testLoanPayDebtDecreaseInvariant(FeatureBitset features) - { - // From FIND-007 - testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease " - "rounding good"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const baseFee = env.current()->fees().base; - auto const loanSetFee = Fee(baseFee * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 60; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return Account{lender}; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); - auto const originalState = getCurrentState(env, broker, keylet); - verifyLoanStatus(originalState); - - Number const payment{3'269'349'176'470'588, -12}; - XRPAmount const payFee{ - baseFee * - ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)}; - auto loanPayTx = - env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee)); - BEAST_EXPECT(to_string(payment) == "3269.349176470588"); - env(loanPayTx, Ter(tesSUCCESS)); - env.close(); - - auto const newState = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale)); - BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding); - BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale)); - BEAST_EXPECT( - isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(FeatureBitset features) - { - // From FIND-010 - testcase << "xrpl::loanComputePaymentParts : valid total interest"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 47299; - createJson["ClosePaymentFee"] = "3985819770"; - createJson["InterestRate"] = 92; - createJson["LatePaymentFee"] = "3866894865"; - createJson["LoanOriginationFee"] = "0"; - createJson["LoanServiceFee"] = "2348810240"; - createJson["OverpaymentFee"] = 58545; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 1; - createJson["PrincipalRequested"] = "0.000763058"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson); - env.close(); - - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - loanPayTx["Amount"]["value"] = "0.000281284125490196"; - env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - } - - void - testDosLoanPay(FeatureBitset features) - { - bool const feeCapped = features[fixCleanup3_1_3]; - - // From FIND-005 - testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped"); - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3)); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(100'000'000))); - env(trust(borrower, iouAsset(100'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(1'000))); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{3959'37, -2}; - auto const baseFee = env.current()->fees().base; - - auto const createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object), - kClosePaymentFee(0), - kGracePeriod(60), - kInterestRate(TenthBips32(20930)), - kLateInterestRate(TenthBips32(77049)), - kLatePaymentFee(0), - kLoanServiceFee(0), - kOverpaymentFee(TenthBips32(7)), - kOverpaymentInterestRate(TenthBips32(66653)), - kPaymentInterval(60), - kPaymentTotal(3239184)); - - // There are enough payments due on this loan that it only needs to be - // created once, and can be paid on multiple times. Just don't create a - // gazillion test cases. - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - env(createJson, Sig(sfCounterpartySignature, lender)); - env.close(); - - auto const roundedPayment = [&]() { - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - return roundToAsset( - iouAsset, - stateBefore.periodicPayment, - stateBefore.loanScale, - Number::RoundingMode::Upward); - }(); - - auto test = [&](int const payFactor, - int const feeFactor, - TER const expectedTer = tesSUCCESS) { - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - Number const amount = roundedPayment * payFactor; - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount})); - XRPAmount const payFee{baseFee * feeFactor}; - env(loanPayTx, Ter(expectedTer), Fee(payFee)); - env.close(); - auto const expectedChange = isTesSuccess(expectedTer) - ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor) - : 0; - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange); - }; - - static constexpr std::int64_t kMaxFeeIncrements = - kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement; - - TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P; - - // * Amount well above threshold -> capped fee - // The original test case - way over the limit - more fee is always ok - test(1819878, 363976); - // The capped fee is only sufficient if the amendment is enabled. - test(1819878, kMaxFeeIncrements, failWithoutFix); - - // * Amount exactly at threshold -> capped fee - test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements); - // More fee is always ok - test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10); - - // * Amount below threshold -> normal calculation - test(1, 1); - test(kLoanPaymentsPerFeeIncrement * 2, 2); - test(0, 0, temBAD_AMOUNT); - test(0, 1, temBAD_AMOUNT); - // Fee difference rounds evenly - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1, - telINSUF_FEE_P); - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement)); - // More fee is always ok - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3); - // Fee rounds up - for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under) - { - test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P); - test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements); - } - // Only when you get one less fee increment can you pay less - test( - kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, - kMaxFeeIncrements - 1); - // And again, more fee is always ok. - test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements); - } - - void - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(FeatureBitset features) - { - // From FIND-009 - testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid " - "rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - - { - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{3074'745'058'823'529, -12}; - BEAST_EXPECT(to_string(amount) == "3074.745058823529"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - } - - { - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{6732'118'170'944'051, -12}; - BEAST_EXPECT(to_string(amount) == "6732.118170944051"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - } - - auto const stateAfter = getCurrentState(env, broker, keylet); - // Total interest outstanding is non-negative - BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); - // Principal paid is non-negative - BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding); - // Total value change is non-negative - BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); - // Value delta is larger or same as principal delta (meaning - // non-negative interest paid) - BEAST_EXPECT( - (stateBefore.totalValue - stateAfter.totalValue) >= - (stateBefore.principalOutstanding - stateAfter.principalOutstanding)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(FeatureBitset features) - { - // From FIND-008 - testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - { - auto const coverDepositValue = broker.asset(broker.params.coverDeposit * 10).value(); - env(loanBroker::coverDeposit(lender, broker.brokerID, coverDepositValue)); - env.close(); - } - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["InterestRate"] = 12833; - createJson["LateInterestRate"] = 77048; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 752; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 5678); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{9924'81, -2}; - BEAST_EXPECT(to_string(amount) == "9924.81"); - XRPAmount const payFee{ - baseFee * (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == - stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction); - } - - void - testLoanNextPaymentDueDateOverflow(FeatureBitset features) - { - // For FIND-013 - testcase << "Prevent nextPaymentDueDate overflow"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env{*this, features}; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - using timeType = decltype(sfNextPaymentDueDate)::type::value_type; - static_assert(std::is_same_v); - constexpr timeType kMaxTime = std::numeric_limits::max(); - static_assert(kMaxTime == 4'294'967'295); - - auto const baseJson = [&]() { - auto createJson = env.json( - set(borrower, broker.brokerID, Number{55524'81, -2}), - Fee(loanSetFee), - kClosePaymentFee(0), - kGracePeriod(LoanSet::kDefaultGracePeriod), - kInterestRate(TenthBips32(12833)), - kLateInterestRate(TenthBips32(77048)), - kLatePaymentFee(0), - kLoanOriginationFee(218), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson.removeMember(sfSequence.getJsonName()); - - return createJson; - }(); - - auto const baseFee = env.current()->fees().base; - - auto parentCloseTime = [&]() { - return env.current()->parentCloseTime().time_since_epoch().count(); - }; - auto maxLoanTime = [&]() { - auto const startDate = parentCloseTime(); - - BEAST_EXPECT(startDate >= 50); - - return kMaxTime - startDate; - }; - - { - // straight-up overflow: interval - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: total - // min interval is 60 - auto const interval = 60; - auto const total = maxLoanTime() + 1; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: grace period - // min interval is 60 - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto const grace = interval; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - // The grace period can't be larger than the interval. - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of a few large intervals - auto const interval = 1'000'000'000; - auto const total = 10; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of many small payments - // min interval is 60 - auto const interval = 60; - auto const total = 1'000'000'000; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with an absurdly large grace period - // min interval is 60 - auto const total = 60; - auto const interval = (maxLoanTime() - total) / total; - auto const grace = interval; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const grace = 100; - auto const interval = maxLoanTime() - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // The transaction is killed in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); - } - - // If the transaction had succeeded, the loan would exist - auto const loanSle = env.le(keylet); - // but it doesn't - BEAST_EXPECT(!loanSle); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; - auto const grace = 5'000; - auto const interval = kMaxTime - closeStartDate - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // The transaction succeeds in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - } - - // This loan exists - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); - BEAST_EXPECT(afterState.previousPaymentDate == 0); - BEAST_EXPECT(afterState.paymentRemaining == 1); - } - - { - // Ensure the borrower has funds to pay back the loan - env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); - - // Start date when the ledger is closed will be larger - auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; - auto const grace = 5'000; - auto const maxLoanTime = kMaxTime - closeStartDate - grace; - auto const total = [&]() { - if (maxLoanTime % 5 == 0) - return 5; - if (maxLoanTime % 3 == 0) - return 3; - if (maxLoanTime % 2 == 0) - return 2; - return 0; - }(); - if (!BEAST_EXPECT(total != 0)) - return; - - auto const brokerState = env.le(keylet::loanBroker(broker.brokerID)); - // Intentionally shadow the outer values - auto const loanSequence = brokerState->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const interval = maxLoanTime / total; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // This loan exists - auto const beforeState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval); - BEAST_EXPECT(beforeState.previousPaymentDate == 0); - BEAST_EXPECT(beforeState.paymentRemaining == total); - BEAST_EXPECT(beforeState.periodicPayment > 0); - - // pay all but the last payment - { - NumberRoundModeGuard const mg{Number::RoundingMode::Upward}; - Number const payment = beforeState.periodicPayment * (total - 1); - XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)}; - STAmount const paymentAmount = - roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale); - auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee)); - env(loanPayTx, Ter(tesSUCCESS)); - env.close(); - } - - // The loan is on the last payment - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.paymentRemaining == 1); - BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); - BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval); - } - } - - void - testRequireAuth() - { - testcase("Require Auth - Implicit Pseudo-account authorization"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Env env(*this); - - env.fund(XRP(100'000), issuer, lender, borrower); - env.close(); - - auto asset = MPTTester({ - .env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock, - .authHolder = true, - }); - - env(pay(issuer, lender, asset(5'000'000))); - BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - auto forUnauthAuth = [&](auto&& doTx) { - for (auto const flag : {tfMPTUnauthorize, 0u}) - { - asset.authorize({.account = issuer, .holder = borrower, .flags = flag}); - env.close(); - doTx(flag == 0); - env.close(); - } - }; - - // Can't create a loan if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - err); - }); - - static constexpr std::uint32_t kLoanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence); - - // Can't loan pay if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); - env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); - }); - } - - void - testLendingCanTradeDisabledNoImpact() - { - testcase("Lending: CanTrade disabled has no impact"); - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTrade}); - PrettyAsset const asset = mpt.issuanceID(); - env(pay(issuer, lender, asset(10'000'000))); - env(pay(issuer, borrower, asset(100'000))); - env.close(); - - auto const broker = createVaultAndBroker(env, asset, lender); - - // CanTrade is not set - env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); - env.close(); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - // New cover deposits still work. - env(coverDeposit(lender, broker.brokerID, asset(100))); - env.close(); - - // New loan issuance still works. - env(loan::set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee); - env.close(); - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - BEAST_EXPECT(env.le(loanKeylet)); - - // Repayment still works. - env(pay(borrower, loanKeylet.key, asset(1'000))); - env.close(); - - // Cover withdrawal still works. - env(coverWithdraw(lender, broker.brokerID, asset(100))); - env.close(); - - // Enable CanTrade and verify the DEX path is restored. - mpt.set({.mutableFlags = tmfMPTSetCanTrade}); - env.close(); - - env(offer(lender, XRP(1), asset(10))); - env.close(); - } - -#if LOAN_TODO - void - testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features) - { - testcase("LoanPay full payment skips late penalties"); - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(100'000'000))); - env(trust(borrower, asset(100'000'000))); - env(pay(issuer, lender, asset(50'000'000))); - env(pay(issuer, borrower, asset(5'000'000))); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID)); - if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value()) - return; - - auto const loanSequence = brokerPreLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - Number const principal = asset(1'000).value(); - Number const serviceFee = asset(2).value(); - Number const lateFee = asset(5).value(); - Number const closeFee = asset(4).value(); - - env(set(borrower, broker.brokerID, principal), - Sig(sfCounterpartySignature, lender), - kLoanServiceFee(serviceFee), - kLatePaymentFee(lateFee), - kClosePaymentFee(closeFee), - kInterestRate(percentageToTenthBips(12)), - kLateInterestRate(percentageToTenthBips(24) / 10), - kCloseInterestRate(percentageToTenthBips(5)), - kPaymentTotal(12), - kPaymentInterval(600), - kGracePeriod(0), - Fee(loanSetFee)); - env.close(); - - auto state1 = getCurrentState(env, broker, loanKeylet); - if (!BEAST_EXPECT(state1.paymentRemaining > 1)) - return; - - using d = NetClock::duration; - using tp = NetClock::time_point; - auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}}; - env.close(overdueClose); - - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(brokerSle && loanSle)) - return; - - auto state = getCurrentState(env, broker, loanKeylet); - - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; - TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)}; - TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)}; - - Number const closePaymentFeeRounded = - roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); - Number const latePaymentFeeRounded = - roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); - - auto const roundedLoanState = constructLoanState( - state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); - Number const totalInterestOutstanding = roundedLoanState.interestDue; - - auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); - auto const rawLoanState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - managementFeeRate); - - auto const parentCloseTime = env.current()->parentCloseTime(); - auto const startDateSeconds = - static_cast(state.startDate.time_since_epoch().count()); - - Number const fullPaymentInterest = computeFullPaymentInterest( - rawLoanState.principalOutstanding, - periodicRate, - parentCloseTime, - state.paymentInterval, - state.previousPaymentDate, - startDateSeconds, - closeInterestRateValue); - - Number const roundedFullInterestAmount = - roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); - Number const roundedFullManagementFee = computeManagementFee( - broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale); - Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee; - - Number const trackedValueDelta = - state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding; - Number const untrackedManagementFee = - closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding; - Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding; - - Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee; - BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale)); - - auto const overdueSeconds = - parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; - if (!BEAST_EXPECT(overdueSeconds > 0)) - return; - - Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds); - Number const lateInterestRaw = state.principalOutstanding * overdueRate; - Number const lateInterestRounded = - roundToAsset(broker.asset, lateInterestRaw, state.loanScale); - Number const lateManagementFeeRounded = computeManagementFee( - broker.asset, lateInterestRounded, managementFeeRate, state.loanScale); - Number const penaltyDue = - lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded; - BEAST_EXPECT(penaltyDue > Number{}); - - auto const balanceBefore = env.balance(borrower, broker.asset).number(); - - STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; - env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); - env.close(); - - if (auto const meta = env.meta(); BEAST_EXPECT(meta)) - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - - auto const balanceAfter = env.balance(borrower, broker.asset).number(); - Number const actualPaid = balanceBefore - balanceAfter; - BEAST_EXPECT(actualPaid == baseFullDue); - - Number const expectedWithPenalty = baseFullDue + penaltyDue; - BEAST_EXPECT(expectedWithPenalty > actualPaid); - BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); - } - - void - testLoanCoverMinimumRoundingExploit(FeatureBitset features) - { - auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) { - testcase << "LoanBrokerCoverClawback drains cover via rounding" - << " principalRequested=" << to_string(principalRequest); - - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(2'000'0000))); - env(trust(borrower, asset(2'000'0000))); - env.close(); - - env(pay(issuer, lender, asset(2'000'0000))); - env.close(); - - BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}}; - BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - auto createTx = env.jt( - set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - kPaymentInterval(600), - kPaymentTotal(1), - kGracePeriod(60)); - env(createTx); - env.close(); - - auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerBefore); - if (!brokerBefore) - return; - - Number const debtOutstanding = brokerBefore->at(sfDebtTotal); - Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); - - BEAST_EXPECT(debtOutstanding > Number{}); - BEAST_EXPECT(coverAvailableBefore > Number{}); - - log << "debt=" << to_string(debtOutstanding) - << " cover_available=" << to_string(coverAvailableBefore); - - env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); - env.close(); - - auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerAfter); - if (!brokerAfter) - return; - - Number const debtAfter = brokerAfter->at(sfDebtTotal); - // the debt has not changed - BEAST_EXPECT(debtAfter == debtOutstanding); - - Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable); - - // since the cover rate min != 0, the cover available should not - // be zero - BEAST_EXPECT(coverAvailableAfter != Number{}); - }; - - // Call the lambda with different principal values - testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units - testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units - testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units - testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units - } -#endif - - void - testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features) - { - // --- PoC Summary ---------------------------------------------------- - // Scenario: Borrower makes one periodic payment early (before next due) - // so doPayment sets sfPreviousPaymentDueDate to the (future) - // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one - // interval. Borrower then immediately performs a full-payment - // (tfLoanFullPayment). Why it matters: Full-payment interest accrual - // uses - // delta = now - max(prevPaymentDate, startDate) - // with an unsigned clock representation (uint32). If prevPaymentDate is - // in the future, the subtraction underflows to a very large positive - // number. This inflates roundedFullInterest and total full-close due, - // and LoanPay applies the inflated valueChange to the vault - // (sfAssetsTotal), increasing NAV. - // -------------------------------------------------------------------- - testcase("PoC: Unsigned-underflow full-pay accrual after early periodic"); - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env{*this, features}; - - Account const lender{"poc_lender4"}; - Account const borrower{"poc_borrower4"}; - env.fund(XRP(3'000'000), lender, borrower); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{}; - auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); - - // Create a 3-payment loan so full-payment path is enabled after 1 - // periodic payment. - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest = asset(1000).value(); - auto const originationFee = asset(0).value(); - auto const serviceFee = asset(1).value(); - auto const serviceFeePA = asset(1); - auto const lateFee = asset(0).value(); - auto const closeFee = asset(0).value(); - auto const interest = percentageToTenthBips(12); - auto const lateInterest = percentageToTenthBips(12) / 10; - auto const closeInterest = percentageToTenthBips(12) / 10; - auto const overpaymentInterest = percentageToTenthBips(12) / 10; - auto const total = 3u; - auto const interval = 600u; - auto const grace = 60u; - - auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, 0), - Sig(sfCounterpartySignature, lender), - kLoanOriginationFee(originationFee), - kLoanServiceFee(serviceFee), - kLatePaymentFee(lateFee), - kClosePaymentFee(closeFee), - kOverpaymentFee(percentageToTenthBips(5) / 10), - kInterestRate(interest), - kLateInterestRate(lateInterest), - kCloseInterestRate(closeInterest), - kOverpaymentInterestRate(overpaymentInterest), - kPaymentTotal(total), - kPaymentInterval(interval), - kGracePeriod(grace), - Fee(loanSetFee)); - - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle); - auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(createJtx); - env.close(); - - // Compute a regular periodic due and pay it early (before next due). - auto state = getCurrentState(env, broker, loanKeylet); - Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); - auto const components = xrpl::detail::computePaymentComponents( - env.current()->rules(), - asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - brokerParams.managementFeeRate); - STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()}; - // now < nextDue immediately after creation, so this is an early pay. - env(pay(borrower, loanKeylet.key, regularDue)); - env.close(); - - // Immediately attempt a full payoff. Compute the exact full-payment - // due to ensure the tx applies. - auto after = getCurrentState(env, broker, loanKeylet); - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle2); - - auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; - auto const closeInterestRate = - loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{}; - auto const managementFeeRate = - brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{}; - - Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval); - // Accrued + prepayment-penalty interest based on current periodic - // schedule - auto const fullPaymentInterest = computeFullPaymentInterest( - xrpl::detail::loanPrincipalFromPeriodicPayment( - env.current()->rules(), - after.periodicPayment, - periodicRate2, - after.paymentRemaining), - periodicRate2, - env.current()->parentCloseTime(), - after.paymentInterval, - after.previousPaymentDate, - static_cast(after.startDate.time_since_epoch().count()), - closeInterestRate); - - // Round to asset scale and split interest/fee parts - auto const roundedInterest = - roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); - Number const roundedFullMgmtFee = - computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale); - Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; - - // Show both signed and unsigned deltas to highlight the underflow. - auto const nowSecs = - static_cast(env.current()->parentCloseTime().time_since_epoch().count()); - auto const startSecs = - static_cast(after.startDate.time_since_epoch().count()); - auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs); - auto const signedDelta = - static_cast(nowSecs) - static_cast(lastPaymentDate); - auto const unsignedDelta = static_cast(nowSecs - lastPaymentDate); - log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs - << " now=" << nowSecs << " signedDelta=" << signedDelta - << " unsignedDelta=" << unsignedDelta << std::endl; - - // Reference (clamped) computation: emulate a non-negative accrual - // window by clamping prevPaymentDate to 'now' for the full-pay path. - auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); - auto const fullPaymentInterestClamped = computeFullPaymentInterest( - xrpl::detail::loanPrincipalFromPeriodicPayment( - env.current()->rules(), - after.periodicPayment, - periodicRate2, - after.paymentRemaining), - periodicRate2, - env.current()->parentCloseTime(), - after.paymentInterval, - prevClamped, - startSecs, - closeInterestRate); - auto const roundedInterestClamped = - roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale); - Number const roundedFullMgmtFeeClamped = computeManagementFee( - asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale); - Number const roundedFullInterestClamped = - roundedInterestClamped - roundedFullMgmtFeeClamped; - STAmount const fullDueClamped{ - asset, - after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped + - closePaymentFee}; - - // Collect vault NAV before closing payment - auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; - auto const vaultKey2 = keylet::vault(vaultId2); - auto const vaultBefore = env.le(vaultKey2); - BEAST_EXPECT(vaultBefore); - Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; - - STAmount const fullDue{ - asset, - after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee + - closePaymentFee}; - - log << "PoC payoff: principalOutstanding=" << after.principalOutstanding - << " roundedFullInterest=" << roundedFullInterest - << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee - << " fullDue=" << to_string(fullDue.getJson()) << std::endl; - log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped - << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped - << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl; - - env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment)); - env.close(); - - // Sanity: underflow present (unsigned delta very large relative to - // interval) - BEAST_EXPECT(unsignedDelta > after.paymentInterval); - - // Compare vault NAV before/after the full close - auto const vaultAfter = env.le(vaultKey2); - BEAST_EXPECT(vaultAfter); - if (vaultAfter) - { - auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); - log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore - << " assetsTotalAfter=" << assetsTotalAfter - << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl; - - // Value-based proof: underflowed window yields a payoff larger than - // the clamped (non-underflow) reference. - BEAST_EXPECT(fullDue == fullDueClamped); - if (fullDue > fullDueClamped) - log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; - } - - // Loan should be paid off - auto const finalLoan = env.le(loanKeylet); - BEAST_EXPECT(finalLoan); - if (finalLoan) - { - BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); - } - } - - void - testDustManipulation(FeatureBitset features) - { - testcase("Dust manipulation"); - - using namespace jtx; - using namespace std::chrono_literals; - Env env{*this, features}; - - // Setup: Create accounts - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - Account const victim{"victim"}; - - env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); - env.close(); - - // Step 1: Create vault with IOU asset - auto asset = issuer["USD"]; - env(trust(lender, asset(100000))); - env(trust(borrower, asset(100000))); - env(trust(victim, asset(100000))); - env(pay(issuer, lender, asset(50000))); - env(pay(issuer, borrower, asset(50000))); - env(pay(issuer, victim, asset(50000))); - env.close(); - - BrokerParameters const brokerParams{ - .vaultDeposit = 10000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{1000}, - .coverRateLiquidation = TenthBips32{2500}}; - - auto broker = createVaultAndBroker(env, asset, lender, brokerParams); - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the - // _LOAN_BROKER_ object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); - }(); - if (!loanKeyletOpt) - return; - - auto const& vaultKeylet = broker.vaultKeylet(); - - { - auto const vaultSle = env.le(vaultKeylet); - Number const assetsTotal = vaultSle->at(sfAssetsTotal); - Number const assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "Before loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - // before the loan the assets total and available should be equal - BEAST_EXPECT(assetsAvail == assetsTotal); - BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number()); - } - - Keylet const& loanKeylet = *loanKeyletOpt; - - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100}, - .interest = TenthBips32{1922}, - .payTotal = 5816, - .payInterval = 86400 * 6, - .gracePd = 86400 * 5, - }; - - env(loanParams(env, broker)); - env.close(); - - // Wait for loan to be late enough to default - env.close(std::chrono::seconds(86400 * 40)); // 40 days - - { - auto const vaultSle = env.le(vaultKeylet); - Number const assetsTotal = vaultSle->at(sfAssetsTotal); - Number const assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "After loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - return; - auto const state = constructLoanState(loanSle); - - log << "Loan state:" << std::endl; - log << " ValueOutstanding: " << state.valueOutstanding << std::endl; - log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl; - log << " InterestOutstanding: " << state.interestOutstanding() << std::endl; - log << " InterestDue: " << state.interestDue << std::endl; - log << " FeeDue: " << state.managementFeeDue << std::endl; - - // after loan creation the assets total and available should - // reflect the value of the loan - BEAST_EXPECT(assetsAvail < assetsTotal); - BEAST_EXPECT( - assetsAvail == - broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number()); - BEAST_EXPECT( - assetsTotal == - broker.asset(brokerParams.vaultDeposit + state.interestDue).number()); - } - - // Step 7: Trigger default (dust adjustment will occur) - env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // Step 8: Verify phantom assets created - { - auto const vaultSle2 = env.le(vaultKeylet); - Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal); - Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable); - - log << "After default:" << std::endl; - log << " AssetsTotal: " << assetsTotal2 << std::endl; - log << " AssetsAvailable: " << assetsAvail2 << std::endl; - log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl; - - // after a default the assets total and available should be equal - BEAST_EXPECT(assetsAvail2 == assetsTotal2); - } - } - - void - testRIPD3831(FeatureBitset features) - { - using namespace jtx; - - testcase("RIPD-3831"); - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200'000, -6}, - .lateFee = Number{200, -6}, - .interest = TenthBips32{50'000}, - .payTotal = 10, - .payInterval = 150}; - - auto const assetType = AssetType::XRP; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - - using tp = NetClock::time_point; - using d = NetClock::duration; - - auto state = getCurrentState(env, broker, loanKeylet); - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); - } - - topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); - - using namespace jtx::loan; - - auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); - - { - auto const submitParam = to_string(jv); - auto const jr = env.rpc("submit", borrower.name(), submitParam); - - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - } - - env.close(); - - // Make sure the system keeps responding - env(noop(borrower)); - env.close(); - env(noop(issuer)); - env.close(); - env(noop(lender)); - env.close(); - } - - void - testRIPD3459(FeatureBitset features) - { - testcase("RIPD-3459 - LoanBroker incorrect debt total"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 200'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{500}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100'000, -4}, - .interest = TenthBips32{100'000}, - .payTotal = 10}; - - auto const assetType = AssetType::MPT; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); - } - } - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero); - } - } - } - - void - testRIPD3901() - { - testcase("Crash with tfLoanOverpayment"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const depositor{"depositor"}; - auto const txFee = Fee(XRP(100)); - - Env env(*this); - Vault const vault(env); - - env.fund(XRP(10'000), lender, issuer, borrower, depositor); - env.close(); - - auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()}); - env(tx, txFee); - env.close(); - - env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}), - txFee); - env.close(); - - auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); - - env(loanBroker::set(lender, vaultKeyLet.key), txFee); - env.close(); - - // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; - - STAmount const debtMaximumRequest = XRPAmount(200'000); - - env(set(borrower, brokerKeyLet.key, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(50'000)), - kPaymentTotal(2), - kPaymentInterval(150), - Txflags(tfLoanOverpayment), - txFee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); - - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), - Txflags(tfLoanOverpayment), - txFee); - env.close(); - } - } - - void - testRoundingAllowsUndercoverage(FeatureBitset features) - { - testcase("Minimum cover rounding allows undercoverage (XRP)"); - - using namespace jtx; - using namespace loanBroker; - - Env env{*this, features}; - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(200'000), lender, borrower); - env.close(); - - // Vault with XRP asset - Vault const vault{env}; - auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()}); - env(vaultCreate); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - // Seed the vault with XRP so it can fund the loan principal - PrettyAsset const xrpAsset{xrpIssue(), 1}; - - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{10'000}, - .coverDeposit = 82, - }; - - auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams); - // Create a loan with principal 804 XRP and 0% interest (so - // DebtTotal increases by exactly 804) - env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), - loan::kInterestRate(TenthBips32(0)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 2)); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Verify DebtTotal is exactly 804 - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - } - - // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. - // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), - Ter(tecINSUFFICIENT_FUNDS)); - BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); - env.close(); - - // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. - // because that leaves sufficient cover, this succeeds - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Validate CoverAvailable == 80 XRP and DebtTotal remains 804 - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - - // Also demonstrate that the true minimum (804 * 10%) exceeds 80 - auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000)); - log << "Theoretical min cover: " << theoreticalMin << std::endl; - BEAST_EXPECT(Number(804, -1) == theoreticalMin); - } - } - - void - testRIPD3902(FeatureBitset features) - { - testcase("RIPD-3902 - 1 IOU loan payments"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{1, 0}, - .interest = TenthBips32{100'000}, - .payTotal = 5, - .payInterval = 150, - .gracePd = 60}; - - auto const assetType = AssetType::IOU; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - } - - void - testBorrowerIsBroker() - { - testcase("Test Borrower is Broker"); - using namespace jtx; - using namespace loan; - Account const broker{"broker"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const depositor{"depositor"}; - - auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { - Env env(*this); - Vault const vault(env); - - if (borrower == broker) - { - env.fund(XRP(10'000), broker, issuer, depositor); - } - else - { - env.fund(XRP(10'000), broker, borrower, issuer, depositor); - } - env.close(); - - auto const xrpFee = XRP(100); - auto const txFee = Fee(xrpFee); - - STAmount const debtMaximumRequest = getMaxDebt(env); - - auto const& asset = debtMaximumRequest.asset(); - auto const initialVault = asset(debtMaximumRequest * 100); - - auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset}); - env(tx, txFee); - env.close(); - - env(vault.deposit( - {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}), - txFee); - env.close(); - - auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); - - env(loanBroker::set(broker, vaultKeylet.key), txFee); - env.close(); - - auto const serviceFee = 101; - - env(set(broker, brokerKeylet.key, debtMaximumRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - kLoanServiceFee(serviceFee), - kPaymentTotal(10), - txFee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); - - auto const brokerBalanceBefore = env.balance(broker, asset); - - if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle)) - { - auto const payment = loanSle->at(sfPeriodicPayment); - auto const totalPayment = payment + serviceFee; - env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee); - env.close(); - if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle)) - { - auto const expected = [&]() { - // The service fee is transferred to the broker if - // a borrower is not the broker - if (borrower != broker) - return brokerBalanceBefore.number() + serviceFee; - // Since a borrower is the broker, the payment is - // transferred to the Vault from the broker but not - // the service fee. - // If the asset is XRP then the broker pays the txFee. - if (asset.native()) - return brokerBalanceBefore.number() - payment - xrpFee.number(); - return brokerBalanceBefore.number() - payment; - }(); - BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value()); - } - } - }; - // Test when a borrower is the broker and is not to verify correct - // service fee transfer in both cases. - for (auto const& borrowerAcct : {broker, borrower}) - { - testLoanAsset( - [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - auto const iou = issuer["USD"]; - env(trust(broker, iou(1'000'000'000))); - env(trust(depositor, iou(1'000'000'000))); - env(pay(issuer, broker, iou(100'000'000))); - env(pay(issuer, depositor, iou(100'000'000))); - env.close(); - return iou(200'000); - }, - borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - MPTTester const mpt( - {.env = env, - .issuer = issuer, - .holders = {broker, depositor}, - .pay = 100'000'000}); - return mpt(200'000); - }, - borrowerAcct); - } - } - - void - testIssuerIsBorrower(FeatureBitset features) - { - testcase("RIPD-4096 - Issuer as borrower"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, .counter = issuer, .principalRequest = Number{10000}}; - - auto const assetType = AssetType::IOU; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - issuer, - PaymentParameters{.showStepBalances = true}); - } - - void - testLimitExceeded() - { - testcase("RIPD-4125 - overpayment"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200000, -6}, - .interest = TenthBips32{50000}, - .payTotal = 3, - .payInterval = 200, - .gracePd = 60, - .flags = tfLoanOverpayment, - }; - - auto const assetType = AssetType::XRP; - - Env env(*this, makeConfig(), all_, nullptr, beast::Severity::Warning); - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - auto const state = getCurrentState(env, broker, loanKeylet); - - env(loan::pay( - borrower, - loanKeylet.key, - STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, - tfLoanOverpayment)); - env.close(); - - PaymentParameters const paymentParams{ - .showStepBalances = false, - .validateBalances = true, - }; - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - paymentParams); - } - - void - testOverpaymentManagementFee(FeatureBitset features) - { - testcase("testOverpaymentManagementFee"); - - using namespace jtx; - using namespace loan; - - Env env{*this, features}; - - Account const lender{"lender"}, borrower{"borrower"}; - - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1000}; - - auto const result = createVaultAndBroker( - env, - asset, - lender, - { - .vaultDeposit = asset(100'000).value(), - .managementFeeRate = TenthBips16(10'000), - }); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const loanKeylet = keylet::loan( - result.brokerKeylet().key, (env.le(result.brokerKeylet()))->at(sfLoanSequence)); - env(loan::set( - borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - loan::kPaymentInterval(86400 * 30), - loan::kPaymentTotal(3), - loan::kOverpaymentInterestRate(TenthBips32(percentageToTenthBips(20))), - loanSetFee); - - // From calculator - auto const expectedOverpaymentManagementFee = Number{33333, 0}; - auto const loanBrokerBalanceBefore = env.balance(lender); - - auto const loanPayFee = Fee(env.current()->fees().base * 2); - env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee); - env.close(); - - BEAST_EXPECTS( - env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee, - "overpayment management fee missmatch; expected:" + - to_string(expectedOverpaymentManagementFee) + - " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore)); - } - - void - testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features) - { - testcase << "LoanPay Broker Owner Missing Trustline (PoC)"; - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - auto const iou = issuer["IOU"]; - Env env(*this, features); - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - // Set up trustlines and fund accounts - env(trust(broker, iou(20'000'000))); - env(trust(borrower, iou(20'000'000))); - env(pay(issuer, broker, iou(10'000'000))); - env(pay(issuer, borrower, iou(1'000))); - env.close(); - // Create vault and broker - auto const brokerInfo = createVaultAndBroker(env, iou, broker); - // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(iou(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = iou(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); - env.close(); - // Verify broker owner has a trustline - auto const brokerTrustline = keylet::trustLine(broker, iou); - BEAST_EXPECT(env.le(brokerTrustline) != nullptr); - // Broker owner deletes their trustline - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, iou); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Remove the trustline by setting limit to 0 - env(trust(broker, iou(0))); - env.close(); - // Verify trustline is deleted - BEAST_EXPECT(env.le(brokerTrustline) == nullptr); - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_LINE. - env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify trustline is still deleted - BEAST_EXPECT(env.le(brokerTrustline) == nullptr); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, iou); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features) - { - testcase << "LoanPay Broker Owner MPT unauthorized"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // Create vault and broker - auto const brokerInfo = createVaultAndBroker(env, mpt, broker); - // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = mpt(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); - env.close(); - // Verify broker owner is authorized - auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); - BEAST_EXPECT(env.le(brokerMpt) != nullptr); - // Broker owner unauthorizes. - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Then, unauthorize the MPT. - mptt.authorize({.account = broker, .flags = tfMPTUnauthorize}); - env.close(); - // Verify the MPT is unauthorized. - BEAST_EXPECT(env.le(brokerMpt) == nullptr); - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_AUTH. - auto const borrowerBalance = env.balance(borrower, mpt); - env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify the MPT is still unauthorized. - BEAST_EXPECT(env.le(brokerMpt) == nullptr); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, mpt); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) - { - testcase << "LoanPay Broker Owner without permissioned domain of the MPT"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - auto credType = "credential1"; - - pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}}; - env(pdomain::setTx(issuer, credentials1)); - env.close(); - - auto domainID = pdomain::getNewDomain(env.meta()); - - env(credentials::create(broker, issuer, credType)); - env(credentials::accept(broker, issuer, credType)); - env.close(); - - env(credentials::create(borrower, issuer, credType)); - env(credentials::accept(borrower, issuer, credType)); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({ - .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, - .domainID = domainID, - }); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // Create vault and broker - auto const brokerInfo = createVaultAndBroker(env, mpt, broker); - // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = mpt(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); - env.close(); - // Verify broker owner is authorized - auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); - BEAST_EXPECT(env.le(brokerMpt) != nullptr); - // Remove the credentials for the Broker owner. - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - - env(credentials::deleteCred(broker, broker, issuer, credType)); - env.close(); - - // Make sure the broker is not authorized to hold the MPT after we - // deleted the credentials - env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); - - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_AUTH. - auto const borrowerBalance = env.balance(borrower, mpt); - env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify broker is still not authorized - env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, mpt); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) - { - testcase << "LoanSet Broker Owner without permissioned domain of the MPT"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - auto credType = "credential1"; - - pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}}; - env(pdomain::setTx(issuer, credentials1)); - env.close(); - - auto domainID = pdomain::getNewDomain(env.meta()); - - // Add credentials for the broker and borrower - env(credentials::create(broker, issuer, credType)); - env(credentials::accept(broker, issuer, credType)); - env.close(); - - env(credentials::create(borrower, issuer, credType)); - env(credentials::accept(borrower, issuer, credType)); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({ - .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, - .domainID = domainID, - }); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // Create vault and broker - auto const brokerInfo = createVaultAndBroker(env, mpt, broker); - - // Remove the credentials for the Broker owner. - // Clear the balance first. - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Delete the credentials - env(credentials::deleteCred(broker, broker, issuer, credType)); - env.close(); - - // Create a loan, this should fail for tecNO_AUTH - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100)), - Ter(tecNO_AUTH)); - env.close(); - } - - void - testSequentialFLCDepletion(FeatureBitset features) - { - testcase << "First-Loss Capital Depletion on Sequential Defaults"; - - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env{*this, features}; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrowerA{"borrowerA"}; - Account const borrowerB{"borrowerB"}; - - env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB); - env.close(); - - PrettyAsset const asset = xrpIssue(); - auto const vaultDepositAmount = - asset(200'000); // Enough for 2 x 50k loans plus interest/fees - - auto const brokerInfo = createVaultAndBroker( - env, - asset, - lender, - { - .vaultDeposit = vaultDepositAmount.value(), - .debtMax = 0, - .coverRateMin = TenthBips32(20000), // 20% - .coverDeposit = 21'000, - .managementFeeRate = TenthBips16(100), // 0.1% - .coverRateLiquidation = TenthBips32(100000), - }); - auto const brokerKeylet = brokerInfo.brokerKeylet(); - - // Create two identical loans: each 50,000 XRP principal (scaled down to - // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal - // + interest) Formula will calculate cover as: 100% × (20% × 100,000) = - // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first - // default - auto const principalAmount = Number(50'000); - auto const loanPaymentInterval = 2592000; // 30 days - auto const loanGracePeriod = 604800; // 7 days - - // Create Loan A - auto loanATx = env.jt( - set(borrowerA, brokerKeylet.key, principalAmount), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(500)), // 5% - kPaymentTotal(12), - loan::kPaymentInterval(loanPaymentInterval), - loan::kGracePeriod(loanGracePeriod), - Fee(XRP(10))); // Sufficient fee for multi-sig transaction - env(loanATx); - env.close(); - - auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); - - // Create Loan B - auto loanBTx = env.jt( - set(borrowerB, brokerKeylet.key, principalAmount), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(500)), // 5% - kPaymentTotal(12), - loan::kPaymentInterval(loanPaymentInterval), - loan::kGracePeriod(loanGracePeriod), - Fee(XRP(10))); // Sufficient fee for multi-sig transaction - env(loanBTx); - env.close(); - - auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); - - auto loanASle = env.le(loanAKeylet); - if (!BEAST_EXPECT(loanASle)) - return; - - // Advance time past grace period for both loans to be defaultable - auto const loanANextDue = loanASle->at(sfNextPaymentDueDate); - auto const loanAGrace = loanASle->at(sfGracePeriod); - env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60}); - - env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - env.close(); - - // Verify Loan A is defaulted - loanASle = env.le(loanAKeylet); - if (!BEAST_EXPECT(loanASle)) - return; - BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault)); - BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0); - - // Check broker state after first default (from committed ledger) - auto brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); - auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); - - // DebtTotal should have decreased by Loan A's debt - BEAST_EXPECT(afterFirstDebtTotal == 50'134); - - // CoverAvailable should have decreased significantly - BEAST_EXPECT(afterFirstCoverAvailable == 946); - - env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - - brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal); - auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable); - - BEAST_EXPECT(afterSecondDebtTotal == 0); - - BEAST_EXPECT(afterSecondCoverAvailable == 0); - } - - void - testYieldTheftRounding(std::uint32_t flags) - { - testcase("Rounding manipulation does not permit yield theft"); - using namespace jtx; - using namespace loan; - - // 1. Setup Environment - Env env(*this, all_); - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1000), issuer, lender, borrower); - env.close(); - - // 2. Asset Selection - PrettyAsset const iou = issuer["USD"]; - env(trust(lender, iou(100'000'000))); - env(trust(borrower, iou(100'000'000))); - env(pay(issuer, lender, iou(100'000'000))); - env(pay(issuer, borrower, iou(100'000'000))); - env.close(); - - // 3. Create Vault and Broker with High Debt Limit (100M) - auto const brokerInfo = createVaultAndBroker( - env, - iou, - lender, - { - .vaultDeposit = 5'000'000, - .debtMax = Number{100'000'000}, - .coverDeposit = 500'000, - }); - auto const [currentSeq, vaultKeylet] = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::make_tuple(0u, keylet::unchecked(beast::kZero)); - auto const currentSeq = brokerSle->at(sfLoanSequence); - auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID)); - return std::make_tuple(currentSeq, vaultKeylet); - }(); - - // 4. Loan Parameters (Attack Vector) - Number const principal = 1'000'000; - TenthBips32 const interestRate = TenthBips32{1}; // 0.001% - std::uint32_t const paymentInterval = 86400; - std::uint32_t const paymentTotal = 3650; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags), - Sig(sfCounterpartySignature, lender), - loan::kInterestRate(interestRate), - loan::kPaymentInterval(paymentInterval), - loan::kPaymentTotal(paymentTotal), - Fee(loanSetFee)); - env.close(); - - // --- RETRIEVE OBJECTS & SETUP ATTACK --- - - auto borrowerBalance = [&]() { return env.balance(borrower, iou); }; - auto const borrowerScale = static_cast(borrowerBalance()).exponent(); - - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq); - auto const maybePeriodicPayment = [&]() -> std::optional { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - return std::nullopt; - // Construct Payment - return STAmount{iou, loanSle->at(sfPeriodicPayment)}; - }(); - if (!maybePeriodicPayment) - return; - auto const periodicPayment = *maybePeriodicPayment; - auto const roundedPayment = - roundToScale(periodicPayment, borrowerScale, Number::RoundingMode::Upward); - - // ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution - STAmount const paymentBuffer{iou, Number(1, -9)}; - STAmount const attackPayment = periodicPayment + paymentBuffer; - - auto const maybeInitialVaultAssets = [&]() -> std::optional { - auto const vault = env.le(vaultKeylet); - if (!BEAST_EXPECT(vault)) - return std::nullopt; - return vault->at(sfAssetsTotal); - }(); - if (!maybeInitialVaultAssets) - return; - auto const initialVaultAssets = *maybeInitialVaultAssets; - - // 5. Execution Loop - int yieldTheftCount = 0; - auto previousAssetsTotal = initialVaultAssets; - - for (int i = 0; i < 100; ++i) - { - auto const balanceBefore = borrowerBalance(); - env(pay(borrower, loanKeylet.key, attackPayment, flags)); - env.close(); - auto const borrowerDelta = balanceBefore - borrowerBalance(); - BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum()); - - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - break; - auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)}; - BEAST_EXPECT( - (roundToScale(updatedPayment, borrowerScale, Number::RoundingMode::Upward) == - roundedPayment)); - BEAST_EXPECT( - (updatedPayment == periodicPayment) || - (flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment)); - - auto const currentVaultSle = env.le(vaultKeylet); - if (!BEAST_EXPECT(currentVaultSle)) - break; - - auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal); - auto const delta = currentAssetsTotal - previousAssetsTotal; - - BEAST_EXPECT( - (delta == beast::kZero && borrowerDelta <= roundedPayment) || - (delta > beast::kZero && borrowerDelta > roundedPayment)); - - // If tx succeeded but Assets Total didn't change, interest was - // stolen. - if (delta == beast::kZero && borrowerDelta > roundedPayment) - { - yieldTheftCount++; - } - - previousAssetsTotal = currentAssetsTotal; - } - - BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount)); - } - - // Tests that vault withdrawals work correctly when the vault has unrealized - // loss from an impaired loan, ensuring the invariant check properly - // accounts for the loss. - void - testWithdrawReflectsUnrealizedLoss(FeatureBitset features) - { - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - testcase("Vault withdraw reflects sfLossUnrealized"); - - // Test constants - static constexpr std::int64_t kInitialFunding = 1'000'000; - static constexpr std::int64_t kLenderInitialIou = 5'000'000; - static constexpr std::int64_t kDepositorInitialIou = 1'000'000; - static constexpr std::int64_t kBorrowerInitialIou = 100'000; - static constexpr std::int64_t kDepositAmount = 5'000; - static constexpr std::int64_t kPrincipalAmount = 99; - static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000; - static constexpr std::uint32_t kLocalPaymentInterval = 600; - static constexpr std::uint32_t kLocalPaymentTotal = 2; - - Env env{*this, features}; - - // Setup accounts - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const depositorA{"lpA"}; - Account const depositorB{"lpB"}; - Account const borrower{"borrowerA"}; - - env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower); - env.close(); - - // Setup trust lines - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(depositorA, iouAsset(10'000'000))); - env(trust(depositorB, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env.close(); - - // Fund accounts with IOUs - env(pay(issuer, lender, iouAsset(kLenderInitialIou))); - env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou))); - env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou))); - env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou))); - env.close(); - - // Create vault and broker, then add deposits from two depositors - auto const broker = createVaultAndBroker(env, iouAsset, lender); - Vault v{env}; - - env(v.deposit({ - .depositor = depositorA, - .id = broker.vaultKeylet().key, - .amount = iouAsset(kDepositAmount), - }), - Ter(tesSUCCESS)); - env(v.deposit({ - .depositor = depositorB, - .id = broker.vaultKeylet().key, - .amount = iouAsset(kDepositAmount), - }), - Ter(tesSUCCESS)); - env.close(); - - // Create a loan - auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(sleBroker)) - return; - - auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); - - env(set(borrower, broker.brokerID, kPrincipalAmount), - Sig(sfCounterpartySignature, lender), - kPaymentTotal(kLocalPaymentTotal), - kPaymentInterval(kLocalPaymentInterval), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // Impair the loan to create unrealized loss - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); - env.close(); - - // Verify unrealized loss is recorded in the vault - auto const vaultAfterImpair = env.le(broker.vaultKeylet()); - if (!BEAST_EXPECT(vaultAfterImpair)) - return; - - BEAST_EXPECT( - vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value()); - - // Helper to get share balance for a depositor - auto const shareAsset = vaultAfterImpair->at(sfShareMPTID); - auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t { - auto const token = env.le(keylet::mptoken(shareAsset, depositor.id())); - return token ? token->getFieldU64(sfMPTAmount) : 0; - }; - - // Verify both depositors have equal shares - auto const sharesLpA = getShareBalance(depositorA); - auto const sharesLpB = getShareBalance(depositorB); - BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor); - BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor); - BEAST_EXPECT(sharesLpA == sharesLpB); - - // Helper to attempt withdrawal - auto const attemptWithdrawShares = [&](Account const& depositor, - std::uint64_t shareAmount, - TER expected) { - STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)}; - env(v.withdraw( - {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}), - Ter(expected)); - env.close(); - }; - - // Regression test: Both depositors should successfully withdraw despite - // unrealized loss. Previously failed with invariant violation: - // "withdrawal must change vault and destination balance by equal - // amount". This was caused by sharesToAssetsWithdraw rounding down, - // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta - // when unrealized loss exists. - attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS); - attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS); - } - - // A residual overpayment can reduce the stored principal by one scale-unit - // *less* than computeOverpaymentComponents predicts, firing the - // "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment: - // - // trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding - // - // tryOverpayment re-amortizes the loan at the reduced principal, then - // re-derives the theoretical principal from the new periodic payment via - // (P * paymentFactor) / paymentFactor. That round-trip is not exact in - // Number's 19-digit arithmetic; a positive residual pushes the recomputed - // principal a hair above the exact grid point `oldPrincipal - delta`, and - // the Upward rounding in tryOverpayment then bumps it a full scale-unit - // higher. The principal therefore drops by `delta - 1 unit`, not `delta`. - // - // Concrete case (isolated, at the tryOverpayment level): - // A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10. - // After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of - // 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by - // 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10. - // - // With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact, - // on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the - // lossy (P*factor)/factor round-trip, so the assertion holds and the - // overpayment applies cleanly. The three "principal change agrees" / - // "interest paid agrees" / "principal payment matches" assertions are - // gated behind the same amendment, so without it they are disabled (the - // server does not abort) and the loan keeps the pre-amendment computation. - // - // The test runs the same scenario under both amendment settings and checks - // the stored principal against a ground-truth value derived independently of - // the loan-state computation under test. - void - testBugOverpaymentPrincipalChange() - { - testcase("bug: doOverpayment asserts 'principal change agrees'"); - - using namespace jtx; - using namespace loan; - using namespace xrpl::detail; - - struct Params - { - TenthBips32 interestRate; - TenthBips16 managementFeeRate; - std::uint32_t paymentTotal; - std::uint32_t paymentInterval; - std::int64_t principal; - Number overpayment; - TenthBips32 overpaymentInterestRate; - TenthBips32 overpaymentFeeRate; - std::optional vaultScale; - }; - - struct Result - { - Number principalOutstanding; // stored principal after the LoanPay - Number expectedNewPrincipal; // ground truth, independent of the fix - Number managementFeeChange; // managementFeeOutstanding after - before - Number unit; // one scale-unit at the loan scale - }; - - auto runScenario = [this](FeatureBitset features, Params const& p) -> Result { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - Asset const asset = iouAsset.raw(); - STAmount const iouLimit{asset, Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 900'000, - .debtMax = 0, - .managementFeeRate = p.managementFeeRate, - .vaultScale = p.vaultScale}); - - auto const brokerSle = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerSle); - auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - kInterestRate(p.interestRate), - kPaymentTotal(p.paymentTotal), - kPaymentInterval(p.paymentInterval), - kGracePeriod(p.paymentInterval), - kOverpaymentFee(p.overpaymentFeeRate), - kOverpaymentInterestRate(p.overpaymentInterestRate), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // The single LoanPay below makes one regular payment (the overpayment - // is smaller than one period) and leaves the residual as an - // overpayment. - auto const s = getCurrentState(env, broker, loanKeylet); - auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval); - auto const onePeriod = computePaymentComponents( - env.current()->rules(), - asset, - s.loanScale, - s.totalValue, - s.principalOutstanding, - s.managementFeeOutstanding, - s.periodicPayment, - periodicRate, - s.paymentRemaining, - p.managementFeeRate); - - // Ground truth: the stored principal must drop by exactly the regular - // payment's principal portion plus the overpayment's principal - // portion. computeOverpaymentComponents depends only on the - // overpayment amount and rates (not on the loan-state computation - // under test), so it is an independent oracle. Both components are - // computed under the same rules as the env so the payment factor - // matches. - auto const overpaymentComponents = computeOverpaymentComponents( - env.current()->rules(), - asset, - s.loanScale, - p.overpayment, - p.overpaymentInterestRate, - p.overpaymentFeeRate, - p.managementFeeRate); - Number const expectedNewPrincipal = s.principalOutstanding - - onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta; - - Number const managementFeeBefore = s.managementFeeOutstanding; - - STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment}; - env(pay(borrower, loanKeylet.key, payAmount), - Txflags(tfLoanOverpayment), - Ter(tesSUCCESS)); - env.close(); - - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - - return Result{ - .principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0, - .expectedNewPrincipal = expectedNewPrincipal, - .managementFeeChange = - (loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) - - managementFeeBefore, - .unit = Number{1, s.loanScale}}; - }; - - // Scenario 1: the original near-zero-rate principal reproduction - // (loanScale -10, no management fee). 0.049999998 is smaller than one - // period, so it stays a residual overpayment. - Params const principalCase{ - .interestRate = TenthBips32{1}, - .managementFeeRate = TenthBips16{0}, - .paymentTotal = 3, - .paymentInterval = 60, - .principal = 100, - .overpayment = Number{49999998, -9}, - .overpaymentInterestRate = TenthBips32{1000}, - .overpaymentFeeRate = TenthBips32{1000}, - .vaultScale = 1}; - - // With fixCleanup3_2_0 the stored principal lands exactly on the - // ground-truth grid point: it is reduced by exactly the overpayment's - // principal portion. This is the key correctness check: if the principal - // pin were removed (even with the assertions still gated off), the lossy - // (P * factor) / factor round-trip would leave the principal one - // scale-unit high and this would fail. - Result const fixed = runScenario(all_, principalCase); - BEAST_EXPECTS( - fixed.principalOutstanding == fixed.expectedNewPrincipal, - "fixed principal " + to_string(fixed.principalOutstanding) + " != expected " + - to_string(fixed.expectedNewPrincipal)); - - // Without the amendment the loan amortizes with the catastrophically - // cancelling near-zero payment factor, so its schedule (and ground truth) - // differ from the fixed case; the gated assertions keep the server from - // aborting and the overpayment still lands exactly on that schedule. - Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase); - BEAST_EXPECTS( - legacy.principalOutstanding == legacy.expectedNewPrincipal, - "legacy principal " + to_string(legacy.principalOutstanding) + " != expected " + - to_string(legacy.expectedNewPrincipal)); - - // Scenario 2: a normal-rate loan with a 10% management fee. At a normal - // rate the payment factor is identical across the amendment, so toggling - // fixCleanup3_2_0 isolates the fix. This overpayment (found by search) - // lands on a state where both the principal and the management fee differ - // by one scale-unit between the fixed and legacy paths. - Params const feeCase{ - .interestRate = TenthBips32{10000}, - .managementFeeRate = TenthBips16{10000}, - .paymentTotal = 6, - .paymentInterval = 30u * 24 * 60 * 60, - .principal = 1000, - .overpayment = Number{214367363, -10}, - .overpaymentInterestRate = TenthBips32{0}, - .overpaymentFeeRate = TenthBips32{0}, - .vaultScale = std::nullopt}; - - Result const feeFixed = runScenario(all_, feeCase); - Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase); - - // With the fix the principal is the exact reduction; without it the lossy - // (P * factor) / factor round-trip leaves it one scale-unit high. - BEAST_EXPECTS( - feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal, - "fee-case fixed principal " + to_string(feeFixed.principalOutstanding) + - " != expected " + to_string(feeFixed.expectedNewPrincipal)); - BEAST_EXPECTS( - feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit, - "fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) + - " != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit)); - - // Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee - // outstanding drops. - // - // Unlike the principal that is already at the correct precision, the re-amortized - // management fee is tenthBipsOfValue of the new schedule's gross interest, which depends - // on the recomputed periodic payment. So the expected change below is a pinned constant - // captured from a passing run a magic value only because there is nothing simpler to - // compare against. - // - // At the integration level, toggling the amendment also changes the regular payment's - // rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee - // fix. - BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange); - BEAST_EXPECTS( - (feeFixed.managementFeeChange == Number{-8219709543, -10}), - "fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange)); - } - - // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero - // rate). At such a near-zero rate the closed-form payment factor - // (1 + r)^n - 1 cancels catastrophically. - // - // Without fixCleanup3_2_0 the resulting amortization is degenerate and the - // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the - // amendment, computePowerMinusOneHybrid uses a numerically-stable series - // expansion, so the loan is created and the scheduled payments - // (2 * periodicPayment) cover the principal — no economic underpayment - // (yield theft). - // - // The test runs the same LoanSet under both amendment settings and pins the - // exact outcome for each. - void - testLoanSetNearZeroInterestRateSucceeds() - { - testcase("LoanSet near-zero interest rate covers principal"); - - using namespace jtx; - using namespace loan; - - Number const principalRequested{1000}; - - struct Result - { - TER ter = tesSUCCESS; - bool created = false; - std::int32_t loanScale = 0; - Number principal; - Number totalValue; - Number managementFee; - Number periodicPayment; - }; - - auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}}); - - auto const brokerSle = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerSle); - auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(set(borrower, broker.brokerID, principalRequested), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32{1}), - kPaymentTotal(2), - kPaymentInterval(400), - Fee(env.current()->fees().base * 2), - Ter(expectedTer)); - env.close(); - - Result r; - r.ter = env.ter(); - if (auto const loanSle = env.le(loanKeylet)) - { - r.created = true; - r.loanScale = loanSle->at(sfLoanScale); - r.principal = loanSle->at(sfPrincipalOutstanding); - r.totalValue = loanSle->at(sfTotalValueOutstanding); - r.managementFee = loanSle->at(sfManagementFeeOutstanding); - r.periodicPayment = loanSle->at(sfPeriodicPayment); - } - return r; - }; - - Result const fixed = runScenario(all_, tesSUCCESS); - Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS); - - // Without the amendment, the catastrophically-cancelling closed-form - // payment factor produces a degenerate amortization that fails - // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no - // loan is created. - BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS); - BEAST_EXPECT(!legacy.created); - - // With the amendment the stable series expansion produces a valid loan - // at loanScale -10. - BEAST_EXPECT(fixed.ter == tesSUCCESS); - BEAST_EXPECT(fixed.created); - BEAST_EXPECT(fixed.loanScale == -10); - BEAST_EXPECT(fixed.principal == principalRequested); - BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10})); - BEAST_EXPECT(fixed.managementFee == beast::kZero); - - // Periodic payment from the numerically-stable series expansion, and the - // scheduled total (2 * periodicPayment) which exceeds the 1000 principal - // — no economic underpayment / yield theft. - BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16})); - BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15})); - BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested); - } - - // An overpayment whose residual amount has more precision than loanScale - // fires the isRounded(asset, overpayment, loanScale) assertion in - // computeOverpaymentComponents (and a downstream "interest paid agrees" - // assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down - // to loanScale before passing it in. The pre-amendment path can't be - // tested here because the assertion fires in Debug builds and aborts - // the test process — see the PR description for context. - void - testBugOverpayUnroundedAmount() - { - testcase("bug: computeOverpaymentComponents isRounded assertion"); - - using namespace jtx; - using namespace loan; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 100'000, - .debtMax = 5000, - .managementFeeRate = TenthBips16{1000}, - .vaultScale = 1}); - - auto const sleBroker = env.le(broker.brokerKeylet()); - if (!BEAST_EXPECT(sleBroker)) - return; - auto const loanSequence = sleBroker->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - using namespace loan; - env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32{10000}), - kPaymentTotal(12), - kPaymentInterval(60), - kGracePeriod(60), - kOverpaymentFee(TenthBips32{1000}), - kOverpaymentInterestRate(TenthBips32{1000}), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This - // has too many digits to round cleanly to loanScale=-10, so the - // overpayment residual fails the isRounded check. - STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}}; - env(pay(borrower, loanKeylet.key, payAmount), Txflags(tfLoanOverpayment), Ter(tesSUCCESS)); - env.close(); - } - - // Regression for the dual-rounding fix at coarse (integer-MPT) scale. - // - // Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The - // amortization schedule produces a fractional principal - // (~0.47) which under round-to-nearest collapses to 0 in a single - // step, causing `doPayment`'s strict `>` assertion on principal to - // fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded - // upward (sticks at 1 across the first two periods) and only clears - // in the final payment. - // - // The test pays one period at a time across three LoanPay - // transactions and verifies the loan completes (paymentRemaining=0) - // with totals matching the loan's economics (1 principal + 2 interest). - void - testIntegerScalePrincipalSticks(FeatureBitset features) - { - // Without fixCleanup3_2_0, this behavior will abort the server, so - // don't run without it. - if (!features[fixCleanup3_2_0]) - return; - - testcase("edge: integer MPT principal stuck mid-loan completes via final"); - - using namespace jtx; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(100'000), issuer, lender, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer}); - PrettyAsset const asset{mptt.issuanceID()}; - - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - - env(pay(issuer, lender, asset(10'000))); - env(pay(issuer, borrower, asset(10'000))); - env.close(); - - Vault const vault{env}; - auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); - env(vaultTx); - env.close(); - - env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)})); - env.close(); - - auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); - env(loanBroker::set(lender, vaultKeylet.key), - loanBroker::kDebtMaximum(Number{100}), - Fee(env.current()->fees().base * 2)); - env.close(); - - auto const brokerStateBefore = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerStateBefore)) - return; - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); - - env(loan::set(borrower, brokerKeylet.key, Number{1}), - Sig(sfCounterpartySignature, lender), - loan::kInterestRate(TenthBips32{50'000}), - loan::kPaymentTotal(3), - loan::kPaymentInterval(31'536'000), - Fee(env.current()->fees().base * 2)); - env.close(); - - auto const borrowerStart = env.balance(borrower, asset).value(); - - // Three separate periodic payments of 1 each. Expected per-period - // evolution at integer MPT scale (TVO = PO + interestDue + - // managementFeeDue): - // start: PO=1, TVO=3, paymentRemaining=3 - // after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks) - // after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks) - // after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears) - std::array const expectedPO{Number{1}, Number{1}, Number{0}}; - std::array const expectedTVO{Number{2}, Number{1}, Number{0}}; - std::array const expectedRemaining{2, 1, 0}; - - for (int i = 0; i < 3; ++i) - { - env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS)); - env.close(); - - auto const sle = env.le(loanKeylet); - if (!BEAST_EXPECT(sle)) - return; - BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]); - BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]); - BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]); - } - - // Borrower paid 3 total regardless of fee split (1 principal + 2 - // interest+fee, matching loan economics). - auto const borrowerEnd = env.balance(borrower, asset).value(); - BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value()); - } - - // A near-zero interest rate on a 100 USD loan - // produces total interest of ~6 units at loanScale -9. Numerical error - // in the amortization formula pushes the theoretical principal above - // the theoretical value, producing a negative theoretical interest. - // The payment delta then exceeds the actual outstanding interest, - // violating XRPL_ASSERT_PARTS in computePaymentComponents. - void - testBugInterestDueDeltaCrash() - { - testcase("bug: LoanPay asserts 'interest due delta' on near-zero rate"); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - env(trust(lender, iouAsset(1'000'000'000))); - env(trust(borrower, iouAsset(1'000'000'000))); - env(pay(issuer, lender, iouAsset(5'000'000))); - env(pay(issuer, borrower, iouAsset(5'000'000))); - env.close(); - - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000, - .debtMax = 1'000'000, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{100}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["InterestRate"] = 1; // minimum non-zero rate - createJson["PaymentTotal"] = 3; - createJson["PaymentInterval"] = 600; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - // For principal=100, n=3 the amortization schedule produces a - // periodic payment ≈ 33.33 USD. We pay 35 USD, which is more than - // one period's worth — enough for the LoanPay path to enter - // computePaymentComponents and reach the assertion that fires - // when the bug is present. With the fix, the tx applies cleanly. - env(pay(borrower, keylet.key, iouAsset(35)), Ter(tesSUCCESS)); - env.close(); - } - - // Integration test: full lifecycle of a $1B loan in the bug regime. - // Verifies that the vault collects the economically-correct interest - // income and that conservation holds at the trust-line level. - // - // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only - // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10. - // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B, - // matching the value computed independently with arbitrary-precision - // Decimal arithmetic. - void - testFullLifecycleVaultPnLNearZeroRate() - { - testcase("integration: full loan lifecycle, vault interest at near-zero rate"); - - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const trustLimit{iouAsset.raw(), Number{1, 17}}; - env(trust(lender, trustLimit)); - env(trust(borrower, trustLimit)); - env.close(); - env(pay(issuer, lender, iouAsset(5'000'000'000LL))); - env(pay(issuer, borrower, iouAsset(5'000'000'000LL))); - env.close(); - - auto usdBalance = [&](Account const& a) { - return env.balance(a, iouAsset.raw().get()).value(); - }; - STAmount const borrowerStartBal = usdBalance(borrower); - - BrokerParameters const brokerParams{ - .vaultDeposit = Number{2, 9}, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - auto const vaultBefore = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultBefore); - Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable); - - // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32. - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 9}; - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - createJson["InterestRate"] = 1; - createJson["PaymentTotal"] = 3; - createJson["PaymentInterval"] = 600; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - Number const expectedTotalInterest = - loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding); - - env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfter = env.le(broker.vaultKeylet()); - Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable); - Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore; - - STAmount const borrowerEndBal = usdBalance(borrower); - STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal; - - // Self-consistency: vault gained exactly the expected interest - // computed at LoanSet, and the borrower's outflow matches. - BEAST_EXPECT(vaultGain == expectedTotalInterest); - BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest); - - // Mathematical correctness: the total interest for this loan - // configuration is 0.38051750382930729983, calculated - // independently using 50-digit Decimal arithmetic (no - // cancellation possible at that precision). At Number's 19-digit - // mantissa this rounds to 0.38051750382930729 — the literal - // below. The vault's actual gain must agree to within - // sub-microcent precision. - Number const decimalReference{38051750382930729LL, -17}; - Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent - Number const error = abs(vaultGain - decimalReference); - BEAST_EXPECTS( - error < tolerance, - "vault gain " + to_string(vaultGain) + " differs from Decimal reference " + - to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " + - to_string(tolerance)); - } - - // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the - // same vault-scale minimum cover when fixCleanup3_2_0 is enabled. - // Before the amendment, each transactor computed its minimum cover at a - // different precision (loanScale, debtScale, or the raw unrounded - // tenthBipsOfValue), which could lead to inconsistent decisions for the - // same broker state. After the amendment all three use - // minimumBrokerCover at vaultScale. - void - testMinimumBrokerCoverConsistency(FeatureBitset features) - { - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - bool const withAmendment = features[fixCleanup3_2_0]; - - struct Ctx - { - jtx::Account issuer; - jtx::Account lender; - jtx::Account borrower; - jtx::PrettyAsset iou; - BrokerInfo broker; - BrokerParameters brokerParams; - }; - - // Shared setup, parametrized by vaultDeposit (the only varying setup - // field across the three scenarios). Each call runs in its own Env - // so multiple invocations within one scenario cannot interfere. - // The caller is responsible for invoking testcase(...) before the - // first runTest call of each scenario. - auto runTest = [&](Number vaultDeposit, auto&& body) { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - // Enable clawback on the issuer *before* any trust lines exist - // (asfAllowTrustLineClawback requires an empty owner directory). - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const iou = issuer[iouCurrency_]; - env(trust(lender, iou(1'000'000'000))); - env(trust(borrower, iou(1'000'000'000))); - env.close(); - env(pay(issuer, lender, iou(100'000'000))); - env(pay(issuer, borrower, iou(100'000'000))); - env.close(); - - // 13.37% — non-round rate produces a messier minimum. - BrokerParameters const brokerParams{ - .vaultDeposit = vaultDeposit, - .debtMax = 0, - .coverRateMin = TenthBips32{13'370}, - .coverDeposit = 5'000, - .managementFeeRate = TenthBips16{500}}; - - BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams); - - body( - env, - Ctx{.issuer = issuer, - .lender = lender, - .borrower = borrower, - .iou = iou, - .broker = broker, - .brokerParams = brokerParams}); - }; - - // Scenario 1 — LoanPay - // - // Verify that LoanPay's minimum cover check uses vault scale (not - // loan scale). Before the amendment, different loans could produce - // different fee routing decisions for the same broker-level state. - // Small vault deposit => vaultScale = -12. - testcase("LoanPay minimum cover scale consistency"); - { - struct LoanKeylets - { - Keylet tiny; - Keylet big; - }; - - // Create the tiny + big loans and reduce cover via clawback so - // that subsequent LoanPay calls hit the minimum-cover boundary. - // Used by the two pay-and-check sub-tests below so each can run - // in its own Env. - auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional { - Asset const asset{c.iou}; - - // Create the TINY loan first (while vaultScale is still - // small). principal 0.01, 0% interest, 1 payment => - // loanScale = vaultScale. - auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle1)) - return std::nullopt; - auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence); - auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq); - - env(set(c.borrower, c.broker.brokerID, Number{1, -2}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Create the BIG loan second. 100% annual interest over 20 - // payments pushes totalValueOutstanding high enough that - // loanScale > vaultScale. - auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle2)) - return std::nullopt; - auto const bigLoanSeq = brokerSle2->at(sfLoanSequence); - auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq); - - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // The tiny loan's scale is frozen at the vault's pre-big-loan - // scale, so it is strictly smaller than the big loan's. - // After the big loan is created the vault absorbs its value, - // pushing vaultScale up to match bigLoanScale. - auto const tinyLoanSle = env.le(tinyLoanKeylet); - auto const bigLoanSle = env.le(bigLoanKeylet); - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) || - !BEAST_EXPECT(vaultSle)) - return std::nullopt; - if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) || - !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) || - !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11)) - return std::nullopt; - - // Use issuer clawback to reduce cover to the minimum the - // clawback transactor allows. Compute the amount as - // initialCover - expectedCoverAfter so we exercise the exact - // clawback rather than relying on the transactor to clip - // down. - // - // Before the amendment the clawback minimum is the - // *unrounded* tenthBipsOfValue — strictly less than the - // rounded-at-vaultScale minimum LoanPay uses for the big - // loan. After the amendment both clawback and LoanPay use - // the same rounded minimum (via minimumBrokerCover), so - // cover lands exactly at that threshold. - Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} - : Number{1330651855688458000, -15}; - Number const clawbackAmount = - Number{c.brokerParams.coverDeposit} - expectedCoverAfter; - - env(coverClawback(c.issuer), - kLoanBrokerId(c.broker.brokerID), - kAmount(STAmount{asset, clawbackAmount})); - env.close(); - - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle) || - !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter)) - return std::nullopt; - - return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet}; - }; - - // Pay one loan and report whether the fee went to the broker's - // pseudo account (the fallback when cover < minimum) rather - // than to the owner. - auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool { - Asset const asset{c.iou}; - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return false; - auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount)); - auto const pseudoBefore = env.balance(pseudoAcct, c.iou); - - auto const payLoan = env.le(loanKeylet); - if (!BEAST_EXPECT(payLoan)) - return false; - auto const periodicPayment = payLoan->at(sfPeriodicPayment); - auto const serviceFee = payLoan->at(sfLoanServiceFee); - std::int32_t const loanScale = payLoan->at(sfLoanScale); - - auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale); - auto const payAmt = STAmount{asset, payment + serviceFee}; - - env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10))); - env.close(); - - auto const pseudoAfter = env.balance(pseudoAcct, c.iou); - return pseudoAfter.number() > pseudoBefore.number(); - }; - - // Pay the BIG loan in its own Env so its outcome cannot affect - // the TINY-loan check. With the fix, LoanPay and clawback use - // the same vaultScale minimum (cover == minAtVaultScale => - // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11, - // rounds up to a larger minimum than what clawback used => - // cover < min => fee to pseudo. - runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - auto const loans = setupLoansAndClawback(env, c); - if (!loans) - return; - BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment); - }); - - // Pay the TINY loan in its own Env. Fee goes to the owner - // either way: - // - With the fix: LoanPay uses vaultScale=-11 (same as - // clawback) => owner. - // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds - // up at -12 (a no-op) => min == cover => owner. - runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - auto const loans = setupLoansAndClawback(env, c); - if (!loans) - return; - BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny)); - }); - } - - // Scenario 2 — LoanBrokerCoverWithdraw - // - // Verify that CoverWithdraw's minimum cover check uses vault scale - // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw - // used: - // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset)) - // which could disagree with LoanPay's minimum (which used loanScale). - // - // Use a large vault deposit so that vaultScale (from AssetsTotal) is - // strictly larger than debtScale (from DebtTotal). With - // vaultDeposit = 100,000: after the big loan - // AssetsTotal ≈ 109,500 → vaultScale = -10 - // DebtTotal ≈ 10,000 → debtScale = -11 - // The one-order-of-magnitude gap makes roundToAsset at -10 truncate - // more aggressively than at -11, exposing the bug. - testcase("CoverWithdraw minimum cover scale consistency"); - runTest( - /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) { - Asset const asset{c.iou}; - - // Create only the big loan to push DebtTotal up to ~10,000 - // while AssetsTotal stays around 109,500 (dominated by the - // large vault deposit). - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Read broker state and compute both old and new minimums. - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle)) - return; - - auto const coverAvail = brokerSle->at(sfCoverAvailable); - auto const debtTotal = brokerSle->at(sfDebtTotal); - auto const vaultScale = getAssetsTotalScale(vaultSle); - auto const debtScale = scale(debtTotal, asset); - - // Sanity: debt scale differs from vault scale for this setup. - BEAST_EXPECT(debtScale < vaultScale); - - auto const oldMin = [&]() { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - return roundToAsset( - asset, - tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}), - debtScale); - }(); - auto const newMin = minimumBrokerCover( - debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle); - - // The new (vaultScale) minimum must be strictly larger than - // the old (debtScale) minimum — that is the gap the amendment - // closes. - Number const expectedNewMin{1330650518688500000, -15}; - Number const expectedOldMin{1330650518688472000, -15}; - BEAST_EXPECT(newMin == expectedNewMin); - BEAST_EXPECT(oldMin == expectedOldMin); - - // Try to withdraw so that remaining cover lands between the - // two minimums: oldMin < target < newMin. - auto const target = oldMin + (newMin - oldMin) / 2; - auto const withdrawAmount = STAmount{asset, coverAvail - target}; - - if (withAmendment) - { - // CoverWithdraw now uses vaultScale: target < newMin - // => FAILS. - env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount), - Ter(tecINSUFFICIENT_FUNDS)); - } - else - { - // Old CoverWithdraw uses debtScale: target > oldMin - // => SUCCEEDS. - env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount)); - } - env.close(); - }); - - // Scenario 3 — LoanSet - // - // Verify that LoanSet's minimum cover check uses vault scale (not the - // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet - // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no - // roundToAsset), while clawback/withdraw used different formulas. - // After the amendment all use minimumBrokerCover at vaultScale, and - // rounding at a coarser scale can absorb a tiny debt increase — - // allowing a loan that would otherwise be rejected. - testcase("LoanSet minimum cover scale consistency"); - runTest( - /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - // Create the tiny loan (scale -12) AND the big loan (scale - // -11). Both loans are needed so that DebtTotal has a full - // 16-digit mantissa — a "messy" value where roundToAsset at - // vaultScale actually truncates digits and produces a - // different result from the raw tenthBipsOfValue. With only - // the big loan, DebtTotal has ~4 significant digits and - // rounding at scale -11 is a no-op, masking the amendment's - // effect. - env(set(c.borrower, c.broker.brokerID, Number{1, -2}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Clawback to reduce cover to the clawback transactor's - // minimum. Pass the exact amount rather than relying on the - // transactor to clip down; the setup matches Scenario 1 so - // the same residual-cover values apply. - Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} - : Number{1330651855688458000, -15}; - Number const clawbackAmount = - Number{c.brokerParams.coverDeposit} - expectedCoverAfter; - env(coverClawback(c.issuer), - kLoanBrokerId(c.broker.brokerID), - kAmount(c.iou(clawbackAmount))); - env.close(); - - // Verify scales. - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(vaultSle)) - return; - auto const vaultScale = getAssetsTotalScale(vaultSle); - BEAST_EXPECT(vaultScale == -11); - - // Now try to create a tiny additional loan. Principal is - // 1e-11 (the smallest value that survives the precision - // check at loanScale = vaultScale = -11), with 0% interest - // and 1 payment. - // - // The tiny debt increase adds ~1.337e-12 to the unrounded - // minimum. - // - Without the amendment: the old LoanSet formula rounds - // up during tenthBipsOfValue (16-digit Number - // normalisation), pushing the minimum past the cover left - // by clawback => tecINSUFFICIENT_FUNDS. - // - With the amendment: minimumBrokerCover rounds at - // vaultScale=-11, which absorbs the tiny increase — the - // rounded minimum stays the same => tesSUCCESS. - auto const tinyPrincipal = Number{1, -11}; - - if (withAmendment) - { - env(set(c.borrower, c.broker.brokerID, tinyPrincipal), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - } - else - { - env(set(c.borrower, c.broker.brokerID, tinyPrincipal), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10)), - Ter(tecINSUFFICIENT_FUNDS)); - } - env.close(); - }); - } - - void - runAmendmentIndependent() - { - testDisabled(); - testInvalidLoanSet(); - testInvalidLoanDelete(); - testInvalidLoanManage(); - testInvalidLoanPay(); - testIssuerLoan(); - testServiceFeeOnBrokerDeepFreeze(); - testRequireAuth(); - testRIPD3901(); - testBorrowerIsBroker(); - testLimitExceeded(); - testLendingCanTradeDisabledNoImpact(); - testBugOverpaymentPrincipalChange(); - testBugOverpayUnroundedAmount(); - - for (auto const flags : {0u, tfLoanOverpayment}) - testYieldTheftRounding(flags); - testBugInterestDueDeltaCrash(); - testFullLifecycleVaultPnLNearZeroRate(); - testLoanSetNearZeroInterestRateSucceeds(); - } - - // Tests run under each entry in amendmentCombinations(). - void - runAmendmentSensitive(FeatureBitset features) - { -#if LOAN_TODO - testLoanPayLateFullPaymentBypassesPenalties(features); - testLoanCoverMinimumRoundingExploit(features); -#endif - // Lifecycle - testLifecycle(features); - testLoanSet(features); - testDosLoanPay(features); - testSelfLoan(features); - - // Payment paths - testWithdrawReflectsUnrealizedLoss(features); - testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); - testBatchBypassCounterparty(features); - testLoanNextPaymentDueDateOverflow(features); - testSequentialFLCDepletion(features); - - // Invariants - testLoanPayComputePeriodicPaymentValidRateInvariant(features); - testAccountSendMptMinAmountInvariant(features); - testLoanPayDebtDecreaseInvariant(features); - testWrongMaxDebtBehavior(features); - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(features); - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(features); - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(features); - - // RPC - testRPC(features); - - // Edge / rounding - testDustManipulation(features); - testRoundingAllowsUndercoverage(features); - testOverpaymentManagementFee(features); - testIssuerIsBorrower(features); - testIntegerScalePrincipalSticks(features); - testMinimumBrokerCoverConsistency(features); - - // RIPD regressions - testRIPD3831(features); - testRIPD3459(features); - testRIPD3902(features); - - // Broker-owner permissions - testLoanPayBrokerOwnerMissingTrustline(features); - testLoanPayBrokerOwnerUnauthorizedMPT(features); - testLoanPayBrokerOwnerNoPermissionedDomainMPT(features); - testLoanSetBrokerOwnerNoPermissionedDomainMPT(features); - } - -public: - void - run() override - { - runAmendmentIndependent(); - for (auto const& features : jtx::amendmentCombinations( - {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) - runAmendmentSensitive(features); - } -}; - -class LoanBatch_test : public Loan_test -{ -protected: - beast::xor_shift_engine engine_; - - std::uniform_int_distribution<> assetDist_{0, 2}; - std::uniform_int_distribution principalDist_{100'000, 1'000'000'000}; - std::uniform_int_distribution interestRateDist_{0, 10000}; - std::uniform_int_distribution<> paymentTotalDist_{12, 10000}; - std::uniform_int_distribution<> paymentIntervalDist_{60, 3600 * 24 * 30}; - std::uniform_int_distribution managementFeeRateDist_{0, 10'000}; - std::uniform_int_distribution<> serviceFeeDist_{0, 20}; - /* - # Generate parameters that are more likely to be valid - principal = Decimal(str(rand.randint(100000, - 100'000'000))).quantize(ROUND_TARGET) - - interest_rate = Decimal(rand.randint(1, 10000)) / - Decimal(100000) - - payment_total = rand.randint(12, 10000) - - payment_interval = Decimal(str(rand.randint(60, 2629746))) - - interest_fee = Decimal(rand.randint(0, 100000)) / - Decimal(100000) -*/ - - void - testRandomLoan() - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - // Determine all the random parameters at once - auto const assetType = static_cast(assetDist_(engine_)); - auto const principalRequest = principalDist_(engine_); - TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; - auto const serviceFee = serviceFeeDist_(engine_); - TenthBips32 interest{interestRateDist_(engine_)}; - auto const payTotal = paymentTotalDist_(engine_); - auto const payInterval = paymentIntervalDist_(engine_); - - BrokerParameters const brokerParams{ - .vaultDeposit = principalRequest * 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = managementFeeRate}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = principalRequest, - .serviceFee = serviceFee, - .interest = interest, - .payTotal = payTotal, - .payInterval = payInterval, - }; - - runLoan(assetType, brokerParams, loanParams, all_); - } - -public: - void - run() override - { - auto const numIterations = [s = arg()]() -> int { - int const defaultNum = 5; - if (s.empty()) - return defaultNum; - try - { - std::size_t pos = 0; - auto const r = stoi(s, &pos); - if (pos != s.size()) - return defaultNum; - return r; - } - catch (...) - { - return defaultNum; - } - }(); - - using namespace jtx; - - auto const updateInterval = std::min(numIterations / 5, 100); - - for (int i = 0; i < numIterations; ++i) - { - if (i % updateInterval == 0) - testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations; - testRandomLoan(); - } - } -}; - -class LoanArbitrary_test : public LoanBatch_test -{ - void - run() override - { - using namespace jtx; - - BrokerParameters const brokerParams{ - .vaultDeposit = 10000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = Account("lender"), - .counter = Account("borrower"), - .principalRequest = Number{200000, -6}, - .interest = TenthBips32{50000}, - .payTotal = 2, - .payInterval = 200}; - - runLoan(AssetType::XRP, brokerParams, loanParams, all_); - } -}; - -BEAST_DEFINE_TESTSUITE(Loan, tx, xrpl); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, xrpl); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl); - -} // namespace xrpl::test diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 7fc7161e36..33721e91d8 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -4324,6 +4324,165 @@ public: env.require(Balance(bob, gwUSD(10))); } + void + testDisallowIncomingTrustline(FeatureBitset features) + { + testcase("DisallowIncomingTrustline in OfferCreate"); + + // Test that asfDisallowIncomingTrustline flag prevents offer crossing + // when the taker doesn't have a trustline. + // + // 1. alice creates a trustline and sells USD/gw tokens. + // + // 2. gw sets asfDisallowIncomingTrustline flag. + // + // 3. An account without a trustline tries to create an offer for USD/gw. + // Without amendment: succeeds and crosses alice's offer (backward compatible). + // With amendment: fails with tecNO_LINE (new behavior). + // + // 4. An account WITH an existing trustline can create an offer. + // The offer succeeds and crosses alice's offer. + // + // Note: The DisallowIncomingTrustline flag also prevents NEW trustlines + // from being created via TrustSet (enforced by fixDisallowIncomingV1). + // So accounts must create trustlines BEFORE the issuer sets the flag. + + using namespace jtx; + auto const gw = Account("gw"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dan = Account("dan"); + auto const eve = Account("eve"); + auto const gwUSD = gw["USD"]; + + // Test without fixCleanup3_4_0 amendment + { + Env env{*this, features - fixCleanup3_4_0}; + + env.fund(XRP(400000), gw, alice, bob); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Without the amendment, bob can still create offer without trustline + // and the offer should cross (old behavior) + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(bob, gwUSD(40))); + } + + // Test with fixCleanup3_4_0 amendment + { + Env env{*this, features}; + + env.fund(XRP(400000), gw, alice, bob, carol, dan); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Bob and carol create trustlines BEFORE the flag is set + env(trust(bob, gwUSD(100))); + env.close(); + env(trust(carol, gwUSD(100))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Dan tries to create offer without trustline - should fail + env(offer(dan, gwUSD(40), XRP(4000)), Ter(tecNO_LINE)); + env.close(); + + // Alice's offer should still exist + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // Dan shouldn't have any offers or balance + env.require(offers(dan, 0)); + BEAST_EXPECT(env.le(keylet::trustLine(dan, gwUSD)) == nullptr); + + // Bob already has trustline, so his offer should succeed and cross + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(alice, gwUSD(10))); + env.require(Balance(bob, gwUSD(40))); + + // Test scenario where carol already has a trustline (created before flag was set) + // Carol should be able to create offer since trustline already exists + env(pay(gw, alice, gwUSD(50))); + env.close(); + env(offer(alice, XRP(1000), gwUSD(10))); + env.close(); + env.require(offers(alice, 1)); + + env(offer(carol, gwUSD(10), XRP(1000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(carol, 0)); + env.require(Balance(alice, gwUSD(50))); + env.require(Balance(carol, gwUSD(10))); + + // Test that gw can clear the flag + env(fclear(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Create new account eve without trustline + env.fund(XRP(400000), eve); + env.close(); + + // Bob creates another sell offer + env(pay(gw, bob, gwUSD(50))); + env.close(); + env(offer(bob, XRP(5000), gwUSD(50))); + env.close(); + env.require(offers(bob, 1)); + + // Eve should now be able to create offer without trustline (flag is cleared) + env(offer(eve, gwUSD(50), XRP(5000))); + env.close(); + + // Offer should have crossed + env.require(offers(bob, 0)); + env.require(offers(eve, 0)); + env.require(Balance(eve, gwUSD(50))); + } + } + void testRCSmoketest(FeatureBitset features) { @@ -5167,6 +5326,7 @@ public: testSelfPayUnlimitedFunds(features); testRequireAuth(features); testMissingAuth(features); + testDisallowIncomingTrustline(features); testRCSmoketest(features); testSelfAuth(features); testDeletedOfferIssuer(features); diff --git a/src/test/app/PathMPT_test.cpp b/src/test/app/PathMPT_test.cpp index 3ba67b58a6..ff4a024cb8 100644 --- a/src/test/app/PathMPT_test.cpp +++ b/src/test/app/PathMPT_test.cpp @@ -112,10 +112,10 @@ public: MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 100}); auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -125,39 +125,39 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; json::Value result; Gate g; - // Test RPC::Tuning::max_src_cur source currencies. + // Test rpc::tuning::max_src_cur source currencies. std::vector numSrc; - numSrc.reserve(RPC::Tuning::kMaxSrcCur); - for (std::uint8_t i = 0; i < RPC::Tuning::kMaxSrcCur; ++i) + numSrc.reserve(rpc::tuning::kMaxSrcCur); + for (std::uint8_t i = 0; i < rpc::tuning::kMaxSrcCur; ++i) numSrc.push_back(makeMptID(i, bob)); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_src_cur source currencies. - numSrc.push_back(makeMptID(RPC::Tuning::kMaxSrcCur, bob)); + // Test more than rpc::tuning::max_src_cur source currencies. + numSrc.push_back(makeMptID(rpc::tuning::kMaxSrcCur, bob)); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(result.isMember(jss::error)); - // Test RPC::Tuning::max_auto_src_cur source currencies. + // Test rpc::tuning::max_auto_src_cur source currencies. numSrc.clear(); - for (auto i = 0; i < (RPC::Tuning::kMaxAutoSrcCur - 1); ++i) + for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i) { auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}}); numSrc.push_back(curm.issuanceID()); @@ -165,18 +165,18 @@ public: app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, {}); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_auto_src_cur source currencies. + // Test more than rpc::tuning::max_auto_src_cur source currencies. auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}}); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, {}); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 8f19a419a0..409a9e86f8 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -151,10 +151,10 @@ public: using namespace jtx; auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -164,7 +164,7 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; @@ -190,7 +190,7 @@ public: app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = std::move(params); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); @@ -262,10 +262,10 @@ public: env.close(); auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -275,49 +275,49 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; json::Value result; Gate g; - // Test RPC::Tuning::max_src_cur source currencies. + // Test rpc::tuning::max_src_cur source currencies. app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { - context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::kMaxSrcCur); + context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_src_cur source currencies. + // Test more than rpc::tuning::max_src_cur source currencies. app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { - context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::kMaxSrcCur + 1); + context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur + 1); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(result.isMember(jss::error)); - // Test RPC::Tuning::max_auto_src_cur source currencies. - for (auto i = 0; i < (RPC::Tuning::kMaxAutoSrcCur - 1); ++i) + // Test rpc::tuning::max_auto_src_cur source currencies. + for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i) env.trust(Account("alice")[std::to_string(i + 100)](100), "bob"); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = rpf(Account("alice"), Account("bob"), 0); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_auto_src_cur source currencies. + // Test more than rpc::tuning::max_auto_src_cur source currencies. env.trust(Account("alice")["AUD"](100), "bob"); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = rpf(Account("alice"), Account("bob"), 0); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 67cb7602a0..68b2fa99a7 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -1816,7 +1816,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.fund(XRP(1000), carol); env.close(); - env(ledgerStateFix::bookExchangeRate(carol, uint256{1}), Ter(temDISABLED)); + env(ledger_state_fix::bookExchangeRate(carol, uint256{1}), Ter(temDISABLED)); } { @@ -1829,13 +1829,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // BookExchangeRate fixes require sfBookDirectory. - auto missingBookDirectory = ledgerStateFix::bookExchangeRate(carol, uint256{1}); + auto missingBookDirectory = ledger_state_fix::bookExchangeRate(carol, uint256{1}); missingBookDirectory.removeMember(sfBookDirectory.jsonName); env(missingBookDirectory, Ter(temINVALID)); // BookExchangeRate fixes reject fields that belong to other // LedgerStateFix types. - auto extraOwner = ledgerStateFix::bookExchangeRate(carol, uint256{1}); + auto extraOwner = ledger_state_fix::bookExchangeRate(carol, uint256{1}); extraOwner[sfOwner.jsonName] = carol.human(); env(extraOwner, Ter(temINVALID)); } @@ -1847,7 +1847,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite { // Preclaim check: the target directory must exist. - env(ledgerStateFix::bookExchangeRate(setup.carol, uint256{1}), + env(ledger_state_fix::bookExchangeRate(setup.carol, uint256{1}), Fee(fixFee), Ter(tecOBJECT_NOT_FOUND)); } @@ -1861,7 +1861,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(ownerDirSle); BEAST_EXPECT(!ownerDirSle->isFieldPresent(sfExchangeRate)); - env(ledgerStateFix::bookExchangeRate(setup.carol, ownerDir.key), + env(ledger_state_fix::bookExchangeRate(setup.carol, ownerDir.key), Fee(fixFee), Ter(tecNO_PERMISSION)); } @@ -1885,7 +1885,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(exchangeRate == quality); } - env(ledgerStateFix::bookExchangeRate(setup.carol, dirKey), + env(ledger_state_fix::bookExchangeRate(setup.carol, dirKey), Fee(fixFee), Ter(tecNO_PERMISSION)); } @@ -1932,7 +1932,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); auto const fixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::bookExchangeRate(carol_, openDirKey), Fee(fixFee)); + env(ledger_state_fix::bookExchangeRate(carol_, openDirKey), Fee(fixFee)); env.close(); // Confirm sfExchangeRate now matches the key quality. @@ -1947,7 +1947,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite } // Submitting again should fail — nothing to fix. - env(ledgerStateFix::bookExchangeRate(carol_, openDirKey), + env(ledger_state_fix::bookExchangeRate(carol_, openDirKey), Fee(fixFee), Ter(tecNO_PERMISSION)); } diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 537ee4c177..6ee7442d23 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -60,7 +60,7 @@ class SHAMapStore_test : public beast::unit_test::Suite static bool goodLedger(jtx::Env& env, json::Value const& json, std::string ledgerID, bool checkDB = false) { - auto good = json.isMember(jss::result) && !RPC::containsError(json[jss::result]) && + auto good = json.isMember(jss::result) && !rpc::containsError(json[jss::result]) && json[jss::result][jss::ledger][jss::ledger_index] == ledgerID; if (!good || !checkDB) return good; @@ -99,7 +99,7 @@ class SHAMapStore_test : public beast::unit_test::Suite static bool bad(json::Value const& json, ErrorCodeI error = RpcLgrNotFound) { - return json.isMember(jss::result) && RPC::containsError(json[jss::result]) && + return json.isMember(jss::result) && rpc::containsError(json[jss::result]) && json[jss::result][jss::error_code] == error; } @@ -347,11 +347,11 @@ public: BEAST_EXPECT(lastRotated != 2); auto canDelete = env.rpc("can_delete"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); canDelete = env.rpc("can_delete", "never"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); auto const firstBatch = kDeleteInterval + ledgerSeq; @@ -370,7 +370,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", std::to_string(ledgerSeq + (kDeleteInterval / 2))); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + (kDeleteInterval / 2)); store.rendezvous(); @@ -423,7 +423,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", "always"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT( canDelete[jss::result][jss::can_delete] == std::numeric_limits::max()); @@ -457,7 +457,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", "now"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq - 1); for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq) diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index f20aac68f9..393a6e58f7 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1664,11 +1664,11 @@ public: env.close(); auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); - env(loanBroker::set(alice, vaultKeylet.key), - loanBroker::kDebtMaximum(xrpAsset(1000).value()), - loanBroker::kManagementFeeRate(TenthBips16{0}), - loanBroker::kCoverRateMinimum(TenthBips32{0}), - loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env(loan_broker::set(alice, vaultKeylet.key), + loan_broker::kDebtMaximum(xrpAsset(1000).value()), + loan_broker::kManagementFeeRate(TenthBips16{0}), + loan_broker::kCoverRateMinimum(TenthBips32{0}), + loan_broker::kCoverRateLiquidation(TenthBips32{0})); env.close(); auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); @@ -2019,7 +2019,7 @@ public: env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); env.close(); - env(ledgerStateFix::nftPageLinks(alice, alice), + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(fixFee), sponsor::As(sponsor, spfSponsorFee), Ter(tecFAILED_PROCESSING)); @@ -2043,7 +2043,7 @@ public: OpenView overlay(&*env.closed()); auto jt = env.jt( - ledgerStateFix::nftPageLinks(alice, alice), + ledger_state_fix::nftPageLinks(alice, alice), Fee(fixFee), sponsor::As(sponsor, spfSponsorFee)); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 3175e742d9..1fe48add27 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -2571,7 +2571,7 @@ public: auto fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; BEAST_EXPECT( @@ -2600,7 +2600,7 @@ public: fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; BEAST_EXPECT( @@ -3225,7 +3225,7 @@ public: { auto const info = env.rpc("json", "account_info", to_string(prevLedgerWithQueue)); - BEAST_EXPECT(info.isMember(jss::result) && RPC::containsError(info[jss::result])); + BEAST_EXPECT(info.isMember(jss::result) && rpc::containsError(info[jss::result])); } env.close(); @@ -4630,7 +4630,7 @@ public: auto const fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; @@ -4688,7 +4688,7 @@ public: auto const fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 12ad7e6782..791cf216c3 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3936,7 +3936,7 @@ class Vault_test : public beast::unit_test::Suite // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60. // Clawback 80 IOU → clamped to 60, then share math uses truncation. testCase(1, [&, this](Env& env, Data d) { - using namespace loanBroker; + using namespace loan_broker; using namespace loan; testcase("Scale clawback clamped with outstanding loan"); @@ -4410,7 +4410,7 @@ class Vault_test : public beast::unit_test::Suite testVaultClawbackBurnShares() { using namespace test::jtx; - using namespace loanBroker; + using namespace loan_broker; using namespace loan; Env env(*this, beast::Severity::Warning); @@ -4670,7 +4670,7 @@ class Vault_test : public beast::unit_test::Suite testVaultClawbackAssets() { using namespace test::jtx; - using namespace loanBroker; + using namespace loan_broker; using namespace loan; Env env(*this); env.enableFeature(fixCleanup3_1_3); @@ -6093,7 +6093,7 @@ class Vault_test : public beast::unit_test::Suite // Loan broker: no cover, no management fee, debt cap 10x principal. f.brokerID = keylet::loanBroker(f.lender.id(), env.seq(f.lender)).key; { - using namespace loanBroker; + using namespace loan_broker; env(set(f.lender, vaultKeylet.key), kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value())); env.close(); @@ -7645,6 +7645,83 @@ class Vault_test : public beast::unit_test::Suite } } + void + testVaultCreateLEVersion() + { + using namespace test::jtx; + + Account const owner{"owner"}; + PrettyAsset const xrpAsset = xrpIssue(); + + { + testcase("VaultCreate LEVersion: featureLendingProtocolV1_1 disabled, field absent"); + Env env{*this}; + env.disableFeature(featureLendingProtocolV1_1); + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, Ter(tesSUCCESS)); + env.close(); + + auto const sleVault = env.le(keylet); + BEAST_EXPECT(sleVault); + BEAST_EXPECT(!sleVault->isFieldPresent(sfLEVersion)); + } + + { + testcase( + "VaultCreate LEVersion: featureLendingProtocolV1_1 enabled, LEVersion == " + "VaultVersion::CashBasis"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, Ter(tesSUCCESS)); + env.close(); + + auto const sleVault = env.le(keylet); + BEAST_EXPECT(sleVault); + BEAST_EXPECT(sleVault->isFieldPresent(sfLEVersion)); + BEAST_EXPECT(sleVault->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis)); + } + + { + testcase("VaultCreate rejects LEVersion set in the transaction"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + tx[sfLEVersion] = 2; + env(tx, Ter(temMALFORMED)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + } + + { + testcase("VaultSet rejects LEVersion set in the transaction"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [createTx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(createTx, Ter(tesSUCCESS)); + env.close(); + + auto setTx = vault.set({.owner = owner, .id = keylet.key}); + setTx[sfLEVersion] = 2; + env(setTx, Ter(temMALFORMED)); + env.close(); + } + } + void testVaultDepositFreezeIOU() { @@ -8317,6 +8394,7 @@ public: testVaultEscrowedMPT(); testAssetsMaximum(); testVaultDeleteMemoData(); + testVaultCreateLEVersion(); testBug6LimitBypassWithShares(); testRemoveEmptyHoldingLockedAmount(); testRemoveEmptyHoldingConfidentialBalances(); diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/lending/LendingHelpers_test.cpp similarity index 83% rename from src/test/app/LendingHelpers_test.cpp rename to src/test/app/lending/LendingHelpers_test.cpp index ac8e0764fc..5d67cdc3c5 100644 --- a/src/test/app/LendingHelpers_test.cpp +++ b/src/test/app/lending/LendingHelpers_test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include namespace xrpl::test { @@ -1470,6 +1472,326 @@ class LendingHelpers_test : public beast::unit_test::Suite Number{-18304, -5})); } + void + testAccrualLoanOriginationDeltas() + { + using namespace xrpl::accrual; + + struct TestCase + { + std::string name; + Number principalRequested; + Number interestDue; + }; + + auto const testCases = std::vector{ + {.name = "Zero interest", + .principalRequested = Number{1'000}, + .interestDue = Number{0}}, + {.name = "Nonzero interest", + .principalRequested = Number{1'000}, + .interestDue = Number{75}}, + }; + + for (auto const& tc : testCases) + { + testcase("accrual::loanOriginationDeltas: " + tc.name); + + auto const deltas = loanOriginationDeltas(tc.principalRequested, tc.interestDue); + BEAST_EXPECTS( + deltas.assetsTotalDelta == tc.interestDue, + "assetsTotalDelta mismatch: expected " + to_string(tc.interestDue) + ", got " + + to_string(deltas.assetsTotalDelta)); + BEAST_EXPECTS( + deltas.debtTotalDelta == tc.principalRequested + tc.interestDue, + "debtTotalDelta mismatch: expected " + + to_string(tc.principalRequested + tc.interestDue) + ", got " + + to_string(deltas.debtTotalDelta)); + } + } + + void + testCashBasisLoanOriginationDeltas() + { + using namespace xrpl::cash_basis; + + testcase("cash_basis::loanOriginationDeltas: interestDue is ignored"); + + Number const principalRequested{1'000}; + Number const interestDue{75}; + + auto const deltas = loanOriginationDeltas(principalRequested); + BEAST_EXPECTS( + deltas.assetsTotalDelta == 0, + "assetsTotalDelta mismatch: expected 0, got " + to_string(deltas.assetsTotalDelta)); + BEAST_EXPECTS( + deltas.debtTotalDelta == principalRequested, + "debtTotalDelta mismatch: expected " + to_string(principalRequested) + ", got " + + to_string(deltas.debtTotalDelta)); + } + + void + testAccrualLoanOriginationExceedsVaultMaximum() + { + using namespace xrpl::accrual; + + struct TestCase + { + std::string name; + Number vaultMaximum; + Number vaultTotal; + Number interestDue; + bool expected; + }; + + auto const testCases = std::vector{ + {.name = "No maximum configured", + .vaultMaximum = Number{0}, + .vaultTotal = Number{900}, + .interestDue = Number{1'000}, + .expected = false}, + {.name = "Interest fits under headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{50}, + .expected = false}, + {.name = "Interest exactly fills headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{100}, + .expected = false}, + {.name = "Interest exceeds headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{101}, + .expected = true}, + }; + + for (auto const& tc : testCases) + { + testcase("accrual::loanOriginationExceedsVaultMaximum: " + tc.name); + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum( + tc.vaultMaximum, tc.vaultTotal, tc.interestDue) == tc.expected); + } + } + + // Constructs a minimal ltLOAN SLE with just the fields needed by + // loanVaultExposure. Mirrors the bare-SLE pattern used by + // testCanApplyToBrokerCover for ltLOAN_BROKER. + static std::shared_ptr + makeLoanSle( + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding) + { + auto sle = std::make_shared(ltLOAN, uint256{1u}); + sle->at(sfTotalValueOutstanding) = totalValueOutstanding; + sle->at(sfPrincipalOutstanding) = principalOutstanding; + sle->at(sfManagementFeeOutstanding) = managementFeeOutstanding; + return sle; + } + + // Constructs a minimal ltVAULT SLE with just LEVersion set (or left + // absent), for exercising the dispatchers' per-Vault gating. + static std::shared_ptr + makeVaultSle( + std::optional leVersion = std::nullopt, + std::optional assetsMaximum = std::nullopt, + std::optional assetsTotal = std::nullopt) + { + auto sle = std::make_shared(ltVAULT, uint256{2u}); + if (leVersion) + sle->at(sfLEVersion) = std::to_underlying(*leVersion); + if (assetsMaximum) + sle->at(sfAssetsMaximum) = *assetsMaximum; + if (assetsTotal) + sle->at(sfAssetsTotal) = *assetsTotal; + return sle; + } + + void + testAccrualLoanVaultExposure() + { + testcase("accrual::loanVaultExposure"); + + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT(xrpl::accrual::loanVaultExposure(sle) == Number{950}); + } + + void + testCashBasisLoanVaultExposure() + { + testcase("cash_basis::loanVaultExposure"); + + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT(xrpl::cash_basis::loanVaultExposure(sle) == Number{800}); + } + + void + testLoanPaymentDeltas() + { + // principalPaid, interestPaid, feePaid, valueChange are all distinct + // and nonzero, with a nonzero valueChange simulating a late-payment + // penalty, so Accrual's formula is meaningfully exercised. + LoanPaymentParts const parts{ + .principalPaid = Number{100}, + .interestPaid = Number{20}, + .valueChange = Number{5}, + .feePaid = Number{3}}; + + { + testcase("accrual::loanPaymentDeltas: nonzero valueChange"); + auto const deltas = xrpl::accrual::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == parts.valueChange); + BEAST_EXPECT( + deltas.debtTotalDelta == + (parts.principalPaid + parts.interestPaid) - parts.valueChange); + } + + { + testcase("cash_basis::loanPaymentDeltas: nonzero valueChange ignored"); + auto const deltas = xrpl::cash_basis::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == parts.interestPaid); + BEAST_EXPECT(deltas.debtTotalDelta == parts.principalPaid); + } + } + + void + testLoanOriginationDeltasDispatcher() + { + using namespace jtx; + + Number const principalRequested{1'000}; + Number const interestDue{75}; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase( + "loanOriginationDeltas dispatcher: amendment enabled, legacy vault picks " + "Accrual"); + Env const env{*this}; + auto const deltas = loanOriginationDeltas(legacyVault, principalRequested, interestDue); + auto const expected = + xrpl::accrual::loanOriginationDeltas(principalRequested, interestDue); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + + { + testcase( + "loanOriginationDeltas dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis picks CashBasis"); + Env const env{*this}; + auto const deltas = + loanOriginationDeltas(cashBasisVault, principalRequested, interestDue); + auto const expected = xrpl::cash_basis::loanOriginationDeltas(principalRequested); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + } + + void + testLoanOriginationExceedsVaultMaximumDispatcher() + { + using namespace jtx; + + Number const vaultMaximum{1'000}; + Number const vaultTotal{900}; + // Exceeds Accrual's headroom (100), but must never trip CashBasis. + Number const interestDue{101}; + + auto const legacyVault = makeVaultSle(std::nullopt, vaultMaximum, vaultTotal); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis, vaultMaximum, vaultTotal); + + { + testcase( + "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, legacy vault " + "picks Accrual"); + Env const env{*this}; + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum(legacyVault, vaultTotal, interestDue) == + xrpl::accrual::loanOriginationExceedsVaultMaximum( + vaultMaximum, vaultTotal, interestDue)); + } + + { + testcase( + "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis picks CashBasis"); + Env const env{*this}; + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum(cashBasisVault, vaultTotal, interestDue) == + false); + } + } + + void + testLoanVaultExposureDispatcher() + { + using namespace jtx; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase("loanVaultExposure dispatcher: amendment enabled, legacy vault picks Accrual"); + Env const env{*this}; + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT( + loanVaultExposure(legacyVault, sle) == xrpl::accrual::loanVaultExposure(sle)); + } + + { + testcase( + "loanVaultExposure dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis " + "picks CashBasis"); + Env const env{*this}; + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT( + loanVaultExposure(cashBasisVault, sle) == xrpl::cash_basis::loanVaultExposure(sle)); + } + } + + void + testLoanPaymentDeltasDispatcher() + { + using namespace jtx; + + LoanPaymentParts const parts{ + .principalPaid = Number{100}, + .interestPaid = Number{20}, + .valueChange = Number{5}, + .feePaid = Number{3}}; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase("loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks Accrual"); + Env const env{*this}; + auto const deltas = loanPaymentDeltas(legacyVault, parts); + auto const expected = xrpl::accrual::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + + { + testcase( + "loanPaymentDeltas dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis " + "picks CashBasis"); + Env const env{*this}; + auto const deltas = loanPaymentDeltas(cashBasisVault, parts); + auto const expected = xrpl::cash_basis::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + } + public: void testCanApplyToBrokerCover() @@ -1573,6 +1895,17 @@ public: testComputeOverpaymentComponents(); testComputeInterestAndFeeParts(); testCanApplyToBrokerCover(); + + testAccrualLoanOriginationDeltas(); + testCashBasisLoanOriginationDeltas(); + testAccrualLoanOriginationExceedsVaultMaximum(); + testAccrualLoanVaultExposure(); + testCashBasisLoanVaultExposure(); + testLoanPaymentDeltas(); + testLoanOriginationDeltasDispatcher(); + testLoanOriginationExceedsVaultMaximumDispatcher(); + testLoanVaultExposureDispatcher(); + testLoanPaymentDeltasDispatcher(); } }; diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/lending/LoanBroker_test.cpp similarity index 98% rename from src/test/app/LoanBroker_test.cpp rename to src/test/app/lending/LoanBroker_test.cpp index f6f85a0cca..a610dbe931 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/lending/LoanBroker_test.cpp @@ -93,7 +93,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(static_cast(env.le(keylet)) == goodVault); - using namespace loanBroker; + using namespace loan_broker; // Can't create a loan broker regardless of whether the vault exists env(set(alice, keylet.key), Ter(temDISABLED)); auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); @@ -168,7 +168,7 @@ class LoanBroker_test : public beast::unit_test::Suite } using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; // Bogus assets to use in test cases static PrettyAsset const kBadMptAsset = [&]() { @@ -290,7 +290,7 @@ class LoanBroker_test : public beast::unit_test::Suite { auto const amount = vault.asset(n); BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number()); - env.require(Balance(pseudoAccount, amount)); + env.require(jtx::Balance(pseudoAccount, amount)); } }; @@ -537,8 +537,8 @@ class LoanBroker_test : public beast::unit_test::Suite auto const expectedBalance = aliceBalance + coverFunds - (aliceBalance.value().native() ? STAmount(env.current()->fees().base.value()) : vault.asset(0)); - env.require(Balance(alice, expectedBalance)); - env.require(Balance(pseudoAccount, vault.asset(kNone))); + env.require(jtx::Balance(alice, expectedBalance)); + env.require(jtx::Balance(pseudoAccount, vault.asset(kNone))); } } @@ -645,8 +645,8 @@ class LoanBroker_test : public beast::unit_test::Suite } } - using namespace loanBroker; - using namespace xrpl::Lending; + using namespace loan_broker; + using namespace xrpl::lending; TenthBips32 const tenthBipsZero{0}; @@ -862,7 +862,7 @@ class LoanBroker_test : public beast::unit_test::Suite LoanBrokerTest brokerTest) { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; Env env(*this); @@ -1093,7 +1093,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("Invalid LoanBrokerCoverClawback"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; // preflight { @@ -1220,7 +1220,7 @@ class LoanBroker_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); // Create LoanBroker pointing to the vault - env(loanBroker::set(alice, vaultKeylet.key)); + env(loan_broker::set(alice, vaultKeylet.key)); env.close(); // Build the CoverDeposit STTx directly @@ -1256,7 +1256,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("Require Auth - Implicit Pseudo-account authorization"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -1364,7 +1364,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("testLoanBrokerSetDebtMaximum"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; Env env(*this); @@ -1550,10 +1550,10 @@ class LoanBroker_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit), Ter(err)); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, deposit), Ter(err)); env.close(); }; @@ -1621,7 +1621,7 @@ class LoanBroker_test : public beast::unit_test::Suite auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo); env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze)); - env(loanBroker::set(lender, vaultKeylet.key), Ter(tecFROZEN)); + env(loan_broker::set(lender, vaultKeylet.key), Ter(tecFROZEN)); } void @@ -1629,7 +1629,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase << "LoanBrokerDelete - locked broker pseudo-account MPT"; using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer("issuer"); Account const alice("alice"); @@ -1749,7 +1749,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase << "LoanBrokerDelete - frozen broker pseudo-account IOU"; using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer("issuer"); Account const alice("alice"); @@ -1833,7 +1833,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverDepositFreezes() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -1984,7 +1984,7 @@ class LoanBroker_test : public beast::unit_test::Suite testcase("LoanBrokerCoverWithdraw IOU self-withdrawal while individually frozen"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -2046,7 +2046,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverWithdrawFreezes() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -2351,7 +2351,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}), - loanBroker::kDestination(dest), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2361,14 +2361,14 @@ class LoanBroker_test : public beast::unit_test::Suite // Test LoanBroker withdraw auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000))); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000))); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2379,8 +2379,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(fclear(issuer, asfRequireAuth)); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2472,7 +2472,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}), - loanBroker::kDestination(dest), + loan_broker::kDestination(dest), Ter(std::ignore)); // Shouldn't fail if at MaximumAmount since no new tokens are issued @@ -2489,14 +2489,14 @@ class LoanBroker_test : public beast::unit_test::Suite // Test LoanBroker withdraw auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000))); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000))); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == err); env.close(); @@ -2522,7 +2522,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverPrecisionGuard() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; diff --git a/src/test/app/lending/LoanCashBasis_test.cpp b/src/test/app/lending/LoanCashBasis_test.cpp new file mode 100644 index 0000000000..c1334180c1 --- /dev/null +++ b/src/test/app/lending/LoanCashBasis_test.cpp @@ -0,0 +1,1013 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +// LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage. +// +// Existing tests never enable featureLendingProtocolV1_1 (see `all_` +// above), so these are the only tests in this file that exercise the +// amendment. They are called once, directly, from +// runAmendmentIndependent() -- not looped through +// runAmendmentSensitive()/amendmentCombinations(), since doing so would +// require re-deriving whole-life-specific expected values for ~15 +// unrelated regression tests. +class LoanCashBasis_test : public LoanTestBase +{ +private: + // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas, + // and the AssetsMaximum/DebtMaximum guards (which always check against + // principal + interestDue, regardless of the amendment). + void + testCashBasisLoanSetOrigination() + { + testcase("cash-basis: LoanSet origination"); + + using namespace jtx; + using namespace loan; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(10)}; + std::uint32_t const paymentTotal = 2; + std::uint32_t const paymentInterval = 86400; + + // Creates a broker/vault, submits a single LoanSet with a nonzero + // interest rate, and returns the observed Vault.AssetsTotal / + // LoanBroker.DebtTotal deltas plus the loan's own computed + // interestDue and principalOutstanding. + auto runOrigination = [&](FeatureBitset features) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + auto const loanSequence = brokerBefore->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value()); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + + return std::make_tuple( + assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding); + }; + + Number interestDueCash{}; + Number principalOutstandingCash{}; + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_ | featureLendingProtocolV1_1); + interestDueCash = interestDue; + principalOutstandingCash = principalOutstanding; + + BEAST_EXPECTS( + assetsTotalDelta == beast::kZero, + "cash-basis origination must not change AssetsTotal; delta=" + + to_string(assetsTotalDelta)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding, + "cash-basis origination must add principal-only to DebtTotal; delta=" + + to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding)); + } + + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_); + + BEAST_EXPECTS( + assetsTotalDelta == interestDue, + "whole-life origination must add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding + interestDue, + "whole-life origination must add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDelta)); + } + + // AssetsMaximum guard checks interestDue headroom only under + // whole-life accounting; DebtMaximum guard also varies by model. + auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal); + + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = assetsTotalBefore + slack; + env(tx); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + env(loan_broker::set(lender, broker.vaultID), + loan_broker::kLoanBrokerId(broker.brokerID), + loan_broker::kDebtMaximum(debtMaximum), + Fee(env.current()->fees().base * 2)); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + Number const oneDrop = xrpAsset(1).value(); + { + testcase("whole-life: LoanSet AssetsMaximum guard checks interestDue headroom"); + // Guard rejects when there's not quite enough headroom for the + // interest. + runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED); + // Guard accepts at the exact boundary. + runVaultGuard(all_, interestDueCash, tesSUCCESS); + } + + { + testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom"); + // Even far less headroom than interestDue still succeeds, since + // cash-basis origination never adds interest to AssetsTotal. + runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS); + } + + // DebtMaximum guard: cash-basis projects principal-only DebtTotal; + // whole-life projects principal + interestDue. + for (auto const cashBasis : {true, false}) + { + testcase( + std::string("LoanSet DebtMaximum guard (") + + (cashBasis ? "cash-basis)" : "whole-life)")); + auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_; + Number const newDebtTotal = + principalOutstandingCash + (cashBasis ? Number{} : interestDueCash); + runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED); + runBrokerGuard(features, newDebtTotal, tesSUCCESS); + } + } + + // 2. LoanPay: regular, late, overpayment, and full-payment types. + // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match + // interestPaid/principalPaid under cash-basis, and cross-check the + // amendment-disabled run's deltas against the documented whole-life + // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own + // TotalValueOutstanding delta exactly, since whole-life debt recognition + // tracks total loan value). + void + testCashBasisLoanPay() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + using tp = NetClock::time_point; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{12'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 300; + + struct PaymentDeltas + { + Number principalPaid; + Number assetsTotalDelta; + Number debtTotalDelta; + Number totalValueDelta; + }; + + // Sets up a fresh broker + loan, advances time, submits a single + // payment of the given type/amount, and returns the observed deltas. + auto runPayment = [&](FeatureBitset features, + std::uint32_t loanSetFlags, + std::uint32_t payFlags, + std::function const& advanceTime, + std::function const& paymentAmount) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + .flags = loanSetFlags, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + + advanceTime(env, state.startDate); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore); + + Number const principalBefore = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + STAmount const amount = paymentAmount(state); + env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + auto const loanAfter = env.le(loanKeylet); + BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter); + + Number const principalAfter = loanAfter->at(sfPrincipalOutstanding); + Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding); + Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + Number const debtTotalAfter = brokerAfter->at(sfDebtTotal); + + return PaymentDeltas{ + .principalPaid = principalBefore - principalAfter, + .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore, + .debtTotalDelta = debtTotalAfter - debtTotalBefore, + .totalValueDelta = totalValueAfter - totalValueBefore}; + }; + + // Compares the disabled (whole-life) and enabled (cash-basis) runs + // of the same payment scenario, and asserts the documented + // relationships between them. + auto checkScenario = [&](std::string const& label, + PaymentDeltas const& off, + PaymentDeltas const& on) { + testcase("cash-basis: LoanPay " + label); + + // The loan's own PrincipalOutstanding field is untouched by + // the amendment. + BEAST_EXPECTS( + off.principalPaid == on.principalPaid, + "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) + + " on=" + to_string(on.principalPaid)); + + // Whole-life structural invariant: DebtTotal (which + // recognizes a loan's full remaining value as debt) must + // change exactly as the loan's own TotalValueOutstanding + // does. + BEAST_EXPECTS( + off.debtTotalDelta == off.totalValueDelta, + "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(off.debtTotalDelta) + + " totalValueDelta=" + to_string(off.totalValueDelta)); + + // Derive interestPaid from the whole-life run's independent + // ledger deltas: + // assetsTotalDelta_off == valueChange + // debtTotalDelta_off == valueChange - (principalPaid + interestPaid) + // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid + Number const interestPaid = + off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid; + BEAST_EXPECTS( + interestPaid >= beast::kZero, + "derived interestPaid must be non-negative: " + to_string(interestPaid)); + + BEAST_EXPECTS( + on.assetsTotalDelta == interestPaid, + "cash-basis AssetsTotal delta must equal interestPaid; delta=" + + to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid)); + BEAST_EXPECTS( + on.debtTotalDelta == -on.principalPaid, + "cash-basis DebtTotal delta must equal -principalPaid; delta=" + + to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid)); + }; + + // ---- Regular, on-time payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const regularAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3, -1} * 5}; // 1.5x, so only a single period is paid + }; + + auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount); + auto const on = + runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount); + + // Regular, on-time payments never change the loan's value beyond + // normal amortization (production asserts valueChange == 0), so + // AssetsTotal must be unaffected in the whole-life run. + BEAST_EXPECTS( + off.assetsTotalDelta == beast::kZero, + "regular on-time payment must not change AssetsTotal under whole-life; delta=" + + to_string(off.assetsTotalDelta)); + + checkScenario("regular payment", off, on); + } + + // ---- Late payment ---- + { + auto const advancePastDue = [&](Env& env, tp const& startDate) { + env.close(startDate + std::chrono::seconds(paymentInterval + 1)); + }; + auto const lateAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3}}; // generous; excess is not withdrawn + }; + + auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + 0, + tfLoanLatePayment, + advancePastDue, + lateAmount); + + checkScenario("late payment", off, on); + } + + // ---- Overpayment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const overpayAmount = [&](LoanState const& state) { + // One regular period, plus a generous extra principal + // paydown. + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) + + xrpAsset(2'000).value()}; + }; + + auto const off = + runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + tfLoanOverpayment, + tfLoanOverpayment, + noAdvance, + overpayAmount); + + checkScenario("overpayment", off, on); + } + + // ---- Full payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const fullAmount = [&](LoanState const&) { + // Generously large: full payment only ever consumes exactly + // what's due (principal + accrued interest; close fee/ + // prepayment penalty are 0 here), excess is not withdrawn. + return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}}; + }; + + auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount); + + checkScenario("full payment", off, on); + } + } + + // 3. LoanManage: impair, unimpair, and default. + void + testCashBasisLoanManage() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto setupLoan = [&](Env& env) { + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + return std::make_tuple(broker, loanKeylet, lender, borrower); + }; + + // ---- impair / unimpair ---- + auto runImpairUnimpair = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(loanBefore); + Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + auto const vaultBeforeImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultBeforeImpair); + Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterImpair); + Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore; + + env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterUnimpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterUnimpair); + Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore; + + return std::make_tuple(expectedExposure, impairDelta, netDelta); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage impair/unimpair (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features); + + BEAST_EXPECTS( + impairDelta == expectedExposure, + "impair must add loanVaultExposure to LossUnrealized; delta=" + + to_string(impairDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + netDelta == beast::kZero, + "unimpair must be an exact reversal of impair; net=" + to_string(netDelta)); + } + + // ---- impair, then default ---- + auto runDefault = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = + loanBeforeImpair->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close( + state.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + Number const lossBefore = vaultBefore->at(sfLossUnrealized); + Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore; + Number const coverAvailableDelta = + Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore; + + Number const defaultCovered = -coverAvailableDelta; + Number const vaultDefaultAmount = expectedExposure - defaultCovered; + + return std::make_tuple( + expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage default (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const + [expectedExposure, + assetsTotalDelta, + debtTotalDelta, + lossDelta, + vaultDefaultAmount] = runDefault(features); + + BEAST_EXPECTS( + debtTotalDelta == -expectedExposure, + "default must reduce DebtTotal by the unified default amount; delta=" + + to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDelta == -expectedExposure, + "default must reverse the earlier impair's LossUnrealized exactly; delta=" + + to_string(lossDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + assetsTotalDelta == -vaultDefaultAmount, + "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" + + to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount)); + } + } + + // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1 + // activates (LEVersion absent) must keep whole-life (accrual) accounting + // forever, even after the amendment is later enabled -- the switch is + // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment + // flag. + void + testLegacyVaultKeepsAccrualAfterAmendmentEnabled() + { + testcase("LEVersion: legacy vault keeps accrual after amendment enabled"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + // Amendment disabled at Vault creation time: LEVersion stays absent. + Env env(*this, all_); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + } + + // Now enable the amendment -- production dispatch must still treat + // this specific Vault as accrual-basis, since its LEVersion is + // (and remains) absent. + env.enableFeature(featureLendingProtocolV1_1); + env.close(); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + // ---- LoanSet origination: whole-life formulas expected ---- + auto const vaultBeforeSet = env.le(broker.vaultKeylet()); + auto const brokerBeforeSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet); + Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal); + Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal); + + env(loanParams(env, broker)); + env.close(); + + auto const loanAfterSet = env.le(loanKeylet); + BEAST_EXPECT(loanAfterSet); + Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + + auto const vaultAfterSet = env.le(broker.vaultKeylet()); + auto const brokerAfterSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterSet && brokerAfterSet); + Number const assetsTotalDeltaSet = + Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet; + Number const debtTotalDeltaSet = + Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet; + + BEAST_EXPECTS( + assetsTotalDeltaSet == interestDue, + "legacy vault origination must still add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDeltaSet == principalOutstanding + interestDue, + "legacy vault origination must still add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDeltaSet)); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close(); + + // ---- LoanPay: whole-life formulas expected ---- + auto const vaultBeforePay = env.le(broker.vaultKeylet()); + auto const brokerBeforePay = env.le(broker.brokerKeylet()); + auto const loanBeforePay = env.le(loanKeylet); + BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay); + Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding); + Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal); + Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal); + + STAmount const paymentAmount{ + xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)}; + env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterPay = env.le(broker.vaultKeylet()); + auto const brokerAfterPay = env.le(broker.brokerKeylet()); + auto const loanAfterPay = env.le(loanKeylet); + BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay); + Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding); + Number const assetsTotalDeltaPay = + Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay; + Number const debtTotalDeltaPay = + Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay; + Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay; + + // A regular, on-time payment has valueChange == 0, so whole-life + // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding. + BEAST_EXPECTS( + assetsTotalDeltaPay == beast::kZero, + "legacy vault regular payment must not change AssetsTotal; delta=" + + to_string(assetsTotalDeltaPay)); + BEAST_EXPECTS( + debtTotalDeltaPay == totalValueDeltaPay, + "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay)); + + // ---- LoanManage: impair, then default -- whole-life exposure expected ---- + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding); + Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet); + env.close( + stateAtImpair.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBeforeDefault = env.le(broker.vaultKeylet()); + auto const brokerBeforeDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault); + Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal); + Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterDefault = env.le(broker.vaultKeylet()); + auto const brokerAfterDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault); + Number const debtTotalDeltaDefault = + Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault; + Number const lossDeltaDefault = + Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault; + + BEAST_EXPECTS( + debtTotalDeltaDefault == -expectedExposure, + "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" + + to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDeltaDefault == -expectedExposure, + "legacy vault default must reverse the earlier impair's LossUnrealized exactly; " + "delta=" + + to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure)); + + // Confirm the Vault's LEVersion truly never got set, throughout. + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy); + } + } + + // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default), + // entirely under the amendment, with independently hand-computed + // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at + // each step. 0% interest keeps the arithmetic exact and tractable; the + // divergence from whole-life accounting is already covered directly by + // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test + // focuses purely on an independent, from-scratch trajectory check. + void + testCashBasisEndToEndTrajectory() + { + testcase("cash-basis: end-to-end trajectory"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}}; + + Env env(*this, all_ | featureLendingProtocolV1_1); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + // Hand computation (all values in XRP, drops == 1e-6 XRP): + // Vault: AssetsTotal starts at 100'000 (the deposit). + // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000 + // (BrokerParameters::defaults().coverDeposit). + auto const vaultKeylet = broker.vaultKeylet(); + auto const brokerKeylet = broker.brokerKeylet(); + + // All the "human XRP unit" constants below (e.g. `100'000`) are + // converted to raw native (drops) values via xrpAsset(...), since + // that's how the ledger fields are actually denominated. + auto const checkVaultBroker = [&](Number const& assetsTotalUnits, + Number const& debtTotalUnits, + Number const& lossUnrealizedUnits, + Number const& coverAvailableUnits, + char const* step) { + Number const assetsTotal = xrpAsset(assetsTotalUnits).value(); + Number const debtTotal = xrpAsset(debtTotalUnits).value(); + Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value(); + Number const coverAvailable = xrpAsset(coverAvailableUnits).value(); + + auto const vaultSle = env.le(vaultKeylet); + auto const brokerSle = env.le(brokerKeylet); + BEAST_EXPECT(vaultSle && brokerSle); + BEAST_EXPECTS( + vaultSle->at(sfAssetsTotal) == assetsTotal, + std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " + + to_string(Number(vaultSle->at(sfAssetsTotal)))); + BEAST_EXPECTS( + brokerSle->at(sfDebtTotal) == debtTotal, + std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " + + to_string(Number(brokerSle->at(sfDebtTotal)))); + BEAST_EXPECTS( + vaultSle->at(sfLossUnrealized) == lossUnrealized, + std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) + + " got " + to_string(Number(vaultSle->at(sfLossUnrealized)))); + BEAST_EXPECTS( + brokerSle->at(sfCoverAvailable) == coverAvailable, + std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) + + " got " + to_string(Number(brokerSle->at(sfCoverAvailable)))); + }; + + checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet"); + + // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees. + Number const principalRequest{1'200}; + std::uint32_t const paymentTotal = 12; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto const brokerBeforeLoan = env.le(brokerKeylet); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = TenthBips32{0}, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + env(loanParams(env, broker)); + env.close(); + + // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal. + checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet"); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value()); + + // Payment 1: principalPaid=100, interestPaid=0. + // AssetsTotal += 0; DebtTotal -= 100. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1"); + + // Payment 2: same as above. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2"); + + // Default (no impair): principalOutstanding remaining is 1'000. + // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000. + // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100. + // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount) + // = min(25, 1'000) = 25. + // defaultCovered = min(covered, CoverAvailable(1'000)) = 25. + // vaultDefaultAmount = 1'000 - 25 = 975. + // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975. + // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired). + auto const loanBeforeDefault = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeDefault); + BEAST_EXPECT( + Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value()); + + env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)"); + } + +public: + void + run() override + { + testCashBasisLoanSetOrigination(); + testCashBasisLoanPay(); + testCashBasisLoanManage(); + testLegacyVaultKeepsAccrualAfterAmendmentEnabled(); + testCashBasisEndToEndTrajectory(); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanCashBasis, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp new file mode 100644 index 0000000000..ec0238c173 --- /dev/null +++ b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp @@ -0,0 +1,722 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl::test { + +class LoanCoverFreezeAuth_test : public LoanTestBase +{ +private: + void + testSequentialFLCDepletion(FeatureBitset features) + { + testcase << "First-Loss Capital Depletion on Sequential Defaults"; + + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrowerA{"borrowerA"}; + Account const borrowerB{"borrowerB"}; + + env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB); + env.close(); + + PrettyAsset const asset = xrpIssue(); + auto const vaultDepositAmount = + asset(200'000); // Enough for 2 x 50k loans plus interest/fees + + auto const brokerInfo = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = vaultDepositAmount.value(), + .debtMax = 0, + .coverRateMin = TenthBips32(20000), // 20% + .coverDeposit = 21'000, + .managementFeeRate = TenthBips16(100), // 0.1% + .coverRateLiquidation = TenthBips32(100000), + }); + auto const brokerKeylet = brokerInfo.brokerKeylet(); + + // Create two identical loans: each 50,000 XRP principal (scaled down to + // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal + // + interest) Formula will calculate cover as: 100% × (20% × 100,000) = + // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first + // default + auto const principalAmount = Number(50'000); + auto const loanPaymentInterval = 2592000; // 30 days + auto const loanGracePeriod = 604800; // 7 days + + // Create Loan A + auto loanATx = env.jt( + set(borrowerA, brokerKeylet.key, principalAmount), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(500)), // 5% + kPaymentTotal(12), + loan::kPaymentInterval(loanPaymentInterval), + loan::kGracePeriod(loanGracePeriod), + Fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanATx); + env.close(); + + auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); + + // Create Loan B + auto loanBTx = env.jt( + set(borrowerB, brokerKeylet.key, principalAmount), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(500)), // 5% + kPaymentTotal(12), + loan::kPaymentInterval(loanPaymentInterval), + loan::kGracePeriod(loanGracePeriod), + Fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanBTx); + env.close(); + + auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); + + auto loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + + // Advance time past grace period for both loans to be defaultable + auto const loanANextDue = loanASle->at(sfNextPaymentDueDate); + auto const loanAGrace = loanASle->at(sfGracePeriod); + env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60}); + + env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + // Verify Loan A is defaulted + loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault)); + BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0); + + // Check broker state after first default (from committed ledger) + auto brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); + + // DebtTotal should have decreased by Loan A's debt + BEAST_EXPECT(afterFirstDebtTotal == 50'134); + + // CoverAvailable should have decreased significantly + BEAST_EXPECT(afterFirstCoverAvailable == 946); + + env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + + brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable); + + BEAST_EXPECT(afterSecondDebtTotal == 0); + + BEAST_EXPECT(afterSecondCoverAvailable == 0); + } + + // Tests that vault withdrawals work correctly when the vault has unrealized + // loss from an impaired loan, ensuring the invariant check properly + // accounts for the loss. + void + testWithdrawReflectsUnrealizedLoss(FeatureBitset features) + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + testcase("Vault withdraw reflects sfLossUnrealized"); + + // Test constants + static constexpr std::int64_t kInitialFunding = 1'000'000; + static constexpr std::int64_t kLenderInitialIou = 5'000'000; + static constexpr std::int64_t kDepositorInitialIou = 1'000'000; + static constexpr std::int64_t kBorrowerInitialIou = 100'000; + static constexpr std::int64_t kDepositAmount = 5'000; + static constexpr std::int64_t kPrincipalAmount = 99; + static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000; + static constexpr std::uint32_t kLocalPaymentInterval = 600; + static constexpr std::uint32_t kLocalPaymentTotal = 2; + + Env env{*this, features}; + + // Setup accounts + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const depositorA{"lpA"}; + Account const depositorB{"lpB"}; + Account const borrower{"borrowerA"}; + + env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower); + env.close(); + + // Setup trust lines + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(depositorA, iouAsset(10'000'000))); + env(trust(depositorB, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + + // Fund accounts with IOUs + env(pay(issuer, lender, iouAsset(kLenderInitialIou))); + env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou))); + env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou))); + env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou))); + env.close(); + + // Create vault and broker, then add deposits from two depositors + auto const broker = createVaultAndBroker(env, iouAsset, lender); + Vault v{env}; + + env(v.deposit({ + .depositor = depositorA, + .id = broker.vaultKeylet().key, + .amount = iouAsset(kDepositAmount), + }), + Ter(tesSUCCESS)); + env(v.deposit({ + .depositor = depositorB, + .id = broker.vaultKeylet().key, + .amount = iouAsset(kDepositAmount), + }), + Ter(tesSUCCESS)); + env.close(); + + // Create a loan + auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(sleBroker)) + return; + + auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); + + env(set(borrower, broker.brokerID, kPrincipalAmount), + Sig(sfCounterpartySignature, lender), + kPaymentTotal(kLocalPaymentTotal), + kPaymentInterval(kLocalPaymentInterval), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // Impair the loan to create unrealized loss + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + // Verify unrealized loss is recorded in the vault + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultAfterImpair)) + return; + + BEAST_EXPECT( + vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value()); + + // Helper to get share balance for a depositor + auto const shareAsset = vaultAfterImpair->at(sfShareMPTID); + auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t { + auto const token = env.le(keylet::mptoken(shareAsset, depositor.id())); + return token ? token->getFieldU64(sfMPTAmount) : 0; + }; + + // Verify both depositors have equal shares + auto const sharesLpA = getShareBalance(depositorA); + auto const sharesLpB = getShareBalance(depositorB); + BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor); + BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor); + BEAST_EXPECT(sharesLpA == sharesLpB); + + // Helper to attempt withdrawal + auto const attemptWithdrawShares = [&](Account const& depositor, + std::uint64_t shareAmount, + TER expected) { + STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)}; + env(v.withdraw( + {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}), + Ter(expected)); + env.close(); + }; + + // Regression test: Both depositors should successfully withdraw despite + // unrealized loss. Previously failed with invariant violation: + // "withdrawal must change vault and destination balance by equal + // amount". This was caused by sharesToAssetsWithdraw rounding down, + // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta + // when unrealized loss exists. + attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS); + attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS); + } + + void + testServiceFeeOnBrokerDeepFreeze() + { + testcase << "Service Fee On Broker Deep Freeze"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const iou = issuer["IOU"]; + + for (bool const deepFreeze : {true, false}) + { + Env env(*this); + + auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) { + if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(le)) + { + auto const account = le->at(accountField); + if (auto const sleLine = env.le(keylet::trustLine(account, iou)); + BEAST_EXPECT(sleLine)) + { + STAmount balance = sleLine->at(sfBalance); + if (account > issuer.id()) + balance.negate(); + return balance; + } + } + return STAmount{iou}; + }; + + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + env(trust(broker, iou(20'000'000))); + env(pay(issuer, broker, iou(10'000'000))); + env.close(); + + auto const brokerInfo = createVaultAndBroker(env, iou, broker); + + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); + + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(iou(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + + env(trust(borrower, iou(20'000'000))); + // The borrower increases their limit and acquires some IOU so + // they can pay interest + env(pay(issuer, borrower, iou(500))); + env.close(); + + if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) + { + if (deepFreeze) + { + env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + } + + env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100))); + env.close(); + + if (deepFreeze) + { + // The fee goes to the broker pseudo-account + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100)); + BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000)); + } + else + { + // The fee goes to the broker account + BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100)); + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); + } + } + }; + } + + void + testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features) + { + testcase << "LoanPay Broker Owner Missing Trustline (PoC)"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const iou = issuer["IOU"]; + Env env(*this, features); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + // Set up trustlines and fund accounts + env(trust(broker, iou(20'000'000))); + env(trust(borrower, iou(20'000'000))); + env(pay(issuer, broker, iou(10'000'000))); + env(pay(issuer, borrower, iou(1'000))); + env.close(); + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, iou, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(iou(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = iou(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); + env.close(); + // Verify broker owner has a trustline + auto const brokerTrustline = keylet::trustLine(broker, iou); + BEAST_EXPECT(env.le(brokerTrustline) != nullptr); + // Broker owner deletes their trustline + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, iou); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Remove the trustline by setting limit to 0 + env(trust(broker, iou(0))); + env.close(); + // Verify trustline is deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_LINE. + env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify trustline is still deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, iou); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features) + { + testcase << "LoanPay Broker Owner MPT unauthorized"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, mpt, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = mpt(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Broker owner unauthorizes. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Then, unauthorize the MPT. + mptt.authorize({.account = broker, .flags = tfMPTUnauthorize}); + env.close(); + // Verify the MPT is unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify the MPT is still unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, mpt); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) + { + testcase << "LoanPay Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, mpt, broker); + // Create a loan first (this creates debt) + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = mpt(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Remove the credentials for the Broker owner. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Make sure the broker is not authorized to hold the MPT after we + // deleted the credentials + env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); + + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify broker is still not authorized + env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, mpt); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) + { + testcase << "LoanSet Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + // Add credentials for the broker and borrower + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, mpt, broker); + + // Remove the credentials for the Broker owner. + // Clear the balance first. + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Delete the credentials + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Create a loan, this should fail for tecNO_AUTH + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100)), + Ter(tecNO_AUTH)); + env.close(); + } + + void + runAmendmentIndependent() + { + testServiceFeeOnBrokerDeepFreeze(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testSequentialFLCDepletion(features); + testWithdrawReflectsUnrealizedLoss(features); + testLoanPayBrokerOwnerMissingTrustline(features); + testLoanPayBrokerOwnerUnauthorizedMPT(features); + testLoanPayBrokerOwnerNoPermissionedDomainMPT(features); + testLoanSetBrokerOwnerNoPermissionedDomainMPT(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanCoverFreezeAuth, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanInvariants_test.cpp b/src/test/app/lending/LoanInvariants_test.cpp new file mode 100644 index 0000000000..381a3f8b48 --- /dev/null +++ b/src/test/app/lending/LoanInvariants_test.cpp @@ -0,0 +1,873 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test { + +class LoanInvariants_test : public LoanTestBase +{ +private: + // Each of these regression tests reproduces a single fuzzer-found (FIND-*) + // scenario against xrpl::detail::computePeriodicPayment / + // loanComputePaymentParts. They're merged into one function, one block + // per finding, because each is a narrow, self-contained repro that + // shares little beyond the surrounding scaffold. + void + testLoanPayComputePeriodicPaymentInvariants(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + + // From FIND-012 + { + testcase << "LoanPay xrpl::detail::computePeriodicPayment : " + "valid rate"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + BrokerParameters const brokerParams; + env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{640562, -5}; + + Number const serviceFee{2462611968}; + std::uint32_t const numPayments{4294967295 / 800}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + kLoanServiceFee(serviceFee), + kPaymentTotal(numPayments), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 55374; + createJson["ClosePaymentFee"] = "3825205248"; + createJson["LatePaymentFee"] = "237"; + createJson["LoanOriginationFee"] = "0"; + createJson["OverpaymentFee"] = 35167; + createJson["OverpaymentInterestRate"] = 1360; + createJson["PaymentInterval"] = 727; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + // Fails in preclaim because principal requested can't be + // represented as XRP + env(createJson, Ter(tecPRECISION_LOSS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + + Number const actualPrincipal{6}; + + createJson[sfPrincipalRequested] = actualPrincipal; + createJson.removeMember(sfSequence.jsonName); + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + // Fails in doApply because the payment is too small to be + // represented as XRP. + env(createJson, Ter(tecPRECISION_LOSS)); + env.close(); + } + + // From FIND-010 + { + testcase << "xrpl::loanComputePaymentParts : valid total interest"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 47299; + createJson["ClosePaymentFee"] = "3985819770"; + createJson["InterestRate"] = 92; + createJson["LatePaymentFee"] = "3866894865"; + createJson["LoanOriginationFee"] = "0"; + createJson["LoanServiceFee"] = "2348810240"; + createJson["OverpaymentFee"] = 58545; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 1; + createJson["PrincipalRequested"] = "0.000763058"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson); + env.close(); + + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + loanPayTx["Amount"]["value"] = "0.000281284125490196"; + env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + } + + // From FIND-009 + { + testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid " + "rounded"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + + { + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{3074'745'058'823'529, -12}; + BEAST_EXPECT(to_string(amount) == "3074.745058823529"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + } + + { + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{6732'118'170'944'051, -12}; + BEAST_EXPECT(to_string(amount) == "6732.118170944051"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + } + + auto const stateAfter = getCurrentState(env, broker, keylet); + // Total interest outstanding is non-negative + BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); + // Principal paid is non-negative + BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding); + // Total value change is non-negative + BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); + // Value delta is larger or same as principal delta (meaning + // non-negative interest paid) + BEAST_EXPECT( + (stateBefore.totalValue - stateAfter.totalValue) >= + (stateBefore.principalOutstanding - stateAfter.principalOutstanding)); + } + + // From FIND-008 + { + testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = + createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + { + auto const coverDepositValue = + broker.asset(broker.params.coverDeposit * 10).value(); + env(loan_broker::coverDeposit(lender, broker.brokerID, coverDepositValue)); + env.close(); + } + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["InterestRate"] = 12833; + createJson["LateInterestRate"] = 77048; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 752; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 5678); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{9924'81, -2}; + BEAST_EXPECT(to_string(amount) == "9924.81"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == + stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction); + } + } + + void + testLoanPayDebtDecreaseInvariant(FeatureBitset features) + { + // From FIND-007 + testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease " + "rounding good"; + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const baseFee = env.current()->fees().base; + auto const loanSetFee = Fee(baseFee * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 60; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const pseudoAcct = brokerPseudoAccount(env, broker, lender); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); + auto const originalState = getCurrentState(env, broker, keylet); + verifyLoanStatus(originalState); + + Number const payment{3'269'349'176'470'588, -12}; + XRPAmount const payFee{ + baseFee * + ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)}; + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee)); + BEAST_EXPECT(to_string(payment) == "3269.349176470588"); + env(loanPayTx, Ter(tesSUCCESS)); + env.close(); + + auto const newState = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale)); + BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding); + BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale)); + BEAST_EXPECT( + isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale)); + } + + void + testAccountSendMptMinAmountInvariant(FeatureBitset features) + { + // (From FIND-006) + testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount " + "and MPT"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env(pay(issuer, lender, mptAsset(2'000'000))); + env(pay(issuer, borrower, mptAsset(1'000))); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 76671; + createJson["ClosePaymentFee"] = "2061925410"; + createJson["GracePeriod"] = 434; + createJson["InterestRate"] = 50302; + createJson["LateInterestRate"] = 30322; + createJson["LatePaymentFee"] = "294427911"; + createJson["LoanOriginationFee"] = "3250635102"; + createJson["LoanServiceFee"] = "9557386"; + createJson["OverpaymentFee"] = 51249; + createJson["OverpaymentInterestRate"] = 14304; + createJson["PaymentInterval"] = 434; + createJson["PaymentTotal"] = "2891743748"; + createJson["PrincipalRequested"] = "8516.98"; + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(temINVALID)); + env.close(); + } + + // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the + // same vault-scale minimum cover when fixCleanup3_2_0 is enabled. + // Before the amendment, each transactor computed its minimum cover at a + // different precision (loanScale, debtScale, or the raw unrounded + // tenthBipsOfValue), which could lead to inconsistent decisions for the + // same broker state. After the amendment all three use + // minimumBrokerCover at vaultScale. + void + testMinimumBrokerCoverConsistency(FeatureBitset features) + { + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + bool const withAmendment = features[fixCleanup3_2_0]; + + struct Ctx + { + jtx::Account issuer; + jtx::Account lender; + jtx::Account borrower; + jtx::PrettyAsset iou; + BrokerInfo broker; + BrokerParameters brokerParams; + }; + + // Shared setup, parametrized by vaultDeposit (the only varying setup + // field across the three scenarios). Each call runs in its own Env + // so multiple invocations within one scenario cannot interfere. + // The caller is responsible for invoking testcase(...) before the + // first runTest call of each scenario. + auto runTest = [&](Number vaultDeposit, auto&& body) { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + // Enable clawback on the issuer *before* any trust lines exist + // (asfAllowTrustLineClawback requires an empty owner directory). + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const iou = issuer[iouCurrency_]; + env(trust(lender, iou(1'000'000'000))); + env(trust(borrower, iou(1'000'000'000))); + env.close(); + env(pay(issuer, lender, iou(100'000'000))); + env(pay(issuer, borrower, iou(100'000'000))); + env.close(); + + // 13.37% — non-round rate produces a messier minimum. + BrokerParameters const brokerParams{ + .vaultDeposit = vaultDeposit, + .debtMax = 0, + .coverRateMin = TenthBips32{13'370}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{500}}; + + BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams); + + body( + env, + Ctx{.issuer = issuer, + .lender = lender, + .borrower = borrower, + .iou = iou, + .broker = broker, + .brokerParams = brokerParams}); + }; + + // Scenario 1 — LoanPay + // + // Verify that LoanPay's minimum cover check uses vault scale (not + // loan scale). Before the amendment, different loans could produce + // different fee routing decisions for the same broker-level state. + // Small vault deposit => vaultScale = -12. + testcase("LoanPay minimum cover scale consistency"); + { + struct LoanKeylets + { + Keylet tiny; + Keylet big; + }; + + // Create the tiny + big loans and reduce cover via clawback so + // that subsequent LoanPay calls hit the minimum-cover boundary. + // Used by the two pay-and-check sub-tests below so each can run + // in its own Env. + auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional { + Asset const asset{c.iou}; + + // Create the TINY loan first (while vaultScale is still + // small). principal 0.01, 0% interest, 1 payment => + // loanScale = vaultScale. + auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle1)) + return std::nullopt; + auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence); + auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq); + + env(set(c.borrower, c.broker.brokerID, Number{1, -2}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Create the BIG loan second. 100% annual interest over 20 + // payments pushes totalValueOutstanding high enough that + // loanScale > vaultScale. + auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle2)) + return std::nullopt; + auto const bigLoanSeq = brokerSle2->at(sfLoanSequence); + auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq); + + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // The tiny loan's scale is frozen at the vault's pre-big-loan + // scale, so it is strictly smaller than the big loan's. + // After the big loan is created the vault absorbs its value, + // pushing vaultScale up to match bigLoanScale. + auto const tinyLoanSle = env.le(tinyLoanKeylet); + auto const bigLoanSle = env.le(bigLoanKeylet); + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) || + !BEAST_EXPECT(vaultSle)) + return std::nullopt; + if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) || + !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) || + !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11)) + return std::nullopt; + + // Use issuer clawback to reduce cover to the minimum the + // clawback transactor allows. Compute the amount as + // initialCover - expectedCoverAfter so we exercise the exact + // clawback rather than relying on the transactor to clip + // down. + // + // Before the amendment the clawback minimum is the + // *unrounded* tenthBipsOfValue — strictly less than the + // rounded-at-vaultScale minimum LoanPay uses for the big + // loan. After the amendment both clawback and LoanPay use + // the same rounded minimum (via minimumBrokerCover), so + // cover lands exactly at that threshold. + Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} + : Number{1330651855688458000, -15}; + Number const clawbackAmount = + Number{c.brokerParams.coverDeposit} - expectedCoverAfter; + + env(coverClawback(c.issuer), + kLoanBrokerId(c.broker.brokerID), + kAmount(STAmount{asset, clawbackAmount})); + env.close(); + + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle) || + !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter)) + return std::nullopt; + + return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet}; + }; + + // Pay one loan and report whether the fee went to the broker's + // pseudo account (the fallback when cover < minimum) rather + // than to the owner. + auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool { + Asset const asset{c.iou}; + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return false; + auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount)); + auto const pseudoBefore = env.balance(pseudoAcct, c.iou); + + auto const payLoan = env.le(loanKeylet); + if (!BEAST_EXPECT(payLoan)) + return false; + auto const periodicPayment = payLoan->at(sfPeriodicPayment); + auto const serviceFee = payLoan->at(sfLoanServiceFee); + std::int32_t const loanScale = payLoan->at(sfLoanScale); + + auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale); + auto const payAmt = STAmount{asset, payment + serviceFee}; + + env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10))); + env.close(); + + auto const pseudoAfter = env.balance(pseudoAcct, c.iou); + return pseudoAfter.number() > pseudoBefore.number(); + }; + + // Pay the BIG loan in its own Env so its outcome cannot affect + // the TINY-loan check. With the fix, LoanPay and clawback use + // the same vaultScale minimum (cover == minAtVaultScale => + // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11, + // rounds up to a larger minimum than what clawback used => + // cover < min => fee to pseudo. + runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + auto const loans = setupLoansAndClawback(env, c); + if (!loans) + return; + BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment); + }); + + // Pay the TINY loan in its own Env. Fee goes to the owner + // either way: + // - With the fix: LoanPay uses vaultScale=-11 (same as + // clawback) => owner. + // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds + // up at -12 (a no-op) => min == cover => owner. + runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + auto const loans = setupLoansAndClawback(env, c); + if (!loans) + return; + BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny)); + }); + } + + // Scenario 2 — LoanBrokerCoverWithdraw + // + // Verify that CoverWithdraw's minimum cover check uses vault scale + // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw + // used: + // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset)) + // which could disagree with LoanPay's minimum (which used loanScale). + // + // Use a large vault deposit so that vaultScale (from AssetsTotal) is + // strictly larger than debtScale (from DebtTotal). With + // vaultDeposit = 100,000: after the big loan + // AssetsTotal ≈ 109,500 → vaultScale = -10 + // DebtTotal ≈ 10,000 → debtScale = -11 + // The one-order-of-magnitude gap makes roundToAsset at -10 truncate + // more aggressively than at -11, exposing the bug. + testcase("CoverWithdraw minimum cover scale consistency"); + runTest( + /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) { + Asset const asset{c.iou}; + + // Create only the big loan to push DebtTotal up to ~10,000 + // while AssetsTotal stays around 109,500 (dominated by the + // large vault deposit). + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Read broker state and compute both old and new minimums. + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle)) + return; + + auto const coverAvail = brokerSle->at(sfCoverAvailable); + auto const debtTotal = brokerSle->at(sfDebtTotal); + auto const vaultScale = getAssetsTotalScale(vaultSle); + auto const debtScale = scale(debtTotal, asset); + + // Sanity: debt scale differs from vault scale for this setup. + BEAST_EXPECT(debtScale < vaultScale); + + auto const oldMin = [&]() { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + return roundToAsset( + asset, + tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}), + debtScale); + }(); + auto const newMin = minimumBrokerCover( + debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle); + + // The new (vaultScale) minimum must be strictly larger than + // the old (debtScale) minimum — that is the gap the amendment + // closes. + Number const expectedNewMin{1330650518688500000, -15}; + Number const expectedOldMin{1330650518688472000, -15}; + BEAST_EXPECT(newMin == expectedNewMin); + BEAST_EXPECT(oldMin == expectedOldMin); + + // Try to withdraw so that remaining cover lands between the + // two minimums: oldMin < target < newMin. + auto const target = oldMin + (newMin - oldMin) / 2; + auto const withdrawAmount = STAmount{asset, coverAvail - target}; + + if (withAmendment) + { + // CoverWithdraw now uses vaultScale: target < newMin + // => FAILS. + env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount), + Ter(tecINSUFFICIENT_FUNDS)); + } + else + { + // Old CoverWithdraw uses debtScale: target > oldMin + // => SUCCEEDS. + env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount)); + } + env.close(); + }); + + // Scenario 3 — LoanSet + // + // Verify that LoanSet's minimum cover check uses vault scale (not the + // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet + // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no + // roundToAsset), while clawback/withdraw used different formulas. + // After the amendment all use minimumBrokerCover at vaultScale, and + // rounding at a coarser scale can absorb a tiny debt increase — + // allowing a loan that would otherwise be rejected. + testcase("LoanSet minimum cover scale consistency"); + runTest( + /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + // Create the tiny loan (scale -12) AND the big loan (scale + // -11). Both loans are needed so that DebtTotal has a full + // 16-digit mantissa — a "messy" value where roundToAsset at + // vaultScale actually truncates digits and produces a + // different result from the raw tenthBipsOfValue. With only + // the big loan, DebtTotal has ~4 significant digits and + // rounding at scale -11 is a no-op, masking the amendment's + // effect. + env(set(c.borrower, c.broker.brokerID, Number{1, -2}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Clawback to reduce cover to the clawback transactor's + // minimum. Pass the exact amount rather than relying on the + // transactor to clip down; the setup matches Scenario 1 so + // the same residual-cover values apply. + Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} + : Number{1330651855688458000, -15}; + Number const clawbackAmount = + Number{c.brokerParams.coverDeposit} - expectedCoverAfter; + env(coverClawback(c.issuer), + kLoanBrokerId(c.broker.brokerID), + kAmount(c.iou(clawbackAmount))); + env.close(); + + // Verify scales. + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(vaultSle)) + return; + auto const vaultScale = getAssetsTotalScale(vaultSle); + BEAST_EXPECT(vaultScale == -11); + + // Now try to create a tiny additional loan. Principal is + // 1e-11 (the smallest value that survives the precision + // check at loanScale = vaultScale = -11), with 0% interest + // and 1 payment. + // + // The tiny debt increase adds ~1.337e-12 to the unrounded + // minimum. + // - Without the amendment: the old LoanSet formula rounds + // up during tenthBipsOfValue (16-digit Number + // normalisation), pushing the minimum past the cover left + // by clawback => tecINSUFFICIENT_FUNDS. + // - With the amendment: minimumBrokerCover rounds at + // vaultScale=-11, which absorbs the tiny increase — the + // rounded minimum stays the same => tesSUCCESS. + auto const tinyPrincipal = Number{1, -11}; + + if (withAmendment) + { + env(set(c.borrower, c.broker.brokerID, tinyPrincipal), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + } + else + { + env(set(c.borrower, c.broker.brokerID, tinyPrincipal), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10)), + Ter(tecINSUFFICIENT_FUNDS)); + } + env.close(); + }); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testLoanPayComputePeriodicPaymentInvariants(features); + testLoanPayDebtDecreaseInvariant(features); + testAccountSendMptMinAmountInvariant(features); + testMinimumBrokerCoverConsistency(features); + } + +public: + void + run() override + { + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanInvariants, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanLifecycle_test.cpp b/src/test/app/lending/LoanLifecycle_test.cpp new file mode 100644 index 0000000000..e9868769b7 --- /dev/null +++ b/src/test/app/lending/LoanLifecycle_test.cpp @@ -0,0 +1,689 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanLifecycle_test : public LoanTestBase +{ +private: + void + testLifecycle(FeatureBitset features) + { + testcase("Lifecycle"); + using namespace jtx; + + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. + Env env(*this, features); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); + env.close(); + + // Create assets + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env(trust(evan, iouAsset(10'000'000))); + env(pay(issuer, evan, iouAsset(1'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, iouAsset(10'000))); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + // Scale the MPT asset a little bit so we can get some interest + PrettyAsset const mptAsset{mptt.issuanceID(), 100}; + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + mptt.authorize({.account = evan}); + env(pay(issuer, lender, mptAsset(10'000'000))); + env(pay(issuer, evan, mptAsset(1'000'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, mptAsset(10'000))); + env.close(); + + std::array const assets{iouAsset, xrpAsset, mptAsset}; + + // Create vaults and loan brokers + std::vector brokers; + brokers.reserve(assets.size()); + for (auto const& asset : assets) + { + brokers.emplace_back(createVaultAndBroker( + env, asset, lender, BrokerParameters{.data = "spam spam spam spam"})); + } + + // Create and update Loans + for (auto const& broker : brokers) + { + for (int amountExponent = 3; amountExponent >= 3; --amountExponent) + { + Number const loanAmount{1, amountExponent}; + for (int interestExponent = 0; interestExponent >= 0; --interestExponent) + { + testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent); + } + } + + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); + + auto const coverAvailable = brokerSle->at(sfCoverAvailable); + env(loan_broker::coverWithdraw( + lender, broker.brokerID, STAmount(broker.asset, coverAvailable))); + env.close(); + + brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); + } + // Verify we can delete the loan broker + env(loan_broker::del(lender, broker.brokerID)); + env.close(); + } + } + + void + testSelfLoan(FeatureBitset features) + { + testcase << "Self Loan"; + + using namespace jtx; + using namespace std::chrono_literals; + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. + Env env(*this, features); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender)); + env.close(); + + // Use an XRP asset for simplicity + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + // Create vaults and loan brokers + BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + // The LoanSet json can be created without a counterparty signature, + // but it will not pass preflight + auto createJson = env.json( + set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee)); + env(createJson, Ter(temBAD_SIGNER)); + + // Adding an empty counterparty signature object also fails, but + // at the RPC level. + createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object)); + env(createJson, Ter(telENV_RPC_FAILED)); + + if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) + { + Serializer s; + jt.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice())); + + BEAST_EXPECT(jr.isMember(jss::result)); + auto const jResult = jr[jss::result]; + BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); + BEAST_EXPECT( + jResult[jss::error_exception] == + "fails local checks: Transaction has bad signature."); + } + + // Copy the transaction signature into the counterparty signature. + json::Value counterpartyJson{json::ValueType::Object}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + // The duplicated signature works + createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + + auto const startDate = env.current()->header().parentCloseTime; + + // Loan is successfully created + { + auto const res = env.rpc("account_objects", lender.human()); + auto const objects = res[jss::result][jss::account_objects]; + + std::map types; + BEAST_EXPECT(objects.size() == 4); + for (auto const& object : objects) + { + ++types[object[sfLedgerEntryType].asString()]; + } + BEAST_EXPECT(types.size() == 4); + for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"}) + { + BEAST_EXPECT(types[type] == 1); + } + } + auto const loanID = [&]() { + json::Value params(json::ValueType::Object); + params[jss::account] = lender.human(); + params[jss::type] = "Loan"; + auto const res = env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + + BEAST_EXPECT(objects.size() == 1); + + auto const loan = objects[0u]; + BEAST_EXPECT(loan[sfBorrower] == lender.human()); + // soeDEFAULT fields are not returned if they're in the default + // state + BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); + BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); + BEAST_EXPECT(loan[sfFlags] == 0); + BEAST_EXPECT(loan[sfGracePeriod] == 60); + BEAST_EXPECT(!loan.isMember(sfInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); + BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); + BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); + BEAST_EXPECT(loan[sfLoanSequence] == 1); + BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); + BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); + BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); + BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); + BEAST_EXPECT(loan[sfPaymentInterval] == 60); + BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); + BEAST_EXPECT(loan[sfPaymentRemaining] == 1); + BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate)); + BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); + BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); + BEAST_EXPECT(!loan.isMember(sfLoanScale)); + BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); + + return loan["index"].asString(); + }(); + auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; + + env.close(startDate); + + // Make a payment + env(pay(lender, loanKeylet.key, broker.asset(1000))); + } + + void + testIssuerLoan() + { + testcase << "Issuer Loan"; + + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower = issuer; + Account const lender("lender"); + Env env(*this); + + env.fund(XRP(1'000), issuer, lender); + + static constexpr std::int64_t kIssuerBalance = 10'000'000; + MPTTester const asset( + {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance}); + + BrokerParameters const brokerParams{ + .debtMax = 200, + }; + auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); + auto const loanSetFee = Fee(env.current()->fees().base * 2); + // Create Loan + env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee); + env.close(); + // Issuer should not create MPToken + BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); + // Issuer "borrowed" 200, OutstandingAmount decreased by 200 + BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance + 200)); + // Pay Loan + auto const loanKeylet = keylet::loan(broker.brokerID, 1); + env(pay(borrower, loanKeylet.key, asset(200))); + env.close(); + // Issuer "re-payed" 200, OutstandingAmount increased by 200 + BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance)); + } + + void + testBorrowerIsBroker() + { + testcase("Test Borrower is Broker"); + using namespace jtx; + using namespace loan; + Account const broker{"broker"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + + auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { + Env env(*this); + Vault const vault(env); + + if (borrower == broker) + { + env.fund(XRP(10'000), broker, issuer, depositor); + } + else + { + env.fund(XRP(10'000), broker, borrower, issuer, depositor); + } + env.close(); + + auto const xrpFee = XRP(100); + auto const txFee = Fee(xrpFee); + + STAmount const debtMaximumRequest = getMaxDebt(env); + + auto const& asset = debtMaximumRequest.asset(); + auto const initialVault = asset(debtMaximumRequest * 100); + + auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset}); + env(tx, txFee); + env.close(); + + env(vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}), + txFee); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); + + env(loan_broker::set(broker, vaultKeylet.key), txFee); + env.close(); + + auto const serviceFee = 101; + + env(set(broker, brokerKeylet.key, debtMaximumRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + kLoanServiceFee(serviceFee), + kPaymentTotal(10), + txFee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + + auto const brokerBalanceBefore = env.balance(broker, asset); + + if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle)) + { + auto const payment = loanSle->at(sfPeriodicPayment); + auto const totalPayment = payment + serviceFee; + env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee); + env.close(); + if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle)) + { + auto const expected = [&]() { + // The service fee is transferred to the broker if + // a borrower is not the broker + if (borrower != broker) + return brokerBalanceBefore.number() + serviceFee; + // Since a borrower is the broker, the payment is + // transferred to the Vault from the broker but not + // the service fee. + // If the asset is XRP then the broker pays the txFee. + if (asset.native()) + return brokerBalanceBefore.number() - payment - xrpFee.number(); + return brokerBalanceBefore.number() - payment; + }(); + BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value()); + } + } + }; + // Test when a borrower is the broker and is not to verify correct + // service fee transfer in both cases. + for (auto const& borrowerAcct : {broker, borrower}) + { + testLoanAsset( + [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + auto const iou = issuer["USD"]; + env(trust(broker, iou(1'000'000'000))); + env(trust(depositor, iou(1'000'000'000))); + env(pay(issuer, broker, iou(100'000'000))); + env(pay(issuer, depositor, iou(100'000'000))); + env.close(); + return iou(200'000); + }, + borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + MPTTester const mpt( + {.env = env, + .issuer = issuer, + .holders = {broker, depositor}, + .pay = 100'000'000}); + return mpt(200'000); + }, + borrowerAcct); + } + } + + void + testIssuerIsBorrower(FeatureBitset features) + { + testcase("RIPD-4096 - Issuer as borrower"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, .counter = issuer, .principalRequest = Number{10000}}; + + auto const assetType = AssetType::IOU; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + issuer, + PaymentParameters{.showStepBalances = true}); + } + + void + testBatchBypassCounterparty(FeatureBitset features) + { + // From FIND-001 + testcase << "Batch Bypass Counterparty"; + + bool const lendingBatchEnabled = !std::ranges::any_of( + Batch::kDisabledTxTypes, [](auto const& disabled) { return disabled == ttLOAN_SET; }); + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + BrokerParameters const brokerParams; + env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0); + + json::Value randomData{json::ValueType::Object}; + randomData[jss::SigningPubKey] = json::StaticString{"2600"}; + json::Value sigObject{json::ValueType::Object}; + sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); + Serializer ss; + ss.add32(HashPrefix::TxSign); + parse(randomData).addWithoutSigningFields(ss); + auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice()); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + + forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject; + + // ? Fails because the lender hasn't signed the tx + env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED)); + + auto const seq = env.seq(borrower); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + // ! Should fail because the lender hasn't signed the tx + env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), + batch::Inner(forgedLoanSet, seq + 1), + batch::Inner(pay(borrower, lender, XRP(1)), seq + 2), + Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH)); + env.close(); + + // ? Check that the loan was NOT created + { + json::Value params(json::ValueType::Object); + params[jss::account] = borrower.human(); + params[jss::type] = "Loan"; + auto const res = env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + BEAST_EXPECT(objects.size() == 0); + } + } + + // Integration test: full lifecycle of a $1B loan in the bug regime. + // Verifies that the vault collects the economically-correct interest + // income and that conservation holds at the trust-line level. + // + // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only + // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10. + // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B, + // matching the value computed independently with arbitrary-precision + // Decimal arithmetic. + void + testFullLifecycleVaultPnLNearZeroRate() + { + testcase("integration: full loan lifecycle, vault interest at near-zero rate"); + + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + STAmount const trustLimit{iouAsset.raw(), Number{1, 17}}; + env(trust(lender, trustLimit)); + env(trust(borrower, trustLimit)); + env.close(); + env(pay(issuer, lender, iouAsset(5'000'000'000LL))); + env(pay(issuer, borrower, iouAsset(5'000'000'000LL))); + env.close(); + + auto usdBalance = [&](Account const& a) { + return env.balance(a, iouAsset.raw().get()).value(); + }; + STAmount const borrowerStartBal = usdBalance(borrower); + + BrokerParameters const brokerParams{ + .vaultDeposit = Number{2, 9}, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + auto const vaultBefore = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultBefore)) + return; + Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable); + + // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32. + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 9}; + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + createJson["InterestRate"] = 1; + createJson["PaymentTotal"] = 3; + createJson["PaymentInterval"] = 600; + + auto const loanKeylet = nextLoanKeylet(env, broker); + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return; + Number const expectedTotalInterest = + loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding); + + env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultAfter)) + return; + Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable); + Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore; + + STAmount const borrowerEndBal = usdBalance(borrower); + STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal; + + // Self-consistency: vault gained exactly the expected interest + // computed at LoanSet, and the borrower's outflow matches. + BEAST_EXPECT(vaultGain == expectedTotalInterest); + BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest); + + // Mathematical correctness: the total interest for this loan + // configuration is 0.38051750382930729983, calculated + // independently using 50-digit Decimal arithmetic (no + // cancellation possible at that precision). At Number's 19-digit + // mantissa this rounds to 0.38051750382930729 — the literal + // below. The vault's actual gain must agree to within + // sub-microcent precision. + Number const decimalReference{38051750382930729LL, -17}; + Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent + Number const error = abs(vaultGain - decimalReference); + BEAST_EXPECTS( + error < tolerance, + "vault gain " + to_string(vaultGain) + " differs from Decimal reference " + + to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " + + to_string(tolerance)); + } + + void + runAmendmentIndependent() + { + testIssuerLoan(); + testBorrowerIsBroker(); + testFullLifecycleVaultPnLNearZeroRate(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testLifecycle(features); + testSelfLoan(features); + testIssuerIsBorrower(features); + testBatchBypassCounterparty(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanLifecycle, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanMisc_test.cpp b/src/test/app/lending/LoanMisc_test.cpp new file mode 100644 index 0000000000..798cddda17 --- /dev/null +++ b/src/test/app/lending/LoanMisc_test.cpp @@ -0,0 +1,561 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanMisc_test : public LoanTestBase +{ +private: + void + testRPC(FeatureBitset features) + { + // This will expand as more test cases are added. Some functionality + // is tested in other test functions. + testcase("RPC"); + + using namespace jtx; + + Env env(*this, features); + + auto lowerFee = [&]() { + // Run the local fee back down. + while (env.app().getFeeTrack().lowerLocalFee()) + ; + }; + + auto const baseFee = env.current()->fees().base; + + Account const alice{"alice"}; + std::string const borrowerPass = "borrower"; + Account const borrower{borrowerPass, KeyType::Ed25519}; + auto const lenderPass = "lender"; + Account const lender{lenderPass, KeyType::Ed25519}; + + env.fund(XRP(1'000'000), alice, lender, borrower); + env.close(); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env.close(); + + { + testcase("RPC AccountSet"); + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const signParams = [&]() { + json::Value signParams{json::ValueType::Object}; + signParams[jss::passphrase] = borrowerPass; + signParams[jss::key_type] = "ed25519"; + signParams[jss::tx_json] = txJson; + return signParams; + }(); + auto const jSign = env.rpc("json", "sign", to_string(signParams)); + BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json)); + auto txSignResult = jSign[jss::result][jss::tx_json]; + auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); + txSignResult.removeMember(jss::hash); + + auto const jtx = env.jt(txJson, Sig(borrower)); + BEAST_EXPECT(txSignResult == jtx.jv); + + lowerFee(); + auto const jSubmit = env.rpc("submit", txSignBlob); + BEAST_EXPECT( + jSubmit.isMember(jss::result) && + jSubmit[jss::result].isMember(jss::engine_result) && + jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); + + lowerFee(); + env(jtx.jv, Sig(kNone), Seq(kNone), Fee(kNone), Ter(tefPAST_SEQ)); + } + + { + testcase("RPC LoanSet - illegal signature_target"); + + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "Destination"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::error) && + jSignBorrower[jss::result][jss::error] == "invalidParams" && + jSignBorrower[jss::result].isMember(jss::error_message) && + jSignBorrower[jss::result][jss::error_message] == "Destination"); + } + { + testcase("RPC LoanSet - sign and submit borrower initiated"); + // 1. Borrower creates the transaction + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = borrower.human(); + txJson[sfCounterparty] = lender.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = to_string(24 * baseFee / 10); + + // 2. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECTS( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json), + to_string(jSignBorrower)); + auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 2a. Borrower attempts to submit the transaction. It doesn't + // work + { + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); + } + + // 3. Borrower sends the signed transaction to the lender + // 4. Lender signs the transaction + auto const lenderSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txBorrowerSignResult; + return params; + }(); + auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); + + // 5. Lender submits the signed transaction blob + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", + to_string(jSubmitBlobResult)); + + BEAST_EXPECT( + !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Lender submits the transaction json originally + // received from the Borrower. It gets signed, but is now a + // duplicate, so fails. Borrower could done this instead of + // steps 4 and 5. + lowerFee(); + auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", + to_string(jSubmitJsonResult)); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); + } + + { + testcase("RPC LoanSet - sign and submit lender initiated"); + // 1. Lender creates the transaction + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = lender.human(); + txJson[sfCounterparty] = borrower.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = to_string(24 * baseFee / 10); + + // 2. Lender signs the transaction + auto const lenderSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); + + // 2a. Lender attempts to submit the transaction. It doesn't + // work + { + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); + } + + // 3. Lender sends the signed transaction to the Borrower + // 4. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txLenderSignResult; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json)); + auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 5. Borrower submits the signed transaction blob + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", + to_string(jSubmitBlobResult)); + + BEAST_EXPECT( + !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Borrower submits the transaction json originally + // received from the Lender. It gets signed, but is now a + // duplicate, so fails. Lender could done this instead of steps + // 4 and 5. + lowerFee(); + auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", + to_string(jSubmitJsonResult)); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); + } + } + + void + testLendingCanTradeDisabledNoImpact() + { + testcase("Lending: CanTrade disabled has no impact"); + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + MPTTester mpt( + {.env = env, + .issuer = issuer, + .holders = {lender, borrower}, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanEnableCanTrade}); + PrettyAsset const asset = mpt.issuanceID(); + env(pay(issuer, lender, asset(10'000'000))); + env(pay(issuer, borrower, asset(100'000))); + env.close(); + + auto const broker = createVaultAndBroker(env, asset, lender); + + // CanTrade is not set + env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); + env.close(); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + // New cover deposits still work. + env(coverDeposit(lender, broker.brokerID, asset(100))); + env.close(); + + // New loan issuance still works. + env(loan::set(borrower, broker.brokerID, 1'000), + Sig(sfCounterpartySignature, lender), + loanSetFee); + env.close(); + auto const loanKeylet = keylet::loan(broker.brokerID, 1); + BEAST_EXPECT(env.le(loanKeylet)); + + // Repayment still works. + env(pay(borrower, loanKeylet.key, asset(1'000))); + env.close(); + + // Cover withdrawal still works. + env(coverWithdraw(lender, broker.brokerID, asset(100))); + env.close(); + + // Enable CanTrade and verify the DEX path is restored. + mpt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(lender, XRP(1), asset(10))); + env.close(); + } + + void + runAmendmentIndependent() + { + testLendingCanTradeDisabledNoImpact(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testRPC(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +class LoanBatch_test : public LoanTestBase +{ +protected: + beast::xor_shift_engine engine_; + + std::uniform_int_distribution<> assetDist_{0, 2}; + std::uniform_int_distribution principalDist_{100'000, 1'000'000'000}; + std::uniform_int_distribution interestRateDist_{0, 10000}; + std::uniform_int_distribution<> paymentTotalDist_{12, 10000}; + std::uniform_int_distribution<> paymentIntervalDist_{60, 3600 * 24 * 30}; + std::uniform_int_distribution managementFeeRateDist_{0, 10'000}; + std::uniform_int_distribution<> serviceFeeDist_{0, 20}; + /* + # Generate parameters that are more likely to be valid + principal = Decimal(str(rand.randint(100000, + 100'000'000))).quantize(ROUND_TARGET) + + interest_rate = Decimal(rand.randint(1, 10000)) / + Decimal(100000) + + payment_total = rand.randint(12, 10000) + + payment_interval = Decimal(str(rand.randint(60, 2629746))) + + interest_fee = Decimal(rand.randint(0, 100000)) / + Decimal(100000) +*/ + + void + testRandomLoan() + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + // Determine all the random parameters at once + auto const assetType = static_cast(assetDist_(engine_)); + auto const principalRequest = principalDist_(engine_); + TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; + auto const serviceFee = serviceFeeDist_(engine_); + TenthBips32 interest{interestRateDist_(engine_)}; + auto const payTotal = paymentTotalDist_(engine_); + auto const payInterval = paymentIntervalDist_(engine_); + + BrokerParameters const brokerParams{ + .vaultDeposit = principalRequest * 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = managementFeeRate}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = principalRequest, + .serviceFee = serviceFee, + .interest = interest, + .payTotal = payTotal, + .payInterval = payInterval, + }; + + runLoan(assetType, brokerParams, loanParams, all_); + } + +public: + void + run() override + { + auto const numIterations = [s = arg()]() -> int { + int const defaultNum = 5; + if (s.empty()) + return defaultNum; + try + { + std::size_t pos = 0; + auto const r = stoi(s, &pos); + if (pos != s.size()) + return defaultNum; + return r; + } + catch (...) + { + return defaultNum; + } + }(); + + using namespace jtx; + + auto const updateInterval = std::max(std::min(numIterations / 5, 100), 1); + + for (int i = 0; i < numIterations; ++i) + { + if (i % updateInterval == 0) + testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations; + testRandomLoan(); + } + } +}; + +class LoanArbitrary_test : public LoanBatch_test +{ + void + run() override + { + using namespace jtx; + + BrokerParameters const brokerParams{ + .vaultDeposit = 10000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = Account("lender"), + .counter = Account("borrower"), + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 2, + .payInterval = 200}; + + runLoan(AssetType::XRP, brokerParams, loanParams, all_); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanMisc, tx, xrpl); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, xrpl); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanPay_test.cpp b/src/test/app/lending/LoanPay_test.cpp new file mode 100644 index 0000000000..e06d728c87 --- /dev/null +++ b/src/test/app/lending/LoanPay_test.cpp @@ -0,0 +1,760 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanPay_test : public LoanTestBase +{ +private: +#if LOAN_TODO + void + testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features) + { + testcase("LoanPay full payment skips late penalties"); + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(100'000'000))); + env(trust(borrower, asset(100'000'000))); + env(pay(issuer, lender, asset(50'000'000))); + env(pay(issuer, borrower, asset(5'000'000))); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID)); + if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value()) + return; + + auto const loanSequence = brokerPreLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + Number const principal = asset(1'000).value(); + Number const serviceFee = asset(2).value(); + Number const lateFee = asset(5).value(); + Number const closeFee = asset(4).value(); + + env(set(borrower, broker.brokerID, principal), + Sig(sfCounterpartySignature, lender), + kLoanServiceFee(serviceFee), + kLatePaymentFee(lateFee), + kClosePaymentFee(closeFee), + kInterestRate(percentageToTenthBips(12)), + kLateInterestRate(percentageToTenthBips(24) / 10), + kCloseInterestRate(percentageToTenthBips(5)), + kPaymentTotal(12), + kPaymentInterval(600), + kGracePeriod(0), + Fee(loanSetFee)); + env.close(); + + auto state1 = getCurrentState(env, broker, loanKeylet); + if (!BEAST_EXPECT(state1.paymentRemaining > 1)) + return; + + using d = NetClock::duration; + using tp = NetClock::time_point; + auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}}; + env.close(overdueClose); + + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(brokerSle && loanSle)) + return; + + auto state = getCurrentState(env, broker, loanKeylet); + + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; + TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)}; + + Number const closePaymentFeeRounded = + roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); + Number const latePaymentFeeRounded = + roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); + + auto const roundedLoanState = constructLoanState( + state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); + Number const totalInterestOutstanding = roundedLoanState.interestDue; + + auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); + auto const rawLoanState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRate); + + auto const parentCloseTime = env.current()->parentCloseTime(); + auto const startDateSeconds = + static_cast(state.startDate.time_since_epoch().count()); + + Number const fullPaymentInterest = computeFullPaymentInterest( + rawLoanState.principalOutstanding, + periodicRate, + parentCloseTime, + state.paymentInterval, + state.previousPaymentDate, + startDateSeconds, + closeInterestRateValue); + + Number const roundedFullInterestAmount = + roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); + Number const roundedFullManagementFee = computeManagementFee( + broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale); + Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee; + + Number const trackedValueDelta = + state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding; + Number const untrackedManagementFee = + closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding; + Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding; + + Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee; + BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale)); + + auto const overdueSeconds = + parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; + if (!BEAST_EXPECT(overdueSeconds > 0)) + return; + + Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds); + Number const lateInterestRaw = state.principalOutstanding * overdueRate; + Number const lateInterestRounded = + roundToAsset(broker.asset, lateInterestRaw, state.loanScale); + Number const lateManagementFeeRounded = computeManagementFee( + broker.asset, lateInterestRounded, managementFeeRate, state.loanScale); + Number const penaltyDue = + lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded; + BEAST_EXPECT(penaltyDue > Number{}); + + auto const balanceBefore = env.balance(borrower, broker.asset).number(); + + STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; + env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); + env.close(); + + if (auto const meta = env.meta(); BEAST_EXPECT(meta)) + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + + auto const balanceAfter = env.balance(borrower, broker.asset).number(); + Number const actualPaid = balanceBefore - balanceAfter; + BEAST_EXPECT(actualPaid == baseFullDue); + + Number const expectedWithPenalty = baseFullDue + penaltyDue; + BEAST_EXPECT(expectedWithPenalty > actualPaid); + BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); + } +#endif + + void + testOverpaymentManagementFee(FeatureBitset features) + { + testcase("testOverpaymentManagementFee"); + + using namespace jtx; + using namespace loan; + + Env env{*this, features}; + + Account const lender{"lender"}, borrower{"borrower"}; + + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1000}; + + auto const result = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = asset(100'000).value(), + .managementFeeRate = TenthBips16(10'000), + }); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const brokerSle = env.le(result.brokerKeylet()); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const loanKeylet = + keylet::loan(result.brokerKeylet().key, brokerSle->at(sfLoanSequence)); + env(loan::set( + borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + loan::kPaymentInterval(86400 * 30), + loan::kPaymentTotal(3), + loan::kOverpaymentInterestRate(TenthBips32(percentageToTenthBips(20))), + loanSetFee); + + // From calculator + auto const expectedOverpaymentManagementFee = Number{33333, 0}; + auto const loanBrokerBalanceBefore = env.balance(lender); + + auto const loanPayFee = Fee(env.current()->fees().base * 2); + env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee); + env.close(); + + BEAST_EXPECTS( + env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee, + "overpayment management fee mismatch; expected:" + + to_string(expectedOverpaymentManagementFee) + + " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore)); + } + + void + testDosLoanPay(FeatureBitset features) + { + bool const feeCapped = features[fixCleanup3_1_3]; + + // From FIND-005 + testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped"); + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3)); + + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(100'000'000))); + env(trust(borrower, iouAsset(100'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(1'000))); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{3959'37, -2}; + auto const baseFee = env.current()->fees().base; + + auto const createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object), + kClosePaymentFee(0), + kGracePeriod(60), + kInterestRate(TenthBips32(20930)), + kLateInterestRate(TenthBips32(77049)), + kLatePaymentFee(0), + kLoanServiceFee(0), + kOverpaymentFee(TenthBips32(7)), + kOverpaymentInterestRate(TenthBips32(66653)), + kPaymentInterval(60), + kPaymentTotal(3239184)); + + // There are enough payments due on this loan that it only needs to be + // created once, and can be paid on multiple times. Just don't create a + // gazillion test cases. + auto const keylet = nextLoanKeylet(env, broker); + + env(createJson, Sig(sfCounterpartySignature, lender)); + env.close(); + + auto const roundedPayment = [&]() { + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + return roundToAsset( + iouAsset, + stateBefore.periodicPayment, + stateBefore.loanScale, + Number::RoundingMode::Upward); + }(); + + auto test = [&](int const payFactor, + int const feeFactor, + TER const expectedTer = tesSUCCESS) { + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + Number const amount = roundedPayment * payFactor; + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount})); + XRPAmount const payFee{baseFee * feeFactor}; + env(loanPayTx, Ter(expectedTer), Fee(payFee)); + env.close(); + auto const expectedChange = isTesSuccess(expectedTer) + ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor) + : 0; + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange); + }; + + static constexpr std::int64_t kMaxFeeIncrements = + kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement; + + TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P; + + // * Amount well above threshold -> capped fee + // The original test case - way over the limit - more fee is always ok + test(1819878, 363976); + // The capped fee is only sufficient if the amendment is enabled. + test(1819878, kMaxFeeIncrements, failWithoutFix); + + // * Amount exactly at threshold -> capped fee + test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements); + // More fee is always ok + test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10); + + // * Amount below threshold -> normal calculation + test(1, 1); + test(kLoanPaymentsPerFeeIncrement * 2, 2); + test(0, 0, temBAD_AMOUNT); + test(0, 1, temBAD_AMOUNT); + // Fee difference rounds evenly + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1, + telINSUF_FEE_P); + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement)); + // More fee is always ok + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3); + // Fee rounds up + for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under) + { + test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P); + test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements); + } + // Only when you get one less fee increment can you pay less + test( + kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, + kMaxFeeIncrements - 1); + // And again, more fee is always ok. + test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements); + } + + // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero + // rate). At such a near-zero rate the closed-form payment factor + // (1 + r)^n - 1 cancels catastrophically. + // + // Without fixCleanup3_2_0 the resulting amortization is degenerate and the + // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the + // amendment, computePowerMinusOneHybrid uses a numerically-stable series + // expansion, so the loan is created and the scheduled payments + // (2 * periodicPayment) cover the principal — no economic underpayment + // (yield theft). + // + // The test runs the same LoanSet under both amendment settings and pins the + // exact outcome for each. + void + testLoanSetNearZeroInterestRateSucceeds() + { + testcase("LoanSet near-zero interest rate covers principal"); + + using namespace jtx; + using namespace loan; + + Number const principalRequested{1000}; + + struct Result + { + TER ter = tesSUCCESS; + bool created = false; + std::int32_t loanScale = 0; + Number principal; + Number totalValue; + Number managementFee; + Number periodicPayment; + }; + + auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}}); + + auto const brokerSle = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerSle); + auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(set(borrower, broker.brokerID, principalRequested), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32{1}), + kPaymentTotal(2), + kPaymentInterval(400), + Fee(env.current()->fees().base * 2), + Ter(expectedTer)); + env.close(); + + Result r; + r.ter = env.ter(); + if (auto const loanSle = env.le(loanKeylet)) + { + r.created = true; + r.loanScale = loanSle->at(sfLoanScale); + r.principal = loanSle->at(sfPrincipalOutstanding); + r.totalValue = loanSle->at(sfTotalValueOutstanding); + r.managementFee = loanSle->at(sfManagementFeeOutstanding); + r.periodicPayment = loanSle->at(sfPeriodicPayment); + } + return r; + }; + + Result const fixed = runScenario(all_, tesSUCCESS); + Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS); + + // Without the amendment, the catastrophically-cancelling closed-form + // payment factor produces a degenerate amortization that fails + // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no + // loan is created. + BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS); + BEAST_EXPECT(!legacy.created); + + // With the amendment the stable series expansion produces a valid loan + // at loanScale -10. + BEAST_EXPECT(fixed.ter == tesSUCCESS); + BEAST_EXPECT(fixed.created); + BEAST_EXPECT(fixed.loanScale == -10); + BEAST_EXPECT(fixed.principal == principalRequested); + BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10})); + BEAST_EXPECT(fixed.managementFee == beast::kZero); + + // Periodic payment from the numerically-stable series expansion, and the + // scheduled total (2 * periodicPayment) which exceeds the 1000 principal + // — no economic underpayment / yield theft. + BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16})); + BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15})); + BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested); + } + + void + testLoanNextPaymentDueDateOverflow(FeatureBitset features) + { + // For FIND-013 + testcase << "Prevent nextPaymentDueDate overflow"; + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = + createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000); + + BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + using timeType = decltype(sfNextPaymentDueDate)::type::value_type; + static_assert(std::is_same_v); + constexpr timeType kMaxTime = std::numeric_limits::max(); + static_assert(kMaxTime == 4'294'967'295); + + auto const baseJson = [&]() { + auto createJson = env.json( + set(borrower, broker.brokerID, Number{55524'81, -2}), + Fee(loanSetFee), + kClosePaymentFee(0), + kGracePeriod(LoanSet::kDefaultGracePeriod), + kInterestRate(TenthBips32(12833)), + kLateInterestRate(TenthBips32(77048)), + kLatePaymentFee(0), + kLoanOriginationFee(218), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson.removeMember(sfSequence.getJsonName()); + + return createJson; + }(); + + auto const baseFee = env.current()->fees().base; + + auto parentCloseTime = [&]() { + return env.current()->parentCloseTime().time_since_epoch().count(); + }; + auto maxLoanTime = [&]() { + auto const startDate = parentCloseTime(); + + BEAST_EXPECT(startDate >= 50); + + return kMaxTime - startDate; + }; + + { + // straight-up overflow: interval + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // straight-up overflow: total + // min interval is 60 + auto const interval = 60; + auto const total = maxLoanTime() + 1; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // straight-up overflow: grace period + // min interval is 60 + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto const grace = interval; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + // The grace period can't be larger than the interval. + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of a few large intervals + auto const interval = 1'000'000'000; + auto const total = 10; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of many small payments + // min interval is 60 + auto const interval = 60; + auto const total = 1'000'000'000; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with an absurdly large grace period + // min interval is 60 + auto const total = 60; + auto const interval = (maxLoanTime() - total) / total; + auto const grace = interval; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Start date when the ledger is closed will be larger + auto const keylet = nextLoanKeylet(env, broker); + + auto const grace = 100; + auto const interval = maxLoanTime() - grace; + auto const total = 1; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // The transaction is killed in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); + } + + // If the transaction had succeeded, the loan would exist + auto const loanSle = env.le(keylet); + // but it doesn't + BEAST_EXPECT(!loanSle); + } + { + // Start date when the ledger is closed will be larger + auto const keylet = nextLoanKeylet(env, broker); + + auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; + auto const grace = 5'000; + auto const interval = kMaxTime - closeStartDate - grace; + auto const total = 1; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // The transaction succeeds in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + } + + // This loan exists + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); + BEAST_EXPECT(afterState.previousPaymentDate == 0); + BEAST_EXPECT(afterState.paymentRemaining == 1); + } + + { + // Ensure the borrower has funds to pay back the loan + env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); + + // Start date when the ledger is closed will be larger + auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; + auto const grace = 5'000; + auto const maxLoanTime = kMaxTime - closeStartDate - grace; + auto const total = [&]() { + if (maxLoanTime % 5 == 0) + return 5; + if (maxLoanTime % 3 == 0) + return 3; + if (maxLoanTime % 2 == 0) + return 2; + return 0; + }(); + if (!BEAST_EXPECT(total != 0)) + return; + + auto const brokerState = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerState)) + return; + // Intentionally shadow the outer values + auto const loanSequence = brokerState->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + auto const interval = maxLoanTime / total; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // This loan exists + auto const beforeState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval); + BEAST_EXPECT(beforeState.previousPaymentDate == 0); + BEAST_EXPECT(beforeState.paymentRemaining == total); + BEAST_EXPECT(beforeState.periodicPayment > 0); + + // pay all but the last payment + { + NumberRoundModeGuard const mg{Number::RoundingMode::Upward}; + Number const payment = beforeState.periodicPayment * (total - 1); + XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)}; + STAmount const paymentAmount = + roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale); + auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee)); + env(loanPayTx, Ter(tesSUCCESS)); + env.close(); + } + + // The loan is on the last payment + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.paymentRemaining == 1); + BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); + BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval); + } + } + + void + runAmendmentIndependent() + { + testLoanSetNearZeroInterestRateSucceeds(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { +#if LOAN_TODO + testLoanPayLateFullPaymentBypassesPenalties(features); +#endif + testOverpaymentManagementFee(features); + testDosLoanPay(features); + testLoanNextPaymentDueDateOverflow(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanPay, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanRounding_test.cpp b/src/test/app/lending/LoanRounding_test.cpp new file mode 100644 index 0000000000..b914961e78 --- /dev/null +++ b/src/test/app/lending/LoanRounding_test.cpp @@ -0,0 +1,993 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanRounding_test : public LoanTestBase +{ +private: + void + testDustManipulation(FeatureBitset features) + { + testcase("Dust manipulation"); + + using namespace jtx; + using namespace std::chrono_literals; + Env env{*this, features}; + + // Setup: Create accounts + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + Account const victim{"victim"}; + + env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); + env.close(); + + // Step 1: Create vault with IOU asset + auto asset = issuer["USD"]; + env(trust(lender, asset(100000))); + env(trust(borrower, asset(100000))); + env(trust(victim, asset(100000))); + env(pay(issuer, lender, asset(50000))); + env(pay(issuer, borrower, asset(50000))); + env(pay(issuer, victim, asset(50000))); + env.close(); + + BrokerParameters const brokerParams{ + .vaultDeposit = 10000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{1000}, + .coverRateLiquidation = TenthBips32{2500}}; + + auto broker = createVaultAndBroker(env, asset, lender, brokerParams); + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the + // _LOAN_BROKER_ object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + }(); + if (!loanKeyletOpt) + return; + + auto const& vaultKeylet = broker.vaultKeylet(); + + { + auto const vaultSle = env.le(vaultKeylet); + Number const assetsTotal = vaultSle->at(sfAssetsTotal); + Number const assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "Before loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + // before the loan the assets total and available should be equal + BEAST_EXPECT(assetsAvail == assetsTotal); + BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number()); + } + + Keylet const& loanKeylet = *loanKeyletOpt; + + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100}, + .interest = TenthBips32{1922}, + .payTotal = 5816, + .payInterval = 86400 * 6, + .gracePd = 86400 * 5, + }; + + env(loanParams(env, broker)); + env.close(); + + // Wait for loan to be late enough to default + env.close(std::chrono::seconds(86400 * 40)); // 40 days + + { + auto const vaultSle = env.le(vaultKeylet); + Number const assetsTotal = vaultSle->at(sfAssetsTotal); + Number const assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "After loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return; + auto const state = constructLoanState(loanSle); + + log << "Loan state:" << std::endl; + log << " ValueOutstanding: " << state.valueOutstanding << std::endl; + log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl; + log << " InterestOutstanding: " << state.interestOutstanding() << std::endl; + log << " InterestDue: " << state.interestDue << std::endl; + log << " FeeDue: " << state.managementFeeDue << std::endl; + + // after loan creation the assets total and available should + // reflect the value of the loan + BEAST_EXPECT(assetsAvail < assetsTotal); + BEAST_EXPECT( + assetsAvail == + broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number()); + BEAST_EXPECT( + assetsTotal == + broker.asset(brokerParams.vaultDeposit + state.interestDue).number()); + } + + // Step 7: Trigger default (dust adjustment will occur) + env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // Step 8: Verify phantom assets created + { + auto const vaultSle2 = env.le(vaultKeylet); + Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal); + Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable); + + log << "After default:" << std::endl; + log << " AssetsTotal: " << assetsTotal2 << std::endl; + log << " AssetsAvailable: " << assetsAvail2 << std::endl; + log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl; + + // after a default the assets total and available should be equal + BEAST_EXPECT(assetsAvail2 == assetsTotal2); + } + } + + void + testRoundingAllowsUndercoverage(FeatureBitset features) + { + testcase("Minimum cover rounding allows undercoverage (XRP)"); + + using namespace jtx; + using namespace loan_broker; + + Env env{*this, features}; + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(200'000), lender, borrower); + env.close(); + + // Vault with XRP asset + Vault const vault{env}; + auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()}); + env(vaultCreate); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + // Seed the vault with XRP so it can fund the loan principal + PrettyAsset const xrpAsset{xrpIssue(), 1}; + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{10'000}, + .coverDeposit = 82, + }; + + auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams); + // Create a loan with principal 804 XRP and 0% interest (so + // DebtTotal increases by exactly 804) + env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), + loan::kInterestRate(TenthBips32(0)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2)); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Verify DebtTotal is exactly 804 + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + log << *brokerSle << std::endl; + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + } + + // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. + // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), + Ter(tecINSUFFICIENT_FUNDS)); + BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); + env.close(); + + // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. + // because that leaves sufficient cover, this succeeds + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Validate CoverAvailable == 81 XRP and DebtTotal remains 804 + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + log << *brokerSle << std::endl; + BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + + // Also demonstrate that the true minimum (804 * 10%) exceeds 80 + auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000)); + log << "Theoretical min cover: " << theoreticalMin << std::endl; + BEAST_EXPECT(Number(804, -1) == theoreticalMin); + } + } + + void + testYieldTheftRounding(std::uint32_t flags) + { + testcase("Rounding manipulation does not permit yield theft"); + using namespace jtx; + using namespace loan; + + // 1. Setup Environment + Env env(*this, all_); + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1000), issuer, lender, borrower); + env.close(); + + // 2. Asset Selection + PrettyAsset const iou = issuer["USD"]; + env(trust(lender, iou(100'000'000))); + env(trust(borrower, iou(100'000'000))); + env(pay(issuer, lender, iou(100'000'000))); + env(pay(issuer, borrower, iou(100'000'000))); + env.close(); + + // 3. Create Vault and Broker with High Debt Limit (100M) + auto const brokerInfo = createVaultAndBroker( + env, + iou, + lender, + { + .vaultDeposit = 5'000'000, + .debtMax = Number{100'000'000}, + .coverDeposit = 500'000, + }); + auto const [currentSeq, vaultKeylet] = [&]() { + auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::make_tuple(0u, keylet::unchecked(beast::kZero)); + auto const currentSeq = brokerSle->at(sfLoanSequence); + auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID)); + return std::make_tuple(currentSeq, vaultKeylet); + }(); + + // 4. Loan Parameters (Attack Vector) + Number const principal = 1'000'000; + TenthBips32 const interestRate = TenthBips32{1}; // 0.001% + std::uint32_t const paymentInterval = 86400; + std::uint32_t const paymentTotal = 3650; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags), + Sig(sfCounterpartySignature, lender), + loan::kInterestRate(interestRate), + loan::kPaymentInterval(paymentInterval), + loan::kPaymentTotal(paymentTotal), + Fee(loanSetFee)); + env.close(); + + // --- RETRIEVE OBJECTS & SETUP ATTACK --- + + auto borrowerBalance = [&]() { return env.balance(borrower, iou); }; + auto const borrowerScale = static_cast(borrowerBalance()).exponent(); + + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq); + auto const maybePeriodicPayment = [&]() -> std::optional { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return std::nullopt; + // Construct Payment + return STAmount{iou, loanSle->at(sfPeriodicPayment)}; + }(); + if (!maybePeriodicPayment) + return; + auto const periodicPayment = *maybePeriodicPayment; + auto const roundedPayment = + roundToScale(periodicPayment, borrowerScale, Number::RoundingMode::Upward); + + // ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution + STAmount const paymentBuffer{iou, Number(1, -9)}; + STAmount const attackPayment = periodicPayment + paymentBuffer; + + auto const maybeInitialVaultAssets = [&]() -> std::optional { + auto const vault = env.le(vaultKeylet); + if (!BEAST_EXPECT(vault)) + return std::nullopt; + return vault->at(sfAssetsTotal); + }(); + if (!maybeInitialVaultAssets) + return; + auto const initialVaultAssets = *maybeInitialVaultAssets; + + // 5. Execution Loop + int yieldTheftCount = 0; + auto previousAssetsTotal = initialVaultAssets; + + for (int i = 0; i < 100; ++i) + { + auto const balanceBefore = borrowerBalance(); + env(pay(borrower, loanKeylet.key, attackPayment, flags)); + env.close(); + auto const borrowerDelta = balanceBefore - borrowerBalance(); + BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum()); + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + break; + auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)}; + BEAST_EXPECT( + (roundToScale(updatedPayment, borrowerScale, Number::RoundingMode::Upward) == + roundedPayment)); + BEAST_EXPECT( + (updatedPayment == periodicPayment) || + (flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment)); + + auto const currentVaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(currentVaultSle)) + break; + + auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal); + auto const delta = currentAssetsTotal - previousAssetsTotal; + + BEAST_EXPECT( + (delta == beast::kZero && borrowerDelta <= roundedPayment) || + (delta > beast::kZero && borrowerDelta > roundedPayment)); + + // If tx succeeded but Assets Total didn't change, interest was + // stolen. + if (delta == beast::kZero && borrowerDelta > roundedPayment) + { + yieldTheftCount++; + } + + previousAssetsTotal = currentAssetsTotal; + } + + BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount)); + } + + // Regression for the dual-rounding fix at coarse (integer-MPT) scale. + // + // Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The + // amortization schedule produces a fractional principal + // (~0.47) which under round-to-nearest collapses to 0 in a single + // step, causing `doPayment`'s strict `>` assertion on principal to + // fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded + // upward (sticks at 1 across the first two periods) and only clears + // in the final payment. + // + // The test pays one period at a time across three LoanPay + // transactions and verifies the loan completes (paymentRemaining=0) + // with totals matching the loan's economics (1 principal + 2 interest). + void + testIntegerScalePrincipalSticks(FeatureBitset features) + { + // Without fixCleanup3_2_0, this behavior will abort the server, so + // don't run without it. + if (!features[fixCleanup3_2_0]) + return; + + testcase("edge: integer MPT principal stuck mid-loan completes via final"); + + using namespace jtx; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(100'000), issuer, lender, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer}); + PrettyAsset const asset{mptt.issuanceID()}; + + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + + env(pay(issuer, lender, asset(10'000))); + env(pay(issuer, borrower, asset(10'000))); + env.close(); + + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + env(vaultTx); + env.close(); + + env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); + env(loan_broker::set(lender, vaultKeylet.key), + loan_broker::kDebtMaximum(Number{100}), + Fee(env.current()->fees().base * 2)); + env.close(); + + auto const brokerStateBefore = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerStateBefore)) + return; + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + + env(loan::set(borrower, brokerKeylet.key, Number{1}), + Sig(sfCounterpartySignature, lender), + loan::kInterestRate(TenthBips32{50'000}), + loan::kPaymentTotal(3), + loan::kPaymentInterval(31'536'000), + Fee(env.current()->fees().base * 2)); + env.close(); + + auto const borrowerStart = env.balance(borrower, asset).value(); + + // Three separate periodic payments of 1 each. Expected per-period + // evolution at integer MPT scale (TVO = PO + interestDue + + // managementFeeDue): + // start: PO=1, TVO=3, paymentRemaining=3 + // after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks) + // after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks) + // after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears) + std::array const expectedPO{Number{1}, Number{1}, Number{0}}; + std::array const expectedTVO{Number{2}, Number{1}, Number{0}}; + std::array const expectedRemaining{2, 1, 0}; + + for (int i = 0; i < 3; ++i) + { + env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS)); + env.close(); + + auto const sle = env.le(loanKeylet); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]); + BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]); + BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]); + } + + // Borrower paid 3 total regardless of fee split (1 principal + 2 + // interest+fee, matching loan economics). + auto const borrowerEnd = env.balance(borrower, asset).value(); + BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value()); + } + +#if LOAN_TODO + void + testLoanCoverMinimumRoundingExploit(FeatureBitset features) + { + auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) { + testcase << "LoanBrokerCoverClawback drains cover via rounding" + << " principalRequested=" << to_string(principalRequest); + + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(2'000'0000))); + env(trust(borrower, asset(2'000'0000))); + env.close(); + + env(pay(issuer, lender, asset(2'000'0000))); + env.close(); + + BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}}; + BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + auto createTx = env.jt( + set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + kPaymentInterval(600), + kPaymentTotal(1), + kGracePeriod(60)); + env(createTx); + env.close(); + + auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerBefore); + if (!brokerBefore) + return; + + Number const debtOutstanding = brokerBefore->at(sfDebtTotal); + Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); + + BEAST_EXPECT(debtOutstanding > Number{}); + BEAST_EXPECT(coverAvailableBefore > Number{}); + + log << "debt=" << to_string(debtOutstanding) + << " cover_available=" << to_string(coverAvailableBefore); + + env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); + env.close(); + + auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerAfter); + if (!brokerAfter) + return; + + Number const debtAfter = brokerAfter->at(sfDebtTotal); + // the debt has not changed + BEAST_EXPECT(debtAfter == debtOutstanding); + + Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable); + + // since the cover rate min != 0, the cover available should not + // be zero + BEAST_EXPECT(coverAvailableAfter != Number{}); + }; + + // Call the lambda with different principal values + testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units + testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units + testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units + testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units + } +#endif + + // A residual overpayment can reduce the stored principal by one scale-unit + // *less* than computeOverpaymentComponents predicts, firing the + // "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment: + // + // trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding + // + // tryOverpayment re-amortizes the loan at the reduced principal, then + // re-derives the theoretical principal from the new periodic payment via + // (P * paymentFactor) / paymentFactor. That round-trip is not exact in + // Number's 19-digit arithmetic; a positive residual pushes the recomputed + // principal a hair above the exact grid point `oldPrincipal - delta`, and + // the Upward rounding in tryOverpayment then bumps it a full scale-unit + // higher. The principal therefore drops by `delta - 1 unit`, not `delta`. + // + // Concrete case (isolated, at the tryOverpayment level): + // A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10. + // After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of + // 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by + // 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10. + // + // With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact, + // on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the + // lossy (P*factor)/factor round-trip, so the assertion holds and the + // overpayment applies cleanly. The three "principal change agrees" / + // "interest paid agrees" / "principal payment matches" assertions are + // gated behind the same amendment, so without it they are disabled (the + // server does not abort) and the loan keeps the pre-amendment computation. + // + // The test runs the same scenario under both amendment settings and checks + // the stored principal against a ground-truth value derived independently of + // the loan-state computation under test. + void + testBugOverpaymentPrincipalChange() + { + testcase("bug: doOverpayment asserts 'principal change agrees'"); + + using namespace jtx; + using namespace loan; + using namespace xrpl::detail; + + struct Params + { + TenthBips32 interestRate; + TenthBips16 managementFeeRate; + std::uint32_t paymentTotal; + std::uint32_t paymentInterval; + std::int64_t principal; + Number overpayment; + TenthBips32 overpaymentInterestRate; + TenthBips32 overpaymentFeeRate; + std::optional vaultScale; + }; + + struct Result + { + Number principalOutstanding; // stored principal after the LoanPay + Number expectedNewPrincipal; // ground truth, independent of the fix + Number managementFeeChange; // managementFeeOutstanding after - before + Number unit; // one scale-unit at the loan scale + }; + + auto runScenario = [this](FeatureBitset features, Params const& p) -> Result { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + Asset const asset = iouAsset.raw(); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 900'000, + .debtMax = 0, + .managementFeeRate = p.managementFeeRate, + .vaultScale = p.vaultScale}); + + auto const brokerSle = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerSle); + auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + kInterestRate(p.interestRate), + kPaymentTotal(p.paymentTotal), + kPaymentInterval(p.paymentInterval), + kGracePeriod(p.paymentInterval), + kOverpaymentFee(p.overpaymentFeeRate), + kOverpaymentInterestRate(p.overpaymentInterestRate), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // The single LoanPay below makes one regular payment (the overpayment + // is smaller than one period) and leaves the residual as an + // overpayment. + auto const s = getCurrentState(env, broker, loanKeylet); + auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval); + auto const onePeriod = computePaymentComponents( + env.current()->rules(), + asset, + s.loanScale, + s.totalValue, + s.principalOutstanding, + s.managementFeeOutstanding, + s.periodicPayment, + periodicRate, + s.paymentRemaining, + p.managementFeeRate); + + // Ground truth: the stored principal must drop by exactly the regular + // payment's principal portion plus the overpayment's principal + // portion. computeOverpaymentComponents depends only on the + // overpayment amount and rates (not on the loan-state computation + // under test), so it is an independent oracle. Both components are + // computed under the same rules as the env so the payment factor + // matches. + auto const overpaymentComponents = computeOverpaymentComponents( + env.current()->rules(), + asset, + s.loanScale, + p.overpayment, + p.overpaymentInterestRate, + p.overpaymentFeeRate, + p.managementFeeRate); + Number const expectedNewPrincipal = s.principalOutstanding - + onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta; + + Number const managementFeeBefore = s.managementFeeOutstanding; + + STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment}; + env(pay(borrower, loanKeylet.key, payAmount), + Txflags(tfLoanOverpayment), + Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + + return Result{ + .principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0, + .expectedNewPrincipal = expectedNewPrincipal, + .managementFeeChange = + (loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) - + managementFeeBefore, + .unit = Number{1, s.loanScale}}; + }; + + // Scenario 1: the original near-zero-rate principal reproduction + // (loanScale -10, no management fee). 0.049999998 is smaller than one + // period, so it stays a residual overpayment. + Params const principalCase{ + .interestRate = TenthBips32{1}, + .managementFeeRate = TenthBips16{0}, + .paymentTotal = 3, + .paymentInterval = 60, + .principal = 100, + .overpayment = Number{49999998, -9}, + .overpaymentInterestRate = TenthBips32{1000}, + .overpaymentFeeRate = TenthBips32{1000}, + .vaultScale = 1}; + + // With fixCleanup3_2_0 the stored principal lands exactly on the + // ground-truth grid point: it is reduced by exactly the overpayment's + // principal portion. This is the key correctness check: if the principal + // pin were removed (even with the assertions still gated off), the lossy + // (P * factor) / factor round-trip would leave the principal one + // scale-unit high and this would fail. + Result const fixed = runScenario(all_, principalCase); + BEAST_EXPECTS( + fixed.principalOutstanding == fixed.expectedNewPrincipal, + "fixed principal " + to_string(fixed.principalOutstanding) + " != expected " + + to_string(fixed.expectedNewPrincipal)); + + // Without the amendment the loan amortizes with the catastrophically + // cancelling near-zero payment factor, so its schedule (and ground truth) + // differ from the fixed case; the gated assertions keep the server from + // aborting and the overpayment still lands exactly on that schedule. + Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase); + BEAST_EXPECTS( + legacy.principalOutstanding == legacy.expectedNewPrincipal, + "legacy principal " + to_string(legacy.principalOutstanding) + " != expected " + + to_string(legacy.expectedNewPrincipal)); + + // Scenario 2: a normal-rate loan with a 10% management fee. At a normal + // rate the payment factor is identical across the amendment, so toggling + // fixCleanup3_2_0 isolates the fix. This overpayment (found by search) + // lands on a state where both the principal and the management fee differ + // by one scale-unit between the fixed and legacy paths. + Params const feeCase{ + .interestRate = TenthBips32{10000}, + .managementFeeRate = TenthBips16{10000}, + .paymentTotal = 6, + .paymentInterval = 30u * 24 * 60 * 60, + .principal = 1000, + .overpayment = Number{214367363, -10}, + .overpaymentInterestRate = TenthBips32{0}, + .overpaymentFeeRate = TenthBips32{0}, + .vaultScale = std::nullopt}; + + Result const feeFixed = runScenario(all_, feeCase); + Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase); + + // With the fix the principal is the exact reduction; without it the lossy + // (P * factor) / factor round-trip leaves it one scale-unit high. + BEAST_EXPECTS( + feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal, + "fee-case fixed principal " + to_string(feeFixed.principalOutstanding) + + " != expected " + to_string(feeFixed.expectedNewPrincipal)); + BEAST_EXPECTS( + feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit, + "fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) + + " != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit)); + + // Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee + // outstanding drops. + // + // Unlike the principal that is already at the correct precision, the re-amortized + // management fee is tenthBipsOfValue of the new schedule's gross interest, which depends + // on the recomputed periodic payment. So the expected change below is a pinned constant + // captured from a passing run a magic value only because there is nothing simpler to + // compare against. + // + // At the integration level, toggling the amendment also changes the regular payment's + // rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee + // fix. + BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange); + BEAST_EXPECTS( + (feeFixed.managementFeeChange == Number{-8219709543, -10}), + "fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange)); + } + + // An overpayment whose residual amount has more precision than loanScale + // fires the isRounded(asset, overpayment, loanScale) assertion in + // computeOverpaymentComponents (and a downstream "interest paid agrees" + // assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down + // to loanScale before passing it in. The pre-amendment path can't be + // tested here because the assertion fires in Debug builds and aborts + // the test process — see the PR description for context. + void + testBugOverpayUnroundedAmount() + { + testcase("bug: computeOverpaymentComponents isRounded assertion"); + + using namespace jtx; + using namespace loan; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 100'000, + .debtMax = 5000, + .managementFeeRate = TenthBips16{1000}, + .vaultScale = 1}); + + auto const sleBroker = env.le(broker.brokerKeylet()); + if (!BEAST_EXPECT(sleBroker)) + return; + auto const loanSequence = sleBroker->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + using namespace loan; + env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32{10000}), + kPaymentTotal(12), + kPaymentInterval(60), + kGracePeriod(60), + kOverpaymentFee(TenthBips32{1000}), + kOverpaymentInterestRate(TenthBips32{1000}), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This + // has too many digits to round cleanly to loanScale=-10, so the + // overpayment residual fails the isRounded check. + STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}}; + env(pay(borrower, loanKeylet.key, payAmount), Txflags(tfLoanOverpayment), Ter(tesSUCCESS)); + env.close(); + } + + // A near-zero interest rate on a 100 USD loan + // produces total interest of ~6 units at loanScale -9. Numerical error + // in the amortization formula pushes the theoretical principal above + // the theoretical value, producing a negative theoretical interest. + // The payment delta then exceeds the actual outstanding interest, + // violating XRPL_ASSERT_PARTS in computePaymentComponents. + void + testBugInterestDueDeltaCrash() + { + testcase("bug: LoanPay asserts 'interest due delta' on near-zero rate"); + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + env(trust(lender, iouAsset(1'000'000'000))); + env(trust(borrower, iouAsset(1'000'000'000))); + env(pay(issuer, lender, iouAsset(5'000'000))); + env(pay(issuer, borrower, iouAsset(5'000'000))); + env.close(); + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 1'000'000, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{100}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["InterestRate"] = 1; // minimum non-zero rate + createJson["PaymentTotal"] = 3; + createJson["PaymentInterval"] = 600; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + // For principal=100, n=3 the amortization schedule produces a + // periodic payment ≈ 33.33 USD. We pay 35 USD, which is more than + // one period's worth — enough for the LoanPay path to enter + // computePaymentComponents and reach the assertion that fires + // when the bug is present. With the fix, the tx applies cleanly. + env(pay(borrower, keylet.key, iouAsset(35)), Ter(tesSUCCESS)); + env.close(); + } + + void + runAmendmentIndependent() + { + for (auto const flags : {0u, tfLoanOverpayment}) + testYieldTheftRounding(flags); + testBugOverpaymentPrincipalChange(); + testBugOverpayUnroundedAmount(); + testBugInterestDueDeltaCrash(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testDustManipulation(features); + testRoundingAllowsUndercoverage(features); + testIntegerScalePrincipalSticks(features); +#if LOAN_TODO + testLoanCoverMinimumRoundingExploit(features); +#endif + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanRounding, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanSecurity_test.cpp b/src/test/app/lending/LoanSecurity_test.cpp new file mode 100644 index 0000000000..b08d80b51c --- /dev/null +++ b/src/test/app/lending/LoanSecurity_test.cpp @@ -0,0 +1,538 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +class LoanSecurity_test : public LoanTestBase +{ +private: + void + testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features) + { + // --- PoC Summary ---------------------------------------------------- + // Scenario: Borrower makes one periodic payment early (before next due) + // so doPayment sets sfPreviousPaymentDueDate to the (future) + // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one + // interval. Borrower then immediately performs a full-payment + // (tfLoanFullPayment). Why it matters: Full-payment interest accrual + // uses + // delta = now - max(prevPaymentDate, startDate) + // with an unsigned clock representation (uint32). If prevPaymentDate is + // in the future, the subtraction underflows to a very large positive + // number. This inflates roundedFullInterest and total full-close due, + // and LoanPay applies the inflated valueChange to the vault + // (sfAssetsTotal), increasing NAV. + // -------------------------------------------------------------------- + testcase("PoC: Unsigned-underflow full-pay accrual after early periodic"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env{*this, features}; + + Account const lender{"poc_lender4"}; + Account const borrower{"poc_borrower4"}; + env.fund(XRP(3'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{}; + auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); + + // Create a 3-payment loan so full-payment path is enabled after 1 + // periodic payment. + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest = asset(1000).value(); + auto const originationFee = asset(0).value(); + auto const serviceFee = asset(1).value(); + auto const serviceFeePA = asset(1); + auto const lateFee = asset(0).value(); + auto const closeFee = asset(0).value(); + auto const interest = percentageToTenthBips(12); + auto const lateInterest = percentageToTenthBips(12) / 10; + auto const closeInterest = percentageToTenthBips(12) / 10; + auto const overpaymentInterest = percentageToTenthBips(12) / 10; + auto const total = 3u; + auto const interval = 600u; + auto const grace = 60u; + + auto createJtx = env.jt( + set(borrower, broker.brokerID, principalRequest, 0), + Sig(sfCounterpartySignature, lender), + kLoanOriginationFee(originationFee), + kLoanServiceFee(serviceFee), + kLatePaymentFee(lateFee), + kClosePaymentFee(closeFee), + kOverpaymentFee(percentageToTenthBips(5) / 10), + kInterestRate(interest), + kLateInterestRate(lateInterest), + kCloseInterestRate(closeInterest), + kOverpaymentInterestRate(overpaymentInterest), + kPaymentTotal(total), + kPaymentInterval(interval), + kGracePeriod(grace), + Fee(loanSetFee)); + + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle); + auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(createJtx); + env.close(); + + // Compute a regular periodic due and pay it early (before next due). + auto state = getCurrentState(env, broker, loanKeylet); + Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); + auto const components = xrpl::detail::computePaymentComponents( + env.current()->rules(), + asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + brokerParams.managementFeeRate); + STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()}; + // now < nextDue immediately after creation, so this is an early pay. + env(pay(borrower, loanKeylet.key, regularDue)); + env.close(); + + // Immediately attempt a full payoff. Compute the exact full-payment + // due to ensure the tx applies. + auto after = getCurrentState(env, broker, loanKeylet); + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle2); + + auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; + auto const closeInterestRate = + loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{}; + auto const managementFeeRate = + brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{}; + + Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval); + // Accrued + prepayment-penalty interest based on current periodic + // schedule + auto const fullPaymentInterest = computeFullPaymentInterest( + xrpl::detail::loanPrincipalFromPeriodicPayment( + env.current()->rules(), + after.periodicPayment, + periodicRate2, + after.paymentRemaining), + periodicRate2, + env.current()->parentCloseTime(), + after.paymentInterval, + after.previousPaymentDate, + static_cast(after.startDate.time_since_epoch().count()), + closeInterestRate); + + // Round to asset scale and split interest/fee parts + auto const roundedInterest = + roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); + Number const roundedFullMgmtFee = + computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale); + Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; + + // Show both signed and unsigned deltas to highlight the underflow. + auto const nowSecs = + static_cast(env.current()->parentCloseTime().time_since_epoch().count()); + auto const startSecs = + static_cast(after.startDate.time_since_epoch().count()); + auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs); + auto const signedDelta = + static_cast(nowSecs) - static_cast(lastPaymentDate); + auto const unsignedDelta = static_cast(nowSecs - lastPaymentDate); + log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs + << " now=" << nowSecs << " signedDelta=" << signedDelta + << " unsignedDelta=" << unsignedDelta << std::endl; + + // Reference (clamped) computation: emulate a non-negative accrual + // window by clamping prevPaymentDate to 'now' for the full-pay path. + auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); + auto const fullPaymentInterestClamped = computeFullPaymentInterest( + xrpl::detail::loanPrincipalFromPeriodicPayment( + env.current()->rules(), + after.periodicPayment, + periodicRate2, + after.paymentRemaining), + periodicRate2, + env.current()->parentCloseTime(), + after.paymentInterval, + prevClamped, + startSecs, + closeInterestRate); + auto const roundedInterestClamped = + roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale); + Number const roundedFullMgmtFeeClamped = computeManagementFee( + asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale); + Number const roundedFullInterestClamped = + roundedInterestClamped - roundedFullMgmtFeeClamped; + STAmount const fullDueClamped{ + asset, + after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped + + closePaymentFee}; + + // Collect vault NAV before closing payment + auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; + auto const vaultKey2 = keylet::vault(vaultId2); + auto const vaultBefore = env.le(vaultKey2); + BEAST_EXPECT(vaultBefore); + Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; + + STAmount const fullDue{ + asset, + after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee + + closePaymentFee}; + + log << "PoC payoff: principalOutstanding=" << after.principalOutstanding + << " roundedFullInterest=" << roundedFullInterest + << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee + << " fullDue=" << to_string(fullDue.getJson()) << std::endl; + log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped + << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped + << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl; + + env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment)); + env.close(); + + // Sanity: underflow present (unsigned delta very large relative to + // interval) + BEAST_EXPECT(unsignedDelta > after.paymentInterval); + + // Compare vault NAV before/after the full close + auto const vaultAfter = env.le(vaultKey2); + BEAST_EXPECT(vaultAfter); + if (vaultAfter) + { + auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore + << " assetsTotalAfter=" << assetsTotalAfter + << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl; + + // Regression check: the underflowed window must be clamped so the + // payoff matches the non-underflow reference, i.e. no overcharge. + BEAST_EXPECT(fullDue == fullDueClamped); + if (fullDue != fullDueClamped) + log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; + } + + // Loan should be paid off + auto const finalLoan = env.le(loanKeylet); + BEAST_EXPECT(finalLoan); + if (finalLoan) + { + BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); + } + } + + void + testRIPD3831(FeatureBitset features) + { + using namespace jtx; + + testcase("RIPD-3831"); + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + // .managementFeeRate = TenthBips16{5919}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200'000, -6}, + .lateFee = Number{200, -6}, + .interest = TenthBips32{50'000}, + .payTotal = 10, + .payInterval = 150}; + + auto const assetType = AssetType::XRP; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + + using tp = NetClock::time_point; + using d = NetClock::duration; + + auto state = getCurrentState(env, broker, loanKeylet); + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); + } + + topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); + + using namespace jtx::loan; + + auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); + + { + auto const submitParam = to_string(jv); + auto const jr = env.rpc("submit", borrower.name(), submitParam); + + BEAST_EXPECT(jr.isMember(jss::result)); + } + + env.close(); + + // Make sure the system keeps responding + env(noop(borrower)); + env.close(); + env(noop(issuer)); + env.close(); + env(noop(lender)); + env.close(); + } + + void + testRIPD3459(FeatureBitset features) + { + testcase("RIPD-3459 - LoanBroker incorrect debt total"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 200'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{500}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100'000, -4}, + .interest = TenthBips32{100'000}, + .payTotal = 10}; + + auto const assetType = AssetType::MPT; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); + } + } + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero); + } + } + } + + void + testRIPD3901() + { + testcase("Crash with tfLoanOverpayment"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + auto const txFee = Fee(XRP(100)); + + Env env(*this); + Vault const vault(env); + + env.fund(XRP(10'000), lender, issuer, borrower, depositor); + env.close(); + + auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()}); + env(tx, txFee); + env.close(); + + env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}), + txFee); + env.close(); + + auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); + + env(loan_broker::set(lender, vaultKeyLet.key), txFee); + env.close(); + + STAmount const debtMaximumRequest = XRPAmount(200'000); + + env(set(borrower, brokerKeyLet.key, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(50'000)), + kPaymentTotal(2), + kPaymentInterval(150), + Txflags(tfLoanOverpayment), + txFee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); + + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), + Txflags(tfLoanOverpayment), + txFee); + env.close(); + } + } + + void + testRIPD3902(FeatureBitset features) + { + testcase("RIPD-3902 - 1 IOU loan payments"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{1, 0}, + .interest = TenthBips32{100'000}, + .payTotal = 5, + .payInterval = 150, + .gracePd = 60}; + + auto const assetType = AssetType::IOU; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + } + + void + runAmendmentIndependent() + { + testRIPD3901(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); + testRIPD3831(features); + testRIPD3459(features); + testRIPD3902(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanSecurity, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanSet_test.cpp b/src/test/app/lending/LoanSet_test.cpp new file mode 100644 index 0000000000..85528ee9a0 --- /dev/null +++ b/src/test/app/lending/LoanSet_test.cpp @@ -0,0 +1,607 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanSet_test : public LoanTestBase +{ +private: + void + testLoanSet(FeatureBitset features) + { + using namespace jtx; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + struct CaseArgs + { + bool requireAuth = false; + bool authorizeBorrower = false; + int initialXRP = 1'000'000; + }; + + auto const testCase = [&, this]( + std::function mptTest, + std::function iouTest, + CaseArgs args = {}) { + Env env(*this, features); + env.fund(XRP(args.initialXRP), issuer, lender, borrower); + env.close(); + if (args.requireAuth) + { + env(fset(issuer, asfRequireAuth)); + env.close(); + } + + // We need two different asset types, MPT and IOU. Prepare MPT + // first + MPTTester mptt{env, issuer, kMptInitNoFund}; + + auto const kNone = LedgerSpecificFlags(0); + mptt.create( + {.flags = tfMPTCanTransfer | tfMPTCanLock | + (args.requireAuth ? tfMPTRequireAuth : kNone)}); + env.close(); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env.close(); + if (args.requireAuth) + { + mptt.authorize({.account = issuer, .holder = lender}); + if (args.authorizeBorrower) + mptt.authorize({.account = issuer, .holder = borrower}); + env.close(); + } + + env(pay(issuer, lender, mptAsset(10'000'000))); + env.close(); + + // Prepare IOU + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + if (args.requireAuth) + { + env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); + env(pay(issuer, lender, iouAsset(10'000'000))); + if (args.authorizeBorrower) + { + env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); + env(pay(issuer, borrower, iouAsset(10'000))); + } + } + else + { + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(10'000))); + } + env.close(); + + // Create vaults and loan brokers + std::array const assets{mptAsset, iouAsset}; + std::vector brokers; + brokers.reserve(assets.size()); + for (auto const& asset : assets) + { + brokers.emplace_back(createVaultAndBroker(env, asset, lender)); + } + + if (mptTest) + mptTest(env, brokers[0], mptt); + if (iouTest) + iouTest(env, brokers[1]); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + + testcase("MPT issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(issuer), + Sig(sfCounterpartySignature, issuer), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + + testcase("IOU issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(issuer), + Sig(sfCounterpartySignature, issuer), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + + testcase("MPT unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + + testcase("IOU unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + }, + CaseArgs{.requireAuth = true}); + + auto const [acctReserve, incReserve] = [this]() -> std::pair { + Env const env{*this, testableAmendments()}; + return { + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), + env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; + }(); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, borrower has " + "no reserve"); + mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize}); + env.close(); + + auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); + + // Burn some XRP + env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 != nullptr); + }, + {}, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, borrower has " + "no reserve"); + // Remove trust line from borrower to issuer + env.trust(broker.asset(0), borrower); + env.close(); + + env(pay(borrower, issuer, broker.asset(10'000))); + env.close(); + auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 == nullptr); + + // Burn some XRP + env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 != nullptr); + }, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender has " + "no reserve"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Burn some XRP + env(noop(lender), Fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 != nullptr); + }, + {}, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender has no " + "reserve"); + // Remove trust line from lender to issuer + env.trust(broker.asset(0), lender); + env.close(); + + auto const trustline = keylet::trustLine(lender, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 != nullptr); + + env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value())))); + env.close(); + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 == nullptr); + + // Burn some XRP + env(noop(lender), Fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine3 = env.le(trustline); + BEAST_EXPECT(sleLine3 != nullptr); + }, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, unauthorized lender"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Cannot create loan, lender not authorized to receive fee + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + env.close(); + + // Cannot create loan, even without an origination fee + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + env.close(); + + // No MPToken for lender - no authorization and no payment + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 == nullptr); + }, + {}, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + jtx::Account const alice{"alice"}; + jtx::Account const bella{"bella"}; + auto const msigSetup = [&](Env& env, Account const& account) { + json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); + env(tx1); + env.close(); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit; + env(tx); + env.close(); + + testcase("Vault at maximum value"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + kInterestRate(TenthBips32(10'000)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter(tecLIMIT_EXCEEDED)); + }, + nullptr); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = + BrokerParameters::defaults().vaultDeposit + broker.asset(1).number(); + env(tx); + env.close(); + + testcase("Vault maximum value exceeded"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + kInterestRate(TenthBips32(100'000)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + kPaymentTotal(2), + kPaymentInterval(3600 * 24), + Ter(tecLIMIT_EXCEEDED)); + }, + nullptr); + } + +public: + void + run() override + { + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + testLoanSet(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanSet, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h new file mode 100644 index 0000000000..35aa1e26cb --- /dev/null +++ b/src/test/app/lending/LoanTestBase.h @@ -0,0 +1,2949 @@ +#pragma once + +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanTestBase : public beast::unit_test::Suite +{ +protected: + // Ensure that all the features needed for Lending Protocol are included, + // even if they are set to unsupported. + // + // featureLendingProtocolV1_1 is excluded from the default set: it changes + // Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and + // most of this file's tests assert whole-life-specific expected values + // for those fields. Tests that specifically exercise the amendment opt + // it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`). + FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1}; + std::string const iouCurrency_{"IOU"}; + + struct BrokerParameters + { + Number vaultDeposit = 1'000'000; + Number debtMax = 25'000; + TenthBips32 coverRateMin = percentageToTenthBips(10); + int coverDeposit = 1000; + TenthBips16 managementFeeRate{100}; + TenthBips32 coverRateLiquidation = percentageToTenthBips(25); + std::string data = {}; // NOLINT(readability-redundant-member-init) + std::uint32_t flags = 0; + // If set, the vault is created with this sfScale value. Useful for + // tests that need finer loanScale to exercise rounding edge cases. + std::optional vaultScale = + std::nullopt; // NOLINT(readability-redundant-member-init) + + [[nodiscard]] Number + maxCoveredLoanValue(Number const& currentDebt) const + { + NumberRoundModeGuard const mg(Number::RoundingMode::Downward); + auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value(); + + return debtLimit - currentDebt; + } + + static BrokerParameters const& + defaults() + { + static BrokerParameters const kResult{}; + return kResult; + } + + // TODO: create an operator() which returns a transaction similar to + // LoanParameters + }; + + struct BrokerInfo + { + jtx::PrettyAsset asset; + uint256 brokerID; + uint256 vaultID; + BrokerParameters params; + BrokerInfo( + jtx::PrettyAsset const& asset, + Keylet const& brokerKeylet, + Keylet const& vaultKeylet, + BrokerParameters p) + : asset(asset) + , brokerID(brokerKeylet.key) + , vaultID(vaultKeylet.key) + , params(std::move(p)) + { + } + + [[nodiscard]] Keylet + brokerKeylet() const + { + return keylet::loanBroker(brokerID); + } + [[nodiscard]] Keylet + vaultKeylet() const + { + return keylet::vault(vaultID); + } + + [[nodiscard]] int + vaultScale(jtx::Env const& env) const + { + using namespace jtx; + + auto const vaultSle = env.le(keylet::vault(vaultID)); + return getAssetsTotalScale(vaultSle); + } + }; + + struct LoanParameters + { + // The account submitting the transaction. May be borrower or broker. + jtx::Account account; + // The counterparty. Should be the other of borrower or broker. + jtx::Account counter; + // Whether the counterparty is specified in the `counterparty` field, or + // only signs. + bool counterpartyExplicit = true; + Number principalRequest; + // NOLINTBEGIN(readability-redundant-member-init) + std::optional setFee = std::nullopt; + std::optional originationFee = std::nullopt; + std::optional serviceFee = std::nullopt; + std::optional lateFee = std::nullopt; + std::optional closeFee = std::nullopt; + std::optional overFee = std::nullopt; + std::optional interest = std::nullopt; + std::optional lateInterest = std::nullopt; + std::optional closeInterest = std::nullopt; + std::optional overpaymentInterest = std::nullopt; + std::optional payTotal = std::nullopt; + std::optional payInterval = std::nullopt; + std::optional gracePd = std::nullopt; + std::optional flags = std::nullopt; + // NOLINTEND(readability-redundant-member-init) + + template + jtx::JTx + operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const + { + using namespace jtx; + using namespace jtx::loan; + + JTx jt{loan::set( + account, + broker.brokerID, + broker.asset(principalRequest).number(), + flags.value_or(0))}; + + Sig(sfCounterpartySignature, counter)(env, jt); + + Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); + + if (counterpartyExplicit) + kCounterparty(counter)(env, jt); + if (originationFee) + kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt); + if (serviceFee) + kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt); + if (lateFee) + kLatePaymentFee(broker.asset(*lateFee).number())(env, jt); + if (closeFee) + kClosePaymentFee(broker.asset(*closeFee).number())(env, jt); + if (overFee) + kOverpaymentFee (*overFee)(env, jt); + if (interest) + kInterestRate (*interest)(env, jt); + if (lateInterest) + kLateInterestRate (*lateInterest)(env, jt); + if (closeInterest) + kCloseInterestRate (*closeInterest)(env, jt); + if (overpaymentInterest) + kOverpaymentInterestRate (*overpaymentInterest)(env, jt); + if (payTotal) + kPaymentTotal (*payTotal)(env, jt); + if (payInterval) + kPaymentInterval (*payInterval)(env, jt); + if (gracePd) + kGracePeriod (*gracePd)(env, jt); + + return env.jt(jt, fN...); + } + }; + + struct PaymentParameters + { + Number overpaymentFactor = Number{1}; + std::optional overpaymentExtra = std::nullopt; + std::uint32_t flags = 0; + bool showStepBalances = false; + bool validateBalances = true; + + static PaymentParameters const& + defaults() + { + static PaymentParameters const kResult{}; + return kResult; + } + }; + + struct LoanState + { + std::uint32_t previousPaymentDate = 0; + NetClock::time_point startDate; + std::uint32_t nextPaymentDate = 0; + std::uint32_t paymentRemaining = 0; + std::int32_t const loanScale = 0; + Number totalValue = 0; + Number principalOutstanding = 0; + Number managementFeeOutstanding = 0; + Number periodicPayment = 0; + std::uint32_t flags = 0; + std::uint32_t const paymentInterval = 0; + TenthBips32 const interestRate{}; + }; + + /** + * Helper class to compare the expected state of a loan and loan broker + * against the data in the ledger. + */ + struct VerifyLoanStatus + { + public: + jtx::Env const& env; + BrokerInfo const& broker; + jtx::Account const& pseudoAccount; + Keylet const& loanKeylet; + + VerifyLoanStatus( + jtx::Env const& env, + BrokerInfo const& broker, + jtx::Account const& pseudo, + Keylet const& keylet) + : env(env), broker(broker), pseudoAccount(pseudo), loanKeylet(keylet) + { + } + + /** + * Checks the expected broker state against the ledger + */ + void + checkBroker( + Number const& principalOutstanding, + Number const& interestOwed, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + std::uint32_t ownerCount) const + { + using namespace jtx; + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const brokerDebt = brokerSle->at(sfDebtTotal); + + if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + auto const expectedDebt = + env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : principalOutstanding + interestOwed; + env.test.BEAST_EXPECT(brokerDebt == expectedDebt); + env.test.BEAST_EXPECT( + env.balance(pseudoAccount, broker.asset).number() == + brokerSle->at(sfCoverAvailable)); + env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); + + Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)}; + env.test.BEAST_EXPECT( + vaultSle->at(sfAssetsAvailable) == + env.balance(vaultPseudo, broker.asset).number()); + if (ownerCount == 0) + { + // The Vault must be perfectly balanced if there + // are no loans outstanding + auto const total = vaultSle->at(sfAssetsTotal); + auto const available = vaultSle->at(sfAssetsAvailable); + env.test.BEAST_EXPECT(total == available); + env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + + void + checkPayment( + std::int32_t loanScale, + jtx::Account const& account, + jtx::PrettyAmount const& balanceBefore, + STAmount const& expectedPayment, + jtx::PrettyAmount const& adjustment) const + { + auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent()); + + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)}; + { + auto const difference = roundToScale( + env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), + borrowerScale); + env.test.expect( + roundToScale(difference, loanScale) >= beast::kZero, + "Balance before: " + to_string(balanceBefore.value()) + + ", expected change: " + to_string(balanceChangeAmount) + + ", difference (balance after - expected): " + to_string(difference), + __FILE__, + __LINE__); + } + } + + /** + * Checks both the loan and broker expect states against the ledger + */ + void + operator()( + std::uint32_t previousPaymentDate, + std::uint32_t nextPaymentDate, + std::uint32_t paymentRemaining, + Number const& loanScale, + Number const& totalValue, + Number const& principalOutstanding, + Number const& managementFeeOutstanding, + Number const& periodicPayment, + std::uint32_t flags) const + { + using namespace jtx; + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate); + env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining); + env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate); + env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); + env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue); + env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding); + env.test.BEAST_EXPECT( + loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding); + env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment); + env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); + + auto const ls = constructLoanState(loan); + + auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; + auto const paymentInterval = loan->at(sfPaymentInterval); + checkBroker( + principalOutstanding, + ls.interestDue, + interestRate, + paymentInterval, + paymentRemaining, + 1); + + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u)) + { + env.test.BEAST_EXPECT( + vaultSle->at(sfLossUnrealized) == + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : totalValue - managementFeeOutstanding)); + } + else + { + env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + } + + /** + * Checks both the loan and broker expect states against the ledger + */ + void + operator()(LoanState const& state) const + { + operator()( + state.previousPaymentDate, + state.nextPaymentDate, + state.paymentRemaining, + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + state.flags); + }; + }; + + BrokerInfo + createVaultAndBroker( + jtx::Env& env, + jtx::PrettyAsset const& asset, + jtx::Account const& lender, + BrokerParameters const& params = BrokerParameters::defaults()) + { + using namespace jtx; + + Vault const vault{env}; + + auto const deposit = asset(params.vaultDeposit); + auto const debtMaximumValue = asset(params.debtMax).value(); + auto const coverDepositValue = asset(params.coverDeposit).value(); + + auto const coverRateMinValue = params.coverRateMin; + + auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + if (params.vaultScale) + tx[sfScale] = *params.vaultScale; + env(tx); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); + env.close(); + if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault)) + { + BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); + } + + auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); + + using namespace loan_broker; + env(set(lender, vaultKeylet.key, params.flags), + kData(params.data), + kManagementFeeRate(params.managementFeeRate), + kDebtMaximum(debtMaximumValue), + kCoverRateMinimum(coverRateMinValue), + kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation))); + + if (coverDepositValue != beast::kZero) + env(coverDeposit(lender, keylet.key, coverDepositValue)); + + env.close(); + + return {asset, keylet, vaultKeylet, params}; + } + + /** + * Get the state without checking anything + */ + LoanState + getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet) + { + using d = NetClock::duration; + using tp = NetClock::time_point; + + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + return LoanState{ + .previousPaymentDate = loan->at(sfPreviousPaymentDueDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .loanScale = loan->at(sfLoanScale), + .totalValue = loan->at(sfTotalValueOutstanding), + .principalOutstanding = loan->at(sfPrincipalOutstanding), + .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding), + .periodicPayment = loan->at(sfPeriodicPayment), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + .interestRate = TenthBips32{loan->at(sfInterestRate)}, + }; + } + return LoanState{}; + } + + /** + * Get the state and check the values against the parameters used in + * `lifecycle` + */ + LoanState + getCurrentState( + jtx::Env const& env, + BrokerInfo const& broker, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + auto const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.loanScale >= + (broker.asset.integral() + ? 0 + : std::max(broker.vaultScale(env), state.principalOutstanding.exponent()))); + BEAST_EXPECT(state.paymentInterval == 600); + { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + BEAST_EXPECT( + state.totalValue == + roundToAsset( + broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale)); + } + BEAST_EXPECT( + state.managementFeeOutstanding == + computeManagementFee( + broker.asset, + state.totalValue - state.principalOutstanding, + broker.params.managementFeeRate, + state.loanScale)); + + verifyLoanStatus(state); + + return state; + } + + bool + canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state) + { + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + BEAST_EXPECT(vaultSle)) + { + // log << vaultSle->getJson() << std::endl; + auto const assetsUnavailable = + vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); + auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? state.principalOutstanding + : state.totalValue - state.managementFeeOutstanding); + + if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) + { + return false; + } + } + } + return true; + } + + enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; + + // Specify the accounts as params to allow other accounts to be used + jtx::PrettyAsset + createAsset( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + switch (assetType) + { + case AssetType::XRP: + // TODO: remove the factor, and set up loans in drops + return PrettyAsset{xrpIssue(), 1'000'000}; + + case AssetType::IOU: { + PrettyAsset const asset{issuer[iouCurrency_]}; + + auto const limit = + asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit)); + if (lender != issuer) + env(trust(lender, limit)); + if (borrower != issuer) + env(trust(borrower, limit)); + + return asset; + } + + case AssetType::MPT: { + // Enough to cover initial fees + if (!env.le(keylet::account(issuer))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); + if (!env.le(keylet::account(lender))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); + if (!env.le(keylet::account(borrower))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + // Scale the MPT asset so interest is interesting + PrettyAsset const asset{mptt.issuanceID(), 10'000}; + // Need to do the authorization here because mptt isn't + // accessible outside + if (lender != issuer) + mptt.authorize({.account = lender}); + if (borrower != issuer) + mptt.authorize({.account = borrower}); + + env.close(); + + return asset; + } + + default: + throw std::runtime_error("Unknown asset type"); + } + } + + // Predicts the keylet of the next loan `broker` will originate, before + // that loan exists, by reading the broker's current LoanSequence. + Keylet + nextLoanKeylet(jtx::Env const& env, BrokerInfo const& broker) + { + auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerStateBefore)) + return keylet::loan(broker.brokerID, 0); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + } + + // Funds issuer/lender/borrower with XRP, creates an IOU asset issued by + // `issuer`, establishes trustlines for lender and borrower, and pays + // them starting balances. This is the exact setup shared by several of + // the fuzzer-derived regression tests below. + jtx::PrettyAsset + createFundedIouAsset( + jtx::Env& env, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + Number const& lenderPay = 100'000'000, + Number const& borrowerPay = 1'000'000) + { + using namespace jtx; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency_]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(lenderPay)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(borrowerPay)); + env(payIssuerTx); + env.close(); + + return iouAsset; + } + + // Funds issuer/lender/borrower with XRP, sets DefaultRipple on the + // issuer, creates a "USD" IOU asset with a large trust limit, and pays + // lender/borrower starting balances. Shared setup for several + // overpayment/rounding regression tests below. + static jtx::PrettyAsset + createFundedRippleIouAsset( + jtx::Env& env, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + Number const& lenderPay = 1'000'000, + Number const& borrowerPay = 1'000'000) + { + using namespace jtx; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; + env(trust(lender, iouLimit)); + env(trust(borrower, iouLimit)); + env(pay(issuer, lender, iouAsset(lenderPay))); + env(pay(issuer, borrower, iouAsset(borrowerPay))); + env.close(); + + return iouAsset; + } + + // Returns the broker's pseudo-account, or `fallback` if the broker's + // ledger entry cannot be read. + jtx::Account + brokerPseudoAccount(jtx::Env const& env, BrokerInfo const& broker, jtx::Account const& fallback) + { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return fallback; + auto const brokerPseudo = brokerSle->at(sfAccount); + return jtx::Account("Broker pseudo-account", brokerPseudo); + } + + void + describeLoan( + jtx::Env& env, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + AssetType assetType, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); + auto const principal = asset(loanParams.principalRequest).number(); + auto const interest = loanParams.interest.value_or(TenthBips32{}); + auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval); + auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal); + auto const feeRate = brokerParams.managementFeeRate; + auto const props = computeLoanProperties( + env.current()->rules(), + asset, + principal, + interest, + interval, + total, + feeRate, + asset(brokerParams.vaultDeposit).number().exponent()); + log << "Loan properties:\n" + << "\tPrincipal: " << principal << std::endl + << "\tInterest rate: " << interest << std::endl + << "\tPayment interval: " << interval << std::endl + << "\tManagement Fee Rate: " << feeRate << std::endl + << "\tTotal Payments: " << total << std::endl + << "\tPeriodic Payment: " << props.periodicPayment << std::endl + << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl + << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl + << "\tLoan Scale: " << props.loanScale << std::endl + << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl; + + // checkGuards returns a TER, so success is 0 + BEAST_EXPECT(!checkLoanGuards( + asset, + asset(loanParams.principalRequest).number(), + loanParams.interest.value_or(TenthBips32{}) != beast::kZero, + loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal), + props, + env.journal)); + } + + std::optional> + createLoan( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + // Enough to cover initial fees + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); + if (lender != issuer) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); + if (borrower != issuer && borrower != lender) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); + + describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); + + // Make the asset + auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); + + env.close(); + if (asset.native() || lender != issuer) + { + env( + pay((asset.native() ? env.master : issuer), + lender, + asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); + } + // Fund the borrower later once we know the total loan + // size + + BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams); + + auto const pseudoAcctOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + if (!pseudoAcctOpt) + return std::nullopt; + Account const& pseudoAcct = *pseudoAcctOpt; + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the + // _LOAN_BROKER_ object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + }(); + if (!loanKeyletOpt) + return std::nullopt; + Keylet const& loanKeylet = *loanKeyletOpt; + + env(loanParams(env, broker)); + + env.close(); + + return std::make_tuple(broker, loanKeylet, pseudoAcct); + } + + static void + topUpBorrower( + jtx::Env& env, + BrokerInfo const& broker, + jtx::Account const& issuer, + jtx::Account const& borrower, + LoanState const& state, + std::optional const& servFee) + { + using namespace jtx; + + STAmount const serviceFee = broker.asset(servFee.value_or(0)); + + // Ensure the borrower has enough funds to make the payments + // (including tx fees, if necessary) + auto const borrowerBalance = env.balance(borrower, broker.asset); + + auto const baseFee = env.current()->fees().base; + + // Add extra for transaction fees and reserves, if appropriate, or a + // tiny amount for the extra paid in each transaction + auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + accountReserve(*env.current(), borrower.id(), env.journal)) + : broker.asset(15).number()); + + auto const shortage = totalNeeded - borrowerBalance.number(); + + if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower)) + { + env( + pay((broker.asset.native() ? env.master : issuer), + borrower, + STAmount{broker.asset, shortage})); + } + } + + void + makeLoanPayments( + jtx::Env& env, + BrokerInfo const& broker, + LoanParameters const& loanParams, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + PaymentParameters const& paymentParams = PaymentParameters::defaults()) + { + // Make all the individual payments + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + + bool const showStepBalances = paymentParams.showStepBalances; + + auto const currencyLabel = getCurrencyLabel(broker.asset); + + auto const baseFee = env.current()->fees().base; + + env.close(); + auto state = getCurrentState(env, broker, loanKeylet); + + verifyLoanStatus(state); + + STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0)); + + topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; + + if (!showStepBalances) + { + log << currencyLabel << " Payment components: " + << "Payments remaining, " + << "rawInterest, rawPrincipal, " + "rawMFee, " + << "trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special" + << std::endl; + } + + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward); + + auto currentRoundedState = constructLoanState( + state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); + { + auto const raw = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + if (showStepBalances) + { + log << currencyLabel << " Starting loan balances: " + << "\n\tTotal value: " << currentRoundedState.valueOutstanding + << "\n\tPrincipal: " << currentRoundedState.principalOutstanding + << "\n\tInterest: " << currentRoundedState.interestDue + << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue + << "\n\tPayments remaining " << state.paymentRemaining << std::endl; + } + else + { + log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", " + << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", " + << currentRoundedState.principalOutstanding << ", " + << currentRoundedState.interestDue << ", " + << currentRoundedState.managementFeeDue << std::endl; + } + } + + // Try to pay a little extra to show that it's _not_ + // taken + auto const extraAmount = paymentParams.overpaymentExtra + ? broker.asset(*paymentParams.overpaymentExtra).value() + : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20}); + + STAmount const transactionAmount = + STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount; + + auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number(); + auto const initialState = state; + xrpl::detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + Number totalFeesPaid = 0; + std::size_t totalPaymentsMade = 0; + + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + auto validateBorrowerBalance = [&]() { + if (borrower == issuer || !paymentParams.validateBalances) + return; + auto const totalSpent = + (totalPaid.trackedValueDelta + totalFeesPaid + + (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero)); + BEAST_EXPECT( + env.balance(borrower, broker.asset).number() == + borrowerInitialBalance - totalSpent); + }; + + auto const defaultRound = broker.asset.integral() ? 3 : 0; + auto truncate = [defaultRound](Number const& n, std::optional places = std::nullopt) { + auto const p = places.value_or(defaultRound); + if (p == 0) + return n; + auto const factor = Number{1, p}; + return (n * factor).truncate() / factor; + }; + while (state.paymentRemaining > 0) + { + validateBorrowerBalance(); + // Compute the expected principal amount + auto const paymentComponents = xrpl::detail::computePaymentComponents( + env.current()->rules(), + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta <= roundedPeriodicPayment || + (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final && + paymentComponents.trackedValueDelta >= roundedPeriodicPayment)); + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + + xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; + BEAST_EXPECT( + deltas.total() == deltas.principal + deltas.interest + deltas.managementFee); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + deltas.total() == state.periodicPayment || + (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14); + + if (!showStepBalances) + { + log << currencyLabel << " Payment components: " << state.paymentRemaining << ", " + + << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee + << ", " << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* { + if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final) + return "final"; + if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra) + return "extra"; + return "none"; + }() << std::endl; + } + + auto const totalDueAmount = + STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee}; + + if (paymentParams.validateBalances) + { + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be dust. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + diff == beast::kZero || + (diff > beast::kZero && + ((broker.asset.integral() && (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::kZero && + paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedPrincipalDelta == state.principalOutstanding); + } + + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags)); + + env.close(d{state.paymentInterval / 2}); + + if (paymentParams.validateBalances) + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + } + + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + { + // No reason for this not to exist + return; + } + auto const current = constructLoanState(loanSle); + auto const errors = nextTrueState - current; + log << currencyLabel << " Loan balances: " + << "\n\tAmount taken: " << paymentComponents.trackedValueDelta + << "\n\tTotal value: " << current.valueOutstanding + << " (true: " << truncate(nextTrueState.valueOutstanding) + << ", error: " << truncate(errors.total()) + << ")\n\tPrincipal: " << current.principalOutstanding + << " (true: " << truncate(nextTrueState.principalOutstanding) + << ", error: " << truncate(errors.principal) + << ")\n\tInterest: " << current.interestDue + << " (true: " << truncate(nextTrueState.interestDue) + << ", error: " << truncate(errors.interest) + << ")\n\tMgmt fee: " << current.managementFeeDue + << " (true: " << truncate(nextTrueState.managementFeeDue) + << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " + << loanSle->at(sfPaymentRemaining) << std::endl; + + currentRoundedState = current; + } + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) + { + state.paymentRemaining = 0; + state.nextPaymentDate = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + if (paymentParams.validateBalances) + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + totalFeesPaid += serviceFee; + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + validateBorrowerBalance(); + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + auto const initialInterestDue = initialState.totalValue - + (initialState.principalOutstanding + initialState.managementFeeOutstanding); + if (paymentParams.validateBalances) + { + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT(totalInterestPaid == initialInterestDue); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + } + + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + { + // No reason for this not to exist + return; + } + log << currencyLabel << " Total amounts paid: " + << "\n\tTotal value: " << totalPaid.trackedValueDelta + << " (initial: " << truncate(initialState.totalValue) + << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta) + << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta + << " (initial: " << truncate(initialState.principalOutstanding) << ", error: " + << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta) + << ")\n\tInterest: " << totalInterestPaid + << " (initial: " << truncate(initialInterestDue) + << ", error: " << truncate(initialInterestDue - totalInterestPaid) + << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta + << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: " + << truncate( + initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta) + << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl; + } + } + + void + runLoan( + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + FeatureBitset features) + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + Env env(*this, features); + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + } + + /** + * Runs through the complete lifecycle of a loan + * + * 1. Create a loan. + * 2. Test a bunch of transaction failure conditions. + * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done + * depends on the callback. e.g. Default, Early payoff, make all the + * normal payments, etc. + * 4. Delete the loan. The loan will alternate between being deleted by the + * lender and the borrower. + */ + void + lifecycle( + std::string const& caseLabel, + char const* label, + jtx::Env& env, + Number const& loanAmount, + int interestExponent, + jtx::Account const& lender, + jtx::Account const& borrower, + jtx::Account const& evan, + BrokerInfo const& broker, + jtx::Account const& pseudoAcct, + std::uint32_t flags, + // The end of life callback is expected to take the loan to 0 payments + // remaining, one way or another + std::function + toEndOfLife) + { + auto const [keylet, loanSequence] = [&]() { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + { + // will be invalid + return std::make_pair(keylet::loan(broker.brokerID), std::uint32_t(0)); + } + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_ + // object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return std::make_pair(keylet::loan(broker.brokerID, loanSequence), loanSequence); + }(); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); + + // No loans yet + verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); + + if (!BEAST_EXPECT(loanSequence != 0)) + return; + + testcase << caseLabel << " " << label; + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + auto applyExponent = [interestExponent, this](TenthBips32 value) mutable { + BEAST_EXPECT(value > TenthBips32(0)); + while (interestExponent > 0) + { + auto const oldValue = value; + value *= 10; + --interestExponent; + BEAST_EXPECT(value / 10 == oldValue); + } + while (interestExponent < 0) + { + auto const oldValue = value; + value /= 10; + ++interestExponent; + BEAST_EXPECT(value * 10 == oldValue); + } + return value; + }; + + auto const borrowerOwnerCount = env.ownerCount(borrower); + + auto const loanSetFee = env.current()->fees().base * 2; + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .counterpartyExplicit = false, + .principalRequest = loanAmount, + .setFee = loanSetFee, + .originationFee = 1, + .serviceFee = 2, + .lateFee = 3, + .closeFee = 4, + .overFee = applyExponent(percentageToTenthBips(5) / 10), + .interest = applyExponent(percentageToTenthBips(12)), + // 2.4% + .lateInterest = applyExponent(percentageToTenthBips(24) / 10), + .closeInterest = applyExponent(percentageToTenthBips(36) / 10), + .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10), + .payTotal = 12, + .payInterval = 600, + .gracePd = 60, + .flags = flags, + }; + Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value(); + auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value(); + auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value(); + auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); + auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); + + auto const borrowerStartbalance = env.balance(borrower, broker.asset); + + auto createJtx = loanParams(env, broker); + // Successfully create a Loan + env(createJtx); + + env.close(); + + auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count(); + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); + } + + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = 2 * env.current()->fees().base; + } + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount - + adjustment.value()); + } + + auto const loanFlags = + createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); + + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // log << "loan after create: " << to_string(loan->getJson()) + // << std::endl; + BEAST_EXPECT( + loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment)); + BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); + BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); + BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); + BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount); + BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); + BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); + BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); + BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); + BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); + BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest); + BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest); + BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest); + BEAST_EXPECT(loan->at(sfStartDate) == startDate); + BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); + BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0); + BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); + BEAST_EXPECT( + loan->at(sfLoanScale) >= + (broker.asset.integral() + ? 0 + : std::max(broker.vaultScale(env), principalRequestAmount.exponent()))); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount); + } + + auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); + + auto const loanProperties = computeLoanProperties( + env.current()->rules(), + broker.asset.raw(), + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + broker.params.managementFeeRate, + state.loanScale); + + verifyLoanStatus( + 0, + startDate + *loanParams.payInterval, + *loanParams.payTotal, + state.loanScale, + loanProperties.loanState.valueOutstanding, + principalRequestAmount, + loanProperties.loanState.managementFeeDue, + loanProperties.periodicPayment, + loanFlags | 0); + + // Manage the loan + // no-op + env(manage(lender, keylet.key, 0)); + { + // no flags + auto jt = manage(lender, keylet.key, 0); + jt.removeMember(sfFlags.getName()); + env(jt); + } + // Only the lender can manage + env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION)); + // unknown flags + env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG)); + // combinations of flags are not allowed + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault), + Ter(temINVALID_FLAG)); + // invalid loan ID + env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY)); + // Loan is unimpaired, can't unimpair it again + env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); + // Loan is unimpaired, it can go into default, but only after it's past + // due + env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON)); + + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, keylet.key, tfLoanImpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); + // Unimpair the loan + env(manage(lender, keylet.key, tfLoanUnimpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); + + auto const nextDueDate = startDate + *loanParams.payInterval; + + env.close(); + + verifyLoanStatus( + 0, + nextDueDate, + *loanParams.payTotal, + loanProperties.loanScale, + loanProperties.loanState.valueOutstanding, + principalRequestAmount, + loanProperties.loanState.managementFeeDue, + loanProperties.periodicPayment, + loanFlags | 0); + + // Can't delete the loan yet. It has payments remaining. + env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS)); + + if (BEAST_EXPECT(toEndOfLife)) + toEndOfLife(keylet, verifyLoanStatus); + env.close(); + + // Verify the loan is at EOL + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); + } + auto const borrowerStartingBalance = env.balance(borrower, broker.asset); + + // Try to delete the loan broker with an active loan + env(loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); + // Ensure the above tx doesn't get ordered after the LoanDelete and + // delete our broker! + env.close(); + + // Test failure cases + env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG)); + env(del(evan, keylet.key), Ter(tecNO_PERMISSION)); + env(del(lender, broker.brokerID), Ter(tecNO_ENTRY)); + + // Delete the loan + // Either the borrower or the lender can delete the loan. Alternate + // between who does it across tests. + static unsigned kDeleteCounter = 0; + auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower; + env(del(deleter, keylet.key)); + env.close(); + + PrettyAmount adjustment = broker.asset(0); + if (deleter == borrower) + { + // Need to account for fees if the loan is in XRP + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + } + + // No loans left + verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartingBalance.value() - adjustment); + BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + } + } + + static std::string + getCurrencyLabel(Asset const& asset) + { + if (asset.native()) + return "XRP"; + if (asset.holds()) + return "IOU"; + if (asset.holds()) + return "MPT"; + return "Unknown"; + } + + /** + * Wrapper to run a series of lifecycle tests for a given asset and loan + * amount + * + * Will be used in the future to vary the loan parameters. For now, it is + * only called once. + * + * Tests a bunch of LoanSet failure conditions before lifecycle. + */ + template + void + testCaseWrapper( + jtx::Env& env, + jtx::MPTTester& mptt, + std::array const& assets, + BrokerInfo const& broker, + Number const& loanAmount, + int interestExponent) + { + using namespace jtx; + using namespace lending; + + auto const& asset = broker.asset.raw(); + auto const currencyLabel = getCurrencyLabel(asset); + auto const caseLabel = [&]() { + std::stringstream ss; + ss << "Lifecycle: " << loanAmount << " " << currencyLabel + << " Scale interest to: " << interestExponent << " "; + return ss.str(); + }(); + testcase << caseLabel; + + using namespace loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + + Number const principalRequest = broker.asset(loanAmount).value(); + Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); + BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); + Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value(); + Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value(); + Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value(); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const pseudoAcct = brokerPseudoAccount(env, broker, lender); + + auto const baseFee = env.current()->fees().base; + + auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); + // Try some failure cases + // flags are checked first + env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + + // field length validation + // sfData: good length, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kData(std::string(kMaxDataPayloadLength, 'X')), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfData: too long + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kData(std::string(kMaxDataPayloadLength + 1, 'Y')), + loanSetFee, + Ter(temINVALID)); + + // field range validation + // sfOverpaymentFee: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentFee(kMaxOverpaymentFee), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfOverpaymentFee: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentFee(kMaxOverpaymentFee + 1), + loanSetFee, + Ter(temINVALID)); + + // sfInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kInterestRate(kMaxInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(kMaxInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + // sfInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfLateInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kLateInterestRate(kMaxLateInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kLateInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfLateInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kLateInterestRate(kMaxLateInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + // sfLateInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kLateInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfCloseInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kCloseInterestRate(kMaxCloseInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kCloseInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfCloseInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kCloseInterestRate(kMaxCloseInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kCloseInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfOverpaymentInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentInterestRate(kMaxOverpaymentInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfOverpaymentInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfPaymentTotal: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentTotal(LoanSet::kMinPaymentTotal), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfPaymentTotal: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentTotal(LoanSet::kMinPaymentTotal - 1), + loanSetFee, + Ter(temINVALID)); + + // sfPaymentInterval: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentInterval(LoanSet::kMinPaymentInterval), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfPaymentInterval: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(LoanSet::kMinPaymentInterval - 1), + loanSetFee, + Ter(temINVALID)); + + // sfGracePeriod: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentInterval(LoanSet::kMinPaymentInterval * 2), + kGracePeriod(LoanSet::kMinPaymentInterval * 2), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfGracePeriod: larger than paymentInterval + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(LoanSet::kMinPaymentInterval * 2), + kGracePeriod(LoanSet::kMinPaymentInterval * 3), + loanSetFee, + Ter(temINVALID)); + + // insufficient fee - single sign + env(set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + Ter(telINSUF_FEE_P)); + // insufficient fee - multisign + env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); + env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5 - 1), + Ter(telINSUF_FEE_P)); + // Bad multisign signatures for borrower (Account) + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(alice, issuer), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5), + Ter(tefBAD_SIGNATURE)); + // Bad multisign signatures for issuer (Counterparty) + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, alice, issuer), + Fee(env.current()->fees().base * 5 - 1), + Ter(tefBAD_SIGNATURE)); + env(signers(lender, kNone)); + env(signers(borrower, kNone)); + // multisign sufficient fee, but no signers set up + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5), + Ter(tefNOT_MULTI_SIGNING)); + // not the broker owner, no counterparty, not signed by broker + // owner + env(set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, evan), + loanSetFee, + Ter(tefBAD_AUTH)); + // not the broker owner, counterparty is borrower + env(set(evan, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + loanSetFee, + Ter(tecNO_PERMISSION)); + // not a LoanBroker object, no counterparty + env(set(lender, badKeylet.key, principalRequest), + Sig(sfCounterpartySignature, evan), + loanSetFee, + Ter(temBAD_SIGNER)); + // not a LoanBroker object, counterparty is valid + env(set(lender, badKeylet.key, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + loanSetFee, + Ter(tecNO_ENTRY)); + // borrower doesn't exist + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(alice), + Sig(sfCounterpartySignature, alice), + loanSetFee, + Ter(terNO_ACCOUNT)); + + // Request more funds than the vault has available + env(set(evan, broker.brokerID, totalVaultRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_FUNDS)); + + // Request more funds than the broker's first-loss capital can + // cover. + env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_FUNDS)); + + // Frozen trust line / locked MPT issuance + // XRP can not be frozen, but run through the loop anyway to test + // the tecLIMIT_EXCEEDED case + { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return; + + auto const vaultPseudo = [&]() { + auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + if (!BEAST_EXPECT(vaultSle)) + { + // This will be wrong, but the test has failed anyway. + return Account{lender}; + } + auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount)); + return vaultPseudo; + }(); + + auto const [freeze, deepfreeze, unfreeze, expectedResult] = + [&]() -> std::tuple< + std::function, + std::function, + std::function, + TER> { + // Freeze / lock the asset + std::function const empty; + if (broker.asset.native()) + { + // XRP can't be frozen + return std::make_tuple(empty, empty, empty, tesSUCCESS); + } + if (broker.asset.holds()) + { + auto freeze = [&](Account const& holder) { + env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze)); + }; + auto deepfreeze = [&](Account const& holder) { + env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze)); + }; + auto unfreeze = [&](Account const& holder) { + env(trust( + issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze)); + }; + return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN); + } + + auto freeze = [&](Account const& holder) { + mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock}); + }; + auto unfreeze = [&](Account const& holder) { + mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock}); + }; + return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); + }(); + + // Try freezing the accounts that can't be frozen + if (freeze) + { + for (auto const& account : {vaultPseudo, evan}) + { + // Freeze the account + freeze(account); + + // Try to create a loan with a frozen line + env(set(evan, broker.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(expectedResult)); + + // Unfreeze the account + BEAST_EXPECT(unfreeze); + unfreeze(account); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecLIMIT_EXCEEDED)); + } + } + + // Deep freeze the borrower, which prevents them from receiving + // funds + if (deepfreeze) + { + // Make sure evan has a trust line that so the issuer can + // freeze it. (Don't need to do this for the borrower, + // because LoanSet will create a line to the borrower + // automatically.) + env(trust(evan, issuer[iouCurrency_](100'000))); + + for (auto const& account : {// these accounts can't be frozen, which deep freeze + // implies + vaultPseudo, + evan, + // these accounts can't be deep frozen + lender}) + { + // Freeze evan + deepfreeze(account); + + // Try to create a loan with a deep frozen line + env(set(evan, broker.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(expectedResult)); + + // Unfreeze evan + BEAST_EXPECT(unfreeze); + unfreeze(account); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecLIMIT_EXCEEDED)); + } + } + } + + // Finally! Create a loan + + auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) { + if (auto const brokerSle = env.le(keylet::loanBroker(brokerID)); + BEAST_EXPECT(brokerSle)) + { + auto const available = brokerSle->at(sfCoverAvailable); + BEAST_EXPECT(available == expected); + return available; + } + return Number{}; + }; + auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) { + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT( + state.loanScale >= + (broker.asset.integral() + ? 0 + : std::max( + broker.vaultScale(env), state.principalOutstanding.exponent()))); + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + auto const defaultAmount = roundToAsset( + broker.asset, + std::min( + tenthBipsOfValue( + tenthBipsOfValue( + brokerSle->at(sfDebtTotal), broker.params.coverRateMin), + broker.params.coverRateLiquidation), + state.totalValue - state.managementFeeOutstanding), + state.loanScale); + return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); + } + return std::make_pair(Number{}, AccountID{}); + }; + auto replenishCover = [&env, &coverAvailable]( + BrokerInfo const& broker, + AccountID const& brokerAcct, + Number const& startingCoverAvailable, + Number const& amountToBeCovered) { + coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered); + env(loan_broker::coverDeposit( + brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered})); + coverAvailable(broker.brokerID, startingCoverAvailable); + env.close(); + }; + + auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) { + return [&, impair, baseFlag]( + Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Default the loan + + // Initialize values with the current state + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, broker.asset(broker.params.coverDeposit).number()); + + if (impair) + { + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, loanKeylet.key, tfLoanImpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); + + if (canImpair) + { + state.flags |= tfLoanImpair; + state.nextPaymentDate = env.now().time_since_epoch().count(); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + } + verifyLoanStatus(state); + } + + auto const nextDueDate = tp{d{state.nextPaymentDate}}; + + // Can't default the loan yet. The grace period hasn't + // expired + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON)); + + // Let some time pass so that the loan can be + // defaulted + env.close(nextDueDate + 60s); + + auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); + + // Default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered); + + state.flags |= tfLoanDefault; + state.paymentRemaining = 0; + state.totalValue = 0; + state.principalOutstanding = 0; + state.managementFeeOutstanding = 0; + state.nextPaymentDate = 0; + verifyLoanStatus(state); + + // Once a loan is defaulted, it can't be managed + env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED)); + }; + }; + + auto singlePayment = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus, + LoanState& state, + STAmount const& payoffAmount, + std::uint32_t numPayments, + std::uint32_t baseFlag, + std::uint32_t txFlags) { + // toEndOfLife + // + verifyLoanStatus(state); + + // Send some bogus pay transactions + env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags), + Ter(temINVALID)); + // broker.asset(80) is less than a single payment, but all these + // checks fail before that matters + env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION)); + + // TODO: Write a general "isFlag" function? See STObject::isFlag. + // Maybe add a static overloaded member? + if (!(state.flags & lsfLoanOverpayment)) + { + // If the loan does not allow overpayments, send a payment that + // tries to make an overpayment. Do not include `txFlags`, so we + // don't end up duplicating the next test transaction. + // + // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag: + // with fix on → preflight passes, apply returns tecNO_PERMISSION; + // with fix off → preflight rejects the flag, returns temINVALID_FLAG. + bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3); + STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}}; + XRPAmount const overpayFee{ + baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)}; + env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), + Fee(overpayFee), + Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG})); + + if (hasFix313) + { + env.disableFeature(fixCleanup3_1_3); + env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), + Fee(overpayFee), + Ter(temINVALID_FLAG)); + env.enableFeature(fixCleanup3_1_3); + } + } + // Try to send a payment marked as multiple mutually exclusive + // payment types. Do not include `txFlags`, so we don't duplicate + // the prior test transaction. + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanOverpayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET)); + } + + // Amount doesn't cover a single payment + env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags), + Ter(tecINSUFFICIENT_PAYMENT)); + + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); + + // Send a transaction that tries to pay more than the borrowers's + // balance + XRPAmount const badFee{ + baseFee * + (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment / + kLoanPaymentsPerFeeIncrement + + 1)}; + env(pay(borrower, + loanKeylet.key, + STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2}, + txFlags), + Fee(badFee), + Ter(tecINSUFFICIENT_FUNDS)); + + XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; + env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee)); + + env.close(); + + // log << env.meta()->getJson() << std::endl; + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = badFee + goodFee; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + state.totalValue = 0; + state.managementFeeOutstanding = 0; + state.previousPaymentDate = + state.nextPaymentDate + (state.paymentInterval * (numPayments - 1)); + state.nextPaymentDate = 0; + verifyLoanStatus(state); + + verifyLoanStatus.checkPayment( + state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); + }; + + auto fullPayment = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + // Full payoff amount will consist of + // 1. principal outstanding (1000) + // 2. accrued interest (at 12%) + // 3. prepayment penalty (closeInterest at 3.6%) + // 4. close payment fee (4) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; + BEAST_EXPECT( + periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{})); + STAmount const principalOutstanding{broker.asset, state.principalOutstanding}; + STAmount const accruedInterest{ + broker.asset, state.principalOutstanding * periodicRate * loanAge / interval}; + BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19))); + STAmount const prepaymentPenalty{ + broker.asset, state.principalOutstanding * Number(36, -3)}; + BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); + STAmount const closePaymentFee = broker.asset(4); + auto const payoffAmount = roundToScale( + principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee, + state.loanScale); + BEAST_EXPECT( + payoffAmount == + roundToAsset( + broker.asset, + broker.asset(Number(1040000114155251, -12)).number(), + state.loanScale)); + + // The terms of this loan actually make the early payoff + // more expensive than just making payments + BEAST_EXPECT( + payoffAmount > + state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); + + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + 1, + baseFlag, + tfLoanFullPayment); + }; + }; + + auto combineAllPayments = [&](std::uint32_t baseFlag) { + return + [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(); + + BEAST_EXPECT( + STAmount(broker.asset, state.periodicPayment) == + broker.asset(Number(8333457002039338267, -17))); + + // Make all the payments in one transaction + // service fee is 2 + auto const startingPayments = state.paymentRemaining; + STAmount const payoffAmount = [&]() { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + auto const rawPayoff = + startingPayments * (state.periodicPayment + broker.asset(2).value()); + STAmount payoffAmount{broker.asset, rawPayoff}; + BEAST_EXPECTS( + payoffAmount == broker.asset(Number(1024014840244721, -12)), + to_string(payoffAmount)); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + + payoffAmount = roundToScale(payoffAmount, state.loanScale); + + return payoffAmount; + }(); + + auto const totalPayoffValue = + state.totalValue + startingPayments * broker.asset(2).value(); + STAmount const totalPayoffAmount{broker.asset, totalPayoffValue}; + + BEAST_EXPECTS( + totalPayoffAmount == payoffAmount, + "Payoff amount: " + to_string(payoffAmount) + + ". Total Value: " + to_string(totalPayoffAmount)); + + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + state.paymentRemaining, + baseFlag, + 0); + }; + }; + + // There are a lot of fields that can be set on a loan, but most + // of them only affect the "math" when a payment is made. The + // only one that really affects behavior is the + // `tfLoanOverpayment` flag. + lifecycle( + caseLabel, + "Loan overpayment allowed - Impair and Default", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultImmediately(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Impair and Default", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultImmediately(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Default without Impair", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultImmediately(lsfLoanOverpayment, false)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Default without Impair", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultImmediately(0, false)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + fullPayment(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + fullPayment(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Combine all payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + combineAllPayments(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Combine all payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + combineAllPayments(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Make payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Draw and make multiple payments + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == 0); + env.close(); + + verifyLoanStatus(state); + + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; + BEAST_EXPECT( + periodicRate == Number(2283105022831050228, -24, Number::Normalized{})); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; + + testcase << currencyLabel << " Payment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "rawMFee, trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special"; + + auto const serviceFee = broker.asset(2); + + BEAST_EXPECT( + roundedPeriodicPayment == + roundToScale( + broker.asset( + Number(8333457002039338267, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + // 83334570.01162141 + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, + state.loanScale, + Number::RoundingMode::Upward); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + totalDue == + roundToScale( + broker.asset( + Number(8533457002039338267, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + + { + auto const raw = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + auto const rounded = constructLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining + << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " + << rounded.principalOutstanding << ", " << rounded.interestDue << ", " + << rounded.managementFeeDue; + } + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = + STAmount{broker.asset, totalDue} + broker.asset(10); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + transactionAmount == + roundToScale( + broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + + auto const initialState = state; + xrpl::detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, + .trackedPrincipalDelta = 0, + .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + std::size_t totalPaymentsMade = 0; + + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + while (state.paymentRemaining > 0) + { + // Compute the expected principal amount + auto const paymentComponents = xrpl::detail::computePaymentComponents( + env.current()->rules(), + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECTS( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedValueDelta <= roundedPeriodicPayment, + "Delta: " + to_string(paymentComponents.trackedValueDelta) + + ", periodic payment: " + to_string(roundedPeriodicPayment)); + + xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; + + testcase << currencyLabel << " Payment components: " << state.paymentRemaining + << ", " << deltas.interest << ", " << deltas.principal << ", " + << deltas.managementFee << ", " << paymentComponents.trackedValueDelta + << ", " << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << [&]() -> char const* { + if (paymentComponents.specialCase == + ::xrpl::detail::PaymentSpecialCase::Final) + return "final"; + if (paymentComponents.specialCase == + ::xrpl::detail::PaymentSpecialCase::Extra) + return "extra"; + return "none"; + }(); + + auto const totalDueAmount = STAmount{ + broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()}; + + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be after the 8th digit. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + diff == beast::kZero || + (diff > beast::kZero && + ((broker.asset.integral() && (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedValueDelta <= roundedPeriodicPayment); + + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToAsset( + broker.asset, + deltas.principal, + state.loanScale, + Number::RoundingMode::Upward) == + roundToScale( + broker.asset( + Number(8333228691531218890, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::kZero && + paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedPrincipalDelta == state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + (state.periodicPayment.exponent() - + (deltas.principal + deltas.interest + deltas.managementFee - + state.periodicPayment) + .exponent()) > 14); + + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + if (canImpairLoan(env, broker, state)) + { + // Making a payment will unimpair the loan + env(manage(lender, loanKeylet.key, tfLoanImpair)); + } + + env.close(); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) + { + state.paymentRemaining = 0; + state.nextPaymentDate = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += + paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT( + totalInterestPaid == + initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding)); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); + }); + +#if LOAN_TODO + // TODO + + /* + LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also + borrower) tries to do the payment. Here's the scenario: Create a XRP + loan with loan broker as borrower, loan origination fee and loan service + fee. Loan broker makes the first payment with periodic payment and loan + service fee. + */ + + auto time = [&](std::string label, std::function timed) { + if (!BEAST_EXPECT(timed)) + return; + + using clock_type = std::chrono::steady_clock; + using duration_type = std::chrono::milliseconds; + + auto const start = clock_type::now(); + timed(); + auto const duration = + std::chrono::duration_cast(clock_type::now() - start); + + log << label << " took " << duration.count() << "ms" << std::endl; + + return duration; + }; + + lifecycle( + caseLabel, + "timing", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // Estimate optimal values for kLoanPaymentsPerFeeIncrement and + // kLoanMaximumPaymentsPerTransaction. + using namespace loan; + + auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet); + auto const serviceFee = broker.asset(2).value(); + + STAmount const totalDue{ + broker.asset, + roundPeriodicPayment( + broker.asset, state.periodicPayment + serviceFee, state.loanScale)}; + + // Make a single payment + time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); }); + env.close(); + + // Make all but the final payment + auto const numPayments = (state.paymentRemaining - 2); + STAmount const bigPayment{broker.asset, totalDue * numPayments}; + XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; + time("ten payments", [&]() { + env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee)); + }); + env.close(); + + time("final payment", [&]() { + // Make the final payment + env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1})); + }); + env.close(); + }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Explicit overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment and overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + +#endif + } +}; + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanValidation_test.cpp b/src/test/app/lending/LoanValidation_test.cpp new file mode 100644 index 0000000000..d2985c4c30 --- /dev/null +++ b/src/test/app/lending/LoanValidation_test.cpp @@ -0,0 +1,558 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test { + +class LoanValidation_test : public LoanTestBase +{ +private: + void + testDisabled() + { + testcase("Disabled"); + // Lending Protocol depends on Single Asset Vault (SAV). Test + // combinations of the two amendments. + // Single Asset Vault depends on MPTokensV1, but don't test every combo + // of that. + using namespace jtx; + auto failAll = [this](FeatureBitset features) { + Env env(*this, features); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + + auto const keylet = keylet::loanBroker(alice, env.seq(alice)); + + using namespace std::chrono_literals; + using namespace loan; + + // counter party signature is optional on LoanSet. Confirm that by + // sending transaction without one. + auto setTx = env.jt(set(alice, keylet.key, Number(10000)), Ter(temDISABLED)); + env(setTx); + + // All loan transactions are disabled. + // 1. LoanSet + setTx = env.jt(setTx, Sig(sfCounterpartySignature, bob), Ter(temDISABLED)); + env(setTx); + // Actual sequence will be based off the loan broker, but we + // obviously don't have one of those if the amendment is disabled + auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice)); + // Other Loan transactions are disabled, too. + // 2. LoanDelete + env(del(alice, loanKeylet.key), Ter(temDISABLED)); + // 3. LoanManage + env(manage(alice, loanKeylet.key, tfLoanImpair), Ter(temDISABLED)); + // 4. LoanPay + env(pay(alice, loanKeylet.key, XRP(500)), Ter(temDISABLED)); + }; + failAll(all_ - featureMPTokensV1); + failAll(all_ - featureSingleAssetVault - featureLendingProtocol); + failAll(all_ - featureSingleAssetVault); + failAll(all_ - featureLendingProtocol); + } + + void + testInvalidLoanSet() + { + testcase("Invalid LoanSet"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; + auto const iou = issuer["IOU"]; + + auto testWrapper = [&](auto&& test) { + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); + env(trust(lender, iou(10'000'000))); + env(pay(issuer, lender, iou(5'000'000))); + BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); + test(env, brokerInfo, loanSetFee, debtMaximumRequest); + }; + + // preflight: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + + // first temBAD_SIGNER: TODO + // invalid grace period + { + // zero grace period + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kGracePeriod(0), + loanSetFee, + Ter(temINVALID)); + + // grace period less than default minimum + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kGracePeriod(LoanSet::kDefaultGracePeriod - 1), + loanSetFee, + Ter(temINVALID)); + + // grace period greater than payment interval + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(120), + kGracePeriod(121), + loanSetFee, + Ter(temINVALID)); + } + // empty/zero broker ID + { + auto jv = set(borrower, uint256{}, debtMaximumRequest); + + auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) { + // empty broker ID + jv[sfLoanBrokerID] = id; + env(jv, + Sig(sfCounterpartySignature, lender), + loanSetFee, + Txflags(flags), + Ter(temINVALID)); + }; + // empty broker ID + testZeroBrokerID(std::string("")); + // zero broker ID + // needs a flag to distinguish the parsed STTx from the prior + // test + testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); + } + + // preflightCheckSigningKey() failure: + // can it happen? the signature is checked before transactor + // executes + + JTx const tx = env.jt( + set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee); + STTx local = *(tx.stx); + auto counterpartySig = local.getFieldObject(sfCounterpartySignature); + auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); + badPubKey[20] ^= 0xAA; + counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); + local.setFieldObject(sfCounterpartySignature, counterpartySig); + json::Value jvResult; + jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); + auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; + BEAST_EXPECT( + res[jss::error] == "invalidTransaction" && + res[jss::error_exception] == + "fails local checks: Counterparty: Invalid signature."); + }); + + // preclaim: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + // canAddHoldingFailure (IOU only, if MPT doesn't have + // MPTCanTransfer set, then can't create Vault/LoanBroker, + // and LoanSet will fail with different error + env(fclear(issuer, asfDefaultRipple)); + env.close(); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(terNO_RIPPLE)); + }); + + // doApply: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + auto const amt = + env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); + env(pay(borrower, issuer, amt)); + + // tecINSUFFICIENT_RESERVE + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_RESERVE)); + + // addEmptyHolding failure + env(pay(issuer, borrower, amt)); + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecFROZEN)); + }); + } + + void + testInvalidLoanDelete() + { + testcase("Invalid LoanDelete"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(del(alice, beast::kZero), Ter(temINVALID)); + } + } + + void + testInvalidLoanManage() + { + testcase("Invalid LoanManage"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(manage(alice, beast::kZero, tfLoanDefault), Ter(temINVALID)); + } + } + + void + testInvalidLoanPay() + { + testcase("Invalid LoanPay"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + auto const iou = issuer["IOU"]; + + // preclaim + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower); + env(trust(lender, iou(10'000'000))); + env(pay(issuer, lender, iou(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee); + + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + // preclaim: tecFROZEN + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); + env.close(); + + env(fclear(issuer, asfGlobalFreeze)); + env.close(); + + auto const pseudoBroker = [&]() -> std::optional { + if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + return Account{"pseudo", brokerSle->at(sfAccount)}; + } + + return std::nullopt; + }(); + if (!pseudoBroker) + return; + + // Lender and pseudoaccount must both be frozen + env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze)); + env(trust( + issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // preclaim: tecFROZEN due to deep frozen + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); + env.close(); + + // Only one needs to be unfrozen + env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // The payment is late by this point + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED)); + env.close(); + env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); + env.close(); + + // preclaim: tecKILLED + // note that tecKILLED in loanMakePayment() + // doesn't happen because of the preclaim check. + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecKILLED)); + } + + void + testRequireAuth() + { + testcase("Require Auth - Implicit Pseudo-account authorization"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Env env(*this); + + env.fund(XRP(100'000), issuer, lender, borrower); + env.close(); + + auto asset = MPTTester({ + .env = env, + .issuer = issuer, + .holders = {lender, borrower}, + .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock, + .authHolder = true, + }); + + env(pay(issuer, lender, asset(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + auto forUnauthAuth = [&](auto&& doTx) { + for (auto const flag : {tfMPTUnauthorize, 0u}) + { + asset.authorize({.account = issuer, .holder = borrower, .flags = flag}); + env.close(); + doTx(flag == 0); + env.close(); + } + }; + + // Can't create a loan if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + err); + }); + + static constexpr std::uint32_t kLoanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence); + + // Can't loan pay if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); + }); + } + + void + testLimitExceeded() + { + testcase("RIPD-4125 - overpayment"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 3, + .payInterval = 200, + .gracePd = 60, + .flags = tfLoanOverpayment, + }; + + auto const assetType = AssetType::XRP; + + Env env(*this, makeConfig(), all_, nullptr, beast::Severity::Warning); + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + auto const state = getCurrentState(env, broker, loanKeylet); + + env(loan::pay( + borrower, + loanKeylet.key, + STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, + tfLoanOverpayment)); + env.close(); + + PaymentParameters const paymentParams{ + .showStepBalances = false, + .validateBalances = true, + }; + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + paymentParams); + } + + void + testWrongMaxDebtBehavior(FeatureBitset features) + { + // From FIND-003 + testcase << "Wrong Max Debt Behavior"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + + BrokerParameters const brokerParams{.debtMax = 0}; + env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); + } + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee)); + + json::Value counterpartyJson{json::ValueType::Object}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + } + + void + runAmendmentIndependent() + { + testDisabled(); + testInvalidLoanSet(); + testInvalidLoanDelete(); + testInvalidLoanManage(); + testInvalidLoanPay(); + testRequireAuth(); + testLimitExceeded(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testWrongMaxDebtBehavior(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanValidation, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/Loan_test.cpp b/src/test/app/lending/Loan_test.cpp new file mode 100644 index 0000000000..717387665e --- /dev/null +++ b/src/test/app/lending/Loan_test.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +/** + * Aggregator: running this suite ("Loan") reruns every topical Loan/Lending + * suite in one invocation. Each member suite below remains independently + * runnable under its own name. Declared manual so an unfiltered full test + * run doesn't execute every case twice. + */ +class Loan_test : public beast::unit_test::Suite +{ + void + run() override + { + static constexpr std::array kMembers{ + "LendingHelpers", + "LoanBroker", + "LoanCashBasis", + "LoanCoverFreezeAuth", + "LoanInvariants", + "LoanLifecycle", + "LoanMisc", + "LoanPay", + "LoanRounding", + "LoanSecurity", + "LoanSet", + "LoanValidation", + }; + + for (auto const& info : beast::unit_test::globalSuites()) + { + if (std::ranges::find(kMembers, info.name()) != kMembers.end()) + info.run(runner()); + } + } +}; + +BEAST_DEFINE_TESTSUITE_MANUAL(Loan, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/basics/PerfLog_test.cpp b/src/test/basics/PerfLog_test.cpp index 41b5f81f5d..24ea971515 100644 --- a/src/test/basics/PerfLog_test.cpp +++ b/src/test/basics/PerfLog_test.cpp @@ -312,7 +312,7 @@ public: // Get the all the labels we can use for RPC interfaces without // causing an assert. - std::vector labels = test::jtx::makeVector(xrpl::RPC::getHandlerNames()); + std::vector labels = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); std::shuffle(labels.begin(), labels.end(), defaultPrng()); // Get two IDs to associate with each label. Errors tend to happen at @@ -483,7 +483,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; @@ -804,7 +804,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; @@ -944,7 +944,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; diff --git a/src/test/beast/IPEndpointCommon.h b/src/test/beast/IPEndpointCommon.h index 45d036476c..6fb2bd9569 100644 --- a/src/test/beast/IPEndpointCommon.h +++ b/src/test/beast/IPEndpointCommon.h @@ -7,7 +7,7 @@ #include -namespace beast::IP { +namespace beast::ip { inline Endpoint randomEP(bool v4 = true) @@ -44,4 +44,4 @@ randomEP(bool v4 = true) randInt(1, UINT16_MAX)}; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/test/beast/IPEndpoint_test.cpp b/src/test/beast/IPEndpoint_test.cpp index bc04087891..b61878aa76 100644 --- a/src/test/beast/IPEndpoint_test.cpp +++ b/src/test/beast/IPEndpoint_test.cpp @@ -22,7 +22,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { //------------------------------------------------------------------------------ @@ -475,4 +475,4 @@ public: BEAST_DEFINE_TESTSUITE(IPEndpoint, beast, beast); -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/test/beast/LexicalCast_test.cpp b/src/test/beast/LexicalCast_test.cpp deleted file mode 100644 index b1d37daab8..0000000000 --- a/src/test/beast/LexicalCast_test.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -namespace beast { - -class LexicalCast_test : public unit_test::Suite -{ -public: - template - static IntType - nextRandomInt(xor_shift_engine& r) - { - return static_cast(r()); - } - - template - void - testInteger(IntType in) - { - std::string s; - auto out = static_cast(~in); // Ensure out != in - - expect(lexicalCastChecked(s, in)); - expect(lexicalCastChecked(out, s)); - expect(out == in); - } - - template - void - testIntegers(xor_shift_engine& r) - { - { - std::stringstream ss; - ss << "random " << typeid(IntType).name(); - testcase(ss.str()); - - for (int i = 0; i < 1000; ++i) - { - auto const value = nextRandomInt(r); - testInteger(value); - } - } - - { - std::stringstream ss; - ss << "numeric_limits <" << typeid(IntType).name() << ">"; - testcase(ss.str()); - - testInteger(std::numeric_limits::min()); - testInteger(std::numeric_limits::max()); - } - } - - void - testPathologies() - { - testcase("pathologies"); - try - { - lexicalCastThrow("\xef\xbc\x91\xef\xbc\x90"); // utf-8 encoded - } - catch (BadLexicalCast const&) - { - pass(); - } - } - - template - void - tryBadConvert(std::string const& s) - { - T out; - expect(!lexicalCastChecked(out, s), s); - } - - void - testConversionOverflows() - { - testcase("conversion overflows"); - - tryBadConvert("99999999999999999999"); - tryBadConvert("4294967300"); - tryBadConvert("75821"); - } - - void - testConversionUnderflows() - { - testcase("conversion underflows"); - - tryBadConvert("-1"); - - tryBadConvert("-99999999999999999999"); - tryBadConvert("-4294967300"); - tryBadConvert("-75821"); - } - - template - bool - tryEdgeCase(std::string const& s) - { - T ret; - - bool const result = lexicalCastChecked(ret, s); - - if (!result) - return false; - - return s == std::to_string(ret); - } - - void - testEdgeCases() - { - testcase("conversion edge cases"); - - expect(tryEdgeCase("18446744073709551614")); - expect(tryEdgeCase("18446744073709551615")); - expect(!tryEdgeCase("18446744073709551616")); - - expect(tryEdgeCase("9223372036854775806")); - expect(tryEdgeCase("9223372036854775807")); - expect(!tryEdgeCase("9223372036854775808")); - - expect(tryEdgeCase("-9223372036854775807")); - expect(tryEdgeCase("-9223372036854775808")); - expect(!tryEdgeCase("-9223372036854775809")); - - expect(tryEdgeCase("4294967294")); - expect(tryEdgeCase("4294967295")); - expect(!tryEdgeCase("4294967296")); - - expect(tryEdgeCase("2147483646")); - expect(tryEdgeCase("2147483647")); - expect(!tryEdgeCase("2147483648")); - - expect(tryEdgeCase("-2147483647")); - expect(tryEdgeCase("-2147483648")); - expect(!tryEdgeCase("-2147483649")); - - expect(tryEdgeCase("65534")); - expect(tryEdgeCase("65535")); - expect(!tryEdgeCase("65536")); - - expect(tryEdgeCase("32766")); - expect(tryEdgeCase("32767")); - expect(!tryEdgeCase("32768")); - - expect(tryEdgeCase("-32767")); - expect(tryEdgeCase("-32768")); - expect(!tryEdgeCase("-32769")); - } - - template - void - testThrowConvert(std::string const& s, bool success) - { - bool result = !success; - T out; - - try - { - out = lexicalCastThrow(s); - result = true; - } - catch (BadLexicalCast const&) - { - result = false; - } - - expect(result == success, s); - } - - void - testThrowingConversions() - { - testcase("throwing conversion"); - - testThrowConvert("99999999999999999999", false); - testThrowConvert("9223372036854775806", true); - - testThrowConvert("4294967290", true); - testThrowConvert("42949672900", false); - testThrowConvert("429496729000", false); - testThrowConvert("4294967290000", false); - - testThrowConvert("5294967295", false); - testThrowConvert("-2147483644", true); - - testThrowConvert("66666", false); - testThrowConvert("-5711", true); - } - - void - testZero() - { - testcase("zero conversion"); - - { - std::int32_t out = 0; - - expect(lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - - { - std::uint32_t out = 0; - - expect(!lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - } - - void - testEntireRange() - { - testcase("entire range"); - - std::int32_t i = std::numeric_limits::min(); - std::string const empty; - - while (i <= std::numeric_limits::max()) - { - auto const j = static_cast(i); - - auto actual = std::to_string(j); - - auto result = lexicalCast(j, empty); - - expect(result == actual, actual + " (string to integer)"); - - if (result == actual) - { - auto number = lexicalCast(result); - - if (number != j) - expect(false, actual + " (integer to string)"); - } - - i++; - } - } - - void - run() override - { - std::int64_t const seedValue = 50; - - xor_shift_engine r(seedValue); - - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - - testPathologies(); - testConversionOverflows(); - testConversionUnderflows(); - testThrowingConversions(); - testZero(); - testEdgeCases(); - testEntireRange(); - } -}; - -BEAST_DEFINE_TESTSUITE(LexicalCast, beast, beast); - -} // namespace beast diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 68b6d9f745..435e32b7b4 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -199,7 +199,7 @@ public: std::optional const& asset2 = std::nullopt, std::optional const& ammAccount = std::nullopt, bool ignoreParams = false, - unsigned apiVersion = RPC::kApiInvalidVersion) const; + unsigned apiVersion = rpc::kApiInvalidVersion) const; [[nodiscard]] json::Value ammRpcInfo( diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index 404ddbe31d..465bac03db 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -94,6 +94,36 @@ protected: return proof; } + // Generate a forged single bulletproof for a single value and blinding factor. + // Used to test ConvertBack overdraft prevention via bulletproof verification. + static Buffer + getForgedSingleBulletproof( + uint64_t value, + Buffer const& blindingFactor, + uint256 const& contextHash) + { + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + Buffer proof(kEcSingleBulletproofLength); + size_t proofLen = kEcSingleBulletproofLength; + + if (secp256k1_bulletproof_prove_agg( + ctx, + proof.data(), + &proofLen, + &value, + blindingFactor.data(), + 1, // m = 1 (single bulletproof) + &h, + contextHash.data()) == 0) + Throw("Failed to generate forged single bulletproof"); + + return proof; + } + // Get a bad ciphertext with valid structure but cryptographic invalid for // testing purposes. For preflight test purposes. static Buffer const& diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index a175fd5006..5cb841578a 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -1081,7 +1081,7 @@ Env::rpc( Args&&... args) { return doRpc( - RPC::kApiCommandLineVersion, + rpc::kApiCommandLineVersion, std::vector{cmd, std::forward(args)...}, headers); } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index e7a2808f07..d5cd8e66b8 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -876,7 +876,7 @@ checkMetrics( /* LoanBroker */ /******************************************************************************/ -namespace loanBroker { +namespace loan_broker { json::Value set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0); @@ -917,7 +917,7 @@ auto const kCoverRateLiquidation = auto const kDestination = JTxFieldWrapper(sfDestination); -} // namespace loanBroker +} // namespace loan_broker /* Loan */ /******************************************************************************/ diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 8effce288e..74232037ce 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -234,7 +234,7 @@ AMM::ammRpcInfo( jv[jss::amm_account] = *ammAccount; } auto jr = - (apiVersion == RPC::kApiInvalidVersion + (apiVersion == rpc::kApiInvalidVersion ? env_.rpc("json", "amm_info", to_string(jv)) : env_.rpc(apiVersion, "json", "amm_info", to_string(jv))); if (jr.isObject() && jr.isMember(jss::result) && jr[jss::result].isMember(jss::status)) diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 4da2e2b521..35553bdeb2 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -498,9 +498,9 @@ Env::postconditions( !test.expect( parsed.rpcCode == jt.rpcCode->first && parsed.rpcMessage == jt.rpcCode->second, "apply " + locStr + ": Got RPC result "s + - (parsed.rpcCode ? RPC::getErrorInfo(*parsed.rpcCode).token.cStr() : "NO RESULT") + + (parsed.rpcCode ? rpc::getErrorInfo(*parsed.rpcCode).token.cStr() : "NO RESULT") + " (" + parsed.rpcMessage + "); Expected " + - RPC::getErrorInfo(jt.rpcCode->first).token.cStr() + " (" + jt.rpcCode->second + + rpc::getErrorInfo(jt.rpcCode->first).token.cStr() + " (" + jt.rpcCode->second + ")")) || bad; // If we have an rpcCode (just checked), then the rpcException check is diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 4d3869b4f9..d73eb8adf4 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -211,10 +211,10 @@ findPathsRequest( using namespace jtx; auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -224,7 +224,7 @@ findPathsRequest( .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; @@ -252,7 +252,7 @@ findPathsRequest( app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = std::move(params); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); @@ -743,7 +743,7 @@ issueHelperMPT(IssuerArgs const& args) /* LoanBroker */ /******************************************************************************/ -namespace loanBroker { +namespace loan_broker { json::Value set(AccountID const& account, uint256 const& vaultId, uint32_t flags) @@ -809,7 +809,7 @@ coverClawback(AccountID const& account, std::uint32_t flags) return jv; } -} // namespace loanBroker +} // namespace loan_broker /* Loan */ /******************************************************************************/ diff --git a/src/test/jtx/impl/attester.cpp b/src/test/jtx/impl/attester.cpp index ac946a1bf3..3799d957e9 100644 --- a/src/test/jtx/impl/attester.cpp +++ b/src/test/jtx/impl/attester.cpp @@ -24,7 +24,7 @@ signClaimAttestation( std::uint64_t claimID, std::optional const& dst) { - auto const toSign = Attestations::AttestationClaim::message( + auto const toSign = attestations::AttestationClaim::message( bridge, sendingAccount, sendingAmount, rewardAccount, wasLockingChainSend, claimID, dst); return sign(pk, sk, makeSlice(toSign)); } @@ -42,7 +42,7 @@ signCreateAccountAttestation( std::uint64_t createCount, AccountID const& dst) { - auto const toSign = Attestations::AttestationCreateAccount::message( + auto const toSign = attestations::AttestationCreateAccount::message( bridge, sendingAccount, sendingAmount, diff --git a/src/test/jtx/impl/ledgerStateFixes.cpp b/src/test/jtx/impl/ledgerStateFixes.cpp index 30c6659124..ae195021b8 100644 --- a/src/test/jtx/impl/ledgerStateFixes.cpp +++ b/src/test/jtx/impl/ledgerStateFixes.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::test::jtx::ledgerStateFix { +namespace xrpl::test::jtx::ledger_state_fix { // Fix NFTokenPage links on owner's account. acct pays fee. json::Value @@ -35,4 +35,4 @@ bookExchangeRate(jtx::Account const& acct, uint256 const& bookDir) return jv; } -} // namespace xrpl::test::jtx::ledgerStateFix +} // namespace xrpl::test::jtx::ledger_state_fix diff --git a/src/test/jtx/ledgerStateFix.h b/src/test/jtx/ledgerStateFix.h index 2fe5c8accc..4ae22f891e 100644 --- a/src/test/jtx/ledgerStateFix.h +++ b/src/test/jtx/ledgerStateFix.h @@ -8,7 +8,7 @@ /** * LedgerStateFix operations. */ -namespace xrpl::test::jtx::ledgerStateFix { +namespace xrpl::test::jtx::ledger_state_fix { /** * Repair the links in an NFToken directory. @@ -22,4 +22,4 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner); json::Value bookExchangeRate(jtx::Account const& acct, uint256 const& bookDir); -} // namespace xrpl::test::jtx::ledgerStateFix +} // namespace xrpl::test::jtx::ledger_state_fix diff --git a/src/test/jtx/rpc.h b/src/test/jtx/rpc.h index 9bd99c15f8..7fd550563c 100644 --- a/src/test/jtx/rpc.h +++ b/src/test/jtx/rpc.h @@ -48,7 +48,7 @@ public: jt.ter = telENV_RPC_FAILED; if (code_) { - auto const& errorInfo = RPC::getErrorInfo(*code_); + auto const& errorInfo = rpc::getErrorInfo(*code_); // When an RPC request returns an error code ('error_code'), it // always includes an error message ('error_message'), and sometimes // includes an error token ('error'). If it does, the error token is diff --git a/src/test/overlay/ProtocolVersion_test.cpp b/src/test/overlay/ProtocolVersion_test.cpp index 2fc8e4447d..e31a574502 100644 --- a/src/test/overlay/ProtocolVersion_test.cpp +++ b/src/test/overlay/ProtocolVersion_test.cpp @@ -63,8 +63,8 @@ public: negotiateProtocolVersion("RTXP/1.2, XRPL/2.0, XRPL/2.1") == makeProtocol(2, 1)); BEAST_EXPECT(negotiateProtocolVersion("XRPL/2.2") == makeProtocol(2, 2)); BEAST_EXPECT( - negotiateProtocolVersion("RTXP/1.2, XRPL/2.2, XRPL/2.3, XRPL/999.999") == - makeProtocol(2, 2)); + negotiateProtocolVersion("RTXP/1.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") == + makeProtocol(2, 3)); BEAST_EXPECT(negotiateProtocolVersion("XRPL/999.999, WebSocket/1.0") == std::nullopt); BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt); } diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index f52220a90c..6c9105164a 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -44,7 +44,7 @@ using namespace jtx; * Test for TMGetObjectByHash reply size limiting. * * This verifies the fix that limits TMGetObjectByHash replies to - * Tuning::hardMaxReplyNodes to prevent excessive memory usage and + * tuning::hardMaxReplyNodes to prevent excessive memory usage and * potential DoS attacks from peers requesting large numbers of objects. */ class TMGetObjectByHash_test : public beast::unit_test::Suite @@ -61,11 +61,11 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite public: PeerTest( Application& app, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : PeerImp( @@ -133,8 +133,8 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto streamPtr = std::make_unique(socket_type(env.app().getIOContext()), *context_); - beast::IP::Endpoint const local(boost::asio::ip::make_address("172.1.1.1"), 51235); - beast::IP::Endpoint const remote(boost::asio::ip::make_address("172.1.1.2"), 51235); + beast::ip::Endpoint const local(boost::asio::ip::make_address("172.1.1.1"), 51235); + beast::ip::Endpoint const remote(boost::asio::ip::make_address("172.1.1.2"), 51235); PublicKey const key(std::get<0>(randomKeyPair(KeyType::Ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); @@ -227,7 +227,7 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite void run() override { - int const limit = static_cast(Tuning::kHardMaxReplyNodes); + int const limit = static_cast(tuning::kHardMaxReplyNodes); testReplyLimit(limit + 1, limit); testReplyLimit(limit, limit); testReplyLimit(limit - 1, limit - 1); diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp index 60cc69a14f..a583a3aeab 100644 --- a/src/test/overlay/compression_test.cpp +++ b/src/test/overlay/compression_test.cpp @@ -413,7 +413,7 @@ public: return env; }; auto handshake = [&](int outboundEnable, int inboundEnable) { - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); auto env = getEnv(outboundEnable); auto request = xrpl::makeRequest( diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 2f42313037..77920007de 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -92,13 +92,13 @@ public: send(std::shared_ptr const& m) override { } - [[nodiscard]] beast::IP::Endpoint + [[nodiscard]] beast::ip::Endpoint getRemoteAddress() const override { return {}; } void - charge(Resource::Charge const& fee, std::string const& context = {}) override + charge(resource::Charge const& fee, std::string const& context = {}) override { } [[nodiscard]] bool @@ -1610,7 +1610,7 @@ vp_base_squelch_max_selected_peers=2 env_.app().config().compression = c.compression; }; auto handshake = [&](int outboundEnable, int inboundEnable) { - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); setEnv(outboundEnable); auto request = xrpl::makeRequest( diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index 43f6ef2506..8626d3e19c 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -121,11 +121,11 @@ private: public: PeerTest( Application& app, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : PeerImp( @@ -192,9 +192,9 @@ private: auto streamPtr = std::make_unique( socket_type(std::forward(env.app().getIOContext())), *context_); - beast::IP::Endpoint const local( + beast::ip::Endpoint const local( boost::asio::ip::make_address("172.1.1." + std::to_string(lid_))); - beast::IP::Endpoint const remote( + beast::ip::Endpoint const remote( boost::asio::ip::make_address("172.1.1." + std::to_string(rid_))); PublicKey const key(std::get<0>(randomKeyPair(KeyType::Ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); diff --git a/src/test/protocol/BuildInfo_test.cpp b/src/test/protocol/BuildInfo_test.cpp index 1741f45938..a669e3e292 100644 --- a/src/test/protocol/BuildInfo_test.cpp +++ b/src/test/protocol/BuildInfo_test.cpp @@ -11,7 +11,7 @@ public: { testcase("EncodeSoftwareVersion"); - auto encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b7"); + auto encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b7"); // the first two bytes identify the particular implementation, 0x183B BEAST_EXPECT((encodedVersion & 0xFFFF'0000'0000'0000LLU) == 0x183B'0000'0000'0000LLU); @@ -25,15 +25,15 @@ public: // 01 if a beta BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b01); // 10 if an RC - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.4-rc7"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.4-rc7"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b10); // 11 if neither an RC nor a beta - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.5"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.5"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b11); } // the next six bits: rc/beta number (1-63) - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6-b63"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.6-b63"); BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) >> 16 == 63); // the last two bytes are zeros @@ -41,14 +41,14 @@ public: // Test some version strings with wrong formats: // no rc/beta number - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); // rc/beta number out of range - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b64"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b64"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); // Check that the rc/beta number of a release is 0: - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.6"); BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) == 0); } @@ -57,9 +57,9 @@ public: { testcase("IsXrpldVersion"); auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; - BEAST_EXPECT(!BuildInfo::isXrpldVersion(vFF)); + BEAST_EXPECT(!build_info::isXrpldVersion(vFF)); auto vXrpld = 0x183B'0000'0000'0000LLU; - BEAST_EXPECT(BuildInfo::isXrpldVersion(vXrpld)); + BEAST_EXPECT(build_info::isXrpldVersion(vXrpld)); } void @@ -67,16 +67,16 @@ public: { testcase("IsNewerVersion"); auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; - BEAST_EXPECT(!BuildInfo::isNewerVersion(vFF)); + BEAST_EXPECT(!build_info::isNewerVersion(vFF)); - auto v159 = BuildInfo::encodeSoftwareVersion("1.5.9"); - BEAST_EXPECT(!BuildInfo::isNewerVersion(v159)); + auto v159 = build_info::encodeSoftwareVersion("1.5.9"); + BEAST_EXPECT(!build_info::isNewerVersion(v159)); - auto vCurrent = BuildInfo::getEncodedVersion(); - BEAST_EXPECT(!BuildInfo::isNewerVersion(vCurrent)); + auto vCurrent = build_info::getEncodedVersion(); + BEAST_EXPECT(!build_info::isNewerVersion(vCurrent)); - auto vMax = BuildInfo::encodeSoftwareVersion("255.255.255"); - BEAST_EXPECT(BuildInfo::isNewerVersion(vMax)); + auto vMax = build_info::encodeSoftwareVersion("255.255.255"); + BEAST_EXPECT(build_info::isNewerVersion(vMax)); } void diff --git a/src/test/protocol/InnerObjectFormats_test.cpp b/src/test/protocol/InnerObjectFormats_test.cpp index 5154153ecf..73a283da39 100644 --- a/src/test/protocol/InnerObjectFormats_test.cpp +++ b/src/test/protocol/InnerObjectFormats_test.cpp @@ -5,7 +5,7 @@ #include #include // json::Reader #include -#include // RPC::containsError +#include // rpc::containsError #include // STParsedJSONObject #include @@ -13,7 +13,7 @@ namespace xrpl { -namespace InnerObjectFormatsUnitTestDetail { +namespace inner_object_formats_unit_test_detail { struct TestJSONTxt { @@ -149,7 +149,7 @@ static TestJSONTxt const kTestArray[] = { }; -} // namespace InnerObjectFormatsUnitTestDetail +} // namespace inner_object_formats_unit_test_detail class InnerObjectFormatsParsedJSON_test : public beast::unit_test::Suite { @@ -157,7 +157,7 @@ public: void run() override { - using namespace InnerObjectFormatsUnitTestDetail; + using namespace inner_object_formats_unit_test_detail; // Instantiate a jtx::Env so debugLog writes are exercised. test::jtx::Env const env(*this); @@ -166,7 +166,7 @@ public: { json::Value req; json::Reader().parse(test.txt, req); - if (RPC::containsError(req)) + if (rpc::containsError(req)) { Throw( "Internal InnerObjectFormatsParsedJSON error. Bad JSON."); diff --git a/src/test/protocol/MultiApiJson_test.cpp b/src/test/protocol/MultiApiJson_test.cpp index c6f844a206..2f0d4cb3ec 100644 --- a/src/test/protocol/MultiApiJson_test.cpp +++ b/src/test/protocol/MultiApiJson_test.cpp @@ -62,35 +62,35 @@ struct MultiApiJson_test : beast::unit_test::Suite // Some static data for test inputs static int const kPrimes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}; - static_assert(std::size(kPrimes) > RPC::kApiMaximumValidVersion); + static_assert(std::size(kPrimes) > rpc::kApiMaximumValidVersion); MultiApiJson<1, 3> s1{}; static_assert( - s1.kSize == RPC::kApiMaximumValidVersion + 1 - RPC::kApiMinimumSupportedVersion); + s1.kSize == rpc::kApiMaximumValidVersion + 1 - rpc::kApiMinimumSupportedVersion); int productAllVersions = 1; - for (unsigned i = RPC::kApiMinimumSupportedVersion; i <= RPC::kApiMaximumValidVersion; + for (unsigned i = rpc::kApiMinimumSupportedVersion; i <= rpc::kApiMaximumValidVersion; ++i) { - auto const index = i - RPC::kApiMinimumSupportedVersion; + auto const index = i - rpc::kApiMinimumSupportedVersion; BEAST_EXPECT(index == s1.index(i)); BEAST_EXPECT(s1.valid(i)); s1.val[index] = makeJson("value", kPrimes[i]); productAllVersions *= kPrimes[i]; } BEAST_EXPECT(!s1.valid(0)); - BEAST_EXPECT(!s1.valid(RPC::kApiMaximumValidVersion + 1)); + BEAST_EXPECT(!s1.valid(rpc::kApiMaximumValidVersion + 1)); BEAST_EXPECT(!s1.valid( - std::numeric_limits::max())); + std::numeric_limits::max())); int result = 1; - static_assert(RPC::kApiMinimumSupportedVersion + 1 <= RPC::kApiMaximumValidVersion); - forApiVersions( + static_assert(rpc::kApiMinimumSupportedVersion + 1 <= rpc::kApiMaximumValidVersion); + forApiVersions( std::as_const(s1).visit(), [this](json::Value const& json, unsigned int version, int* result) { BEAST_EXPECT( - version >= RPC::kApiMinimumSupportedVersion && - version <= RPC::kApiMinimumSupportedVersion + 1); + version >= rpc::kApiMinimumSupportedVersion && + version <= rpc::kApiMinimumSupportedVersion + 1); if (BEAST_EXPECT(json.isMember("value"))) { *result *= json["value"].asInt(); @@ -99,8 +99,8 @@ struct MultiApiJson_test : beast::unit_test::Suite &result); BEAST_EXPECT( result == - kPrimes[RPC::kApiMinimumSupportedVersion] * - kPrimes[RPC::kApiMinimumSupportedVersion + 1]); + kPrimes[rpc::kApiMinimumSupportedVersion] * + kPrimes[rpc::kApiMinimumSupportedVersion + 1]); // Check all the values with mutable data forAllApiVersions(s1.visit(), [&s1, this](json::Value& json, auto version) { @@ -116,8 +116,8 @@ struct MultiApiJson_test : beast::unit_test::Suite std::as_const(s1).visit(), [this](json::Value const& json, unsigned int version, int* result) { BEAST_EXPECT( - version >= RPC::kApiMinimumSupportedVersion && - version <= RPC::kApiMaximumValidVersion); + version >= rpc::kApiMinimumSupportedVersion && + version <= rpc::kApiMaximumValidVersion); if (BEAST_EXPECT(json.isMember("value"))) { *result *= json["value"].asInt(); diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 8d55c5e19d..cb20de9bf5 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -34,7 +34,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class AccountLines_test : public beast::unit_test::Suite { @@ -51,7 +51,7 @@ public: auto const lines = env.rpc("json", "account_lines", "{ }"); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::missingFieldError(jss::account)[jss::error_message]); + rpc::missingFieldError(jss::account)[jss::error_message]); } { // account_lines with a malformed account. @@ -60,7 +60,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); } { // test account non-string @@ -87,7 +87,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActNotFound)[jss::error_message]); + rpc::makeError(RpcActNotFound)[jss::error_message]); } env.fund(XRP(10000), alice); env.close(); @@ -250,7 +250,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); } { // A negative limit should fail. @@ -260,7 +260,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); } { // Limit the response to 1 trust line. @@ -297,7 +297,7 @@ public: auto const linesD = env.rpc("json", "account_lines", to_string(paramsD)); BEAST_EXPECT( linesD[jss::result][jss::error_message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); } { // A non-string marker should also fail. @@ -307,7 +307,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::marker, "string")); + rpc::expectedFieldMessage(jss::marker, "string")); } { // Check that the flags we expect from alice to gw2 are present. @@ -496,7 +496,7 @@ public: auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams)); BEAST_EXPECT( linesEnd[jss::result][jss::error_message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); } void @@ -728,7 +728,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::missingFieldError(jss::account)[jss::error_message]); + rpc::missingFieldError(jss::account)[jss::error_message]); 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); @@ -746,7 +746,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); 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); @@ -765,7 +765,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActNotFound)[jss::error_message]); + rpc::makeError(RpcActNotFound)[jss::error_message]); 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); @@ -998,7 +998,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); 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); @@ -1017,7 +1017,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); 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); @@ -1090,7 +1090,7 @@ public: auto const linesD = env.rpc("json2", to_string(requestD)); BEAST_EXPECT( linesD[jss::error][jss::message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0"); BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5); @@ -1109,7 +1109,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::expectedFieldMessage(jss::marker, "string")); + rpc::expectedFieldMessage(jss::marker, "string")); 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); @@ -1266,7 +1266,7 @@ public: auto const linesEnd = env.rpc("json2", to_string(linesEndRequest)); BEAST_EXPECT( linesEnd[jss::error][jss::message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0"); BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5); @@ -1286,4 +1286,4 @@ public: BEAST_DEFINE_TESTSUITE(AccountLines, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index f1fbc2871b..b144a6dc84 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -199,7 +199,7 @@ class AccountTx_test : public beast::unit_test::Suite auto isErr = [](json::Value const& j, ErrorCodeI code) { return j.isMember(jss::result) && j[jss::result].isMember(jss::error) && - j[jss::result][jss::error] == RPC::getErrorInfo(code).token; + j[jss::result][jss::error] == rpc::getErrorInfo(code).token; }; json::Value jParams; @@ -425,56 +425,56 @@ class AccountTx_test : public beast::unit_test::Suite p[jss::limit] = 1.2; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = "10" should fail (string instead of integer) p[jss::limit] = "10"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = true should fail (boolean instead of integer) p[jss::limit] = true; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = false should fail (boolean instead of integer) p[jss::limit] = false; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = -1 should fail (negative number) p[jss::limit] = -1; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = [] should fail (array instead of integer) p[jss::limit] = json::Value(json::ValueType::Array); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = {} should fail (object instead of integer) p[jss::limit] = json::Value(json::ValueType::Object); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = "malformed" should fail (malformed string) p[jss::limit] = "malformed"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = ["limit"] should fail (array with string) p[jss::limit] = json::Value(json::ValueType::Array); p[jss::limit].append("limit"); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = {"limit": 10} should fail (object with // property) @@ -482,7 +482,7 @@ class AccountTx_test : public beast::unit_test::Suite p[jss::limit][jss::limit] = 10; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = 10 should succeed (valid integer) p[jss::limit] = 10; diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 83f7b64b4b..646cf190ff 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -1548,7 +1548,7 @@ public: auto usd = gw["USD"]; - for (auto i = 0; i <= RPC::Tuning::kBookOffers.rmax; i++) + for (auto i = 0; i <= rpc::tuning::kBookOffers.rmax; i++) env(offer(gw, XRP(50 + (1 * i)), usd(1.0 + (0.1 * i)))); if (asAdmin) @@ -1565,15 +1565,15 @@ public: BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u)); // NOTE - a marker field is not returned for this method - jvParams[jss::limit] = RPC::Tuning::kBookOffers.rmax + 1; + jvParams[jss::limit] = rpc::tuning::kBookOffers.rmax + 1; jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::offers].isArray()); - BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::kBookOffers.rmax + 1 : 0u)); + BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? rpc::tuning::kBookOffers.rmax + 1 : 0u)); jvParams[jss::limit] = json::ValueType::Null; jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::offers].isArray()); - BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::kBookOffers.rDefault : 0u)); + BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? rpc::tuning::kBookOffers.rDefault : 0u)); } void diff --git a/src/test/rpc/Handler_test.cpp b/src/test/rpc/Handler_test.cpp index e900b92fc3..be78864cac 100644 --- a/src/test/rpc/Handler_test.cpp +++ b/src/test/rpc/Handler_test.cpp @@ -88,7 +88,7 @@ class Handler_test : public beast::unit_test::Suite std::random_device dev; std::ranlux48 prng(dev()); - std::vector names = test::jtx::makeVector(xrpl::RPC::getHandlerNames()); + std::vector names = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); std::uniform_int_distribution distr{0, names.size() - 1}; @@ -96,7 +96,7 @@ class Handler_test : public beast::unit_test::Suite auto const [mean, stdev, n] = time( 1'000'000, [&](std::size_t i) { - auto const d = RPC::getHandler(1, false, names[i]); + auto const d = rpc::getHandler(1, false, names[i]); dummy = dummy + i + (int)d->role; }, [&]() -> std::size_t { return distr(prng); }); diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index e18974e7e7..efba4075c7 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -36,7 +36,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct TxnTestData { @@ -2248,7 +2248,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == baseFee); } @@ -2268,7 +2268,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == baseFee); } @@ -2285,7 +2285,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2306,7 +2306,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2325,7 +2325,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2344,7 +2344,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2400,7 +2400,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT(req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 10); } @@ -2422,7 +2422,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT(req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 10); } @@ -2450,7 +2450,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 8889); } @@ -2473,7 +2473,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2496,7 +2496,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2519,7 +2519,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 8889); } @@ -2542,7 +2542,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } { @@ -2563,7 +2563,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } { @@ -2585,7 +2585,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } env.close(); @@ -2598,7 +2598,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "10"); BEAST_EXPECT( @@ -2624,7 +2624,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "7813"); @@ -2651,7 +2651,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "47"); BEAST_EXPECT( @@ -2682,7 +2682,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "6806"); @@ -2711,7 +2711,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::NetworkID) && result[jss::tx_json][jss::NetworkID] == 1025); @@ -2791,7 +2791,7 @@ public: { json::Value req; json::Reader().parse(txnTest.json, req); - if (RPC::containsError(req)) + if (rpc::containsError(req)) Throw("Internal JSONRPC_test error. Bad test JSON."); static Role const kTestedRoles[] = { @@ -2815,7 +2815,7 @@ public: } std::string errStr; - if (RPC::containsError(result)) + if (rpc::containsError(result)) errStr = result["error_message"].asString(); if (errStr == txnTest.expMsg[get<3>(testFunc)]) @@ -2848,4 +2848,4 @@ public: BEAST_DEFINE_TESTSUITE(JSONRPC, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/KeyGeneration_test.cpp b/src/test/rpc/KeyGeneration_test.cpp index aafe6f75a5..2b056fc6d2 100644 --- a/src/test/rpc/KeyGeneration_test.cpp +++ b/src/test/rpc/KeyGeneration_test.cpp @@ -15,7 +15,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct KeyStrings { @@ -800,4 +800,4 @@ public: BEAST_DEFINE_TESTSUITE(WalletPropose, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 7adb5a4518..b8301d5656 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -349,7 +349,7 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; auto const expectedErrMsg = - RPC::expectedFieldMessage(fieldName, getTypeName(typeID)); + rpc::expectedFieldMessage(fieldName, getTypeName(typeID)); checkErrorValue(jrr, expectedError, expectedErrMsg, location); }; @@ -383,13 +383,13 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; checkErrorValue( - jrr, "malformedRequest", RPC::missingFieldMessage(fieldName.cStr()), location); + jrr, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location); correctRequest[parentFieldName][fieldName] = json::ValueType::Null; json::Value const jrr2 = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; checkErrorValue( - jrr2, "malformedRequest", RPC::missingFieldMessage(fieldName.cStr()), location); + jrr2, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location); } auto tryField = [&](json::Value fieldValue) -> void { correctRequest[parentFieldName][fieldName] = fieldValue; @@ -399,7 +399,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, expectedError, - RPC::expectedFieldMessage(fieldName, getTypeName(typeID)), + rpc::expectedFieldMessage(fieldName, getTypeName(typeID)), location); }; @@ -1083,8 +1083,8 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; auto const expectedErrMsg = fieldValue.isNull() - ? RPC::missingFieldMessage(jss::issuer.cStr()) - : RPC::expectedFieldMessage(jss::issuer, "AccountID"); + ? rpc::missingFieldMessage(jss::issuer.cStr()) + : rpc::expectedFieldMessage(jss::issuer, "AccountID"); checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg); }; @@ -1114,7 +1114,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr[jss::result], "malformedAuthorizedCredentials", - RPC::expectedFieldMessage(jss::authorized_credentials, "array")); + rpc::expectedFieldMessage(jss::authorized_credentials, "array")); } { @@ -1134,8 +1134,8 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; auto const expectedErrMsg = fieldValue.isNull() - ? RPC::missingFieldMessage(jss::credential_type.cStr()) - : RPC::expectedFieldMessage(jss::credential_type, "hex string"); + ? rpc::missingFieldMessage(jss::credential_type.cStr()) + : rpc::expectedFieldMessage(jss::credential_type, "hex string"); checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg); }; @@ -1836,7 +1836,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, "malformedAddress", - RPC::expectedFieldMessage(jss::accounts, "array of Accounts")); + rpc::expectedFieldMessage(jss::accounts, "array of Accounts")); } { @@ -1851,7 +1851,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, "malformedAddress", - RPC::expectedFieldMessage(jss::accounts, "array of Accounts")); + rpc::expectedFieldMessage(jss::accounts, "array of Accounts")); } }; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 3a2c957691..af93108ff2 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -158,7 +158,7 @@ class LedgerRPC_test : public beast::unit_test::Suite { // Request a ledger with a very large (double) sequence. auto const ret = env.rpc("json", "ledger", "{ \"ledger_index\" : 2e15 }"); - BEAST_EXPECT(RPC::containsError(ret)); + BEAST_EXPECT(rpc::containsError(ret)); BEAST_EXPECT(ret[jss::error_message] == "Invalid parameters."); } diff --git a/src/test/rpc/LedgerRequest_test.cpp b/src/test/rpc/LedgerRequest_test.cpp index 93feee9497..98bde4e5a5 100644 --- a/src/test/rpc/LedgerRequest_test.cpp +++ b/src/test/rpc/LedgerRequest_test.cpp @@ -15,7 +15,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class LedgerRequest_test : public beast::unit_test::Suite { @@ -43,28 +43,28 @@ public: // arbitrary text is converted to 0. auto const result = env.rpc("ledger_request", "arbitrary_text"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "-1"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "0"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "1"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 1 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -75,7 +75,7 @@ public: { auto const result = env.rpc("ledger_request", "2"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 2 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -86,7 +86,7 @@ public: { auto const result = env.rpc("ledger_request", "3"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 3 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -98,7 +98,7 @@ public: { auto const r = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - !RPC::containsError(r[jss::result]) && r[jss::result][jss::ledger_index] == 3 && + !rpc::containsError(r[jss::result]) && r[jss::result][jss::ledger_index] == 3 && r[jss::result].isMember(jss::ledger)); BEAST_EXPECT( r[jss::result][jss::ledger].isMember(jss::ledger_hash) && @@ -112,7 +112,7 @@ public: auto const result = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Invalid field 'ledger_hash', not hex string."); } @@ -123,21 +123,21 @@ public: auto const result = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::have_header] == false); } { auto const result = env.rpc("ledger_request", "4"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too large"); } { auto const result = env.rpc("ledger_request", "5"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too large"); } } @@ -357,4 +357,4 @@ public: BEAST_DEFINE_TESTSUITE(LedgerRequest, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 9c17d291a1..6e30f944c7 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -286,9 +286,9 @@ class NoRippleCheckLimits_test : public beast::unit_test::Suite // be better if we could add this functionality to Env somehow // or otherwise disable endpoint charging for certain test // cases. - using namespace xrpl::Resource; + using namespace xrpl::resource; using namespace std::chrono; - using namespace beast::IP; + using namespace beast::ip; auto c = env.app().getResourceManager().newInboundEndpoint( Endpoint::fromString(test::getEnvLocalhostAddr())); @@ -301,7 +301,7 @@ class NoRippleCheckLimits_test : public beast::unit_test::Suite } }; - for (auto i = 0; i < xrpl::RPC::Tuning::kNoRippleCheck.rmax + 5; ++i) + for (auto i = 0; i < xrpl::rpc::tuning::kNoRippleCheck.rmax + 5; ++i) { if (!admin) checkBalance(); diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 4b5ab1f230..ef3213008c 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -5855,8 +5855,8 @@ public: { testcase << "RPCCall API version " << apiVersion; if (!BEAST_EXPECT( - apiVersion >= RPC::kApiMinimumSupportedVersion && - apiVersion <= RPC::kApiMaximumValidVersion)) + apiVersion >= rpc::kApiMinimumSupportedVersion && + apiVersion <= rpc::kApiMaximumValidVersion)) return; test::jtx::Env const env(*this, makeNetworkConfig(11111)); // Used only for its Journal. @@ -5870,8 +5870,8 @@ public: std::vector const args{rpcCallTest.args.begin(), rpcCallTest.args.end()}; char const* const expVersioned = - (apiVersion - RPC::kApiMinimumSupportedVersion) < rpcCallTest.exp.size() - ? rpcCallTest.exp[apiVersion - RPC::kApiMinimumSupportedVersion] + (apiVersion - rpc::kApiMinimumSupportedVersion) < rpcCallTest.exp.size() + ? rpcCallTest.exp[apiVersion - rpc::kApiMinimumSupportedVersion] : rpcCallTest.exp.back(); // Note that, over the long term, kNone of these tests should diff --git a/src/test/rpc/RPCHelpers_test.cpp b/src/test/rpc/RPCHelpers_test.cpp index 1458c0aa80..25368235a3 100644 --- a/src/test/rpc/RPCHelpers_test.cpp +++ b/src/test/rpc/RPCHelpers_test.cpp @@ -19,50 +19,50 @@ public: // Test no type. json::Value tx = json::ValueType::Object; - auto result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + auto result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == 0); // Test empty type. tx[jss::type] = ""; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test type using canonical name in mixedcase. tx[jss::type] = "MPTokenIssuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using canonical name in lowercase. tx[jss::type] = "mptokenissuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using RPC name with exact match. tx[jss::type] = "mpt_issuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using RPC name with inexact match. tx[jss::type] = "MPT_Issuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test invalid type. tx[jss::type] = 1234; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test unknown type. tx[jss::type] = "unknown"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); } diff --git a/src/test/rpc/Status_test.cpp b/src/test/rpc/Status_test.cpp index c4f8544980..aaf696e9af 100644 --- a/src/test/rpc/Status_test.cpp +++ b/src/test/rpc/Status_test.cpp @@ -12,7 +12,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class codeString_test : public beast::unit_test::Suite { @@ -202,6 +202,6 @@ public: } }; -BEAST_DEFINE_TESTSUITE(fillJson, rpc, RPC); +BEAST_DEFINE_TESTSUITE(fillJson, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/TransactionEntry_test.cpp b/src/test/rpc/TransactionEntry_test.cpp index 38a95e84f8..b57c615b71 100644 --- a/src/test/rpc/TransactionEntry_test.cpp +++ b/src/test/rpc/TransactionEntry_test.cpp @@ -183,7 +183,7 @@ class TransactionEntry_test : public beast::unit_test::Suite { json::Value expected; json::Reader().parse(expectedJson, expected); - if (RPC::containsError(expected)) + if (rpc::containsError(expected)) Throw("Internal JSONRPC_test error. Bad test JSON."); for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++) diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 4dae475b63..2921c63c17 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -62,9 +62,9 @@ class Transaction_test : public beast::unit_test::Suite char const* command = jss::tx.cStr(); char const* binary = jss::binary.cStr(); - char const* notFound = RPC::getErrorInfo(RpcTxnNotFound).token; - char const* invalid = RPC::getErrorInfo(RpcInvalidLgrRange).token; - char const* excessive = RPC::getErrorInfo(RpcExcessiveLgrRange).token; + char const* notFound = rpc::getErrorInfo(RpcTxnNotFound).token; + char const* invalid = rpc::getErrorInfo(RpcInvalidLgrRange).token; + char const* excessive = rpc::getErrorInfo(RpcExcessiveLgrRange).token; Env env{*this, features}; auto const alice = Account("alice"); @@ -301,9 +301,9 @@ class Transaction_test : public beast::unit_test::Suite char const* command = jss::tx.cStr(); char const* binary = jss::binary.cStr(); - char const* notFound = RPC::getErrorInfo(RpcTxnNotFound).token; - char const* invalid = RPC::getErrorInfo(RpcInvalidLgrRange).token; - char const* excessive = RPC::getErrorInfo(RpcExcessiveLgrRange).token; + char const* notFound = rpc::getErrorInfo(RpcTxnNotFound).token; + char const* invalid = rpc::getErrorInfo(RpcInvalidLgrRange).token; + char const* excessive = rpc::getErrorInfo(RpcExcessiveLgrRange).token; Env env{*this, makeNetworkConfig(11111)}; uint32_t const netID = env.app().getNetworkIDService().getNetworkID(); @@ -333,7 +333,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(startLegSeq + i, txnIdx, netID), + *rpc::encodeCTID(startLegSeq + i, txnIdx, netID), binary, to_string(startLegSeq), to_string(endLegSeq)); @@ -345,7 +345,7 @@ class Transaction_test : public beast::unit_test::Suite auto const tx = env.jt(noop(alice), Seq(env.seq(alice))).stx; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - auto const ctid = *RPC::encodeCTID(endLegSeq, tx->getSeqValue(), netID); + auto const ctid = *rpc::encodeCTID(endLegSeq, tx->getSeqValue(), netID); for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq) { auto const result = env.rpc( @@ -374,7 +374,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(startLegSeq + i, txnIdx, netID), + *rpc::encodeCTID(startLegSeq + i, txnIdx, netID), binary, to_string(endLegSeq + 1), to_string(endLegSeq + 100)); @@ -434,7 +434,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(endLegSeq, txnIdx, netID), + *rpc::encodeCTID(endLegSeq, txnIdx, netID), to_string(startLegSeq), to_string(deletedLedger - 1)); @@ -527,75 +527,75 @@ class Transaction_test : public beast::unit_test::Suite // Test case 1: Valid input values auto const expected11 = std::optional("CFFFFFFFFFFFFFFF"); - BEAST_EXPECT(RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11); + BEAST_EXPECT(rpc::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11); auto const expected12 = std::optional("C000000000000000"); - BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12); + BEAST_EXPECT(rpc::encodeCTID(0, 0, 0) == expected12); auto const expected13 = std::optional("C000000100020003"); - BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13); + BEAST_EXPECT(rpc::encodeCTID(1U, 2U, 3U) == expected13); auto const expected14 = std::optional("C0CA2AA7326FFFFF"); - BEAST_EXPECT(RPC::encodeCTID(13249191UL, 12911U, 65535U) == expected14); + BEAST_EXPECT(rpc::encodeCTID(13249191UL, 12911U, 65535U) == expected14); // Test case 2: ledger_seq greater than 0xFFFFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU)); + BEAST_EXPECT(!rpc::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU)); // Test case 3: txn_index greater than 0xFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF)); + BEAST_EXPECT(!rpc::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF)); // Test case 4: network_id greater than 0xFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U)); + BEAST_EXPECT(!rpc::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U)); // Test case 5: Valid input values auto const expected51 = std::optional>(std::make_tuple(0, 0, 0)); - BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51); + BEAST_EXPECT(rpc::decodeCTID("C000000000000000") == expected51); auto const expected52 = std::optional>(std::make_tuple(1U, 2U, 3U)); - BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52); + BEAST_EXPECT(rpc::decodeCTID("C000000100020003") == expected52); auto const expected53 = std::optional>( std::make_tuple(13249191UL, 12911U, 49221U)); - BEAST_EXPECT(RPC::decodeCTID("C0CA2AA7326FC045") == expected53); + BEAST_EXPECT(rpc::decodeCTID("C0CA2AA7326FC045") == expected53); // Test case 6: ctid not a string or big int - BEAST_EXPECT(!RPC::decodeCTID(0xCFF)); + BEAST_EXPECT(!rpc::decodeCTID(0xCFF)); // Test case 7: ctid not a hexadecimal string - BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFFG")); + BEAST_EXPECT(!rpc::decodeCTID("C003FFFFFFFFFFFG")); // Test case 8: ctid not exactly 16 nibbles - BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("C003FFFFFFFFFFF")); // Test case 9: ctid too large to be a valid CTID value - BEAST_EXPECT(!RPC::decodeCTID("CFFFFFFFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("CFFFFFFFFFFFFFFFF")); // Test case 10: ctid doesn't start with a C nibble - BEAST_EXPECT(!RPC::decodeCTID("FFFFFFFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("FFFFFFFFFFFFFFFF")); // Test case 11: Valid input values BEAST_EXPECT( - (RPC::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) == + (rpc::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) == std::optional>( std::make_tuple(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC000'0000'0000'0000ULL) == + (rpc::decodeCTID(0xC000'0000'0000'0000ULL) == std::optional>(std::make_tuple(0, 0, 0)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC000'0001'0002'0003ULL) == + (rpc::decodeCTID(0xC000'0001'0002'0003ULL) == std::optional>(std::make_tuple(1U, 2U, 3U)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC0CA'2AA7'326F'C045ULL) == + (rpc::decodeCTID(0xC0CA'2AA7'326F'C045ULL) == std::optional>( std::make_tuple(1324'9191UL, 12911U, 49221U)))); // Test case 12: ctid not exactly 16 nibbles - BEAST_EXPECT(!RPC::decodeCTID(0xC003'FFFF'FFFF'FFF)); + BEAST_EXPECT(!rpc::decodeCTID(0xC003'FFFF'FFFF'FFF)); // Test case 13: ctid too large to be a valid CTID value // this test case is not possible in c++ because it would overflow the // type, left in for completeness - // BEAST_EXPECT(!RPC::decodeCTID(0xCFFFFFFFFFFFFFFFFULL)); + // BEAST_EXPECT(!rpc::decodeCTID(0xCFFFFFFFFFFFFFFFFULL)); // Test case 14: ctid doesn't start with a C nibble - BEAST_EXPECT(!RPC::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL)); + BEAST_EXPECT(!rpc::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL)); } void @@ -619,7 +619,7 @@ class Transaction_test : public beast::unit_test::Suite env(pay(alice, bob, XRP(10))); env.close(); - auto const ctid = RPC::encodeCTID(startLegSeq, 0, netID); + auto const ctid = rpc::encodeCTID(startLegSeq, 0, netID); if (netID > 0xFFFF) { // Concise transaction IDs do not support a network ID > 0xFFFF. @@ -650,7 +650,7 @@ class Transaction_test : public beast::unit_test::Suite env.close(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - std::string const ctid = *RPC::encodeCTID(startLegSeq, 0, netID); + std::string const ctid = *rpc::encodeCTID(startLegSeq, 0, netID); auto isUpper = [](char c) { return std::isupper(c) != 0; }; // Verify that there are at least two upper case letters in ctid and @@ -705,7 +705,7 @@ class Transaction_test : public beast::unit_test::Suite BEAST_EXPECT(jrr.isMember(jss::ctid) == (netID <= 0xFFFF)); if (jrr.isMember(jss::ctid)) { - auto const ctid = RPC::encodeCTID(ledgerSeq, 0, netID); + auto const ctid = rpc::encodeCTID(ledgerSeq, 0, netID); BEAST_EXPECT( jrr[jss::ctid] == *ctid); // NOLINT(bugprone-unchecked-optional-access) } @@ -725,7 +725,7 @@ class Transaction_test : public beast::unit_test::Suite env.close(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID + 1); + auto const ctid = *rpc::encodeCTID(startLegSeq, 0, netID + 1); json::Value jsonTx; jsonTx[jss::binary] = false; jsonTx[jss::ctid] = ctid; diff --git a/src/test/rpc/Version_test.cpp b/src/test/rpc/Version_test.cpp index 71830c2219..b5c1abc160 100644 --- a/src/test/rpc/Version_test.cpp +++ b/src/test/rpc/Version_test.cpp @@ -32,7 +32,7 @@ class Version_test : public beast::unit_test::Suite auto jrr = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiMaximumSupportedVersion) + + "{\"api_version\": " + std::to_string(rpc::kApiMaximumSupportedVersion) + "}")[jss::result]; BEAST_EXPECT(isCorrectReply(jrr)); @@ -62,7 +62,7 @@ class Version_test : public beast::unit_test::Suite auto re = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiMinimumSupportedVersion - 1) + "}"); + "{\"api_version\": " + std::to_string(rpc::kApiMinimumSupportedVersion - 1) + "}"); BEAST_EXPECT(badVersion(re)); BEAST_EXPECT(env.app().config().betaRpcApi); @@ -71,7 +71,7 @@ class Version_test : public beast::unit_test::Suite "version", "{\"api_version\": " + std::to_string( - std::max(RPC::kApiMaximumSupportedVersion.value, RPC::kApiBetaVersion.value) + + std::max(rpc::kApiMaximumSupportedVersion.value, rpc::kApiBetaVersion.value) + 1) + "}"); BEAST_EXPECT(badVersion(re)); @@ -86,38 +86,38 @@ class Version_test : public beast::unit_test::Suite testcase("test getAPIVersionNumber function"); unsigned int const versionIfUnspecified = - RPC::kApiVersionIfUnspecified < RPC::kApiMinimumSupportedVersion - ? RPC::kApiInvalidVersion - : RPC::kApiVersionIfUnspecified; + rpc::kApiVersionIfUnspecified < rpc::kApiMinimumSupportedVersion + ? rpc::kApiInvalidVersion + : rpc::kApiVersionIfUnspecified; json::Value const jArray = json::Value(json::ValueType::Array); json::Value const jNull = json::Value(json::ValueType::Null); - BEAST_EXPECT(RPC::getAPIVersionNumber(jArray, false) == versionIfUnspecified); - BEAST_EXPECT(RPC::getAPIVersionNumber(jNull, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jArray, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jNull, false) == versionIfUnspecified); json::Value jObject = json::Value(json::ValueType::Object); - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == versionIfUnspecified); - jObject[jss::api_version] = RPC::kApiVersionIfUnspecified.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == versionIfUnspecified); + jObject[jss::api_version] = rpc::kApiVersionIfUnspecified.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == versionIfUnspecified); - jObject[jss::api_version] = RPC::kApiMinimumSupportedVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiMinimumSupportedVersion); - jObject[jss::api_version] = RPC::kApiMaximumSupportedVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiMaximumSupportedVersion); + jObject[jss::api_version] = rpc::kApiMinimumSupportedVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiMinimumSupportedVersion); + jObject[jss::api_version] = rpc::kApiMaximumSupportedVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiMaximumSupportedVersion); - jObject[jss::api_version] = RPC::kApiMinimumSupportedVersion - 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiMaximumSupportedVersion + 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiBetaVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, true) == RPC::kApiBetaVersion); - jObject[jss::api_version] = RPC::kApiBetaVersion + 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, true) == RPC::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiMinimumSupportedVersion - 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiMaximumSupportedVersion + 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiBetaVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, true) == rpc::kApiBetaVersion); + jObject[jss::api_version] = rpc::kApiBetaVersion + 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, true) == rpc::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiInvalidVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiInvalidVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); jObject[jss::api_version] = "a"; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); } void @@ -141,7 +141,7 @@ class Version_test : public beast::unit_test::Suite "\"method\": \"version\", " "\"params\": { " "\"api_version\": " + - std::to_string(RPC::kApiMaximumSupportedVersion) + "}}"; + std::to_string(rpc::kApiMaximumSupportedVersion) + "}}"; auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withApiVerion + ']'); if (!BEAST_EXPECT(re.isArray())) @@ -176,7 +176,7 @@ class Version_test : public beast::unit_test::Suite "\"params\": { " "\"api_version\": " + std::to_string( - std::max(RPC::kApiMaximumSupportedVersion.value, RPC::kApiBetaVersion.value) + 1) + + std::max(rpc::kApiMaximumSupportedVersion.value, rpc::kApiBetaVersion.value) + 1) + "}}"; auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withWrongApiVerion + ']'); @@ -226,15 +226,15 @@ class Version_test : public beast::unit_test::Suite auto jrr = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiBetaVersion) + "}")[jss::result]; + "{\"api_version\": " + std::to_string(rpc::kApiBetaVersion) + "}")[jss::result]; if (!BEAST_EXPECT(jrr.isMember(jss::version))) return; if (!BEAST_EXPECT(jrr[jss::version].isMember(jss::first)) && jrr[jss::version].isMember(jss::last)) return; - BEAST_EXPECT(jrr[jss::version][jss::first] == RPC::kApiMinimumSupportedVersion.value); - BEAST_EXPECT(jrr[jss::version][jss::last] == RPC::kApiBetaVersion.value); + BEAST_EXPECT(jrr[jss::version][jss::first] == rpc::kApiMinimumSupportedVersion.value); + BEAST_EXPECT(jrr[jss::version][jss::last] == rpc::kApiBetaVersion.value); } public: diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp index e798cd1ccc..b9f8930b7b 100644 --- a/src/tests/libxrpl/basics/IntrusiveShared.cpp +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -50,11 +50,11 @@ struct Barrier { std::mutex mtx; std::condition_variable cv; - int count; - int const initial; + std::size_t count; + std::size_t const initial; std::size_t generation{0}; - explicit Barrier(int n) : count(n), initial(n) + explicit Barrier(std::size_t n) : count(n), initial(n) { } @@ -217,7 +217,7 @@ TEST(IntrusiveSharedTest, basics) auto id = b->id; EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); - for (int i = 0; i < 10; ++i) + for (auto i = 0uz; i < 10; ++i) { strong.push_back(b); } @@ -232,7 +232,7 @@ TEST(IntrusiveSharedTest, basics) id = b->id; EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); - for (int i = 0; i < 10; ++i) + for (auto i = 0uz; i < 10; ++i) { weak.emplace_back(b); EXPECT_EQ(b->useCount(), 1); @@ -280,17 +280,17 @@ TEST(IntrusiveSharedTest, basics) TIBase::ResetStatesGuard const rsg{true}; using enum TrackedState; - using swu = SharedWeakUnion; - swu b = makeSharedIntrusive(); + using SharedWeak = SharedWeakUnion; + SharedWeak b = makeSharedIntrusive(); EXPECT_TRUE(b.isStrong() && b.useCount() == 1); auto id = b.get()->id; EXPECT_EQ(TIBase::getState(id), Alive); - swu w = b; + SharedWeak w = b; EXPECT_TRUE(TIBase::getState(id) == Alive); EXPECT_TRUE(w.isStrong() && b.useCount() == 2); w.convertToWeak(); EXPECT_TRUE(w.isWeak() && b.useCount() == 1); - swu s = w; + SharedWeak s = w; EXPECT_TRUE(s.isWeak() && b.useCount() == 1); s.convertToStrong(); EXPECT_TRUE(s.isStrong() && b.useCount() == 2); @@ -380,43 +380,57 @@ TEST(IntrusiveSharedTest, partial_delete) std::atomic destructorRan{false}; std::atomic partialDeleteRan{false}; std::latch partialDeleteStartedSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; - if (next == DeletedStarted) + if (!next) + return; + + switch (*next) { - // strong goes out of scope while weak is still in scope - // This checks that partialDelete has run to completion - // before the destructor is called. A sleep is inserted - // inside the partial delete to make sure the destructor is - // given an opportunity to run during partial delete. - EXPECT_EQ(cur, PartiallyDeleted); - } - if (next == PartiallyDeletedStarted) - { - partialDeleteStartedSyncPoint.arrive_and_wait(); - using namespace std::chrono_literals; - // Sleep and let the weak pointer go out of scope, - // potentially triggering a destructor while partial delete - // is running. The test is to make sure that doesn't happen. - std::this_thread::sleep_for(800ms); - } - if (next == PartiallyDeleted) - { - EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan.exchange(true)); + case DeletedStarted: + // strong goes out of scope while weak is still in scope + // This checks that partialDelete has run to completion + // before the destructor is called. A sleep is inserted + // inside the partial delete to make sure the destructor is + // given an opportunity to run during partial delete. + EXPECT_EQ(cur, PartiallyDeleted); + break; + + case PartiallyDeletedStarted: { + partialDeleteStartedSyncPoint.arrive_and_wait(); + using namespace std::chrono_literals; + // Sleep and let the weak pointer go out of scope, + // potentially triggering a destructor while partial delete + // is running. The test is to make sure that doesn't happen. + std::this_thread::sleep_for(800ms); + break; + } + + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + break; + + case Deleted: + EXPECT_FALSE(destructorRan.exchange(true)); + break; + + case Uninitialized: + case Alive: + break; } }; + std::thread t1{[&] { partialDeleteStartedSyncPoint.arrive_and_wait(); weak.reset(); // Trigger a full delete as soon as the partial // delete starts }}; + std::thread t2{[&] { strong.reset(); // Trigger a partial delete }}; + t1.join(); t2.join(); @@ -444,13 +458,24 @@ TEST(IntrusiveSharedTest, destructor) std::latch weakResetSyncPoint{2}; strong->tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan.exchange(true)); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + break; + + case Deleted: + EXPECT_FALSE(destructorRan.exchange(true)); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; std::thread t1{[&] { @@ -492,25 +517,36 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) -> std::vector, WeakIntrusive>> { std::vector, WeakIntrusive>> result; - std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution toCreateDist(4, 64); std::uniform_int_distribution<> isStrongDist(0, 1); auto numToCreate = toCreateDist(eng); result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) + for (auto i = 0uz; i < numToCreate; ++i) { if (isStrongDist(eng)) { @@ -523,8 +559,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) } return result; }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kNumThreads = 16uz; std::vector> toClone; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToCloneSyncPoint{kNumThreads}; @@ -533,7 +569,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) std::random_device rd; std::vector result; result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) result.emplace_back(rd()); return result; }(); @@ -541,8 +577,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) // cloneAndDestroy clones the strong pointer into a vector of mixed // strong and weak pointers and destroys them all at once. // threadId==0 is special. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto cloneAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -582,11 +618,11 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(cloneAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } @@ -623,31 +659,42 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) -> std::vector> { std::vector> result; - std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution toCreateDist(4, 64); auto numToCreate = toCreateDist(eng); result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) + for (auto i = 0uz; i < numToCreate; ++i) result.emplace_back(SharedIntrusive(toClone)); return result; }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kFlipPointersLoopIters = 256; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kFlipPointersLoopIters = 256uz; + constexpr auto kNumThreads = 16uz; std::vector> toClone; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToCloneSyncPoint{kNumThreads}; @@ -657,7 +704,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) std::random_device rd; std::vector result; result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) result.emplace_back(rd()); return result; }(); @@ -666,8 +713,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) // mixed strong and weak pointers, runs a loop that randomly // changes strong pointers to weak pointers, and destroys them // all at once. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto cloneAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -702,7 +749,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) postCreateVecOfPointersSyncPoint.arriveAndWait(); std::uniform_int_distribution<> isStrongDist(0, 1); - for (int f = 0; f < kFlipPointersLoopIters; ++f) + for (auto f = 0uz; f < kFlipPointersLoopIters; ++f) { for (auto& p : v) { @@ -725,11 +772,11 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(cloneAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } @@ -761,21 +808,32 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kLockWeakLoopIters = 256; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kLockWeakLoopIters = 256uz; + constexpr auto kNumThreads = 16uz; std::vector> toLock; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToLockSyncPoint{kNumThreads}; @@ -784,8 +842,8 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) // lockAndDestroy creates weak pointers from the strong pointer // and runs a loop that locks the weak pointer. At the end of the loop // all the pointers are destroyed all at once. - auto lockAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto lockAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -816,7 +874,7 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) // Multiple threads all create a weak pointer from the same // strong pointer WeakIntrusive const weak{toLock[threadId]}; - for (int wi = 0; wi < kLockWeakLoopIters; ++wi) + for (auto wi = 0uz; wi < kLockWeakLoopIters; ++wi) { EXPECT_FALSE(weak.expired()); auto strong = weak.lock(); @@ -831,11 +889,11 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(lockAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } diff --git a/src/tests/libxrpl/basics/MallocTrim.cpp b/src/tests/libxrpl/basics/MallocTrim.cpp index 6ac8957f0e..52151262b0 100644 --- a/src/tests/libxrpl/basics/MallocTrim.cpp +++ b/src/tests/libxrpl/basics/MallocTrim.cpp @@ -199,7 +199,7 @@ TEST(mallocTrim, repeated_calls) beast::Journal const journal{beast::Journal::getNullSink()}; // Call malloc_trim multiple times to ensure it's safe - for (int i = 0; i < 5; ++i) + for (auto i = 0uz; i < 5; ++i) { MallocTrimReport const report = mallocTrim("iteration_" + std::to_string(i), journal); diff --git a/src/tests/libxrpl/basics/Number.cpp b/src/tests/libxrpl/basics/Number.cpp index 36e1b4a700..32f93eb1f7 100644 --- a/src/tests/libxrpl/basics/Number.cpp +++ b/src/tests/libxrpl/basics/Number.cpp @@ -323,11 +323,11 @@ TEST(NumberTest, add) __LINE__, }, { - // Does not round. Mantissas are going to be > maxRep, so if + // Does not round. Mantissas are going to be > kMaxRep, so if // added together as uint64_t's, the result will overflow. // With addition using uint128_t, there's no problem. After // normalizing, the resulting mantissa ends up less than - // maxRep. + // kMaxRep. Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, @@ -1078,14 +1078,6 @@ TEST(NumberTest, root) EXPECT_EQ(result, z) << ss.str(); } }; - /* - auto tests = [&](auto const& cSmall, auto const& cLarge) { - test(cSmall); - if (scale != MantissaRange::mantissa_scale::small) - test(cLarge); - }; - */ - auto const cSmall = std::to_array( {{Number{2}, 2, Number{1414213562373095049, -18}}, {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, @@ -1511,7 +1503,7 @@ TEST(NumberTest, to_string) NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - EXPECT_EQ(maxMantissa, (9'999'999'999'999'999)); + EXPECT_EQ(maxMantissa, 9'999'999'999'999'999); test( Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, "9999999999999999", @@ -1550,7 +1542,7 @@ TEST(NumberTest, to_string) NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - EXPECT_EQ((maxMantissa), (9'999'999'999'999'999'999ULL)); + EXPECT_EQ(maxMantissa, 9'999'999'999'999'999'999ULL); test( Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990", diff --git a/src/tests/libxrpl/basics/base58.cpp b/src/tests/libxrpl/basics/base58.cpp index d452453f76..d6b1d2c3f9 100644 --- a/src/tests/libxrpl/basics/base58.cpp +++ b/src/tests/libxrpl/basics/base58.cpp @@ -151,7 +151,7 @@ randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) auto const numCoeff = numCoeffDist(eng); std::vector coeffs; coeffs.reserve(numCoeff); - for (int i = 0; i < numCoeff; ++i) + for (auto i = 0uz; i < numCoeff; ++i) { coeffs.push_back(dist(eng)); } @@ -167,7 +167,7 @@ TEST(Base58Test, multiprecision) auto eng = randEngine(); std::uniform_int_distribution dist; std::uniform_int_distribution dist1(1); - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); if (d == 0u) @@ -185,7 +185,7 @@ TEST(Base58Test, multiprecision) EXPECT_EQ(refMod.convert_to(), mod); EXPECT_EQ(foundDiv, refDiv); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); @@ -204,7 +204,7 @@ TEST(Base58Test, multiprecision) auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); EXPECT_EQ(refAdd, foundAdd); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist1(eng); // Force overflow @@ -221,7 +221,7 @@ TEST(Base58Test, multiprecision) auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); EXPECT_NE(refAdd, foundAdd); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); @@ -239,7 +239,7 @@ TEST(Base58Test, multiprecision) auto const foundMul = multiprecision_utils::toBoostMP(bigInt); EXPECT_EQ(refMul, foundMul); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist1(eng); // Force overflow @@ -265,7 +265,7 @@ TEST(Base58Test, fast_matches_ref) std::array b256ResultBuf[2]; std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b58ResultBuf[i]}; if (i == 0) @@ -297,7 +297,7 @@ TEST(Base58Test, fast_matches_ref) } } - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; if (i == 0) @@ -339,7 +339,7 @@ TEST(Base58Test, fast_matches_ref) std::array b256ResultBuf[2]; std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; if (i == 0) @@ -370,7 +370,7 @@ TEST(Base58Test, fast_matches_ref) } } - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; if (i == 0) @@ -425,7 +425,7 @@ TEST(Base58Test, fast_matches_ref) // test with random data constexpr std::size_t kIters = 100000; - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::array b256DataBuf{}; auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp index 174cc33aa0..10795f4563 100644 --- a/src/tests/libxrpl/basics/base_uint.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -59,65 +59,67 @@ struct BaseUintTest : public ::testing::Test static void testComparisons() { - { - static constexpr std::array, 6> kTestArgs{ - {{"0000000000000000", "0000000000000001"}, - {"0000000000000000", "ffffffffffffffff"}, - {"1234567812345678", "2345678923456789"}, - {"8000000000000000", "8000000000000001"}, - {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, - {"fffffffffffffffe", "ffffffffffffffff"}}}; + using HexPair = std::pair; - for (auto const& arg : kTestArgs) + { + static constexpr auto kTestArgs = std::to_array({ + {"0000000000000000", "0000000000000001"}, + {"0000000000000000", "ffffffffffffffff"}, + {"1234567812345678", "2345678923456789"}, + {"8000000000000000", "8000000000000001"}, + {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, + {"fffffffffffffffe", "ffffffffffffffff"}, + }); + + for (auto const& [smallerText, largerText] : kTestArgs) { - xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; + xrpl::BaseUInt<64> const smaller{smallerText}, larger{largerText}; // For code readability, we want to use general boolean // expectations instead of specific EXPECT_LT etc. - EXPECT_TRUE(u < v); - EXPECT_TRUE(u <= v); - EXPECT_TRUE(u != v); - EXPECT_FALSE(u == v); - EXPECT_FALSE(u > v); - EXPECT_FALSE(u >= v); - EXPECT_FALSE(v < u); - EXPECT_FALSE(v <= u); - EXPECT_TRUE(v != u); - EXPECT_FALSE(v == u); - EXPECT_TRUE(v > u); - EXPECT_TRUE(v >= u); - EXPECT_TRUE(u == u); - EXPECT_TRUE(v == v); + EXPECT_TRUE(smaller < larger); + EXPECT_TRUE(smaller <= larger); + EXPECT_TRUE(smaller != larger); + EXPECT_FALSE(smaller == larger); + EXPECT_FALSE(smaller > larger); + EXPECT_FALSE(smaller >= larger); + EXPECT_FALSE(larger < smaller); + EXPECT_FALSE(larger <= smaller); + EXPECT_TRUE(larger != smaller); + EXPECT_FALSE(larger == smaller); + EXPECT_TRUE(larger > smaller); + EXPECT_TRUE(larger >= smaller); + EXPECT_TRUE(smaller == smaller); + EXPECT_TRUE(larger == larger); } } { - static constexpr std::array, 6> kTestArgs{ - { - {"000000000000000000000000", "000000000000000000000001"}, - {"000000000000000000000000", "ffffffffffffffffffffffff"}, - {"0123456789ab0123456789ab", "123456789abc123456789abc"}, - {"555555555555555555555555", "55555555555a555555555555"}, - {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, - {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, - }}; + static constexpr auto kTestArgs = std::to_array({ + {"000000000000000000000000", "000000000000000000000001"}, + {"000000000000000000000000", "ffffffffffffffffffffffff"}, + {"0123456789ab0123456789ab", "123456789abc123456789abc"}, + {"555555555555555555555555", "55555555555a555555555555"}, + {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, + {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, + }); - for (auto const& arg : kTestArgs) + for (auto const& [smallerText, largerText] : kTestArgs) { - xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; - EXPECT_TRUE(u < v); - EXPECT_TRUE(u <= v); - EXPECT_TRUE(u != v); - EXPECT_FALSE(u == v); - EXPECT_FALSE(u > v); - EXPECT_FALSE(u >= v); - EXPECT_FALSE(v < u); - EXPECT_FALSE(v <= u); - EXPECT_TRUE(v != u); - EXPECT_FALSE(v == u); - EXPECT_TRUE(v > u); - EXPECT_TRUE(v >= u); - EXPECT_TRUE(u == u); - EXPECT_TRUE(v == v); + xrpl::BaseUInt<96> const smaller{smallerText}, larger{largerText}; + EXPECT_TRUE(smaller < larger); + EXPECT_TRUE(smaller <= larger); + EXPECT_TRUE(smaller != larger); + EXPECT_FALSE(smaller == larger); + EXPECT_FALSE(smaller > larger); + EXPECT_FALSE(smaller >= larger); + EXPECT_FALSE(larger < smaller); + EXPECT_FALSE(larger <= smaller); + EXPECT_TRUE(larger != smaller); + EXPECT_FALSE(larger == smaller); + EXPECT_TRUE(larger > smaller); + EXPECT_TRUE(larger >= smaller); + EXPECT_TRUE(smaller == smaller); + EXPECT_TRUE(larger == larger); } } } @@ -401,14 +403,14 @@ TEST_F(BaseUintTest, base_uint) { } }; - constexpr StrBaseUInt kTestCases[] = { + constexpr auto kTestCases = std::to_array({ "000000000000000000000000", "000000000000000000000001", "fedcba9876543210ABCDEF91", "19FEDCBA0123456789abcdef", "800000000000000000000000", "fFfFfFfFfFfFfFfFfFfFfFfF", - }; + }); for (StrBaseUInt const& t : kTestCases) { diff --git a/src/tests/libxrpl/basics/join.cpp b/src/tests/libxrpl/basics/join.cpp index 66c832678b..427f0b42bc 100644 --- a/src/tests/libxrpl/basics/join.cpp +++ b/src/tests/libxrpl/basics/join.cpp @@ -19,11 +19,11 @@ struct JoinTest : public ::testing::Test TEST_F(JoinTest, join) { - auto test = [](auto collectionanddelimiter, std::string expected) { + auto test = [](auto collectionAndDelimiter, std::string expected) { std::stringstream ss; // Put something else in the buffer before and after to ensure that // the << operator returns the stream correctly. - ss << "(" << collectionanddelimiter << ")"; + ss << "(" << collectionAndDelimiter << ")"; auto const str = ss.str(); EXPECT_EQ(str.substr(1, str.length() - 2), expected); EXPECT_EQ(str.front(), '('); diff --git a/src/tests/libxrpl/beast/LexicalCast.cpp b/src/tests/libxrpl/beast/LexicalCast.cpp new file mode 100644 index 0000000000..d18af4e1cd --- /dev/null +++ b/src/tests/libxrpl/beast/LexicalCast.cpp @@ -0,0 +1,339 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace { + +template +[[nodiscard]] constexpr bool +parses(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text); +} + +template +[[nodiscard]] constexpr T +parsed(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) ? out : T{}; +} + +template +constexpr T kMax = std::numeric_limits::max(); + +template +constexpr T kMin = std::numeric_limits::min(); + +template +constexpr T kUnderMax = kMax - 1; + +template +constexpr T kOverMin = kMin + 1; + +// Comfortably inside the range, not boundary values. +constexpr auto kNearMax32 = kMax - 5; +constexpr auto kNearMin32 = kMin + 4; +constexpr auto kUnderInt64Max = uint64_t{kMax} - 1; +constexpr auto kInRangeInt16 = int16_t{-5711}; + +// No wider integer type can hold these, so ToString cannot produce them. +constexpr auto kAboveUint64Max = "18446744073709551616"; +constexpr auto kBelowInt64Min = "-9223372036854775809"; + +// Out of range for every integer type we test. +constexpr auto kTwentyNines = "99999999999999999999"; +constexpr auto kNegativeTwentyNines = "-99999999999999999999"; + +// Arbitrary values chosen to sit well outside a type's range, not just over it. +constexpr auto kAboveUint16Max = "75821"; +constexpr auto kBelowInt16Min = "-75821"; +constexpr auto kAboveInt32Max = "5294967295"; +constexpr auto kAboveInt16Max = "66666"; + +constexpr auto kPositiveInt32 = int32_t{42}; +constexpr auto kNegativeInt32 = int32_t{-42}; + +constexpr auto kPositiveInt32Text = "+42"; +constexpr auto kNegativeInt32Text = "-42"; + +constexpr auto kNegativeOne = "-1"; +constexpr auto kNegativeZero = "-0"; +constexpr auto kBareZero = "0"; +constexpr auto kPositiveZero = "+0"; + +// Full-width digits one and zero, not ASCII ones. +constexpr std::string_view kFullWidthDigits = "\xef\xbc\x91\xef\xbc\x90"; + +// The decimal text of a value, usable in a constant expression. +template +struct ToString +{ + std::array buffer{}; + std::size_t length{}; + + constexpr explicit ToString(T value) + { + auto const result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + length = static_cast(result.ptr - buffer.data()); + } + + constexpr + operator std::string_view() const + { + return {buffer.data(), length}; + } +}; + +template +constexpr auto kMaxText = ToString{kMax}; + +template +constexpr auto kUnderMaxText = ToString{kUnderMax}; + +template +constexpr auto kOverMaxText = ToString{Wider{kMax} + 1}; + +template +constexpr auto kMinText = ToString{kMin}; + +template +constexpr auto kOverMinText = ToString{kOverMin}; + +template +constexpr auto kUnderMinText = ToString{Wider{kMin} - 1}; + +constexpr auto kOverUint32MaxText = ToString{uint64_t{kMax} + 5}; +constexpr auto kNegatedOverUint32MaxText = ToString{-(int64_t{kMax} + 5)}; + +// lexicalCastThrow deduces its input type, so the text has to be an explicit +// string_view rather than a ToString. +template +[[nodiscard]] constexpr T +castThrow(Value value) +{ + return lexicalCastThrow(std::string_view{ToString{value}}); +} + +template +[[nodiscard]] bool +roundTrips(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) && std::to_string(out) == text; +} + +template +void +expectRoundTrip(T value) +{ + SCOPED_TRACE(::testing::Message() << "value: " << value); + + auto const text = lexicalCast(value); + EXPECT_EQ(text, std::to_string(value)); + + auto decoded = static_cast(~value); // ensure decoded != value + EXPECT_TRUE(lexicalCastChecked(decoded, text)); + EXPECT_EQ(decoded, value); +} + +} // namespace + +// int/unsigned/short/unsigned short are covered by the list below — they are +// these exact types everywhere we build. +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +using IntegerTypes = ::testing::Types< // + int16_t, + uint16_t, + int32_t, + uint32_t, + int64_t, + uint64_t>; + +struct IntegerTypeNames +{ + template + static std::string + // NOLINTNEXTLINE(readability-identifier-naming) - required by gtest + GetName(int) + { + return (std::is_signed_v ? "int" : "uint") + std::to_string(sizeof(T) * 8) + "_t"; + } +}; + +template +class LexicalCastIntegers : public ::testing::Test +{ +}; + +TYPED_TEST_SUITE(LexicalCastIntegers, IntegerTypes, IntegerTypeNames); + +TYPED_TEST(LexicalCastIntegers, round_trips_random_values) +{ + static constexpr auto kSampleCount = 1000uz; + + xor_shift_engine r{50}; // seeded per test so a failure reproduces on its own + + for (auto i = 0uz; i < kSampleCount; ++i) + expectRoundTrip(static_cast(r())); +} + +TYPED_TEST(LexicalCastIntegers, round_trips_numeric_limits) +{ + expectRoundTrip(std::numeric_limits::min()); + expectRoundTrip(std::numeric_limits::max()); +} + +TEST(LexicalCast, round_trips_every_int16_value) +{ + for (int32_t i = kMin; i <= kMax; ++i) + { + auto const value = static_cast(i); + + // ASSERT, or a broken cast reports all 65536 iterations. + auto const text = lexicalCast(value); + ASSERT_EQ(text, std::to_string(value)); + ASSERT_EQ(lexicalCast(text), value); + } +} + +TEST(LexicalCast, rejects_overflow) +{ + static_assert(not parses(kOverUint32MaxText)); + static_assert(not parses(kTwentyNines)); + static_assert(not parses(kAboveUint16Max)); +} + +TEST(LexicalCast, rejects_underflow) +{ + static_assert(not parses(kNegativeOne)); + static_assert(not parses(kNegatedOverUint32MaxText)); + static_assert(not parses(kNegativeTwentyNines)); + static_assert(not parses(kBelowInt16Min)); +} + +TEST(LexicalCast, accepts_up_to_the_maximum) +{ + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kAboveUint64Max)); +} + +TEST(LexicalCast, accepts_down_to_the_minimum) +{ + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kBelowInt64Min)); +} + +TEST(LexicalCast, limits_round_trip_through_to_string) +{ + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); +} + +TEST(LexicalCast, accepts_signed_zero_in_every_form) +{ + static_assert(parsed(kNegativeZero) == 0); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, rejects_negative_zero_when_unsigned) +{ + static_assert(not parses(kNegativeZero)); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, accepts_char_pointer_and_std_string_input) +{ + int32_t fromLiteral = 0; + EXPECT_TRUE(lexicalCastChecked(fromLiteral, kPositiveInt32Text)); + EXPECT_EQ(fromLiteral, kPositiveInt32); + + int32_t fromString = 0; + EXPECT_TRUE(lexicalCastChecked(fromString, std::string{kNegativeInt32Text})); + EXPECT_EQ(fromString, kNegativeInt32); +} + +TEST(LexicalCast, throwing_cast_returns_in_range_values) +{ + static_assert(castThrow(kUnderInt64Max) == kUnderInt64Max); + static_assert(castThrow(kNearMax32) == kNearMax32); + static_assert(castThrow(kNearMin32) == kNearMin32); + static_assert(castThrow(kInRangeInt16) == kInRangeInt16); +} + +TEST(LexicalCast, throwing_cast_throws_on_out_of_range) +{ + EXPECT_THROW(lexicalCastThrow(kTwentyNines), BadLexicalCast); + + // kNearMax32 with digits appended, so each is further past uint32_t's range. + for (auto const scale : {10, 100, 1000}) + { + auto const tooBig = ToString{uint64_t{kNearMax32} * scale}; + EXPECT_THROW(lexicalCastThrow(std::string_view{tooBig}), BadLexicalCast); + } + + EXPECT_THROW(lexicalCastThrow(kAboveInt32Max), BadLexicalCast); + EXPECT_THROW(lexicalCastThrow(kAboveInt16Max), BadLexicalCast); +} + +// Full-width digits, not ASCII ones. +TEST(LexicalCast, throwing_cast_throws_on_utf8_digits) +{ + EXPECT_THROW(lexicalCastThrow(kFullWidthDigits), BadLexicalCast); +} + +} // namespace beast diff --git a/src/tests/libxrpl/consensus/CensorshipDetector.cpp b/src/tests/libxrpl/consensus/CensorshipDetector.cpp index aa6b2d086b..2c6b6ec731 100644 --- a/src/tests/libxrpl/consensus/CensorshipDetector.cpp +++ b/src/tests/libxrpl/consensus/CensorshipDetector.cpp @@ -69,7 +69,7 @@ TEST(CensorshipDetectorTest, censorship_detector) runRound(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24}); runRound(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {}); - for (int i = 0; i != 10; ++i) + for (auto i = 0uz; i != 10; ++i) runRound(cdet, ++round, {23}, {}, {23}, {}); runRound(cdet, ++round, {23, 29}, {29}, {23}, {}); diff --git a/src/tests/libxrpl/csf/TrustGraph.h b/src/tests/libxrpl/csf/TrustGraph.h index d010b954e0..8a804fcd5b 100644 --- a/src/tests/libxrpl/csf/TrustGraph.h +++ b/src/tests/libxrpl/csf/TrustGraph.h @@ -118,9 +118,9 @@ public: std::vector res; // Loop over all pairs of uniqueUNLs - for (int i = 0; i < uniqueUNLs.size(); ++i) + for (auto i = 0uz; i < uniqueUNLs.size(); ++i) { - for (int j = (i + 1); j < uniqueUNLs.size(); ++j) + for (auto j = i + 1; j < uniqueUNLs.size(); ++j) { auto const& unlA = uniqueUNLs[i]; auto const& unlB = uniqueUNLs[j]; diff --git a/src/tests/libxrpl/csf/random.h b/src/tests/libxrpl/csf/random.h index 007bdecb1b..56838bb280 100644 --- a/src/tests/libxrpl/csf/random.h +++ b/src/tests/libxrpl/csf/random.h @@ -24,11 +24,12 @@ randomWeightedShuffle(std::vector v, std::vector w, G& g) { using std::swap; - for (int i = 0; i < v.size() - 1; ++i) + for (auto i = 0uz; i + 1 < v.size(); ++i) { - // pick a random item weighted by w - std::discrete_distribution<> dd(w.begin() + i, w.end()); // NOLINT(misc-const-correctness) - auto idx = dd(g); + // Pick a random item from the unplaced tail, weighted by w. + // NOLINTNEXTLINE(misc-const-correctness) + std::discrete_distribution dd(w.begin() + i, w.end()); + auto const idx = i + dd(g); std::swap(v[i], v[idx]); std::swap(w[i], w[idx]); } diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index 0e1ff0f2c0..eae3180aa5 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -214,7 +214,7 @@ public: throw std::logic_error("TestServiceRegistry::peerReservations() not implemented"); } - Resource::Manager& + resource::Manager& getResourceManager() override { throw std::logic_error("TestServiceRegistry::getResourceManager() not implemented"); diff --git a/src/tests/libxrpl/nodestore/Database.cpp b/src/tests/libxrpl/nodestore/Database.cpp index 23087a2f84..82012ed347 100644 --- a/src/tests/libxrpl/nodestore/Database.cpp +++ b/src/tests/libxrpl/nodestore/Database.cpp @@ -27,11 +27,11 @@ namespace { std::vector allBackends() { - std::vector types{"memory", "nudb"}; #if XRPL_ROCKSDB_AVAILABLE - types.emplace_back("rocksdb"); + return {"memory", "nudb", "rocksdb"}; +#else + return {"memory", "nudb"}; #endif - return types; } std::vector diff --git a/src/tests/libxrpl/peerfinder/Livecache.cpp b/src/tests/libxrpl/peerfinder/Livecache.cpp index 464ec0e5da..298b09e04e 100644 --- a/src/tests/libxrpl/peerfinder/Livecache.cpp +++ b/src/tests/libxrpl/peerfinder/Livecache.cpp @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace { class LivecacheTest : public ::testing::Test @@ -41,22 +41,22 @@ protected: return beast::Journal{TestSink::instance()}; } - static beast::IP::Endpoint + static beast::ip::Endpoint endpoint(std::uint16_t index, bool v4 = true) { auto const port = static_cast(10000 + index); if (v4) { - auto bytes = beast::IP::AddressV4::bytes_type{ + auto bytes = beast::ip::AddressV4::bytes_type{ {54, static_cast((index / 256) % 256), static_cast(index % 256), 1}}; - return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port}; + return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV4{bytes}}, port}; } - auto bytes = beast::IP::AddressV6::bytes_type{ + auto bytes = beast::ip::AddressV6::bytes_type{ {0x20, 0x01, 0x0d, @@ -73,11 +73,11 @@ protected: static_cast((index / 256) % 256), static_cast(index % 256), 1}}; - return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port}; + return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV6{bytes}}, port}; } void - addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0) + addEndpoint(beast::ip::Endpoint const& ep, std::uint32_t hops = 0) { cache_.insert(Endpoint{ep, hops}); } @@ -161,7 +161,7 @@ TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back) TEST_F(LivecacheTest, on_write_reports_entries_and_expiration) { cache_.insert(Endpoint{endpoint(1), 1}); - cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1}); + cache_.insert(Endpoint{endpoint(2), tuning::kMaxHops + 1}); JsonPropertyStream stream; { @@ -190,7 +190,7 @@ TEST_F(LivecacheTest, expire_removes_entries_after_ttl) cache_.expire(); EXPECT_EQ(cache_.size(), 1u); - clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); + clock_.advance(tuning::kLiveCacheSecondsToLive - 1s); cache_.expire(); EXPECT_EQ(cache_.size(), 1u); @@ -206,7 +206,7 @@ TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl) cache_.insert(Endpoint{endpoint(1), 1}); cache_.insert(Endpoint{endpoint(2), 2}); - clock_.advance(Tuning::kLiveCacheSecondsToLive); + clock_.advance(tuning::kLiveCacheSecondsToLive); cache_.expire(); EXPECT_TRUE(cache_.empty()); } @@ -240,11 +240,11 @@ TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) { for (auto i = 0; i < 100; ++i) { - addEndpoint(endpoint(static_cast(i)), xrpl::randInt(Tuning::kMaxHops + 1)); + addEndpoint(endpoint(static_cast(i)), xrpl::randInt(tuning::kMaxHops + 1)); } using AtHop = std::vector; - using AllHops = std::array; + using AllHops = std::array; auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address); @@ -291,4 +291,4 @@ TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) EXPECT_FALSE(allBucketsKeptOriginalOrder); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/tests/libxrpl/peerfinder/PeerFinder.cpp b/src/tests/libxrpl/peerfinder/PeerFinder.cpp index 31fa59d1ce..3a52bbb5aa 100644 --- a/src/tests/libxrpl/peerfinder/PeerFinder.cpp +++ b/src/tests/libxrpl/peerfinder/PeerFinder.cpp @@ -38,7 +38,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace { using ::testing::_; @@ -51,10 +51,10 @@ journal() return beast::Journal{TestSink::instance()}; } -beast::IP::Endpoint +beast::ip::Endpoint endpoint(std::string const& value) { - return beast::IP::Endpoint::fromString(value); + return beast::ip::Endpoint::fromString(value); } class MockStore : public Store @@ -86,7 +86,7 @@ public: }; Store::Entry -storeEntry(beast::IP::Endpoint const& endpoint, int valence) +storeEntry(beast::ip::Endpoint const& endpoint, int valence) { Store::Entry entry; entry.endpoint = endpoint; @@ -106,15 +106,15 @@ class MockChecker public: MOCK_METHOD(void, stop, ()); MOCK_METHOD(void, wait, ()); - MOCK_METHOD(void, recordAsyncConnect, (beast::IP::Endpoint const& ep)); + MOCK_METHOD(void, recordAsyncConnect, (beast::ip::Endpoint const& ep)); boost::system::error_code nextError; bool completeAsync = true; - std::vector asyncConnects; + std::vector asyncConnects; template void - asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) + asyncConnect(beast::ip::Endpoint const& ep, Handler&& handler) { asyncConnects.push_back(ep); recordAsyncConnect(ep); @@ -204,7 +204,7 @@ protected: }; int -savedValence(std::vector const& entries, beast::IP::Endpoint const& endpoint) +savedValence(std::vector const& entries, beast::ip::Endpoint const& endpoint) { for (auto const& entry : entries) { @@ -458,7 +458,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) EXPECT_EQ(counts.outboundSlotsFree(), 1); EXPECT_EQ(counts.totalActive(), 0); EXPECT_FALSE(counts.isConnectedToNetwork()); - EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(counts.attemptsNeeded(), tuning::kMaxConnectAttempts); EXPECT_EQ(counts.stateString(), "0/1 out, 0/1 in, 0 connecting, 0 closing"); SlotImp inbound(endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); @@ -483,7 +483,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) counts.add(outbound); EXPECT_EQ(counts.attempts(), 1); EXPECT_EQ(counts.connectCount(), 1); - EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts - 1); + EXPECT_EQ(counts.attemptsNeeded(), tuning::kMaxConnectAttempts - 1); counts.remove(outbound); outbound.state(Slot::State::Connected); @@ -535,7 +535,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) Counts saturatedAttempts; saturatedAttempts.onConfig(config); std::vector> attempts; - for (int i = 0; i < Tuning::kMaxConnectAttempts; ++i) + for (int i = 0; i < tuning::kMaxConnectAttempts; ++i) { attempts.push_back( std::make_unique( @@ -544,7 +544,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) clock)); saturatedAttempts.add(*attempts.back()); } - EXPECT_EQ(saturatedAttempts.attempts(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(saturatedAttempts.attempts(), tuning::kMaxConnectAttempts); EXPECT_EQ(saturatedAttempts.attemptsNeeded(), 0u); Config disconnected; @@ -563,7 +563,7 @@ TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) EXPECT_EQ(redirects.slot(), slot); EXPECT_TRUE(redirects.list().empty()); EXPECT_FALSE(redirects.full()); - EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), tuning::kMaxHops + 1})); EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 0})); EXPECT_FALSE(redirects.tryInsert(Endpoint{remote.atPort(12000), 1})); EXPECT_TRUE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 1})); @@ -574,7 +574,7 @@ TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) EXPECT_EQ(slotHandouts.slot(), slot); EXPECT_FALSE(slotHandouts.full()); EXPECT_FALSE( - slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), Tuning::kMaxHops + 1})); + slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), tuning::kMaxHops + 1})); EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{remote.atPort(12001), 1})); auto const recent = endpoint("65.0.0.5:10005"); @@ -620,7 +620,7 @@ TEST(PeerFinderHandouts, distributes_livecache_entries) EXPECT_FALSE(targets.front().list().empty()); EXPECT_FALSE(targets.back().list().empty()); - for (std::uint32_t i = 0; i < Tuning::kNumberOfEndpoints; ++i) + for (std::uint32_t i = 0; i < tuning::kNumberOfEndpoints; ++i) targets.front().insert(Endpoint{endpoint("65.1.0." + std::to_string(i + 1) + ":12000"), 1}); handout(targets.begin(), targets.begin() + 1, cache.hops.begin(), cache.hops.end()); @@ -633,7 +633,7 @@ TEST_F(PeerFinderTest, preprocess_filters_invalid_duplicate_and_extra_self_endpo auto const remote = endpoint("65.0.0.2:10002"); auto const slot = std::make_shared(local, remote, false, clock_); Endpoints endpoints{ - Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1}, + Endpoint{endpoint("65.0.0.3:10003"), tuning::kMaxHops + 1}, Endpoint{endpoint("0.0.0.0:2459"), 0}, Endpoint{endpoint("0.0.0.0:2460"), 0}, Endpoint{endpoint("10.0.0.1:10004"), 1}, @@ -677,7 +677,7 @@ TEST_F(PeerFinderTest, on_endpoints_checks_neighbor_before_caching_it) EXPECT_TRUE(slot->canAccept); EXPECT_TRUE(logic_.livecache.empty()); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_EQ(logic_.livecache.size(), 1u); EXPECT_EQ(logic_.bootcache.size(), 1u); @@ -711,7 +711,7 @@ TEST_F(PeerFinderTest, on_endpoints_skips_failed_neighbor_connectivity_checks) EXPECT_TRUE(slot->checked); EXPECT_FALSE(slot->canAccept); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_TRUE(logic_.livecache.empty()); @@ -740,7 +740,7 @@ TEST_F(PeerFinderTest, on_endpoints_waits_for_pending_connectivity_check) logic_.onEndpoints(slot, advertised); EXPECT_TRUE(slot->connectivityCheckInProgress); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_EQ(checker_.asyncConnects.size(), 1u); EXPECT_TRUE(logic_.livecache.empty()); @@ -950,7 +950,7 @@ TEST(PeerFinderBootcache, periodic_activity_saves_after_cooldown) cache.periodicActivity(); EXPECT_TRUE(store.saves.empty()); - clock.advance(Tuning::kBootcacheCooldownTime + 1s); + clock.advance(tuning::kBootcacheCooldownTime + 1s); cache.periodicActivity(); ASSERT_EQ(store.saves.size(), 1u); @@ -967,23 +967,23 @@ TEST(PeerFinderBootcache, prunes_when_cache_exceeds_limit) TestStopwatch clock; Bootcache cache(store, clock, journal()); - for (std::uint16_t i = 0; i <= Tuning::kBootcacheSize; ++i) + for (std::uint16_t i = 0; i <= tuning::kBootcacheSize; ++i) { EXPECT_TRUE(cache.insert(endpoint( "65.0." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256) + ":" + std::to_string(10000 + i)))); } - EXPECT_LE(cache.size(), Tuning::kBootcacheSize); + EXPECT_LE(cache.size(), tuning::kBootcacheSize); } TEST(PeerFinderEndpoint, clamps_hops_to_overflow_bucket) { auto const address = endpoint("65.0.0.1:10001"); - Endpoint const ep(address, Tuning::kMaxHops + 10); + Endpoint const ep(address, tuning::kMaxHops + 10); EXPECT_EQ(ep.address, address); - EXPECT_EQ(ep.hops, Tuning::kMaxHops + 1); + EXPECT_EQ(ep.hops, tuning::kMaxHops + 1); } TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) @@ -1001,7 +1001,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) EXPECT_FALSE(inbound.reserved()); EXPECT_EQ(inbound.state(), State::Accept); EXPECT_EQ(inbound.remoteEndpoint(), remote); - EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); + EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); EXPECT_FALSE(inbound.publicKey()); EXPECT_FALSE(inbound.listeningPort()); EXPECT_FALSE(inbound.checked); @@ -1018,7 +1018,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) inbound.reserved(true); inbound.setListeningPort(2459); - EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); + EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); EXPECT_EQ(inbound.remoteEndpoint(), newRemote); EXPECT_EQ(inbound.publicKey(), std::optional{publicKey}); EXPECT_TRUE(inbound.reserved()); @@ -1055,7 +1055,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) EXPECT_TRUE(outbound.recent.filter(recent, 1)); EXPECT_FALSE(outbound.recent.filter(recent, 0)); - clock.advance(Tuning::kLiveCacheSecondsToLive + 1s); + clock.advance(tuning::kLiveCacheSecondsToLive + 1s); outbound.expire(); EXPECT_FALSE(outbound.recent.filter(recent, 1)); } @@ -1112,7 +1112,7 @@ TEST(PeerFinderConfig, calculates_outbound_peers_and_clamps_ip_limits) { Config config; config.maxPeers = 1; - EXPECT_EQ(config.calcOutPeers(), Tuning::kMinOutCount); + EXPECT_EQ(config.calcOutPeers(), tuning::kMinOutCount); config.maxPeers = 100; EXPECT_EQ(config.calcOutPeers(), 15u); @@ -1267,4 +1267,4 @@ TEST(PeerFinderConfig, rejects_incomplete_or_out_of_range_peer_limits) } } // namespace -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/tests/libxrpl/protocol/ApiVersion.cpp b/src/tests/libxrpl/protocol/ApiVersion.cpp index 8af7787102..5bb6a158cf 100644 --- a/src/tests/libxrpl/protocol/ApiVersion.cpp +++ b/src/tests/libxrpl/protocol/ApiVersion.cpp @@ -6,21 +6,21 @@ using namespace xrpl; TEST(ApiVersion, invariants) { - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumSupportedVersion); - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiMaximumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiBetaVersion <= RPC::kApiMaximumValidVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= rpc::kApiMaximumSupportedVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiMaximumSupportedVersion <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiBetaVersion <= rpc::kApiMaximumValidVersion); } // Update when we change versions TEST(ApiVersion, versions) { - static_assert(RPC::kApiMinimumSupportedVersion >= 1); - static_assert(RPC::kApiMinimumSupportedVersion < 2); - static_assert(RPC::kApiMaximumSupportedVersion >= 2); - static_assert(RPC::kApiMaximumSupportedVersion < 3); - static_assert(RPC::kApiMaximumValidVersion >= 3); - static_assert(RPC::kApiMaximumValidVersion < 4); - static_assert(RPC::kApiBetaVersion >= 3); - static_assert(RPC::kApiBetaVersion < 4); + static_assert(rpc::kApiMinimumSupportedVersion >= 1); + static_assert(rpc::kApiMinimumSupportedVersion < 2); + static_assert(rpc::kApiMaximumSupportedVersion >= 2); + static_assert(rpc::kApiMaximumSupportedVersion < 3); + static_assert(rpc::kApiMaximumValidVersion >= 3); + static_assert(rpc::kApiMaximumValidVersion < 4); + static_assert(rpc::kApiBetaVersion >= 3); + static_assert(rpc::kApiBetaVersion < 4); } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp index 2697924d37..f55d01f606 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp @@ -35,6 +35,7 @@ TEST(VaultTests, BuilderSettersRoundTrip) auto const shareMPTIDValue = canonical_UINT192(); auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); + auto const lEVersionValue = canonical_UINT8(); VaultBuilder builder{ previousTxnIDValue, @@ -54,6 +55,7 @@ TEST(VaultTests, BuilderSettersRoundTrip) builder.setAssetsMaximum(assetsMaximumValue); builder.setLossUnrealized(lossUnrealizedValue); builder.setScale(scaleValue); + builder.setLEVersion(lEVersionValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -166,6 +168,14 @@ TEST(VaultTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasScale()); } + { + auto const& expected = lEVersionValue; + auto const actualOpt = entry.getLEVersion(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLEVersion"); + EXPECT_TRUE(entry.hasLEVersion()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -194,6 +204,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip) auto const shareMPTIDValue = canonical_UINT192(); auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); + auto const lEVersionValue = canonical_UINT8(); auto sle = std::make_shared(Vault::entryType, index); @@ -212,6 +223,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip) sle->at(sfShareMPTID) = shareMPTIDValue; sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue; sle->at(sfScale) = scaleValue; + sle->at(sfLEVersion) = lEVersionValue; VaultBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -390,6 +402,19 @@ TEST(VaultTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfScale"); } + { + auto const& expected = lEVersionValue; + + auto const fromSleOpt = entryFromSle.getLEVersion(); + auto const fromBuilderOpt = entryFromBuilder.getLEVersion(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLEVersion"); + expectEqualField(expected, *fromBuilderOpt, "sfLEVersion"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -472,5 +497,7 @@ TEST(VaultTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getLossUnrealized().has_value()); EXPECT_FALSE(entry.hasScale()); EXPECT_FALSE(entry.getScale().has_value()); + EXPECT_FALSE(entry.hasLEVersion()); + EXPECT_FALSE(entry.getLEVersion().has_value()); } } diff --git a/src/tests/libxrpl/resource/Logic.cpp b/src/tests/libxrpl/resource/Logic.cpp index 1f935ebf4b..b38ca2e051 100644 --- a/src/tests/libxrpl/resource/Logic.cpp +++ b/src/tests/libxrpl/resource/Logic.cpp @@ -17,11 +17,14 @@ #include #include +#include #include #include +#include #include +#include -namespace xrpl::Resource { +namespace xrpl::resource { class ResourceManagerTest : public ::testing::Test { @@ -54,9 +57,10 @@ protected: //-------------------------------------------------------------------------- - static void - populateGossip(Gossip& gossip) + static Gossip + makeGossip() { + Gossip gossip; std::uint8_t const v(10 + randInt(9)); std::uint8_t const n(10 + randInt(9)); gossip.items.reserve(n); @@ -64,15 +68,16 @@ protected: { Gossip::Item item; item.balance = 100 + randInt(499); - beast::IP::AddressV4::bytes_type const d = {{ + beast::ip::AddressV4::bytes_type const d = {{ 192, 0, 2, static_cast(v + i), }}; - item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; - gossip.items.push_back(item); + item.address = beast::ip::Endpoint{beast::ip::AddressV4{d}}; + gossip.items.push_back(std::move(item)); } + return gossip; } }; @@ -81,14 +86,14 @@ TEST_F(ResourceManagerTest, limited_warn_drop) TestLogic logic{j_}; Charge const fee{kDropThreshold + 1}; - beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const addr{beast::ip::Endpoint::fromString("192.0.2.2")}; { Consumer c{logic.newInboundEndpoint(addr)}; // Create load until we get a warning - int n = 10000; - bool warned = false; + auto n = 10000; + auto warned = false; while (--n >= 0) { @@ -97,7 +102,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) warned = true; break; } - ++logic.clock(); + logic.advance(); } ASSERT_TRUE(warned) << "Loop count exceeded without warning"; @@ -113,7 +118,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) EXPECT_TRUE(c.disconnect(j_)); break; } - ++logic.clock(); + logic.advance(); } ASSERT_TRUE(dropped) << "Loop count exceeded without dropping"; @@ -135,7 +140,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) auto n = kSecondsUntilExpiration + 1s; while (--n > 0s) { - ++logic.clock(); + logic.advance(); logic.periodicActivity(); Consumer const c{logic.newInboundEndpoint(addr)}; if (c.disposition() != Disposition::Drop) @@ -153,7 +158,7 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) TestLogic logic{j_}; Charge const fee{kDropThreshold + 1}; - beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const addr{beast::ip::Endpoint::fromString("192.0.2.2")}; Consumer c{logic.newUnlimitedEndpoint(addr)}; // Create load until we get a warning @@ -167,7 +172,7 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) warned = true; break; } - ++logic.clock(); + logic.advance(); } EXPECT_FALSE(warned) << "Should loop forever with no warning"; @@ -175,15 +180,17 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) TEST_F(ResourceManagerTest, charges) { + static constexpr auto kDecayTicks = 128uz; + TestLogic logic{j_}; { - beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.1")}; + beast::ip::Endpoint const address{beast::ip::Endpoint::fromString("192.0.2.1")}; Consumer c{logic.newInboundEndpoint(address)}; Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; c.charge(fee); - for (int i = 0; i < 128; ++i) + for (auto tick = 0uz; tick < kDecayTicks; ++tick) { JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count() << ", Balance = " << c.balance(); @@ -192,11 +199,11 @@ TEST_F(ResourceManagerTest, charges) } { - beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const address{beast::ip::Endpoint::fromString("192.0.2.2")}; Consumer c{logic.newInboundEndpoint(address)}; Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; - for (int i = 0; i < 128; ++i) + for (auto tick = 0uz; tick < kDecayTicks; ++tick) { c.charge(fee); JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count() @@ -210,13 +217,10 @@ TEST_F(ResourceManagerTest, imports) { TestLogic logic{j_}; - Gossip g[5]; - - for (auto& i : g) - populateGossip(i); - - for (int i = 0; i < 5; ++i) - logic.importConsumers(std::to_string(i), g[i]); + static constexpr auto kGossipSources = 5uz; + std::ranges::for_each(std::views::iota(0uz, kGossipSources), [&](auto const i) { + logic.importConsumers(std::to_string(i), makeGossip()); + }); } TEST_F(ResourceManagerTest, import) @@ -226,16 +230,16 @@ TEST_F(ResourceManagerTest, import) Gossip g; Gossip::Item item; item.balance = 100; - beast::IP::AddressV4::bytes_type const d = {{ + beast::ip::AddressV4::bytes_type const d = {{ 192, 0, 2, 1, }}; - item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; - g.items.push_back(item); + item.address = beast::ip::Endpoint{beast::ip::AddressV4{d}}; + g.items.push_back(std::move(item)); logic.importConsumers("g", g); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/tests/libxrpl/shamap/SHAMap.cpp b/src/tests/libxrpl/shamap/SHAMap.cpp index 238c34bf9c..e662e16be4 100644 --- a/src/tests/libxrpl/shamap/SHAMap.cpp +++ b/src/tests/libxrpl/shamap/SHAMap.cpp @@ -257,12 +257,9 @@ TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate) map.invariants(); } - int h = 7; + auto keyIndex = kKeys.size(); for (auto const& k : map) - { - EXPECT_EQ(k.key(), kKeys[h]); - --h; - } + EXPECT_EQ(k.key(), kKeys[--keyIndex]); } } @@ -288,7 +285,11 @@ TEST_F(SHAMapPathProof, verify_proof_path) uint256 rootHash; std::vector goodPath; - for (unsigned char c = 1; c < 100; ++c) + static constexpr unsigned char kFirstKey = 1; + static constexpr unsigned char kKeyCount = 100; + static constexpr unsigned char kLastKey = kKeyCount - 1; + + for (unsigned char c = kFirstKey; c < kKeyCount; ++c) { uint256 k(c); map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()})); @@ -304,7 +305,7 @@ TEST_F(SHAMapPathProof, verify_proof_path) auto& proofPath = *path; EXPECT_TRUE(map.verifyProofPath(root, k, proofPath)); - if (c == 1) + if (c == kFirstKey) { // extra node proofPath.insert(proofPath.begin(), proofPath.front()); @@ -313,7 +314,7 @@ TEST_F(SHAMapPathProof, verify_proof_path) uint256 const wrongKey(c + 1); EXPECT_FALSE(map.getProofPath(wrongKey)); } - if (c == 99) + if (c == kLastKey) { key = k; rootHash = root; diff --git a/src/tests/libxrpl/shamap/SHAMapSync.cpp b/src/tests/libxrpl/shamap/SHAMapSync.cpp index 5cefbae8a1..e4bcbd8970 100644 --- a/src/tests/libxrpl/shamap/SHAMapSync.cpp +++ b/src/tests/libxrpl/shamap/SHAMapSync.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -18,6 +17,7 @@ #include #include +#include #include #include #include @@ -34,15 +34,17 @@ protected: boost::intrusive_ptr makeRandomAS() { + static constexpr auto kWordsPerState = 3uz; + Serializer s; - for (int d = 0; d < 3; ++d) + for (auto word = 0uz; word < kWordsPerState; ++word) s.add32(randInt(eng_)); return makeShamapitem(s.getSHA512Half(), s.slice()); } bool - confuseMap(SHAMap& map, int count) + confuseMap(SHAMap& map, std::size_t count) { // add a bunch of random states to a map, then remove them // map should be the same @@ -50,7 +52,7 @@ protected: std::list items; - for (int i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) { auto item = makeRandomAS(); items.push_back(item->key()); @@ -87,39 +89,45 @@ TEST_F(SHAMapSyncTest, sync) SHAMap source{SHAMapType::FREE, f}; SHAMap destination{SHAMapType::FREE, f2}; - int const items = 10000; - for (int i = 0; i < items; ++i) + static constexpr auto kItemCount = 10000uz; + static constexpr auto kInvariantInterval = 100uz; + static constexpr auto kNodesToConfuse = 500uz; + static constexpr auto kMaxNodesPerRequest = 2048; + + for (auto i = 0uz; i < kItemCount; ++i) { source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS()); - if (i % 100 == 0) + if (i % kInvariantInterval == 0) source.invariants(); } source.invariants(); - ASSERT_TRUE(confuseMap(source, 500)); + ASSERT_TRUE(confuseMap(source, kNodesToConfuse)); source.invariants(); source.setImmutable(); - int count = 0; + std::size_t count = 0; source.visitLeaves([&count]([[maybe_unused]] auto const& item) { ++count; }); - EXPECT_EQ(count, items); + EXPECT_EQ(count, kItemCount); std::vector missingNodes; - source.walkMap(missingNodes, 2048); + source.walkMap(missingNodes, kMaxNodesPerRequest); EXPECT_TRUE(missingNodes.empty()); destination.setSynching(); { - std::vector> a; + std::vector a; ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2))); ASSERT_FALSE(a.empty()) << "NodeSize"; - ASSERT_TRUE( - destination.addRootNode(source.getHash(), makeSlice(a[0].second), nullptr).isGood()); + auto node = SHAMapTreeNode::makeFromWire(makeSlice(a[0].data)); + if (!node) + FAIL() << "Could not create node"; + ASSERT_TRUE(destination.addRootNode(source.getHash(), std::move(node), nullptr).isGood()); } do @@ -127,13 +135,13 @@ TEST_F(SHAMapSyncTest, sync) f.clock().advance(std::chrono::seconds(1)); // get the list of nodes we know we need - auto nodesMissing = destination.getMissingNodes(2048, nullptr); + auto nodesMissing = destination.getMissingNodes(kMaxNodesPerRequest, nullptr); if (nodesMissing.empty()) break; // get as many nodes as possible based on this information - std::vector> b; + std::vector b; for (auto& it : nodesMissing) { @@ -155,7 +163,12 @@ TEST_F(SHAMapSyncTest, sync) // Keep failures fatal here because this loop is data-dependent. // non-deterministic number of times and the number of tests run // should be deterministic - if (!destination.addKnownNode(i.first, makeSlice(i.second), nullptr).isUseful()) + auto node = SHAMapTreeNode::makeFromWire(makeSlice(i.data)); + if (!node) + FAIL() << "Could not create node"; + if (i.isLeaf != node->isLeaf()) + FAIL() << "Node is not a leaf"; + if (!destination.addKnownNode(i.nodeID, std::move(node), nullptr).isUseful()) FAIL() << "Known node was not useful"; } } while (true); diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index 20421ab916..28b910c8e5 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -847,7 +847,7 @@ RCLConsensus::Adaptor::validate(RCLCxLedger const& ledger, RCLTxSet const& txns, // Report our server version every flag ledger: if (ledger.ledger->isVotingLedger()) - v.setFieldU64(sfServerVersion, BuildInfo::getEncodedVersion()); + v.setFieldU64(sfServerVersion, build_info::getEncodedVersion()); // Report our load { diff --git a/src/xrpld/app/ledger/InboundLedger.h b/src/xrpld/app/ledger/InboundLedger.h index 31ca4169ce..9a7ee510f6 100644 --- a/src/xrpld/app/ledger/InboundLedger.h +++ b/src/xrpld/app/ledger/InboundLedger.h @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -24,7 +23,7 @@ #include #include #include -#include +#include #include #include @@ -154,16 +153,19 @@ private: processData(std::shared_ptr peer, protocol::TMLedgerData const& data); bool - takeHeader(std::string const& data); + takeHeader(std::string_view data); void - receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode&); + receiveNode( + std::shared_ptr const& peer, + protocol::TMLedgerData const& packet, + SHAMapAddNode& san); bool - takeTxRootNode(Slice const& data, SHAMapAddNode&); + takeTxRootNode(std::string_view data, SHAMapAddNode& san); bool - takeAsRootNode(Slice const& data, SHAMapAddNode&); + takeAsRootNode(std::string_view data, SHAMapAddNode& san); std::vector neededTxHashes(int max, SHAMapSyncFilter const* filter) const; diff --git a/src/xrpld/app/ledger/LedgerNodeHelpers.h b/src/xrpld/app/ledger/LedgerNodeHelpers.h new file mode 100644 index 0000000000..9df9ab06c7 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerNodeHelpers.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include + +namespace protocol { +class TMLedgerNode; +} // namespace protocol + +namespace xrpl { + +/** + * @brief Deserializes a SHAMapTreeNode from wire format data. + * + * This function attempts to create a SHAMapTreeNode from the provided data string. If the data is + * malformed or deserialization fails, the function returns a nullptr instead of throwing an + * exception. + * + * @param data The serialized node data in wire format. + * @return The deserialized tree node if successful, or a nullptr if deserialization fails. + */ +[[nodiscard]] SHAMapTreeNodePtr +getTreeNode(std::string_view data); + +/** + * @brief Extracts or reconstructs the SHAMapNodeID from a ledger node proto message. + * + * This function retrieves the SHAMapNodeID for a tree node, with behavior that depends on which + * field is set and the node type (inner vs. leaf). + * + * When the legacy `nodeid` field is set in the message: + * - For all nodes: Deserializes the node ID from the field. + * - For leaf nodes: Validates that the node ID is consistent with the leaf's key. + * + * When the new `id` or `depth` field is set in the message: + * - For inner nodes: Deserializes the node ID from the `id` field. + * - For leaf nodes: Reconstructs the node ID using both the depth from the `depth` field and the + * key from the leaf node's item. + * Note that root nodes may be inner nodes or leaf nodes. + * + * @param ledgerNode The validated protocol message containing the ledger node data. + * @param treeNode The deserialized tree node (inner or leaf node). + * @return An optional containing the node ID if extraction/reconstruction succeeds, or std::nullopt + * if the required fields are missing or validation fails. + */ +[[nodiscard]] std::optional +getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode); + +} // namespace xrpl diff --git a/src/xrpld/app/ledger/LedgerReplayer.h b/src/xrpld/app/ledger/LedgerReplayer.h index 6feb187df6..b2806ee813 100644 --- a/src/xrpld/app/ledger/LedgerReplayer.h +++ b/src/xrpld/app/ledger/LedgerReplayer.h @@ -24,7 +24,7 @@ namespace test { class LedgerReplayClient; } // namespace test -namespace LedgerReplayParameters { +namespace ledger_replay_parameters { // timeout value for LedgerReplayTask constexpr auto kTaskTimeout = std::chrono::milliseconds{500}; @@ -53,7 +53,7 @@ constexpr std::uint32_t kMaxTaskSize = 256; // to limit the number of LedgerReplay related jobs in JobQueue constexpr std::uint32_t kMaxQueuedTasks = 100; -} // namespace LedgerReplayParameters +} // namespace ledger_replay_parameters /** * Manages the lifetime of ledger replay tasks. diff --git a/src/xrpld/app/ledger/LedgerToJson.h b/src/xrpld/app/ledger/LedgerToJson.h index 1eac4d68f1..e1172e897a 100644 --- a/src/xrpld/app/ledger/LedgerToJson.h +++ b/src/xrpld/app/ledger/LedgerToJson.h @@ -18,7 +18,7 @@ struct LedgerFill { LedgerFill( ReadView const& l, - RPC::Context const* ctx, + rpc::Context const* ctx, int o = 0, std::vector q = {}) : ledger(l), options(o), txQueue(std::move(q)), context(ctx) @@ -40,7 +40,7 @@ struct LedgerFill ReadView const& ledger; int options; std::vector txQueue; - RPC::Context const* context; + rpc::Context const* context; std::optional closeTime; }; diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 55a2a9d283..246c7d567b 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -44,8 +45,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -779,7 +780,7 @@ InboundLedger::filterNodes( */ // data must not have hash prefix bool -InboundLedger::takeHeader(std::string const& data) +InboundLedger::takeHeader(std::string_view data) { // Return value: true=normal, false=bad data JLOG(journal_.trace()) << "got header acquiring ledger " << hash_; @@ -825,7 +826,10 @@ InboundLedger::takeHeader(std::string const& data) * Call with a lock */ void -InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& san) +InboundLedger::receiveNode( + std::shared_ptr const& peer, + protocol::TMLedgerData const& packet, + SHAMapAddNode& san) { if (!haveHeader_) { @@ -868,32 +872,47 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& { auto const f = filter.get(); - for (auto const& node : packet.nodes()) + for (auto const& ledgerNode : packet.nodes()) { - auto const nodeID = deserializeSHAMapNodeID(node.nodeid()); + auto treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) + { + JLOG(journal_.warn()) + << "Got invalid node data for ledger " << hash_ << " from peer " << peer->id(); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_data invalid"); + san.incInvalid(); + return; + } + auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode); if (!nodeID) - throw std::runtime_error("data does not properly deserialize"); - - if (nodeID->isRoot()) { - san += map.addRootNode(rootHash, makeSlice(node.nodedata()), f); - } - else - { - san += map.addKnownNode(*nodeID, makeSlice(node.nodedata()), f); + JLOG(journal_.warn()) + << "Got invalid node id for ledger " << hash_ << " from peer " << peer->id(); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_id invalid"); + san.incInvalid(); + return; } - if (!san.isGood()) + auto const result = nodeID->isRoot() + ? map.addRootNode(rootHash, std::move(treeNode), f) + : map.addKnownNode(*nodeID, std::move(treeNode), f); + san += result; + + if (result.isInvalid()) { - JLOG(journal_.warn()) << "Received bad node data"; + JLOG(journal_.warn()) << "Got invalid node " << *nodeID << " for ledger " << hash_ + << " from peer " << peer->id(); + peer->charge(resource::kFeeInvalidData, "ledger_node invalid"); return; } } } catch (std::exception const& e) { - JLOG(journal_.error()) << "Received bad node data: " << e.what(); + // If we get here it is not necessarily because the node was bad, so don't charge the peer. + JLOG(journal_.error()) << "Could not process node for ledger " << hash_ << " from peer " + << peer->id() << ": " << e.what(); san.incInvalid(); return; } @@ -922,7 +941,7 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& * Call with a lock */ bool -InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) +InboundLedger::takeAsRootNode(std::string_view data, SHAMapAddNode& san) { if (failed_ || haveState_) { @@ -938,10 +957,19 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) // LCOV_EXCL_STOP } + auto treeNode = getTreeNode(data); + if (!treeNode) + { + JLOG(journal_.warn()) << "Got invalid AS root node data for ledger " << hash_; + san.incInvalid(); + return false; + } + AccountStateSF filter(ledger_->stateMap().family().db(), app_.getLedgerMaster()); - san += - ledger_->stateMap().addRootNode(SHAMapHash{ledger_->header().accountHash}, data, &filter); - return san.isGood(); + auto const result = ledger_->stateMap().addRootNode( + SHAMapHash{ledger_->header().accountHash}, std::move(treeNode), &filter); + san += result; + return !result.isInvalid(); } /** @@ -949,7 +977,7 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) * Call with a lock */ bool -InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) +InboundLedger::takeTxRootNode(std::string_view data, SHAMapAddNode& san) { if (failed_ || haveTransactions_) { @@ -965,9 +993,19 @@ InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) // LCOV_EXCL_STOP } + auto treeNode = getTreeNode(data); + if (!treeNode) + { + JLOG(journal_.warn()) << "Got invalid TX root node data for ledger " << hash_; + san.incInvalid(); + return false; + } + TransactionStateSF filter(ledger_->txMap().family().db(), app_.getLedgerMaster()); - san += ledger_->txMap().addRootNode(SHAMapHash{ledger_->header().txHash}, data, &filter); - return san.isGood(); + auto const result = ledger_->txMap().addRootNode( + SHAMapHash{ledger_->header().txHash}, std::move(treeNode), &filter); + san += result; + return !result.isInvalid(); } std::vector @@ -1030,7 +1068,7 @@ InboundLedger::gotData( * Returns the number of useful nodes */ // VFALCO NOTE, it is not necessary to pass the entire Peer, -// we can get away with just a Resource::Consumer endpoint. +// we can get away with just a resource::Consumer endpoint. // // TODO Change peer to Consumer // @@ -1042,7 +1080,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (packet.nodes().empty()) { JLOG(journal_.warn()) << peer->id() << ": empty header data"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data empty header"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data empty header"); return -1; } @@ -1057,7 +1095,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (!takeHeader(packet.nodes(0).nodedata())) { JLOG(journal_.warn()) << "Got invalid header data"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data invalid header"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data invalid header"); return -1; } @@ -1065,22 +1103,35 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co } if (!haveState_ && (packet.nodes().size() > 1) && - !takeAsRootNode(makeSlice(packet.nodes(1).nodedata()), san)) + !takeAsRootNode(packet.nodes(1).nodedata(), san)) { - JLOG(journal_.warn()) << "Included AS root invalid"; + JLOG(journal_.warn()) << "Included AS root invalid for ledger " << hash_ + << " from peer " << peer->id(); + if (san.isInvalid()) + { + peer->charge(resource::kFeeInvalidData, "ledger_data invalid AS root"); + return -1; + } } if (!haveTransactions_ && (packet.nodes().size() > 2) && - !takeTxRootNode(makeSlice(packet.nodes(2).nodedata()), san)) + !takeTxRootNode(packet.nodes(2).nodedata(), san)) { - JLOG(journal_.warn()) << "Included TX root invalid"; + JLOG(journal_.warn()) << "Included TX root invalid for ledger " << hash_ + << " from peer " << peer->id(); + if (san.isInvalid()) + { + peer->charge(resource::kFeeInvalidData, "ledger_data invalid TX root"); + return -1; + } } } catch (std::exception const& ex) { - JLOG(journal_.warn()) << "Included AS/TX root invalid: " << ex.what(); + JLOG(journal_.warn()) << "Included AS/TX root invalid for ledger " << hash_ + << " from peer " << peer->id() << ": " << ex.what(); using namespace std::string_literals; - peer->charge(Resource::kFeeInvalidData, "ledger_data "s + ex.what()); + peer->charge(resource::kFeeInvalidData, "ledger_data "s + ex.what()); return -1; } @@ -1096,30 +1147,24 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (packet.nodes().empty()) { JLOG(journal_.info()) << peer->id() << ": response with no nodes"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data no nodes"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data no nodes"); return -1; } ScopedLockType const sl(mtx_); - // Verify node IDs and data are complete - for (auto const& node : packet.nodes()) - { - if (!node.has_nodeid() || !node.has_nodedata()) - { - JLOG(journal_.warn()) << "Got bad node"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data bad node"); - return -1; - } - } - SHAMapAddNode san; - receiveNode(packet, san); + receiveNode(peer, packet, san); JLOG(journal_.debug()) << "Ledger " << ((packet.type() == protocol::liTX_NODE) ? "TX" : "AS") << " node stats: " << san.get(); + // `san` accumulates across the whole packet, so `isInvalid()` (bad_ > 0) does not mean the + // packet had no useful nodes: credit whatever good/useful nodes were sent rather than + // discarding everything because one node in an otherwise-good packet was bad. + // Note: Peer charges for invalid/malformed data are issued from within receiveNode at the + // exact failure site, so the peer is only charged for problems they are responsible for. if (san.isUseful()) progress_ = true; diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index dc361694cf..4d565ca674 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -2,13 +2,13 @@ #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -252,23 +252,17 @@ public: Serializer s; try { - for (int i = 0; i < packetPtr->nodes().size(); ++i) + for (auto const& ledgerNode : packetPtr->nodes()) { - auto const& node = packetPtr->nodes(i); - - if (!node.has_nodeid() || !node.has_nodedata()) - return; - - auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata())); - - if (!newNode) + auto const treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) return; s.erase(); - newNode->serializeWithPrefix(s); + treeNode->serializeWithPrefix(s); app_.getLedgerMaster().addFetchPack( - newNode->getHash().asUInt256(), std::make_shared(s.begin(), s.end())); + treeNode->getHash().asUInt256(), std::make_shared(s.begin(), s.end())); } } catch (std::exception const&) // NOLINT(bugprone-empty-catch) diff --git a/src/xrpld/app/ledger/detail/InboundTransactions.cpp b/src/xrpld/app/ledger/detail/InboundTransactions.cpp index 9b50a1584f..62897fe617 100644 --- a/src/xrpld/app/ledger/detail/InboundTransactions.cpp +++ b/src/xrpld/app/ledger/detail/InboundTransactions.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include -#include #include #include #include @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -137,34 +138,45 @@ public: if (ta == nullptr) { - peer->charge(Resource::kFeeUselessData, "ledger_data"); + peer->charge(resource::kFeeUselessData, "ledger_data useless"); return; } - std::vector> data; + std::vector> data; data.reserve(packet.nodes().size()); - for (auto const& node : packet.nodes()) + for (auto const& ledgerNode : packet.nodes()) { - if (!node.has_nodeid() || !node.has_nodedata()) + auto treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) { - peer->charge(Resource::kFeeMalformedRequest, "ledger_data"); + JLOG(j_.warn()) << "Got invalid node data for TX set " << hash << " from peer " + << peer->id(); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_data invalid"); return; } - auto const id = deserializeSHAMapNodeID(node.nodeid()); - - if (!id) + auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode); + if (!nodeID) { - peer->charge(Resource::kFeeInvalidData, "ledger_data"); + JLOG(j_.warn()) << "Got invalid node id for TX set " << hash << " from peer " + << peer->id(); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_id invalid"); return; } - data.emplace_back(*id, makeSlice(node.nodedata())); + data.emplace_back(*nodeID, std::move(treeNode)); } - if (!ta->takeNodes(data, peer).isUseful()) - peer->charge(Resource::kFeeUselessData, "ledger_data not useful"); + auto const san = ta->takeNodes(std::move(data), peer); + if (san.isInvalid()) + { + peer->charge(resource::kFeeInvalidData, "ledger_data invalid"); + } + else if (!san.isUseful()) + { + peer->charge(resource::kFeeUselessData, "ledger_data useless"); + } } void diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp index 7ac85b892e..344d5cb8fc 100644 --- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp @@ -43,10 +43,10 @@ LedgerDeltaAcquire::LedgerDeltaAcquire( : TimeoutCounter( app, ledgerHash, - LedgerReplayParameters::kSubTaskTimeout, + ledger_replay_parameters::kSubTaskTimeout, {.jobType = JtReplayTask, .jobName = "LedReplDelta", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplayDelta")) , inboundLedgers_(inboundLedgers) , ledgerSeq_(ledgerSeq) @@ -101,10 +101,10 @@ LedgerDeltaAcquire::trigger(std::size_t limit, ScopedLockType& sl) } else { - if (++noFeaturePeerCount_ >= LedgerReplayParameters::kMaxNoFeaturePeerCount) + if (++noFeaturePeerCount_ >= ledger_replay_parameters::kMaxNoFeaturePeerCount) { JLOG(journal_.debug()) << "Fall back for " << hash_; - timerInterval_ = LedgerReplayParameters::kSubTaskFallbackTimeout; + timerInterval_ = ledger_replay_parameters::kSubTaskFallbackTimeout; fallBack_ = true; } } @@ -119,7 +119,7 @@ void LedgerDeltaAcquire::onTimer(bool progress, ScopedLockType& sl) { JLOG(journal_.trace()) << "timeouts_=" << timeouts_ << " for " << hash_; - if (timeouts_ > LedgerReplayParameters::kSubTaskMaxTimeouts) + if (timeouts_ > ledger_replay_parameters::kSubTaskMaxTimeouts) { failed_ = true; JLOG(journal_.debug()) << "too many timeouts " << hash_; diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 9a0335fc3c..83d76bcd2a 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1042,8 +1042,8 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) if (v->isFieldPresent(sfServerVersion)) { auto version = v->getFieldU64(sfServerVersion); - higherVersionCount += BuildInfo::isNewerVersion(version) ? 1 : 0; - xrpldCount += BuildInfo::isXrpldVersion(version) ? 1 : 0; + higherVersionCount += build_info::isNewerVersion(version) ? 1 : 0; + xrpldCount += build_info::isXrpldVersion(version) ? 1 : 0; } } // We report only if (1) we have accumulated validation messages @@ -2088,21 +2088,21 @@ LedgerMaster::makeFetchPack( if (!have) { JLOG(journal_.info()) << "Peer requests fetch pack for ledger we don't have: " << have; - peer->charge(Resource::kFeeRequestNoReply, "get_object ledger"); + peer->charge(resource::kFeeRequestNoReply, "get_object ledger"); return; } if (have->open()) { JLOG(journal_.warn()) << "Peer requests fetch pack from open ledger: " << have; - peer->charge(Resource::kFeeMalformedRequest, "get_object ledger open"); + peer->charge(resource::kFeeMalformedRequest, "get_object ledger open"); return; } if (have->header().seq < getEarliestFetch()) { JLOG(journal_.debug()) << "Peer requests fetch pack that is too early"; - peer->charge(Resource::kFeeMalformedRequest, "get_object ledger early"); + peer->charge(resource::kFeeMalformedRequest, "get_object ledger early"); return; } @@ -2112,7 +2112,7 @@ LedgerMaster::makeFetchPack( { JLOG(journal_.info()) << "Peer requests fetch pack for ledger whose predecessor we " << "don't have: " << have; - peer->charge(Resource::kFeeRequestNoReply, "get_object ledger no parent"); + peer->charge(resource::kFeeRequestNoReply, "get_object ledger no parent"); return; } diff --git a/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp new file mode 100644 index 0000000000..531dba59f9 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl { + +SHAMapTreeNodePtr +getTreeNode(std::string_view data) +{ + auto const slice = makeSlice(data); + try + { + return SHAMapTreeNode::makeFromWire(slice); + } + catch (std::exception const&) + { + return {}; + } +} + +std::optional +getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode) +{ + if (ledgerNode.has_id() || ledgerNode.has_depth()) + { + // Reject ambiguous messages that mix the legacy and new reference fields. + if (ledgerNode.has_nodeid()) + return std::nullopt; + + if (treeNode.isInner()) + { + if (!ledgerNode.has_id()) + return std::nullopt; + + REACHABLE("xrpl::getSHAMapNodeID : inner node ID from id field"); + return deserializeSHAMapNodeID(ledgerNode.id()); + } + + if (treeNode.isLeaf()) + { + SOMETIMES( + ledgerNode.has_depth() && ledgerNode.depth() > SHAMap::kLeafDepth, + "xrpl::getSHAMapNodeID : leaf depth exceeds max"); + if (!ledgerNode.has_depth() || ledgerNode.depth() > SHAMap::kLeafDepth) + return std::nullopt; + + auto const key = leafKey(treeNode); + REACHABLE("xrpl::getSHAMapNodeID : leaf node ID reconstructed from depth"); + return SHAMapNodeID::createID(ledgerNode.depth(), key); + } + // LCOV_EXCL_START + UNREACHABLE("xrpl::getSHAMapNodeID : tree node is neither inner nor leaf"); + return std::nullopt; + // LCOV_EXCL_STOP + } + + if (!ledgerNode.has_nodeid()) + return std::nullopt; + + auto nodeID = deserializeSHAMapNodeID(ledgerNode.nodeid()); + if (!nodeID.has_value()) + return std::nullopt; + + if (treeNode.isLeaf()) + { + auto const key = leafKey(treeNode); + auto const expectedID = SHAMapNodeID::createID(static_cast(nodeID->getDepth()), key); + SOMETIMES( + nodeID->getNodeID() != expectedID.getNodeID(), + "xrpl::getSHAMapNodeID : legacy leaf ID inconsistent with key"); + if (nodeID->getNodeID() != expectedID.getNodeID()) + return std::nullopt; + } + + return nodeID; +} + +} // namespace xrpl diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp index e7cd031247..3d7b1e0f92 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp @@ -87,18 +87,18 @@ LedgerReplayTask::LedgerReplayTask( : TimeoutCounter( app, parameter.finishHash, - LedgerReplayParameters::kTaskTimeout, + ledger_replay_parameters::kTaskTimeout, {.jobType = JtReplayTask, .jobName = "LedReplTask", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplayTask")) , inboundLedgers_(inboundLedgers) , replayer_(replayer) , parameter_(parameter) , maxTimeouts_( std::max( - LedgerReplayParameters::kTaskMaxTimeoutsMinimum, - parameter.totalLedgers * LedgerReplayParameters::kTaskMaxTimeoutsMultiplier)) + ledger_replay_parameters::kTaskMaxTimeoutsMinimum, + parameter.totalLedgers * ledger_replay_parameters::kTaskMaxTimeoutsMultiplier)) , skipListAcquirer_(skipListAcquirer) { JLOG(journal_.trace()) << "Create " << hash_; diff --git a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp index 3bb5ca9434..52184c1723 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp @@ -51,7 +51,7 @@ LedgerReplayer::replay( { XRPL_ASSERT( finishLedgerHash.isNonZero() && totalNumLedgers > 0 && - totalNumLedgers <= LedgerReplayParameters::kMaxTaskSize, + totalNumLedgers <= ledger_replay_parameters::kMaxTaskSize, "xrpl::LedgerReplayer::replay : valid inputs"); // NOLINTNEXTLINE(misc-const-correctness) @@ -64,7 +64,7 @@ LedgerReplayer::replay( std::scoped_lock const lock(mtx_); if (app_.isStopping()) return; - if (tasks_.size() >= LedgerReplayParameters::kMaxTasks) + if (tasks_.size() >= ledger_replay_parameters::kMaxTasks) { JLOG(j_.info()) << "Too many replay tasks, dropping new task " << parameter.finishHash; return; diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 7a581e2389..9d3820e9f7 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -135,7 +135,7 @@ fillJsonTx( { copyFrom(txJson[jss::tx_json], txn->getJson(JsonOptions::Values::DisableApiPriorV2, false)); txJson[jss::hash] = to_string(txn->getTransactionID()); - RPC::insertDeliverMax(txJson[jss::tx_json], txnType, fill.context->apiVersion); + rpc::insertDeliverMax(txJson[jss::tx_json], txnType, fill.context->apiVersion); if (stMeta) { @@ -144,7 +144,7 @@ fillJsonTx( // If applicable, insert delivered amount if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) { - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( txJson[jss::meta], fill.ledger, txn, @@ -152,7 +152,7 @@ fillJsonTx( } // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( txJson[jss::meta], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } @@ -172,7 +172,7 @@ fillJsonTx( else { copyFrom(txJson, txn->getJson(JsonOptions::Values::None)); - RPC::insertDeliverMax(txJson, txnType, fill.context->apiVersion); + rpc::insertDeliverMax(txJson, txnType, fill.context->apiVersion); if (stMeta) { txJson[jss::metaData] = stMeta->getJson(JsonOptions::Values::None); @@ -180,7 +180,7 @@ fillJsonTx( // If applicable, insert delivered amount if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) { - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( txJson[jss::metaData], fill.ledger, txn, @@ -188,7 +188,7 @@ fillJsonTx( } // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( txJson[jss::metaData], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } } @@ -337,7 +337,7 @@ fillJson(json::Value& json, LedgerFill const& fill) fill.ledger.header(), bFull, ((fill.context != nullptr) ? fill.context->apiVersion - : RPC::kApiMaximumSupportedVersion)); + : rpc::kApiMaximumSupportedVersion)); } if (bFull || ((fill.options & static_cast(LedgerFill::Options::DumpTxrp)) != 0)) diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp index 8ebd14083a..97b50a9a3a 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp @@ -37,10 +37,10 @@ SkipListAcquire::SkipListAcquire( : TimeoutCounter( app, ledgerHash, - LedgerReplayParameters::kSubTaskTimeout, + ledger_replay_parameters::kSubTaskTimeout, {.jobType = JtReplayTask, .jobName = "SkipListAcq", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplaySkipList")) , inboundLedgers_(inboundLedgers) , peerSet_(std::move(peerSet)) @@ -96,10 +96,10 @@ SkipListAcquire::trigger(std::size_t limit, ScopedLockType& sl) { JLOG(journal_.trace()) << "Add a no feature peer " << peer->id() << " for " << hash_; - if (++noFeaturePeerCount_ >= LedgerReplayParameters::kMaxNoFeaturePeerCount) + if (++noFeaturePeerCount_ >= ledger_replay_parameters::kMaxNoFeaturePeerCount) { JLOG(journal_.debug()) << "Fall back for " << hash_; - timerInterval_ = LedgerReplayParameters::kSubTaskFallbackTimeout; + timerInterval_ = ledger_replay_parameters::kSubTaskFallbackTimeout; fallBack_ = true; } } @@ -114,7 +114,7 @@ void SkipListAcquire::onTimer(bool progress, ScopedLockType& sl) { JLOG(journal_.trace()) << "timeouts_=" << timeouts_ << " for " << hash_; - if (timeouts_ > LedgerReplayParameters::kSubTaskMaxTimeouts) + if (timeouts_ > ledger_replay_parameters::kSubTaskMaxTimeouts) { failed_ = true; JLOG(journal_.debug()) << "too many timeouts " << hash_; diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp index 62312b04d2..db99299fd6 100644 --- a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp @@ -7,13 +7,13 @@ #include #include -#include #include #include #include #include #include #include +#include #include @@ -171,7 +171,7 @@ TransactionAcquire::trigger(std::shared_ptr const& peer) SHAMapAddNode TransactionAcquire::takeNodes( - std::vector> const& data, + std::vector> data, std::shared_ptr const& peer) { ScopedLockType const sl(mtx_); @@ -195,7 +195,7 @@ TransactionAcquire::takeNodes( ConsensusTransSetSF sf(app_, app_.getTempNodeCache()); - for (auto const& d : data) + for (auto& d : data) { if (d.first.isRoot()) { @@ -203,18 +203,22 @@ TransactionAcquire::takeNodes( { JLOG(journal_.debug()) << "Got root TXS node, already have it"; } - else if (!map_->addRootNode(SHAMapHash{hash_}, d.second, nullptr).isGood()) + else if (!map_->addRootNode(SHAMapHash{hash_}, std::move(d.second), nullptr) + .isGood()) { - JLOG(journal_.warn()) << "TX acquire got bad root node"; + JLOG(journal_.warn()) << "TX acquire got bad root node for TX set " << hash_ + << " from peer " << peer->id(); + return SHAMapAddNode::invalid(); } else { haveRoot_ = true; } } - else if (!map_->addKnownNode(d.first, d.second, &sf).isGood()) + else if (!map_->addKnownNode(d.first, std::move(d.second), &sf).isGood()) { - JLOG(journal_.warn()) << "TX acquire got bad non-root node"; + JLOG(journal_.warn()) << "TX acquire got bad non-root node " << d.first + << " for TX set " << hash_ << " from peer " << peer->id(); return SHAMapAddNode::invalid(); } } diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h b/src/xrpld/app/ledger/detail/TransactionAcquire.h index 5b33066390..2faf74b557 100644 --- a/src/xrpld/app/ledger/detail/TransactionAcquire.h +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h @@ -6,10 +6,10 @@ #include #include -#include #include #include #include +#include #include #include @@ -32,8 +32,8 @@ public: SHAMapAddNode takeNodes( - std::vector> const& data, - std::shared_ptr const&); + std::vector> data, + std::shared_ptr const& peer); void init(int startPeers); diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index ae984bcaf9..fb37b94569 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -234,7 +234,7 @@ public: std::optional> nodeIdentity_; ValidatorKeys const validatorKeys_; - std::unique_ptr resourceManager_; + std::unique_ptr resourceManager_; std::unique_ptr nodeStore_; NodeFamily nodeFamily_; @@ -388,7 +388,7 @@ public: , networkIDService_(std::make_unique(config_->networkId)) , validatorKeys_(*config_, journal_) , resourceManager_( - Resource::makeManager(collectorManager_->collector(), logs_->journal("Resource"))) + resource::makeManager(collectorManager_->collector(), logs_->journal("Resource"))) , nodeStore_(shaMapStore_->makeNodeStore( config_->prefetchWorkers > 0 ? config_->prefetchWorkers : 4)) , nodeFamily_(*this, *collectorManager_) @@ -692,7 +692,7 @@ public: return *loadManager_; } - Resource::Manager& + resource::Manager& getResourceManager() override { return *resourceManager_; @@ -1222,7 +1222,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) logs_->threshold(Severity::Debug); } - JLOG(journal_.info()) << "Process starting: " << BuildInfo::getFullVersionString() + JLOG(journal_.info()) << "Process starting: " << build_info::getFullVersionString() << ", Instance Cookie: " << instanceCookie_; if (numberOfThreads(*config_) < 2) @@ -1511,9 +1511,9 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl; } - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; - RPC::JsonContext context{ + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; + rpc::JsonContext context{ {.j = getJournal("RPCHandler"), .app = *this, .loadType = loadType, @@ -1523,11 +1523,11 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) .role = Role::ADMIN, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiMaximumSupportedVersion}, + .apiVersion = rpc::kApiMaximumSupportedVersion}, jvCommand}; json::Value jvResult; - RPC::doCommand(context, jvResult); + rpc::doCommand(context, jvResult); if (!config_->quiet()) { @@ -1543,7 +1543,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) void ApplicationImp::start(bool withTimers) { - JLOG(journal_.info()) << "Application starting. Version is " << BuildInfo::getVersionString(); + JLOG(journal_.info()) << "Application starting. Version is " << build_info::getVersionString(); if (withTimers) { diff --git a/src/xrpld/app/main/CollectorManager.cpp b/src/xrpld/app/main/CollectorManager.cpp index 9e1278607f..87b5286f97 100644 --- a/src/xrpld/app/main/CollectorManager.cpp +++ b/src/xrpld/app/main/CollectorManager.cpp @@ -30,8 +30,8 @@ public: if (server == "statsd") { - beast::IP::Endpoint const address( - beast::IP::Endpoint::fromString(get(params, Keys::kAddress))); + beast::ip::Endpoint const address( + beast::ip::Endpoint::fromString(get(params, Keys::kAddress))); std::string const& prefix(get(params, Keys::kPrefix)); collector_ = beast::insight::StatsDCollector::make(address, prefix, journal); diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1146cbdc08..1b20ff1d49 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -71,10 +71,10 @@ getEndpoint(std::string const& peer) peerClean = peer.substr(first + 1); } - std::optional endpoint = - beast::IP::Endpoint::fromStringChecked(peerClean); + std::optional endpoint = + beast::ip::Endpoint::fromStringChecked(peerClean); if (endpoint) - return beast::IP::toAsioEndpoint(endpoint.value()); + return beast::ip::toAsioEndpoint(endpoint.value()); } catch (std::exception const&) // NOLINT(bugprone-empty-catch) { @@ -92,8 +92,8 @@ GRPCServerImpl::CallData::CallData( BindListener bindListener, Handler handler, Forward forward, - RPC::Condition requiredCondition, - Resource::Charge loadType, + rpc::Condition requiredCondition, + resource::Charge loadType, std::vector const& secureGatewayIPs) : service_(service) , cq_(cq) @@ -195,7 +195,7 @@ GRPCServerImpl::CallData::process(std::shared_ptr context{ + rpc::GRPCContext context{ {app_.getJournal("gRPCServer"), app_, loadType, @@ -209,11 +209,11 @@ GRPCServerImpl::CallData::process(std::shared_ptr::isFinished() } template -Resource::Charge +resource::Charge GRPCServerImpl::CallData::getLoadType() { return loadType_; @@ -323,12 +323,12 @@ GRPCServerImpl::CallData::setIsUnlimited(Response& response, } template -Resource::Consumer +resource::Consumer GRPCServerImpl::CallData::getUsage() { auto endpoint = getClientEndpoint(); if (endpoint) - return app_.getResourceManager().newInboundEndpoint(beast::IP::fromAsio(endpoint.value())); + return app_.getResourceManager().newInboundEndpoint(beast::ip::fromAsio(endpoint.value())); Throw("Failed to get client endpoint"); } @@ -527,7 +527,7 @@ GRPCServerImpl::handleRpcs() std::vector> GRPCServerImpl::setupListeners() { - using RPC::Condition; + using rpc::Condition; std::vector> requests; auto addToRequests = [&requests](auto callData) { requests.push_back(std::move(callData)); }; @@ -545,7 +545,7 @@ GRPCServerImpl::setupListeners() doLedgerGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedger, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -562,7 +562,7 @@ GRPCServerImpl::setupListeners() doLedgerDataGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerData, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -579,7 +579,7 @@ GRPCServerImpl::setupListeners() doLedgerDiffGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerDiff, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -596,7 +596,7 @@ GRPCServerImpl::setupListeners() doLedgerEntryGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerEntry, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } return requests; diff --git a/src/xrpld/app/main/GRPCServer.h b/src/xrpld/app/main/GRPCServer.h index db948cab99..98b50fcd0c 100644 --- a/src/xrpld/app/main/GRPCServer.h +++ b/src/xrpld/app/main/GRPCServer.h @@ -102,7 +102,7 @@ private: // typedef for actual handler (that populates a response) // handlers are defined in rpc/GRPCHandlers.h template - using Handler = std::function(RPC::GRPCContext&)>; + using Handler = std::function(rpc::GRPCContext&)>; // This implementation is currently limited to v1 of the API static constexpr unsigned kApiVersion = 1; @@ -189,10 +189,10 @@ private: Forward forward_; // Condition required for this RPC - RPC::Condition requiredCondition_; + rpc::Condition requiredCondition_; // Load type for this RPC - Resource::Charge loadType_; + resource::Charge loadType_; std::vector const& secureGatewayIPs_; @@ -209,8 +209,8 @@ private: BindListener bindListener, Handler handler, Forward forward, - RPC::Condition requiredCondition, - Resource::Charge loadType, + rpc::Condition requiredCondition, + resource::Charge loadType, std::vector const& secureGatewayIPs); CallData(CallData const&) = delete; @@ -233,7 +233,7 @@ private: process(std::shared_ptr coro); // return load type of this RPC - Resource::Charge + resource::Charge getLoadType(); // return the Role used for this RPC @@ -241,7 +241,7 @@ private: getRole(bool isUnlimited); // register endpoint with ResourceManager and return usage - Resource::Consumer + resource::Consumer getUsage(); // Returns the ip of the client @@ -290,7 +290,7 @@ private: // forward request to a p2p node void - forwardToP2p(RPC::GRPCContext& context); + forwardToP2p(rpc::GRPCContext& context); }; // CallData diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index d0b40efce8..a23b84f2e8 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -506,7 +506,7 @@ run(int argc, char** argv) if (vm.contains("version")) { // LCOV_EXCL_START - std::cout << "xrpld version " << BuildInfo::getVersionString() << std::endl; + std::cout << "xrpld version " << build_info::getVersionString() << std::endl; std::cout << "Git commit hash: " << xrpl::git::getCommitHash() << std::endl; std::cout << "Git build branch: " << xrpl::git::getBuildBranch() << std::endl; return 0; @@ -716,7 +716,7 @@ run(int argc, char** argv) // happen after the config file is loaded. if (vm.contains("rpc_ip")) { - auto endpoint = beast::IP::Endpoint::fromStringChecked(vm["rpc_ip"].as()); + auto endpoint = beast::ip::Endpoint::fromStringChecked(vm["rpc_ip"].as()); if (!endpoint) { std::cerr << "Invalid rpc_ip = " << vm["rpc_ip"].as() << "\n"; @@ -826,7 +826,7 @@ run(int argc, char** argv) // We have an RPC command to process: beast::setCurrentThreadName("xrpld: rpc"); - return RPCCall::fromCommandLine( + return rpc_call::fromCommandLine( *config, vm["parameters"].as>(), *logs); // LCOV_EXCL_STOP } diff --git a/src/xrpld/app/misc/DeliverMax.h b/src/xrpld/app/misc/DeliverMax.h index 73ccc95800..1683219c8b 100644 --- a/src/xrpld/app/misc/DeliverMax.h +++ b/src/xrpld/app/misc/DeliverMax.h @@ -6,7 +6,7 @@ namespace json { class Value; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Copy `Amount` field to `DeliverMax` field in transaction output JSON. @@ -22,4 +22,4 @@ insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 47cbebb901..8f31ce1eb3 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1215,7 +1215,7 @@ NetworkOPsImp::processClusterTimer() n.set_nodename(node.name()); }); - Resource::Gossip const gossip = registry_.get().getResourceManager().exportConsumers(); + resource::Gossip const gossip = registry_.get().getResourceManager().exportConsumers(); for (auto& item : gossip.items) { protocol::TMLoadSource& node = *cluster.add_loadsources(); @@ -2487,7 +2487,7 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) // for consumers supporting different API versions MultiApiJson multiObj{jvObj}; multiObj.visit( - RPC::kApiVersion<1>, // + rpc::kApiVersion<1>, // [](json::Value& jvTx) { // Type conversion for older API versions to string if (jvTx.isMember(jss::ledger_index)) @@ -2688,7 +2688,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) if (!registry_.get().getApp().config().serverDomain.empty()) info[jss::server_domain] = registry_.get().getApp().config().serverDomain; - info[jss::build_version] = BuildInfo::getVersionString(); + info[jss::build_version] = build_info::getVersionString(); info[jss::server_state] = strOperatingMode(admin); @@ -3167,7 +3167,7 @@ NetworkOPsImp::pubLedger(std::shared_ptr const& lpAccepted) if (!streamMaps_[SBookChanges].empty()) { - json::Value const jvObj = xrpl::RPC::computeBookChanges(lpAccepted); + json::Value const jvObj = xrpl::rpc::computeBookChanges(lpAccepted); auto it = streamMaps_[SBookChanges].begin(); while (it != streamMaps_[SBookChanges].end()) @@ -3281,9 +3281,9 @@ NetworkOPsImp::transJson( if (meta) { jvObj[jss::meta] = meta->get().getJson(JsonOptions::Values::None); - RPC::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get()); - RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); - RPC::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get()); + rpc::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get()); + rpc::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); + rpc::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get()); } // add CTID where the needed data for it exists @@ -3295,7 +3295,7 @@ NetworkOPsImp::transJson( if (transaction->isFieldPresent(sfNetworkID)) netID = transaction->getFieldU32(sfNetworkID); - if (std::optional ctid = RPC::encodeCTID(ledger->header().seq, txnSeq, netID); + if (std::optional ctid = rpc::encodeCTID(ledger->header().seq, txnSeq, netID); ctid) jvObj[jss::ctid] = *ctid; } @@ -3346,7 +3346,7 @@ NetworkOPsImp::transJson( forAllApiVersions( multiObj.visit(), // [&](json::Value& jvTx, std::integral_constant) { - RPC::insertDeliverMax(jvTx[jss::transaction], transaction->getTxnType(), Version); + rpc::insertDeliverMax(jvTx[jss::transaction], transaction->getTxnType(), Version); if constexpr (Version > 1) { @@ -3891,7 +3891,7 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) int feeChargeCount = 0; if (auto sptr = subInfo.sinkWptr.lock(); sptr) { - sptr->getConsumer().charge(Resource::kFeeMediumBurdenRpc); + sptr->getConsumer().charge(resource::kFeeMediumBurdenRpc); ++feeChargeCount; } else diff --git a/src/xrpld/app/misc/detail/DeliverMax.cpp b/src/xrpld/app/misc/detail/DeliverMax.cpp index add3cf89ee..e512b078c7 100644 --- a/src/xrpld/app/misc/detail/DeliverMax.cpp +++ b/src/xrpld/app/misc/detail/DeliverMax.cpp @@ -3,7 +3,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { void insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion) @@ -19,4 +19,4 @@ insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion) } } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/app/misc/detail/Transaction.cpp b/src/xrpld/app/misc/detail/Transaction.cpp index e29181bfe9..59cc4b6c4c 100644 --- a/src/xrpld/app/misc/detail/Transaction.cpp +++ b/src/xrpld/app/misc/detail/Transaction.cpp @@ -182,7 +182,7 @@ Transaction::getJson(JsonOptions options, bool binary) const if (txnSeq_ && netID) { - std::optional const ctid = RPC::encodeCTID(ledgerIndex_, *txnSeq_, *netID); + std::optional const ctid = rpc::encodeCTID(ledgerIndex_, *txnSeq_, *netID); if (ctid) ret[jss::ctid] = *ctid; } diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h index 73e5081036..fd36cb5318 100644 --- a/src/xrpld/app/misc/detail/WorkBase.h +++ b/src/xrpld/app/misc/detail/WorkBase.h @@ -223,7 +223,7 @@ WorkBase::onStart() req_.target(path_.empty() ? "/" : path_); req_.version(11); req_.set("Host", host_ + ":" + port_); - req_.set("User-Agent", BuildInfo::getFullVersionString()); + req_.set("User-Agent", build_info::getFullVersionString()); req_.prepare_payload(); boost::beast::http::async_write( impl().stream(), diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h index 5d916000a3..3ff7b7268b 100644 --- a/src/xrpld/app/rdb/PeerFinder.h +++ b/src/xrpld/app/rdb/PeerFinder.h @@ -44,6 +44,6 @@ readPeerFinderDB(soci::session& session, std::function const& v); +savePeerFinderDB(soci::session& session, std::vector const& v); } // namespace xrpl diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp index 72a275c7cd..8d9af69ae3 100644 --- a/src/xrpld/app/rdb/detail/PeerFinder.cpp +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp @@ -108,7 +108,7 @@ updatePeerFinderDB(soci::session& session, int currentSchemaVersion, beast::Jour std::size_t count = 0; session << "SELECT COUNT(*) FROM PeerFinder_BootstrapCache;", soci::into(count); - std::vector list; + std::vector list; { list.reserve(count); @@ -125,8 +125,8 @@ updatePeerFinderDB(soci::session& session, int currentSchemaVersion, beast::Jour st.execute(); while (st.fetch()) { - PeerFinder::Store::Entry entry; - entry.endpoint = beast::IP::Endpoint::fromString(s); + peer_finder::Store::Entry entry; + entry.endpoint = beast::ip::Endpoint::fromString(s); if (!isUnspecified(entry.endpoint)) { entry.valence = valence; @@ -226,7 +226,7 @@ readPeerFinderDB(soci::session& session, std::function const& v) +savePeerFinderDB(soci::session& session, std::vector const& v) { soci::transaction tr(session); session << "DELETE FROM PeerFinder_BootstrapCache;"; diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 852e46218a..d43a7a566d 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -277,7 +277,7 @@ public: std::size_t txRelayPercentage = 25; // These override the command line client settings - std::optional rpcIp; + std::optional rpcIp; std::unordered_set> features; diff --git a/src/xrpld/overlay/Overlay.h b/src/xrpld/overlay/Overlay.h index 6cc229f5a0..9ab80e6697 100644 --- a/src/xrpld/overlay/Overlay.h +++ b/src/xrpld/overlay/Overlay.h @@ -55,7 +55,7 @@ public: explicit Setup() = default; std::shared_ptr context; - beast::IP::Address publicIp; + beast::ip::Address publicIp; int ipLimit = 0; std::uint32_t crawlOptions = 0; std::optional networkID; @@ -92,7 +92,7 @@ public: * performed asynchronously. */ virtual void - connect(beast::IP::Endpoint const& address) = 0; + connect(beast::ip::Endpoint const& address) = 0; /** * Returns the maximum number of peers we are configured to allow. diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 23a45dc512..c2631cc7ce 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -15,14 +15,15 @@ namespace xrpl { -namespace Resource { +namespace resource { class Charge; -} // namespace Resource +} // namespace resource enum class ProtocolFeature { ValidatorListPropagation, ValidatorList2Propagation, LedgerReplay, + LedgerNodeDepth, }; /** @@ -50,7 +51,7 @@ public: virtual void send(std::shared_ptr const& m) = 0; - [[nodiscard]] virtual beast::IP::Endpoint + [[nodiscard]] virtual beast::ip::Endpoint getRemoteAddress() const = 0; /** @@ -75,7 +76,7 @@ public: * Adjust this peer's load balance based on the type of load imposed. */ virtual void - charge(Resource::Charge const& fee, std::string const& context) = 0; + charge(resource::Charge const& fee, std::string const& context) = 0; // // Identity diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 0f0b3242de..b78b8eb7b8 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -49,10 +49,10 @@ ConnectAttempt::ConnectAttempt( Application& app, boost::asio::io_context& ioContext, endpoint_type remoteEndpoint, - Resource::Consumer usage, + resource::Consumer usage, shared_context const& context, Peer::id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, beast::Journal journal, OverlayImpl& overlay) : Child(overlay) @@ -462,7 +462,7 @@ ConnectAttempt::processResponse() auto const result = overlay_.peerFinder().activate(slot_, publicKey, static_cast(member)); - if (result != PeerFinder::Result::Success) + if (result != peer_finder::Result::Success) { fail("Outbound " + std::string(to_string(result))); return; diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index f9ba33571f..3ebffc529d 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -41,7 +41,7 @@ private: beast::WrappedSink sink_; beast::Journal const journal_; endpoint_type remoteEndpoint_; - Resource::Consumer usage_; + resource::Consumer usage_; boost::asio::strand strand_; boost::asio::basic_waitable_timer timer_; std::unique_ptr streamPtr_; @@ -49,7 +49,7 @@ private: stream_type& stream_; boost::beast::multi_buffer readBuf_; response_type response_; - std::shared_ptr slot_; + std::shared_ptr slot_; request_type req_; public: @@ -57,10 +57,10 @@ public: Application& app, boost::asio::io_context& ioContext, endpoint_type remoteEndpoint, - Resource::Consumer usage, + resource::Consumer usage, shared_context const& context, Peer::id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, beast::Journal journal, OverlayImpl& overlay); @@ -102,7 +102,7 @@ private: static boost::asio::ip::tcp::endpoint parseEndpoint(std::string const& s, boost::system::error_code& ec) { - beast::IP::Endpoint bep; + beast::ip::Endpoint bep; std::istringstream is(s); is >> bep; if (is.fail()) diff --git a/src/xrpld/overlay/detail/Handshake.cpp b/src/xrpld/overlay/detail/Handshake.cpp index a860d2d604..a12923d1c3 100644 --- a/src/xrpld/overlay/detail/Handshake.cpp +++ b/src/xrpld/overlay/detail/Handshake.cpp @@ -186,8 +186,8 @@ buildHandshake( boost::beast::http::fields& h, xrpl::uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, Application& app) { if (networkID) @@ -213,7 +213,7 @@ buildHandshake( if (!app.config().serverDomain.empty()) h.insert("Server-Domain", app.config().serverDomain); - if (beast::IP::isPublic(remoteIp)) + if (beast::ip::isPublic(remoteIp)) h.insert("Remote-IP", remoteIp.to_string()); if (!publicIp.is_unspecified()) @@ -231,8 +231,8 @@ verifyHandshake( boost::beast::http::fields const& headers, xrpl::uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remote, + beast::ip::Address publicIp, + beast::ip::Address remote, Application& app) { if (auto const iter = headers.find("Server-Domain"); iter != headers.end()) @@ -331,7 +331,7 @@ verifyHandshake( if (ec) throw std::runtime_error("Invalid Local-IP"); - if (beast::IP::isPublic(remote) && remote != localIp) + if (beast::ip::isPublic(remote) && remote != localIp) { throw std::runtime_error( "Incorrect Local-IP: " + remote.to_string() + " instead of " + localIp.to_string()); @@ -346,7 +346,7 @@ verifyHandshake( if (ec) throw std::runtime_error("Invalid Remote-IP"); - if (beast::IP::isPublic(remote) && !beast::IP::isUnspecified(publicIp)) + if (beast::ip::isPublic(remote) && !beast::ip::isUnspecified(publicIp)) { // We know our public IP and peer reports our connection came // from some other IP. @@ -374,7 +374,7 @@ makeRequest( m.method(boost::beast::http::verb::get); m.target("/"); m.version(11); - m.insert("User-Agent", BuildInfo::getFullVersionString()); + m.insert("User-Agent", build_info::getFullVersionString()); m.insert("Upgrade", supportedProtocolVersions()); m.insert("Connection", "Upgrade"); m.insert("Connect-As", "Peer"); @@ -390,8 +390,8 @@ http_response_type makeResponse( bool crawlPublic, http_request_type const& req, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, uint256 const& sharedValue, std::optional networkID, ProtocolVersion protocol, @@ -403,7 +403,7 @@ makeResponse( resp.insert("Connection", "Upgrade"); resp.insert("Upgrade", to_string(protocol)); resp.insert("Connect-As", "Peer"); - resp.insert("Server", BuildInfo::getFullVersionString()); + resp.insert("Server", build_info::getFullVersionString()); resp.insert("Crawl", crawlPublic ? "public" : "private"); resp.insert( "X-Protocol-Ctl", diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h index 9a4e5ba507..d54cd3a0ea 100644 --- a/src/xrpld/overlay/detail/Handshake.h +++ b/src/xrpld/overlay/detail/Handshake.h @@ -47,8 +47,8 @@ buildHandshake( boost::beast::http::fields& h, uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, Application& app); /** @@ -68,8 +68,8 @@ verifyHandshake( boost::beast::http::fields const& headers, uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remote, + beast::ip::Address publicIp, + beast::ip::Address remote, Application& app); /** @@ -109,8 +109,8 @@ http_response_type makeResponse( bool crawlPublic, http_request_type const& req, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, uint256 const& sharedValue, std::optional networkID, ProtocolVersion version, diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index f9972548d1..81ead14111 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -93,13 +93,13 @@ namespace xrpl { -namespace CrawlOptions { +namespace crawl_options { static constexpr auto kDisabled = 0; static constexpr auto kOverlay = (1 << 0); static constexpr auto kServerInfo = (1 << 1); static constexpr auto kServerCounts = (1 << 2); static constexpr auto kUnl = (1 << 3); -} // namespace CrawlOptions +} // namespace crawl_options //------------------------------------------------------------------------------ @@ -155,7 +155,7 @@ OverlayImpl::Timer::onTimer(error_code ec) if (overlay_.app_.config().txReduceRelayEnable) overlay_.sendTxQueue(); - if ((++overlay_.timerCount_ % Tuning::kCheckIdlePeers) == 0) + if ((++overlay_.timerCount_ % tuning::kCheckIdlePeers) == 0) overlay_.deleteIdlePeers(); asyncWait(); @@ -167,7 +167,7 @@ OverlayImpl::OverlayImpl( Application& app, Setup setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, @@ -182,7 +182,7 @@ OverlayImpl::OverlayImpl( , resourceManager_(resourceManager) , store_(app_.getJournal("PeerFinder")) , peerFinder_( - PeerFinder::makeManager( + peer_finder::makeManager( ioContext, stopwatch(), app_.getJournal("PeerFinder"), @@ -308,7 +308,7 @@ OverlayImpl::onHandoff( bool const reserved = static_cast(app_.getCluster().member(publicKey)) || app_.getPeerReservations().contains(publicKey); auto const result = peerFinder_->activate(slot, publicKey, reserved); - if (result != PeerFinder::Result::Success) + if (result != peer_finder::Result::Success) { peerFinder_->onClosed(slot); JLOG(journal.debug()) @@ -381,14 +381,14 @@ OverlayImpl::makePrefix(std::uint32_t id) std::shared_ptr OverlayImpl::makeRedirectResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress) { boost::beast::http::response msg; msg.version(request.version()); msg.result(boost::beast::http::status::service_unavailable); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); { std::ostringstream ostr; ostr << remoteAddress; @@ -408,7 +408,7 @@ OverlayImpl::makeRedirectResponse( std::shared_ptr OverlayImpl::makeErrorResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress, std::string const& text) @@ -417,7 +417,7 @@ OverlayImpl::makeErrorResponse( msg.version(request.version()); msg.result(boost::beast::http::status::bad_request); msg.reason("Bad Request (" + text + ")"); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Remote-Address", remoteAddress.to_string()); msg.insert(boost::beast::http::field::connection, "close"); msg.prepare_payload(); @@ -427,7 +427,7 @@ OverlayImpl::makeErrorResponse( //------------------------------------------------------------------------------ void -OverlayImpl::connect(beast::IP::Endpoint const& remoteEndpoint) +OverlayImpl::connect(beast::ip::Endpoint const& remoteEndpoint) { XRPL_ASSERT(work_, "xrpl::OverlayImpl::connect : work is set"); @@ -497,7 +497,7 @@ OverlayImpl::addActive(std::shared_ptr const& peer) } void -OverlayImpl::remove(std::shared_ptr const& slot) +OverlayImpl::remove(std::shared_ptr const& slot) { std::scoped_lock const lock(mutex_); auto const iter = peers_.find(slot); @@ -508,7 +508,7 @@ OverlayImpl::remove(std::shared_ptr const& slot) void OverlayImpl::start() { - PeerFinder::Config const config = PeerFinder::makeConfig( + peer_finder::Config const config = peer_finder::makeConfig( app_.config(), serverHandler_.setup().overlay.port(), app_.getValidationPublicKey().has_value(), @@ -541,7 +541,7 @@ OverlayImpl::start() resolver_.resolve( bootstrapIps, - [this](std::string const& name, std::vector const& addresses) { + [this](std::string const& name, std::vector const& addresses) { std::vector ips; ips.reserve(addresses.size()); for (auto const& addr : addresses) @@ -566,8 +566,8 @@ OverlayImpl::start() { resolver_.resolve( app_.config().ipsFixed, - [this](std::string const& name, std::vector const& addresses) { - std::vector ips; + [this](std::string const& name, std::vector const& addresses) { + std::vector ips; ips.reserve(addresses.size()); for (auto& addr : addresses) @@ -868,30 +868,30 @@ OverlayImpl::json() bool OverlayImpl::processCrawl(http_request_type const& req, Handoff& handoff) { - if (req.target() != "/crawl" || setup_.crawlOptions == CrawlOptions::kDisabled) + if (req.target() != "/crawl" || setup_.crawlOptions == crawl_options::kDisabled) return false; boost::beast::http::response msg; msg.version(req.version()); msg.result(boost::beast::http::status::ok); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); msg.body()["version"] = json::Value(2u); - if ((setup_.crawlOptions & CrawlOptions::kOverlay) != 0u) + if ((setup_.crawlOptions & crawl_options::kOverlay) != 0u) { msg.body()["overlay"] = getOverlayInfo(); } - if ((setup_.crawlOptions & CrawlOptions::kServerInfo) != 0u) + if ((setup_.crawlOptions & crawl_options::kServerInfo) != 0u) { msg.body()["server"] = getServerInfo(); } - if ((setup_.crawlOptions & CrawlOptions::kServerCounts) != 0u) + if ((setup_.crawlOptions & crawl_options::kServerCounts) != 0u) { msg.body()["counts"] = getServerCounts(); } - if ((setup_.crawlOptions & CrawlOptions::kUnl) != 0u) + if ((setup_.crawlOptions & crawl_options::kUnl) != 0u) { msg.body()["unl"] = getUnlInfo(); } @@ -915,7 +915,7 @@ OverlayImpl::processValidatorList(http_request_type const& req, Handoff& handoff boost::beast::http::response msg; msg.version(req.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); @@ -972,7 +972,7 @@ OverlayImpl::processHealth(http_request_type const& req, Handoff& handoff) return false; boost::beast::http::response msg; msg.version(req.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); @@ -1535,7 +1535,7 @@ setupOverlay(BasicConfig const& config, beast::Journal j) { boost::system::error_code ec; setup.publicIp = boost::asio::ip::make_address(ip, ec); - if (ec || !beast::IP::isPublic(setup.publicIp)) + if (ec || !beast::ip::isPublic(setup.publicIp)) Throw("Configured public IP is invalid"); } @@ -1577,19 +1577,19 @@ setupOverlay(BasicConfig const& config, beast::Journal j) { if (get(section, Keys::kOverlay, true)) { - setup.crawlOptions |= CrawlOptions::kOverlay; + setup.crawlOptions |= crawl_options::kOverlay; } if (get(section, Keys::kServer, true)) { - setup.crawlOptions |= CrawlOptions::kServerInfo; + setup.crawlOptions |= crawl_options::kServerInfo; } if (get(section, Keys::kCounts, false)) { - setup.crawlOptions |= CrawlOptions::kServerCounts; + setup.crawlOptions |= crawl_options::kServerCounts; } if (get(section, Keys::kUnl, true)) { - setup.crawlOptions |= CrawlOptions::kUnl; + setup.crawlOptions |= crawl_options::kUnl; } } } @@ -1632,7 +1632,7 @@ makeOverlay( Application& app, Overlay::Setup const& setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index cd2c7d630b..f274bbba5a 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -109,11 +109,11 @@ private: Setup setup_; beast::Journal const journal_; ServerHandler& serverHandler_; - Resource::Manager& resourceManager_; - PeerFinder::StoreSqdb store_; - std::unique_ptr peerFinder_; + resource::Manager& resourceManager_; + peer_finder::StoreSqdb store_; + std::unique_ptr peerFinder_; TrafficCount traffic_; - hash_map, std::weak_ptr> peers_; + hash_map, std::weak_ptr> peers_; hash_map> ids_; Resolver& resolver_; std::atomic nextId_; @@ -141,7 +141,7 @@ public: Application& app, Setup setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, @@ -157,13 +157,13 @@ public: void stop() override; - PeerFinder::Manager& + peer_finder::Manager& peerFinder() { return *peerFinder_; } - Resource::Manager& + resource::Manager& resourceManager() { return resourceManager_; @@ -182,7 +182,7 @@ public: endpoint_type remoteEndpoint) override; void - connect(beast::IP::Endpoint const& remoteEndpoint) override; + connect(beast::ip::Endpoint const& remoteEndpoint) override; int limit() override; @@ -252,7 +252,7 @@ public: addActive(std::shared_ptr const& peer); void - remove(std::shared_ptr const& slot); + remove(std::shared_ptr const& slot); /** * Called when a peer has connected successfully @@ -451,13 +451,13 @@ private: std::shared_ptr makeRedirectResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress); static std::shared_ptr makeErrorResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress, std::string const& msg); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index d7a9a9e449..ca6fb180d4 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include #include #include @@ -123,11 +125,11 @@ constexpr std::chrono::seconds kPeerTimerInterval{60}; PeerImp::PeerImp( Application& app, id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : Child(overlay) @@ -155,7 +157,7 @@ PeerImp::PeerImp( , creationTime_(clock_type::now()) , squelch_(app_.getJournal("Squelch")) , usage_(consumer) - , fee_{.fee = Resource::kFeeTrivialPeer, .context = ""} + , fee_{.fee = resource::kFeeTrivialPeer, .context = ""} , slot_(slot) , request_(std::move(request)) , headers_(request_) @@ -301,7 +303,7 @@ PeerImp::send(std::shared_ptr const& m) auto sendqSize = self->sendQueue_.size(); - if (sendqSize < Tuning::kTargetSendQueue) + if (sendqSize < tuning::kTargetSendQueue) { // To detect a peer that does not read from their // side of the connection, we expect a peer to have @@ -310,7 +312,7 @@ PeerImp::send(std::shared_ptr const& m) } else if ( auto sink = self->journal_.debug(); - sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) + sink && (sendqSize % tuning::kSendQueueLogFreq) == 0) { std::string const n = self->name(); sink << n << " sendq: " << sendqSize; @@ -372,10 +374,10 @@ PeerImp::removeTxQueue(uint256 const& hash) } void -PeerImp::charge(Resource::Charge const& fee, std::string const& context) +PeerImp::charge(resource::Charge const& fee, std::string const& context) { dispatch(strand_, [self = shared_from_this(), fee, context]() { - if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && + if ((self->usage_.charge(fee, context) == resource::Disposition::Drop) && self->usage_.disconnect(self->pJournal_)) { // Idempotent: only the first worker to observe Drop counts the @@ -543,6 +545,8 @@ PeerImp::supportsFeature(ProtocolFeature f) const return protocol_ >= makeProtocol(2, 1); case ProtocolFeature::ValidatorList2Propagation: return protocol_ >= makeProtocol(2, 2); + case ProtocolFeature::LedgerNodeDepth: + return protocol_ >= makeProtocol(2, 3); case ProtocolFeature::LedgerReplay: return ledgerReplayEnabled_; } @@ -714,7 +718,7 @@ PeerImp::onTimer(error_code const& ec) return; } - if (largeSendq_++ >= Tuning::kSendqIntervals) + if (largeSendq_++ >= tuning::kSendqIntervals) { fail("Large send queue"); return; @@ -944,7 +948,7 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) readBuffer_.commit(bytesTransferred); - auto hint = Tuning::kReadBufferBytes; + auto hint = tuning::kReadBufferBytes; while (readBuffer_.size() > 0) { @@ -976,7 +980,7 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) // Timeout on writes only stream_.async_read_some( - readBuffer_.prepare(std::max(Tuning::kReadBufferBytes, hint)), + readBuffer_.prepare(std::max(tuning::kReadBufferBytes, hint)), bind_executor( strand_, [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { @@ -1052,7 +1056,7 @@ PeerImp::onMessageBegin( { auto const name = protocolMessageName(type); loadEvent_ = app_.getJobQueue().makeLoadEvent(JtPeer, name); - fee_ = {.fee = Resource::kFeeTrivialPeer, .context = name}; + fee_ = {.fee = resource::kFeeTrivialPeer, .context = name}; auto const category = TrafficCount::categorize(*m, static_cast(type), true); @@ -1096,12 +1100,12 @@ PeerImp::onMessage(std::shared_ptr const& m) if (s == 0) { - fee_.update(Resource::kFeeUselessData, "empty"); + fee_.update(resource::kFeeUselessData, "empty"); return; } if (s > 100) - fee_.update(Resource::kFeeModerateBurdenPeer, "oversize"); + fee_.update(resource::kFeeModerateBurdenPeer, "oversize"); app_.getJobQueue().addJob(JtManifest, "RcvManifests", [this, that = shared_from_this(), m]() { overlay_.onManifests(m, that); @@ -1114,7 +1118,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (m->type() == protocol::TMPing::ptPING) { // We have received a ping request, reply with a pong - fee_.update(Resource::kFeeModerateBurdenPeer, "ping request"); + fee_.update(resource::kFeeModerateBurdenPeer, "ping request"); m->set_type(protocol::TMPing::ptPONG); send(std::make_shared(*m, protocol::mtPING)); return; @@ -1155,7 +1159,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // VFALCO NOTE I think we should drop the peer immediately if (!cluster()) { - fee_.update(Resource::kFeeUselessData, "unknown cluster"); + fee_.update(resource::kFeeUselessData, "unknown cluster"); return; } @@ -1182,15 +1186,15 @@ PeerImp::onMessage(std::shared_ptr const& m) int const loadSources = m->loadsources().size(); if (loadSources != 0) { - Resource::Gossip gossip; + resource::Gossip gossip; gossip.items.reserve(loadSources); for (int i = 0; i < m->loadsources().size(); ++i) { protocol::TMLoadSource const& node = m->loadsources(i); - Resource::Gossip::Item item; - item.address = beast::IP::Endpoint::fromString(node.name()); + resource::Gossip::Item item; + item.address = beast::ip::Endpoint::fromString(node.name()); item.balance = node.cost(); - if (item.address != beast::IP::Endpoint()) + if (item.address != beast::ip::Endpoint()) gossip.items.push_back(item); } overlay_.resourceManager().importConsumers(name(), gossip); @@ -1230,17 +1234,17 @@ PeerImp::onMessage(std::shared_ptr const& m) // implication for the protocol. if (m->endpoints_v2().size() >= 1024) { - fee_.update(Resource::kFeeUselessData, "endpoints too large"); + fee_.update(resource::kFeeUselessData, "endpoints too large"); return; } - std::vector endpoints; + std::vector endpoints; endpoints.reserve(m->endpoints_v2().size()); auto malformed = 0; for (auto const& tm : m->endpoints_v2()) { - auto result = beast::IP::Endpoint::fromStringChecked(tm.endpoint()); + auto result = beast::ip::Endpoint::fromStringChecked(tm.endpoint()); if (!result) { @@ -1252,7 +1256,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // If hops == 0, this Endpoint describes the peer we are connected // to -- in that case, we take the remote address seen on the - // socket and store that in the IP::Endpoint. If this is the first + // socket and store that in the ip::Endpoint. If this is the first // time, then we'll verify that their listener can receive incoming // by performing a connectivity test. if hops > 0, then we just // take the address/port we were given @@ -1267,7 +1271,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (malformed > 0) { fee_.update( - Resource::kFeeInvalidData * malformed, + resource::kFeeInvalidData * malformed, std::to_string(malformed) + " malformed endpoints"); } @@ -1328,7 +1332,7 @@ PeerImp::handleTransaction( { JLOG(pJournal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (handleTransaction)."; - fee_.update(Resource::kFeeModerateBurdenPeer, "inner batch txn"); + fee_.update(resource::kFeeModerateBurdenPeer, "inner batch txn"); return; } // LCOV_EXCL_STOP @@ -1341,7 +1345,7 @@ PeerImp::handleTransaction( // we have seen this transaction recently if (any(flags & HashRouterFlags::BAD)) { - fee_.update(Resource::kFeeUselessData, "known bad"); + fee_.update(resource::kFeeUselessData, "known bad"); JLOG(pJournal_.debug()) << "Ignoring known bad tx " << txID; } @@ -1415,7 +1419,7 @@ void PeerImp::onMessage(std::shared_ptr const& m) { auto badData = [&](std::string const& msg) { - fee_.update(Resource::kFeeInvalidData, "get_ledger " + msg); + fee_.update(resource::kFeeInvalidData, "get_ledger " + msg); JLOG(pJournal_.warn()) << "TMGetLedger: " << msg; }; auto const itype{m->itype()}; @@ -1477,23 +1481,12 @@ PeerImp::onMessage(std::shared_ptr const& m) } } - // Verify ledger node IDs - if (itype != protocol::liBASE) + // Verify ledger node counts. Full parsing of the node IDs is deferred to the job, so the I/O + // thread is not burdened with SHAMapNodeID deserialization for every TMGetLedger message. + if (itype != protocol::liBASE && m->nodeids_size() <= 0) { - if (m->nodeids_size() <= 0) - { - badData("Invalid ledger node IDs"); - return; - } - - for (auto const& nodeId : m->nodeids()) - { - if (deserializeSHAMapNodeID(nodeId) == std::nullopt) - { - badData("Invalid SHAMap node ID"); - return; - } - } + badData("Invalid ledger node IDs"); + return; } // Verify query type @@ -1506,18 +1499,64 @@ PeerImp::onMessage(std::shared_ptr const& m) // Verify query depth if (m->has_querydepth()) { - if (m->querydepth() > Tuning::kMaxQueryDepth || itype == protocol::liBASE) + if (m->querydepth() > tuning::kMaxQueryDepth || itype == protocol::liBASE) { badData("Invalid query depth"); return; } } - // Queue a job to process the request + // Queue a job to process the request. std::weak_ptr const weak = shared_from_this(); - app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m]() { - if (auto peer = weak.lock()) - peer->processLedgerRequest(m); + app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m, itype]() { + auto peer = weak.lock(); + if (!peer) + return; + + std::vector nodeIDs; + bool tooManyNodeIds = false; + if (itype != protocol::liBASE) + { + nodeIDs.reserve(std::min(m->nodeids_size(), tuning::kSoftMaxReplyNodes)); + for (auto const& nodeId : m->nodeids()) + { + if (nodeIDs.size() >= tuning::kSoftMaxReplyNodes) + { + // The peer requested too many node IDs. Continue processing the received node + // IDs up to the limit. If the request is legitimate then at least they will get + // a response and won't have to resend these nodes in their next request. + tooManyNodeIds = true; + break; + } + auto parsed = deserializeSHAMapNodeID(nodeId); + if (!parsed) + { + peer->charge(resource::kFeeInvalidData, "TMGetLedger: Invalid node ID"); + return; + } + nodeIDs.push_back(std::move(*parsed)); + } + } + + // These are two distinct infractions and are charged independently: requesting too many + // node IDs is charged even for a relay response, while the base "get ledger request" charge + // below is skipped for relay responses. + if (tooManyNodeIds) + { + peer->charge(resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs"); + + // Truncate the request to what was actually parsed and charged for, so that if this + // request ends up being relayed to another peer, we don't forward the oversized list. + m->mutable_nodeids()->DeleteSubrange( + static_cast(nodeIDs.size()), + m->nodeids_size() - static_cast(nodeIDs.size())); + } + if (!m->has_requestcookie()) + { + peer->charge(resource::kFeeModerateBurdenPeer, "TMGetLedger: get ledger request"); + } + + peer->processLedgerRequest(m, std::move(nodeIDs)); }); } @@ -1527,11 +1566,11 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.trace()) << "onMessage, TMProofPathRequest"; if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "proof_path_request disabled"); + fee_.update(resource::kFeeMalformedRequest, "proof_path_request disabled"); return; } - fee_.update(Resource::kFeeModerateBurdenPeer, "received a proof path request"); + fee_.update(resource::kFeeModerateBurdenPeer, "received a proof path request"); std::weak_ptr const weak = shared_from_this(); app_.getJobQueue().addJob(JtReplayReq, "RcvProofPReq", [weak, m]() { if (auto peer = weak.lock()) @@ -1541,11 +1580,11 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (reply.error() == protocol::TMReplyError::reBAD_REQUEST) { - peer->charge(Resource::kFeeMalformedRequest, "proof_path_request"); + peer->charge(resource::kFeeMalformedRequest, "proof_path_request"); } else { - peer->charge(Resource::kFeeRequestNoReply, "proof_path_request"); + peer->charge(resource::kFeeRequestNoReply, "proof_path_request"); } } else @@ -1561,13 +1600,13 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "proof_path_response disabled"); + fee_.update(resource::kFeeMalformedRequest, "proof_path_response disabled"); return; } if (!ledgerReplayMsgHandler_.processProofPathResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + fee_.update(resource::kFeeInvalidData, "proof_path_response"); } } @@ -1577,11 +1616,11 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.trace()) << "onMessage, TMReplayDeltaRequest"; if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "replay_delta_request disabled"); + fee_.update(resource::kFeeMalformedRequest, "replay_delta_request disabled"); return; } - fee_.fee = Resource::kFeeModerateBurdenPeer; + fee_.fee = resource::kFeeModerateBurdenPeer; std::weak_ptr const weak = shared_from_this(); app_.getJobQueue().addJob(JtReplayReq, "RcvReplDReq", [weak, m]() { if (auto peer = weak.lock()) @@ -1591,11 +1630,11 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (reply.error() == protocol::TMReplyError::reBAD_REQUEST) { - peer->charge(Resource::kFeeMalformedRequest, "replay_delta_request"); + peer->charge(resource::kFeeMalformedRequest, "replay_delta_request"); } else { - peer->charge(Resource::kFeeRequestNoReply, "replay_delta_request"); + peer->charge(resource::kFeeRequestNoReply, "replay_delta_request"); } } else @@ -1611,13 +1650,13 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "replay_delta_response disabled"); + fee_.update(resource::kFeeMalformedRequest, "replay_delta_response disabled"); return; } if (!ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + fee_.update(resource::kFeeInvalidData, "replay_delta_response"); } } @@ -1625,7 +1664,7 @@ void PeerImp::onMessage(std::shared_ptr const& m) { auto badData = [&](std::string const& msg) { - fee_.update(Resource::kFeeInvalidData, msg); + fee_.update(resource::kFeeInvalidData, msg); JLOG(pJournal_.warn()) << "TMLedgerData: " << msg; }; @@ -1676,18 +1715,125 @@ PeerImp::onMessage(std::shared_ptr const& m) } // Verify ledger nodes. - if (m->nodes_size() <= 0 || m->nodes_size() > Tuning::kHardMaxReplyNodes) + if (m->nodes_size() <= 0 || m->nodes_size() > tuning::kHardMaxReplyNodes) { badData("Invalid Ledger/TXset nodes " + std::to_string(m->nodes_size())); return; } - // If there is a request cookie, attempt to relay the message + // If there is a request cookie, attempt to relay the message. if (m->has_requestcookie()) { if (auto peer = overlay_.findPeerByShortID(m->requestcookie())) { m->clear_requestcookie(); + + // If the original requester doesn't support the new depth-based format, rewrite any + // nodes that use it back to the legacy nodeid format before relaying. Once all nodes + // have upgraded, the old protocol version and this code can be removed. Make sure that + // the format of the nodes is consistent - either all use the legacy format or the new + // format, unless it is liBASE data in which case none of these fields should be set. + auto const peerSupportsNodeDepth = + peer->supportsFeature(ProtocolFeature::LedgerNodeDepth); + enum class MessageType { Unknown, Base, Legacy, Depth }; + MessageType messageType = MessageType::Unknown; + for (int i = 0; i < m->nodes_size(); ++i) + { + auto* ledgerNode = m->mutable_nodes(i); + + // All nodes should have non-empty data. The field is required so we don't need to + // check for presence first. + if (ledgerNode->nodedata().empty()) + { + badData( + "Received node with empty data while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + MessageType msgType = MessageType::Unknown; + if (m->type() == protocol::liBASE) + { + if (ledgerNode->has_nodeid() || ledgerNode->has_id() || ledgerNode->has_depth()) + { + badData( + "Received liBASE message with node reference while relaying ledger " + "data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + msgType = MessageType::Base; + } + else + { + msgType = ledgerNode->has_nodeid() ? MessageType::Legacy : MessageType::Depth; + } + if (messageType != MessageType::Unknown && messageType != msgType) + { + badData( + "Received mixed mode message while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + messageType = msgType; + + if (peerSupportsNodeDepth || msgType != MessageType::Depth) + continue; + + SOMETIMES( + !peerSupportsNodeDepth, + "xrpl::PeerImp : relaying depth-format ledger data to pre-2.3 peer"); + switch (ledgerNode->reference_case()) + { + case protocol::TMLedgerNode::kId: { + // We can directly copy the `id` field, because it uses the same wire format + // as the legacy `nodeid` field. + REACHABLE("xrpl::PeerImp : relay downgrade id to nodeid"); + ledgerNode->set_nodeid(ledgerNode->id()); + ledgerNode->clear_id(); + break; + } + case protocol::TMLedgerNode::kDepth: { + // We need to regenerate the node ID from the node data and depth. + auto treeNode = getTreeNode(ledgerNode->nodedata()); + if (!treeNode) + { + badData( + "Unable to get tree node while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + auto const nodeID = getSHAMapNodeID(*ledgerNode, *treeNode); + if (!nodeID) + { + badData( + "Unable to get node ID while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + REACHABLE("xrpl::PeerImp : relay downgrade depth to nodeid"); + ledgerNode->set_nodeid(nodeID->getRawString()); + ledgerNode->clear_depth(); + break; + } + default: { + SOMETIMES(true, "xrpl::PeerImp : relay node has empty reference"); + badData( + "Empty node reference while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + } + } + peer->send(std::make_shared(*m, protocol::mtLEDGER_DATA)); } else @@ -1729,14 +1875,14 @@ PeerImp::onMessage(std::shared_ptr const& m) (publicKeyType(makeSlice(set.nodepubkey())) != KeyType::Secp256k1)) { JLOG(pJournal_.warn()) << "Proposal: malformed"; - fee_.update(Resource::kFeeInvalidSignature, " signature can't be longer than 72 bytes"); + fee_.update(resource::kFeeInvalidSignature, " signature can't be longer than 72 bytes"); return; } if (!stringIsUInt256Sized(set.currenttxhash()) || !stringIsUInt256Sized(set.previousledger())) { JLOG(pJournal_.warn()) << "Proposal: malformed"; - fee_.update(Resource::kFeeMalformedRequest, "bad hashes"); + fee_.update(resource::kFeeMalformedRequest, "bad hashes"); return; } @@ -2018,13 +2164,13 @@ PeerImp::checkTracking(std::uint32_t seq1, std::uint32_t seq2) { std::uint32_t const diff = std::max(seq1, seq2) - std::min(seq1, seq2); - if (diff < Tuning::kConvergedLedgerLimit) + if (diff < tuning::kConvergedLedgerLimit) { // The peer's ledger sequence is close to the validation's tracking_ = Tracking::Converged; } - if ((diff > Tuning::kDivergedLedgerLimit) && (tracking_.load() != Tracking::Diverged)) + if ((diff > tuning::kDivergedLedgerLimit) && (tracking_.load() != Tracking::Diverged)) { // The peer's ledger sequence is way off the validation's std::scoped_lock const sl(recentLock_); @@ -2039,7 +2185,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!stringIsUInt256Sized(m->hash())) { - fee_.update(Resource::kFeeMalformedRequest, "bad hash"); + fee_.update(resource::kFeeMalformedRequest, "bad hash"); return; } @@ -2051,7 +2197,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (std::ranges::find(recentTxSets_, hash) != recentTxSets_.end()) { - fee_.update(Resource::kFeeUselessData, "duplicate (tsHAVE)"); + fee_.update(resource::kFeeUselessData, "duplicate (tsHAVE)"); return; } @@ -2072,7 +2218,7 @@ PeerImp::onValidatorListMessage( { JLOG(pJournal_.warn()) << "Ignored malformed " << messageType; // This shouldn't ever happen with a well-behaved peer - fee_.update(Resource::kFeeHeavyBurdenPeer, "no blobs"); + fee_.update(resource::kFeeHeavyBurdenPeer, "no blobs"); return; } @@ -2086,7 +2232,7 @@ PeerImp::onValidatorListMessage( // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, "duplicate"); + fee_.update(resource::kFeeUselessData, "duplicate"); return; } @@ -2177,27 +2323,27 @@ PeerImp::onValidatorListMessage( // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, " duplicate (same_sequence or known_sequence)"); + fee_.update(resource::kFeeUselessData, " duplicate (same_sequence or known_sequence)"); break; case ListDisposition::Stale: // There are very few good reasons for a peer to send an // old list, particularly more than once. - fee_.update(Resource::kFeeInvalidData, "expired"); + fee_.update(resource::kFeeInvalidData, "expired"); break; case ListDisposition::Untrusted: // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, "untrusted"); + fee_.update(resource::kFeeUselessData, "untrusted"); break; case ListDisposition::Invalid: // This shouldn't ever happen with a well-behaved peer - fee_.update(Resource::kFeeInvalidSignature, "invalid list disposition"); + fee_.update(resource::kFeeInvalidSignature, "invalid list disposition"); break; case ListDisposition::UnsupportedVersion: // During a version transition, this may be legitimate. // If it happens frequently, that's probably bad. - fee_.update(Resource::kFeeInvalidData, "version"); + fee_.update(resource::kFeeInvalidData, "version"); break; // LCOV_EXCL_START default: @@ -2265,7 +2411,7 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.debug()) << "ValidatorList: received validator list from peer using " << "protocol version " << to_string(protocol_) << " which shouldn't support this feature."; - fee_.update(Resource::kFeeUselessData, "unsupported peer"); + fee_.update(resource::kFeeUselessData, "unsupported peer"); return; } onValidatorListMessage( @@ -2275,7 +2421,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { JLOG(pJournal_.warn()) << "ValidatorList: Exception, " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeInvalidData, e.what()); + fee_.update(resource::kFeeInvalidData, e.what()); } } @@ -2289,7 +2435,7 @@ PeerImp::onMessage(std::shared_ptr const& m JLOG(pJournal_.debug()) << "ValidatorListCollection: received validator list from peer " << "using protocol version " << to_string(protocol_) << " which shouldn't support this feature."; - fee_.update(Resource::kFeeUselessData, "unsupported peer"); + fee_.update(resource::kFeeUselessData, "unsupported peer"); return; } if (m->version() < 2) @@ -2298,7 +2444,7 @@ PeerImp::onMessage(std::shared_ptr const& m << "ValidatorListCollection: received invalid validator list " "version " << m->version() << " from peer using protocol version " << to_string(protocol_); - fee_.update(Resource::kFeeInvalidData, "wrong version"); + fee_.update(resource::kFeeInvalidData, "wrong version"); return; } onValidatorListMessage( @@ -2308,7 +2454,7 @@ PeerImp::onMessage(std::shared_ptr const& m { JLOG(pJournal_.warn()) << "ValidatorListCollection: Exception, " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeInvalidData, e.what()); + fee_.update(resource::kFeeInvalidData, e.what()); } } @@ -2318,7 +2464,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (m->validation().size() < 50) { JLOG(pJournal_.warn()) << "Validation: Too small"; - fee_.update(Resource::kFeeMalformedRequest, "too small"); + fee_.update(resource::kFeeMalformedRequest, "too small"); return; } @@ -2345,7 +2491,7 @@ PeerImp::onMessage(std::shared_ptr const& m) val->getSeenTime())) { JLOG(pJournal_.trace()) << "Validation: Not current"; - fee_.update(Resource::kFeeUselessData, "not current"); + fee_.update(resource::kFeeUselessData, "not current"); return; } @@ -2414,7 +2560,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { JLOG(pJournal_.warn()) << "Exception processing validation: " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeMalformedRequest, e.what()); + fee_.update(resource::kFeeMalformedRequest, e.what()); } } @@ -2429,7 +2575,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (packet.query()) { // this is a query - if (sendQueue_.size() >= Tuning::kDropSendQueue) + if (sendQueue_.size() >= tuning::kDropSendQueue) { JLOG(pJournal_.debug()) << "GetObject: Large send queue"; return; @@ -2446,7 +2592,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMGetObjectByHash: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2463,19 +2609,19 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!stringIsUInt256Sized(packet.ledgerhash())) { JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; - fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); + fee_.update(resource::kFeeMalformedRequest, "get object ledger hash"); return; } } // Reject oversized requests before touching the NodeStore. // The legitimate upper bound (InboundLedger::getNeededHashes()) // is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming. - if (packet.objects_size() > Tuning::kHardMaxReplyNodes) + if (packet.objects_size() > tuning::kHardMaxReplyNodes) { JLOG(pJournal_.warn()) << "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size() - << " > " << Tuning::kHardMaxReplyNodes << ")"; - fee_.update(Resource::kFeeInvalidData, "oversized get object request"); + << " > " << tuning::kHardMaxReplyNodes << ")"; + fee_.update(resource::kFeeInvalidData, "oversized get object request"); return; } @@ -2497,7 +2643,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // back through the resource model so a misbehaving peer // is still accountable rather than silently dropped. JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what(); - peer->charge(Resource::kFeeRequestNoReply, "get object handler exception"); + peer->charge(resource::kFeeRequestNoReply, "get object handler exception"); } }); if (!queued) @@ -2514,7 +2660,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // uncharged DoS window. Charge the base burden up-front (after // a successful enqueue); the per-lookup differential is added // in the worker. - fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request"); + fee_.update(resource::kFeeModerateBurdenPeer, "received a get object by hash request"); } else { @@ -2593,7 +2739,7 @@ PeerImp::processGetObjectByHash(std::shared_ptr con // a peer cannot drive unbounded NodeStore lookups by sending // non-existent hashes. int const requested = packet.objects_size(); - int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes); + int const iterLimit = std::min(requested, tuning::kHardMaxReplyNodes); for (int i = 0; i < iterLimit; ++i) { @@ -2624,7 +2770,7 @@ PeerImp::processGetObjectByHash(std::shared_ptr con // JobQueue worker thread. charge( // We pass `requested` directly here, instead of actual lookups done. Which could be - // std::min(packet.objects_size(), static_cast(Tuning::kHardMaxReplyNodes)); + // std::min(packet.objects_size(), static_cast(tuning::kHardMaxReplyNodes)); // Because we want to charge as per the request size, to discourage large requests. computeGetObjectByHashFee(requested, reply.objects_size()), "processed get object by hash request"); @@ -2639,7 +2785,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMHaveTransactions: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2664,7 +2810,7 @@ PeerImp::handleHaveTransactions(std::shared_ptr co if (!stringIsUInt256Sized(m->hashes(i))) { JLOG(pJournal_.error()) << "TMHaveTransactions with invalid hash size"; - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } @@ -2702,7 +2848,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMTransactions: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2726,14 +2872,14 @@ PeerImp::onMessage(std::shared_ptr const& m) dispatch(strand_, [self = shared_from_this(), m]() { if (!m->has_validatorpubkey()) { - self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); + self->fee_.update(resource::kFeeInvalidData, "squelch no pubkey"); return; } auto validator = m->validatorpubkey(); auto const slice{makeSlice(validator)}; if (!publicKeyType(slice)) { - self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); + self->fee_.update(resource::kFeeInvalidData, "squelch bad pubkey"); return; } PublicKey const key(slice); @@ -2753,7 +2899,7 @@ PeerImp::onMessage(std::shared_ptr const& m) } else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration})) { - self->fee_.update(Resource::kFeeInvalidData, "squelch duration"); + self->fee_.update(resource::kFeeInvalidData, "squelch duration"); } JLOG(self->pJournal_.debug()) @@ -2793,11 +2939,11 @@ PeerImp::doFetchPack(std::shared_ptr const& packet) if (!stringIsUInt256Sized(packet->ledgerhash())) { JLOG(pJournal_.warn()) << "FetchPack hash size malformed"; - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } - fee_.fee = Resource::kFeeHeavyBurdenPeer; + fee_.fee = resource::kFeeHeavyBurdenPeer; uint256 const hash = uint256::fromRaw(packet->ledgerhash()); @@ -2820,7 +2966,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack if (packet->objects_size() > reduce_relay::kMaxTxQueueSize) { JLOG(pJournal_.error()) << "doTransactions, invalid number of hashes"; - fee_.update(Resource::kFeeMalformedRequest, "too big"); + fee_.update(resource::kFeeMalformedRequest, "too big"); return; } @@ -2830,7 +2976,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack if (!stringIsUInt256Sized(obj.hash())) { - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } @@ -2842,7 +2988,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack { JLOG(pJournal_.error()) << "doTransactions, transaction not found " << Slice(hash.data(), hash.size()); - fee_.update(Resource::kFeeMalformedRequest, "tx not found"); + fee_.update(resource::kFeeMalformedRequest, "tx not found"); return; } @@ -2893,7 +3039,7 @@ PeerImp::checkTransaction( { JLOG(pJournal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (checkSignature)."; - charge(Resource::kFeeModerateBurdenPeer, "inner batch txn"); + charge(resource::kFeeModerateBurdenPeer, "inner batch txn"); return; } // LCOV_EXCL_STOP @@ -2905,7 +3051,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.info()) << "Marking transaction " << stx->getTransactionID() << "as BAD because it's expired"; app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeUselessData, "expired tx"); + charge(resource::kFeeUselessData, "expired tx"); return; } @@ -2936,7 +3082,7 @@ PeerImp::checkTransaction( if (!batch) { JLOG(pJournal_.debug()) << "Charging for pseudo-transaction tx " << tx->getID(); - charge(Resource::kFeeUselessData, "pseudo tx"); + charge(resource::kFeeUselessData, "pseudo tx"); } return; @@ -2958,7 +3104,7 @@ PeerImp::checkTransaction( // Probably not necessary to set HashRouterFlags::BAD, but // doesn't hurt. app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeInvalidSignature, "check transaction signature failure"); + charge(resource::kFeeInvalidSignature, "check transaction signature failure"); return; } } @@ -2977,7 +3123,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.debug()) << "Exception checking transaction: " << reason; } app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeInvalidSignature, "tx (impossible)"); + charge(resource::kFeeInvalidSignature, "tx (impossible)"); return; } @@ -2989,7 +3135,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.warn()) << "Exception in " << __func__ << ": " << ex.what(); app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); using namespace std::string_literals; - charge(Resource::kFeeInvalidData, "tx "s + ex.what()); + charge(resource::kFeeInvalidData, "tx "s + ex.what()); } } @@ -3008,7 +3154,7 @@ PeerImp::checkPropose( { std::string const desc{"Proposal fails sig check"}; JLOG(pJournal_.warn()) << desc; - charge(Resource::kFeeInvalidSignature, desc); + charge(resource::kFeeInvalidSignature, desc); return; } @@ -3052,7 +3198,7 @@ PeerImp::checkValidation( { std::string const desc{"Validation forwarded by peer is invalid"}; JLOG(pJournal_.debug()) << desc; - charge(Resource::kFeeInvalidSignature, desc); + charge(resource::kFeeInvalidSignature, desc); return; } @@ -3077,7 +3223,7 @@ PeerImp::checkValidation( { JLOG(pJournal_.trace()) << "Exception processing validation: " << ex.what(); using namespace std::string_literals; - charge(Resource::kFeeMalformedRequest, "validation "s + ex.what()); + charge(resource::kFeeMalformedRequest, "validation "s + ex.what()); } } @@ -3234,7 +3380,7 @@ PeerImp::getLedger(std::shared_ptr const& m) { // Do not resource charge a peer responding to a relay if (!m->has_requestcookie()) - charge(Resource::kFeeMalformedRequest, "get_ledger ledgerSeq"); + charge(resource::kFeeMalformedRequest, "get_ledger ledgerSeq"); ledger.reset(); JLOG(pJournal_.warn()) << "getLedger: Invalid ledger sequence " << ledgerSeq; @@ -3287,12 +3433,10 @@ PeerImp::getTxSet(std::shared_ptr const& m) const } void -PeerImp::processLedgerRequest(std::shared_ptr const& m) +PeerImp::processLedgerRequest( + std::shared_ptr const& m, + std::vector nodeIDs) { - // Do not resource charge a peer responding to a relay - if (!m->has_requestcookie()) - charge(Resource::kFeeModerateBurdenPeer, "received a get ledger request"); - std::shared_ptr ledger; std::shared_ptr sharedMap; SHAMap const* map{nullptr}; @@ -3318,7 +3462,7 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) } else { - if (sendQueue_.size() >= Tuning::kDropSendQueue) + if (sendQueue_.size() >= tuning::kDropSendQueue) { JLOG(pJournal_.debug()) << "processLedgerRequest: Large send queue"; return; @@ -3372,37 +3516,54 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) } // Add requested node data to reply - if (m->nodeids_size() > 0) + if (!nodeIDs.empty()) { std::uint32_t const defaultDepth = isHighLatency() ? 2 : 1; auto const queryDepth{m->has_querydepth() ? m->querydepth() : defaultDepth}; - std::vector> data; + std::vector data; + data.reserve(tuning::kSoftMaxReplyNodes); + auto const useLedgerNodeDepth = supportsFeature(ProtocolFeature::LedgerNodeDepth); - for (int i = 0; - i < m->nodeids_size() && ledgerData.nodes_size() < Tuning::kSoftMaxReplyNodes; - ++i) + for (auto const& nodeID : nodeIDs) { - auto const shaMapNodeId{deserializeSHAMapNodeID(m->nodeids(i))}; + if (ledgerData.nodes_size() >= tuning::kSoftMaxReplyNodes) + break; data.clear(); - data.reserve(Tuning::kSoftMaxReplyNodes); try { - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) nodeids checked in onGetLedger - if (map->getNodeFat(*shaMapNodeId, data, fatLeaves, queryDepth)) + if (map->getNodeFat(nodeID, data, fatLeaves, queryDepth)) { JLOG(pJournal_.trace()) << "processLedgerRequest: getNodeFat got " << data.size() << " nodes"; for (auto const& d : data) { - if (ledgerData.nodes_size() >= Tuning::kHardMaxReplyNodes) + if (ledgerData.nodes_size() >= tuning::kHardMaxReplyNodes) break; + protocol::TMLedgerNode* node{ledgerData.add_nodes()}; - node->set_nodeid(d.first.getRawString()); - node->set_nodedata(d.second.data(), d.second.size()); + node->set_nodedata(d.data.data(), d.data.size()); + + // When the LedgerNodeDepth protocol feature is not supported by the peer, + // we always set the `nodeid` field. However, when it is supported then we + // set the `id` field for inner nodes and the `depth` field for leaf nodes. + if (!useLedgerNodeDepth) + { + node->set_nodeid(d.nodeID.getRawString()); + } + else if (d.isLeaf) + { + REACHABLE("xrpl::PeerImp : emit leaf depth in reply"); + node->set_depth(d.nodeID.getDepth()); + } + else + { + REACHABLE("xrpl::PeerImp : emit inner id in reply"); + node->set_id(d.nodeID.getRawString()); + } } } else @@ -3441,13 +3602,13 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) info += ", no hash specified"; JLOG(pJournal_.warn()) - << "processLedgerRequest: getNodeFat with nodeId " << *shaMapNodeId + << "processLedgerRequest: getNodeFat with nodeId " << nodeID << " and ledger info type " << info << " throws exception: " << e.what(); } } JLOG(pJournal_.info()) << "processLedgerRequest: Got request for " << m->nodeids_size() - << " nodes at depth " << queryDepth << ", return " + << " node IDs at depth " << queryDepth << ", return " << ledgerData.nodes_size() << " nodes"; } @@ -3478,30 +3639,30 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) // // Misses are billed first against the billable budget because a node store // seek dominates a cache hit and because invalid hashes are ~100% miss by construction. -Resource::Charge +resource::Charge PeerImp::computeGetObjectByHashFee(int const requested, int const found) { - int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); + int const billable = std::max(0, requested - static_cast(tuning::kFreeObjectsPerRequest)); // Clamp `missed` so a future caller passing found > requested cannot // produce a negative value that flips the hits/misses split. int const missed = std::max(0, requested - found); int const billableMisses = std::min(missed, billable); int const billableHits = billable - billableMisses; - int sizeBand = Tuning::kCostBandSmall; - if (requested > Tuning::kBandMediumMax) + int sizeBand = tuning::kCostBandSmall; + if (requested > tuning::kBandMediumMax) { - sizeBand = Tuning::kCostBandLarge; + sizeBand = tuning::kCostBandLarge; } - else if (requested > Tuning::kBandSmallMax) + else if (requested > tuning::kBandSmallMax) { - sizeBand = Tuning::kCostBandMedium; + sizeBand = tuning::kCostBandMedium; } - int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + - (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; + int const dynamic = (billableHits * tuning::kCostPerLookupHit) + + (billableMisses * tuning::kCostPerLookupMiss) + sizeBand; - return Resource::Charge(dynamic, "GetObject differential"); + return resource::Charge(dynamic, "GetObject differential"); } int diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 90f8a917f4..3fcfe6359a 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -96,7 +97,7 @@ private: // Updated at each stage of the connection process to reflect // the current conditions as closely as possible. - beast::IP::Endpoint const remoteAddress_; + beast::ip::Endpoint const remoteAddress_; // These are up here to prevent warnings about order of initializations // @@ -160,11 +161,11 @@ private: struct ChargeWithContext { - Resource::Charge fee = Resource::kFeeTrivialPeer; + resource::Charge fee = resource::kFeeTrivialPeer; std::string context{}; // NOLINT(readability-redundant-member-init) void - update(Resource::Charge f, std::string const& add) + update(resource::Charge f, std::string const& add) { XRPL_ASSERT(f >= fee, "xrpl::PeerImp::ChargeWithContext::update : fee increases"); fee = f; @@ -178,7 +179,7 @@ private: std::mutex mutable recentLock_; protocol::TMStatusChange lastStatus_; - Resource::Consumer usage_; + resource::Consumer usage_; ChargeWithContext fee_; // One-shot guard so concurrent JobQueue workers cannot double-count @@ -186,7 +187,7 @@ private: // post duplicate fail() calls) when several queued requests cross // kDropThreshold before the first fail() lands on the strand. std::atomic chargeDisconnectFired_{false}; - std::shared_ptr const slot_; + std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; http_response_type response_; @@ -257,11 +258,11 @@ public: PeerImp( Application& app, id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay); @@ -274,9 +275,9 @@ public: Application& app, std::unique_ptr&& streamPtr, Buffers const& buffers, - std::shared_ptr&& slot, + std::shared_ptr&& slot, http_response_type&& response, - Resource::Consumer usage, + resource::Consumer usage, PublicKey const& publicKey, ProtocolVersion protocol, id_t id, @@ -290,7 +291,7 @@ public: return pJournal_; } - std::shared_ptr const& + std::shared_ptr const& slot() { return slot_; @@ -338,16 +339,18 @@ public: void sendEndpoints(FwdIt first, FwdIt last) requires( - std::is_same_v::value_type, PeerFinder::Endpoint>); + std::is_same_v< // + typename std::iterator_traits::value_type, + peer_finder::Endpoint>); - beast::IP::Endpoint + beast::ip::Endpoint getRemoteAddress() const override { return remoteAddress_; } void - charge(Resource::Charge const& fee, std::string const& context) override; + charge(resource::Charge const& fee, std::string const& context) override; // // Identity @@ -679,7 +682,9 @@ private: getTxSet(std::shared_ptr const& m) const; void - processLedgerRequest(std::shared_ptr const& m); + processLedgerRequest( + std::shared_ptr const& m, + std::vector nodeIDs); protected: // Kept `protected` so test subclasses (see @@ -694,7 +699,7 @@ protected: * * Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue * (`JtLedgerReq`) so synchronous NodeStore lookups do not block the - * peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes` + * peer's I/O strand. Caps iteration at `tuning::kHardMaxReplyNodes` * regardless of hit/miss outcome and applies differential pricing * via `computeGetObjectByHashFee()` after the fetch loop completes. * @@ -708,25 +713,25 @@ protected: * request based on how much work was actually performed. * * The charge has three components on top of the base - * `Resource::kFeeModerateBurdenPeer`: + * `resource::kFeeModerateBurdenPeer`: * - per-hit lookup cost (cheap; usually served from cache) * - per-miss lookup cost (expensive node store seeks) * - request-size band surcharge (escalates abusive batch sizes) * - * The first `Tuning::kFreeObjectsPerRequest` objects are free so + * The first `tuning::kFreeObjectsPerRequest` objects are free so * that legitimate `InboundLedger::getNeededHashes()` traffic * (at most 8 objects) is unaffected. * * @param requested Number of objects requested by the message. This * value is used for request-size pricing and may - * exceed `Tuning::kHardMaxReplyNodes` when this + * exceed `tuning::kHardMaxReplyNodes` when this * helper is called directly, even though processing - * caps the iterations to `Tuning::kHardMaxReplyNodes`. + * caps the iterations to `tuning::kHardMaxReplyNodes`. * @param found Number of objects successfully returned in the * reply. - * @return A `Resource::Charge` whose cost reflects the work performed. + * @return A `resource::Charge` whose cost reflects the work performed. */ - static Resource::Charge + static resource::Charge computeGetObjectByHashFee(int const requested, int const found); /** @@ -737,9 +742,9 @@ protected: * full JobQueue handler. Production callers should never read this back — * the value is consumed by `charge()`/`disconnect()` internally. * - * @return The current `Resource::Charge` accumulated on `fee_`. + * @return The current `resource::Charge` accumulated on `fee_`. */ - Resource::Charge + resource::Charge currentFeeCharge() const { return fee_.fee; @@ -753,9 +758,9 @@ PeerImp::PeerImp( Application& app, std::unique_ptr&& streamPtr, Buffers const& buffers, - std::shared_ptr&& slot, + std::shared_ptr&& slot, http_response_type&& response, - Resource::Consumer usage, + resource::Consumer usage, PublicKey const& publicKey, ProtocolVersion protocol, id_t id, @@ -785,7 +790,7 @@ PeerImp::PeerImp( , creationTime_(clock_type::now()) , squelch_(app_.getJournal("Squelch")) , usage_(usage) - , fee_{.fee = Resource::kFeeTrivialPeer} + , fee_{.fee = resource::kFeeTrivialPeer} , slot_(std::move(slot)) , response_(std::move(response)) , headers_(response_) @@ -812,7 +817,8 @@ PeerImp::PeerImp( template void PeerImp::sendEndpoints(FwdIt first, FwdIt last) - requires(std::is_same_v::value_type, PeerFinder::Endpoint>) + requires( + std::is_same_v::value_type, peer_finder::Endpoint>) { protocol::TMEndpoints tm; diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp index 347e59accb..2d5d0a56f7 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.cpp +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp @@ -29,6 +29,7 @@ namespace xrpl { constexpr ProtocolVersion const kSupportedProtocolList[]{ {2, 1}, {2, 2}, + {2, 3}, }; // This ugly construct ensures that supportedProtocolList is sorted in strictly diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index 5488fab07b..7561a4f385 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -5,7 +5,7 @@ #include #include -namespace xrpl::Tuning { +namespace xrpl::tuning { /** * How many ledgers off a server can be and we will @@ -74,7 +74,7 @@ constexpr std::size_t kReadBufferBytes = 16384; * while a hit is usually served from cache. On top of that, a size-band * surcharge kicks in for larger requests so an attacker who crams a * single message with thousands of hashes blows past - * `Resource::kDropThreshold` and gets disconnected. + * `resource::kDropThreshold` and gets disconnected. * * The numbers below are picked to keep three things true given * `kDropThreshold = 25000`: @@ -165,4 +165,4 @@ static constexpr auto kLegitHashesPerType = 4; static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; -} // namespace xrpl::Tuning +} // namespace xrpl::tuning diff --git a/src/xrpld/overlay/make_Overlay.h b/src/xrpld/overlay/make_Overlay.h index a62d4b49de..c730a05c54 100644 --- a/src/xrpld/overlay/make_Overlay.h +++ b/src/xrpld/overlay/make_Overlay.h @@ -27,7 +27,7 @@ makeOverlay( Application& app, Overlay::Setup const& setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index f96ea31943..5934e3bc4a 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -6,7 +6,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Config makeConfig( @@ -16,4 +16,4 @@ makeConfig( int ipLimit, bool verifyEndpoints); -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index a893b969e0..b222f6c077 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -5,7 +5,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Config makeConfig( @@ -36,4 +36,4 @@ makeConfig( verifyEndpoints); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h index b0973a42b3..ce13d72c15 100644 --- a/src/xrpld/peerfinder/detail/StoreSqdb.h +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h @@ -14,7 +14,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Database persistence for PeerFinder using SQLite @@ -50,7 +50,7 @@ public: std::size_t n(0); readPeerFinderDB(sqlDb_, [&](std::string const& s, int valence) { - beast::IP::Endpoint const endpoint(beast::IP::Endpoint::fromString(s)); + beast::ip::Endpoint const endpoint(beast::ip::Endpoint::fromString(s)); if (!isUnspecified(endpoint)) { @@ -90,4 +90,4 @@ private: } }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/perflog/detail/PerfLogImp.h b/src/xrpld/perflog/detail/PerfLogImp.h index 14477512ff..7efdbe1b7f 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.h +++ b/src/xrpld/perflog/detail/PerfLogImp.h @@ -109,7 +109,7 @@ class PerfLogImp : public PerfLog Application& app_; beast::Journal const j_; std::function const signalStop_; - Counters counters_{xrpl::RPC::getHandlerNames(), JobTypes::instance()}; + Counters counters_{xrpl::rpc::getHandlerNames(), JobTypes::instance()}; std::ofstream logFile_; std::thread thread_; std::mutex mutex_; diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h index 3c10ece78f..16f7ea8e43 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -32,7 +32,7 @@ class Transaction; class TxMeta; class STTx; -namespace RPC { +namespace rpc { template json::Value @@ -233,5 +233,5 @@ computeBookChanges(std::shared_ptr const& lpAccepted) return jvObj; } -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index 7566e0e143..71ded5834f 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { // CTID stands for Concise Transaction ID. // @@ -111,4 +111,4 @@ decodeCTID(T const ctid) noexcept return std::make_tuple(ledgerSeq, txnIndex, networkID); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Context.h b/src/xrpld/rpc/Context.h index 81ba068d8f..e85bf6dfe6 100644 --- a/src/xrpld/rpc/Context.h +++ b/src/xrpld/rpc/Context.h @@ -18,7 +18,7 @@ class Application; class NetworkOPs; class LedgerMaster; -namespace RPC { +namespace rpc { /** * The context of information needed to call an RPC. @@ -27,10 +27,10 @@ struct Context { beast::Journal const j; Application& app; - Resource::Charge& loadType; + resource::Charge& loadType; NetworkOPs& netOps; LedgerMaster& ledgerMaster; - Resource::Consumer& consumer; + resource::Consumer& consumer; Role role; std::shared_ptr coro; InfoSub::pointer infoSub; @@ -59,5 +59,5 @@ struct GRPCContext : public Context RequestType params; }; -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/DeliveredAmount.h b/src/xrpld/rpc/DeliveredAmount.h index dc635c6861..a7878070d6 100644 --- a/src/xrpld/rpc/DeliveredAmount.h +++ b/src/xrpld/rpc/DeliveredAmount.h @@ -17,7 +17,7 @@ class Transaction; class TxMeta; class STTx; -namespace RPC { +namespace rpc { struct JsonContext; @@ -41,23 +41,23 @@ insertDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const&, + rpc::JsonContext const&, std::shared_ptr const&, TxMeta const&); void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const&, + rpc::JsonContext const&, std::shared_ptr const&, TxMeta const&); std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, LedgerIndex const& ledgerIndex); /** @} */ -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/GRPCHandlers.h b/src/xrpld/rpc/GRPCHandlers.h index 9dc7e0b13a..cabd55d53b 100644 --- a/src/xrpld/rpc/GRPCHandlers.h +++ b/src/xrpld/rpc/GRPCHandlers.h @@ -14,22 +14,22 @@ namespace xrpl { /* * These handlers are for gRPC. They each take in a protobuf message that is - * nested inside RPC::GRPCContext, where T is the request type + * nested inside rpc::GRPCContext, where T is the request type * The return value is the response type, as well as a status * If the status is not Status::OK (meaning an error occurred), then only * the status will be sent to the client, and the response will be omitted */ std::pair -doLedgerGrpc(RPC::GRPCContext& context); +doLedgerGrpc(rpc::GRPCContext& context); std::pair -doLedgerEntryGrpc(RPC::GRPCContext& context); +doLedgerEntryGrpc(rpc::GRPCContext& context); std::pair -doLedgerDataGrpc(RPC::GRPCContext& context); +doLedgerDataGrpc(rpc::GRPCContext& context); std::pair -doLedgerDiffGrpc(RPC::GRPCContext& context); +doLedgerDiffGrpc(rpc::GRPCContext& context); } // namespace xrpl diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index f56826bfb8..678fda0369 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -8,7 +8,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Add a `mpt_issuance_id` field to the `meta` input/output parameter. @@ -32,4 +32,4 @@ insertMPTokenIssuanceID( TxMeta const& transactionMeta); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Output.h b/src/xrpld/rpc/Output.h index 30b5c090d7..f528efef56 100644 --- a/src/xrpld/rpc/Output.h +++ b/src/xrpld/rpc/Output.h @@ -5,7 +5,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { using Output = std::function; @@ -15,4 +15,4 @@ stringOutput(std::string& s) return [&](boost::string_ref const& b) { s.append(b.data(), b.size()); }; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/RPCCall.h b/src/xrpld/rpc/RPCCall.h index a72b35e344..2fec78f93b 100644 --- a/src/xrpld/rpc/RPCCall.h +++ b/src/xrpld/rpc/RPCCall.h @@ -26,7 +26,7 @@ namespace xrpl { /** * Processes XRPL RPC calls. */ -namespace RPCCall { +namespace rpc_call { int fromCommandLine(Config const& config, std::vector const& vCmd, Logs& logs); @@ -47,7 +47,7 @@ fromNetwork( std::function callbackFuncP = std::function(), std::unordered_map headers = {}); -} // namespace RPCCall +} // namespace rpc_call json::Value rpcCmdToJson( diff --git a/src/xrpld/rpc/RPCHandler.h b/src/xrpld/rpc/RPCHandler.h index fcd0f54265..637a492943 100644 --- a/src/xrpld/rpc/RPCHandler.h +++ b/src/xrpld/rpc/RPCHandler.h @@ -8,7 +8,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct JsonContext; @@ -16,9 +16,9 @@ struct JsonContext; * Execute an RPC command and store the results in a json::Value. */ Status -doCommand(RPC::JsonContext&, json::Value&); +doCommand(rpc::JsonContext&, json::Value&); Role roleRequired(unsigned int version, bool betaEnabled, std::string const& method); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Role.h b/src/xrpld/rpc/Role.h index 2c2ae6b781..48c89333bd 100644 --- a/src/xrpld/rpc/Role.h +++ b/src/xrpld/rpc/Role.h @@ -40,13 +40,13 @@ requestRole( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string_view user); -Resource::Consumer +resource::Consumer requestInboundEndpoint( - Resource::Manager& manager, - beast::IP::Endpoint const& remoteAddress, + resource::Manager& manager, + beast::ip::Endpoint const& remoteAddress, Role const& role, std::string_view user, std::string_view forwardedFor); @@ -66,7 +66,7 @@ isUnlimited(Role const& role); */ bool ipAllowed( - beast::IP::Address const& remoteIp, + beast::ip::Address const& remoteIp, std::vector const& nets4, std::vector const& nets6); diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h index 054bec9b5b..a09fc1c18a 100644 --- a/src/xrpld/rpc/ServerHandler.h +++ b/src/xrpld/rpc/ServerHandler.h @@ -80,7 +80,7 @@ private: using stream_type = boost::beast::ssl_stream; Application& app_; - Resource::Manager& resourceManager_; + resource::Manager& resourceManager_; beast::Journal journal_; NetworkOPs& networkOPs_; std::unique_ptr server_; @@ -109,7 +109,7 @@ private: boost::asio::io_context&, JobQueue&, NetworkOPs&, - Resource::Manager&, + resource::Manager&, CollectorManager& cm); public: @@ -120,7 +120,7 @@ public: boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm); ~ServerHandler(); @@ -196,7 +196,7 @@ private: processRequest( Port const& port, std::string const& request, - beast::IP::Endpoint const& remoteIPAddress, + beast::ip::Endpoint const& remoteIPAddress, Output const&, std::shared_ptr coro, std::string_view forwardedFor, @@ -215,7 +215,7 @@ makeServerHandler( boost::asio::io_context&, JobQueue&, NetworkOPs&, - Resource::Manager&, + resource::Manager&, CollectorManager& cm); } // namespace xrpl diff --git a/src/xrpld/rpc/Status.h b/src/xrpld/rpc/Status.h index dda1e89d31..e2716bd579 100644 --- a/src/xrpld/rpc/Status.h +++ b/src/xrpld/rpc/Status.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Status represents the results of an operation that might fail. @@ -56,9 +56,11 @@ public: { } - /* Returns a representation of the integer status Code as a string. - If the Status is OK, the result is an empty string. - */ + /** + * If the Status is OK, the result is an empty string. + * + * @return a representation of the integer status Code as a string. + */ [[nodiscard]] std::string codeString() const; @@ -86,7 +88,7 @@ public: [[nodiscard]] TER toTER() const { - XRPL_ASSERT(type_ == Type::TER, "xrpl::RPC::Status::toTER : type is TER"); + XRPL_ASSERT(type_ == Type::TER, "xrpl::rpc::Status::toTER : type is TER"); return TER::fromInt(code_); } @@ -97,7 +99,8 @@ public: [[nodiscard]] ErrorCodeI toErrorCode() const { - XRPL_ASSERT(type_ == Type::ErrorCodeI, "xrpl::RPC::Status::toTER : type is error code"); + XRPL_ASSERT( + type_ == Type::ErrorCodeI, "xrpl::rpc::Status::toErrorCode : type is error code"); return ErrorCodeI(code_); } @@ -155,4 +158,4 @@ private: Strings messages_; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp b/src/xrpld/rpc/detail/DeliveredAmount.cpp index 8d8aac33bf..7b8c5e0623 100644 --- a/src/xrpld/rpc/detail/DeliveredAmount.cpp +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /* GetLedgerIndex and GetCloseTime are lambdas that allow the close time and @@ -114,7 +114,7 @@ insertDeliveredAmount( template static std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, GetLedgerIndex const& getLedgerIndex) @@ -133,7 +133,7 @@ getDeliveredAmount( std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, LedgerIndex const& ledgerIndex) @@ -145,7 +145,7 @@ getDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const& context, + rpc::JsonContext const& context, std::shared_ptr const& transaction, TxMeta const& transactionMeta) { @@ -155,7 +155,7 @@ insertDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const& context, + rpc::JsonContext const& context, std::shared_ptr const& transaction, TxMeta const& transactionMeta) { @@ -178,4 +178,4 @@ insertDeliveredAmount( } } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 4f5ce34c1f..326af4f4ee 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -18,7 +18,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { /** @@ -33,8 +33,8 @@ byRef(Function const& f) if (result.type() != json::ValueType::Object) { // LCOV_EXCL_START - UNREACHABLE("xrpl::RPC::byRef : result is object"); - result = RPC::makeObjectValue(result); + UNREACHABLE("xrpl::rpc::byRef : result is object"); + result = rpc::makeObjectValue(result); // LCOV_EXCL_STOP } @@ -49,7 +49,7 @@ handle(JsonContext& context, Object& object) XRPL_ASSERT( context.apiVersion >= HandlerImpl::minApiVer && context.apiVersion <= HandlerImpl::maxApiVer, - "xrpl::RPC::handle : valid API version"); + "xrpl::rpc::handle : valid API version"); HandlerImpl handler(context); auto status = handler.check(); @@ -382,10 +382,10 @@ private: unsigned minVer, unsigned maxVer) { - XRPL_ASSERT(minVer <= maxVer, "xrpl::RPC::HandlerTable : valid API version range"); + XRPL_ASSERT(minVer <= maxVer, "xrpl::rpc::HandlerTable : valid API version range"); XRPL_ASSERT( - maxVer <= RPC::kApiMaximumValidVersion, - "xrpl::RPC::HandlerTable : valid max API version"); + maxVer <= rpc::kApiMaximumValidVersion, + "xrpl::rpc::HandlerTable : valid max API version"); return std::any_of( range.first, @@ -427,8 +427,8 @@ public: [[nodiscard]] Handler const* getHandler(unsigned version, bool betaEnabled, std::string const& name) const { - if (version < RPC::kApiMinimumSupportedVersion || - version > (betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion)) + if (version < rpc::kApiMinimumSupportedVersion || + version > (betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion)) return nullptr; auto const range = table_.equal_range(name); @@ -457,8 +457,8 @@ private: addHandler() { static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer); - static_assert(HandlerImpl::maxApiVer <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer); + static_assert(HandlerImpl::maxApiVer <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer); if (overlappingApiVersion( table_.equal_range(HandlerImpl::name), @@ -488,4 +488,4 @@ getHandlerNames() return HandlerTable::instance().getHandlerNames(); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/Handler.h b/src/xrpld/rpc/detail/Handler.h index 37259c8648..7342c5fcbf 100644 --- a/src/xrpld/rpc/detail/Handler.h +++ b/src/xrpld/rpc/detail/Handler.h @@ -20,7 +20,7 @@ namespace json { class Object; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { // Under what condition can we call this RPC? enum class Condition { @@ -38,7 +38,7 @@ struct Handler char const* name; Method valueMethod; Role role; - RPC::Condition condition; + rpc::Condition condition; unsigned minApiVer = kApiMinimumSupportedVersion; unsigned maxApiVer = kApiMaximumValidVersion; @@ -92,7 +92,7 @@ conditionMet(Condition conditionRequired, T& context) if (!context.app.config().standalone() && conditionRequired != Condition::NoCondition) { - if (context.ledgerMaster.getValidatedLedgerAge() > Tuning::kMaxValidatedLedgerAge) + if (context.ledgerMaster.getValidatedLedgerAge() > tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return RpcNoCurrent; @@ -122,4 +122,4 @@ conditionMet(Condition conditionRequired, T& context) return RpcSuccess; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/LegacyPathFind.cpp b/src/xrpld/rpc/detail/LegacyPathFind.cpp index 0bfa19a1f4..837d98084e 100644 --- a/src/xrpld/rpc/detail/LegacyPathFind.cpp +++ b/src/xrpld/rpc/detail/LegacyPathFind.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { LegacyPathFind::LegacyPathFind(bool isAdmin, Application& app) { @@ -21,13 +21,13 @@ LegacyPathFind::LegacyPathFind(bool isAdmin, Application& app) } auto const& jobCount = app.getJobQueue().getJobCountGE(JtClient); - if (jobCount > Tuning::kMaxPathfindJobCount || app.getFeeTrack().isLoadedLocal()) + if (jobCount > tuning::kMaxPathfindJobCount || app.getFeeTrack().isLoadedLocal()) return; while (true) { int prevVal = inProgress.load(); - if (prevVal >= Tuning::kMaxPathfindsInProgress) + if (prevVal >= tuning::kMaxPathfindsInProgress) return; if (inProgress.compare_exchange_strong( @@ -47,4 +47,4 @@ LegacyPathFind::~LegacyPathFind() std::atomic LegacyPathFind::inProgress(0); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/LegacyPathFind.h b/src/xrpld/rpc/detail/LegacyPathFind.h index 226191848f..30e176f245 100644 --- a/src/xrpld/rpc/detail/LegacyPathFind.h +++ b/src/xrpld/rpc/detail/LegacyPathFind.h @@ -6,7 +6,7 @@ namespace xrpl { class Application; -namespace RPC { +namespace rpc { class LegacyPathFind { @@ -26,5 +26,5 @@ private: bool isOk_{false}; }; -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index e34980aee2..4f57bab9ab 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { bool canHaveMPTokenIssuanceID( @@ -67,4 +67,4 @@ insertMPTokenIssuanceID( response[jss::mpt_issuance_id] = to_string(result.value()); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/PathRequest.cpp b/src/xrpld/rpc/detail/PathRequest.cpp index 3c09917dad..fb132199bc 100644 --- a/src/xrpld/rpc/detail/PathRequest.cpp +++ b/src/xrpld/rpc/detail/PathRequest.cpp @@ -73,7 +73,7 @@ PathRequest::PathRequest( PathRequest::PathRequest( Application& app, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, int id, PathRequestManager& owner, beast::Journal journal) @@ -344,7 +344,7 @@ PathRequest::parseJson(json::Value const& jvParams) { json::Value const& jvSrcCurrencies = jvParams[jss::source_currencies]; if (!jvSrcCurrencies.isArray() || jvSrcCurrencies.size() == 0 || - jvSrcCurrencies.size() > RPC::Tuning::kMaxSrcCur) + jvSrcCurrencies.size() > rpc::tuning::kMaxSrcCur) { jvStatus_ = rpcError(RpcSrcCurMalformed); return PFR_PJ_INVALID; @@ -556,7 +556,7 @@ PathRequest::findPaths( [&](TAsset const& a) { if (!sameAccount || a != saDstAmount_.asset()) { - if (sourceAssets.size() >= RPC::Tuning::kMaxAutoSrcCur) + if (sourceAssets.size() >= rpc::tuning::kMaxAutoSrcCur) return false; if constexpr (std::is_same_v) { diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h index d40d9c82d6..f56b3d0652 100644 --- a/src/xrpld/rpc/detail/PathRequest.h +++ b/src/xrpld/rpc/detail/PathRequest.h @@ -64,7 +64,7 @@ public: PathRequest( Application& app, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, int id, PathRequestManager&, beast::Journal journal); @@ -137,7 +137,7 @@ private: std::weak_ptr wpSubscriber_; // Who this request came from std::function fCompletion_; - Resource::Consumer& consumer_; // Charge according to source currencies + resource::Consumer& consumer_; // Charge according to source currencies json::Value jvId_; json::Value jvStatus_; // Last result diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp b/src/xrpld/rpc/detail/PathRequestManager.cpp index 4953634181..117bfbda1e 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.cpp +++ b/src/xrpld/rpc/detail/PathRequestManager.cpp @@ -254,7 +254,7 @@ json::Value PathRequestManager::makeLegacyPathRequest( PathRequest::pointer& req, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request) { @@ -285,7 +285,7 @@ PathRequestManager::makeLegacyPathRequest( json::Value PathRequestManager::doLegacyPathRequest( - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request) { diff --git a/src/xrpld/rpc/detail/PathRequestManager.h b/src/xrpld/rpc/detail/PathRequestManager.h index f6eb80d291..29a80e66c0 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.h +++ b/src/xrpld/rpc/detail/PathRequestManager.h @@ -65,7 +65,7 @@ public: makeLegacyPathRequest( PathRequest::pointer& req, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request); @@ -73,7 +73,7 @@ public: // with the ledger specified by the caller json::Value doLegacyPathRequest( - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request); diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index b5d5c680cd..a752858527 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -74,7 +74,7 @@ createHTTPPost( // CHECKME this uses a different version than the replies below use. Is // this by design or an accident or should it be using - // BuildInfo::getFullVersionString () as well? + // build_info::getFullVersionString () as well? s << "POST " << (strPath.empty() ? "/" : strPath) << " HTTP/1.0\r\n" << "User-Agent: " << systemName() << "-json-rpc/v1\r\n" @@ -149,7 +149,7 @@ private: return jvResult; } - return RPC::makeParamError( + return rpc::makeParamError( std::string("Invalid currency/issuer '") + strCurrencyIssuer + "'"); } @@ -355,7 +355,7 @@ private: } catch (std::exception const&) { - return RPC::invalidFieldError(jss::limit); + return rpc::invalidFieldError(jss::limit); } } @@ -369,7 +369,7 @@ private: } catch (std::exception const&) { - return RPC::invalidFieldError(jss::proof); + return rpc::invalidFieldError(jss::proof); } } @@ -1182,7 +1182,7 @@ private: std::string param = jvParams[index++].asString(); if (param.empty()) - return RPC::makeParamError("Invalid first parameter"); + return rpc::makeParamError("Invalid first parameter"); if (param[0] != 'r') { @@ -1196,7 +1196,7 @@ private: } if (size <= index) - return RPC::makeParamError("Invalid hotwallet"); + return rpc::makeParamError("Invalid hotwallet"); param = jvParams[index++].asString(); } @@ -1726,7 +1726,7 @@ rpcClient( { boost::asio::io_context isService; - RPCCall::fromNetwork( + rpc_call::fromNetwork( isService, setup.client.ip, setup.client.port, @@ -1813,12 +1813,12 @@ rpcClient( //------------------------------------------------------------------------------ -namespace RPCCall { +namespace rpc_call { int fromCommandLine(Config const& config, std::vector const& vCmd, Logs& logs) { - auto const result = rpcClient(vCmd, config, logs, RPC::kApiCommandLineVersion); + auto const result = rpcClient(vCmd, config, logs, rpc::kApiCommandLineVersion); std::cout << result.second.toStyledString(); @@ -1883,6 +1883,6 @@ fromNetwork( j); } -} // namespace RPCCall +} // namespace rpc_call } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 6f46aed62d..96d6bf72d7 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -24,7 +24,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { @@ -114,7 +114,7 @@ fillHandler(JsonContext& context, Handler const*& result) { // Count all jobs at jtCLIENT priority or higher. int const jobCount = context.app.getJobQueue().getJobCountGE(JtClient); - if (jobCount > Tuning::kMaxJobQueueClients) + if (jobCount > tuning::kMaxJobQueueClients) { JLOG(context.j.debug()) << "Too busy for command: " << jobCount; return RpcTooBusy; @@ -179,8 +179,8 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& perfLog.rpcError(name, curId); JLOG(context.j.info()) << "Caught throw: " << e.what(); - if (context.loadType == Resource::kFeeReferenceRpc) - context.loadType = Resource::kFeeExceptionRpc; + if (context.loadType == resource::kFeeReferenceRpc) + context.loadType = resource::kFeeExceptionRpc; injectError(RpcInternal, result); return RpcInternal; @@ -190,7 +190,7 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& } // namespace Status -doCommand(RPC::JsonContext& context, json::Value& result) +doCommand(rpc::JsonContext& context, json::Value& result) { Handler const* handler = nullptr; if (auto error = fillHandler(context, handler)) @@ -226,7 +226,7 @@ doCommand(RPC::JsonContext& context, json::Value& result) Role roleRequired(unsigned int version, bool betaEnabled, std::string const& method) { - auto handler = RPC::getHandler(version, betaEnabled, method); + auto handler = rpc::getHandler(version, betaEnabled, method); if (handler == nullptr) return Role::FORBID; @@ -234,4 +234,4 @@ roleRequired(unsigned int version, bool betaEnabled, std::string const& method) return handler->role; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 1764375812..4fa0fab6f7 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -42,7 +42,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { std::uint64_t getStartHint(SLE::const_ref sle, AccountID const& accountID) @@ -116,7 +116,7 @@ parseAccountIds(json::Value const& jvArray) } std::optional -readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext const& context) +readLimitField(unsigned int& limit, tuning::LimitRange const& range, JsonContext const& context) { limit = range.rDefault; if (!context.params.isMember(jss::limit) || context.params[jss::limit].isNull()) @@ -124,11 +124,11 @@ readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext auto const& jvLimit = context.params[jss::limit]; if (!jvLimit.isUInt() && (!jvLimit.isInt() || jvLimit.asInt() < 0)) - return RPC::expectedFieldError(jss::limit, "unsigned integer"); + return rpc::expectedFieldError(jss::limit, "unsigned integer"); limit = jvLimit.asUInt(); if (limit == 0) - return RPC::invalidFieldError(jss::limit); + return rpc::invalidFieldError(jss::limit); if (!isUnlimited(context.role)) limit = std::max(range.rmin, std::min(range.rmax, limit)); @@ -184,7 +184,7 @@ getSeedFromRPC(json::Value const& params, json::Value& error) if (count != 1) { - error = RPC::makeParamError( + error = rpc::makeParamError( "Exactly one of the following must be specified: " + std::string(jss::passphrase) + ", " + std::string(jss::seed) + " or " + std::string(jss::seed_hex)); return std::nullopt; @@ -194,7 +194,7 @@ getSeedFromRPC(json::Value const& params, json::Value& error) auto const& param = params[seedType->first]; if (!param.isString()) { - error = RPC::expectedFieldError(seedType->first, "string"); + error = rpc::expectedFieldError(seedType->first, "string"); return std::nullopt; } @@ -232,13 +232,13 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int if (count == 0 || secretType == nullptr) { - error = RPC::missingFieldError(jss::secret); + error = rpc::missingFieldError(jss::secret); return {}; } if (count > 1) { - error = RPC::makeParamError( + error = rpc::makeParamError( "Exactly one of the following must be specified: " + std::string(jss::passphrase) + ", " + std::string(jss::secret) + ", " + std::string(jss::seed) + " or " + std::string(jss::seed_hex)); @@ -252,7 +252,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!params[jss::key_type].isString()) { - error = RPC::expectedFieldError(jss::key_type, "string"); + error = rpc::expectedFieldError(jss::key_type, "string"); return {}; } @@ -262,11 +262,11 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (apiVersion > 1u) { - error = RPC::makeError(RpcBadKeyType); + error = rpc::makeError(RpcBadKeyType); } else { - error = RPC::invalidFieldError(jss::key_type); + error = rpc::invalidFieldError(jss::key_type); } return {}; } @@ -275,7 +275,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // https://developercommunity.visualstudio.com/t/assigning-constexpr-char--to-static-cha/10021357?entry=problem) if (strcmp(secretType, jss::secret.cStr()) == 0) { - error = RPC::makeParamError( + error = rpc::makeParamError( "The secret field is not allowed if " + std::string(jss::key_type) + " is used."); return {}; } @@ -288,7 +288,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // https://developercommunity.visualstudio.com/t/assigning-constexpr-char--to-static-cha/10021357?entry=problem) if (strcmp(secretType, jss::seed_hex.cStr()) != 0) { - seed = RPC::parseXrplLibSeed(params[secretType]); + seed = rpc::parseXrplLibSeed(params[secretType]); if (seed) { @@ -296,7 +296,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // requested another key type, return an error. if (keyType.value_or(KeyType::Ed25519) != KeyType::Ed25519) { - error = RPC::makeError(RpcBadSeed, "Specified seed is for an Ed25519 wallet."); + error = rpc::makeError(RpcBadSeed, "Specified seed is for an Ed25519 wallet."); return {}; } @@ -317,7 +317,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!params[jss::secret].isString()) { - error = RPC::expectedFieldError(jss::secret, "string"); + error = rpc::expectedFieldError(jss::secret, "string"); return {}; } @@ -329,7 +329,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!containsError(error)) { - error = RPC::makeError(RpcBadSeed, RPC::invalidFieldMessage(secretType)); + error = rpc::makeError(RpcBadSeed, rpc::invalidFieldMessage(secretType)); } return {}; @@ -341,10 +341,10 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int return generateKeyPair(*keyType, *seed); } -std::pair +std::pair chooseLedgerEntryType(json::Value const& params) { - std::pair result{RPC::Status::kOK, ltANY}; + std::pair result{rpc::Status::kOK, ltANY}; if (params.isMember(jss::type)) { static constexpr auto kTypes = @@ -363,10 +363,10 @@ chooseLedgerEntryType(json::Value const& params) auto const& p = params[jss::type]; if (!p.isString()) { - result.first = RPC::Status{RpcInvalidParams, "Invalid field 'type', not string."}; + result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type', not string."}; XRPL_ASSERT( - result.first.type() == RPC::Status::Type::ErrorCodeI, - "xrpl::RPC::chooseLedgerEntryType : first valid result type"); + result.first.type() == rpc::Status::Type::ErrorCodeI, + "xrpl::rpc::chooseLedgerEntryType : first valid result type"); return result; } @@ -379,10 +379,10 @@ chooseLedgerEntryType(json::Value const& params) }); if (iter == kTypes.end()) { - result.first = RPC::Status{RpcInvalidParams, "Invalid field 'type'."}; + result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type'."}; XRPL_ASSERT( - result.first.type() == RPC::Status::Type::ErrorCodeI, - "xrpl::RPC::chooseLedgerEntryType : second valid result " + result.first.type() == rpc::Status::Type::ErrorCodeI, + "xrpl::rpc::chooseLedgerEntryType : second valid result " "type"); return result; } @@ -466,4 +466,4 @@ parseSubUnsubJson( return RpcSuccess; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index 881b758487..24c06021c0 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -27,7 +27,7 @@ namespace xrpl { class ReadView; -namespace RPC { +namespace rpc { struct JsonContext; @@ -85,7 +85,7 @@ parseAccountIds(json::Value const& jvArray); * std::nullopt on success. */ std::optional -readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext const& context); +readLimitField(unsigned int& limit, tuning::LimitRange const& range, JsonContext const& context); /** * @brief Extracts a Seed from RPC parameters. @@ -123,7 +123,7 @@ parseXrplLibSeed(json::Value const& params); * @param params The JSON value containing RPC parameters. * @return A pair consisting of the RPC status and the chosen LedgerEntryType. */ -std::pair +std::pair chooseLedgerEntryType(json::Value const& params); /** @@ -172,6 +172,6 @@ parseSubUnsubJson( json::StaticString const& name, beast::Journal j); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 6843c34b19..52e68e87f1 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -30,7 +30,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { @@ -40,7 +40,7 @@ isValidatedOld(LedgerMaster& ledgerMaster, bool standalone) if (standalone) return false; - return ledgerMaster.getValidatedLedgerAge() > Tuning::kMaxValidatedLedgerAge; + return ledgerMaster.getValidatedLedgerAge() > tuning::kMaxValidatedLedgerAge; } template @@ -282,19 +282,19 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context) return {RpcNotSynced, "notSynced"}; } - XRPL_ASSERT(!ledger->open(), "xrpl::RPC::getLedger : validated is not open"); + XRPL_ASSERT(!ledger->open(), "xrpl::rpc::getLedger : validated is not open"); } else { if (shortcut == LedgerShortcut::Current) { ledger = context.ledgerMaster.getCurrentLedger(); - XRPL_ASSERT(ledger->open(), "xrpl::RPC::getLedger : current is open"); + XRPL_ASSERT(ledger->open(), "xrpl::rpc::getLedger : current is open"); } else if (shortcut == LedgerShortcut::Closed) { ledger = context.ledgerMaster.getClosedLedger(); - XRPL_ASSERT(!ledger->open(), "xrpl::RPC::getLedger : closed is not open"); + XRPL_ASSERT(!ledger->open(), "xrpl::rpc::getLedger : closed is not open"); } else { @@ -386,7 +386,7 @@ lookupLedger(std::shared_ptr& ledger, JsonContext const& context } std::expected, json::Value> -getOrAcquireLedger(RPC::JsonContext const& context) +getOrAcquireLedger(rpc::JsonContext const& context) { auto const hasHash = context.params.isMember(jss::ledger_hash); auto const hasIndex = context.params.isMember(jss::ledger_index); @@ -398,7 +398,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if ((static_cast(hasHash) + static_cast(hasIndex)) != 1) { return std::unexpected( - RPC::makeParamError( + rpc::makeParamError( "Exactly one of 'ledger_hash' or " "'ledger_index' can be specified.")); } @@ -407,16 +407,16 @@ getOrAcquireLedger(RPC::JsonContext const& context) { auto const& jsonHash = context.params.get(jss::ledger_hash, json::ValueType::Null); if (!jsonHash.isString() || !ledgerHash.parseHex(jsonHash.asString())) - return std::unexpected(RPC::expectedFieldError(jss::ledger_hash, "hex string")); + return std::unexpected(rpc::expectedFieldError(jss::ledger_hash, "hex string")); } else { auto const& jsonIndex = context.params.get(jss::ledger_index, json::ValueType::Null); if (!jsonIndex.isInt() && !jsonIndex.isUInt()) - return std::unexpected(RPC::expectedFieldError(jss::ledger_index, "number")); + return std::unexpected(rpc::expectedFieldError(jss::ledger_index, "number")); // We need a validated ledger to get the hash from the sequence - if (ledgerMaster.getValidatedLedgerAge() > RPC::Tuning::kMaxValidatedLedgerAge) + if (ledgerMaster.getValidatedLedgerAge() > rpc::tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return std::unexpected(rpcError(RpcNoCurrent)); @@ -427,9 +427,9 @@ getOrAcquireLedger(RPC::JsonContext const& context) auto ledger = ledgerMaster.getValidatedLedger(); if (ledgerIndex >= ledger->header().seq) - return std::unexpected(RPC::makeParamError("Ledger index too large")); + return std::unexpected(rpc::makeParamError("Ledger index too large")); if (ledgerIndex <= 0) - return std::unexpected(RPC::makeParamError("Ledger index too small")); + return std::unexpected(rpc::makeParamError("Ledger index too small")); auto const j = context.app.getJournal("RPCHandler"); // Try to get the hash of the desired ledger from the validated @@ -441,7 +441,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) // ledger auto const refIndex = getCandidateLedger(ledgerIndex); auto refHash = hashOfSeq(*ledger, refIndex, j); - XRPL_ASSERT(refHash, "xrpl::RPC::getOrAcquireLedger : nonzero ledger hash"); + XRPL_ASSERT(refHash, "xrpl::rpc::getOrAcquireLedger : nonzero ledger hash"); // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above ledger = ledgerMaster.getLedgerByHash(*refHash); @@ -453,7 +453,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if (auto il = context.app.getInboundLedgers().acquire( *refHash, refIndex, InboundLedger::Reason::GENERIC)) { - json::Value jvResult = RPC::makeError( + json::Value jvResult = rpc::makeError( RpcLgrNotFound, "acquiring ledger containing requested index"); jvResult[jss::acquiring] = getJson(LedgerFill(*il, &context)); return std::unexpected(jvResult); @@ -462,7 +462,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if (auto il = context.app.getInboundLedgers().find(*refHash)) // NOLINTEND(bugprone-unchecked-optional-access) { - json::Value jvResult = RPC::makeError( + json::Value jvResult = rpc::makeError( RpcLgrNotFound, "acquiring ledger containing requested index"); jvResult[jss::acquiring] = il->getJson(0); return std::unexpected(jvResult); @@ -474,7 +474,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) neededHash = hashOfSeq(*ledger, ledgerIndex, j); } - XRPL_ASSERT(neededHash, "xrpl::RPC::getOrAcquireLedger : nonzero needed hash"); + XRPL_ASSERT(neededHash, "xrpl::rpc::getOrAcquireLedger : nonzero needed hash"); ledgerHash = neededHash ? *neededHash : beast::kZero; // kludge } @@ -494,7 +494,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) return std::unexpected(il->getJson(0)); return std::unexpected( - RPC::makeError(RpcNotReady, "findCreate failed to return an inbound ledger")); + rpc::makeError(RpcNotReady, "findCreate failed to return an inbound ledger")); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h b/src/xrpld/rpc/detail/RPCLedgerHelpers.h index cbd47d38e6..cad5141917 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.h +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h @@ -21,7 +21,7 @@ namespace xrpl { class ReadView; class Transaction; -namespace RPC { +namespace rpc { struct JsonContext; @@ -172,8 +172,8 @@ ledgerFromSpecifier( * On failure, contains a json::Value describing the error. */ std::expected, json::Value> -getOrAcquireLedger(RPC::JsonContext const& context); +getOrAcquireLedger(rpc::JsonContext const& context); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCSub.cpp b/src/xrpld/rpc/detail/RPCSub.cpp index 92e9849939..8cea8b6bdd 100644 --- a/src/xrpld/rpc/detail/RPCSub.cpp +++ b/src/xrpld/rpc/detail/RPCSub.cpp @@ -75,7 +75,7 @@ public: } path_ = pUrl.path; - JLOG(j_.info()) << "RPCCall::fromNetwork sub: ip=" << ip_ << " port=" << port_ + JLOG(j_.info()) << "rpc_call::fromNetwork sub: ip=" << ip_ << " port=" << port_ << " ssl= " << (ssl_ ? "yes" : "no") << " path='" << path_ << "'"; } @@ -87,14 +87,14 @@ public: std::scoped_lock const sl(lock_); auto jm = broadcast ? j_.debug() : j_.info(); - JLOG(jm) << "RPCCall::fromNetwork push: " << jvObj; + JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj; deque_.emplace_back(seq_++, jvObj); if (!sending_) { // Start a sending thread. - JLOG(j_.info()) << "RPCCall::fromNetwork start"; + JLOG(j_.info()) << "rpc_call::fromNetwork start"; sending_ = jobQueue_.addJob(JtClientSubscribe, "RPCSubSendThr", [this]() { sendThread(); }); @@ -156,9 +156,9 @@ private: // XXX Might not need this in a try. try { - JLOG(j_.info()) << "RPCCall::fromNetwork: " << ip_; + JLOG(j_.info()) << "rpc_call::fromNetwork: " << ip_; - RPCCall::fromNetwork( + rpc_call::fromNetwork( ioContext_, ip_, port_, @@ -173,7 +173,7 @@ private: } catch (std::exception const& e) { - JLOG(j_.info()) << "RPCCall::fromNetwork exception: " << e.what(); + JLOG(j_.info()) << "rpc_call::fromNetwork exception: " << e.what(); } } } while (bSend); diff --git a/src/xrpld/rpc/detail/Role.cpp b/src/xrpld/rpc/detail/Role.cpp index 68c5fcc484..34970b0580 100644 --- a/src/xrpld/rpc/detail/Role.cpp +++ b/src/xrpld/rpc/detail/Role.cpp @@ -40,7 +40,7 @@ passwordUnrequiredOrSentCorrect(Port const& port, json::Value const& params) bool ipAllowed( - beast::IP::Address const& remoteIp, + beast::ip::Address const& remoteIp, std::vector const& nets4, std::vector const& nets6) { @@ -78,7 +78,7 @@ ipAllowed( } bool -isAdmin(Port const& port, json::Value const& params, beast::IP::Address const& remoteIp) +isAdmin(Port const& port, json::Value const& params, beast::ip::Address const& remoteIp) { return ipAllowed(remoteIp, port.adminNetsV4, port.adminNetsV6) && passwordUnrequiredOrSentCorrect(port, params); @@ -89,7 +89,7 @@ requestRole( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string_view user) { if (isAdmin(port, params, remoteIp.address())) @@ -122,16 +122,16 @@ isUnlimited( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string const& user) { return isUnlimited(requestRole(required, port, params, remoteIp, user)); } -Resource::Consumer +resource::Consumer requestInboundEndpoint( - Resource::Manager& manager, - beast::IP::Endpoint const& remoteAddress, + resource::Manager& manager, + beast::ip::Endpoint const& remoteAddress, Role const& role, std::string_view user, std::string_view forwardedFor) diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 768cbf0dc0..0181d5b10f 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -93,7 +93,7 @@ statusRequestResponse(http_request_type const& request, boost::beast::http::stat response msg; msg.version(request.version()); msg.result(status); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "text/html"); msg.insert("Connection", "close"); msg.body() = "Invalid protocol."; @@ -129,7 +129,7 @@ ServerHandler::ServerHandler( boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm) : app_(app) , resourceManager_(resourceManager) @@ -337,7 +337,7 @@ ServerHandler::onWSMessage( { json::Value jv; auto const size = boost::asio::buffer_size(buffers); - if (size > RPC::Tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject()) + if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject()) { json::Value jvResult(json::ValueType::Object); jvResult[jss::type] = jss::error; @@ -426,11 +426,11 @@ ServerHandler::processSession( // Requests without "command" are invalid. json::Value jr(json::ValueType::Object); - Resource::Charge loadType = Resource::kFeeReferenceRpc; + resource::Charge loadType = resource::kFeeReferenceRpc; try { - auto apiVersion = RPC::getAPIVersionNumber(jv, app_.config().betaRpcApi); - if (apiVersion == RPC::kApiInvalidVersion || + auto apiVersion = rpc::getAPIVersionNumber(jv, app_.config().betaRpcApi); + if (apiVersion == rpc::kApiInvalidVersion || (!jv.isMember(jss::command) && !jv.isMember(jss::method)) || (jv.isMember(jss::command) && !jv[jss::command].isString()) || (jv.isMember(jss::method) && !jv[jss::method].isString()) || @@ -439,7 +439,7 @@ ServerHandler::processSession( { jr[jss::type] = jss::response; jr[jss::status] = jss::error; - jr[jss::error] = apiVersion == RPC::kApiInvalidVersion ? jss::invalid_API_version + jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version : jss::missingCommand; jr[jss::request] = jv; if (jv.isMember(jss::id)) @@ -451,11 +451,11 @@ ServerHandler::processSession( if (jv.isMember(jss::api_version)) jr[jss::api_version] = jv[jss::api_version]; - is->getConsumer().charge(Resource::kFeeMalformedRpc); + is->getConsumer().charge(resource::kFeeMalformedRpc); return jr; } - auto required = RPC::roleRequired( + auto required = rpc::roleRequired( apiVersion, app_.config().betaRpcApi, jv.isMember(jss::command) ? jv[jss::command].asString() : jv[jss::method].asString()); @@ -463,16 +463,16 @@ ServerHandler::processSession( required, session->port(), jv, - beast::IP::fromAsio(session->remoteEndpoint().address()), + beast::ip::fromAsio(session->remoteEndpoint().address()), is->user()); if (Role::FORBID == role) { - loadType = Resource::kFeeMalformedRpc; + loadType = resource::kFeeMalformedRpc; jr[jss::result] = rpcError(RpcForbidden); } else { - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = app_.getJournal("RPCHandler"), .app = app_, .loadType = loadType, @@ -487,7 +487,7 @@ ServerHandler::processSession( {.user = is->user(), .forwardedFor = is->forwardedFor()}}; auto start = std::chrono::system_clock::now(); - RPC::doCommand(context, jr[jss::result]); + rpc::doCommand(context, jr[jss::result]); auto end = std::chrono::system_clock::now(); logDuration(jv, end - start, journal_); } @@ -495,7 +495,7 @@ ServerHandler::processSession( catch (std::exception const& ex) { // LCOV_EXCL_START - jr[jss::result] = RPC::makeError(RpcInternal); + jr[jss::result] = rpc::makeError(RpcInternal); JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n" << "Input JSON: " << json::Compact{json::Value{jv}}; // LCOV_EXCL_STOP @@ -601,7 +601,7 @@ void ServerHandler::processRequest( Port const& port, std::string const& request, - beast::IP::Endpoint const& remoteIPAddress, + beast::ip::Endpoint const& remoteIPAddress, Output const& output, std::shared_ptr coro, std::string_view forwardedFor, @@ -612,7 +612,7 @@ ServerHandler::processRequest( json::Value jsonOrig; { json::Reader reader; - if ((request.size() > RPC::Tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || + if ((request.size() > rpc::tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || !jsonOrig || !jsonOrig.isObject()) { httpReply( @@ -652,21 +652,21 @@ ServerHandler::processRequest( continue; } - unsigned apiVersion = RPC::kApiVersionIfUnspecified; + unsigned apiVersion = rpc::kApiVersionIfUnspecified; if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() && jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][0u].isObject()) { - apiVersion = RPC::getAPIVersionNumber( + apiVersion = rpc::getAPIVersionNumber( jsonRPC[jss::params][json::UInt(0)], app_.config().betaRpcApi); } - if (apiVersion == RPC::kApiVersionIfUnspecified && batch) + if (apiVersion == rpc::kApiVersionIfUnspecified && batch) { // for batch request, api_version may be at a different level - apiVersion = RPC::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi); + apiVersion = rpc::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi); } - if (apiVersion == RPC::kApiInvalidVersion) + if (apiVersion == rpc::kApiInvalidVersion) { if (!batch) { @@ -685,7 +685,7 @@ ServerHandler::processRequest( auto required = Role::FORBID; if (jsonRPC.isMember(jss::method) && jsonRPC[jss::method].isString()) { - required = RPC::roleRequired( + required = rpc::roleRequired( apiVersion, app_.config().betaRpcApi, jsonRPC[jss::method].asString()); } @@ -700,7 +700,7 @@ ServerHandler::processRequest( role = requestRole(required, port, json::ValueType::Object, remoteIPAddress, user); } - Resource::Consumer usage; + resource::Consumer usage; if (isUnlimited(role)) { usage = resourceManager_.newUnlimitedEndpoint(remoteIPAddress); @@ -725,7 +725,7 @@ ServerHandler::processRequest( if (role == Role::FORBID) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(403, "Forbidden", output, rpcJ); @@ -739,7 +739,7 @@ ServerHandler::processRequest( if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "Null method", output, rpcJ); @@ -754,7 +754,7 @@ ServerHandler::processRequest( json::Value const& method = jsonRPC[jss::method]; if (!method.isString()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "method is not string", output, rpcJ); @@ -769,7 +769,7 @@ ServerHandler::processRequest( std::string const strMethod = method.asString(); if (strMethod.empty()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "method is empty", output, rpcJ); @@ -797,7 +797,7 @@ ServerHandler::processRequest( } else if (!params.isArray() || params.size() != 1) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); httpReply(400, "params unparsable", output, rpcJ); return; } @@ -806,7 +806,7 @@ ServerHandler::processRequest( params = std::move(params[0u]); if (!params.isObjectOrNull()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); httpReply(400, "params unparsable", output, rpcJ); return; } @@ -822,7 +822,7 @@ ServerHandler::processRequest( { if (!params[jss::ripplerpc].isString()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "ripplerpc is not a string", output, rpcJ); @@ -853,9 +853,9 @@ ServerHandler::processRequest( params[jss::command] = strMethod; JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params; - Resource::Charge loadType = Resource::kFeeReferenceRpc; + resource::Charge loadType = resource::kFeeReferenceRpc; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = journal_, .app = app_, .loadType = loadType, @@ -874,12 +874,12 @@ ServerHandler::processRequest( try { - RPC::doCommand(context, result); + rpc::doCommand(context, result); } catch (std::exception const& ex) { // LCOV_EXCL_START - result = RPC::makeError(RpcInternal); + result = rpc::makeError(RpcInternal); JLOG(journal_.error()) << "Internal error : " << ex.what() << " when processing request: " << json::Compact{json::Value{params}}; @@ -984,7 +984,7 @@ ServerHandler::processRequest( reply[jss::error][jss::error_code].isInt()) { int const errCode = reply[jss::error][jss::error_code].asInt(); - return RPC::errorCodeHttpStatus(static_cast(errCode)); + return rpc::errorCodeHttpStatus(static_cast(errCode)); } } // Return OK. @@ -1043,7 +1043,7 @@ ServerHandler::statusResponse(http_request_type const& request) const msg.body() = "Server cannot accept clients: " + reason + ""; } msg.version(request.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "text/html"); msg.insert("Connection", "close"); msg.prepare_payload(); @@ -1208,7 +1208,7 @@ setupClient(ServerHandler::Setup& setup) if (iter == setup.ports.cend()) return; setup.client.secure = iter->protocol.contains("https"); - if (beast::IP::isUnspecified(iter->ip)) + if (beast::ip::isUnspecified(iter->ip)) { // VFALCO HACK! to make localhost work setup.client.ip = iter->ip.is_v6() ? "::1" : "127.0.0.1"; @@ -1256,7 +1256,7 @@ makeServerHandler( boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm) { return std::make_unique( diff --git a/src/xrpld/rpc/detail/Status.cpp b/src/xrpld/rpc/detail/Status.cpp index 58d2f8cb80..147f2b31e0 100644 --- a/src/xrpld/rpc/detail/Status.cpp +++ b/src/xrpld/rpc/detail/Status.cpp @@ -9,7 +9,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { std::string Status::codeString() const @@ -25,7 +25,7 @@ Status::codeString() const std::string s1, s2; [[maybe_unused]] auto const success = transResultInfo(toTER(), s1, s2); - XRPL_ASSERT(success, "xrpl::RPC::codeString : valid TER result"); + XRPL_ASSERT(success, "xrpl::rpc::codeString : valid TER result"); return s1 + ": " + s2; } @@ -39,7 +39,7 @@ Status::codeString() const } // LCOV_EXCL_START - UNREACHABLE("xrpl::RPC::codeString : invalid type"); + UNREACHABLE("xrpl::rpc::codeString : invalid type"); return ""; // LCOV_EXCL_STOP } @@ -85,4 +85,4 @@ Status::toString() const return ""; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index e1c5180b5c..9c97577b27 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -64,7 +64,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace detail { // Used to pass extra parameters used when returning a @@ -218,7 +218,7 @@ checkPayment( { if (txJson[jss::DeliverMax] != txJson[jss::Amount]) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Cannot specify differing 'Amount' and 'DeliverMax'"); } } @@ -231,31 +231,31 @@ checkPayment( } if (!txJson.isMember(jss::Amount)) - return RPC::missingFieldError("tx_json.Amount"); + return rpc::missingFieldError("tx_json.Amount"); STAmount amount; if (!amountFromJsonNoThrow(amount, txJson[jss::Amount])) - return RPC::invalidFieldError("tx_json.Amount"); + return rpc::invalidFieldError("tx_json.Amount"); if (!txJson.isMember(jss::Destination)) - return RPC::missingFieldError("tx_json.Destination"); + return rpc::missingFieldError("tx_json.Destination"); auto const dstAccountID = parseBase58(txJson[jss::Destination].asString()); if (!dstAccountID) - return RPC::invalidFieldError("tx_json.Destination"); + return rpc::invalidFieldError("tx_json.Destination"); if (params.isMember(jss::build_path) && (!doPath || (!app.getOpenLedger().current()->rules().enabled(featureMPTokensV2) && amount.holds()))) { - return RPC::makeError(RpcInvalidParams, "Field 'build_path' not allowed in this context."); + return rpc::makeError(RpcInvalidParams, "Field 'build_path' not allowed in this context."); } if (txJson.isMember(jss::Paths) && params.isMember(jss::build_path)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Cannot specify both 'tx_json.Paths' and 'build_path'"); } @@ -266,7 +266,7 @@ checkPayment( if (!txJson[sfDomainID.jsonName].isString() || !num.parseHex(txJson[sfDomainID.jsonName].asString())) { - return RPC::makeError(RpcDomainMalformed, "Unable to parse 'DomainID'."); + return rpc::makeError(RpcDomainMalformed, "Unable to parse 'DomainID'."); } domain = num; @@ -279,7 +279,7 @@ checkPayment( if (txJson.isMember(jss::SendMax)) { if (!amountFromJsonNoThrow(sendMax, txJson[jss::SendMax])) - return RPC::invalidFieldError("tx_json.SendMax"); + return rpc::invalidFieldError("tx_json.SendMax"); } else { @@ -291,7 +291,7 @@ checkPayment( } if (sendMax.native() && amount.native()) - return RPC::makeError(RpcInvalidParams, "Cannot build XRP to XRP paths."); + return rpc::makeError(RpcInvalidParams, "Cannot build XRP to XRP paths."); { LegacyPathFind const lpf(isUnlimited(role), app); @@ -357,19 +357,19 @@ checkTxJsonFields( if (!txJson.isObject()) { - ret.first = RPC::objectFieldError(jss::tx_json); + ret.first = rpc::objectFieldError(jss::tx_json); return ret; } if (!txJson.isMember(jss::TransactionType)) { - ret.first = RPC::missingFieldError("tx_json.TransactionType"); + ret.first = rpc::missingFieldError("tx_json.TransactionType"); return ret; } if (!txJson.isMember(jss::Account)) { - ret.first = RPC::makeError(RpcSrcActMissing, RPC::missingFieldMessage("tx_json.Account")); + ret.first = rpc::makeError(RpcSrcActMissing, rpc::missingFieldMessage("tx_json.Account")); return ret; } @@ -377,12 +377,12 @@ checkTxJsonFields( if (!srcAddressID) { - ret.first = RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage("tx_json.Account")); + ret.first = rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage("tx_json.Account")); return ret; } // Check for current ledger. - if (verify && !config.standalone() && (validatedLedgerAge > Tuning::kMaxValidatedLedgerAge)) + if (verify && !config.standalone() && (validatedLedgerAge > tuning::kMaxValidatedLedgerAge)) { if (apiVersion == 1) { @@ -415,12 +415,12 @@ checkNetworkID(json::Value const& txJson, uint32_t appNetworkId) if (!txJson.isMember(jss::NetworkID)) { return std::unexpected( - RPC::makeError(RpcInvalidParams, RPC::missingFieldMessage("tx_json.NetworkID"))); + rpc::makeError(RpcInvalidParams, rpc::missingFieldMessage("tx_json.NetworkID"))); } if (!txJson[jss::NetworkID].isIntegral() || txJson[jss::NetworkID].asUInt() != appNetworkId) { return std::unexpected( - RPC::makeError(RpcInvalidParams, RPC::invalidFieldMessage("tx_json.NetworkID"))); + rpc::makeError(RpcInvalidParams, rpc::invalidFieldMessage("tx_json.NetworkID"))); } } return std::expected(); @@ -490,13 +490,13 @@ transactionPreProcessImpl( { if (signatureTemplate == nullptr) { // Invalid target field - return RPC::makeError(RpcInvalidParams, signatureTarget->get().getName()); + return rpc::makeError(RpcInvalidParams, signatureTarget->get().getName()); } signingArgs.setSignatureTarget(signatureTarget); } if (!params.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); json::Value& txJson(params[jss::tx_json]); @@ -510,14 +510,14 @@ transactionPreProcessImpl( app.getFeeTrack(), getAPIVersionNumber(params, app.config().betaRpcApi)); - if (RPC::containsError(txJsonResult)) + if (rpc::containsError(txJsonResult)) return std::move(txJsonResult); // This test covers the case where we're offline so the sequence number // cannot be determined locally. If we're offline then the caller must // provide the sequence number. if (!verify && !txJson.isMember(jss::Sequence)) - return RPC::missingFieldError("tx_json.Sequence"); + return rpc::missingFieldError("tx_json.Sequence"); SLE::const_pointer sle; if (verify) @@ -565,7 +565,7 @@ transactionPreProcessImpl( app.getTxQ(), app); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -573,7 +573,7 @@ transactionPreProcessImpl( json::Value err = checkPayment( params, txJson, srcAddressID, role, app, verify && signingArgs.editFields()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -614,8 +614,8 @@ transactionPreProcessImpl( if (!ptrDelegatedAddressID) { - return RPC::makeError( - RpcSrcActMalformed, RPC::invalidFieldMessage("tx_json.Delegate")); + return rpc::makeError( + RpcSrcActMalformed, rpc::invalidFieldMessage("tx_json.Delegate")); } auto delegatedAddressID = *ptrDelegatedAddressID; @@ -672,17 +672,17 @@ transactionPreProcessImpl( } catch (STObject::FieldErr const& err) { - return RPC::makeError(RpcInvalidParams, err.what()); + return rpc::makeError(RpcInvalidParams, err.what()); } catch (std::exception&) { - return RPC::makeError( + return rpc::makeError( RpcInternal, "Exception occurred constructing serialized transaction"); } std::string reason; if (!passesLocalChecks(*stTx, reason)) - return RPC::makeError(RpcInvalidParams, reason); + return rpc::makeError(RpcInvalidParams, reason); // If multisign then return multiSignature, else set TxnSignature field. if (signingArgs.isMultiSigning()) @@ -716,7 +716,7 @@ transactionConstructImpl( tpTrans = std::make_shared(stTx, reason, app); if (tpTrans->getStatus() != TransStatus::NEW) { - ret.first = RPC::makeError(RpcInternal, "Unable to construct transaction: " + reason); + ret.first = rpc::makeError(RpcInternal, "Unable to construct transaction: " + reason); return ret; } } @@ -741,7 +741,7 @@ transactionConstructImpl( } if (checkValidity(app.getHashRouter(), *sttxNew, rules).first != Validity::Valid) { - ret.first = RPC::makeError(RpcInternal, "Invalid signature."); + ret.first = rpc::makeError(RpcInternal, "Invalid signature."); return ret; } @@ -766,7 +766,7 @@ transactionConstructImpl( if (!tpTrans) { - ret.first = RPC::makeError(RpcInternal, "Unable to sterilize transaction."); + ret.first = rpc::makeError(RpcInternal, "Unable to sterilize transaction."); return ret; } ret.second = std::move(tpTrans); @@ -789,7 +789,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) jvResult[jss::tx_json] = tpTrans->getJson(JsonOptions::Values::None); } - RPC::insertDeliverMax( + rpc::insertDeliverMax( jvResult[jss::tx_json], tpTrans->getSTransaction()->getTxnType(), apiVersion); jvResult[jss::tx_blob] = strHex(tpTrans->getSTransaction()->getSerializer().peekData()); @@ -808,7 +808,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) } catch (std::exception&) { - jvResult = RPC::makeError(RpcInternal, "Exception occurred during JSON handling."); + jvResult = rpc::makeError(RpcInternal, "Exception occurred during JSON handling."); } return jvResult; } @@ -922,7 +922,7 @@ getCurrentNetworkFee( { std::stringstream ss; ss << "Fee of " << fee << " exceeds the requested tx limit of " << *limit; - return RPC::makeError(RpcHighFee, ss.str()); + return rpc::makeError(RpcHighFee, ss.str()); } return fee.jsonClipped(); @@ -943,10 +943,10 @@ checkFee( return json::Value(); if (!doAutoFill) - return RPC::missingFieldError("tx_json.Fee"); + return rpc::missingFieldError("tx_json.Fee"); - int mult = Tuning::kDefaultAutoFillFeeMultiplier; - int div = Tuning::kDefaultAutoFillFeeDivisor; + int mult = tuning::kDefaultAutoFillFeeMultiplier; + int div = tuning::kDefaultAutoFillFeeDivisor; if (request.isMember(jss::fee_mult_max)) { if (request[jss::fee_mult_max].isInt()) @@ -954,15 +954,15 @@ checkFee( mult = request[jss::fee_mult_max].asInt(); if (mult < 0) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); + rpc::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); } } else { - return RPC::makeError( - RpcHighFee, RPC::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); + return rpc::makeError( + RpcHighFee, rpc::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); } } if (request.isMember(jss::fee_div_max)) @@ -972,15 +972,15 @@ checkFee( div = request[jss::fee_div_max].asInt(); if (div <= 0) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage(jss::fee_div_max, "a positive integer")); + rpc::expectedFieldMessage(jss::fee_div_max, "a positive integer")); } } else { - return RPC::makeError( - RpcHighFee, RPC::expectedFieldMessage(jss::fee_div_max, "a positive integer")); + return rpc::makeError( + RpcHighFee, rpc::expectedFieldMessage(jss::fee_div_max, "a positive integer")); } } @@ -1071,7 +1071,7 @@ transactionSubmit( } catch (std::exception&) { - return RPC::makeError(RpcInternal, "Exception occurred during transaction submission."); + return rpc::makeError(RpcInternal, "Exception occurred during transaction submission."); } return transactionFormatResultImpl(txn.second, apiVersion); @@ -1084,21 +1084,21 @@ static json::Value checkMultiSignFields(json::Value const& jvRequest) { if (!jvRequest.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); json::Value const& txJson(jvRequest[jss::tx_json]); if (!txJson.isObject()) - return RPC::invalidFieldMessage(jss::tx_json); + return rpc::invalidFieldMessage(jss::tx_json); // There are a couple of additional fields we need to check before // we serialize. If we serialize first then we generate less useful // error messages. if (!txJson.isMember(jss::Sequence)) - return RPC::missingFieldError("tx_json.Sequence"); + return rpc::missingFieldError("tx_json.Sequence"); if (!txJson.isMember(sfSigningPubKey.getJsonName())) - return RPC::missingFieldError("tx_json.SigningPubKey"); + return rpc::missingFieldError("tx_json.SigningPubKey"); // Multi-signing into a signature_target object field is fine, // because it means the signature is not for the transaction @@ -1106,7 +1106,7 @@ checkMultiSignFields(json::Value const& jvRequest) if (!jvRequest.isMember(jss::signature_target) && !txJson[sfSigningPubKey.getJsonName()].asString().empty()) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "When multi-signing 'tx_json.SigningPubKey' must be empty."); } @@ -1120,7 +1120,7 @@ static json::Value sortAndValidateSigners(STArray& signers, AccountID const& signingForID) { if (signers.empty()) - return RPC::makeParamError("Signers array may not be empty."); + return rpc::makeParamError("Signers array may not be empty."); // Signers must be sorted by Account. std::ranges::sort(signers, [](STObject const& a, STObject const& b) { @@ -1137,7 +1137,7 @@ sortAndValidateSigners(STArray& signers, AccountID const& signingForID) std::ostringstream err; err << "Duplicate Signers:Signer:Account entries (" << toBase58((*dupIter)[sfAccount]) << ") are not allowed."; - return RPC::makeParamError(err.str()); + return rpc::makeParamError(err.str()); } // An account may not sign for itself. @@ -1147,7 +1147,7 @@ sortAndValidateSigners(STArray& signers, AccountID const& signingForID) { std::ostringstream err; err << "A Signer may not be the transaction's Account (" << toBase58(signingForID) << ")."; - return RPC::makeParamError(err.str()); + return rpc::makeParamError(err.str()); } return {}; } @@ -1174,23 +1174,23 @@ transactionSignFor( char const accountField[] = "account"; if (!jvRequest.isMember(accountField)) - return RPC::missingFieldError(accountField); + return rpc::missingFieldError(accountField); // Turn the signer's account into an AccountID for multi-sign. auto const signerAccountID = parseBase58(jvRequest[accountField].asString()); if (!signerAccountID) { - return RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage(accountField)); + return rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage(accountField)); } if (!jvRequest.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); { json::Value& txJson(jvRequest[jss::tx_json]); if (!txJson.isObject()) - return RPC::objectFieldError(jss::tx_json); + return rpc::objectFieldError(jss::tx_json); if (auto checkResult = detail::checkNetworkID(txJson, app.getNetworkIDService().getNetworkID()); @@ -1211,7 +1211,7 @@ transactionSignFor( using namespace detail; { json::Value err = checkMultiSignFields(jvRequest); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1225,7 +1225,7 @@ transactionSignFor( return preprocResult.first; XRPL_ASSERT( - signForParams.validMultiSign(), "xrpl::RPC::transactionSignFor : valid multi-signature"); + signForParams.validMultiSign(), "xrpl::rpc::transactionSignFor : valid multi-signature"); { SLE::const_pointer const accountState = ledger->read(keylet::account(*signerAccountID)); @@ -1263,7 +1263,7 @@ transactionSignFor( // For delegated transactions, the delegate account is // the one forbidden from appearing in its own Signers array. auto err = sortAndValidateSigners(signers, sttx->getInitiator()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1299,7 +1299,7 @@ transactionSubmitMultiSigned( using namespace detail; { json::Value err = checkMultiSignFields(jvRequest); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1314,7 +1314,7 @@ transactionSubmitMultiSigned( app.getFeeTrack(), getAPIVersionNumber(jvRequest, app.config().betaRpcApi)); - if (RPC::containsError(txJsonResult)) + if (rpc::containsError(txJsonResult)) return std::move(txJsonResult); SLE::const_pointer const sle = ledger->read(keylet::account(srcAddressID)); @@ -1332,12 +1332,12 @@ transactionSubmitMultiSigned( json::Value err = checkFee(jvRequest, role, false, app.config(), app.getFeeTrack(), app.getTxQ(), app); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; err = checkPayment(jvRequest, txJson, srcAddressID, role, app, false); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1359,17 +1359,17 @@ transactionSubmitMultiSigned( } catch (STObject::FieldErr const& err) { - return RPC::makeError(RpcInvalidParams, err.what()); + return rpc::makeError(RpcInvalidParams, err.what()); } catch (std::exception& ex) { std::string const reason(ex.what()); - return RPC::makeError( + return rpc::makeError( RpcInternal, "Exception while serializing transaction: " + reason); } std::string reason; if (!passesLocalChecks(*stTx, reason)) - return RPC::makeError(RpcInvalidParams, reason); + return rpc::makeError(RpcInvalidParams, reason); } // Validate the fields in the serialized transaction. @@ -1383,7 +1383,7 @@ transactionSubmitMultiSigned( std::ostringstream err; err << "Invalid " << sfSigningPubKey.fieldName << " field. Field must be empty when multi-signing."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } // There may not be a TxnSignature field. @@ -1397,26 +1397,26 @@ transactionSubmitMultiSigned( { std::ostringstream err; err << "Invalid " << sfFee.fieldName << " field. Fees must be specified in XRP."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } if (fee <= STAmount{0}) { std::ostringstream err; err << "Invalid " << sfFee.fieldName << " field. Fees must be greater than zero."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } } // Verify that the Signers field is present. if (!stTx->isFieldPresent(sfSigners)) - return RPC::missingFieldError("tx_json.Signers"); + return rpc::missingFieldError("tx_json.Signers"); // If the Signers field is present the SField guarantees it to be an array. // Get a reference to the Signers array so we can verify and sort it. auto& signers = stTx->peekFieldArray(sfSigners); if (signers.empty()) - return RPC::makeParamError("tx_json.Signers array may not be empty."); + return rpc::makeParamError("tx_json.Signers array may not be empty."); // The Signers array may only contain Signer objects. if (std::ranges::find_if_not(signers, [](STObject const& obj) { @@ -1427,14 +1427,14 @@ transactionSubmitMultiSigned( obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3); }) != signers.end()) { - return RPC::makeParamError("Signers array may only contain Signer entries."); + return rpc::makeParamError("Signers array may only contain Signer entries."); } // The array must be sorted and validated. // For delegated transactions, getInitiator() returns sfDelegate, // that account is the one forbidden from appearing in its own Signers array. auto err = sortAndValidateSigners(signers, stTx->getInitiator()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; // Make sure the SerializedTransaction makes a legitimate Transaction. @@ -1452,10 +1452,10 @@ transactionSubmitMultiSigned( } catch (std::exception&) { - return RPC::makeError(RpcInternal, "Exception occurred during transaction submission."); + return rpc::makeError(RpcInternal, "Exception occurred during transaction submission."); } return transactionFormatResultImpl(txn.second, apiVersion); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/TransactionSign.h b/src/xrpld/rpc/detail/TransactionSign.h index dcb417dd16..9b07cbde20 100644 --- a/src/xrpld/rpc/detail/TransactionSign.h +++ b/src/xrpld/rpc/detail/TransactionSign.h @@ -20,7 +20,7 @@ class LoadFeeTrack; class Transaction; class TxQ; -namespace RPC { +namespace rpc { json::Value getCurrentNetworkFee( @@ -30,8 +30,8 @@ getCurrentNetworkFee( TxQ const& txQ, Application const& app, json::Value const& tx, - int mult = Tuning::kDefaultAutoFillFeeMultiplier, - int div = Tuning::kDefaultAutoFillFeeDivisor); + int mult = tuning::kDefaultAutoFillFeeMultiplier, + int div = tuning::kDefaultAutoFillFeeDivisor); /** * Fill in the fee on behalf of the client. @@ -140,5 +140,5 @@ transactionSubmitMultiSigned( Application& app, ProcessTransactionFn const& processTransaction); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index b904822698..5c47ab4365 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -6,7 +6,7 @@ * Tuned constants. */ /** @{ */ -namespace xrpl::RPC::Tuning { +namespace xrpl::rpc::tuning { /** * Represents RPC limit parameter values that have a min, default and max. @@ -98,5 +98,5 @@ static constexpr int kMaxSrcCur = 18; */ static constexpr int kMaxAutoSrcCur = 88; -} // namespace xrpl::RPC::Tuning +} // namespace xrpl::rpc::tuning /** @} */ diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp b/src/xrpld/rpc/handlers/ChannelVerify.cpp index 64f616e829..ab54745a5d 100644 --- a/src/xrpld/rpc/handlers/ChannelVerify.cpp +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp @@ -27,13 +27,13 @@ namespace xrpl { // signature: signature to verify // } json::Value -doChannelVerify(RPC::JsonContext& context) +doChannelVerify(rpc::JsonContext& context) { auto const& params(context.params); for (auto const& p : {jss::public_key, jss::channel_id, jss::amount, jss::signature}) { if (!params.isMember(p)) - return RPC::missingFieldError(p); + return rpc::missingFieldError(p); } std::optional pk; diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 7b347b2ecc..d192e8726c 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -7,147 +7,147 @@ namespace xrpl { json::Value -doAccountCurrencies(RPC::JsonContext&); +doAccountCurrencies(rpc::JsonContext&); json::Value -doAccountInfo(RPC::JsonContext&); +doAccountInfo(rpc::JsonContext&); json::Value -doAccountLines(RPC::JsonContext&); +doAccountLines(rpc::JsonContext&); json::Value -doAccountChannels(RPC::JsonContext&); +doAccountChannels(rpc::JsonContext&); json::Value -doAccountNFTs(RPC::JsonContext&); +doAccountNFTs(rpc::JsonContext&); json::Value -doAccountObjects(RPC::JsonContext&); +doAccountObjects(rpc::JsonContext&); json::Value -doAccountOffers(RPC::JsonContext&); +doAccountOffers(rpc::JsonContext&); json::Value -doAccountTx(RPC::JsonContext&); +doAccountTx(rpc::JsonContext&); json::Value -doAMMInfo(RPC::JsonContext&); +doAMMInfo(rpc::JsonContext&); json::Value -doBookOffers(RPC::JsonContext&); +doBookOffers(rpc::JsonContext&); json::Value -doBookChanges(RPC::JsonContext&); +doBookChanges(rpc::JsonContext&); json::Value -doBlackList(RPC::JsonContext&); +doBlackList(rpc::JsonContext&); json::Value -doCanDelete(RPC::JsonContext&); +doCanDelete(rpc::JsonContext&); json::Value -doChannelAuthorize(RPC::JsonContext&); +doChannelAuthorize(rpc::JsonContext&); json::Value -doChannelVerify(RPC::JsonContext&); +doChannelVerify(rpc::JsonContext&); json::Value -doConnect(RPC::JsonContext&); +doConnect(rpc::JsonContext&); json::Value -doConsensusInfo(RPC::JsonContext&); +doConsensusInfo(rpc::JsonContext&); json::Value -doDepositAuthorized(RPC::JsonContext&); +doDepositAuthorized(rpc::JsonContext&); json::Value -doFeature(RPC::JsonContext&); +doFeature(rpc::JsonContext&); json::Value -doFee(RPC::JsonContext&); +doFee(rpc::JsonContext&); json::Value -doFetchInfo(RPC::JsonContext&); +doFetchInfo(rpc::JsonContext&); json::Value -doGatewayBalances(RPC::JsonContext&); +doGatewayBalances(rpc::JsonContext&); json::Value -doGetCounts(RPC::JsonContext&); +doGetCounts(rpc::JsonContext&); json::Value -doGetAggregatePrice(RPC::JsonContext&); +doGetAggregatePrice(rpc::JsonContext&); json::Value -doLedgerAccept(RPC::JsonContext&); +doLedgerAccept(rpc::JsonContext&); json::Value -doLedgerCleaner(RPC::JsonContext&); +doLedgerCleaner(rpc::JsonContext&); json::Value -doLedgerClosed(RPC::JsonContext&); +doLedgerClosed(rpc::JsonContext&); json::Value -doLedgerCurrent(RPC::JsonContext&); +doLedgerCurrent(rpc::JsonContext&); json::Value -doLedgerData(RPC::JsonContext&); +doLedgerData(rpc::JsonContext&); json::Value -doLedgerEntry(RPC::JsonContext&); +doLedgerEntry(rpc::JsonContext&); json::Value -doLedgerHeader(RPC::JsonContext&); +doLedgerHeader(rpc::JsonContext&); json::Value -doLedgerRequest(RPC::JsonContext&); +doLedgerRequest(rpc::JsonContext&); json::Value -doLogLevel(RPC::JsonContext&); +doLogLevel(rpc::JsonContext&); json::Value -doLogRotate(RPC::JsonContext&); +doLogRotate(rpc::JsonContext&); json::Value -doManifest(RPC::JsonContext&); +doManifest(rpc::JsonContext&); json::Value -doNFTBuyOffers(RPC::JsonContext&); +doNFTBuyOffers(rpc::JsonContext&); json::Value -doNFTSellOffers(RPC::JsonContext&); +doNFTSellOffers(rpc::JsonContext&); json::Value -doNoRippleCheck(RPC::JsonContext&); +doNoRippleCheck(rpc::JsonContext&); json::Value -doOwnerInfo(RPC::JsonContext&); +doOwnerInfo(rpc::JsonContext&); json::Value -doPathFind(RPC::JsonContext&); +doPathFind(rpc::JsonContext&); json::Value -doPause(RPC::JsonContext&); +doPause(rpc::JsonContext&); json::Value -doPeers(RPC::JsonContext&); +doPeers(rpc::JsonContext&); json::Value -doPing(RPC::JsonContext&); +doPing(rpc::JsonContext&); json::Value -doPrint(RPC::JsonContext&); +doPrint(rpc::JsonContext&); json::Value -doRandom(RPC::JsonContext&); +doRandom(rpc::JsonContext&); json::Value -doResume(RPC::JsonContext&); +doResume(rpc::JsonContext&); json::Value -doPeerReservationsAdd(RPC::JsonContext&); +doPeerReservationsAdd(rpc::JsonContext&); json::Value -doPeerReservationsDel(RPC::JsonContext&); +doPeerReservationsDel(rpc::JsonContext&); json::Value -doPeerReservationsList(RPC::JsonContext&); +doPeerReservationsList(rpc::JsonContext&); json::Value -doRipplePathFind(RPC::JsonContext&); +doRipplePathFind(rpc::JsonContext&); json::Value -doServerDefinitions(RPC::JsonContext&); +doServerDefinitions(rpc::JsonContext&); json::Value -doServerInfo(RPC::JsonContext&); // for humans +doServerInfo(rpc::JsonContext&); // for humans json::Value -doServerState(RPC::JsonContext&); // for machines +doServerState(rpc::JsonContext&); // for machines json::Value -doSign(RPC::JsonContext&); +doSign(rpc::JsonContext&); json::Value -doSignFor(RPC::JsonContext&); +doSignFor(rpc::JsonContext&); json::Value -doSimulate(RPC::JsonContext&); +doSimulate(rpc::JsonContext&); json::Value -doStop(RPC::JsonContext&); +doStop(rpc::JsonContext&); json::Value -doSubmit(RPC::JsonContext&); +doSubmit(rpc::JsonContext&); json::Value -doSubmitMultiSigned(RPC::JsonContext&); +doSubmitMultiSigned(rpc::JsonContext&); json::Value -doSubscribe(RPC::JsonContext&); +doSubscribe(rpc::JsonContext&); json::Value -doTransactionEntry(RPC::JsonContext&); +doTransactionEntry(rpc::JsonContext&); json::Value -doTxJson(RPC::JsonContext&); +doTxJson(rpc::JsonContext&); json::Value -doTxHistory(RPC::JsonContext&); +doTxHistory(rpc::JsonContext&); json::Value -doTxReduceRelay(RPC::JsonContext&); +doTxReduceRelay(rpc::JsonContext&); json::Value -doUnlList(RPC::JsonContext&); +doUnlList(rpc::JsonContext&); json::Value -doUnsubscribe(RPC::JsonContext&); +doUnsubscribe(rpc::JsonContext&); json::Value -doValidationCreate(RPC::JsonContext&); +doValidationCreate(rpc::JsonContext&); json::Value -doWalletPropose(RPC::JsonContext&); +doWalletPropose(rpc::JsonContext&); json::Value -doValidators(RPC::JsonContext&); +doValidators(rpc::JsonContext&); json::Value -doValidatorListSites(RPC::JsonContext&); +doValidatorListSites(rpc::JsonContext&); json::Value -doValidatorInfo(RPC::JsonContext&); +doValidatorInfo(rpc::JsonContext&); json::Value -doVaultInfo(RPC::JsonContext&); +doVaultInfo(rpc::JsonContext&); } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp index b6d2fe259f..034cd8383f 100644 --- a/src/xrpld/rpc/handlers/VaultInfo.cpp +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp @@ -27,7 +27,7 @@ parseVault(json::Value const& params, json::Value& jvResult) { if (!uNodeIndex.parseHex(params[jss::vault_id].asString())) { - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } // else uNodeIndex holds the value we need @@ -37,14 +37,14 @@ parseVault(json::Value const& params, json::Value& jvResult) auto const id = parseBase58(params[jss::owner].asString()); if (!id) { - RPC::injectError(RpcActMalformed, jvResult); + rpc::injectError(RpcActMalformed, jvResult); return std::nullopt; } if (!(params[jss::seq].isInt() || params[jss::seq].isUInt()) || params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(json::Value::kMaxUInt)) { - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } @@ -53,7 +53,7 @@ parseVault(json::Value const& params, json::Value& jvResult) else { // Invalid combination of fields vault_id/owner/seq - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } @@ -61,10 +61,10 @@ parseVault(json::Value const& params, json::Value& jvResult) } json::Value -doVaultInfo(RPC::JsonContext& context) +doVaultInfo(rpc::JsonContext& context) { std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp b/src/xrpld/rpc/handlers/account/AccountChannels.cpp index 8a5c7dc6e3..d50bf1cf07 100644 --- a/src/xrpld/rpc/handlers/account/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp @@ -69,17 +69,17 @@ addChannel(json::Value& jsonLines, SLE const& line) // marker: opaque // optional, resume previous query // } json::Value -doAccountChannels(RPC::JsonContext& context) +doAccountChannels(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -97,7 +97,7 @@ doAccountChannels(RPC::JsonContext& context) if (params.isMember(jss::destination_account)) { if (!params[jss::destination_account].isString()) - return RPC::invalidFieldError(jss::destination_account); + return rpc::invalidFieldError(jss::destination_account); strDst = params[jss::destination_account].asString(); } @@ -108,7 +108,7 @@ doAccountChannels(RPC::JsonContext& context) return rpcError(RpcActMalformed); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountChannels, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountChannels, context)) return *err; json::Value jsonChannels{json::ValueType::Array}; @@ -126,7 +126,7 @@ doAccountChannels(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. @@ -157,7 +157,7 @@ doAccountChannels(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -182,7 +182,7 @@ doAccountChannels(RPC::JsonContext& context) if (++count == limit) { marker = sleCur->key(); - nextHint = RPC::getStartHint(sleCur, visitData.accountID); + nextHint = rpc::getStartHint(sleCur, visitData.accountID); } if (count <= limit && sleCur->getType() == ltPAYCHAN && @@ -213,7 +213,7 @@ doAccountChannels(RPC::JsonContext& context) for (auto const& item : visitData.items) addChannel(jsonChannels, *item); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; result[jss::channels] = std::move(jsonChannels); return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp index 058c10e224..d9cd41cbbc 100644 --- a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp +++ b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp @@ -19,30 +19,30 @@ namespace xrpl { json::Value -doAccountCurrencies(RPC::JsonContext& context) +doAccountCurrencies(rpc::JsonContext& context) { auto& params = context.params; if (!(params.isMember(jss::account) || params.isMember(jss::ident))) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); std::string strIdent; if (params.isMember(jss::account)) { if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); strIdent = params[jss::account].asString(); } else if (params.isMember(jss::ident)) { if (!params[jss::ident].isString()) - return RPC::invalidFieldError(jss::ident); + return rpc::invalidFieldError(jss::ident); strIdent = params[jss::ident].asString(); } // Get the current ledger std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -50,7 +50,7 @@ doAccountCurrencies(RPC::JsonContext& context) auto id = parseBase58(strIdent); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index 3a96593452..c618ad3b3a 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -84,7 +84,7 @@ injectSLE(json::Value& jv, SLE const& sle) // TODO(tom): what is that "default"? json::Value -doAccountInfo(RPC::JsonContext& context) +doAccountInfo(rpc::JsonContext& context) { auto& params = context.params; @@ -92,22 +92,22 @@ doAccountInfo(RPC::JsonContext& context) if (params.isMember(jss::account)) { if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); strIdent = params[jss::account].asString(); } else if (params.isMember(jss::ident)) { if (!params[jss::ident].isString()) - return RPC::invalidFieldError(jss::ident); + return rpc::invalidFieldError(jss::ident); strIdent = params[jss::ident].asString(); } else { - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -116,7 +116,7 @@ doAccountInfo(RPC::JsonContext& context) auto id = parseBase58(strIdent); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -154,7 +154,7 @@ doAccountInfo(RPC::JsonContext& context) { // It doesn't make sense to request the queue // with any closed or validated ledger. - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -206,7 +206,7 @@ doAccountInfo(RPC::JsonContext& context) if (context.apiVersion > 1u && params.isMember(jss::signer_lists) && !params[jss::signer_lists].isBool()) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -331,7 +331,7 @@ doAccountInfo(RPC::JsonContext& context) else { result[jss::account] = toBase58(accountID); - RPC::injectError(RpcActNotFound, result); + rpc::injectError(RpcActNotFound, result); } return result; diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index e69f70ca5a..f134c8af92 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -82,24 +82,24 @@ addLine(json::Value& jsonLines, RPCTrustLine const& line) // this account's side) // } json::Value -doAccountLines(RPC::JsonContext& context) +doAccountLines(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -116,12 +116,12 @@ doAccountLines(RPC::JsonContext& context) }(); if (!strPeer.empty() && !raPeerAccount) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountLines, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountLines, context)) return *err; // this flag allows the requester to ask incoming trustlines in default @@ -150,7 +150,7 @@ doAccountLines(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. @@ -181,7 +181,7 @@ doAccountLines(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -207,7 +207,7 @@ doAccountLines(RPC::JsonContext& context) if (++count == limit) { marker = sleCur->key(); - nextHint = RPC::getStartHint(sleCur, visitData.accountID); + nextHint = rpc::getStartHint(sleCur, visitData.accountID); } if (sleCur->getType() != ltRIPPLE_STATE) @@ -259,7 +259,7 @@ doAccountLines(RPC::JsonContext& context) for (auto const& item : visitData.items) addLine(jsonLines, item); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp index ea9bec0f45..580b93caa3 100644 --- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp @@ -35,14 +35,14 @@ namespace xrpl { * } */ json::Value -doAccountNFTs(RPC::JsonContext& context) +doAccountNFTs(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); auto id = parseBase58(params[jss::account].asString()); if (!id) @@ -51,7 +51,7 @@ doAccountNFTs(RPC::JsonContext& context) } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; auto const accountID{id.value()}; @@ -60,7 +60,7 @@ doAccountNFTs(RPC::JsonContext& context) return rpcError(RpcActNotFound); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountNfTokens, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountNfTokens, context)) return *err; uint256 marker; @@ -70,10 +70,10 @@ doAccountNFTs(RPC::JsonContext& context) { auto const& m = params[jss::marker]; if (!m.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); if (!marker.parseHex(m.asString())) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } auto const first = keylet::nftokenPage(keylet::nftokenPageMin(accountID), marker); @@ -125,7 +125,7 @@ doAccountNFTs(RPC::JsonContext& context) } if (markerSet && !markerFound) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); pastMarker = true; @@ -160,10 +160,10 @@ doAccountNFTs(RPC::JsonContext& context) } if (markerSet && !markerFound) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 4a34ff02fc..e855ed65e6 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -191,6 +192,13 @@ getAccountObjects( for (; entryIter != dirEntries.end(); ++entryIter) { auto const sleNode = ledger.read(keylet::child(*entryIter)); + if (!sleNode) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::doAccountObjects : null SLE"); + continue; + // LCOV_EXCL_STOP + } bool canAppend = true; @@ -257,24 +265,24 @@ getAccountObjects( } json::Value -doAccountObjects(RPC::JsonContext& context) +doAccountObjects(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; auto const id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -323,10 +331,10 @@ doAccountObjects(RPC::JsonContext& context) } else { - auto [rpcStatus, type] = RPC::chooseLedgerEntryType(params); + auto [rpcStatus, type] = rpc::chooseLedgerEntryType(params); - if (!RPC::isAccountObjectsValidType(type)) - return RPC::invalidFieldError(jss::type); + if (!rpc::isAccountObjectsValidType(type)) + return rpc::invalidFieldError(jss::type); if (rpcStatus) { @@ -341,7 +349,7 @@ doAccountObjects(RPC::JsonContext& context) } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountObjects, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountObjects, context)) return *err; uint256 dirIndex; @@ -350,18 +358,18 @@ doAccountObjects(RPC::JsonContext& context) { auto const& marker = params[jss::marker]; if (!marker.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); auto const& markerStr = marker.asString(); auto const& idx = markerStr.find(','); if (idx == std::string::npos) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!dirIndex.parseHex(markerStr.substr(0, idx))) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!entryIndex.parseHex(markerStr.substr(idx + 1))) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } std::optional sponsoredFilter; @@ -369,17 +377,17 @@ doAccountObjects(RPC::JsonContext& context) { auto const& sponsoredJv = params[jss::sponsored]; if (!sponsoredJv.isBool()) - return RPC::expectedFieldError(jss::sponsored, "boolean"); + return rpc::expectedFieldError(jss::sponsored, "boolean"); sponsoredFilter = sponsoredJv.asBool(); } if (!getAccountObjects( *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsoredFilter, result)) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp b/src/xrpld/rpc/handlers/account/AccountOffers.cpp index 4829ff56b1..1467b14b48 100644 --- a/src/xrpld/rpc/handlers/account/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp @@ -54,24 +54,24 @@ appendOfferJson(SLE::const_ref offer, json::Value& offers) // marker: opaque // optional, resume previous query // } json::Value -doAccountOffers(RPC::JsonContext& context) +doAccountOffers(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -83,7 +83,7 @@ doAccountOffers(RPC::JsonContext& context) return rpcError(RpcActNotFound); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountOffers, context)) return *err; json::Value& jsonOffers(result[jss::offers] = json::ValueType::Array); @@ -94,20 +94,20 @@ doAccountOffers(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!startAfter.parseHex(value)) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!std::getline(marker, value, ',')) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); try { @@ -115,7 +115,7 @@ doAccountOffers(RPC::JsonContext& context) } catch (boost::bad_lexical_cast&) { - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } // We then must check if the object pointed to by the marker is actually @@ -125,7 +125,7 @@ doAccountOffers(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -150,7 +150,7 @@ doAccountOffers(RPC::JsonContext& context) if (++count == limit) { marker = sle->key(); - nextHint = RPC::getStartHint(sle, accountID); + nextHint = rpc::getStartHint(sle, accountID); } if (count <= limit && sle->getType() == ltOFFER) @@ -176,7 +176,7 @@ doAccountOffers(RPC::JsonContext& context) for (auto const& offer : offers) appendOfferJson(offer, jsonOffers); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountTx.cpp b/src/xrpld/rpc/handlers/account/AccountTx.cpp index c43f560861..7b0c34e048 100644 --- a/src/xrpld/rpc/handlers/account/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/account/AccountTx.cpp @@ -43,11 +43,11 @@ static std::expected parseDelegateFilter(json::Value const& delegateNode) { if (!delegateNode.isObject()) - return std::unexpected(RPC::invalidFieldError(jss::delegate)); + return std::unexpected(rpc::invalidFieldError(jss::delegate)); if (!delegateNode.isMember(jss::delegate_filter) || !delegateNode[jss::delegate_filter].isString()) - return std::unexpected(RPC::invalidFieldError(jss::delegate_filter)); + return std::unexpected(rpc::invalidFieldError(jss::delegate_filter)); auto const& delegateFilterStr = delegateNode[jss::delegate_filter].asString(); @@ -58,7 +58,7 @@ parseDelegateFilter(json::Value const& delegateNode) if (delegateFilterStr == "authorizer") return DelegateType::Authorizer; - return std::unexpected(RPC::invalidFieldError(jss::delegate_filter)); + return std::unexpected(rpc::invalidFieldError(jss::delegate_filter)); }(); if (!typeResult) @@ -70,7 +70,7 @@ parseDelegateFilter(json::Value const& delegateNode) if (delegateNode.isMember(jss::counter_party)) { if (!delegateNode[jss::counter_party].isString()) - return std::unexpected(RPC::invalidFieldError(jss::counter_party)); + return std::unexpected(rpc::invalidFieldError(jss::counter_party)); counterparty = parseBase58(delegateNode[jss::counter_party].asString()); @@ -90,7 +90,7 @@ using LedgerSpecifier = RelationalDatabase::LedgerSpecifier; // parses args into a ledger specifier, or returns a Json object on error std::variant, json::Value> -parseLedgerArgs(RPC::Context& context, json::Value const& params) +parseLedgerArgs(rpc::Context& context, json::Value const& params) { json::Value response; // if ledger_index_min or max is specified, then ledger_hash or ledger_index @@ -100,7 +100,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) if ((params.isMember(jss::ledger_index_min) || params.isMember(jss::ledger_index_max)) && (params.isMember(jss::ledger_hash) || params.isMember(jss::ledger_index))) { - RPC::Status const status{RpcInvalidParams, "invalidParams"}; + rpc::Status const status{RpcInvalidParams, "invalidParams"}; status.inject(response); return response; } @@ -123,7 +123,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) auto& hashValue = params[jss::ledger_hash]; if (!hashValue.isString()) { - RPC::Status const status{RpcInvalidParams, "ledgerHashNotString"}; + rpc::Status const status{RpcInvalidParams, "ledgerHashNotString"}; status.inject(response); return response; } @@ -131,7 +131,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) LedgerHash hash; if (!hash.parseHex(hashValue.asString())) { - RPC::Status const status{RpcInvalidParams, "ledgerHashMalformed"}; + rpc::Status const status{RpcInvalidParams, "ledgerHashMalformed"}; status.inject(response); return response; } @@ -162,7 +162,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) } else { - RPC::Status const status{RpcInvalidParams, "ledger_index string malformed"}; + rpc::Status const status{RpcInvalidParams, "ledger_index string malformed"}; status.inject(response); return response; } @@ -172,8 +172,8 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) return std::optional{}; } -std::variant -getLedgerRange(RPC::Context& context, std::optional const& ledgerSpecifier) +std::variant +getLedgerRange(rpc::Context& context, std::optional const& ledgerSpecifier) { std::uint32_t uValidatedMin = 0; std::uint32_t uValidatedMax = 0; @@ -193,7 +193,7 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg if (ledgerSpecifier) { auto status = std::visit( - [&](auto const& ls) -> RPC::Status { + [&](auto const& ls) -> rpc::Status { using T = std::decay_t; if constexpr (std::is_same_v) { @@ -241,7 +241,7 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg } uLedgerMin = uLedgerMax = ledgerView->header().seq; } - return RPC::Status::kOK; + return rpc::Status::kOK; }, *ledgerSpecifier); @@ -251,15 +251,15 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg return LedgerRange{.min = uLedgerMin, .max = uLedgerMax}; } -std::pair -doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args) +std::pair +doAccountTxHelp(rpc::Context& context, AccountTxArgs const& args) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; AccountTxResult result; auto lgrRange = getLedgerRange(context, args.ledger); - if (auto stat = std::get_if(&lgrRange)) + if (auto stat = std::get_if(&lgrRange)) { // An error occurred getting the requested ledger range return {result, *stat}; @@ -318,12 +318,12 @@ doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args) json::Value populateJsonResponse( - std::pair const& res, + std::pair const& res, AccountTxArgs const& args, - RPC::JsonContext const& context) + rpc::JsonContext const& context) { json::Value response; - RPC::Status const& error = res.second; + rpc::Status const& error = res.second; if (error.toErrorCode() != RpcSuccess) { error.inject(response); @@ -374,13 +374,13 @@ populateJsonResponse( } auto const& sttx = txn->getSTransaction(); - RPC::insertDeliverMax(jvObj[jsonTx], sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(jvObj[jsonTx], sttx->getTxnType(), context.apiVersion); if (txnMeta) { jvObj[jss::meta] = txnMeta->getJson(JsonOptions::Values::IncludeDate); insertDeliveredAmount(jvObj[jss::meta], context, txn, *txnMeta); - RPC::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); - RPC::insertMPTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta); + rpc::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + rpc::insertMPTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta); } else { @@ -445,7 +445,7 @@ populateJsonResponse( // delegate-filtered query is only valid for a follow-up request that repeats // the same `delegate` object json::Value -doAccountTx(RPC::JsonContext& context) +doAccountTx(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); @@ -460,24 +460,24 @@ doAccountTx(RPC::JsonContext& context) // onwards only if (context.apiVersion > 1u && params.isMember(jss::binary) && !params[jss::binary].isBool()) { - return RPC::invalidFieldError(jss::binary); + return rpc::invalidFieldError(jss::binary); } if (context.apiVersion > 1u && params.isMember(jss::forward) && !params[jss::forward].isBool()) { - return RPC::invalidFieldError(jss::forward); + return rpc::invalidFieldError(jss::forward); } - if (auto const err = RPC::readLimitField(args.limit, RPC::Tuning::kAccountTx, context)) + if (auto const err = rpc::readLimitField(args.limit, rpc::tuning::kAccountTx, context)) return *err; args.binary = params.isMember(jss::binary) && params[jss::binary].asBool(); args.forward = params.isMember(jss::forward) && params[jss::forward].asBool(); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); auto const account = parseBase58(params[jss::account].asString()); if (!account) @@ -500,7 +500,7 @@ doAccountTx(RPC::JsonContext& context) !token[jss::ledger].isConvertibleTo(json::ValueType::UInt) || !token[jss::seq].isConvertibleTo(json::ValueType::UInt)) { - RPC::Status const status{ + rpc::Status const status{ RpcInvalidParams, "invalid marker. Provide ledger index via ledger field, and " "transaction sequence number via seq field"}; @@ -534,7 +534,7 @@ doAccountTx(RPC::JsonContext& context) params[jss::marker][jss::delegate].asBool(); if (markerFromDelegate != args.delegate.has_value()) { - RPC::Status const status{ + rpc::Status const status{ RpcInvalidParams, "Do not mix delegate and non-delegate pagination markers in account_tx; " "repeat the same `delegate` object when using a delegate marker."}; diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index a6730c8e2b..ff19d1d1e5 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -49,19 +49,19 @@ namespace xrpl { // gateway_balances [] [ [ ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; if (!(params.isMember(jss::account) || params.isMember(jss::ident))) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); std::string const strIdent( params.isMember(jss::account) ? params[jss::account].asString() @@ -72,13 +72,13 @@ doGatewayBalances(RPC::JsonContext& context) if (!id) return rpcError(RpcActMalformed); auto const accountID{id.value()}; - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; result[jss::account] = toBase58(accountID); if (context.apiVersion > 1u && !ledger->exists(keylet::account(accountID))) { - RPC::injectError(RpcActNotFound, result); + rpc::injectError(RpcActNotFound, result); return result; } @@ -126,11 +126,11 @@ doGatewayBalances(RPC::JsonContext& context) // not have currency issued by the account from the request. if (context.apiVersion < 2u) { - RPC::injectError(RpcInvalidHotwallet, result); + rpc::injectError(RpcInvalidHotwallet, result); } else { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); } return result; } diff --git a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp index d8bb65aba9..4be6e6f1af 100644 --- a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp +++ b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp @@ -26,7 +26,7 @@ namespace xrpl { static void fillTransaction( - RPC::JsonContext& context, + rpc::JsonContext& context, json::Value& txArray, AccountID const& accountID, std::uint32_t& sequence, @@ -49,17 +49,17 @@ fillTransaction( // transactions: true // optional, recommend transactions // } json::Value -doNoRippleCheck(RPC::JsonContext& context) +doNoRippleCheck(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError("account"); + return rpc::missingFieldError("account"); if (!params.isMember("role")) - return RPC::missingFieldError("role"); + return rpc::missingFieldError("role"); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); bool roleGateway = false; { @@ -70,12 +70,12 @@ doNoRippleCheck(RPC::JsonContext& context) } else if (role != "user") { - return RPC::invalidFieldError("role"); + return rpc::invalidFieldError("role"); } } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kNoRippleCheck, context)) + if (auto err = readLimitField(limit, rpc::tuning::kNoRippleCheck, context)) return *err; bool transactions = false; @@ -89,11 +89,11 @@ doNoRippleCheck(RPC::JsonContext& context) if (context.apiVersion > 1u && params.isMember(jss::transactions) && !params[jss::transactions].isBool()) { - return RPC::invalidFieldError(jss::transactions); + return rpc::invalidFieldError(jss::transactions); } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -104,7 +104,7 @@ doNoRippleCheck(RPC::JsonContext& context) auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; diff --git a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp index 7cfed3cf53..2f5f76c619 100644 --- a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp +++ b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp @@ -17,11 +17,11 @@ namespace xrpl { // 'ident' : , // } json::Value -doOwnerInfo(RPC::JsonContext& context) +doOwnerInfo(rpc::JsonContext& context) { if (!context.params.isMember(jss::account) && !context.params.isMember(jss::ident)) { - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); } std::string const strIdent = context.params.isMember(jss::account) diff --git a/src/xrpld/rpc/handlers/admin/BlackList.cpp b/src/xrpld/rpc/handlers/admin/BlackList.cpp index 5065a41ec4..7a72651373 100644 --- a/src/xrpld/rpc/handlers/admin/BlackList.cpp +++ b/src/xrpld/rpc/handlers/admin/BlackList.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doBlackList(RPC::JsonContext& context) +doBlackList(rpc::JsonContext& context) { auto& rm = context.app.getResourceManager(); if (context.params.isMember(jss::threshold)) diff --git a/src/xrpld/rpc/handlers/admin/UnlList.cpp b/src/xrpld/rpc/handlers/admin/UnlList.cpp index c3835c7ae0..61b5e4c640 100644 --- a/src/xrpld/rpc/handlers/admin/UnlList.cpp +++ b/src/xrpld/rpc/handlers/admin/UnlList.cpp @@ -12,7 +12,7 @@ namespace xrpl { json::Value -doUnlList(RPC::JsonContext& context) +doUnlList(rpc::JsonContext& context) { json::Value obj(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp index 9ec1157e66..91db16bb4f 100644 --- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp @@ -19,10 +19,10 @@ namespace xrpl { // can_delete [||now|always|never] json::Value -doCanDelete(RPC::JsonContext& context) +doCanDelete(rpc::JsonContext& context) { if (!context.app.getSHAMapStore().advisoryDelete()) - return RPC::makeError(RpcNotEnabled); + return rpc::makeError(RpcNotEnabled); json::Value ret(json::ValueType::Object); @@ -56,20 +56,20 @@ doCanDelete(RPC::JsonContext& context) { canDeleteSeq = context.app.getSHAMapStore().getLastRotated(); if (canDeleteSeq == 0u) - return RPC::makeError(RpcNotReady); + return rpc::makeError(RpcNotReady); } else if (uint256 lh; lh.parseHex(canDeleteStr)) { auto ledger = context.ledgerMaster.getLedgerByHash(lh); if (!ledger) - return RPC::makeError(RpcLgrNotFound, "ledgerNotFound"); + return rpc::makeError(RpcLgrNotFound, "ledgerNotFound"); canDeleteSeq = ledger->header().seq; } else { - return RPC::makeError(RpcInvalidParams); + return rpc::makeError(RpcInvalidParams); } } diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp index a62ca44cca..78a2fec410 100644 --- a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp +++ b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp @@ -9,10 +9,10 @@ namespace xrpl { json::Value -doLedgerCleaner(RPC::JsonContext& context) +doLedgerCleaner(rpc::JsonContext& context) { context.app.getLedgerCleaner().clean(context.params); - return RPC::makeObjectValue("Cleaner configured"); + return rpc::makeObjectValue("Cleaner configured"); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp index de1e4439cf..80a49aea3f 100644 --- a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp +++ b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp @@ -13,10 +13,10 @@ namespace xrpl { // ledger_index : // } json::Value -doLedgerRequest(RPC::JsonContext& context) +doLedgerRequest(rpc::JsonContext& context) { - context.loadType = Resource::kFeeHeavyBurdenRpc; - auto res = RPC::getOrAcquireLedger(context); + context.loadType = resource::kFeeHeavyBurdenRpc; + auto res = rpc::getOrAcquireLedger(context); if (!res.has_value()) return res.error(); diff --git a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp index 0849bad944..5ff3e6727e 100644 --- a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp +++ b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp @@ -30,7 +30,7 @@ validationSeed(json::Value const& params) // This command requires Role::ADMIN access because it makes // no sense to ask an untrusted server for this. json::Value -doValidationCreate(RPC::JsonContext& context) +doValidationCreate(rpc::JsonContext& context) { json::Value obj(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp index 4b5f1821e3..62def76f9a 100644 --- a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp @@ -52,7 +52,7 @@ estimateEntropy(std::string const& input) // passphrase: // } json::Value -doWalletPropose(RPC::JsonContext& context) +doWalletPropose(rpc::JsonContext& context) { return walletPropose(context.params); } @@ -68,7 +68,7 @@ walletPropose(json::Value const& params) { if (!params[jss::key_type].isString()) { - return RPC::expectedFieldError(jss::key_type, "string"); + return rpc::expectedFieldError(jss::key_type, "string"); } keyType = keyTypeFromString(params[jss::key_type].asString()); @@ -83,11 +83,11 @@ walletPropose(json::Value const& params) { if (params.isMember(jss::passphrase)) { - seed = RPC::parseXrplLibSeed(params[jss::passphrase]); + seed = rpc::parseXrplLibSeed(params[jss::passphrase]); } else if (params.isMember(jss::seed)) { - seed = RPC::parseXrplLibSeed(params[jss::seed]); + seed = rpc::parseXrplLibSeed(params[jss::seed]); } if (seed) @@ -110,7 +110,7 @@ walletPropose(json::Value const& params) { json::Value err; - seed = RPC::getSeedFromRPC(params, err); + seed = rpc::getSeedFromRPC(params, err); if (!seed) return err; diff --git a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp index 1ff5aa1a27..4aae350810 100644 --- a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp +++ b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp @@ -16,7 +16,7 @@ namespace xrpl { json::Value -doLogLevel(RPC::JsonContext& context) +doLogLevel(rpc::JsonContext& context) { // log_level if (not context.params.isMember(jss::severity)) diff --git a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp index 5f5c3e64df..ca935540dc 100644 --- a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp +++ b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp @@ -9,10 +9,10 @@ namespace xrpl { json::Value -doLogRotate(RPC::JsonContext& context) +doLogRotate(rpc::JsonContext& context) { context.app.getPerfLog().rotate(); - return RPC::makeObjectValue(context.app.getLogs().rotate()); + return rpc::makeObjectValue(context.app.getLogs().rotate()); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp index 568dcdaa26..b318af06f9 100644 --- a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp @@ -21,15 +21,15 @@ namespace xrpl { // } // XXX Might allow domain for manual connections. json::Value -doConnect(RPC::JsonContext& context) +doConnect(rpc::JsonContext& context) { if (context.app.config().standalone()) { - return RPC::makeError(RpcNotSynced); + return rpc::makeError(RpcNotSynced); } if (!context.params.isMember(jss::ip)) - return RPC::missingFieldError(jss::ip); + return rpc::missingFieldError(jss::ip); if (context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(json::ValueType::Int)) @@ -49,12 +49,12 @@ doConnect(RPC::JsonContext& context) } auto const ipStr = context.params[jss::ip].asString(); - auto ip = beast::IP::Endpoint::fromString(ipStr); + auto ip = beast::ip::Endpoint::fromString(ipStr); if (!isUnspecified(ip)) context.app.getOverlay().connect(ip.atPort(iPort)); - return RPC::makeObjectValue( + return rpc::makeObjectValue( "attempting connection to IP:" + ipStr + " port: " + std::to_string(iPort)); } diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp index 23b0d094b4..579ad85f41 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp @@ -15,12 +15,12 @@ namespace xrpl { json::Value -doPeerReservationsAdd(RPC::JsonContext& context) +doPeerReservationsAdd(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); // Returning JSON from every function ruins any attempt to encapsulate // the pattern of "get field F as type T, and diagnose an error if it is @@ -36,7 +36,7 @@ doPeerReservationsAdd(RPC::JsonContext& context) // essentially an optional (the "maybe monad" in Haskell) with a non-unit // type for the failure case to capture more information. if (!params[jss::public_key].isString()) - return RPC::expectedFieldError(jss::public_key, "a string"); + return rpc::expectedFieldError(jss::public_key, "a string"); // Same for the pattern of "if field F is present, make sure it has type T // and get it". @@ -44,7 +44,7 @@ doPeerReservationsAdd(RPC::JsonContext& context) if (params.isMember(jss::description)) { if (!params[jss::description].isString()) - return RPC::expectedFieldError(jss::description, "a string"); + return rpc::expectedFieldError(jss::description, "a string"); desc = params[jss::description].asString(); } diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp index c2a8319876..e5912a5eca 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp @@ -14,15 +14,15 @@ namespace xrpl { json::Value -doPeerReservationsDel(RPC::JsonContext& context) +doPeerReservationsDel(rpc::JsonContext& context) { auto const& params = context.params; // We repeat much of the parameter parsing from `doPeerReservationsAdd`. if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); if (!params[jss::public_key].isString()) - return RPC::expectedFieldError(jss::public_key, "a string"); + return rpc::expectedFieldError(jss::public_key, "a string"); std::optional optPk = parseBase58(TokenType::NodePublic, params[jss::public_key].asString()); diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp index e0204159fd..30e19a5c0b 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doPeerReservationsList(RPC::JsonContext& context) +doPeerReservationsList(rpc::JsonContext& context) { auto const& reservations = context.app.getPeerReservations().list(); // Enumerate the reservations in context.app.getPeerReservations() diff --git a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp index ab14325f0e..99f069e27e 100644 --- a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp @@ -17,7 +17,7 @@ namespace xrpl { json::Value -doPeers(RPC::JsonContext& context) +doPeers(rpc::JsonContext& context) { json::Value jvResult(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp index ce06a6e480..00a259bb52 100644 --- a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp +++ b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp @@ -12,7 +12,7 @@ namespace xrpl { json::Value -doLedgerAccept(RPC::JsonContext& context) +doLedgerAccept(rpc::JsonContext& context) { json::Value jvResult; diff --git a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp index 3e86bd4632..949eccf1e1 100644 --- a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp +++ b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp @@ -6,15 +6,15 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc json::Value -doStop(RPC::JsonContext& context) +doStop(rpc::JsonContext& context) { context.app.signalStop("RPC"); - return RPC::makeObjectValue(systemName() + " server stopping"); + return rpc::makeObjectValue(systemName() + " server stopping"); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp index be3ce13d45..d97ce9dac4 100644 --- a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp @@ -30,34 +30,34 @@ namespace xrpl { // drops: 64-bit uint (as string) // } json::Value -doChannelAuthorize(RPC::JsonContext& context) +doChannelAuthorize(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } auto const& params(context.params); for (auto const& p : {jss::channel_id, jss::amount}) { if (!params.isMember(p)) - return RPC::missingFieldError(p); + return rpc::missingFieldError(p); } // Compatibility if a key type isn't specified. If it is, the // keypairForSignature code will validate parameters and return // the appropriate error. if (!params.isMember(jss::key_type) && !params.isMember(jss::secret)) - return RPC::missingFieldError(jss::secret); + return rpc::missingFieldError(jss::secret); json::Value result; std::optional> const keyPair = - RPC::keypairForSignature(params, result, context.apiVersion); + rpc::keypairForSignature(params, result, context.apiVersion); XRPL_ASSERT( - keyPair || RPC::containsError(result), + keyPair || rpc::containsError(result), "xrpl::doChannelAuthorize : valid keyPair or an error"); - if (!keyPair || RPC::containsError(result)) + if (!keyPair || rpc::containsError(result)) return result; PublicKey const& pk = keyPair->first; @@ -86,7 +86,7 @@ doChannelAuthorize(RPC::JsonContext& context) catch (std::exception const& ex) { // LCOV_EXCL_START - result = RPC::makeError( + result = rpc::makeError( RpcInternal, "Exception occurred during signing: " + std::string(ex.what())); // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp index 781e160f54..6aac058a56 100644 --- a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp @@ -15,18 +15,18 @@ namespace xrpl { // secret: // } json::Value -doSign(RPC::JsonContext& context) +doSign(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; NetworkOPs::FailHard const failType = NetworkOPs::doFailHard( context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()); - auto ret = RPC::transactionSign( + auto ret = rpc::transactionSign( context.params, context.apiVersion, failType, diff --git a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp index 35274d51f4..2b9c830647 100644 --- a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp @@ -16,18 +16,18 @@ namespace xrpl { // secret: // } json::Value -doSignFor(RPC::JsonContext& context) +doSignFor(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; auto const failHard = context.params[jss::fail_hard].asBool(); auto const failType = NetworkOPs::doFailHard(failHard); - auto ret = RPC::transactionSignFor( + auto ret = rpc::transactionSignFor( context.params, context.apiVersion, failType, diff --git a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp index 341980ba6d..8017e7058b 100644 --- a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doConsensusInfo(RPC::JsonContext& context) +doConsensusInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp index 16e4789025..ca9bff31f5 100644 --- a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doFetchInfo(RPC::JsonContext& context) +doFetchInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp index 789c6dcf17..421f23d237 100644 --- a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp @@ -111,7 +111,7 @@ getCountsJson(Application& app, int minObjectCount) // min_count: // optional, defaults to 10 // } json::Value -doGetCounts(RPC::JsonContext& context) +doGetCounts(rpc::JsonContext& context) { int minCount = 10; diff --git a/src/xrpld/rpc/handlers/admin/status/Print.cpp b/src/xrpld/rpc/handlers/admin/status/Print.cpp index 99fd01c9f4..1e1f7f0662 100644 --- a/src/xrpld/rpc/handlers/admin/status/Print.cpp +++ b/src/xrpld/rpc/handlers/admin/status/Print.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doPrint(RPC::JsonContext& context) +doPrint(rpc::JsonContext& context) { JsonPropertyStream stream; if (context.params.isObject() && context.params[jss::params].isArray() && diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp index efe1529ccb..705e03c1a0 100644 --- a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp @@ -12,12 +12,12 @@ namespace xrpl { json::Value -doValidatorInfo(RPC::JsonContext& context) +doValidatorInfo(rpc::JsonContext& context) { // return error if not configured as validator auto const validationPK = context.app.getValidationPublicKey(); if (!validationPK) - return RPC::notValidatorError(); + return rpc::notValidatorError(); json::Value ret; diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp index 7497105c50..9bc8b6bcda 100644 --- a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doValidatorListSites(RPC::JsonContext& context) +doValidatorListSites(rpc::JsonContext& context) { return context.app.getValidatorSites().getJson(); } diff --git a/src/xrpld/rpc/handlers/admin/status/Validators.cpp b/src/xrpld/rpc/handlers/admin/status/Validators.cpp index 48e4466861..d605a38f1b 100644 --- a/src/xrpld/rpc/handlers/admin/status/Validators.cpp +++ b/src/xrpld/rpc/handlers/admin/status/Validators.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doValidators(RPC::JsonContext& context) +doValidators(rpc::JsonContext& context) { return context.app.getValidators().getJson(); } diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.cpp b/src/xrpld/rpc/handlers/ledger/Ledger.cpp index 23a97a5026..51f5bdf348 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.cpp +++ b/src/xrpld/rpc/handlers/ledger/Ledger.cpp @@ -34,7 +34,7 @@ #include namespace xrpl { -namespace RPC { +namespace rpc { LedgerHandler::LedgerHandler(JsonContext& context) : context_(context) { @@ -107,7 +107,7 @@ LedgerHandler::check() { return RpcTooBusy; } - context_.loadType = binary ? Resource::kFeeMediumBurdenRpc : Resource::kFeeHeavyBurdenRpc; + context_.loadType = binary ? resource::kFeeMediumBurdenRpc : resource::kFeeHeavyBurdenRpc; } if (*queue) @@ -162,10 +162,10 @@ LedgerHandler::writeResult(json::Value& value) value[jss::warnings] = std::move(warnings); } -} // namespace RPC +} // namespace rpc std::pair -doLedgerGrpc(RPC::GRPCContext& context) +doLedgerGrpc(rpc::GRPCContext& context) { auto begin = std::chrono::system_clock::now(); org::xrpl::rpc::v1::GetLedgerRequest const& request = context.params; @@ -173,7 +173,7 @@ doLedgerGrpc(RPC::GRPCContext& context) grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h b/src/xrpld/rpc/handlers/ledger/Ledger.h index 59b64832f7..07d24b497d 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.h +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h @@ -18,7 +18,7 @@ namespace json { class Object; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { struct JsonContext; @@ -42,9 +42,9 @@ public: // NOLINTBEGIN(readability-identifier-naming) static constexpr char name[] = "ledger"; - static constexpr unsigned minApiVer = RPC::kApiMinimumSupportedVersion; + static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion; - static constexpr unsigned maxApiVer = RPC::kApiMaximumValidVersion; + static constexpr unsigned maxApiVer = rpc::kApiMaximumValidVersion; static constexpr Role role = Role::USER; @@ -59,4 +59,4 @@ private: int options_ = 0; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp index 7ea314292f..def0a73cd3 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp @@ -10,7 +10,7 @@ namespace xrpl { json::Value -doLedgerClosed(RPC::JsonContext& context) +doLedgerClosed(rpc::JsonContext& context) { auto ledger = context.ledgerMaster.getClosedLedger(); XRPL_ASSERT(ledger, "xrpl::doLedgerClosed : non-null closed ledger"); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp index 1d05774163..ac04084848 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doLedgerCurrent(RPC::JsonContext& context) +doLedgerCurrent(rpc::JsonContext& context) { json::Value jvResult; jvResult[jss::ledger_current_index] = context.ledgerMaster.getCurrentLedgerIndex(); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp index 64ab30374b..697d8e52b8 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp @@ -36,12 +36,12 @@ namespace xrpl { // state: array of state nodes // marker: resume point, if any json::Value -doLedgerData(RPC::JsonContext& context) +doLedgerData(rpc::JsonContext& context) { std::shared_ptr lpLedger; auto const& params = context.params; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -51,14 +51,14 @@ doLedgerData(RPC::JsonContext& context) { json::Value const& jMarker = params[jss::marker]; if (!(jMarker.isString() && key.parseHex(jMarker.asString()))) - return RPC::expectedFieldError(jss::marker, "valid"); + return rpc::expectedFieldError(jss::marker, "valid"); } bool isBinary = false; if (params.isMember(jss::binary)) { if (!params[jss::binary].isBool()) - return RPC::expectedFieldError(jss::binary, "boolean"); + return rpc::expectedFieldError(jss::binary, "boolean"); isBinary = params[jss::binary].asBool(); } @@ -67,12 +67,12 @@ doLedgerData(RPC::JsonContext& context) { json::Value const& jLimit = params[jss::limit]; if (!jLimit.isIntegral()) - return RPC::expectedFieldError(jss::limit, "integer"); + return rpc::expectedFieldError(jss::limit, "integer"); limit = jLimit.asInt(); } - auto maxLimit = RPC::Tuning::pageLength(isBinary); + auto maxLimit = rpc::tuning::pageLength(isBinary); if ((limit < 0) || ((limit > maxLimit) && (!isUnlimited(context.role)))) limit = maxLimit; @@ -86,7 +86,7 @@ doLedgerData(RPC::JsonContext& context) *lpLedger, &context, isBinary ? static_cast(LedgerFill::Options::Binary) : 0)); } - auto [rpcStatus, type] = RPC::chooseLedgerEntryType(params); + auto [rpcStatus, type] = rpc::chooseLedgerEntryType(params); if (rpcStatus) { jvResult.clear(); @@ -131,14 +131,14 @@ doLedgerData(RPC::JsonContext& context) } std::pair -doLedgerDataGrpc(RPC::GRPCContext& context) +doLedgerDataGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerDataRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerDataResponse response; grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) @@ -177,7 +177,7 @@ doLedgerDataGrpc(RPC::GRPCContext& con e = ledger->sles.upperBound(*key); } - int maxLimit = RPC::Tuning::pageLength(true); + int maxLimit = rpc::tuning::pageLength(true); for (auto i = ledger->sles.upperBound(startKey); i != e; ++i) { diff --git a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp index f1a9253de2..5e83bedf08 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp @@ -15,7 +15,7 @@ namespace xrpl { std::pair -doLedgerDiffGrpc(RPC::GRPCContext& context) +doLedgerDiffGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerDiffRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerDiffResponse response; @@ -24,13 +24,13 @@ doLedgerDiffGrpc(RPC::GRPCContext& con std::shared_ptr baseLedgerRv; std::shared_ptr desiredLedgerRv; - if (RPC::ledgerFromSpecifier(baseLedgerRv, request.base_ledger(), context)) + if (rpc::ledgerFromSpecifier(baseLedgerRv, request.base_ledger(), context)) { grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "base ledger not found"}; return {response, errorStatus}; } - if (RPC::ledgerFromSpecifier(desiredLedgerRv, request.desired_ledger(), context)) + if (rpc::ledgerFromSpecifier(desiredLedgerRv, request.desired_ledger(), context)) { grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "desired ledger not found"}; return {response, errorStatus}; diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 784be779bb..0dd52b6776 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -66,11 +66,11 @@ parseObjectID( json::StaticString const fieldName, std::string const& expectedType = "hex string or object") { - if (auto const uNodeIndex = LedgerEntryHelpers::parse(params)) + if (auto const uNodeIndex = ledger_entry_helpers::parse(params)) { return *uNodeIndex; } - return LedgerEntryHelpers::invalidFieldError("malformedRequest", fieldName, expectedType); + return ledger_entry_helpers::invalidFieldError("malformedRequest", fieldName, expectedType); } static std::expected @@ -101,12 +101,12 @@ parseAccountRoot( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - if (auto const account = LedgerEntryHelpers::parse(params)) + if (auto const account = ledger_entry_helpers::parse(params)) { return keylet::account(*account).key; } - return LedgerEntryHelpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); + return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); } auto const parseAmendments = fixed(keylet::amendments()); @@ -122,17 +122,18 @@ parseAMM( return parseObjectID(params, fieldName); } - if (auto const value = LedgerEntryHelpers::hasRequired(params, {jss::asset, jss::asset2}); + if (auto const value = ledger_entry_helpers::hasRequired(params, {jss::asset, jss::asset2}); !value) { return std::unexpected(value.error()); } - auto const asset = LedgerEntryHelpers::requiredAsset(params, jss::asset, "malformedRequest"); + auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, "malformedRequest"); if (!asset) return std::unexpected(asset.error()); - auto const asset2 = LedgerEntryHelpers::requiredAsset(params, jss::asset2, "malformedRequest"); + auto const asset2 = + ledger_entry_helpers::requiredAsset(params, jss::asset2, "malformedRequest"); if (!asset2) return std::unexpected(asset2.error()); @@ -147,7 +148,7 @@ parseBridge( { if (!params.isMember(jss::bridge)) { - return std::unexpected(LedgerEntryHelpers::missingFieldError(jss::bridge)); + return std::unexpected(ledger_entry_helpers::missingFieldError(jss::bridge)); } if (params[jss::bridge].isString()) @@ -155,11 +156,11 @@ parseBridge( return parseObjectID(params, fieldName); } - auto const bridge = LedgerEntryHelpers::parseBridgeFields(params[jss::bridge]); + auto const bridge = ledger_entry_helpers::parseBridgeFields(params[jss::bridge]); if (!bridge) return std::unexpected(bridge.error()); - auto const account = LedgerEntryHelpers::requiredAccountID( + auto const account = ledger_entry_helpers::requiredAccountID( params, jss::bridge_account, "malformedBridgeAccount"); if (!account) return std::unexpected(account.error()); @@ -167,7 +168,7 @@ parseBridge( STXChainBridge::ChainType const chainType = STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor()); if (account.value() != bridge->door(chainType)) - return LedgerEntryHelpers::malformedError("malformedRequest", ""); + return ledger_entry_helpers::malformedError("malformedRequest", ""); return keylet::bridge(*bridge, chainType).key; } @@ -193,16 +194,16 @@ parseCredential( } auto const subject = - LedgerEntryHelpers::requiredAccountID(cred, jss::subject, "malformedRequest"); + ledger_entry_helpers::requiredAccountID(cred, jss::subject, "malformedRequest"); if (!subject) return std::unexpected(subject.error()); auto const issuer = - LedgerEntryHelpers::requiredAccountID(cred, jss::issuer, "malformedRequest"); + ledger_entry_helpers::requiredAccountID(cred, jss::issuer, "malformedRequest"); if (!issuer) return std::unexpected(issuer.error()); - auto const credType = LedgerEntryHelpers::requiredHexBlob( + auto const credType = ledger_entry_helpers::requiredHexBlob( cred, jss::credential_type, kMaxCredentialTypeLength, "malformedRequest"); if (!credType) return std::unexpected(credType.error()); @@ -222,12 +223,12 @@ parseDelegate( } auto const account = - LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!account) return std::unexpected(account.error()); auto const authorize = - LedgerEntryHelpers::requiredAccountID(params, jss::authorize, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(params, jss::authorize, "malformedAddress"); if (!authorize) return std::unexpected(authorize.error()); @@ -239,7 +240,7 @@ parseAuthorizeCredentials(json::Value const& jv) { if (!jv.isArray()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array"); } @@ -247,7 +248,7 @@ parseAuthorizeCredentials(json::Value const& jv) if (n > kMaxCredentialsArraySize) { return std::unexpected( - LedgerEntryHelpers::malformedError( + ledger_entry_helpers::malformedError( "malformedAuthorizedCredentials", "Invalid field '" + std::string(jss::authorized_credentials) + "', array too long.")); @@ -256,7 +257,7 @@ parseAuthorizeCredentials(json::Value const& jv) if (n == 0) { return std::unexpected( - LedgerEntryHelpers::malformedError( + ledger_entry_helpers::malformedError( "malformedAuthorizedCredentials", "Invalid field '" + std::string(jss::authorized_credentials) + "', array empty.")); } @@ -266,23 +267,23 @@ parseAuthorizeCredentials(json::Value const& jv) { if (!jo.isObject()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array of objects"); } - if (auto const value = LedgerEntryHelpers::hasRequired( + if (auto const value = ledger_entry_helpers::hasRequired( jo, {jss::issuer, jss::credential_type}, "malformedAuthorizedCredentials"); !value) { return std::unexpected(value.error()); } - auto const issuer = LedgerEntryHelpers::requiredAccountID( + auto const issuer = ledger_entry_helpers::requiredAccountID( jo, jss::issuer, "malformedAuthorizedCredentials"); if (!issuer) return std::unexpected(issuer.error()); - auto const credentialType = LedgerEntryHelpers::requiredHexBlob( + auto const credentialType = ledger_entry_helpers::requiredHexBlob( jo, jss::credential_type, kMaxCredentialTypeLength, "malformedAuthorizedCredentials"); if (!credentialType) return std::unexpected(credentialType.error()); @@ -309,13 +310,13 @@ parseDepositPreauth( if ((dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials))) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Must have exactly one of `authorized` and " "`authorized_credentials`."); } - auto const owner = LedgerEntryHelpers::requiredAccountID(dp, jss::owner, "malformedOwner"); + auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, "malformedOwner"); if (!owner) { return std::unexpected(owner.error()); @@ -323,11 +324,11 @@ parseDepositPreauth( if (dp.isMember(jss::authorized)) { - if (auto const authorized = LedgerEntryHelpers::parse(dp[jss::authorized])) + if (auto const authorized = ledger_entry_helpers::parse(dp[jss::authorized])) { return keylet::depositPreauth(*owner, *authorized).key; } - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorized", jss::authorized, "AccountID"); } @@ -340,7 +341,7 @@ parseDepositPreauth( if (sorted.empty()) { // TODO: this error message is bad/inaccurate - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array"); } @@ -353,10 +354,10 @@ parseDID( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - auto const account = LedgerEntryHelpers::parse(params); + auto const account = ledger_entry_helpers::parse(params); if (!account) { - return LedgerEntryHelpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); + return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); } return keylet::did(*account).key; @@ -377,12 +378,13 @@ parseDirectoryNode( (!params[jss::sub_index].isConvertibleTo(json::ValueType::UInt) || params[jss::sub_index].isBool())) { - return LedgerEntryHelpers::invalidFieldError("malformedRequest", jss::sub_index, "number"); + return ledger_entry_helpers::invalidFieldError( + "malformedRequest", jss::sub_index, "number"); } if (params.isMember(jss::owner) == params.isMember(jss::dir_root)) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields."); } @@ -390,27 +392,27 @@ parseDirectoryNode( if (params.isMember(jss::dir_root)) { - if (auto const uDirRoot = LedgerEntryHelpers::parse(params[jss::dir_root])) + if (auto const uDirRoot = ledger_entry_helpers::parse(params[jss::dir_root])) { return keylet::page(*uDirRoot, uSubIndex).key; } - return LedgerEntryHelpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash"); + return ledger_entry_helpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash"); } if (params.isMember(jss::owner)) { - auto const ownerID = LedgerEntryHelpers::parse(params[jss::owner]); + auto const ownerID = ledger_entry_helpers::parse(params[jss::owner]); if (!ownerID) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAddress", jss::owner, "AccountID"); } return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key; } - return LedgerEntryHelpers::malformedError("malformedRequest", ""); + return ledger_entry_helpers::malformedError("malformedRequest", ""); } static std::expected @@ -424,10 +426,10 @@ parseEscrow( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -449,7 +451,7 @@ parseFixed( } if (!params.asBool()) { - return LedgerEntryHelpers::invalidFieldError("invalidParams", fieldName, "true"); + return ledger_entry_helpers::invalidFieldError("invalidParams", fieldName, "true"); } return keylet.key; @@ -486,10 +488,10 @@ parseLoanBroker( return parseObjectID(params, fieldName, "hex string"); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -508,10 +510,10 @@ parseLoan( } auto const id = - LedgerEntryHelpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker"); + ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::loan_seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -529,13 +531,13 @@ parseMPToken( return parseObjectID(params, fieldName); } - auto const mptIssuanceID = - LedgerEntryHelpers::requiredUInt192(params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); + auto const mptIssuanceID = ledger_entry_helpers::requiredUInt192( + params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); if (!mptIssuanceID) return std::unexpected(mptIssuanceID.error()); auto const account = - LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAccount"); + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount"); if (!account) return std::unexpected(account.error()); @@ -548,10 +550,10 @@ parseMPTokenIssuance( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - auto const mptIssuanceID = LedgerEntryHelpers::parse(params); + auto const mptIssuanceID = ledger_entry_helpers::parse(params); if (!mptIssuanceID) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedMPTokenIssuance", fieldName, "Hash192"); } @@ -589,11 +591,12 @@ parseOffer( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -611,12 +614,13 @@ parseOracle( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAccount"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount"); if (!id) return std::unexpected(id.error()); - auto const seq = - LedgerEntryHelpers::requiredUInt32(params, jss::oracle_document_id, "malformedDocumentID"); + auto const seq = ledger_entry_helpers::requiredUInt32( + params, jss::oracle_document_id, "malformedDocumentID"); if (!seq) return std::unexpected(seq.error()); @@ -645,16 +649,16 @@ parsePermissionedDomain( if (!pd.isObject()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedRequest", fieldName, "hex string or object"); } auto const account = - LedgerEntryHelpers::requiredAccountID(pd, jss::account, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(pd, jss::account, "malformedAddress"); if (!account) return std::unexpected(account.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(pd, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -675,7 +679,7 @@ parseRippleState( } if (auto const value = - LedgerEntryHelpers::hasRequired(jvRippleState, {jss::currency, jss::accounts}); + ledger_entry_helpers::hasRequired(jvRippleState, {jss::currency, jss::accounts}); !value) { return std::unexpected(value.error()); @@ -683,27 +687,27 @@ parseRippleState( if (!jvRippleState[jss::accounts].isArray() || jvRippleState[jss::accounts].size() != 2) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedRequest", jss::accounts, "length-2 array of Accounts"); } - auto const id1 = LedgerEntryHelpers::parse(jvRippleState[jss::accounts][0u]); - auto const id2 = LedgerEntryHelpers::parse(jvRippleState[jss::accounts][1u]); + auto const id1 = ledger_entry_helpers::parse(jvRippleState[jss::accounts][0u]); + auto const id2 = ledger_entry_helpers::parse(jvRippleState[jss::accounts][1u]); if (!id1 || !id2) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAddress", jss::accounts, "array of Accounts"); } if (id1 == id2) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Cannot have a trustline to self."); } if (!jvRippleState[jss::currency].isString() || jvRippleState[jss::currency] == "" || !toCurrency(uCurrency, jvRippleState[jss::currency].asString())) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedCurrency", jss::currency, "Currency"); } @@ -729,12 +733,12 @@ parseSponsorship( return parseObjectID(params, fieldName); auto const sponsorID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); + ledger_entry_helpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); if (!sponsorID) return std::unexpected(sponsorID.error()); auto const sponseeID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + ledger_entry_helpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); if (!sponseeID) return std::unexpected(sponseeID.error()); @@ -752,12 +756,13 @@ parseTicket( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!id) return std::unexpected(id.error()); auto const seq = - LedgerEntryHelpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest"); + ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -775,11 +780,11 @@ parseVault( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -797,11 +802,11 @@ parseXChainOwnedClaimID( return parseObjectID(claimId, fieldName); } - auto const bridgeSpec = LedgerEntryHelpers::parseBridgeFields(claimId); + auto const bridgeSpec = ledger_entry_helpers::parseBridgeFields(claimId); if (!bridgeSpec) return std::unexpected(bridgeSpec.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32( + auto const seq = ledger_entry_helpers::requiredUInt32( claimId, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID"); if (!seq) { @@ -823,11 +828,11 @@ parseXChainOwnedCreateAccountClaimID( return parseObjectID(claimId, fieldName); } - auto const bridgeSpec = LedgerEntryHelpers::parseBridgeFields(claimId); + auto const bridgeSpec = ledger_entry_helpers::parseBridgeFields(claimId); if (!bridgeSpec) return std::unexpected(bridgeSpec.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32( + auto const seq = ledger_entry_helpers::requiredUInt32( claimId, jss::xchain_owned_create_account_claim_id, "malformedXChainOwnedCreateAccountClaimID"); @@ -853,7 +858,7 @@ struct LedgerEntry // ... // } json::Value -doLedgerEntry(RPC::JsonContext& context) +doLedgerEntry(rpc::JsonContext& context) { static auto kLedgerEntryParsers = std::to_array({ #pragma push_macro("LEDGER_ENTRY") @@ -892,11 +897,11 @@ doLedgerEntry(RPC::JsonContext& context) if (hasMoreThanOneMember) { - return RPC::makeParamError("Too many fields provided."); + return rpc::makeParamError("Too many fields provided."); } std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -936,7 +941,7 @@ doLedgerEntry(RPC::JsonContext& context) jvResult[jss::error] = "unknownOption"; return jvResult; } - return RPC::makeParamError("No ledger_entry params provided."); + return rpc::makeParamError("No ledger_entry params provided."); } } catch (json::Error const& e) @@ -945,7 +950,7 @@ doLedgerEntry(RPC::JsonContext& context) { // For apiVersion 2 onwards, any parsing failures that throw // this exception return an invalidParam error. - return RPC::makeError(RpcInvalidParams); + return rpc::makeError(RpcInvalidParams); } throw; @@ -956,7 +961,7 @@ doLedgerEntry(RPC::JsonContext& context) if (uNodeIndex.isZero()) { - RPC::injectError(RpcEntryNotFound, jvResult); + rpc::injectError(RpcEntryNotFound, jvResult); return jvResult; } @@ -969,13 +974,13 @@ doLedgerEntry(RPC::JsonContext& context) if (!sleNode) { // Not found. - RPC::injectError(RpcEntryNotFound, jvResult); + rpc::injectError(RpcEntryNotFound, jvResult); return jvResult; } if ((expectedType != ltANY) && (expectedType != sleNode->getType())) { - RPC::injectError(RpcUnexpectedLedgerType, jvResult); + rpc::injectError(RpcUnexpectedLedgerType, jvResult); return jvResult; } @@ -996,14 +1001,14 @@ doLedgerEntry(RPC::JsonContext& context) } std::pair -doLedgerEntryGrpc(RPC::GRPCContext& context) +doLedgerEntryGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerEntryRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerEntryResponse response; grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h index 57c4e58242..1b119db04e 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h @@ -22,7 +22,7 @@ #include #include -namespace xrpl::LedgerEntryHelpers { +namespace xrpl::ledger_entry_helpers { inline std::unexpected missingFieldError(json::StaticString const field, std::optional err = std::nullopt) @@ -30,7 +30,7 @@ missingFieldError(json::StaticString const field, std::optional err json::Value json = json::ValueType::Object; json[jss::error] = err.value_or("malformedRequest"); json[jss::error_code] = RpcInvalidParams; - json[jss::error_message] = RPC::missingFieldMessage(std::string(field.cStr())); + json[jss::error_message] = rpc::missingFieldMessage(std::string(field.cStr())); return std::unexpected(json); } @@ -40,7 +40,7 @@ invalidFieldError(std::string const& err, json::StaticString const field, std::s json::Value json = json::ValueType::Object; json[jss::error] = err; json[jss::error_code] = RpcInvalidParams; - json[jss::error_message] = RPC::expectedFieldMessage(field, type); + json[jss::error_message] = rpc::expectedFieldMessage(field, type); return std::unexpected(json); } @@ -291,4 +291,4 @@ parseBridgeFields(json::Value const& params) *lockingChainDoor, lockingChainIssue, *issuingChainDoor, issuingChainIssue); } -} // namespace xrpl::LedgerEntryHelpers +} // namespace xrpl::ledger_entry_helpers diff --git a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp index e2cb80615b..ec3c9fe602 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp @@ -18,10 +18,10 @@ namespace xrpl { // ledger_index : // } json::Value -doLedgerHeader(RPC::JsonContext& context) +doLedgerHeader(rpc::JsonContext& context) { std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp index 7f54f81423..e95c51c483 100644 --- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp @@ -61,13 +61,13 @@ toIso8601(NetClock::time_point tp) } json::Value -doAMMInfo(RPC::JsonContext& context) +doAMMInfo(rpc::JsonContext& context) { auto const& params(context.params); json::Value result; std::shared_ptr ledger; - result = RPC::lookupLedger(ledger, context); + result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -174,7 +174,7 @@ doAMMInfo(RPC::JsonContext& context) auto const r = getValuesFromContextParams(); if (!r) { - RPC::injectError(r.error(), result); + rpc::injectError(r.error(), result); return result; } diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp index abad196246..0f796df3a4 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp @@ -11,15 +11,15 @@ namespace xrpl { json::Value -doBookChanges(RPC::JsonContext& context) +doBookChanges(rpc::JsonContext& context) { std::shared_ptr ledger; - json::Value result = RPC::lookupLedger(ledger, context); + json::Value result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; - return RPC::computeBookChanges(ledger); + return rpc::computeBookChanges(ledger); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp index 63dee76f1b..ae539a59f3 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp @@ -32,19 +32,19 @@ validateTakerJSON(json::Value const& taker, json::StaticString const& name) { if (!taker.isMember(jss::currency) && !taker.isMember(jss::mpt_issuance_id)) { - return RPC::missingFieldError((boost::format("%s.currency") % name.cStr()).str()); + return rpc::missingFieldError((boost::format("%s.currency") % name.cStr()).str()); } if (taker.isMember(jss::mpt_issuance_id) && (taker.isMember(jss::currency) || taker.isMember(jss::issuer))) { - return RPC::invalidFieldError(name.cStr()); + return rpc::invalidFieldError(name.cStr()); } if ((taker.isMember(jss::currency) && !taker[jss::currency].isString()) || (taker.isMember(jss::mpt_issuance_id) && !taker[jss::mpt_issuance_id].isString())) { - return RPC::expectedFieldError( + return rpc::expectedFieldError( (boost::format("%s.currency") % name.cStr()).str(), "string"); } @@ -71,7 +71,7 @@ parseTakerAssetJSON( if (!toCurrency(issue.currency, taker[jss::currency].asString())) { JLOG(j.info()) << boost::format("Bad %s currency.") % name.cStr(); - return RPC::makeError( + return rpc::makeError( assetError, (boost::format("Invalid field '%s.currency', bad currency.") % name.cStr()).str()); } @@ -82,7 +82,7 @@ parseTakerAssetJSON( MPTID mptid; if (!mptid.parseHex(taker[jss::mpt_issuance_id].asString())) { - return RPC::makeError( + return rpc::makeError( assetError, (boost::format("Invalid field '%s.mpt_issuance_id'") % name.cStr()).str()); } @@ -113,20 +113,20 @@ parseTakerIssuerJSON( { if (!taker[jss::issuer].isString()) { - return RPC::expectedFieldError( + return rpc::expectedFieldError( (boost::format("%s.issuer") % name.cStr()).str(), "string"); } if (!toIssuer(issue.account, taker[jss::issuer].asString())) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', bad issuer.") % name.cStr()).str()); } if (issue.account == noAccount()) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', bad issuer account one.") % name.cStr()) @@ -140,7 +140,7 @@ parseTakerIssuerJSON( if (isXRP(issue.currency) && !isXRP(issue.account)) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format( "Unneeded field '%s.issuer' for XRP currency " @@ -151,7 +151,7 @@ parseTakerIssuerJSON( if (!isXRP(issue.currency) && isXRP(issue.account)) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', expected non-XRP issuer.") % name.cStr()) .str()); @@ -162,7 +162,7 @@ parseTakerIssuerJSON( } json::Value -doBookOffers(RPC::JsonContext& context) +doBookOffers(rpc::JsonContext& context) { // VFALCO TODO Here is a terrible place for this kind of business // logic. It needs to be moved elsewhere and documented, @@ -171,25 +171,25 @@ doBookOffers(RPC::JsonContext& context) return rpcError(RpcTooBusy); std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; if (!context.params.isMember(jss::taker_pays)) - return RPC::missingFieldError(jss::taker_pays); + return rpc::missingFieldError(jss::taker_pays); if (!context.params.isMember(jss::taker_gets)) - return RPC::missingFieldError(jss::taker_gets); + return rpc::missingFieldError(jss::taker_gets); json::Value const& takerPays = context.params[jss::taker_pays]; json::Value const& takerGets = context.params[jss::taker_gets]; if (!takerPays.isObjectOrNull()) - return RPC::objectFieldError(jss::taker_pays); + return rpc::objectFieldError(jss::taker_pays); if (!takerGets.isObjectOrNull()) - return RPC::objectFieldError(jss::taker_gets); + return rpc::objectFieldError(jss::taker_gets); if (auto const err = validateTakerJSON(takerPays, jss::taker_pays)) return *err; @@ -215,11 +215,11 @@ doBookOffers(RPC::JsonContext& context) if (context.params.isMember(jss::taker)) { if (!context.params[jss::taker].isString()) - return RPC::expectedFieldError(jss::taker, "string"); + return rpc::expectedFieldError(jss::taker, "string"); takerID = parseBase58(context.params[jss::taker].asString()); if (!takerID) - return RPC::invalidFieldError(jss::taker); + return rpc::invalidFieldError(jss::taker); } std::optional domain; @@ -229,7 +229,7 @@ doBookOffers(RPC::JsonContext& context) if (!context.params[jss::domain].isString() || !num.parseHex(context.params[jss::domain].asString())) { - return RPC::makeError(RpcDomainMalformed, "Unable to parse domain."); + return rpc::makeError(RpcDomainMalformed, "Unable to parse domain."); } domain = num; @@ -238,11 +238,11 @@ doBookOffers(RPC::JsonContext& context) if (book.in == book.out) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; - return RPC::makeError(RpcBadMarket); + return rpc::makeError(RpcBadMarket); } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kBookOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kBookOffers, context)) return *err; bool const bProof(context.params.isMember(jss::proof)); @@ -260,7 +260,7 @@ doBookOffers(RPC::JsonContext& context) jvMarker, jvResult); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return jvResult; } diff --git a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp index 343d539277..9d109fe16f 100644 --- a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp +++ b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp @@ -32,17 +32,17 @@ namespace xrpl { // } json::Value -doDepositAuthorized(RPC::JsonContext& context) +doDepositAuthorized(rpc::JsonContext& context) { json::Value const& params = context.params; // Validate source_account. if (!params.isMember(jss::source_account)) - return RPC::missingFieldError(jss::source_account); + return rpc::missingFieldError(jss::source_account); if (!params[jss::source_account].isString()) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::source_account, "a string")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::source_account, "a string")); } auto srcID = parseBase58(params[jss::source_account].asString()); @@ -52,11 +52,11 @@ doDepositAuthorized(RPC::JsonContext& context) // Validate destination_account. if (!params.isMember(jss::destination_account)) - return RPC::missingFieldError(jss::destination_account); + return rpc::missingFieldError(jss::destination_account); if (!params[jss::destination_account].isString()) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::destination_account, "a string")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::destination_account, "a string")); } auto dstID = parseBase58(params[jss::destination_account].asString()); @@ -66,7 +66,7 @@ doDepositAuthorized(RPC::JsonContext& context) // Validate ledger. std::shared_ptr ledger; - json::Value result = RPC::lookupLedger(ledger, context); + json::Value result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -74,7 +74,7 @@ doDepositAuthorized(RPC::JsonContext& context) // If source account is not in the ledger it can't be authorized. if (!ledger->exists(keylet::account(srcAcct))) { - RPC::injectError(RpcSrcActNotFound, result); + rpc::injectError(RpcSrcActNotFound, result); return result; } @@ -82,7 +82,7 @@ doDepositAuthorized(RPC::JsonContext& context) auto const sleDest = ledger->read(keylet::account(dstAcct)); if (!sleDest) { - RPC::injectError(RpcDstActNotFound, result); + rpc::injectError(RpcDstActNotFound, result); return result; } @@ -96,15 +96,15 @@ doDepositAuthorized(RPC::JsonContext& context) auto const& creds(params[jss::credentials]); if (!creds.isArray() || !creds) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "is non-empty array of CredentialID(hash256)")); } if (creds.size() > kMaxCredentialsArraySize) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::credentials, "array too long")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::credentials, "array too long")); } lifeExtender.reserve(creds.size()); @@ -112,9 +112,9 @@ doDepositAuthorized(RPC::JsonContext& context) { if (!jo.isString()) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "an array of CredentialID(hash256)")); } @@ -122,34 +122,34 @@ doDepositAuthorized(RPC::JsonContext& context) auto const credS = jo.asString(); if (!credH.parseHex(credS)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "an array of CredentialID(hash256)")); } SLE::const_pointer sleCred = ledger->read(keylet::credential(credH)); if (!sleCred) { - RPC::injectError(RpcBadCredentials, "credentials don't exist", result); + rpc::injectError(RpcBadCredentials, "credentials don't exist", result); return result; } if (!sleCred->isFlag(lsfAccepted)) { - RPC::injectError(RpcBadCredentials, "credentials aren't accepted", result); + rpc::injectError(RpcBadCredentials, "credentials aren't accepted", result); return result; } if (credentials::checkExpired(*sleCred, ledger->header().parentCloseTime)) { - RPC::injectError(RpcBadCredentials, "credentials are expired", result); + rpc::injectError(RpcBadCredentials, "credentials are expired", result); return result; } if ((*sleCred)[sfSubject] != srcAcct) { - RPC::injectError( + rpc::injectError( RpcBadCredentials, "credentials doesn't belong to the root account", result); return result; } @@ -157,7 +157,7 @@ doDepositAuthorized(RPC::JsonContext& context) auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]); if (!ins) { - RPC::injectError(RpcBadCredentials, "duplicates in credentials", result); + rpc::injectError(RpcBadCredentials, "duplicates in credentials", result); return result; } lifeExtender.push_back(std::move(sleCred)); diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp index 632456a3fa..f493000d0b 100644 --- a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp @@ -48,7 +48,7 @@ using Prices = bimap>, multiset_of const& f) { @@ -149,26 +149,26 @@ getStats(Prices::right_const_iterator const& begin, Prices::right_const_iterator * range - {most recent, most recent - time_threshold} [optional] */ json::Value -doGetAggregatePrice(RPC::JsonContext& context) +doGetAggregatePrice(rpc::JsonContext& context) { json::Value result; auto const& params(context.params); static constexpr std::uint16_t kMaxOracles = 200; if (!params.isMember(jss::oracles)) - return RPC::missingFieldError(jss::oracles); + return rpc::missingFieldError(jss::oracles); if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 || params[jss::oracles].size() > kMaxOracles) { - RPC::injectError(RpcOracleMalformed, result); + rpc::injectError(RpcOracleMalformed, result); return result; } if (!params.isMember(jss::base_asset)) - return RPC::missingFieldError(jss::base_asset); + return rpc::missingFieldError(jss::base_asset); if (!params.isMember(jss::quote_asset)) - return RPC::missingFieldError(jss::quote_asset); + return rpc::missingFieldError(jss::quote_asset); // Lambda to validate uint type // support positive int, uint, and a number represented as a string @@ -213,38 +213,38 @@ doGetAggregatePrice(RPC::JsonContext& context) auto const trim = getField(jss::trim); if (std::holds_alternative(trim)) { - RPC::injectError(std::get(trim), result); + rpc::injectError(std::get(trim), result); return result; } if (params.isMember(jss::trim) && (std::get(trim) == 0 || std::get(trim) > kMaxTrim)) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } auto const timeThreshold = getField(jss::time_threshold, 0); if (std::holds_alternative(timeThreshold)) { - RPC::injectError(std::get(timeThreshold), result); + rpc::injectError(std::get(timeThreshold), result); return result; } auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset); if (std::holds_alternative(baseAsset)) { - RPC::injectError(std::get(baseAsset), result); + rpc::injectError(std::get(baseAsset), result); return result; } auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset); if (std::holds_alternative(quoteAsset)) { - RPC::injectError(std::get(quoteAsset), result); + rpc::injectError(std::get(quoteAsset), result); return result; } std::shared_ptr ledger; - result = RPC::lookupLedger(ledger, context); + result = rpc::lookupLedger(ledger, context); if (!ledger) return result; // LCOV_EXCL_LINE @@ -255,7 +255,7 @@ doGetAggregatePrice(RPC::JsonContext& context) { if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account)) { - RPC::injectError(RpcOracleMalformed, result); + rpc::injectError(RpcOracleMalformed, result); return result; } auto const documentID = validUInt(oracle, jss::oracle_document_id) @@ -264,7 +264,7 @@ doGetAggregatePrice(RPC::JsonContext& context) auto const account = parseBase58(oracle[jss::account].asString()); if (!account || account->isZero() || !documentID) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -298,7 +298,7 @@ doGetAggregatePrice(RPC::JsonContext& context) if (prices.empty()) { - RPC::injectError(RpcObjectNotFound, result); + rpc::injectError(RpcObjectNotFound, result); return result; } @@ -321,7 +321,7 @@ doGetAggregatePrice(RPC::JsonContext& context) if (prices.empty()) { // LCOV_EXCL_START - RPC::injectError(RpcInternal, result); + rpc::injectError(RpcInternal, result); return result; // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp index 88e4392dad..60b48ac5a6 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp @@ -10,15 +10,15 @@ namespace xrpl { json::Value -doNFTBuyOffers(RPC::JsonContext& context) +doNFTBuyOffers(rpc::JsonContext& context) { if (!context.params.isMember(jss::nft_id)) - return RPC::missingFieldError(jss::nft_id); + return rpc::missingFieldError(jss::nft_id); uint256 nftId; if (!nftId.parseHex(context.params[jss::nft_id].asString())) - return RPC::invalidFieldError(jss::nft_id); + return rpc::invalidFieldError(jss::nft_id); return enumerateNFTOffers(context, nftId, keylet::nftBuys(nftId)); } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h index 70bc258d77..e03830ae0d 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h @@ -52,15 +52,15 @@ appendNftOfferJson(Application const& app, SLE::const_ref offer, json::Value& of // marker: opaque // optional, resume previous query // } inline json::Value -enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const& directory) +enumerateNFTOffers(rpc::JsonContext& context, uint256 const& nftId, Keylet const& directory) { unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kNftOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kNftOffers, context)) return *err; std::shared_ptr ledger; - if (auto result = RPC::lookupLedger(ledger, context); !ledger) + if (auto result = rpc::lookupLedger(ledger, context); !ledger) return result; if (!ledger->exists(directory)) @@ -83,7 +83,7 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const json::Value const& marker(context.params[jss::marker]); if (!marker.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); if (!startAfter.parseHex(marker.asString())) return rpcError(RpcInvalidParams); @@ -127,7 +127,7 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const for (auto const& offer : offers) appendNftOfferJson(context.app, offer, jsonOffers); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp index 309df93605..8b09b42a34 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp @@ -10,15 +10,15 @@ namespace xrpl { json::Value -doNFTSellOffers(RPC::JsonContext& context) +doNFTSellOffers(rpc::JsonContext& context) { if (!context.params.isMember(jss::nft_id)) - return RPC::missingFieldError(jss::nft_id); + return rpc::missingFieldError(jss::nft_id); uint256 nftId; if (!nftId.parseHex(context.params[jss::nft_id].asString())) - return RPC::invalidFieldError(jss::nft_id); + return rpc::invalidFieldError(jss::nft_id); return enumerateNFTOffers(context, nftId, keylet::nftSells(nftId)); } diff --git a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp index 3a6b52ee98..c46238a5a5 100644 --- a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp +++ b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp @@ -13,7 +13,7 @@ namespace xrpl { json::Value -doPathFind(RPC::JsonContext& context) +doPathFind(rpc::JsonContext& context) { if (context.app.config().pathSearchMax == 0) return rpcError(RpcNotSupported); @@ -34,7 +34,7 @@ doPathFind(RPC::JsonContext& context) if (sSubCommand == "create") { - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; context.infoSub->clearRequest(); return context.app.getPathRequestManager().makePathRequest( context.infoSub, lpLedger, context.params); diff --git a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp index b7edfb6dbe..923cb0f7f5 100644 --- a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp +++ b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp @@ -21,12 +21,12 @@ namespace xrpl { // This interface is deprecated. json::Value -doRipplePathFind(RPC::JsonContext& context) +doRipplePathFind(rpc::JsonContext& context) { if (context.app.config().pathSearchMax == 0) return rpcError(RpcNotSupported); - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; std::shared_ptr lpLedger; json::Value jvResult; @@ -37,7 +37,7 @@ doRipplePathFind(RPC::JsonContext& context) // No ledger specified, use pathfinding defaults // and dispatch to pathfinding engine if (context.app.getLedgerMaster().getValidatedLedgerAge() > - RPC::Tuning::kMaxValidatedLedgerAge) + rpc::tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return rpcError(RpcNoNetwork); @@ -146,11 +146,11 @@ doRipplePathFind(RPC::JsonContext& context) } // The caller specified a ledger - jvResult = RPC::lookupLedger(lpLedger, context); + jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; - RPC::LegacyPathFind const lpf(isUnlimited(context.role), context.app); + rpc::LegacyPathFind const lpf(isUnlimited(context.role), context.app); if (!lpf.isOk()) return rpcError(RpcTooBusy); diff --git a/src/xrpld/rpc/handlers/server_info/Feature.cpp b/src/xrpld/rpc/handlers/server_info/Feature.cpp index bd7198b61c..1906658106 100644 --- a/src/xrpld/rpc/handlers/server_info/Feature.cpp +++ b/src/xrpld/rpc/handlers/server_info/Feature.cpp @@ -18,7 +18,7 @@ namespace xrpl { // vetoed : true/false // } json::Value -doFeature(RPC::JsonContext& context) +doFeature(rpc::JsonContext& context) { if (context.params.isMember(jss::feature)) { diff --git a/src/xrpld/rpc/handlers/server_info/Fee.cpp b/src/xrpld/rpc/handlers/server_info/Fee.cpp index 1fe5476d50..2e4147fc1e 100644 --- a/src/xrpld/rpc/handlers/server_info/Fee.cpp +++ b/src/xrpld/rpc/handlers/server_info/Fee.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doFee(RPC::JsonContext& context) +doFee(rpc::JsonContext& context) { auto result = context.app.getTxQ().doRPC(context.app); if (result.type() == json::ValueType::Object) @@ -16,7 +16,7 @@ doFee(RPC::JsonContext& context) // LCOV_EXCL_START UNREACHABLE("xrpl::doFee : invalid result type"); - RPC::injectError(RpcInternal, context.params); + rpc::injectError(RpcInternal, context.params); return context.params; // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/server_info/Manifest.cpp b/src/xrpld/rpc/handlers/server_info/Manifest.cpp index cb1771750b..c0b29d8275 100644 --- a/src/xrpld/rpc/handlers/server_info/Manifest.cpp +++ b/src/xrpld/rpc/handlers/server_info/Manifest.cpp @@ -12,12 +12,12 @@ namespace xrpl { json::Value -doManifest(RPC::JsonContext& context) +doManifest(rpc::JsonContext& context) { auto& params = context.params; if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); auto const requested = params[jss::public_key].asString(); @@ -27,7 +27,7 @@ doManifest(RPC::JsonContext& context) auto const pk = parseBase58(TokenType::NodePublic, requested); if (!pk) { - RPC::injectError(RpcInvalidParams, ret); + rpc::injectError(RpcInvalidParams, ret); return ret; } diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index cce1b3e07f..b561ce6d38 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -382,7 +382,7 @@ getServerDefinitionsJson() } json::Value -doServerDefinitions(RPC::JsonContext& context) +doServerDefinitions(rpc::JsonContext& context) { auto& params = context.params; @@ -390,7 +390,7 @@ doServerDefinitions(RPC::JsonContext& context) if (params.isMember(jss::hash)) { if (!params[jss::hash].isString() || !hash.parseHex(params[jss::hash].asString())) - return RPC::invalidFieldError(jss::hash); + return rpc::invalidFieldError(jss::hash); } auto const& defs = detail::getDefinitions(); diff --git a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp index aaad9d2b02..fd6e2f717f 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp @@ -9,7 +9,7 @@ namespace xrpl { json::Value -doServerInfo(RPC::JsonContext& context) +doServerInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/server_info/ServerState.cpp b/src/xrpld/rpc/handlers/server_info/ServerState.cpp index acf4e9eb43..e0d43d4053 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerState.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerState.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doServerState(RPC::JsonContext& context) +doServerState(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/server_info/Version.h b/src/xrpld/rpc/handlers/server_info/Version.h index f25d8679ba..40ad4e5e71 100644 --- a/src/xrpld/rpc/handlers/server_info/Version.h +++ b/src/xrpld/rpc/handlers/server_info/Version.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class VersionHandler { @@ -34,9 +34,9 @@ public: // NOLINTBEGIN(readability-identifier-naming) static constexpr char const* name = "version"; - static constexpr unsigned minApiVer = RPC::kApiMinimumSupportedVersion; + static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion; - static constexpr unsigned maxApiVer = RPC::kApiMaximumValidVersion; + static constexpr unsigned maxApiVer = rpc::kApiMaximumValidVersion; static constexpr Role role = Role::USER; @@ -48,4 +48,4 @@ private: bool betaEnabled_; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp index 93840bb6d6..7ce432c49e 100644 --- a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp @@ -27,7 +27,7 @@ namespace xrpl { json::Value -doSubscribe(RPC::JsonContext& context) +doSubscribe(rpc::JsonContext& context) { InfoSub::pointer ispSub; json::Value jvResult(json::ValueType::Object); @@ -79,7 +79,7 @@ doSubscribe(RPC::JsonContext& context) } catch (std::runtime_error const& ex) { - return RPC::makeParamError(ex.what()); + return rpc::makeParamError(ex.what()); } } else @@ -174,7 +174,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.params[accountsProposed].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[accountsProposed]); + auto ids = rpc::parseAccountIds(context.params[accountsProposed]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.subAccount(ispSub, ids, true); @@ -185,7 +185,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.params[jss::accounts].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[jss::accounts]); + auto ids = rpc::parseAccountIds(context.params[jss::accounts]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.subAccount(ispSub, ids, false); @@ -197,7 +197,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; auto const& req = context.params[jss::account_history_tx_stream]; if (!req.isMember(jss::account) || !req[jss::account].isString()) return rpcError(RpcInvalidParams); @@ -230,11 +230,11 @@ doSubscribe(RPC::JsonContext& context) Book book; - if (auto const err = RPC::parseSubUnsubJson(book.in, j, jss::taker_pays, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.in, j, jss::taker_pays, context.j); err != RpcSuccess) return rpcError(err); - if (auto const err = RPC::parseSubUnsubJson(book.out, j, jss::taker_gets, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.out, j, jss::taker_gets, context.j); err != RpcSuccess) return rpcError(err); @@ -285,7 +285,7 @@ doSubscribe(RPC::JsonContext& context) if ((j.isMember(jss::snapshot) && j[jss::snapshot].asBool()) || (j.isMember(jss::state_now) && j[jss::state_now].asBool())) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; std::shared_ptr lpLedger = context.app.getLedgerMaster().getPublishedLedger(); if (lpLedger) @@ -299,7 +299,7 @@ doSubscribe(RPC::JsonContext& context) field == jss::asks ? reversed(book) : book, takerID ? *takerID : noAccount(), false, - RPC::Tuning::kBookOffers.rDefault, + rpc::tuning::kBookOffers.rDefault, jvMarker, jvOffers); diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp index af42af2a55..33c785a9bb 100644 --- a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp @@ -18,7 +18,7 @@ namespace xrpl { json::Value -doUnsubscribe(RPC::JsonContext& context) +doUnsubscribe(rpc::JsonContext& context) { InfoSub::pointer ispSub; json::Value jvResult(json::ValueType::Object); @@ -106,7 +106,7 @@ doUnsubscribe(RPC::JsonContext& context) if (!context.params[accountsProposed].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[accountsProposed]); + auto ids = rpc::parseAccountIds(context.params[accountsProposed]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.unsubAccount(ispSub, ids, true); @@ -117,7 +117,7 @@ doUnsubscribe(RPC::JsonContext& context) if (!context.params[jss::accounts].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[jss::accounts]); + auto ids = rpc::parseAccountIds(context.params[jss::accounts]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.unsubAccount(ispSub, ids, false); @@ -161,11 +161,11 @@ doUnsubscribe(RPC::JsonContext& context) Book book; - if (auto const err = RPC::parseSubUnsubJson(book.in, jv, jss::taker_pays, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.in, jv, jss::taker_pays, context.j); err != RpcSuccess) return rpcError(err); - if (auto const err = RPC::parseSubUnsubJson(book.out, jv, jss::taker_gets, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.out, jv, jss::taker_gets, context.j); err != RpcSuccess) return rpcError(err); diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 0f163c7356..8441add08b 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -44,7 +44,7 @@ namespace xrpl { static std::expected -getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) +getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context) { // autofill Sequence bool const hasTicketSeq = txJson.isMember(sfTicketSequence.jsonName); @@ -53,14 +53,14 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) { // sanity check, should fail earlier // LCOV_EXCL_START - return std::unexpected(RPC::invalidFieldError("tx.Account")); + return std::unexpected(rpc::invalidFieldError("tx.Account")); // LCOV_EXCL_STOP } auto const srcAddressID = parseBase58(accountStr.asString()); if (!srcAddressID.has_value()) { return std::unexpected( - RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage("tx.Account"))); + rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage("tx.Account"))); } SLE::const_pointer const sle = context.app.getOpenLedger().current()->read(keylet::account(*srcAddressID)); @@ -88,7 +88,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") if (sigObject.isMember(jss::Signers)) { if (!sigObject[jss::Signers].isArray()) - return RPC::invalidFieldError(fieldPrefix + ".Signers"); + return rpc::invalidFieldError(fieldPrefix + ".Signers"); // check multisigned signers for (unsigned index = 0; index < sigObject[jss::Signers].size(); index++) { @@ -96,7 +96,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") if (!signer.isObject() || !signer.isMember(jss::Signer) || !signer[jss::Signer].isObject()) { - return RPC::invalidFieldError( + return rpc::invalidFieldError( fieldPrefix + ".Signers[" + std::to_string(index) + "]"); } @@ -133,7 +133,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") } static std::optional -autofillTx(json::Value& txJson, RPC::JsonContext& context) +autofillTx(json::Value& txJson, rpc::JsonContext& context) { if (auto error = autofillSignature(txJson)) return error; @@ -142,7 +142,7 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) { auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; if (!sponsorSignature.isObject()) - return RPC::objectFieldError(sfSponsorSignature.jsonName); + return rpc::objectFieldError(sfSponsorSignature.jsonName); if (auto const error = autofillSignature(sponsorSignature, "tx.SponsorSignature")) return error; @@ -167,7 +167,7 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) { // Autofill Fee after normalizing nested signer fields so the fee // estimator sees the full transaction shape. - auto feeOrError = RPC::getCurrentNetworkFee( + auto feeOrError = rpc::getCurrentNetworkFee( context.role, context.app.config(), context.app.getFeeTrack(), @@ -191,18 +191,18 @@ getTxJsonFromParams(json::Value const& params) { if (params.isMember(jss::tx_json)) { - return RPC::makeParamError("Can only include one of `tx_blob` and `tx_json`."); + return rpc::makeParamError("Can only include one of `tx_blob` and `tx_json`."); } auto const txBlob = params[jss::tx_blob]; if (!txBlob.isString()) { - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); } auto unHexed = strUnHex(txBlob.asString()); if (!unHexed || unHexed->empty()) - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); try { @@ -211,7 +211,7 @@ getTxJsonFromParams(json::Value const& params) } catch (std::runtime_error const&) { - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); } } else if (params.isMember(jss::tx_json)) @@ -219,30 +219,30 @@ getTxJsonFromParams(json::Value const& params) txJson = params[jss::tx_json]; if (!txJson.isObject()) { - return RPC::objectFieldError(jss::tx_json); + return rpc::objectFieldError(jss::tx_json); } } else { - return RPC::makeParamError("Neither `tx_blob` nor `tx_json` included."); + return rpc::makeParamError("Neither `tx_blob` nor `tx_json` included."); } // basic sanity checks for transaction shape if (!txJson.isMember(jss::TransactionType)) { - return RPC::missingFieldError("tx.TransactionType"); + return rpc::missingFieldError("tx.TransactionType"); } if (!txJson.isMember(jss::Account)) { - return RPC::missingFieldError("tx.Account"); + return rpc::missingFieldError("tx.Account"); } return txJson; } static json::Value -simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) +simulateTxn(rpc::JsonContext& context, std::shared_ptr transaction) { json::Value jvResult; // Process the transaction @@ -290,11 +290,11 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::Values::None); - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata); - RPC::insertNFTSyntheticInJson( + rpc::insertNFTSyntheticInJson( jvResult, transaction->getSTransaction(), *result.metadata); - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( jvResult[jss::meta], transaction->getSTransaction(), *result.metadata); } } @@ -317,23 +317,23 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) // binary: // } json::Value -doSimulate(RPC::JsonContext& context) +doSimulate(rpc::JsonContext& context) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; json::Value txJson; // the tx as a JSON // check validity of `binary` param if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) { - return RPC::invalidFieldError(jss::binary); + return rpc::invalidFieldError(jss::binary); } for (auto const field : {jss::secret, jss::seed, jss::seed_hex, jss::passphrase}) { if (context.params.isMember(field)) { - return RPC::invalidFieldError(field); + return rpc::invalidFieldError(field); } } @@ -365,13 +365,13 @@ doSimulate(RPC::JsonContext& context) if (stTx->getTxnType() == ttBATCH) { - return RPC::makeError(RpcNotImpl); + return rpc::makeError(RpcNotImpl); } // Reject transactions with the tfInnerBatchTxn flag. if (stTx->isFlag(tfInnerBatchTxn)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "tfInnerBatchTxn flag is not allowed on top-level transactions."); } diff --git a/src/xrpld/rpc/handlers/transaction/Submit.cpp b/src/xrpld/rpc/handlers/transaction/Submit.cpp index 79f3680684..05a1552221 100644 --- a/src/xrpld/rpc/handlers/transaction/Submit.cpp +++ b/src/xrpld/rpc/handlers/transaction/Submit.cpp @@ -27,11 +27,11 @@ namespace xrpl { static std::expected -getFailHard(RPC::JsonContext const& context) +getFailHard(rpc::JsonContext const& context) { if (context.params.isMember(jss::fail_hard) && !context.params[jss::fail_hard].isBool()) { - return std::unexpected(RPC::expectedFieldError(jss::fail_hard, "boolean")); + return std::unexpected(rpc::expectedFieldError(jss::fail_hard, "boolean")); } return NetworkOPs::doFailHard( context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()); @@ -42,9 +42,9 @@ getFailHard(RPC::JsonContext const& context) // secret: // } json::Value -doSubmit(RPC::JsonContext& context) +doSubmit(rpc::JsonContext& context) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; if (!context.params.isMember(jss::tx_blob)) { @@ -53,16 +53,16 @@ doSubmit(RPC::JsonContext& context) return failType.error(); if (context.role != Role::ADMIN && !context.app.config().canSign()) - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); - auto ret = RPC::transactionSubmit( + auto ret = rpc::transactionSubmit( context.params, context.apiVersion, *failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - RPC::getProcessTxnFn(context.netOps)); + rpc::getProcessTxnFn(context.netOps)); ret[jss::deprecated] = "Signing support in the 'submit' command has been " diff --git a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp index cc04ed073e..09301ca8a6 100644 --- a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp +++ b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp @@ -13,20 +13,20 @@ namespace xrpl { // tx_json: , // } json::Value -doSubmitMultiSigned(RPC::JsonContext& context) +doSubmitMultiSigned(rpc::JsonContext& context) { - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; auto const failHard = context.params[jss::fail_hard].asBool(); auto const failType = NetworkOPs::doFailHard(failHard); - return RPC::transactionSubmitMultiSigned( + return rpc::transactionSubmitMultiSigned( context.params, context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - RPC::getProcessTxnFn(context.netOps)); + rpc::getProcessTxnFn(context.netOps)); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp index bb68226334..2bd97e852f 100644 --- a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp +++ b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp @@ -21,10 +21,10 @@ namespace xrpl { // XXX In this case, not specify either ledger does not mean ledger current. It // means any ledger. json::Value -doTransactionEntry(RPC::JsonContext& context) +doTransactionEntry(rpc::JsonContext& context) { std::shared_ptr lpLedger; - json::Value jvResult = RPC::lookupLedger(lpLedger, context); + json::Value jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -84,7 +84,7 @@ doTransactionEntry(RPC::JsonContext& context) jvResult[jss::tx_json] = sttx->getJson(JsonOptions::Values::None); } - RPC::insertDeliverMax(jvResult[jss::tx_json], sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(jvResult[jss::tx_json], sttx->getTxnType(), context.apiVersion); auto const jsonMeta = (context.apiVersion > 1 ? jss::meta : jss::metadata); if (stobj) diff --git a/src/xrpld/rpc/handlers/transaction/Tx.cpp b/src/xrpld/rpc/handlers/transaction/Tx.cpp index c065bc268e..ee7110bf6b 100644 --- a/src/xrpld/rpc/handlers/transaction/Tx.cpp +++ b/src/xrpld/rpc/handlers/transaction/Tx.cpp @@ -68,8 +68,8 @@ struct TxArgs std::optional> ledgerRange; }; -std::pair -doTxHelp(RPC::Context& context, TxArgs args) +std::pair +doTxHelp(rpc::Context& context, TxArgs args) { TxResult result; @@ -169,7 +169,7 @@ doTxHelp(RPC::Context& context, TxArgs args) uint32_t const netID = context.app.getNetworkIDService().getNetworkID(); if (txnIdx <= 0xFFFFU && netID < 0xFFFFU && lgrSeq < 0x0FFF'FFFFUL) - result.ctid = RPC::encodeCTID(lgrSeq, txnIdx, netID); + result.ctid = rpc::encodeCTID(lgrSeq, txnIdx, netID); } } @@ -178,12 +178,12 @@ doTxHelp(RPC::Context& context, TxArgs args) json::Value populateJsonResponse( - std::pair const& res, + std::pair const& res, TxArgs const& args, - RPC::JsonContext const& context) + rpc::JsonContext const& context) { json::Value response; - RPC::Status const& error = res.second; + rpc::Status const& error = res.second; TxResult const& result = res.first; // handle errors if (error.toErrorCode() != RpcSuccess) @@ -215,7 +215,7 @@ populateJsonResponse( else { response[jss::tx_json] = result.txn->getJson(kOptionsJson); - RPC::insertDeliverMax( + rpc::insertDeliverMax( response[jss::tx_json], sttx->getTxnType(), context.apiVersion); } @@ -236,7 +236,7 @@ populateJsonResponse( { response = result.txn->getJson(JsonOptions::Values::IncludeDate, args.binary); if (!args.binary) - RPC::insertDeliverMax(response, sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(response, sttx->getTxnType(), context.apiVersion); } // populate binary metadata @@ -254,8 +254,8 @@ populateJsonResponse( { response[jss::meta] = meta->getJson(JsonOptions::Values::None); insertDeliveredAmount(response[jss::meta], context, result.txn, *meta); - RPC::insertNFTSyntheticInJson(response, sttx, *meta); - RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); + rpc::insertNFTSyntheticInJson(response, sttx, *meta); + rpc::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); } } response[jss::validated] = result.validated; @@ -267,7 +267,7 @@ populateJsonResponse( } json::Value -doTxJson(RPC::JsonContext& context) +doTxJson(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); @@ -291,7 +291,7 @@ doTxJson(RPC::JsonContext& context) } else if (context.params.isMember(jss::ctid)) { - auto ctid = RPC::decodeCTID(context.params[jss::ctid].asString()); + auto ctid = rpc::decodeCTID(context.params[jss::ctid].asString()); if (!ctid) return rpcError(RpcInvalidParams); @@ -302,7 +302,7 @@ doTxJson(RPC::JsonContext& context) out << "Wrong network. You should submit this request to a node " "running on NetworkID: " << net_id; - return RPC::makeError(RpcWrongNetwork, out.str()); + return rpc::makeError(RpcWrongNetwork, out.str()); } args.ctid = {lgr_seq, txn_idx}; } @@ -327,7 +327,7 @@ doTxJson(RPC::JsonContext& context) } } - std::pair const res = doTxHelp(context, args); + std::pair const res = doTxHelp(context, args); return populateJsonResponse(res, args, context); } diff --git a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp index a45046773c..2d5ad8cbe5 100644 --- a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp +++ b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp @@ -16,12 +16,12 @@ namespace xrpl { // start: // } json::Value -doTxHistory(RPC::JsonContext& context) +doTxHistory(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; if (!context.params.isMember(jss::start)) return rpcError(RpcInvalidParams); @@ -40,7 +40,7 @@ doTxHistory(RPC::JsonContext& context) for (auto const& t : trans) { json::Value txJson = t->getJson(JsonOptions::Values::None); - RPC::insertDeliverMax(txJson, t->getSTransaction()->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(txJson, t->getSTransaction()->getTxnType(), context.apiVersion); txs.append(txJson); } diff --git a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp index edc4eff057..8956603012 100644 --- a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp +++ b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doTxReduceRelay(RPC::JsonContext& context) +doTxReduceRelay(rpc::JsonContext& context) { return context.app.getOverlay().txMetrics(); } diff --git a/src/xrpld/rpc/handlers/utility/Ping.cpp b/src/xrpld/rpc/handlers/utility/Ping.cpp index 0d34b9e0fc..68fb06456e 100644 --- a/src/xrpld/rpc/handlers/utility/Ping.cpp +++ b/src/xrpld/rpc/handlers/utility/Ping.cpp @@ -6,12 +6,12 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc json::Value -doPing(RPC::JsonContext& context) +doPing(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); switch (context.role) diff --git a/src/xrpld/rpc/handlers/utility/Random.cpp b/src/xrpld/rpc/handlers/utility/Random.cpp index 56df442cf1..d3428ee6e9 100644 --- a/src/xrpld/rpc/handlers/utility/Random.cpp +++ b/src/xrpld/rpc/handlers/utility/Random.cpp @@ -10,16 +10,16 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc // Result: // { // random: // } json::Value -doRandom(RPC::JsonContext& context) +doRandom(rpc::JsonContext& context) { // TODO(tom): the try/catch is almost certainly redundant, we catch at the // top level too. diff --git a/tests/conan/src/example.cpp b/tests/conan/src/example.cpp index acfb253a7d..4720af4384 100644 --- a/tests/conan/src/example.cpp +++ b/tests/conan/src/example.cpp @@ -5,6 +5,6 @@ int main(int argc, char const** argv) { - std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str()); + std::printf("%s\n", xrpl::build_info::getVersionString().c_str()); return 0; }