From 778da954b4c38274fce90fef6e876fa0cc207eb9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 23 Jan 2026 14:09:00 -0500 Subject: [PATCH 1/4] refactor: clean up uses of `std::source_location` (#6272) Since the minimum Clang version we support is 16, the checks for version < 15 are no longer necessary. This change therefore removes the macros checking if the clang version is < 15 and simplifies uses of `std::source_location`. --- src/test/jtx/TestHelpers.h | 11 ++--------- src/test/rpc/LedgerEntry_test.cpp | 16 +++++----------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 3096a902b1..8149c2d464 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -15,15 +15,8 @@ #include #include -#include - -#if (defined(__clang_major__) && __clang_major__ < 15) -#include -using source_location = std::experimental::source_location; -#else #include -using std::source_location; -#endif +#include namespace xrpl { namespace test { @@ -640,7 +633,7 @@ checkMetrics( std::size_t expectedPerLedger, std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee(), - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { int line = location.line(); char const* file = location.file_name(); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 1b7079341c..cc6d958a2d 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -14,13 +14,7 @@ #include #include -#if (defined(__clang_major__) && __clang_major__ < 15) -#include -using source_location = std::experimental::source_location; -#else #include -using std::source_location; -#endif namespace xrpl { namespace test { @@ -114,7 +108,7 @@ class LedgerEntry_test : public beast::unit_test::suite Json::Value const& jv, std::string const& err, std::string const& msg, - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { if (BEAST_EXPECT(jv.isMember(jss::status))) BEAST_EXPECTS( @@ -297,7 +291,7 @@ class LedgerEntry_test : public beast::unit_test::suite FieldType const typeID, std::string const& expectedError, bool required = true, - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { forAllApiVersions([&, this](unsigned apiVersion) { if (required) @@ -350,7 +344,7 @@ class LedgerEntry_test : public beast::unit_test::suite FieldType typeID, std::string const& expectedError, bool required = true, - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { forAllApiVersions([&, this](unsigned apiVersion) { if (required) @@ -407,7 +401,7 @@ class LedgerEntry_test : public beast::unit_test::suite runLedgerEntryTest( test::jtx::Env& env, Json::StaticString const& parentField, - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { testMalformedField( env, @@ -431,7 +425,7 @@ class LedgerEntry_test : public beast::unit_test::suite test::jtx::Env& env, Json::StaticString const& parentField, std::vector const& subfields, - source_location const location = source_location::current()) + std::source_location const location = std::source_location::current()) { testMalformedField( env, From 847e87563571ea88333f9440bba1df5cd9443110 Mon Sep 17 00:00:00 2001 From: Bart Date: Mon, 26 Jan 2026 18:54:43 +0000 Subject: [PATCH 2/4] refactor: Update Boost to 1.90 (#6280) Upcoming feature work requires functionality present in a newer Boost version. These newer versions also have improvements for sanitizers. --- BUILD.md | 4 ++-- cmake/deps/Boost.cmake | 1 - conan.lock | 14 +++++++++----- conanfile.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index b239b10be6..f90aa0c148 100644 --- a/BUILD.md +++ b/BUILD.md @@ -148,8 +148,8 @@ function extract_version { } # Define which recipes to export. -recipes=('ed25519' 'grpc' 'openssl' 'secp256k1' 'snappy' 'soci') -folders=('all' 'all' '3.x.x' 'all' 'all' 'all') +recipes=('ed25519' 'grpc' 'nudb' 'openssl' 'secp256k1' 'snappy' 'soci') +folders=('all' 'all' 'all' '3.x.x' 'all' 'all' 'all') # Selectively check out the recipes from our CCI fork. cd external diff --git a/cmake/deps/Boost.cmake b/cmake/deps/Boost.cmake index b73698efd8..49025ae342 100644 --- a/cmake/deps/Boost.cmake +++ b/cmake/deps/Boost.cmake @@ -30,7 +30,6 @@ target_link_libraries(xrpl_boost Boost::process Boost::program_options Boost::regex - Boost::system Boost::thread) if(Boost_COMPILER) target_link_libraries(xrpl_boost INTERFACE Boost::disable_autolinking) diff --git a/conan.lock b/conan.lock index f3fa1e5f6e..64def2e916 100644 --- a/conan.lock +++ b/conan.lock @@ -11,7 +11,7 @@ "re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103", "protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038", "openssl/3.5.4#1b986e61b38fdfda3b40bebc1b234393%1768312656.257", - "nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1765850143.957", + "nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32", "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914", "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03", @@ -23,7 +23,7 @@ "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1765850143.772", "c-ares/1.34.5#5581c2b62a608b40bb85d965ab3ec7c8%1765850144.336", "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1765850143.837", - "boost/1.88.0#8852c0b72ce8271fb8ff7c53456d4983%1765850172.862", + "boost/1.90.0#d5e8defe7355494953be18524a7f135b%1765955095.179", "abseil/20250127.0#99262a368bd01c0ccca8790dfced9719%1766517936.993" ], "build_requires": [ @@ -42,18 +42,22 @@ ], "python_requires": [], "overrides": { + "boost/1.90.0#d5e8defe7355494953be18524a7f135b": [ + null, + "boost/1.90.0" + ], "protobuf/5.27.0": [ "protobuf/6.32.1" ], "lz4/1.9.4": [ "lz4/1.10.0" ], - "boost/1.83.0": [ - "boost/1.88.0" - ], "sqlite3/3.44.2": [ "sqlite3/3.49.1" ], + "boost/1.83.0": [ + "boost/1.90.0" + ], "lz4/[>=1.9.4 <2]": [ "lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504" ] diff --git a/conanfile.py b/conanfile.py index 8501909ce3..84ef1e2aa7 100644 --- a/conanfile.py +++ b/conanfile.py @@ -131,7 +131,7 @@ class Xrpl(ConanFile): transitive_headers_opt = ( {"transitive_headers": True} if conan_version.split(".")[0] == "2" else {} ) - self.requires("boost/1.88.0", force=True, **transitive_headers_opt) + self.requires("boost/1.90.0", force=True, **transitive_headers_opt) self.requires("date/3.0.4", **transitive_headers_opt) self.requires("lz4/1.10.0", force=True) self.requires("protobuf/6.32.1", force=True) From a2f1973574d4a370ffbbcad8e8d6590425657924 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 26 Jan 2026 15:58:12 -0400 Subject: [PATCH 3/4] fix: Remove DEFAULT fields that change to the default in associateAsset (#6259) (#6273) - Add Vault creation tests for showing valid range for AssetsMaximum --- include/xrpl/protocol/STObject.h | 3 + src/libxrpl/protocol/STAmount.cpp | 2 + src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STTakesAsset.cpp | 18 ++ src/test/app/Vault_test.cpp | 258 ++++++++++++++++++++++++++ 5 files changed, 287 insertions(+) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c6ca133e89..d282e9bf01 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -397,6 +397,9 @@ public: void delField(int index); + SOEStyle + getStyle(SField const& field) const; + bool hasMatchingEntry(STBase const&); diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index ec60971e63..91ad029dda 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -100,6 +100,8 @@ areComparable(STAmount const& v1, STAmount const& v2) return false; } +static_assert(INITIAL_XRP.drops() == STAmount::cMaxNativeN); + STAmount::STAmount(SerialIter& sit, SField const& name) : STBase(name) { std::uint64_t value = sit.get64(); diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 6007753ed1..a45eb4b7c7 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -580,6 +580,12 @@ STObject::delField(int index) v_.erase(v_.begin() + index); } +SOEStyle +STObject::getStyle(SField const& field) const +{ + return mType ? mType->style(field) : soeINVALID; +} + unsigned char STObject::getFieldU8(SField const& field) const { diff --git a/src/libxrpl/protocol/STTakesAsset.cpp b/src/libxrpl/protocol/STTakesAsset.cpp index d43e7b04a1..9167c6ace0 100644 --- a/src/libxrpl/protocol/STTakesAsset.cpp +++ b/src/libxrpl/protocol/STTakesAsset.cpp @@ -18,10 +18,28 @@ associateAsset(SLE& sle, Asset const& asset) // If the field is not set or present, skip it. if (type == STI_NOTPRESENT) continue; + // If the type doesn't downcast, then the flag shouldn't be on the // SField auto& ta = entry.downcast(); + auto const style = sle.getStyle(ta.getFName()); + XRPL_ASSERT_PARTS( + style != soeINVALID, + "xrpl::associateAsset", + "valid template element style"); + + XRPL_ASSERT_PARTS( + style != soeDEFAULT || !ta.isDefault(), + "xrpl::associateAsset", + "non-default value"); ta.associateAsset(asset); + + // associateAsset in derived classes may change the underlying + // value, but it won't know anything about how the value relates to + // the SLE. If the template element is soeDEFAULT, and the value + // changed to the default value, remove the field. + if (style == soeDEFAULT && ta.isDefault()) + sle.makeFieldAbsent(field); } } } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 41a4fc2b3b..d8bfa71d46 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5782,6 +5782,263 @@ class Vault_test : public beast::unit_test::suite testCase(MPT, "MPT", owner, depositor, issuer); } + void + testAssetsMaximum() + { + testcase("Assets Maximum"); + + using namespace test::jtx; + + Env env{*this, testable_amendments() | featureSingleAssetVault}; + Account const owner{"owner"}; + Account const issuer{"issuer"}; + + Vault vault{env}; + env.fund(XRP(1'000'000), issuer, owner); + env.close(); + + auto const maxInt64 = + std::to_string(std::numeric_limits::max()); + BEAST_EXPECT(maxInt64 == "9223372036854775807"); + + // Naming things is hard + auto const maxInt64Plus1 = std::to_string( + static_cast( + std::numeric_limits::max()) + + 1); + BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808"); + + auto const initialXRP = to_string(INITIAL_XRP); + BEAST_EXPECT(initialXRP == "100000000000000000"); + + auto const initialXRPPlus1 = to_string(INITIAL_XRP + 1); + BEAST_EXPECT(initialXRPPlus1 == "100000000000000001"); + + { + testcase("Assets Maximum: XRP"); + + PrettyAsset const xrpAsset = xrpIssue(); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = xrpAsset}); + tx[sfData] = "4D65746144617461"; + + tx[sfAssetsMaximum] = maxInt64; + env(tx, ter(tefEXCEPTION), THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRPPlus1; + env(tx, ter(tefEXCEPTION), THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRP; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = maxInt64Plus1; + env(tx, ter(tefEXCEPTION), THISLINE); + env.close(); + + // This value will be rounded + auto const insertAt = maxInt64Plus1.size() - 3; + auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + + maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000 + BEAST_EXPECT(decimalTest == "9223372036854775.808"); + tx[sfAssetsMaximum] = decimalTest; + auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 9223372036854776); + } + + { + testcase("Assets Maximum: MPT"); + + PrettyAsset const mptAsset = [&]() { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = + tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + env.close(); + PrettyAsset const mptAsset = mptt["MPT"]; + mptt.authorize({.account = owner}); + env.close(); + return mptAsset; + }(); + + env(pay(issuer, owner, mptAsset(100'000)), THISLINE); + env.close(); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = mptAsset}); + tx[sfData] = "4D65746144617461"; + + tx[sfAssetsMaximum] = maxInt64; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRPPlus1; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRP; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = maxInt64Plus1; + env(tx, ter(tefEXCEPTION), THISLINE); + env.close(); + + // This value will be rounded + auto const insertAt = maxInt64Plus1.size() - 1; + auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + + maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.8"); + tx[sfAssetsMaximum] = decimalTest; + auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 922337203685477581); + } + + { + testcase("Assets Maximum: IOU"); + + // Almost anything goes with IOUs + PrettyAsset iouAsset = issuer["IOU"]; + env.trust(iouAsset(1000), owner); + env(pay(issuer, owner, iouAsset(200))); + env.close(); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = iouAsset}); + tx[sfData] = "4D65746144617461"; + + tx[sfAssetsMaximum] = maxInt64; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRPPlus1; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = initialXRP; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = maxInt64Plus1; + env(tx, THISLINE); + env.close(); + + tx[sfAssetsMaximum] = "1000000000000000e80"; + env.close(); + + tx[sfAssetsMaximum] = "1000000000000000e-96"; + env.close(); + + // These values will be rounded to 15 significant digits + { + auto const insertAt = maxInt64Plus1.size() - 1; + auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + + "." + maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.8"); + tx[sfAssetsMaximum] = decimalTest; + auto const newKeylet = + keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT( + (vaultSle->at(sfAssetsMaximum) == + Number{9223372036854776, 2, Number::normalized{}})); + } + { + tx[sfAssetsMaximum] = + "9223372036854775807e40"; // max int64 * 10^40 + auto const newKeylet = + keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT( + (vaultSle->at(sfAssetsMaximum) == + Number{9223372036854776, 43, Number::normalized{}})); + } + { + tx[sfAssetsMaximum] = + "9223372036854775807e-40"; // max int64 * 10^-40 + auto const newKeylet = + keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT( + (vaultSle->at(sfAssetsMaximum) == + Number{9223372036854776, -37, Number::normalized{}})); + } + { + tx[sfAssetsMaximum] = + "9223372036854775807e-100"; // max int64 * 10^-100 + auto const newKeylet = + keylet::vault(owner.id(), env.seq(owner)); + env(tx, THISLINE); + env.close(); + + // Field 'AssetsMaximum' may not be explicitly set to default. + auto const vaultSle = env.le(newKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == numZero); + } + + // What _can't_ IOUs do? + // 1. Exceed maximum exponent / offset + tx[sfAssetsMaximum] = "1000000000000000e81"; + env(tx, ter(tefEXCEPTION), THISLINE); + env.close(); + + // 2. Mantissa larger than uint64 max + try + { + tx[sfAssetsMaximum] = + "18446744073709551617e5"; // uint64 max + 1 + env(tx, THISLINE); + BEAST_EXPECT(false); + } + catch (parse_error const& e) + { + using namespace std::string_literals; + BEAST_EXPECT( + e.what() == + "invalidParamsField 'tx_json.AssetsMaximum' has invalid " + "data."s); + } + } + } + public: void run() override @@ -5802,6 +6059,7 @@ public: testDelegate(); testVaultClawbackBurnShares(); testVaultClawbackAssets(); + testAssetsMaximum(); } }; From bb529d03175fc988a9d7b593bf60e21b6803cbb7 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Mon, 26 Jan 2026 21:39:28 +0000 Subject: [PATCH 4/4] fix: Stop embedded tests from hanging on ARM by using `atomic_flag` (#6248) This change replaces the mutex `stoppingMutex_`, the `atomic_bool` variable `isTimeToStop`, and the conditional variable `stoppingCondition_` with an `atomic_flag` variable. When `xrpld` is running the embedded tests as a child process, it has a control thread (the app bundle thread) that starts the application, and an application thread (the thread that executes `app_->run()`). Due to the relaxed memory ordering on ARM, it's not guaranteed that the application thread can see the change of the value resulting from the `isTimeToStop.exchange(true)` call before it is notified by `stoppingCondition_.notify_all()`, even though they do happen in the right order in the app bundle thread in `ApplicationImp::signalStop`. We therefore often get into the situation where `isTimeToStop` is `true`, but the application thread is waiting for `stoppingCondition_` to notify, because the app bundle thread may have already notified before the application thread actually starts waiting. Switching to a single `atomic_flag` variable makes sure that there's only one synchronisation object and then the memory order guarantee provided by c++ can make sure that `notify_all` gets synchronised after `test_and_set` does. Fixing this issue will stop the unit tests hanging forever and then we should see less (or hopefully no) time out errors in daily github action runs --- src/xrpld/app/main/Application.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 15abff9b14..785b02dbb8 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -203,11 +203,7 @@ public: boost::asio::signal_set m_signals; - // Once we get C++20, we could use `std::atomic_flag` for `isTimeToStop` - // and eliminate the need for the condition variable and the mutex. - std::condition_variable stoppingCondition_; - mutable std::mutex stoppingMutex_; - std::atomic isTimeToStop = false; + std::atomic_flag isTimeToStop; std::atomic checkSigs_; @@ -1539,10 +1535,7 @@ ApplicationImp::run() getLoadManager().activateStallDetector(); } - { - std::unique_lock lk{stoppingMutex_}; - stoppingCondition_.wait(lk, [this] { return isTimeToStop.load(); }); - } + isTimeToStop.wait(false, std::memory_order_relaxed); JLOG(m_journal.debug()) << "Application stopping"; @@ -1629,14 +1622,14 @@ ApplicationImp::run() void ApplicationImp::signalStop(std::string msg) { - if (!isTimeToStop.exchange(true)) + if (!isTimeToStop.test_and_set(std::memory_order_acquire)) { if (msg.empty()) JLOG(m_journal.warn()) << "Server stopping"; else JLOG(m_journal.warn()) << "Server stopping: " << msg; - stoppingCondition_.notify_all(); + isTimeToStop.notify_all(); } } @@ -1655,7 +1648,7 @@ ApplicationImp::checkSigs(bool check) bool ApplicationImp::isStopping() const { - return isTimeToStop.load(); + return isTimeToStop.test(std::memory_order_relaxed); } int