From 742aa0878bbccdce385db4c8b6b77f2da19fad4a Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 10 Jun 2026 05:16:53 -0400 Subject: [PATCH 01/10] test: Do not create data directory for memory databases (#7323) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 7259c233e4..33bbf1c613 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -387,8 +387,12 @@ void SHAMapStoreImp::dbPaths() { Section const section{app_.config().section(Sections::kNodeDatabase)}; - boost::filesystem::path dbPath = get(section, Keys::kPath); + // Skip creating the directory when an in-memory database is used. + if (boost::iequals(get(section, Keys::kType), "memory")) + return; + + boost::filesystem::path dbPath = get(section, Keys::kPath); if (boost::filesystem::exists(dbPath)) { if (!boost::filesystem::is_directory(dbPath)) From 8a4bf2dee60e4082b584c1702eabf93225f8da40 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:16:03 +0100 Subject: [PATCH 02/10] refactor: Retire fixUniversalNumber amendment (#5962) Signed-off-by: Pratik Mankawde Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- include/xrpl/protocol/AMMCore.h | 2 +- include/xrpl/protocol/IOUAmount.h | 33 ---- include/xrpl/protocol/Rules.h | 2 - include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/protocol/AMMCore.cpp | 2 +- src/libxrpl/protocol/IOUAmount.cpp | 104 +---------- src/libxrpl/protocol/Rules.cpp | 4 - src/libxrpl/protocol/STAmount.cpp | 188 +++----------------- src/libxrpl/tx/Transactor.cpp | 3 - src/libxrpl/tx/apply.cpp | 2 - src/libxrpl/tx/applySteps.cpp | 4 +- src/test/app/AMM_test.cpp | 7 +- src/test/app/NFToken_test.cpp | 14 +- src/test/app/OfferMPT_test.cpp | 1 - src/test/app/Offer_test.cpp | 60 +++---- src/test/basics/Number_test.cpp | 1 - src/xrpld/app/misc/detail/TxQ.cpp | 5 - 17 files changed, 55 insertions(+), 379 deletions(-) diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index ced84c4c87..a83c8bfa84 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -65,7 +65,7 @@ invalidAMMAssetPair( std::optional ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot); -/** Return true if required AMM amendments are enabled +/** Return true if required AMM amendment is enabled */ bool ammEnabled(Rules const&); diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index 9e0fbe38eb..b057f1c245 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -179,36 +178,4 @@ to_string(IOUAmount const& amount); IOUAmount mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp); -// Since many uses of the number class do not have access to a ledger, -// getSTNumberSwitchover needs to be globally accessible. - -bool -getSTNumberSwitchover(); - -void -setSTNumberSwitchover(bool v); - -/** RAII class to set and restore the Number switchover. - */ - -class NumberSO -{ - bool saved_; - -public: - ~NumberSO() - { - setSTNumberSwitchover(saved_); - } - - NumberSO(NumberSO const&) = delete; - NumberSO& - operator=(NumberSO const&) = delete; - - explicit NumberSO(bool v) : saved_(getSTNumberSwitchover()) - { - setSTNumberSwitchover(v); - } -}; - } // namespace xrpl diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 9c17ff2391..47b20756db 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -122,7 +122,6 @@ private: std::optional saved_; }; -class NumberSO; class NumberMantissaScaleGuard; bool @@ -131,7 +130,6 @@ useRulesGuards(Rules const& rules); void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c99c1e5ce8..2b2f24ba53 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -64,7 +64,6 @@ XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMM, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Clawback, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FIX (UniversalNumber, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XRPFees, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (RemoveNFTokenAutoTrustLine, Supported::Yes, VoteBehavior::DefaultYes) @@ -112,6 +111,7 @@ XRPL_RETIRE_FIX(RmSmallIncreasedQOffers) XRPL_RETIRE_FIX(STAmountCanonicalize) XRPL_RETIRE_FIX(TakerDryOfferRemoval) XRPL_RETIRE_FIX(TrustLinesToSelf) +XRPL_RETIRE_FIX(UniversalNumber) XRPL_RETIRE_FEATURE(Checks) XRPL_RETIRE_FEATURE(CheckCashMakesTrustLine) diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index eccb581c6d..e58ab29257 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -127,7 +127,7 @@ ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot) bool ammEnabled(Rules const& rules) { - return rules.enabled(featureAMM) && rules.enabled(fixUniversalNumber); + return rules.enabled(featureAMM); } } // namespace xrpl diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index d214995809..acbf6724e1 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -1,6 +1,5 @@ #include -#include #include #include #include @@ -17,29 +16,6 @@ namespace xrpl { -namespace { - -// Use a static inside a function to help prevent order-of-initialization issues -LocalValue& -getStaticSTNumberSwitchover() -{ - static LocalValue kR{true}; - return kR; -} -} // namespace - -bool -getSTNumberSwitchover() -{ - return *getStaticSTNumberSwitchover(); -} - -void -setSTNumberSwitchover(bool v) -{ - *getStaticSTNumberSwitchover() = v; -} - /* The range for the mantissa when normalized */ // log(2^63,10) ~ 18.96 // @@ -75,56 +51,20 @@ IOUAmount::normalize() return; } - if (getSTNumberSwitchover()) - { - Number const v{mantissa_, exponent_}; - *this = fromNumber(v); - if (exponent_ > kMaxExponent) - Throw("value overflow"); - if (exponent_ < kMinExponent) - *this = beast::kZero; - return; - } - - bool const negative = (mantissa_ < 0); - - if (negative) - mantissa_ = -mantissa_; - - while ((mantissa_ < kMinMantissa) && (exponent_ > kMinExponent)) - { - mantissa_ *= 10; - --exponent_; - } - - while (mantissa_ > kMaxMantissa) - { - if (exponent_ >= kMaxExponent) - Throw("IOUAmount::normalize"); - - mantissa_ /= 10; - ++exponent_; - } - - if ((exponent_ < kMinExponent) || (mantissa_ < kMinMantissa)) - { - *this = beast::kZero; - return; - } - - if (exponent_ > kMaxExponent) - Throw("value overflow"); - - if (negative) - mantissa_ = -mantissa_; + Number const v{mantissa_, exponent_}; + *this = IOUAmount(v); } IOUAmount::IOUAmount(Number const& other) : IOUAmount(fromNumber(other)) { if (exponent_ > kMaxExponent) + { Throw("value overflow"); + } if (exponent_ < kMinExponent) + { *this = beast::kZero; + } } IOUAmount& @@ -139,37 +79,7 @@ IOUAmount::operator+=(IOUAmount const& other) return *this; } - if (getSTNumberSwitchover()) - { - *this = IOUAmount{Number{*this} + Number{other}}; - return *this; - } - auto m = other.mantissa_; - auto e = other.exponent_; - - while (exponent_ < e) - { - mantissa_ /= 10; - ++exponent_; - } - - while (e < exponent_) - { - m /= 10; - ++e; - } - - // This addition cannot overflow an std::int64_t but we may throw from - // normalize if the result isn't representable. - mantissa_ += m; - - if (mantissa_ >= -10 && mantissa_ <= 10) - { - *this = beast::kZero; - return *this; - } - - normalize(); + *this = IOUAmount{Number{*this} + Number{other}}; return *this; } diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 08a95145eb..e0968ea868 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -83,15 +82,12 @@ useRulesGuards(Rules const& rules) void createGuards( Rules const& rules, - std::optional& stNumberSO, std::optional& rulesGuard, std::optional& mantissaScaleGuard) { if (useRulesGuards(rules)) { // raii classes for the current ledger rules. - // fixUniversalNumber predates the rulesGuard and should be replaced. - stNumberSO.emplace(rules.enabled(fixUniversalNumber)); rulesGuard.emplace(rules); } else diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 1ba9cd042f..449efc9543 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -388,47 +388,9 @@ operator+(STAmount const& v1, STAmount const& v2) if (v1.holds()) return {v1.asset_, v1.mpt().value() + v2.mpt().value()}; - if (getSTNumberSwitchover()) - { - auto x = v1; - x = v1.iou() + v2.iou(); - return x; - } - - int ov1 = v1.exponent(), ov2 = v2.exponent(); - std::int64_t vv1 = static_cast(v1.mantissa()); - std::int64_t vv2 = static_cast(v2.mantissa()); - - if (v1.negative()) - vv1 = -vv1; - - if (v2.negative()) - vv2 = -vv2; - - while (ov1 < ov2) - { - vv1 /= 10; - ++ov1; - } - - while (ov2 < ov1) - { - vv2 /= 10; - ++ov2; - } - - // This addition cannot overflow an std::int64_t. It can overflow an - // STAmount and the constructor will throw. - - std::int64_t const fv = vv1 + vv2; - - if ((fv >= -10) && (fv <= 10)) - return {v1.getFName(), v1.asset()}; - - if (fv >= 0) - return STAmount{v1.getFName(), v1.asset(), static_cast(fv), ov1, false}; - - return STAmount{v1.getFName(), v1.asset(), static_cast(-fv), ov1, true}; + auto x = v1; + x = v1.iou() + v2.iou(); + return x; } STAmount @@ -877,53 +839,25 @@ STAmount::canonicalize() if (asset_.holds() && offset_ > 18) Throw("MPT amount out of range"); - if (getSTNumberSwitchover()) + Number const num(isNegative_, value_, offset_, Number::Unchecked{}); + auto set = [&](auto const& val) { + auto const value = val.value(); + isNegative_ = value < 0; + value_ = isNegative_ ? -value : value; + }; + if (native()) { - Number const num(isNegative_, value_, offset_, Number::Unchecked{}); - auto set = [&](auto const& val) { - auto const value = val.value(); - isNegative_ = value < 0; - value_ = isNegative_ ? -value : value; - }; - if (native()) - { - set(XRPAmount{num}); - } - else if (asset_.holds()) - { - set(MPTAmount{num}); - } - else - { - Throw("Unknown integral asset type"); - } - offset_ = 0; + set(XRPAmount{num}); + } + else if (asset_.holds()) + { + set(MPTAmount{num}); } else { - while (offset_ < 0) - { - value_ /= 10; - ++offset_; - } - - while (offset_ > 0) - { - // N.B. do not move the overflow check to after the - // multiplication - if (native() && value_ > kMaxNativeN) - { - Throw("Native currency amount out of range"); - } - else if (!native() && value_ > kMaxMpTokenAmount) - { - Throw("MPT amount out of range"); - } - - value_ *= 10; - --offset_; - } + Throw("Unknown integral asset type"); // LCOV_EXCL_LINE } + offset_ = 0; if (native() && value_ > kMaxNativeN) { @@ -937,53 +871,7 @@ STAmount::canonicalize() return; } - if (getSTNumberSwitchover()) - { - *this = iou(); - return; - } - - if (value_ == 0) - { - offset_ = -100; - isNegative_ = false; - return; - } - - while ((value_ < kMinValue) && (offset_ > kMinOffset)) - { - value_ *= 10; - --offset_; - } - - while (value_ > kMaxValue) - { - if (offset_ >= kMaxOffset) - Throw("value overflow"); - - value_ /= 10; - ++offset_; - } - - if ((offset_ < kMinOffset) || (value_ < kMinValue)) - { - value_ = 0; - isNegative_ = false; - offset_ = -100; - return; - } - - if (offset_ > kMaxOffset) - Throw("value overflow"); - - XRPL_ASSERT( - (value_ == 0) || ((value_ >= kMinValue) && (value_ <= kMaxValue)), - "xrpl::STAmount::canonicalize : value inside range"); - XRPL_ASSERT( - (value_ == 0) || ((offset_ >= kMinOffset) && (offset_ <= kMaxOffset)), - "xrpl::STAmount::canonicalize : offset inside range"); - XRPL_ASSERT( - (value_ != 0) || (offset_ != -100), "xrpl::STAmount::canonicalize : value or offset set"); + *this = iou(); } void @@ -1395,44 +1283,8 @@ multiply(STAmount const& v1, STAmount const& v2, Asset const& asset) return STAmount(asset, minV * maxV); } - if (getSTNumberSwitchover()) - { - auto const r = Number{v1} * Number{v2}; - return STAmount{asset, r}; - } - - std::uint64_t value1 = v1.mantissa(); - std::uint64_t value2 = v2.mantissa(); - int offset1 = v1.exponent(); - int offset2 = v2.exponent(); - - if (v1.integral()) - { - while (value1 < STAmount::kMinValue) - { - value1 *= 10; - --offset1; - } - } - - if (v2.integral()) - { - while (value2 < STAmount::kMinValue) - { - value2 *= 10; - --offset2; - } - } - - // We multiply the two mantissas (each is between 10^15 - // and 10^16), so their product is in the 10^30 to 10^32 - // range. Dividing their product by 10^14 maintains the - // precision, by scaling the result to 10^16 to 10^18. - return STAmount( - asset, - muldiv(value1, value2, kTenTO14) + 7, - offset1 + offset2 + 14, - v1.negative() != v2.negative()); + auto const r = Number{v1} * Number{v2}; + return STAmount{asset, r}; } // This is the legacy version of canonicalizeRound. It's been in use diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 51541cc2e3..68d3f36916 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -1199,8 +1198,6 @@ Transactor::operator()() // with_txn_type(). // // raii classes for the current ledger rules. - // fixUniversalNumber predate the rulesGuard and should be replaced. - NumberSO const stNumberSO{view().rules().enabled(fixUniversalNumber)}; CurrentTransactionRulesGuard const currentTransactionRulesGuard(view().rules()); #ifdef DEBUG diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index 0fc0275eb0..b70cb0d345 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -133,7 +132,6 @@ template ApplyResult apply(ServiceRegistry& registry, OpenView& view, PreflightChecks&& preflightChecks) { - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; return doApply(preclaim(preflightChecks(), registry, view), registry, view); } diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 217fdd717f..336bb2004b 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -70,10 +69,9 @@ withTxnType(Rules const& rules, TxType txnType, F&& f) // // See also Transactor::operator(). // - std::optional stNumberSO; std::optional rulesGuard; std::optional mantissaScaleGuard; - createGuards(rules, stNumberSO, rulesGuard, mantissaScaleGuard); + createGuards(rules, rulesGuard, mantissaScaleGuard); switch (txnType) { diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 64972c24ab..e3a1cc935f 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4333,15 +4333,10 @@ private: testAmendment() { testcase("Amendment"); - FeatureBitset const all{testableAmendments()}; - FeatureBitset const noAMM{all - featureAMM}; - FeatureBitset const noNumber{all - fixUniversalNumber}; - FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber}; using namespace jtx; + Env env{*this, testableAmendments() - featureAMM}; - for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { - Env env{*this, feature}; fund(env, gw_, {alice_}, {USD(1'000)}, Fund::All); AMM amm(env, alice_, XRP(1'000), USD(1'000), Ter(temDISABLED)); diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 269bc72c53..ba8f09c449 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -2236,17 +2236,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // See the impact of rounding when the nft is sold for small amounts // of drops. - for (auto numberSwitchOver : {true}) { - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } - // An nft with a transfer fee of 1 basis point. uint256 const nftID = token::getNextID(env, alice, 0u, tfTransferable, 1); env(token::mint(alice), Txflags(tfTransferable), token::XferFee(1)); @@ -2268,7 +2258,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells to carol. The payment is just small enough that // alice does not get any transfer fee. - auto pmt = numberSwitchOver ? drops(50000) : drops(99999); + auto pmt = drops(50000); STAmount carolBalance = env.balance(carol); uint256 const minterSellOfferIndex = keylet::nftoffer(minter, env.seq(minter)).key; env(token::createOffer(minter, nftID, pmt), Txflags(tfSellNFToken)); @@ -2285,7 +2275,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // transfer that enables a transfer fee of 1 basis point. STAmount beckyBalance = env.balance(becky); uint256 const beckyBuyOfferIndex = keylet::nftoffer(becky, env.seq(becky)).key; - pmt = numberSwitchOver ? drops(50001) : drops(100000); + pmt = drops(50001); env(token::createOffer(becky, nftID, pmt), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, beckyBuyOfferIndex)); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index e0f2f4eab0..ed0b2ffbe3 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -1827,7 +1827,6 @@ public: using namespace jtx; Env env{*this, features}; - env.enableFeature(fixUniversalNumber); auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 83c58884e0..7382f4f090 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -1973,54 +1973,36 @@ public: using namespace jtx; - for (auto numberSwitchOver : {false, true}) - { - Env env{*this, features}; - if (numberSwitchOver) - { - env.enableFeature(fixUniversalNumber); - } - else - { - env.disableFeature(fixUniversalNumber); - } + Env env{*this, features}; - auto const gw = Account{"gateway"}; - auto const alice = Account{"alice"}; - auto const bob = Account{"bob"}; - auto const usd = gw["USD"]; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const usd = gw["USD"]; - env.fund(XRP(10000), gw, alice, bob); - env.close(); + env.fund(XRP(10000), gw, alice, bob); + env.close(); - env(rate(gw, 1.005)); + env(rate(gw, 1.005)); - env(trust(alice, usd(1000))); - env(trust(bob, usd(1000))); - env(trust(gw, alice["USD"](50))); + env(trust(alice, usd(1000))); + env(trust(bob, usd(1000))); + env(trust(gw, alice["USD"](50))); - env(pay(gw, bob, bob["USD"](1))); - env(pay(alice, gw, usd(50))); + env(pay(gw, bob, bob["USD"](1))); + env(pay(alice, gw, usd(50))); - env(trust(gw, alice["USD"](0))); + env(trust(gw, alice["USD"](0))); - env(offer(alice, usd(50), XRP(150000))); - env(offer(bob, XRP(100), usd(0.1))); + env(offer(alice, usd(50), XRP(150000))); + env(offer(bob, XRP(100), usd(0.1))); - auto jrr = ledgerEntryState(env, alice, gw, "USD"); - BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); + auto jrr = ledgerEntryState(env, alice, gw, "USD"); + BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "49.96666666666667"); - jrr = ledgerEntryState(env, bob, gw, "USD"); - json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; - if (!numberSwitchOver) - { - BEAST_EXPECT(bobUSD == "-0.966500000033334"); - } - else - { - BEAST_EXPECT(bobUSD == "-0.9665000000333333"); - } - } + jrr = ledgerEntryState(env, bob, gw, "USD"); + json::Value const bobUSD = jrr[jss::node][sfBalance.fieldName][jss::value]; + BEAST_EXPECT(bobUSD == "-0.9665000000333333"); } void diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f4bd1c9d66..2d2745de14 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -1514,7 +1514,6 @@ public: void testToStAmount() { - NumberSO const stNumberSO{true}; Issue const issue; Number const n{7'518'783'80596, -5}; SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index f98580ede4..0326828a70 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -16,8 +16,6 @@ #include #include #include -#include -#include #include #include #include @@ -306,7 +304,6 @@ TxQ::MaybeTx::apply(Application& app, OpenView& view, beast::Journal j) { // If the rules or flags change, preflight again XRPL_ASSERT(pfResult, "xrpl::TxQ::MaybeTx::apply : preflight result is set"); - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above if (pfResult->rules != view.rules() || pfResult->flags != flags) @@ -731,8 +728,6 @@ TxQ::apply( ApplyFlags flags, beast::Journal j) { - NumberSO const stNumberSO{view.rules().enabled(fixUniversalNumber)}; - // See if the transaction is valid, properly formed, // etc. before doing potentially expensive queue // replace and multi-transaction operations. From 97ca7d57bcedab341e7887cd7950299d2bde9598 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:44:57 +0200 Subject: [PATCH 03/10] perf: Dispatch "hasInvalidAmount()" on type tag instead of dynamic_cast (#7402) --- src/libxrpl/protocol/STAmount.cpp | 32 ++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 449efc9543..748d00f25a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1138,16 +1138,34 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j) return true; } - if (auto const amount = dynamic_cast(&field)) - return !isLegalMPT(*amount) || !isLegalNet(*amount); + // Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path + // and a dynamic_cast chain over every field of every modified entry is measurably expensive. + // The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the + // downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers. + // safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to + // static_cast in release. + switch (field.getSType()) + { + case STI_AMOUNT: { + auto const& amount = safeDowncast(field); + return !isLegalMPT(amount) || !isLegalNet(amount); + } - if (auto const object = dynamic_cast(&field)) - return hasInvalidAmount(*object, depth + 1, j); + case STI_OBJECT: + case STI_LEDGERENTRY: + case STI_TRANSACTION: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - if (auto const array = dynamic_cast(&field)) - return hasInvalidAmount(*array, depth + 1, j); + case STI_ARRAY: + return hasInvalidAmount(safeDowncast(field), depth + 1, j); - return false; + default: { + XRPL_ASSERT( + dynamic_cast(&field) == nullptr, + "xrpl::hasInvalidAmount : unhandled STObject type"); + return false; + } + } } bool From 83cc5df72e9fa2f4d9d91a674908afcd6a50302a Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:05:53 +0200 Subject: [PATCH 04/10] fix: Disable transaction invariants (#7409) --- src/libxrpl/tx/Transactor.cpp | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 68d3f36916..aa7b81c015 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1171,21 +1171,17 @@ Transactor::checkTransactionInvariants(TER result, XRPAmount fee) [[nodiscard]] TER Transactor::checkInvariants(TER result, XRPAmount fee) { - // Transaction invariants first (more specific). These check post-conditions of the specific - // transaction. If these fail, the transaction's core logic is wrong. - auto const txResult = checkTransactionInvariants(result, fee); - - // Protocol invariants second (broader). These check properties that must hold regardless of - // transaction type. - auto const protoResult = ctx_.checkInvariants(result, fee); - - // Fail if either check failed. tef (fatal) takes priority over tec. - if (protoResult == tefINVARIANT_FAILED) - return tefINVARIANT_FAILED; - if (txResult == tecINVARIANT_FAILED || protoResult == tecINVARIANT_FAILED) - return tecINVARIANT_FAILED; - - return result; + /* + * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0 + * + * Transaction invariants are disabled due to a performance regression: + * the two-pass design (transaction-specific invariants + protocol invariants) + * iterates over modified ledger entries twice per transaction. + * + * Until resolved, only protocol invariants are checked (delegated to ctx_). + * This is safe because all transaction invariants in 3.2.0 are no-ops. + */ + return ctx_.checkInvariants(result, fee); } //------------------------------------------------------------------------------ ApplyResult From dd0b6754d4c33d906d54f317dcc9f4da800488ec Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 15:45:51 +0100 Subject: [PATCH 05/10] ci: Add `gh` and `file` to nix packages (#7444) --- cspell.config.yaml | 1 + flake.lock | 6 +++--- nix/docker/check-tools.sh | 2 ++ nix/packages.nix | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cspell.config.yaml b/cspell.config.yaml index cab2fc3da6..926ac06596 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -84,6 +84,7 @@ words: - coro - coros - cowid + - cpack - cryptocondition - cryptoconditional - cryptoconditions diff --git a/flake.lock b/flake.lock index 2013cfabd4..f8553af703 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1780243769, - "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { diff --git a/nix/docker/check-tools.sh b/nix/docker/check-tools.sh index 67bcdff8a9..276e5977ff 100755 --- a/nix/docker/check-tools.sh +++ b/nix/docker/check-tools.sh @@ -10,10 +10,12 @@ cmake --version conan --version curl --version doxygen --version +file --version g++ --version gcc --version gcov --version gcovr --version +gh --version git --version git-cliff --version gpg --version diff --git a/nix/packages.nix b/nix/packages.nix index d40472634b..fc4eff679e 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -13,7 +13,9 @@ in conan curlMinimal # needed for codecov/codecov-action doxygen + file # needed for cpack in Clio gcovr + gh git git-cliff gnumake From 1f359f719cbf475e6c010ad0fcf5b4044c6768d1 Mon Sep 17 00:00:00 2001 From: Shi Cheng <97218929+shichengripple001@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:24:44 +0800 Subject: [PATCH 06/10] fix: Add [[maybe_unused]] to fix320Enabled for assert=OFF builds (#7446) Co-authored-by: Claude Sonnet 4.6 --- src/libxrpl/ledger/helpers/LendingHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index 676b473132..f7ec8a8bc3 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -813,7 +813,7 @@ doOverpayment( // 3. The overpayment's penalty interest part (= untrackedInterest // for the overpayment path; see computeOverpaymentComponents): // trackedInterestPart() - bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); + [[maybe_unused]] bool const fix320Enabled = rules.enabled(fixCleanup3_2_0); XRPL_ASSERT_IF( fix320Enabled, overpaymentComponents.trackedPrincipalDelta == From 8000adfa797f3d5c81284cb3bcc8c6e9213549ec Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 19:08:34 +0100 Subject: [PATCH 07/10] ci: Make configurations launch on certain event types (#7447) --- .github/scripts/strategy-matrix/generate.py | 57 ++++++++++++++++--- .github/scripts/strategy-matrix/linux.json | 3 +- .github/scripts/strategy-matrix/macos.json | 3 +- .github/scripts/strategy-matrix/windows.json | 6 +- .../workflows/reusable-strategy-matrix.yml | 3 +- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index aaf84a51d0..6353567f27 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -27,6 +27,19 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: return " ".join(args) +def runs_on_event(exclude_event_types: list[str], event: str | None) -> bool: + """Whether a config should run for the current event. + + 'exclude_event_types' is a list of GitHub event names (e.g. + ["pull_request"]) on which the config should NOT run; an empty list means + the config runs on every event. When no event is given (event is None), no + filtering is applied. + """ + if event is None: + return True + return event not in exclude_event_types + + # --------------------------------------------------------------------------- # Input types — shapes of the JSON config files # --------------------------------------------------------------------------- @@ -43,6 +56,9 @@ class LinuxConfig: suffix: str = "" extra_cmake_args: str = "" image: str = "" # only used by package_configs entries + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass @@ -77,6 +93,9 @@ class PlatformConfig: build_type: list[str] build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) extra_cmake_args: str = "" + # List of GitHub event names (e.g. "pull_request") on which this config + # should NOT run. Empty means it runs on every event. + exclude_event_types: list[str] = dataclasses.field(default_factory=list) def __post_init__(self) -> None: if isinstance(self.build_type, str): @@ -151,16 +170,21 @@ _ARCHS: dict[str, Architecture] = { } -def expand_linux_matrix(linux: LinuxFile) -> list[MatrixEntry]: +def expand_linux_matrix( + linux: LinuxFile, event: str | None = None +) -> list[MatrixEntry]: """Expand a LinuxFile into a flat list of matrix entries. Each config entry is expanded over the cross-product of its - compiler, build_type, sanitizers, and architecture lists. + compiler, build_type, sanitizers, and architecture lists. Configs that + exclude the current event are skipped. """ entries: list[MatrixEntry] = [] for distro, configs in linux.configs.items(): for cfg in configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue # An empty sanitizers list means "one entry with no sanitizer". effective_sanitizers = cfg.sanitizers or [""] effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch} @@ -218,13 +242,20 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: return entries -def expand_platform_matrix(pf: PlatformFile) -> list[MatrixEntry]: - """Expand a PlatformFile (macOS or Windows) into matrix entries.""" +def expand_platform_matrix( + pf: PlatformFile, event: str | None = None +) -> list[MatrixEntry]: + """Expand a PlatformFile (macOS or Windows) into matrix entries. + + Configs that exclude the current event are skipped. + """ platform_name, arch = pf.platform.split("/") is_windows = platform_name == "windows" entries: list[MatrixEntry] = [] for cfg in pf.configs: + if not runs_on_event(cfg.exclude_event_types, event): + continue for build_type in cfg.build_type: entries.append( MatrixEntry( @@ -262,6 +293,14 @@ if __name__ == "__main__": help="Emit the Linux packaging matrix instead of the build/test matrix.", action="store_true", ) + parser.add_argument( + "-e", + "--event", + help="The GitHub event name that triggered the workflow (e.g. 'push', " + "'pull_request'). Configs are filtered by their 'event_type'. If " + "omitted, no filtering is applied.", + default=None, + ) args = parser.parse_args() matrix: list[MatrixEntry] | list[PackagingEntry] = [] @@ -270,12 +309,16 @@ if __name__ == "__main__": matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json")) else: if args.config in ("linux", None): - matrix += expand_linux_matrix(LinuxFile.load(THIS_DIR / "linux.json")) + matrix += expand_linux_matrix( + LinuxFile.load(THIS_DIR / "linux.json"), args.event + ) if args.config in ("macos", None): - matrix += expand_platform_matrix(PlatformFile.load(THIS_DIR / "macos.json")) + matrix += expand_platform_matrix( + PlatformFile.load(THIS_DIR / "macos.json"), args.event + ) if args.config in ("windows", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "windows.json") + PlatformFile.load(THIS_DIR / "windows.json"), args.event ) print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}") diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index edacdbde4c..e6c807ac95 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -41,7 +41,8 @@ "build_type": ["Debug"], "arch": ["amd64"], "suffix": "unity", - "extra_cmake_args": "-Dunity=ON" + "extra_cmake_args": "-Dunity=ON", + "exclude_event_types": ["pull_request"] } ], diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 5b9e32f88e..66d7a55a43 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -9,7 +9,8 @@ { "build_type": "Debug", "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", - "build_only": true + "build_only": true, + "exclude_event_types": ["pull_request"] } ] } diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index e4678b60db..e25f9ad131 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -3,6 +3,10 @@ "runner": ["self-hosted", "Windows", "devbox"], "configs": [ { "build_type": "Release" }, - { "build_type": "Debug", "build_only": true } + { + "build_type": "Debug", + "build_only": true, + "exclude_event_types": ["pull_request"] + } ] } diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index ea134b43b2..4518a8ffef 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,4 +35,5 @@ jobs: id: generate env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }} - run: ./generate.py ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}" + GENERATE_EVENT: ${{ github.event_name }} + run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}" From 2f6b466feb716198bf62dacd55b23332dd09e16c Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 10 Jun 2026 19:24:34 +0100 Subject: [PATCH 08/10] ci: Make sanitizer flags lists in the profile, not a string (#7449) --- conan/profiles/sanitizers | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers index 4a05fda734..083807ea9e 100644 --- a/conan/profiles/sanitizers +++ b/conan/profiles/sanitizers @@ -52,52 +52,50 @@ include(default) {% endif %} {# Frame pointer required for meaningful stack traces; -O1 for reasonable performance #} -{% set compile_flags = ["-fno-omit-frame-pointer", "-O1"] %} +{% set sanitizer_compiler_flags = ["-fno-omit-frame-pointer", "-O1"] %} {% if compiler == "gcc" %} {# Suppress false positive warnings with GCC #} - {% set _ = compile_flags.append("-Wno-stringop-overflow") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-stringop-overflow") %} {% set relocation_flags = [] %} {% if arch == "x86_64" and enable_asan %} {# Large code model prevents relocation errors in instrumented ASAN binaries #} - {% set _ = compile_flags.append("-mcmodel=large") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=large") %} {% set _ = relocation_flags.append("-mcmodel=large") %} {% elif enable_tsan %} {# GCC doesn't support atomic_thread_fence with TSAN; suppress warnings #} - {% set _ = compile_flags.append("-Wno-tsan") %} + {% set _ = sanitizer_compiler_flags.append("-Wno-tsan") %} {% if arch == "x86_64" %} {# Medium code model for TSAN; large is incompatible #} - {% set _ = compile_flags.append("-mcmodel=medium") %} + {% set _ = sanitizer_compiler_flags.append("-mcmodel=medium") %} {% set _ = relocation_flags.append("-mcmodel=medium") %} {% endif %} {% endif %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} {% set _ = relocation_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = " ".join(relocation_flags) %} + {% set sanitizer_linker_flags = relocation_flags %} {% elif compiler == "clang" or compiler == "apple-clang" %} {% set fsanitize = "-fsanitize=" ~ ",".join(sanitizer_types) %} - {% set _ = compile_flags.append(fsanitize) %} + {% set _ = sanitizer_compiler_flags.append(fsanitize) %} - {% set sanitizer_compiler_flags = " ".join(compile_flags) %} - {% set sanitizer_linker_flags = fsanitize %} + {% set sanitizer_linker_flags = [fsanitize] %} {% endif %} [conf] tools.build:defines+={{defines}} -tools.build:cxxflags+=['{{sanitizer_compiler_flags}}'] -tools.build:sharedlinkflags+=['{{sanitizer_linker_flags}}'] -tools.build:exelinkflags+=['{{sanitizer_linker_flags}}'] +tools.build:cxxflags+={{sanitizer_compiler_flags}} +tools.build:sharedlinkflags+={{sanitizer_linker_flags}} +tools.build:exelinkflags+={{sanitizer_linker_flags}} tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"] # &: means "apply only to the consumer/root package" -&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags}}"} +&:tools.cmake.cmaketoolchain:extra_variables={"SANITIZERS": "{{sanitizers}}", "SANITIZERS_COMPILER_FLAGS": "{{sanitizer_compiler_flags | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"} [options] {% if enable_asan %} From 09c36d066ec6e1ffdcc6b98075c991e202873d5f Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:42:41 -0400 Subject: [PATCH 09/10] fix: Correct hybrid offer deletion on credential expiry (#6843) Co-authored-by: Bart --- include/xrpl/protocol/detail/features.macro | 1 + src/libxrpl/tx/paths/OfferStream.cpp | 9 +- src/test/app/PermissionedDEX_test.cpp | 801 ++++++++++++-------- 3 files changed, 475 insertions(+), 336 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 2b2f24ba53..d3500ab144 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index b7defb4df8..ecc8416a2b 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -249,7 +250,13 @@ TOfferStreamBase::step() continue; } - if (entry->isFieldPresent(sfDomainID) && + // Pre-fixCleanup3_3_0: validate domain membership for any book. + // Post-fixCleanup3_3_0: only validate when walking a domain book. + // Hybrid offers carry sfDomainID but also participate in the open + // book; expiry of the owner's domain credential should not evict + // the offer from the open book. + if ((!view_.rules().enabled(fixCleanup3_3_0) || book_.domain.has_value()) && + entry->isFieldPresent(sfDomainID) && !permissioned_dex::offerInDomain( view_, entry->key(), entry->getFieldH256(sfDomainID), j_)) { diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index d534f20248..99e69ce482 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -185,15 +185,15 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); } @@ -214,7 +214,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone outside of the domain cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -223,7 +223,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION)); @@ -247,7 +247,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - someone with expired cred cannot create domain offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -256,7 +256,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -282,13 +282,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot create an offer in a non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(offer(bob_, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); + env(offer(bob, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION)); env.close(); } @@ -296,68 +296,68 @@ class PermissionedDEX_test : public beast::unit_test::Suite // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } // apply - offer can be created even if takerpays issuer is not in // domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, USD(10), XRP(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true)); } // apply - two domain offers cross with each other { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // a non domain offer cannot cross with domain offer - env(offer(carol_, USD(10), XRP(10))); + env(offer(carol, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - create lots of domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -365,19 +365,19 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 0; i <= 100; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); } } } @@ -390,10 +390,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - without enabling featurePermissionedDEX amendment { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -403,10 +403,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.enableFeature(featurePermissionedDEX); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - env(pay(bob_, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } @@ -431,13 +431,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - cannot send payment with non existent domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); uint256 const badDomain{ "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" "E5"}; - env(pay(bob_, alice_, USD(10)), + env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(badDomain), @@ -448,10 +448,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // preclaim - payment with non-domain destination fails { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -460,11 +460,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin is not part of domain - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -476,7 +476,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(alice_, devin, USD(10)), + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -487,17 +487,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now receive payment after he is in domain - env(pay(alice_, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // preclaim - non-domain sender cannot send payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create devin account who is not part of the domain @@ -506,11 +506,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); // devin tries to send domain payment - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -522,7 +522,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin has not yet accepted cred - env(pay(devin, alice_, USD(10)), + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -533,28 +533,28 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin can now send payment after he is in domain - env(pay(devin, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // apply - domain owner can always send and receive domain payment { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can always be destination - env(pay(alice_, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // domain owner can send - env(pay(domainOwner, alice_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(domainOwner, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); } } @@ -567,22 +567,22 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain cross currency payment consuming one offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create a regular offer without domain - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const regularDirKey = getDefaultOfferDirKey(env, bob_, regularOfferSeq); + auto const regularDirKey = getDefaultOfferDirKey(env, bob, regularOfferSeq); BEAST_EXPECT(regularDirKey); BEAST_EXPECT(checkDirectorySize( env, *regularDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // a domain payment cannot consume regular offers - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), @@ -590,23 +590,23 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // create a domain offer - auto const domainOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const domainOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, domainOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, domainOfferSeq, XRP(10), USD(10), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, domainOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, domainOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) // cross-currency permissioned payment consumed // domain offer instead of regular offer - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, domainOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); // domain directory is empty BEAST_EXPECT(checkDirectorySize( @@ -618,79 +618,79 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test domain payment consuming two offers in the path { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); // create XRP/USD domain offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a regular USD/EUR offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10))); + // bob creates a regular USD/EUR offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); - // alice_ tries to pay again, but still fails because the regular + // alice tries to pay again, but still fails because the regular // offer cannot be consumed - env(pay(alice_, carol_, eur(10)), + env(pay(alice, carol, eur(10)), Path(~USD, ~eur), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates a domain USD/EUR offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID)); + // bob creates a domain USD/EUR offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), 0, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), 0, true)); - // alice_ successfully consume two domain offers and deletes them + // alice successfully consume two domain offers and deletes them // we compute path this time using `paths` - env(pay(alice_, carol_, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); + env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, usdOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, eurOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq)); // regular offer is not consumed - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, USD(10), eur(10))); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10))); } // domain payment cannot consume offer from another domain { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund devin and create USD trustline @@ -700,7 +700,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto const badCredType = "badCred"; @@ -720,24 +720,24 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // domain payment can't consume an offer from another domain - env(pay(alice_, carol_, USD(10)), + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - // bob_ creates an offer under the right domain - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + // bob creates an offer under the right domain + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); // domain payment now consumes from the right domain - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); } // sanity check: devin, who is part of the domain but doesn't have a @@ -745,10 +745,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // fund devin but don't create a USD trustline with gateway @@ -764,14 +764,14 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // successful payment because offer is consumed - env(pay(devin, alice_, USD(10)), Sendmax(XRP(10)), Domain(domainID)); + env(pay(devin, alice, USD(10)), Sendmax(XRP(10)), Domain(domainID)); env.close(); } // offer becomes unfunded when offer owner's cred expires { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create devin account who is not part of the domain @@ -780,7 +780,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); env.trust(USD(1000), devin); env.close(); - env(pay(gw_, devin, USD(100))); + env(pay(gw, devin, USD(100))); env.close(); auto jv = credentials::create(devin, domainOwner, credType); @@ -797,7 +797,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // devin's offer can still be consumed while his cred isn't expired - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); BEAST_EXPECT(checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); @@ -805,7 +805,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(std::chrono::seconds(20)); // devin's offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -817,30 +817,30 @@ class PermissionedDEX_test : public beast::unit_test::Suite // offer becomes unfunded when offer owner's cred is removed { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - // bob_'s offer can still be consumed while his cred exists - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + // bob's offer can still be consumed while his cred exists + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); - // remove bob_'s cred - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob's cred + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s offer is unfunded now due to expired cred - env(pay(alice_, carol_, USD(5)), + // bob's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); } } @@ -853,34 +853,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite // payment. If the domain wishes to control who is allowed to ripple // through, they should set the rippling individually Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eura = alice_["EUR"]; - auto const eurb = bob_["EUR"]; + auto const eura = alice["EUR"]; + auto const eurb = bob["EUR"]; - env.trust(eura(100), bob_); - env.trust(eurb(100), carol_); + env.trust(eura(100), bob); + env.trust(eurb(100), carol); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // alice_ can still ripple through bob_ even though he's not part + // alice can still ripple through bob even though he's not part // of the domain, this is intentional - env(pay(alice_, carol_, eurb(10)), Paths(eura), Domain(domainID)); + env(pay(alice, carol, eurb(10)), Paths(eura), Domain(domainID)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); - // carol_ sets no ripple on bob_ - env(trust(carol_, bob_["EUR"](0), bob_, tfSetNoRipple)); + // carol sets no ripple on bob + env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple)); env.close(); - // payment no longer works because carol_ has no ripple on bob_ - env(pay(alice_, carol_, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); + // payment no longer works because carol has no ripple on bob + env(pay(alice, carol, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY)); env.close(); - env.require(Balance(bob_, eura(10)), Balance(carol_, eurb(10))); + env.require(Balance(bob, eura(10)), Balance(carol, eurb(10))); } void @@ -891,37 +891,37 @@ class PermissionedDEX_test : public beast::unit_test::Suite // whether the issuer is in the domain should NOT affect whether an // offer can be consumed in domain payment Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // create an xrp/usd offer with usd as takergets - auto const bobOffer1Seq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOffer1Seq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); // create an usd/xrp offer with usd as takerpays - auto const bobOffer2Seq{env.seq(bob_)}; - env(offer(bob_, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); + auto const bobOffer2Seq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer1Seq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); // remove gateway from domain - env(credentials::deleteCred(domainOwner, gw_, domainOwner, credType)); + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); env.close(); // payment succeeds even if issuer is not in domain // xrp/usd offer is consumed - env(pay(alice_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer1Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq)); // payment succeeds even if issuer is not in domain // usd/xrp offer is consumed - env(pay(alice_, carol_, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); + env(pay(alice, carol, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, bobOffer2Seq)); + BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq)); } void @@ -932,36 +932,36 @@ class PermissionedDEX_test : public beast::unit_test::Suite // checking that an unfunded offer will be implicitly removed by a // successful payment tx Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, XRP(100), USD(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, XRP(100), USD(100)), Domain(domainID)); env.close(); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(20), USD(20)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(20), USD(20)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(20), USD(20), 0, true)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, XRP(100), USD(100), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true)); - auto const domainDirKey = getDefaultOfferDirKey(env, bob_, bobOfferSeq); + auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq); BEAST_EXPECT(domainDirKey); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 2)); // NOLINT(bugprone-unchecked-optional-access) - // remove alice_ from domain and thus alice_'s offer becomes unfunded - env(credentials::deleteCred(domainOwner, alice_, domainOwner, credType)); + // remove alice from domain and thus alice's offer becomes unfunded + env(credentials::deleteCred(domainOwner, alice, domainOwner, credType)); env.close(); - env(pay(gw_, carol_, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); + env(pay(gw, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); - // alice_'s unfunded offer is removed implicitly - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); + // alice's unfunded offer is removed implicitly + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); BEAST_EXPECT(checkDirectorySize( env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access) } @@ -972,12 +972,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("AMM not used"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - AMM const amm(env, alice_, XRP(10), USD(50)); + AMM const amm(env, alice, XRP(10), USD(50)); // a domain payment isn't able to consume AMM - env(pay(bob_, carol_, USD(5)), + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), @@ -985,7 +985,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // a non domain payment can use AMM - env(pay(bob_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); // USD amount in AMM is changed @@ -1001,126 +1001,126 @@ class PermissionedDEX_test : public beast::unit_test::Suite // test preflight - invalid hybrid flag { Env env(*this, features - featurePermissionedDEX); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - env(offer(bob_, XRP(10), USD(10)), + env(offer(bob, XRP(10), USD(10)), Domain(domainID), Txflags(tfHybrid), Ter(temDISABLED)); env.close(); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); env.enableFeature(featurePermissionedDEX); env.close(); // hybrid offer must have domainID - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG)); env.close(); // hybrid offer must have domainID - auto const offerSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, offerSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true)); } // apply - domain offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - open offer can cross with hybrid { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10))); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10))); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - by default, hybrid offer tries to cross with offers in the // domain book { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, true)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(!offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(ownerCount(env, alice_) == 2); + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); } // apply - hybrid offer does not automatically cross with open offers // because by default, it only tries to cross domain offers { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(ownerCount(env, bob_) == 3); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(ownerCount(env, bob) == 3); // hybrid offer auto crosses with domain offer - auto const aliceOfferSeq{env.seq(alice_)}; - env(offer(alice_, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(offerExists(env, alice_, aliceOfferSeq)); - BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), 0, false)); - BEAST_EXPECT(checkOffer(env, alice_, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); - BEAST_EXPECT(ownerCount(env, alice_) == 3); + BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); + BEAST_EXPECT(ownerCount(env, alice) == 3); } } @@ -1129,58 +1129,97 @@ class PermissionedDEX_test : public beast::unit_test::Suite { testcase("Hybrid invalid offer"); - // bob_ has a hybrid offer and then he is removed from domain. - // in this case, the hybrid offer will be considered as unfunded even in - // a regular payment + // bob has a hybrid offer and then he is removed from the domain. + // Domain payments must not consume the offer; regular open-book + // payments follow the fixCleanup3_3_0 behavior checked below. Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID)); env.close(); - // remove bob_ from domain - env(credentials::deleteCred(domainOwner, bob_, domainOwner, credType)); + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); env.close(); - // bob_'s hybrid offer is unfunded and can not be consumed in a domain + // bob's hybrid offer is unfunded and can not be consumed in a domain // payment - env(pay(alice_, carol_, USD(5)), + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); - // bob_'s unfunded hybrid offer can't be consumed even with a regular - // payment - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); - env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + if (features[fixCleanup3_3_0]) + { + // Post-fixCleanup3_3_0: hybrid offer can still be consumed via a regular + // open-book payment even though the domain credential was revoked. + auto const carolBalBefore = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalBefore == USD(5)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(45), USD(45), lsfHybrid, true)); - // create a regular offer - auto const regularOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); - env.close(); - BEAST_EXPECT(offerExists(env, bob_, regularOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(10), USD(10))); + // create a regular offer alongside the hybrid one + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const sleHybridOffer = env.le(keylet::offer(bob_.id(), hybridOfferSeq)); - BEAST_EXPECT(sleHybridOffer); - auto const openDir = - sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); - BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + // both offers are in the open book directory + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); - // this normal payment should consume the regular offer and remove the - // unfunded hybrid offer - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); - env.close(); + // A regular payment crosses the hybrid offer first (FIFO, older + // offer), then stops; the regular offer is untouched. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); - BEAST_EXPECT(checkOffer(env, bob_, regularOfferSeq, XRP(5), USD(5))); - BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(40), USD(40), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + } + else + { + // Pre-fixCleanup3_3_0: the open-book traversal + // also runs the offerInDomain eviction check, so the hybrid offer + // is treated as unfunded and the regular payment fails. + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // create a regular offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + if (!BEAST_EXPECT(sleHybridOffer)) + return; + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + + // This payment crosses the regular offer and permanently evicts the + // hybrid offer from the open book (since the payment succeeds, the + // sandbox, including the hybrid eviction, is committed). + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + } } void @@ -1191,29 +1230,29 @@ class PermissionedDEX_test : public beast::unit_test::Suite // both non domain and domain payments can consume hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // someone from another domain can't cross hybrid if they specified // wrong domainID { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); // Fund accounts @@ -1235,8 +1274,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(credentials::accept(devin, badDomainOwner, badCredType)); env.close(); - auto const hybridOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); // other domains can't consume the offer @@ -1246,107 +1285,197 @@ class PermissionedDEX_test : public beast::unit_test::Suite Domain(badDomainID), Ter(tecPATH_DRY)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); - // hybrid offer can't be consumed since bob_ is not in domain anymore - env(pay(alice_, carol_, USD(5)), Path(~USD), Sendmax(XRP(5))); + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, hybridOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); } // test domain payment consuming two offers w/ hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10)), Domain(domainID)); + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); // payment fail because there isn't eur offer - env(pay(alice_, carol_, eur(5)), + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID), Ter(tecPATH_PARTIAL)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two domain offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, true)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } // test regular payment using a regular offer and a hybrid offer { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; - env.trust(eur(1000), alice_); + auto const eur = gw["EUR"]; + env.trust(eur(1000), alice); env.close(); - env.trust(eur(1000), bob_); + env.trust(eur(1000), bob); env.close(); - env.trust(eur(1000), carol_); + env.trust(eur(1000), carol); env.close(); - env(pay(gw_, bob_, eur(100))); + env(pay(gw, bob, eur(100))); env.close(); - // bob_ creates a regular usd offer - auto const usdOfferSeq{env.seq(bob_)}; - env(offer(bob_, XRP(10), USD(10))); + // bob creates a regular usd offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false)); - // bob_ creates a hybrid eur offer - auto const eurOfferSeq{env.seq(bob_)}; - env(offer(bob_, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid)); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true)); - // alice_ successfully consume two offers: xrp/usd and usd/eur - env(pay(alice_, carol_, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); + // alice successfully consume two offers: xrp/usd and usd/eur + env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5))); env.close(); - BEAST_EXPECT(checkOffer(env, bob_, usdOfferSeq, XRP(5), USD(5), 0, false)); - BEAST_EXPECT(checkOffer(env, bob_, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false)); + BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true)); } } + // Test that a hybrid offer remains crossable in the open book after the + // owner's domain credential expires. A domain payment after expiry should + // fail (domain book evicts the offer in its sandbox), but the open book + // remains usable. + void + testHybridOpenBookAfterCredentialExpiry(FeatureBitset features) + { + testcase("Hybrid open book after credential expiry"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + Account const devin("devin"); + env.fund(XRP(100000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // Give devin a credential that expires far enough in the future to + // survive the setup env.close() calls. + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count(); + jv[sfExpiration.jsonName] = t + 100; + env(jv); + env.close(); + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // Devin creates a hybrid offer: sell USD(10) for XRP(10). + // The offer is placed in both the domain book and the open book. + auto const hybridOfferSeq{env.seq(devin)}; + env(offer(devin, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + // A non-domain open-book payment partially crosses the offer while + // devin's credential is still valid. + auto carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(5)); + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // Advance time so that devin's credential expires. + env.close(std::chrono::seconds(100)); + + // Confirm devin can no longer create domain offers. + env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecNO_PERMISSION)); + env.close(); + + // The hybrid offer must still exist in the open book after expiry. + BEAST_EXPECT(offerExists(env, devin, hybridOfferSeq)); + + // A non-domain open-book payment must cross (not evict) the + // remaining portion of devin's hybrid offer. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(2)), Path(~USD), Sendmax(XRP(2))); + env.close(); + + // Carol received USD; the offer was crossed, not evicted. + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(2)); + // Offer still exists with 3 USD / 3 XRP remaining. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // A domain payment now fails because the domain book evicts devin's + // offer (his credential has expired). The eviction is rolled back with + // the failed sandbox, so the offer is NOT permanently removed. + env(pay(alice, carol, USD(1)), + Path(~USD), + Sendmax(XRP(1)), + Domain(domainID), + Ter(tecPATH_PARTIAL)); + env.close(); + + // Offer still intact in the open book; domain payment did not + // permanently delete it. + BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true)); + + // The open book can still fully consume the remaining portion. + carolBalance = env.balance(carol, USD); + env(pay(alice, carol, USD(3)), Path(~USD), Sendmax(XRP(3))); + env.close(); + BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(3)); + BEAST_EXPECT(!offerExists(env, devin, hybridOfferSeq)); + } + void testHybridOfferDirectories(FeatureBitset features) { Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); std::vector offerSeqs; @@ -1362,12 +1491,12 @@ class PermissionedDEX_test : public beast::unit_test::Suite for (size_t i = 1; i <= dirCnt; i++) { - auto const bobOfferSeq{env.seq(bob_)}; + auto const bobOfferSeq{env.seq(bob)}; offerSeqs.emplace_back(bobOfferSeq); - env(offer(bob_, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); + env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - auto const sleOffer = env.le(keylet::offer(bob_.id(), bobOfferSeq)); + auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); BEAST_EXPECT(sleOffer); BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); BEAST_EXPECT(sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); @@ -1375,17 +1504,17 @@ class PermissionedDEX_test : public beast::unit_test::Suite sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory) == openDir); - BEAST_EXPECT(checkOffer(env, bob_, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); BEAST_EXPECT(checkDirectorySize(env, domainDir, i)); BEAST_EXPECT(checkDirectorySize(env, openDir, i)); } for (auto const offerSeq : offerSeqs) { - env(offerCancel(bob_, offerSeq)); + env(offerCancel(bob, offerSeq)); env.close(); dirCnt--; - BEAST_EXPECT(!offerExists(env, bob_, offerSeq)); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt)); BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt)); } @@ -1397,34 +1526,34 @@ class PermissionedDEX_test : public beast::unit_test::Suite testcase("Auto bridge"); Env env(*this, features); - auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] = + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = PermissionedDEX(env); - auto const eur = gw_["EUR"]; + auto const eur = gw["EUR"]; - for (auto const& account : {alice_, bob_, carol_}) + for (auto const& account : {alice, bob, carol}) { env(trust(account, eur(10000))); env.close(); } - env(pay(gw_, carol_, eur(1))); + env(pay(gw, carol, eur(1))); env.close(); - auto const aliceOfferSeq{env.seq(alice_)}; - auto const bobOfferSeq{env.seq(bob_)}; - env(offer(alice_, XRP(100), USD(1)), Domain(domainID)); - env(offer(bob_, eur(1), XRP(100)), Domain(domainID)); + auto const aliceOfferSeq{env.seq(alice)}; + auto const bobOfferSeq{env.seq(bob)}; + env(offer(alice, XRP(100), USD(1)), Domain(domainID)); + env(offer(bob, eur(1), XRP(100)), Domain(domainID)); env.close(); - // carol_'s offer should cross bob_ and alice_'s offers due to auto + // carol's offer should cross bob and alice's offers due to auto // bridging - auto const carolOfferSeq{env.seq(carol_)}; - env(offer(carol_, USD(1), eur(1)), Domain(domainID)); + auto const carolOfferSeq{env.seq(carol)}; + env(offer(carol, USD(1), eur(1)), Domain(domainID)); env.close(); - BEAST_EXPECT(!offerExists(env, bob_, aliceOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, bobOfferSeq)); - BEAST_EXPECT(!offerExists(env, bob_, carolOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); } void @@ -1819,7 +1948,9 @@ public: // Test hybrid offers testHybridOfferCreate(all); testHybridBookStep(all); + testHybridInvalidOffer(all - fixCleanup3_3_0); testHybridInvalidOffer(all); + testHybridOpenBookAfterCredentialExpiry(all); testHybridOfferDirectories(all); testHybridMalformedOffer(all); testHybridMalformedOffer(all - fixCleanup3_1_3); From c0272f131446c26c1ef6cc65245d62054d9e39b3 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:29:53 +0100 Subject: [PATCH 10/10] docs: Add span attribute naming convention (OTel phase 1a) Establish the single, authoritative naming convention for OpenTelemetry span attribute keys so the code, collector, Tempo, dashboards, and docs stay in sync. - CONTRIBUTING.md: new "Telemetry span attribute naming" section under the Style guide as the permanent, canonical home for the rules. - OpenTelemetryPlan/02-design-decisions.md: new section 2.3.3 stating the decided convention as design, and section 2.4 attribute schema realigned to the underscore form (exact key spelling defers to the *SpanNames.h constants). - Sweep the remaining plan docs: convert dotted xrpl.. span attribute keys to the underscore form; leave span names and the OTel-standard service.*/http.* and xrpl.network.* resource keys dotted. Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 29 +++ OpenTelemetryPlan/00-tracing-fundamentals.md | 2 +- OpenTelemetryPlan/01-architecture-analysis.md | 36 ++-- OpenTelemetryPlan/02-design-decisions.md | 178 +++++++++++------- .../03-implementation-strategy.md | 6 +- OpenTelemetryPlan/04-code-samples.md | 36 ++-- .../05-configuration-reference.md | 20 +- .../07-observability-backends.md | 22 +-- OpenTelemetryPlan/POC_taskList.md | 30 +-- OpenTelemetryPlan/presentation.md | 30 +-- 10 files changed, 229 insertions(+), 160 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25dd7ac059..bc56d5a6bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -300,6 +300,35 @@ If you wish to automatically fix whatever clang-tidy finds _and_ is capable of f run-clang-tidy -p build -quiet -fix -allow-no-checks src tests ``` +## Telemetry span attribute naming + +OpenTelemetry span attribute keys follow these rules so they stay consistent +across the code, the OTel collector, Tempo, Grafana dashboards, and docs. The +constants in the `*SpanNames.h` headers are the single source of truth; every +other layer must match them. A CI check enforces this end to end. + +1. Per-span unique attribute: bare field name — the span name already carries + the domain (e.g. `command`, `local`, `version` on `rpc.command` / `tx.process`). +2. Collision qualifier: `_` when a bare name would collide across + domains (in the shared spanmetrics label space) or with the OTel-reserved + `status` key (e.g. `rpc_status`, `grpc_status`, `proposal_trusted`, + `validation_trusted`). +3. Shared cross-span attribute: `_` underscore form + (e.g. `tx_hash`, `peer_id`, `ledger_seq`, `consensus_round`). +4. Resource attribute: dotted `xrpl..` — reserved ONLY for + process/network identity set once at startup (`xrpl.network.id`, + `xrpl.network.type`). Never use the dotted `xrpl.` form for span attributes. +5. Span names use `[.]` (dotted). Only attribute _keys_ + follow rules 1–4. + +Standard OpenTelemetry semantic-convention keys keep their canonical dotted +form (e.g. `service.*` resource attributes, `http.*` span attributes); the +"no dotted form" rule above applies to xrpl-custom keys, not to OTel-standard +conventions. + +Always reference the `*SpanNames.h` constants — never pass string literals as +attribute keys or values to `setAttribute`/`addEvent`. + ## Contracts and instrumentation We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, diff --git a/OpenTelemetryPlan/00-tracing-fundamentals.md b/OpenTelemetryPlan/00-tracing-fundamentals.md index d7cc40c5ac..7839273ff2 100644 --- a/OpenTelemetryPlan/00-tracing-fundamentals.md +++ b/OpenTelemetryPlan/00-tracing-fundamentals.md @@ -68,7 +68,7 @@ A **span** represents a single unit of work within a trace. Each span has: | `name` | Operation name | `rpc.submit` | | `start_time` | When work began (local time) | `2024-01-15T10:30:00Z` | | `end_time` | When work completed (local time) | `2024-01-15T10:30:00.050Z` | -| `attributes` | Key-value metadata | `tx.hash=ABC...` | +| `attributes` | Key-value metadata | `tx_hash=ABC...` | | `status` | OK, ERROR MSG | `OK` | ### 3. Trace Context diff --git a/OpenTelemetryPlan/01-architecture-analysis.md b/OpenTelemetryPlan/01-architecture-analysis.md index c62ac3454c..1161a1645b 100644 --- a/OpenTelemetryPlan/01-architecture-analysis.md +++ b/OpenTelemetryPlan/01-architecture-analysis.md @@ -199,7 +199,7 @@ Consensus rounds are multi-phase operations that benefit significantly from trac ```mermaid flowchart TB subgraph round["consensus.round (root span)"] - attrs["Attributes:
xrpl.consensus.ledger.seq = 12345678
xrpl.consensus.mode = proposing
xrpl.consensus.proposers = 35"] + attrs["Attributes:
ledger_seq = 12345678
consensus_mode = proposing
proposers = 35"] subgraph open["consensus.phase.open"] open_desc["Duration: ~3s
Waiting for transactions"] @@ -211,7 +211,7 @@ flowchart TB end subgraph accept["consensus.phase.accept"] - acc_attrs["transactions_applied = 150
ledger.hash = DEF456..."] + acc_attrs["transactions_applied = 150
ledger_hash = DEF456..."] acc_children["├── ledger.build
└── ledger.validate"] end @@ -247,14 +247,14 @@ flowchart TB subgraph request["rpc.request (root span)"] http["HTTP Request — POST /
traceparent:
00-abc123...-def456...-01"] - attrs["Attributes:
http.method = POST
net.peer.ip = 192.168.1.100
xrpl.rpc.command = submit"] + attrs["Attributes:
http.method = POST
net.peer.ip = 192.168.1.100
command = submit"] subgraph enqueue["jobqueue.enqueue"] - job_attr["xrpl.job.type = jtCLIENT_RPC"] + job_attr["job_type = jtCLIENT_RPC"] end subgraph command["rpc.command.submit"] - cmd_attrs["xrpl.rpc.version = 2
xrpl.rpc.role = user"] + cmd_attrs["version = 2
rpc_role = user"] cmd_children["├── tx.deserialize
├── tx.validate_local
└── tx.submit_to_network"] end @@ -354,17 +354,17 @@ After implementing OpenTelemetry, operators and developers will gain visibility ### 1.8.1 What You Will See: Traces -| Trace Type | Description | Example Query in Grafana/Tempo | -| -------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && xrpl.tx.hash="ABC123..."}` | -| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{xrpl.tx.relay_count > 0}` | -| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` | -| **RPC Request Processing** | Individual command execution with timing breakdown | `{xrpl.rpc.command="account_info"}` | -| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` | -| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` | -| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` | -| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` | -| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` | +| Trace Type | Description | Example Query in Grafana/Tempo | +| -------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| **Transaction Lifecycle** | Full journey from RPC submission through validation, relay, consensus, and ledger inclusion | `{service.name="xrpld" && tx_hash="ABC123..."}` | +| **Cross-Node Propagation** | Transaction path across multiple xrpld nodes with timing | `{relay_count > 0}` | +| **Consensus Rounds** | Complete round with all phases (open, establish, accept) | `{span.name=~"consensus.round.*"}` | +| **RPC Request Processing** | Individual command execution with timing breakdown | `{command="account_info"}` | +| **Ledger Acquisition** | Peer-to-peer ledger data requests and responses | `{span.name="ledger.acquire"}` | +| **PathFinding Latency** | Path computation time and cache effectiveness for payment RPCs | `{span.name="pathfind.compute"}` | +| **TxQ Behavior** | Queue depth, eviction patterns, fee escalation during congestion | `{span.name=~"txq.*"}` | +| **Ledger Sync** | Full acquisition timeline including delta and transaction fetches | `{span.name=~"ledger.acquire.*"}` | +| **Validator Health** | UNL fetch success, manifest updates, stale list detection | `{span.name=~"validator.*"}` | ### 1.8.2 What You Will See: Metrics (Derived from Traces) @@ -456,9 +456,9 @@ xychart-beta ### 1.8.5 Developer Debugging Workflow -1. **Find Transaction**: Query by `xrpl.tx.hash` to get full trace +1. **Find Transaction**: Query by `tx_hash` to get full trace 2. **Identify Bottleneck**: Look at span durations to find slowest component -3. **Check Attributes**: Review `xrpl.tx.validity`, `xrpl.rpc.status` for errors +3. **Check Attributes**: Review `validity`, `rpc_status` for errors 4. **Correlate Logs**: Use `trace_id` to find related PerfLog entries 5. **Compare Nodes**: Filter by `service.instance.id` to compare behavior across nodes diff --git a/OpenTelemetryPlan/02-design-decisions.md b/OpenTelemetryPlan/02-design-decisions.md index 62b0d83fa3..58ef3abdc1 100644 --- a/OpenTelemetryPlan/02-design-decisions.md +++ b/OpenTelemetryPlan/02-design-decisions.md @@ -215,6 +215,39 @@ job: execute: "Job execution" ``` +### 2.3.3 Attribute Naming Conventions + +Span **names** follow §2.3.1 (dotted `.`). Span +**attribute keys** follow the rules below. The constants in the `*SpanNames.h` +headers are the single source of truth; the collector, Tempo, the Grafana +dashboards, and the runbook all consume these exact keys, so every layer must +agree with the code. A CI check enforces this end to end. + +1. **Per-span unique attribute** → bare field name. The span name already + carries the domain, so no prefix is needed (e.g. `command`, `version`, + `local` on `rpc.command`). +2. **Collision qualifier** → `_` when a bare name would collide + across domains in the shared spanmetrics label space, or with the + OTel-reserved `status` key (e.g. `rpc_status`, `grpc_status`, + `proposal_trusted`, `validation_trusted`). +3. **Shared cross-span attribute** → `_` underscore form, used + wherever the same field appears on more than one span (e.g. `tx_hash`, + `peer_id`, `ledger_seq`, `consensus_round`, `consensus_mode`). +4. **Resource attribute** → dotted `xrpl..`, reserved ONLY + for process/network identity set once at startup (`xrpl.network.id`, + `xrpl.network.type`). Span attributes are never dotted in the `xrpl.` form — + it blurs the resource/span scope boundary and parses awkwardly in TraceQL. +5. **Span names** use `[.]` (dotted, per §2.3.1). Only + attribute _keys_ follow rules 1–4. + +Standard OpenTelemetry semantic-convention keys keep their canonical dotted +form (e.g. `service.*` resource attributes, `http.*` span attributes); the +"no dotted form" rule applies to xrpl-custom keys only. + +The same rules are recorded in `CONTRIBUTING.md` (the permanent home, since +`OpenTelemetryPlan/` is removed once the rollout completes). The attribute +examples in §2.4 below follow these rules. + --- ## 2.4 Attribute Schema @@ -238,128 +271,135 @@ resource::SemanticConventions::SERVICE_INSTANCE_ID = ### 2.4.2 Span Attributes by Category +> Span attribute keys use the underscore form from §2.3.3 (shared/qualified +> keys are `_`; per-span unique keys are bare). The dotted form +> is reserved for the resource attributes in §2.4.1 above. This catalog lists +> the planned attribute set by category; the exact emitted key for each +> implemented span is defined by the `*SpanNames.h` constants, which are the +> single source of truth where the two differ. + #### Transaction Attributes ```cpp -"xrpl.tx.hash" = string // Transaction hash (hex) -"xrpl.tx.type" = string // "Payment", "OfferCreate", etc. -"xrpl.tx.account" = string // Source account (redacted in prod) -"xrpl.tx.sequence" = int64 // Account sequence number -"xrpl.tx.fee" = int64 // Fee in drops -"xrpl.tx.result" = string // "tesSUCCESS", "tecPATH_DRY", etc. -"xrpl.tx.ledger_index" = int64 // Ledger containing transaction +"tx_hash" = string // Transaction hash (hex) +"tx_type" = string // "Payment", "OfferCreate", etc. +"tx_account" = string // Source account (redacted in prod) +"tx_sequence" = int64 // Account sequence number +"tx_fee" = int64 // Fee in drops +"tx_result" = string // "tesSUCCESS", "tecPATH_DRY", etc. +"ledger_index" = int64 // Ledger containing transaction ``` #### Consensus Attributes ```cpp -"xrpl.consensus.round" = int64 // Round number -"xrpl.consensus.phase" = string // "open", "establish", "accept" -"xrpl.consensus.mode" = string // "proposing", "observing", etc. -"xrpl.consensus.proposers" = int64 // Number of proposers -"xrpl.consensus.ledger.prev" = string // Previous ledger hash -"xrpl.consensus.ledger.seq" = int64 // Ledger sequence -"xrpl.consensus.tx_count" = int64 // Transactions in consensus set -"xrpl.consensus.duration_ms" = float64 // Round duration +"consensus_round" = int64 // Round number +"consensus_phase" = string // "open", "establish", "accept" +"consensus_mode" = string // "proposing", "observing", etc. +"proposers" = int64 // Number of proposers +"prev_ledger_prefix" = string // Previous ledger hash prefix +"ledger_seq" = int64 // Ledger sequence +"tx_count" = int64 // Transactions in consensus set +"round_time_ms" = float64 // Round duration ``` #### RPC Attributes ```cpp -"xrpl.rpc.command" = string // Command name -"xrpl.rpc.version" = int64 // API version -"xrpl.rpc.role" = string // "admin" or "user" -"xrpl.rpc.params" = string // Sanitized parameters (optional) +"command" = string // Command name (per-span unique on rpc.command) +"version" = int64 // API version +"rpc_role" = string // "admin" or "user" (qualified — "role" is generic) +"params" = string // Sanitized parameters (optional) ``` #### Peer & Message Attributes ```cpp -"xrpl.peer.id" = string // Peer public key (base58) -"xrpl.peer.address" = string // IP:port -"xrpl.peer.latency_ms" = float64 // Measured latency -"xrpl.peer.cluster" = string // Cluster name if clustered -"xrpl.message.type" = string // Protocol message type name -"xrpl.message.size_bytes" = int64 // Message size -"xrpl.message.compressed" = bool // Whether compressed +"peer_id" = string // Peer public key (base58) +"peer_address" = string // IP:port +"peer_latency_ms" = float64 // Measured latency +"peer_cluster" = string // Cluster name if clustered +"message_type" = string // Protocol message type name +"message_size_bytes" = int64 // Message size +"message_compressed" = bool // Whether compressed ``` #### Ledger & Job Attributes ```cpp -"xrpl.ledger.hash" = string // Ledger hash -"xrpl.ledger.index" = int64 // Ledger sequence/index -"xrpl.ledger.close_time" = int64 // Close time (epoch) -"xrpl.ledger.tx_count" = int64 // Transaction count -"xrpl.job.type" = string // Job type name -"xrpl.job.queue_ms" = float64 // Time spent in queue -"xrpl.job.worker" = int64 // Worker thread ID +"ledger_hash" = string // Ledger hash +"ledger_index" = int64 // Ledger sequence/index +"close_time" = int64 // Close time (epoch) +"ledger_tx_count" = int64 // Transaction count +"job_type" = string // Job type name +"job_queue_ms" = float64 // Time spent in queue +"job_worker" = int64 // Worker thread ID ``` #### PathFinding Attributes ```cpp -"xrpl.pathfind.source_currency" = string // Source currency code -"xrpl.pathfind.dest_currency" = string // Destination currency code -"xrpl.pathfind.path_count" = int64 // Number of paths found -"xrpl.pathfind.cache_hit" = bool // RippleLineCache hit +"pathfind_source_currency" = string // Source currency code +"pathfind_dest_currency" = string // Destination currency code +"pathfind_path_count" = int64 // Number of paths found +"pathfind_cache_hit" = bool // RippleLineCache hit ``` #### TxQ Attributes ```cpp -"xrpl.txq.queue_depth" = int64 // Current queue depth -"xrpl.txq.fee_level" = int64 // Fee level of transaction -"xrpl.txq.eviction_reason" = string // Why transaction was evicted +"txq_queue_depth" = int64 // Current queue depth +"txq_fee_level" = int64 // Fee level of transaction +"txq_eviction_reason" = string // Why transaction was evicted ``` #### Fee Attributes ```cpp -"xrpl.fee.load_factor" = int64 // Current load factor -"xrpl.fee.escalation_level" = int64 // Fee escalation multiplier +"fee_load_factor" = int64 // Current load factor +"fee_escalation_level" = int64 // Fee escalation multiplier ``` #### Validator Attributes ```cpp -"xrpl.validator.list_size" = int64 // UNL size -"xrpl.validator.list_age_sec" = int64 // Seconds since last update +"validator_list_size" = int64 // UNL size +"validator_list_age_sec" = int64 // Seconds since last update ``` #### Amendment Attributes ```cpp -"xrpl.amendment.name" = string // Amendment name -"xrpl.amendment.status" = string // "enabled", "vetoed", "supported" +"amendment_name" = string // Amendment name +"amendment_status" = string // "enabled", "vetoed", "supported" ``` #### SHAMap Attributes ```cpp -"xrpl.shamap.type" = string // "transaction", "state", "account_state" -"xrpl.shamap.missing_nodes" = int64 // Number of missing nodes during sync -"xrpl.shamap.duration_ms" = float64 // Sync duration +"shamap_type" = string // "transaction", "state", "account_state" +"shamap_missing_nodes" = int64 // Number of missing nodes during sync +"shamap_duration_ms" = float64 // Sync duration ``` ### 2.4.3 Data Collection Summary The following table summarizes what data is collected by category: -| Category | Attributes Collected | Purpose | -| --------------- | ---------------------------------------------------------------------- | ---------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (public keys), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id` (public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | -| **PathFinding** | `pathfind.source_currency`, `dest_currency`, `path_count`, `cache_hit` | Payment path analysis | -| **TxQ** | `txq.queue_depth`, `fee_level`, `eviction_reason` | Queue depth and fee tracking | -| **Fee** | `fee.load_factor`, `escalation_level` | Fee escalation monitoring | -| **Validator** | `validator.list_size`, `list_age_sec` | UNL health monitoring | -| **Amendment** | `amendment.name`, `status` | Protocol upgrade tracking | -| **SHAMap** | `shamap.type`, `missing_nodes`, `duration_ms` | State tree sync performance | +| Category | Attributes Collected | Purpose | +| --------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle | +| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers`, `round_time_ms` | Analyze consensus timing | +| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance | +| **Peer** | `peer_id` (public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis | +| **Ledger** | `ledger_hash`, `ledger_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | +| **PathFinding** | `pathfind_source_currency`, `pathfind_dest_currency`, `pathfind_path_count`, `pathfind_cache_hit` | Payment path analysis | +| **TxQ** | `txq_queue_depth`, `txq_fee_level`, `txq_eviction_reason` | Queue depth and fee tracking | +| **Fee** | `fee_load_factor`, `fee_escalation_level` | Fee escalation monitoring | +| **Validator** | `validator_list_size`, `validator_list_age_sec` | UNL health monitoring | +| **Amendment** | `amendment_name`, `amendment_status` | Protocol upgrade tracking | +| **SHAMap** | `shamap_type`, `shamap_missing_nodes`, `shamap_duration_ms` | State tree sync performance | ### 2.4.4 Privacy & Sensitive Data Policy @@ -384,7 +424,7 @@ The following data is explicitly **excluded** from telemetry collection: | Mechanism | Description | | ----------------------------- | ------------------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | | **Configurable Redaction** | Sensitive fields can be excluded via `[telemetry]` config section | | **Sampling** | Only 10% of traces recorded by default, reducing data exposure | | **Local Control** | Node operators have full control over what gets exported | @@ -400,13 +440,13 @@ processors: attributes: actions: # Hash account addresses before storage - - key: xrpl.tx.account + - key: tx_account action: hash # Remove IP addresses entirely - - key: xrpl.peer.address + - key: peer_address action: delete # Redact specific fields - - key: xrpl.rpc.params + - key: params action: delete ``` @@ -544,8 +584,8 @@ insight.timing("consensus.round", duration); ```cpp // Example OpenTelemetry span auto span = telemetry.startSpan("tx.relay"); -span->SetAttribute("tx.hash", hash); -span->SetAttribute("peer.id", peerId); +span->SetAttribute("tx_hash", hash); +span->SetAttribute("peer_id", peerId); // Span automatically linked to parent via context ``` diff --git a/OpenTelemetryPlan/03-implementation-strategy.md b/OpenTelemetryPlan/03-implementation-strategy.md index 4f6beb61d8..8a3eda6338 100644 --- a/OpenTelemetryPlan/03-implementation-strategy.md +++ b/OpenTelemetryPlan/03-implementation-strategy.md @@ -494,11 +494,11 @@ void ServerHandler::onRequest(...) { // After (only ~10 lines added) void ServerHandler::onRequest(...) { XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request"); // +1 line - XRPL_TRACE_SET_ATTR("xrpl.rpc.command", command); // +1 line + XRPL_TRACE_SET_ATTR("command", command); // +1 line auto result = processRequest(req); - XRPL_TRACE_SET_ATTR("xrpl.rpc.status", status); // +1 line + XRPL_TRACE_SET_ATTR("rpc_status", status); // +1 line send(result); } ``` @@ -514,7 +514,7 @@ void RCLConsensusAdaptor::startRound(...) { // After (context storage required) void RCLConsensusAdaptor::startRound(...) { XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.seq", seq); + XRPL_TRACE_SET_ATTR("ledger_seq", seq); // Store context for child spans in phase transitions currentRoundContext_ = _xrpl_guard_->context(); // New member variable diff --git a/OpenTelemetryPlan/04-code-samples.md b/OpenTelemetryPlan/04-code-samples.md index d70bcbc760..497ff9d5ea 100644 --- a/OpenTelemetryPlan/04-code-samples.md +++ b/OpenTelemetryPlan/04-code-samples.md @@ -674,9 +674,9 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); // Add transaction attributes - guard.setAttribute("xrpl.tx.hash", to_string(stx->getTransactionID())); - guard.setAttribute("xrpl.tx.type", stx->getTxnType()); - guard.setAttribute("xrpl.peer.id", remote_address_.to_string()); + guard.setAttribute("tx_hash", to_string(stx->getTransactionID())); + guard.setAttribute("tx_type", stx->getTxnType()); + guard.setAttribute("peer_id", remote_address_.to_string()); // Check if we've seen this transaction (HashRouter) auto const [flags, suppressed] = @@ -686,7 +686,7 @@ PeerImp::handleTransaction( if (suppressed) { - guard.setAttribute("xrpl.tx.suppressed", true); + guard.setAttribute("suppressed", true); guard.addEvent("tx.duplicate"); return; // Already processing this transaction } @@ -697,7 +697,7 @@ PeerImp::handleTransaction( telemetry::SpanGuard validateGuard(validateSpan); auto [validity, reason] = checkTransaction(stx); - validateGuard.setAttribute("xrpl.tx.validity", + validateGuard.setAttribute("validity", validity == Validity::Valid ? "valid" : "invalid"); if (validity != Validity::Valid) @@ -732,7 +732,7 @@ PeerImp::handleTransaction( protoCtx, // Pass trace context exclusions); - relayGuard.setAttribute("xrpl.tx.relay_count", + relayGuard.setAttribute("relay_count", static_cast(relayCount)); } @@ -763,12 +763,12 @@ RCLConsensusAdaptor::startRound( { XRPL_TRACE_CONSENSUS(app_.getTelemetry(), "consensus.round"); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.prev", to_string(prevLedgerHash)); - XRPL_TRACE_SET_ATTR("xrpl.consensus.ledger.seq", + XRPL_TRACE_SET_ATTR("prev_ledger_prefix", to_string(prevLedgerHash)); + XRPL_TRACE_SET_ATTR("ledger_seq", static_cast(prevLedger.seq() + 1)); - XRPL_TRACE_SET_ATTR("xrpl.consensus.proposers", + XRPL_TRACE_SET_ATTR("proposers", static_cast(peers.size())); - XRPL_TRACE_SET_ATTR("xrpl.consensus.mode", + XRPL_TRACE_SET_ATTR("consensus_mode", proposing ? "proposing" : "observing"); // Store trace context for use in phase transitions @@ -788,7 +788,7 @@ RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) currentRoundContext_); telemetry::SpanGuard guard(span); - guard.setAttribute("xrpl.consensus.phase", to_string(newPhase)); + guard.setAttribute("consensus_phase", to_string(newPhase)); guard.addEvent("phase.enter"); auto const startTime = std::chrono::steady_clock::now(); @@ -798,7 +798,7 @@ RCLConsensusAdaptor::phaseTransition(ConsensusPhase newPhase) auto result = doPhaseTransition(newPhase); auto const duration = std::chrono::steady_clock::now() - startTime; - guard.setAttribute("xrpl.consensus.phase_duration_ms", + guard.setAttribute("consensus_phase_duration_ms", std::chrono::duration(duration).count()); guard.setOk(); @@ -830,9 +830,9 @@ RCLConsensusAdaptor::peerProposal( opentelemetry::trace::SpanKind::kServer); telemetry::SpanGuard guard(span); - guard.setAttribute("xrpl.consensus.proposer", + guard.setAttribute("proposer", toBase58(TokenType::NodePublic, proposal.nodeId())); - guard.setAttribute("xrpl.consensus.round", + guard.setAttribute("consensus_round", static_cast(proposal.proposal().proposeSeq())); // ... existing implementation ... @@ -901,7 +901,7 @@ ServerHandler::onRequest( ? jv["method"].asString() : "unknown"; - guard.setAttribute("xrpl.rpc.command", command); + guard.setAttribute("command", command); // Create child span for command execution auto cmdSpan = app_.getTelemetry().startSpan( @@ -915,7 +915,7 @@ ServerHandler::onRequest( // Record result attributes if (result.isMember("status")) { - cmdGuard.setAttribute("xrpl.rpc.status", + cmdGuard.setAttribute("rpc_status", result["status"].asString()); } @@ -1027,8 +1027,8 @@ JobQueue::processTask(int instance) if (telemetry_.isEnabled()) { guard.emplace(telemetry_.startSpan("job.execute")); - guard->setAttribute("xrpl.job.type", to_string(job.type())); - guard->setAttribute("xrpl.job.worker", + guard->setAttribute("job_type", to_string(job.type())); + guard->setAttribute("job_worker", static_cast(instance)); } #endif diff --git a/OpenTelemetryPlan/05-configuration-reference.md b/OpenTelemetryPlan/05-configuration-reference.md index 0ea40c08a9..9702b1af6c 100644 --- a/OpenTelemetryPlan/05-configuration-reference.md +++ b/OpenTelemetryPlan/05-configuration-reference.md @@ -481,7 +481,7 @@ processors: - name: rpc-spans type: string_attribute string_attribute: - key: xrpl.rpc.command + key: command values: [".*"] enabled_regex_matching: true - name: latency @@ -498,7 +498,7 @@ processors: attributes: actions: # Hash sensitive data - - key: xrpl.tx.account + - key: tx_account action: hash # Add deployment info - key: deployment.environment @@ -674,7 +674,7 @@ datasources: httpMethod: GET tracesToLogs: datasourceUid: loki - tags: ["service.name", "xrpl.tx.hash"] + tags: ["service.name", "tx_hash"] mappedTags: [{ key: "trace_id", value: "traceID" }] mapTagNamesEnabled: true filterByTraceID: true @@ -739,7 +739,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.xrpl.rpc.command != \"\"} | histogram_over_time(duration) by (span.xrpl.rpc.command)" + "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | histogram_over_time(duration) by (span.command)" } ], "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 } @@ -751,7 +751,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.xrpl.rpc.command)" + "query": "{resource.service.name=\"xrpld\" && status.code=error} | rate() by (span.command)" } ], "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 } @@ -763,7 +763,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && span.xrpl.rpc.command != \"\"} | avg(duration) by (span.xrpl.rpc.command) | topk(10)" + "query": "{resource.service.name=\"xrpld\" && span.command != \"\"} | avg(duration) by (span.command) | topk(10)" } ], "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 } @@ -810,7 +810,7 @@ providers: "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.xrpl.tx.relay_count)" + "query": "{resource.service.name=\"xrpld\" && name=\"tx.relay\"} | avg(span.relay_count)" } ], "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 } @@ -837,7 +837,7 @@ Common queries for xrpld traces: ``` # Find all traces for a specific transaction hash -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} # Find slow RPC commands (>100ms) {resource.service.name="xrpld" && name=~"rpc.command.*"} | duration > 100ms @@ -849,7 +849,7 @@ Common queries for xrpld traces: {resource.service.name="xrpld" && name="tx.validate" && status.code=error} # Find transactions relayed to many peers -{resource.service.name="xrpld" && name="tx.relay"} | span.xrpl.tx.relay_count > 10 +{resource.service.name="xrpld" && name="tx.relay"} | span.relay_count > 10 # Compare latency across nodes {resource.service.name="xrpld" && name="rpc.command.account_info"} | avg(duration) by (resource.service.instance.id) @@ -909,7 +909,7 @@ In Tempo data source configuration, set up the derived field: jsonData: tracesToLogs: datasourceUid: loki - tags: ["trace_id", "xrpl.tx.hash"] + tags: ["trace_id", "tx_hash"] filterByTraceID: true filterBySpanID: false ``` diff --git a/OpenTelemetryPlan/07-observability-backends.md b/OpenTelemetryPlan/07-observability-backends.md index 5d1638670a..b8e35d91f5 100644 --- a/OpenTelemetryPlan/07-observability-backends.md +++ b/OpenTelemetryPlan/07-observability-backends.md @@ -279,7 +279,7 @@ Pre-built dashboards for xrpld observability. "targets": [ { "queryType": "traceql", - "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.xrpl.consensus.proposers)" + "query": "{resource.service.name=\"xrpld\" && name=\"consensus.round\"} | avg(span.proposers)" } ], "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 } @@ -503,18 +503,18 @@ flowchart TB - **xrpld Node (three sources)**: A single node emits three independent data streams -- OpenTelemetry spans, PerfLog JSON logs, and Beast Insight StatsD metrics. - **Data Collection layer**: Each stream has its own collector -- OTel Collector for spans, Promtail/Fluentd for logs, and a StatsD exporter for metrics. They operate independently. - **Storage layer (Tempo, Loki, Prometheus)**: Each data type lands in a purpose-built store optimized for its query patterns (trace search, log grep, metric aggregation). -- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `xrpl.tx.hash`, `ledger_seq`), enabling a single-pane debugging experience. +- **Grafana Correlation Panel**: The key integration point -- Grafana queries all three stores and links them via shared fields (`trace_id`, `tx_hash`, `ledger_seq`), enabling a single-pane debugging experience. ### 7.7.2 Correlation Fields -| Source | Field | Link To | Purpose | -| ----------- | --------------------------- | ------------- | -------------------------- | -| **Trace** | `trace_id` | Logs | Find log entries for trace | -| **Trace** | `xrpl.tx.hash` | Logs, Metrics | Find TX-related data | -| **Trace** | `xrpl.consensus.ledger.seq` | Logs | Find ledger-related logs | -| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log | -| **PerfLog** | `ledger_seq` | Traces | Find consensus trace | -| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike | +| Source | Field | Link To | Purpose | +| ----------- | ------------------- | ------------- | -------------------------- | +| **Trace** | `trace_id` | Logs | Find log entries for trace | +| **Trace** | `tx_hash` | Logs, Metrics | Find TX-related data | +| **Trace** | `ledger_seq` | Logs | Find ledger-related logs | +| **PerfLog** | `trace_id` (new) | Traces | Jump to trace from log | +| **PerfLog** | `ledger_seq` | Traces | Find consensus trace | +| **Insight** | `exemplar.trace_id` | Traces | Jump from metric spike | ### 7.7.3 Example: Debugging a Slow Transaction @@ -522,7 +522,7 @@ flowchart TB ``` # In Grafana Explore with Tempo -{resource.service.name="xrpld" && span.xrpl.tx.hash="ABC123..."} +{resource.service.name="xrpld" && span.tx_hash="ABC123..."} ``` **Step 2: Get the trace_id from the trace view** diff --git a/OpenTelemetryPlan/POC_taskList.md b/OpenTelemetryPlan/POC_taskList.md index 32fd73170f..3e2b96d86a 100644 --- a/OpenTelemetryPlan/POC_taskList.md +++ b/OpenTelemetryPlan/POC_taskList.md @@ -344,12 +344,12 @@ - `#include` the `TracingInstrumentation.h` header - In `ServerHandler::onRequest(Session& session)`: - At the top of the method, add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.request");` - - After the RPC command name is extracted, set attribute: `XRPL_TRACE_SET_ATTR("xrpl.rpc.command", command);` + - After the RPC command name is extracted, set attribute: `XRPL_TRACE_SET_ATTR("command", command);` - After the response status is known, set: `XRPL_TRACE_SET_ATTR("http.status_code", static_cast(statusCode));` - Wrap error paths with: `XRPL_TRACE_EXCEPTION(e);` - In `ServerHandler::processRequest(...)`: - Add a child span: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.process");` - - Set method attribute: `XRPL_TRACE_SET_ATTR("xrpl.rpc.method", request_method);` + - Set method attribute: `XRPL_TRACE_SET_ATTR("rpc_method", request_method);` - In `ServerHandler::onWSMessage(...)` (WebSocket path): - Add: `XRPL_TRACE_RPC(app_.getTelemetry(), "rpc.ws.message");` @@ -370,7 +370,7 @@ - [01-architecture-analysis.md §1.5](./01-architecture-analysis.md) — RPC request flow diagram: HTTP request -> attributes -> jobqueue.enqueue -> rpc.command -> response - [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.request` in `ServerHandler.cpp::onRequest()` (Priority: High) - [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming convention: `rpc.request`, `rpc.command.*` -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `xrpl.rpc.command`, `xrpl.rpc.version`, `xrpl.rpc.role`, `xrpl.rpc.params` +- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC span attributes: `command`, `version`, `rpc_role`, `params` - [03-implementation-strategy.md §3.9.2](./03-implementation-strategy.md) — File impact: `ServerHandler.cpp` ~40 lines added, ~10 changed (Low risk) --- @@ -386,17 +386,17 @@ - In `doCommand(RPC::JsonContext& context, Json::Value& result)`: - At the top: `XRPL_TRACE_RPC(context.app.getTelemetry(), "rpc.command." + context.method);` - Set attributes: - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.command", context.method);` - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.version", static_cast(context.apiVersion));` - - `XRPL_TRACE_SET_ATTR("xrpl.rpc.role", (context.role == Role::ADMIN) ? "admin" : "user");` - - On success: `XRPL_TRACE_SET_ATTR("xrpl.rpc.status", "success");` - - On error: `XRPL_TRACE_SET_ATTR("xrpl.rpc.status", "error");` and set the error message + - `XRPL_TRACE_SET_ATTR("command", context.method);` + - `XRPL_TRACE_SET_ATTR("version", static_cast(context.apiVersion));` + - `XRPL_TRACE_SET_ATTR("rpc_role", (context.role == Role::ADMIN) ? "admin" : "user");` + - On success: `XRPL_TRACE_SET_ATTR("rpc_status", "success");` + - On error: `XRPL_TRACE_SET_ATTR("rpc_status", "error");` and set the error message - After this, traces in Tempo/Grafana should look like: ``` - rpc.request (xrpl.rpc.command=account_info) + rpc.request (command=account_info) └── rpc.process - └── rpc.command.account_info (xrpl.rpc.version=2, xrpl.rpc.role=user, xrpl.rpc.status=success) + └── rpc.command.account_info (version=2, rpc_role=user, rpc_status=success) ``` **Key modified file**: @@ -407,7 +407,7 @@ - [04-code-samples.md §4.5.3](./04-code-samples.md) — `ServerHandler::onRequest()` code sample (includes child span pattern for `rpc.command.*`) - [02-design-decisions.md §2.3](./02-design-decisions.md) — Span naming: `rpc.command.*` pattern with dynamic command name (e.g., `rpc.command.server_info`) -- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `xrpl.rpc.command`, `xrpl.rpc.version`, `xrpl.rpc.role`, `xrpl.rpc.status` +- [02-design-decisions.md §2.4.2](./02-design-decisions.md) — RPC attribute schema: `command`, `version`, `rpc_role`, `rpc_status` - [01-architecture-analysis.md §1.6](./01-architecture-analysis.md) — Key trace points table: `rpc.command.*` in `RPCHandler.cpp::doCommand()` (Priority: High) - [02-design-decisions.md §2.6.5](./02-design-decisions.md) — Correlation with PerfLog: how `doCommand()` can link trace_id with existing PerfLog entries - [03-implementation-strategy.md §3.4.4](./03-implementation-strategy.md) — RPC request overhead budget: ~1.75 μs total per request @@ -480,7 +480,7 @@ - Navigate to Explore → select Tempo datasource - Search for service `xrpld` - Confirm you see traces with spans: `rpc.request` -> `rpc.process` -> `rpc.command.server_info` - - Click into a trace and verify attributes: `xrpl.rpc.command`, `xrpl.rpc.status`, `xrpl.rpc.version` + - Click into a trace and verify attributes: `command`, `rpc_status`, `version` 7. **Verify zero-overhead when disabled**: - Rebuild with `XRPL_ENABLE_TELEMETRY=OFF`, or set `enabled=0` in config @@ -494,7 +494,7 @@ - [ ] xrpld starts and connects to OTel Collector (check xrpld logs for telemetry messages) - [ ] Traces appear in Grafana/Tempo under service "xrpld" - [ ] Span hierarchy is correct (parent-child relationships) -- [ ] Span attributes are populated (`xrpl.rpc.command`, `xrpl.rpc.status`, etc.) +- [ ] Span attributes are populated (`command`, `rpc_status`, etc.) - [ ] Error spans show error status and message - [ ] Building with `XRPL_ENABLE_TELEMETRY=OFF` produces no regressions - [ ] Setting `enabled=0` at runtime produces no traces and no errors @@ -580,8 +580,8 @@ The current POC exports **traces only**. Grafana's Explore view can query Tempo explicit: buckets: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 5s] dimensions: - - name: xrpl.rpc.command - - name: xrpl.rpc.status + - name: command + - name: rpc_status exporters: prometheus: diff --git a/OpenTelemetryPlan/presentation.md b/OpenTelemetryPlan/presentation.md index 479aa8fa55..535807a96e 100644 --- a/OpenTelemetryPlan/presentation.md +++ b/OpenTelemetryPlan/presentation.md @@ -618,14 +618,14 @@ flowchart LR ### What Data is Collected -| Category | Attributes Collected | Purpose | -| --------------- | ------------------------------------------------------------------------------------ | --------------------------- | -| **Transaction** | `tx.hash`, `tx.type`, `tx.result`, `tx.fee`, `ledger_index` | Trace transaction lifecycle | -| **Consensus** | `round`, `phase`, `mode`, `proposers` (count of proposing validators), `duration_ms` | Analyze consensus timing | -| **RPC** | `command`, `version`, `status`, `duration_ms` | Monitor RPC performance | -| **Peer** | `peer.id`(public key), `latency_ms`, `message.type`, `message.size` | Network topology analysis | -| **Ledger** | `ledger.hash`, `ledger.index`, `close_time`, `tx_count` | Ledger progression tracking | -| **Job** | `job.type`, `queue_ms`, `worker` | JobQueue performance | +| Category | Attributes Collected | Purpose | +| --------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| **Transaction** | `tx_hash`, `tx_type`, `tx_result`, `tx_fee`, `ledger_index` | Trace transaction lifecycle | +| **Consensus** | `consensus_round`, `consensus_phase`, `consensus_mode`, `proposers` (count of proposing validators), `round_time_ms` | Analyze consensus timing | +| **RPC** | `command`, `version`, `rpc_status`, `duration_ms` | Monitor RPC performance | +| **Peer** | `peer_id`(public key), `peer_latency_ms`, `message_type`, `message_size_bytes` | Network topology analysis | +| **Ledger** | `ledger_hash`, `ledger_index`, `close_time`, `ledger_tx_count` | Ledger progression tracking | +| **Job** | `job_type`, `job_queue_ms`, `job_worker` | JobQueue performance | ### What is NOT Collected (Privacy Guarantees) @@ -658,13 +658,13 @@ flowchart LR ### Privacy Protection Mechanisms -| Mechanism | Description | -| -------------------------- | ------------------------------------------------------------- | -| **Account Hashing** | `xrpl.tx.account` is hashed at collector level before storage | -| **Configurable Redaction** | Sensitive fields can be excluded via config | -| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | -| **Local Control** | Node operators control what gets exported | -| **No Raw Payloads** | Transaction content is never recorded, only metadata | +| Mechanism | Description | +| -------------------------- | --------------------------------------------------------- | +| **Account Hashing** | `tx_account` is hashed at collector level before storage | +| **Configurable Redaction** | Sensitive fields can be excluded via config | +| **Sampling** | Only 10% of traces recorded by default (reduces exposure) | +| **Local Control** | Node operators control what gets exported | +| **No Raw Payloads** | Transaction content is never recorded, only metadata | > **Key Principle**: Telemetry collects **operational metadata** (timing, counts, hashes) — never **sensitive content** (keys, balances, amounts).