From 73ff54143d0a9b378cf3612b9ba9ac3751928551 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:26:26 +0100 Subject: [PATCH 1/7] docs: Add warning about using std::counting_semaphore (#5595) This adds a comment to avoid using `std::counting_semaphore` until the minimum compiler versions of GCC and Clang have been updated to no longer contain the bug that is present in older compilers. --- src/xrpld/core/detail/semaphore.h | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/xrpld/core/detail/semaphore.h b/src/xrpld/core/detail/semaphore.h index 3b64265bb1..fbeb79c66a 100644 --- a/src/xrpld/core/detail/semaphore.h +++ b/src/xrpld/core/detail/semaphore.h @@ -17,6 +17,34 @@ */ //============================================================================== +/** + * + * TODO: Remove ripple::basic_semaphore (and this file) and use + * std::counting_semaphore. + * + * Background: + * - PR: https://github.com/XRPLF/rippled/pull/5512/files + * - std::counting_semaphore had a bug fixed in both GCC and Clang: + * * GCC PR 104928: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928 + * * LLVM PR 79265: https://github.com/llvm/llvm-project/pull/79265 + * + * GCC: + * According to GCC Bugzilla PR104928 + * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104928#c15), the fix is + * scheduled for inclusion in GCC 16.0 (see comment #15, Target + * Milestone: 16.0). It is not included in GCC 14.x or earlier, and there is no + * indication that it will be backported to GCC 13.x or 14.x branches. + * + * Clang: + * The fix for is included in Clang 19.1.0+ + * + * Once the minimum compiler version is updated to > GCC 16.0 or Clang 19.1.0, + * we can remove this file. + * + * WARNING: Avoid using std::counting_semaphore until the minimum compiler + * version is updated. + */ + #ifndef RIPPLE_CORE_SEMAPHORE_H_INCLUDED #define RIPPLE_CORE_SEMAPHORE_H_INCLUDED From 51c5f2bfc9db56a18d8ede1b3d9f9fde75a9f62d Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 25 Sep 2025 15:14:29 +0100 Subject: [PATCH 2/7] Improve ValidatorList invalid UNL manifest logging (#5804) This change raises logging severity from `INFO` to `WARN` when handling UNL manifest signed with an unexpected / invalid key. It also changes the internal error code for an invalid format of UNL manifest to `invalid` (from `untrusted`). This is a follow up to problems experienced by an UNL node due to old manifest key configured in `validators.txt`, which would be easier to diagnose with improved logging. It also replaces a log line with `UNREACHABLE` for an impossible situation when we match UNL manifest key against a configured key which has an invalid type (we cannot configure such a key because of checks when loading configured keys). --- src/test/app/ValidatorList_test.cpp | 18 ++++++++++ src/xrpld/app/misc/ValidatorList.h | 2 +- src/xrpld/app/misc/detail/ValidatorList.cpp | 40 +++++++++++++-------- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index a3b62bd4f7..2b004c3b52 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -768,6 +768,24 @@ private: expectUntrusted(lists.at(7)); expectTrusted(lists.at(2)); + // try empty or mangled manifest + checkResult( + trustedKeys->applyLists( + "", version, {{blob7, sig7, {}}, {blob6, sig6, {}}}, siteUri), + publisherPublic, + ListDisposition::invalid, + ListDisposition::invalid); + + checkResult( + trustedKeys->applyLists( + base64_encode("not a manifest"), + version, + {{blob7, sig7, {}}, {blob6, sig6, {}}}, + siteUri), + publisherPublic, + ListDisposition::invalid, + ListDisposition::invalid); + // do not use list from untrusted publisher auto const untrustedManifest = base64_encode(makeManifestString( randomMasterKey(), diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 1f5d728824..9a2018cbd4 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -877,7 +877,7 @@ private: verify( lock_guard const&, Json::Value& list, - std::string const& manifest, + Manifest manifest, std::string const& blob, std::string const& signature); diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 1ddb51c9dd..2b45cec3be 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1149,21 +1149,33 @@ ValidatorList::applyList( Json::Value list; auto const& manifest = localManifest ? *localManifest : globalManifest; - auto [result, pubKeyOpt] = verify(lock, list, manifest, blob, signature); + auto m = deserializeManifest(base64_decode(manifest)); + if (!m) + { + JLOG(j_.warn()) << "UNL manifest cannot be deserialized"; + return PublisherListStats{ListDisposition::invalid}; + } + + auto [result, pubKeyOpt] = + verify(lock, list, std::move(*m), blob, signature); if (!pubKeyOpt) { - JLOG(j_.info()) << "ValidatorList::applyList unable to retrieve the " - "master public key from the verify function\n"; + JLOG(j_.warn()) + << "UNL manifest is signed with an unrecognized master public key"; return PublisherListStats{result}; } if (!publicKeyType(*pubKeyOpt)) - { - JLOG(j_.info()) << "ValidatorList::applyList Invalid Public Key type" - " retrieved from the verify function\n "; + { // LCOV_EXCL_START + // This is an impossible situation because we will never load an + // invalid public key type (see checks in `ValidatorList::load`) however + // we can only arrive here if the key used by the manifest matched one of + // the loaded keys + UNREACHABLE( + "ripple::ValidatorList::applyList : invalid public key type"); return PublisherListStats{result}; - } + } // LCOV_EXCL_STOP PublicKey pubKey = *pubKeyOpt; if (result > ListDisposition::pending) @@ -1356,19 +1368,17 @@ std::pair> ValidatorList::verify( ValidatorList::lock_guard const& lock, Json::Value& list, - std::string const& manifest, + Manifest manifest, std::string const& blob, std::string const& signature) { - auto m = deserializeManifest(base64_decode(manifest)); - - if (!m || !publisherLists_.count(m->masterKey)) + if (!publisherLists_.count(manifest.masterKey)) return {ListDisposition::untrusted, {}}; - PublicKey masterPubKey = m->masterKey; - auto const revoked = m->revoked(); + PublicKey masterPubKey = manifest.masterKey; + auto const revoked = manifest.revoked(); - auto const result = publisherManifests_.applyManifest(std::move(*m)); + auto const result = publisherManifests_.applyManifest(std::move(manifest)); if (revoked && result == ManifestDisposition::accepted) { @@ -1796,7 +1806,7 @@ ValidatorList::getAvailable( if (!keyBlob || !publicKeyType(makeSlice(*keyBlob))) { - JLOG(j_.info()) << "Invalid requested validator list publisher key: " + JLOG(j_.warn()) << "Invalid requested validator list publisher key: " << pubKey; return {}; } From a12f5de68d0acd2641829fdc144404e1ad1ff9e3 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 25 Sep 2025 18:08:07 +0200 Subject: [PATCH 3/7] chore: Pin all CI Docker tags (#5813) To avoid surprises and ensure reproducibility, this change pins all CI Docker image tags to the latest version in the XRPLF/CI repo. --- .github/workflows/build-test.yml | 2 +- .github/workflows/notify-clio.yml | 2 +- .github/workflows/pre-commit.yml | 3 ++- .github/workflows/publish-docs.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 634ed42690..2197e88a42 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -63,7 +63,7 @@ jobs: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} max-parallel: 10 runs-on: ${{ matrix.architecture.runner }} - container: ${{ inputs.os == 'linux' && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} + container: ${{ inputs.os == 'linux' && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} steps: - name: Check strategy matrix run: | diff --git a/.github/workflows/notify-clio.yml b/.github/workflows/notify-clio.yml index 692904ff12..2d6fa63796 100644 --- a/.github/workflows/notify-clio.yml +++ b/.github/workflows/notify-clio.yml @@ -40,7 +40,7 @@ jobs: upload: if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} runs-on: ubuntu-latest - container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13 + container: ghcr.io/xrplf/ci/ubuntu-noble:gcc-13-sha-5dd7158 steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index ead137308d..9b85a3bd11 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,8 +7,9 @@ on: workflow_dispatch: jobs: + # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: uses: XRPLF/actions/.github/workflows/pre-commit.yml@af1b0f0d764cda2e5435f5ac97b240d4bd4d95d3 with: runs_on: ubuntu-latest - container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit" }' + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-d1496b8" }' diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 2fcdd581d1..efd89a5b22 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -27,7 +27,7 @@ env: jobs: publish: runs-on: ubuntu-latest - container: ghcr.io/xrplf/ci/tools-rippled-documentation + container: ghcr.io/xrplf/ci/tools-rippled-documentation:sha-d1496b8 permissions: contents: write steps: diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index c52b3c89d3..98db52a436 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -56,7 +56,7 @@ jobs: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} max-parallel: 10 runs-on: ${{ matrix.architecture.runner }} - container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} + container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} steps: - name: Cleanup workspace From 2c3024716b5de465a59c2e48572add6cc3321d22 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 26 Sep 2025 20:07:48 +0900 Subject: [PATCH 4/7] change `fixPriceOracleOrder` to `Supported::yes` (#5749) --- include/xrpl/protocol/detail/features.macro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 9dc40dc8e5..ce9583dace 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -36,7 +36,7 @@ XRPL_FIX (IncludeKeyletFields, Supported::no, VoteBehavior::DefaultNo XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo) -XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo) +XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) From cfd26f444cddb297fe6273f956b653997abf6de7 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Fri, 26 Sep 2025 12:40:43 +0100 Subject: [PATCH 5/7] fix: Address http header case sensitivity (#5767) This change makes the regex in `HttpClient.cpp` that matches the content-length http header case insensitive to improve compatibility, as http headers are case insensitive. --- .../scripts/levelization/results/ordering.txt | 1 + src/libxrpl/net/HTTPClient.cpp | 2 +- src/tests/libxrpl/CMakeLists.txt | 2 + src/tests/libxrpl/net/HTTPClient.cpp | 346 ++++++++++++++++++ src/tests/libxrpl/net/main.cpp | 21 ++ 5 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/tests/libxrpl/net/HTTPClient.cpp create mode 100644 src/tests/libxrpl/net/main.cpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 13de36e2a5..55df4c2672 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -138,6 +138,7 @@ test.toplevel > test.csf test.toplevel > xrpl.json test.unit_test > xrpl.basics tests.libxrpl > xrpl.basics +tests.libxrpl > xrpl.net xrpl.json > xrpl.basics xrpl.ledger > xrpl.basics xrpl.ledger > xrpl.protocol diff --git a/src/libxrpl/net/HTTPClient.cpp b/src/libxrpl/net/HTTPClient.cpp index 964be32dd8..74b8b61ca6 100644 --- a/src/libxrpl/net/HTTPClient.cpp +++ b/src/libxrpl/net/HTTPClient.cpp @@ -383,7 +383,7 @@ public: static boost::regex reStatus{ "\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK static boost::regex reSize{ - "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"}; + "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase}; static boost::regex reBody{"\\`.*\\r\\n\\r\\n(.*)\\'"}; boost::smatch smMatch; diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 68c6fa6cb3..f97283c955 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -12,3 +12,5 @@ xrpl_add_test(basics) target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test) xrpl_add_test(crypto) target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test) +xrpl_add_test(net) +target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test) diff --git a/src/tests/libxrpl/net/HTTPClient.cpp b/src/tests/libxrpl/net/HTTPClient.cpp new file mode 100644 index 0000000000..4d50c47220 --- /dev/null +++ b/src/tests/libxrpl/net/HTTPClient.cpp @@ -0,0 +1,346 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace ripple; + +namespace { + +// Simple HTTP server using Beast for testing +class TestHTTPServer +{ +private: + boost::asio::io_context ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::endpoint endpoint_; + std::atomic running_{true}; + unsigned short port_; + + // Custom headers to return + std::map custom_headers_; + std::string response_body_; + unsigned int status_code_{200}; + +public: + TestHTTPServer() : acceptor_(ioc_), port_(0) + { + // Bind to any available port + endpoint_ = {boost::asio::ip::tcp::v4(), 0}; + acceptor_.open(endpoint_.protocol()); + acceptor_.set_option(boost::asio::socket_base::reuse_address(true)); + acceptor_.bind(endpoint_); + acceptor_.listen(); + + // Get the actual port that was assigned + port_ = acceptor_.local_endpoint().port(); + + accept(); + } + + ~TestHTTPServer() + { + stop(); + } + + boost::asio::io_context& + ioc() + { + return ioc_; + } + + unsigned short + port() const + { + return port_; + } + + void + setHeader(std::string const& name, std::string const& value) + { + custom_headers_[name] = value; + } + + void + setResponseBody(std::string const& body) + { + response_body_ = body; + } + + void + setStatusCode(unsigned int code) + { + status_code_ = code; + } + +private: + void + stop() + { + running_ = false; + acceptor_.close(); + } + + void + accept() + { + if (!running_) + return; + + acceptor_.async_accept( + ioc_, + endpoint_, + [&](boost::system::error_code const& error, + boost::asio::ip::tcp::socket peer) { + if (!running_) + return; + + if (!error) + { + handleConnection(std::move(peer)); + } + }); + } + + void + handleConnection(boost::asio::ip::tcp::socket socket) + { + try + { + // Read the HTTP request + boost::beast::flat_buffer buffer; + boost::beast::http::request req; + boost::beast::http::read(socket, buffer, req); + + // Create response + boost::beast::http::response res; + res.version(req.version()); + res.result(status_code_); + res.set(boost::beast::http::field::server, "TestServer"); + + // Add custom headers + for (auto const& [name, value] : custom_headers_) + { + res.set(name, value); + } + + // Set body and prepare payload first + res.body() = response_body_; + res.prepare_payload(); + + // Override Content-Length with custom headers after prepare_payload + // This allows us to test case-insensitive header parsing + for (auto const& [name, value] : custom_headers_) + { + if (boost::iequals(name, "Content-Length")) + { + res.erase(boost::beast::http::field::content_length); + res.set(name, value); + } + } + + // Send response + boost::beast::http::write(socket, res); + + // Shutdown socket gracefully + boost::system::error_code ec; + socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); + } + catch (std::exception const&) + { + // Connection handling errors are expected + } + + if (running_) + accept(); + } +}; + +// Helper function to run HTTP client test +bool +runHTTPTest( + TestHTTPServer& server, + std::string const& path, + std::atomic& completed, + std::atomic& result_status, + std::string& result_data, + boost::system::error_code& result_error) +{ + // Create a null journal for testing + beast::Journal j{beast::Journal::getNullSink()}; + + // Initialize HTTPClient SSL context + HTTPClient::initializeSSLContext("", "", false, j); + + HTTPClient::get( + false, // no SSL + server.ioc(), + "127.0.0.1", + server.port(), + path, + 1024, // max response size + std::chrono::seconds(5), + [&](boost::system::error_code const& ec, + int status, + std::string const& data) -> bool { + result_error = ec; + result_status = status; + result_data = data; + completed = true; + return false; // don't retry + }, + j); + + // Run the IO context until completion + auto start = std::chrono::steady_clock::now(); + while (!completed && + std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) + { + if (server.ioc().run_one() == 0) + { + break; + } + } + + return completed; +} + +} // anonymous namespace + +TEST_CASE("HTTPClient case insensitive Content-Length") +{ + // Test different cases of Content-Length header + std::vector header_cases = { + "Content-Length", // Standard case + "content-length", // Lowercase - this tests the regex icase fix + "CONTENT-LENGTH", // Uppercase + "Content-length", // Mixed case + "content-Length" // Mixed case 2 + }; + + for (auto const& header_name : header_cases) + { + TestHTTPServer server; + std::string test_body = "Hello World!"; + server.setResponseBody(test_body); + server.setHeader(header_name, std::to_string(test_body.size())); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, + "/test", + completed, + result_status, + result_data, + result_error); + + // Verify results + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data == test_body); + } +} + +TEST_CASE("HTTPClient basic HTTP request") +{ + TestHTTPServer server; + std::string test_body = "Test response body"; + server.setResponseBody(test_body); + server.setHeader("Content-Type", "text/plain"); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, "/basic", completed, result_status, result_data, result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data == test_body); +} + +TEST_CASE("HTTPClient empty response") +{ + TestHTTPServer server; + server.setResponseBody(""); // Empty body + server.setHeader("Content-Length", "0"); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, "/empty", completed, result_status, result_data, result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == 200); + CHECK(result_data.empty()); +} + +TEST_CASE("HTTPClient different status codes") +{ + std::vector status_codes = {200, 404, 500}; + + for (auto status : status_codes) + { + TestHTTPServer server; + server.setStatusCode(status); + server.setResponseBody("Status " + std::to_string(status)); + + std::atomic completed{false}; + std::atomic result_status{0}; + std::string result_data; + boost::system::error_code result_error; + + bool test_completed = runHTTPTest( + server, + "/status", + completed, + result_status, + result_data, + result_error); + + CHECK(test_completed); + CHECK(!result_error); + CHECK(result_status == static_cast(status)); + } +} diff --git a/src/tests/libxrpl/net/main.cpp b/src/tests/libxrpl/net/main.cpp new file mode 100644 index 0000000000..be9fc14bbf --- /dev/null +++ b/src/tests/libxrpl/net/main.cpp @@ -0,0 +1,21 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include From d02c306f1e3f954cbeeedba40da85db125f4986b Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 26 Sep 2025 13:40:19 -0400 Subject: [PATCH 6/7] test: add more comprehensive tests for `FeeVote` (#5746) This change adds more comprehensive tests for the `FeeVote` module, which previously only checked the basics, and not the more comprehensive flows in that class. --- src/test/app/FeeVote_test.cpp | 699 ++++++++++++++++++++++++++++++++++ 1 file changed, 699 insertions(+) diff --git a/src/test/app/FeeVote_test.cpp b/src/test/app/FeeVote_test.cpp index ba3d379219..4fe0a62e3b 100644 --- a/src/test/app/FeeVote_test.cpp +++ b/src/test/app/FeeVote_test.cpp @@ -19,11 +19,203 @@ #include +#include +#include +#include + #include +#include +#include +#include +#include +#include +#include namespace ripple { namespace test { +struct FeeSettingsFields +{ + std::optional baseFee = std::nullopt; + std::optional reserveBase = std::nullopt; + std::optional reserveIncrement = std::nullopt; + std::optional referenceFeeUnits = std::nullopt; + std::optional baseFeeDrops = std::nullopt; + std::optional reserveBaseDrops = std::nullopt; + std::optional reserveIncrementDrops = std::nullopt; +}; + +STTx +createFeeTx( + Rules const& rules, + std::uint32_t seq, + FeeSettingsFields const& fields) +{ + auto fill = [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldU32(sfLedgerSequence, seq); + + if (rules.enabled(featureXRPFees)) + { + // New XRPFees format - all three fields are REQUIRED + obj.setFieldAmount( + sfBaseFeeDrops, + fields.baseFeeDrops ? *fields.baseFeeDrops : XRPAmount{0}); + obj.setFieldAmount( + sfReserveBaseDrops, + fields.reserveBaseDrops ? *fields.reserveBaseDrops + : XRPAmount{0}); + obj.setFieldAmount( + sfReserveIncrementDrops, + fields.reserveIncrementDrops ? *fields.reserveIncrementDrops + : XRPAmount{0}); + } + else + { + // Legacy format - all four fields are REQUIRED + obj.setFieldU64(sfBaseFee, fields.baseFee ? *fields.baseFee : 0); + obj.setFieldU32( + sfReserveBase, fields.reserveBase ? *fields.reserveBase : 0); + obj.setFieldU32( + sfReserveIncrement, + fields.reserveIncrement ? *fields.reserveIncrement : 0); + obj.setFieldU32( + sfReferenceFeeUnits, + fields.referenceFeeUnits ? *fields.referenceFeeUnits : 0); + } + }; + return STTx(ttFEE, fill); +} + +STTx +createInvalidFeeTx( + Rules const& rules, + std::uint32_t seq, + bool missingRequiredFields = true, + bool wrongFeatureFields = false, + std::uint32_t uniqueValue = 42) +{ + auto fill = [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldU32(sfLedgerSequence, seq); + + if (wrongFeatureFields) + { + if (rules.enabled(featureXRPFees)) + { + obj.setFieldU64(sfBaseFee, 10 + uniqueValue); + obj.setFieldU32(sfReserveBase, 200000); + obj.setFieldU32(sfReserveIncrement, 50000); + obj.setFieldU32(sfReferenceFeeUnits, 10); + } + else + { + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + } + } + else if (!missingRequiredFields) + { + // Create valid transaction (all required fields present) + if (rules.enabled(featureXRPFees)) + { + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10 + uniqueValue}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + } + else + { + obj.setFieldU64(sfBaseFee, 10 + uniqueValue); + obj.setFieldU32(sfReserveBase, 200000); + obj.setFieldU32(sfReserveIncrement, 50000); + obj.setFieldU32(sfReferenceFeeUnits, 10); + } + } + // If missingRequiredFields is true, we don't add the required fields + // (default behavior) + }; + return STTx(ttFEE, fill); +} + +bool +applyFeeAndTestResult(jtx::Env& env, OpenView& view, STTx const& tx) +{ + auto const res = + apply(env.app(), view, tx, ApplyFlags::tapNONE, env.journal); + return res.ter == tesSUCCESS; +} + +bool +verifyFeeObject( + std::shared_ptr const& ledger, + Rules const& rules, + FeeSettingsFields const& expected) +{ + auto const feeObject = ledger->read(keylet::fees()); + if (!feeObject) + return false; + + auto checkEquality = [&](auto const& field, auto const& expected) { + if (!feeObject->isFieldPresent(field)) + return false; + return feeObject->at(field) == expected; + }; + + if (rules.enabled(featureXRPFees)) + { + if (feeObject->isFieldPresent(sfBaseFee) || + feeObject->isFieldPresent(sfReserveBase) || + feeObject->isFieldPresent(sfReserveIncrement) || + feeObject->isFieldPresent(sfReferenceFeeUnits)) + return false; + + if (!checkEquality( + sfBaseFeeDrops, expected.baseFeeDrops.value_or(XRPAmount{0}))) + return false; + if (!checkEquality( + sfReserveBaseDrops, + expected.reserveBaseDrops.value_or(XRPAmount{0}))) + return false; + if (!checkEquality( + sfReserveIncrementDrops, + expected.reserveIncrementDrops.value_or(XRPAmount{0}))) + return false; + } + else + { + if (feeObject->isFieldPresent(sfBaseFeeDrops) || + feeObject->isFieldPresent(sfReserveBaseDrops) || + feeObject->isFieldPresent(sfReserveIncrementDrops)) + return false; + + // Read sfBaseFee as a hex string and compare to expected.baseFee + if (!checkEquality(sfBaseFee, expected.baseFee)) + return false; + if (!checkEquality(sfReserveBase, expected.reserveBase)) + return false; + if (!checkEquality(sfReserveIncrement, expected.reserveIncrement)) + return false; + if (!checkEquality(sfReferenceFeeUnits, expected.referenceFeeUnits)) + return false; + } + + return true; +} + +std::vector +getTxs(std::shared_ptr const& txSet) +{ + std::vector txs; + for (auto i = txSet->begin(); i != txSet->end(); ++i) + { + auto const data = i->slice(); + auto serialIter = SerialIter(data); + txs.push_back(STTx(serialIter)); + } + return txs; +}; + class FeeVote_test : public beast::unit_test::suite { void @@ -93,10 +285,517 @@ class FeeVote_test : public beast::unit_test::suite } } + void + testBasic() + { + testcase("Basic SetFee transaction"); + + // Test with XRPFees disabled (legacy format) + { + jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test successful fee transaction with legacy fields + + FeeSettingsFields fields{ + .baseFee = 10, + .reserveBase = 200000, + .reserveIncrement = 50000, + .referenceFeeUnits = 10}; + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + accum.apply(*ledger); + + // Verify fee object was created/updated correctly + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); + } + + // Test with XRPFees enabled (new format) + { + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + // Test successful fee transaction with new fields + auto feeTx = createFeeTx(ledger->rules(), ledger->seq(), fields); + + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + accum.apply(*ledger); + + // Verify fee object was created/updated correctly + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields)); + } + } + + void + testTransactionValidation() + { + testcase("Fee Transaction Validation"); + + { + jtx::Env env(*this, jtx::testable_amendments() - featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with missing required legacy fields + auto invalidTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), true, false, 1); + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + + // Test transaction with new format fields when XRPFees is disabled + auto disallowedTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), false, true, 2); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + } + + { + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with missing required new fields + auto invalidTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), true, false, 3); + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + + // Test transaction with legacy fields when XRPFees is enabled + auto disallowedTx = createInvalidFeeTx( + ledger->rules(), ledger->seq(), false, true, 4); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, disallowedTx)); + } + } + + void + testPseudoTransactionProperties() + { + testcase("Pseudo Transaction Properties"); + + jtx::Env env(*this, jtx::testable_amendments()); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // Create the next ledger to apply transaction to + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + auto feeTx = createFeeTx( + ledger->rules(), + ledger->seq(), + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}); + + // Verify pseudo-transaction properties + BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); + BEAST_EXPECT(feeTx.getFieldAmount(sfFee) == XRPAmount{0}); + BEAST_EXPECT(feeTx.getSigningPubKey().empty()); + BEAST_EXPECT(feeTx.getSignature().empty()); + BEAST_EXPECT(!feeTx.isFieldPresent(sfSigners)); + BEAST_EXPECT(feeTx.getFieldU32(sfSequence) == 0); + BEAST_EXPECT(!feeTx.isFieldPresent(sfPreviousTxnID)); + + // But can be applied to a closed ledger + { + OpenView closedAccum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, closedAccum, feeTx)); + } + } + + void + testMultipleFeeUpdates() + { + testcase("Multiple Fee Updates"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields1{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + accum.apply(*ledger); + } + + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); + + // Apply second fee transaction with different values + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields2{ + .baseFeeDrops = XRPAmount{20}, + .reserveBaseDrops = XRPAmount{300000}, + .reserveIncrementDrops = XRPAmount{75000}}; + auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + accum.apply(*ledger); + } + + // Verify second update overwrote the first + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); + } + + void + testWrongLedgerSequence() + { + testcase("Wrong Ledger Sequence"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test transaction with wrong ledger sequence + auto feeTx = createFeeTx( + ledger->rules(), + ledger->seq() + 5, // Wrong sequence (should be ledger->seq()) + {.baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}); + + OpenView accum(ledger.get()); + + // The transaction should still succeed as long as other fields are + // valid + // The ledger sequence field is only used for informational purposes + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx)); + } + + void + testPartialFieldUpdates() + { + testcase("Partial Field Updates"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + FeeSettingsFields fields1{ + .baseFeeDrops = XRPAmount{10}, + .reserveBaseDrops = XRPAmount{200000}, + .reserveIncrementDrops = XRPAmount{50000}}; + auto feeTx1 = createFeeTx(ledger->rules(), ledger->seq(), fields1); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx1)); + accum.apply(*ledger); + } + + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields1)); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Apply partial update (only some fields) + FeeSettingsFields fields2{ + .baseFeeDrops = XRPAmount{20}, + .reserveBaseDrops = XRPAmount{200000}}; + auto feeTx2 = createFeeTx(ledger->rules(), ledger->seq(), fields2); + + { + OpenView accum(ledger.get()); + BEAST_EXPECT(applyFeeAndTestResult(env, accum, feeTx2)); + accum.apply(*ledger); + } + + // Verify the partial update worked + BEAST_EXPECT(verifyFeeObject(ledger, ledger->rules(), fields2)); + } + + void + testSingleInvalidTransaction() + { + testcase("Single Invalid Transaction"); + + jtx::Env env(*this, jtx::testable_amendments() | featureXRPFees); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + + // Test invalid transaction with non-zero account - this should fail + // validation + auto invalidTx = STTx(ttFEE, [&](auto& obj) { + obj.setAccountID( + sfAccount, + AccountID(1)); // Should be zero (this makes it invalid) + obj.setFieldU32(sfLedgerSequence, ledger->seq()); + obj.setFieldAmount(sfBaseFeeDrops, XRPAmount{10}); + obj.setFieldAmount(sfReserveBaseDrops, XRPAmount{200000}); + obj.setFieldAmount(sfReserveIncrementDrops, XRPAmount{50000}); + }); + + OpenView accum(ledger.get()); + BEAST_EXPECT(!applyFeeAndTestResult(env, accum, invalidTx)); + } + + void + testDoValidation() + { + testcase("doValidation"); + + using namespace jtx; + + FeeSetup setup; + setup.reference_fee = 42; + setup.account_reserve = 1234567; + setup.owner_reserve = 7654321; + + // Test with XRPFees enabled + { + Env env(*this, testable_amendments() | featureXRPFees); + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [](STValidation& v) { + v.setFieldU32(sfLedgerSequence, 12345); + }); + + // Use the current ledger's fees as the "current" fees for + // doValidation + auto const& currentFees = ledger->fees(); + + feeVote->doValidation(currentFees, ledger->rules(), *val); + + BEAST_EXPECT(val->isFieldPresent(sfBaseFeeDrops)); + BEAST_EXPECT( + val->getFieldAmount(sfBaseFeeDrops) == + XRPAmount(setup.reference_fee)); + } + + // Test with XRPFees disabled (legacy format) + { + Env env(*this, testable_amendments() - featureXRPFees); + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [](STValidation& v) { + v.setFieldU32(sfLedgerSequence, 12345); + }); + + auto const& currentFees = ledger->fees(); + + feeVote->doValidation(currentFees, ledger->rules(), *val); + + // In legacy mode, should vote using legacy fields + BEAST_EXPECT(val->isFieldPresent(sfBaseFee)); + BEAST_EXPECT(val->getFieldU64(sfBaseFee) == setup.reference_fee); + } + } + + void + testDoVoting() + { + testcase("doVoting"); + + using namespace jtx; + + FeeSetup setup; + setup.reference_fee = 42; + setup.account_reserve = 1234567; + setup.owner_reserve = 7654321; + + Env env(*this, testable_amendments() | featureXRPFees); + + // establish what the current fees are + BEAST_EXPECT( + env.current()->fees().base == XRPAmount{UNIT_TEST_REFERENCE_FEE}); + BEAST_EXPECT(env.current()->fees().reserve == XRPAmount{200'000'000}); + BEAST_EXPECT(env.current()->fees().increment == XRPAmount{50'000'000}); + + auto feeVote = make_FeeVote(setup, env.app().journal("FeeVote")); + auto ledger = std::make_shared( + create_genesis, + env.app().config(), + std::vector{}, + env.app().getNodeFamily()); + + // doVoting requires a flag ledger (every 256th ledger) + // We need to create a ledger at sequence 256 to make it a flag ledger + for (int i = 0; i < 256 - 1; ++i) + { + ledger = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + } + BEAST_EXPECT(ledger->isFlagLedger()); + + // Create some mock validations with fee votes + std::vector> validations; + + for (int i = 0; i < 5; i++) + { + auto sec = randomSecretKey(); + auto pub = derivePublicKey(KeyType::secp256k1, sec); + + auto val = std::make_shared( + env.app().timeKeeper().now(), + pub, + sec, + calcNodeID(pub), + [&](STValidation& v) { + v.setFieldU32(sfLedgerSequence, ledger->seq()); + // Vote for different fees than current + v.setFieldAmount( + sfBaseFeeDrops, XRPAmount{setup.reference_fee}); + v.setFieldAmount( + sfReserveBaseDrops, XRPAmount{setup.account_reserve}); + v.setFieldAmount( + sfReserveIncrementDrops, + XRPAmount{setup.owner_reserve}); + }); + if (i % 2) + val->setTrusted(); + validations.push_back(val); + } + + auto txSet = std::make_shared( + SHAMapType::TRANSACTION, env.app().getNodeFamily()); + + // This should not throw since we have a flag ledger + feeVote->doVoting(ledger, validations, txSet); + + auto const txs = getTxs(txSet); + BEAST_EXPECT(txs.size() == 1); + auto const& feeTx = txs[0]; + + BEAST_EXPECT(feeTx.getTxnType() == ttFEE); + + BEAST_EXPECT(feeTx.getAccountID(sfAccount) == AccountID()); + BEAST_EXPECT(feeTx.getFieldU32(sfLedgerSequence) == ledger->seq() + 1); + + BEAST_EXPECT(feeTx.isFieldPresent(sfBaseFeeDrops)); + BEAST_EXPECT(feeTx.isFieldPresent(sfReserveBaseDrops)); + BEAST_EXPECT(feeTx.isFieldPresent(sfReserveIncrementDrops)); + + // The legacy fields should NOT be present + BEAST_EXPECT(!feeTx.isFieldPresent(sfBaseFee)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveBase)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReserveIncrement)); + BEAST_EXPECT(!feeTx.isFieldPresent(sfReferenceFeeUnits)); + + // Check the values + BEAST_EXPECT( + feeTx.getFieldAmount(sfBaseFeeDrops) == + XRPAmount{setup.reference_fee}); + BEAST_EXPECT( + feeTx.getFieldAmount(sfReserveBaseDrops) == + XRPAmount{setup.account_reserve}); + BEAST_EXPECT( + feeTx.getFieldAmount(sfReserveIncrementDrops) == + XRPAmount{setup.owner_reserve}); + } + void run() override { testSetup(); + testBasic(); + testTransactionValidation(); + testPseudoTransactionProperties(); + testMultipleFeeUpdates(); + testWrongLedgerSequence(); + testPartialFieldUpdates(); + testSingleInvalidTransaction(); + testDoValidation(); + testDoVoting(); } }; From 19c4226d3d8c5c9ed47930cfb96c731f8d4959f2 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 26 Sep 2025 19:33:42 +0100 Subject: [PATCH 7/7] ci: Call all reusable workflows reusable (#5818) --- .github/scripts/levelization/README.md | 6 +++--- .github/workflows/on-pr.yml | 12 ++++++------ .github/workflows/on-trigger.yml | 8 ++++---- .../{build-test.yml => reusable-build-test.yml} | 0 ...elization.yml => reusable-check-levelization.yml} | 0 ...ommits.yml => reusable-check-missing-commits.yml} | 0 .../{notify-clio.yml => reusable-notify-clio.yml} | 0 7 files changed, 13 insertions(+), 13 deletions(-) rename .github/workflows/{build-test.yml => reusable-build-test.yml} (100%) rename .github/workflows/{check-levelization.yml => reusable-check-levelization.yml} (100%) rename .github/workflows/{check-missing-commits.yml => reusable-check-missing-commits.yml} (100%) rename .github/workflows/{notify-clio.yml => reusable-notify-clio.yml} (100%) diff --git a/.github/scripts/levelization/README.md b/.github/scripts/levelization/README.md index 31c6d34b6b..f3ba1e2518 100644 --- a/.github/scripts/levelization/README.md +++ b/.github/scripts/levelization/README.md @@ -72,15 +72,15 @@ It generates many files of [results](results): desired as described above. In a perfect repo, this file will be empty. This file is committed to the repo, and is used by the [levelization - Github workflow](../../workflows/check-levelization.yml) to validate + Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed. - [`ordering.txt`](results/ordering.txt): A list showing relationships between modules where there are no loops as they actually exist, as opposed to how they are desired as described above. This file is committed to the repo, and is used by the [levelization - Github workflow](../../workflows/check-levelization.yml) to validate + Github workflow](../../workflows/reusable-check-levelization.yml) to validate that nothing changed. -- [`levelization.yml`](../../workflows/check-levelization.yml) +- [`levelization.yml`](../../workflows/reusable-check-levelization.yml) Github Actions workflow to test that levelization loops haven't changed. Unfortunately, if changes are detected, it can't tell if they are improvements or not, so if you have resolved any issues or diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 9befd31e71..a206bbf041 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -50,8 +50,8 @@ jobs: files: | # These paths are unique to `on-pr.yml`. .github/scripts/levelization/** - .github/workflows/check-levelization.yml - .github/workflows/notify-clio.yml + .github/workflows/reusable-check-levelization.yml + .github/workflows/reusable-notify-clio.yml .github/workflows/on-pr.yml # Keep the paths below in sync with those in `on-trigger.yml`. @@ -59,7 +59,7 @@ jobs: .github/actions/build-test/** .github/actions/setup-conan/** .github/scripts/strategy-matrix/** - .github/workflows/build-test.yml + .github/workflows/reusable-build-test.yml .github/workflows/reusable-strategy-matrix.yml .codecov.yml cmake/** @@ -93,12 +93,12 @@ jobs: check-levelization: needs: should-run if: ${{ needs.should-run.outputs.go == 'true' }} - uses: ./.github/workflows/check-levelization.yml + uses: ./.github/workflows/reusable-check-levelization.yml build-test: needs: should-run if: ${{ needs.should-run.outputs.go == 'true' }} - uses: ./.github/workflows/build-test.yml + uses: ./.github/workflows/reusable-build-test.yml strategy: matrix: os: [linux, macos, windows] @@ -112,7 +112,7 @@ jobs: - should-run - build-test if: ${{ needs.should-run.outputs.go == 'true' && contains(fromJSON('["release", "master"]'), github.ref_name) }} - uses: ./.github/workflows/notify-clio.yml + uses: ./.github/workflows/reusable-notify-clio.yml secrets: clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }} conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }} diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 06abbd3f17..7b5bda021f 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -14,7 +14,7 @@ on: - master paths: # These paths are unique to `on-trigger.yml`. - - ".github/workflows/check-missing-commits.yml" + - ".github/workflows/reusable-check-missing-commits.yml" - ".github/workflows/on-trigger.yml" - ".github/workflows/publish-docs.yml" @@ -23,7 +23,7 @@ on: - ".github/actions/build-test/**" - ".github/actions/setup-conan/**" - ".github/scripts/strategy-matrix/**" - - ".github/workflows/build-test.yml" + - ".github/workflows/reusable-build-test.yml" - ".github/workflows/reusable-strategy-matrix.yml" - ".codecov.yml" - "cmake/**" @@ -71,10 +71,10 @@ defaults: jobs: check-missing-commits: if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && contains(fromJSON('["develop", "release"]'), github.ref_name) }} - uses: ./.github/workflows/check-missing-commits.yml + uses: ./.github/workflows/reusable-check-missing-commits.yml build-test: - uses: ./.github/workflows/build-test.yml + uses: ./.github/workflows/reusable-build-test.yml strategy: matrix: os: [linux, macos, windows] diff --git a/.github/workflows/build-test.yml b/.github/workflows/reusable-build-test.yml similarity index 100% rename from .github/workflows/build-test.yml rename to .github/workflows/reusable-build-test.yml diff --git a/.github/workflows/check-levelization.yml b/.github/workflows/reusable-check-levelization.yml similarity index 100% rename from .github/workflows/check-levelization.yml rename to .github/workflows/reusable-check-levelization.yml diff --git a/.github/workflows/check-missing-commits.yml b/.github/workflows/reusable-check-missing-commits.yml similarity index 100% rename from .github/workflows/check-missing-commits.yml rename to .github/workflows/reusable-check-missing-commits.yml diff --git a/.github/workflows/notify-clio.yml b/.github/workflows/reusable-notify-clio.yml similarity index 100% rename from .github/workflows/notify-clio.yml rename to .github/workflows/reusable-notify-clio.yml