From 652b5f9af1b58c974ddcb8c4684227aca9fd1527 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Fri, 26 Jun 2026 16:34:22 -0400 Subject: [PATCH 01/31] fix: Block delegate tx from being queued (#7640) --- src/test/app/TxQ_test.cpp | 38 +++++++++++++++++++++++++++++++ src/xrpld/app/misc/detail/TxQ.cpp | 4 ++++ 2 files changed, 42 insertions(+) diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 97cb035578..2d10eb4beb 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -2335,6 +2336,42 @@ public: BEAST_EXPECT(env.balance(alice) == drops(5)); } + void + testDelegateTxCannotQueue() + { + using namespace jtx; + testcase("disallow delegate transaction from being queued"); + + Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}})); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + env.fund(XRP(50000), alice, bob); + env.close(); + env.fund(XRP(50000), carol); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + fillQueue(env, alice); + checkMetrics(*this, env, 0, 8, 5, 4); + + // Delegated transactions are not allowed to be queued. + env(pay(alice, carol, drops(1)), delegate::As(bob), Ter(telCAN_NOT_QUEUE)); + checkMetrics(*this, env, 0, 8, 5, 4); + + // Delegated transactions may still apply directly if they pay the + // open ledger fee. They just cannot be held in the queue. + env(pay(alice, carol, drops(1)), + delegate::As(bob), + Fee(openLedgerCost(env)), + Ter(tesSUCCESS)); + checkMetrics(*this, env, 0, 8, 6, 4); + } + void testConsequences() { @@ -4662,6 +4699,7 @@ public: testBlockersSeq(); testBlockersTicket(); testInFlightBalance(); + testDelegateTxCannotQueue(); testConsequences(); } diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 9022095ea8..ec5ac569d6 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -398,6 +398,10 @@ TxQ::canBeHeld( ((flags & TapFailHard) != 0u)) return telCAN_NOT_QUEUE; + // Disallow delegated transactions from being queued. + if (tx.isFieldPresent(sfDelegate)) + return telCAN_NOT_QUEUE; + { // To be queued and relayed, the transaction needs to // promise to stick around for long enough that it has From 3e9f1d0ab856775ee5a1fc3cf5e2ee942b508821 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:38:59 +0200 Subject: [PATCH 02/31] fix: Unify freeze checks for pseudo-account deposit/withdraw (#7382) Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Ayaz Salikhov Co-authored-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Co-authored-by: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> --- include/xrpl/ledger/helpers/TokenHelpers.h | 69 ++ include/xrpl/tx/transactors/dex/AMMWithdraw.h | 5 + src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 26 +- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 96 +- src/libxrpl/tx/invariants/MPTInvariant.cpp | 40 +- src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 68 +- .../tx/transactors/dex/AMMWithdraw.cpp | 67 +- .../lending/LoanBrokerCoverDeposit.cpp | 24 +- .../lending/LoanBrokerCoverWithdraw.cpp | 35 +- .../tx/transactors/vault/VaultDeposit.cpp | 24 +- .../tx/transactors/vault/VaultWithdraw.cpp | 53 +- src/test/app/AMMMPT_test.cpp | 222 ++++- src/test/app/AMM_test.cpp | 145 ++- src/test/app/Invariants_test.cpp | 205 ++++ src/test/app/LoanBroker_test.cpp | 476 ++++++++- src/test/app/Loan_test.cpp | 21 +- src/test/app/MPToken_test.cpp | 51 +- src/test/app/Vault_test.cpp | 943 +++++++++++------- src/test/jtx/Env.h | 20 + src/test/jtx/impl/utility.cpp | 1 - src/test/jtx/utility.h | 2 +- 21 files changed, 2007 insertions(+), 586 deletions(-) diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index f736e51d28..0c9871cd76 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -131,6 +131,75 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& [[nodiscard]] TER checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset); +/** + * Checks freeze compliance for withdrawing an asset from a pseudo-account (e.g. Vault, AMM, + * LoanBroker) to a destination account. + * + * Asserts that sourceAcct is a pseudo-account and that submitterAcct and dstAcct are not. + * + * Issuer exemption: returns tesSUCCESS immediately when dstAcct is the asset issuer — the issuer + * can always receive their own token, even when the pool is frozen. Callers that need to block + * withdrawals from a frozen pool even for the issuer (e.g. because the pool math cannot handle it) + * must check checkFrozen(sourceAcct, asset) separately before calling this function. + * + * Otherwise checks, in order: + * 1. If the asset is globally frozen the remaining checks are redundant. + * 2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its + * underlying asset. + * 3. The pseudo-account's trustline / MPToken must not be frozen for sending. + * 4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent + * recovering one's own funds. + * 5. The destination must not be deep-frozen (cannot receive under any circumstance). + * + * For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep + * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always + * blocked. + * + * @param view Ledger view to read freeze state from. + * @param srcAcct Pseudo-account the funds are withdrawn from (sender). + * @param submitterAcct Account that submitted the withdrawal transaction. + * @param dstAcct Account receiving the withdrawn funds. + * @param asset Asset being withdrawn. + * @return tesSUCCESS if the withdrawal is permitted, otherwise a freeze + * result (tecFROZEN for IOUs, tecLOCKED for MPTs). + */ +[[nodiscard]] TER +checkWithdrawFreeze( + ReadView const& view, + AccountID const& srcAcct, + AccountID const& submitterAcct, + AccountID const& dstAcct, + Asset const& asset); + +/** + * Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM, + * LoanBroker). + * + * + * Checks, in order: + * 1. If the asset is globally frozen the remaining checks are redundant. + * 2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its + * underlying asset (returns tecLOCKED). + * 3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer, + * since the issuer can always send its own asset. + * 4. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts, + * pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds + * could not later be withdrawn. + * + * @param view Ledger view to read freeze state from. + * @param srcAcct Depositor sending the funds. + * @param dstAcct Pseudo-account receiving the deposit. + * @param asset Asset being deposited. + * @return tesSUCCESS if the deposit is permitted, otherwise a freeze result + * (tecFROZEN for IOUs, tecLOCKED for MPTs). + */ +[[nodiscard]] TER +checkDepositFreeze( + ReadView const& view, + AccountID const& srcAcct, + AccountID const& dstAcct, + Asset const& asset); + //------------------------------------------------------------------------------ // // Account balance functions (Asset-based dispatchers) diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 6e88320eae..8b6d39349b 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -159,6 +159,11 @@ public: beast::Journal const& journal); private: + /** Returns IgnoreFreeze when the withdrawer is the issuer of a pool + * asset (post-fixCleanup3_3_0), ZeroIfFrozen otherwise. */ + [[nodiscard]] FreezeHandling + issuerFreezeHandling() const; + std::pair applyGuts(Sandbox& view); diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index a1af63a80f..2781902f5b 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -308,6 +308,18 @@ requireAuth( AuthType authType, std::uint8_t depth) { + bool const fix330Enabled = view.rules().enabled(fixCleanup3_3_0); + bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault); + bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2); + + // Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their participants. + // They are implicitly authorized for any MPT they hold, including vault shares whose + // underlying asset would otherwise require auth. + auto const isPseudoAccountExempt = [&] { + return (featureSAVEnabled || featureMPTV2Enabled) && + isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}); + }; + auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID()); auto const sleIssuance = view.read(mptID); if (!sleIssuance) @@ -319,7 +331,9 @@ requireAuth( if (mptIssuer == account) // Issuer won't have MPToken return tesSUCCESS; - bool const featureSAVEnabled = view.rules().enabled(featureSingleAssetVault); + // Post-fix330: exempt before the recursive underlying-asset auth check. + if (fix330Enabled && isPseudoAccountExempt()) + return tesSUCCESS; if (featureSAVEnabled) { @@ -382,13 +396,9 @@ requireAuth( // belong to someone who is explicitly authorized e.g. a vault owner. } - bool const featureMPTV2Enabled = view.rules().enabled(featureMPTokensV2); - if (featureSAVEnabled || featureMPTV2Enabled) - { - // Implicitly authorize Vault, LoanBroker, and AMM pseudo-accounts - if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID})) - return tesSUCCESS; - } + // Pre-fix330: exempt after domain/sleToken checks, preserving prior behavior. + if (!fix330Enabled && isPseudoAccountExempt()) + return tesSUCCESS; // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->isFlag(lsfMPTRequireAuth) && diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index f0a0220e1e..b9adfbd3e6 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -34,14 +35,6 @@ namespace xrpl { -// Forward declaration for function that remains in View.h/cpp -bool -isLPTokenFrozen( - ReadView const& view, - AccountID const& account, - Asset const& asset, - Asset const& asset2); - //------------------------------------------------------------------------------ // // Freeze checking (Asset-based) @@ -164,6 +157,90 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass [&](auto const& issue) { return checkDeepFrozen(view, account, issue); }, asset.value()); } +[[nodiscard]] TER +checkWithdrawFreeze( + ReadView const& view, + AccountID const& srcAcct, + AccountID const& submitterAcct, + AccountID const& dstAcct, + Asset const& asset) +{ + XRPL_ASSERT( + isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account"); + XRPL_ASSERT( + !isPseudoAccount(view, submitterAcct), + "xrpl::checkWithdrawFreeze : submitter is not a pseudo-account"); + XRPL_ASSERT( + !isPseudoAccount(view, dstAcct), + "xrpl::checkWithdrawFreeze : destination is not a pseudo-account"); + + // Funds can always be sent to the issuer + if (dstAcct == asset.getIssuer()) + return tesSUCCESS; + + // If the asset is globally frozen, other checks are redundant + if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) + return ret; + + // Special case for shares - check if the shares (and the transitive asset) is not frozen + if (asset.holds() && + isVaultPseudoAccountFrozen(view, srcAcct, asset.get(), 0)) + { + return tecLOCKED; + } + + // The transfer is from Submitter to Destination via Source (pseudo-account) + // Both Source and Submitter must not be frozen to allow sending funds + if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) + return ret; + + // Check submitter's individual freeze only when Submitter != Destination (a regular freeze + // should not block self-withdrawal). + if (submitterAcct != dstAcct) + { + if (auto const ret = checkIndividualFrozen(view, submitterAcct, asset); !isTesSuccess(ret)) + return ret; + } + + // The destination account must not be deep frozen to receive the funds + return checkDeepFrozen(view, dstAcct, asset); +} + +[[nodiscard]] TER +checkDepositFreeze( + ReadView const& view, + AccountID const& srcAcct, + AccountID const& dstAcct, + Asset const& asset) +{ + XRPL_ASSERT( + isPseudoAccount(view, dstAcct), + "xrpl::checkDepositFreeze : destination is a pseudo-account"); + XRPL_ASSERT( + !isPseudoAccount(view, srcAcct), + "xrpl::checkDepositFreeze : source is not a pseudo-account"); + + if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) + return ret; + + // Special case for shares - check if the shares and the transitive asset is not frozen + if (asset.holds() && + isVaultPseudoAccountFrozen(view, dstAcct, asset.get(), 0)) + { + return tecLOCKED; + } + + if (srcAcct != asset.getIssuer()) + { + if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) + return ret; + } + + // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze + // because those funds cannot be later withdrawn + return checkIndividualFrozen(view, dstAcct, asset); +} + //------------------------------------------------------------------------------ // // Account balance functions @@ -776,7 +853,8 @@ directSendNoLimitMultiIOU( if (senderID == issuer || receiverID == issuer || issuer == noAccount()) { // Direct send: redeeming IOUs and/or sending own IOUs. - if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j)) + if (auto const ter = directSendNoFeeIOU(view, senderID, receiverID, amount, false, j); + !isTesSuccess(ter)) return ter; actual += amount; // Do not add amount to takeFromSender, because directSendNoFeeIOU took diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 1176594bbf..278c7a7858 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -509,6 +511,15 @@ ValidMPTTransfer::isAuthorized( AccountID const& holder, bool reqAuth) const { + // Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their + // participants and are implicitly authorized for any MPT they hold, + // including vault shares whose underlying asset would otherwise require + // auth. Exempt them here rather than relying on requireAuth: the recursive + // share -> underlying descent in requireAuth fails for a pseudo-account + // that holds the share but not the underlying. + if (isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID})) + return true; + auto const key = keylet::mptoken(mptid, holder); auto const it = deletedAuthorized_.find(key.key); if (it != deletedAuthorized_.end()) @@ -524,6 +535,8 @@ ValidMPTTransfer::finalize( ReadView const& view, beast::Journal const& j) { + auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0); + if (hasPrivilege(tx, OverrideFreeze)) return true; @@ -584,12 +597,29 @@ ValidMPTTransfer::finalize( ++senders; } - // Check once: if any involved account is frozen, the whole - // issuance transfer is considered frozen. Only need to check for - // frozen if there is a transfer of funds. + // Check once: if any involved account is frozen, the whole issuance transfer is + // considered frozen. Only need to check for frozen if there is a transfer of funds. + // + // Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of + // the freeze semantics for all changed holders. + // + // Pre-fix330: legacy AMM withdraw only checked individual freeze on the + // destination, not the transitive vault freeze. All other paths (and the AMM + // account itself as sender) did apply the full check. + MPTIssue const issue{mptID}; + auto const legacyAccountFrozen = [&] { + if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue)) + return true; + bool const isReceiver = + !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore; + if (txnType == ttAMM_WITHDRAW && isReceiver) + return false; + return isVaultPseudoAccountFrozen(view, account, issue, 0); + }; + bool const accountFrozen = + fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen(); if (!invalidTransfer && - (isFrozen(view, account, MPTIssue{mptID}) || - !isAuthorized(view, mptID, account, reqAuth))) + (accountFrozen || !isAuthorized(view, mptID, account, reqAuth))) { invalidTransfer = true; } diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 3665850f2d..ff7fec019a 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -251,7 +251,36 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) : tecUNFUNDED_AMM; }; - if (ctx.view.rules().enabled(featureAMMClawback)) + auto const amount = ctx.tx[~sfAmount]; + auto const amount2 = ctx.tx[~sfAmount2]; + auto const ammAccountID = ammSle->getAccountID(sfAccount); + + if (ctx.view.rules().enabled(fixCleanup3_3_0)) + { + // Unified deposit freeze check for both pool assets. + // AMMDeposit is not allowed if either asset is frozen. + auto checkAsset = [&](Asset const& asset) -> TER { + if (auto const ter = requireAuth(ctx.view, asset, accountID, AuthType::WeakAuth)) + { + JLOG(ctx.j.debug()) << "AMM Deposit: account is not authorized, " << asset; + return ter; + } + if (auto const ter = checkDepositFreeze(ctx.view, accountID, ammAccountID, asset)) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: frozen, " << to_string(accountID) << " " << to_string(asset); + return ter; + } + return tesSUCCESS; + }; + + if (auto const ter = checkAsset(ctx.tx[sfAsset])) + return ter; + + if (auto const ter = checkAsset(ctx.tx[sfAsset2])) + return ter; + } + else if (ctx.view.rules().enabled(featureAMMClawback)) { // Check if either of the assets is frozen, AMMDeposit is not allowed // if either asset is frozen @@ -283,10 +312,6 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return ter; } - auto const amount = ctx.tx[~sfAmount]; - auto const amount2 = ctx.tx[~sfAmount2]; - auto const ammAccountID = ammSle->getAccountID(sfAccount); - auto checkAmount = [&](std::optional const& amount, bool checkBalance) -> TER { if (amount) { @@ -301,21 +326,26 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) return ter; // LCOV_EXCL_STOP } - // AMM account or currency frozen - if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset()); - !isTesSuccess(ter)) + if (!ctx.view.rules().enabled(fixCleanup3_3_0)) { - JLOG(ctx.j.debug()) << "AMM Deposit: AMM account or currency is frozen or locked, " - << to_string(accountID); - return ter; - } - // Account frozen - if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset()); - !isTesSuccess(ter)) - { - JLOG(ctx.j.debug()) << "AMM Deposit: account is frozen or locked, " - << to_string(accountID) << " " << to_string(amount->asset()); - return ter; + // AMM account or currency frozen + if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset()); + !isTesSuccess(ter)) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: AMM account or currency is frozen or locked, " + << to_string(accountID); + return ter; + } + // Account frozen + if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset()); + !isTesSuccess(ter)) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: account is frozen or locked, " << to_string(accountID) + << " " << to_string(amount->asset()); + return ter; + } } if (checkBalance) { diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 6cbcfb1e50..f26164c5fe 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -238,21 +238,36 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) << "AMM Withdraw: account is not authorized, " << amount->asset(); return ter; } - // AMM account or currency frozen - if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset()); - !isTesSuccess(ter)) + if (ctx.view.rules().enabled(fixCleanup3_3_0)) { - JLOG(ctx.j.debug()) << "AMM Withdraw: AMM account or currency is frozen or locked, " - << to_string(accountID); - return ter; + if (auto const ret = checkWithdrawFreeze( + ctx.view, ammAccountID, accountID, accountID, amount->asset())) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: frozen, " << to_string(accountID) << " " + << to_string(amount->asset()); + return ret; + } } - // Account frozen - if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset()); - !isTesSuccess(ter)) + else { - JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen or locked, " - << to_string(accountID) << " " << to_string(amount->asset()); - return ter; + // AMM account or currency frozen + if (auto const ter = checkFrozen(ctx.view, ammAccountID, amount->asset()); + !isTesSuccess(ter)) + { + JLOG(ctx.j.debug()) + << "AMM Withdraw: AMM account or currency is frozen or locked, " + << to_string(accountID); + return ter; + } + // Account frozen + if (auto const ter = checkIndividualFrozen(ctx.view, accountID, amount->asset()); + !isTesSuccess(ter)) + { + JLOG(ctx.j.debug()) + << "AMM Withdraw: account is frozen or locked, " << to_string(accountID) + << " " << to_string(amount->asset()); + return ter; + } } } return tesSUCCESS; @@ -302,6 +317,25 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +FreezeHandling +AMMWithdraw::issuerFreezeHandling() const +{ + // When the withdrawer is the issuer of a pool asset, the issuer can + // always receive their own token — even when the pool is frozen. + // Use IgnoreFreeze so ammHolds returns real balances instead of zero. + if (!ctx_.view().rules().enabled(fixCleanup3_3_0)) + return FreezeHandling::ZeroIfFrozen; + + auto const asset1 = Asset{ctx_.tx[sfAsset]}; + auto const asset2 = Asset{ctx_.tx[sfAsset2]}; + if (!asset1.native() && accountID_ == asset1.getIssuer()) + return FreezeHandling::IgnoreFreeze; + if (!asset2.native() && accountID_ == asset2.getIssuer()) + return FreezeHandling::IgnoreFreeze; + + return FreezeHandling::ZeroIfFrozen; +} + std::pair AMMWithdraw::applyGuts(Sandbox& sb) { @@ -329,18 +363,19 @@ AMMWithdraw::applyGuts(Sandbox& sb) auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_); + auto const freezeHandling = issuerFreezeHandling(); + auto const expected = ammHolds( sb, *ammSle, amount ? amount->asset() : std::optional{}, amount2 ? amount2->asset() : std::optional{}, - FreezeHandling::ZeroIfFrozen, + freezeHandling, AuthHandling::ZeroIfUnauthorized, ctx_.journal); if (!expected) return {expected.error(), false}; auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected; - auto const subTxType = ctx_.tx.getFlags() & tfWithdrawSubTx; auto const [result, newLPTokenBalance] = [&, @@ -469,7 +504,7 @@ AMMWithdraw::withdraw( lpTokensAMMBalance, lpTokensWithdraw, tfee, - FreezeHandling::ZeroIfFrozen, + issuerFreezeHandling(), AuthHandling::ZeroIfUnauthorized, isWithdrawAll(ctx_.tx), preFeeBalance_, @@ -756,7 +791,7 @@ AMMWithdraw::equalWithdrawTokens( lpTokens, lpTokensWithdraw, tfee, - FreezeHandling::ZeroIfFrozen, + issuerFreezeHandling(), AuthHandling::ZeroIfUnauthorized, isWithdrawAll(ctx_.tx), preFeeBalance_, diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 76a5493a73..69b28b57af 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -43,6 +43,8 @@ LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx) TER LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) { + auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); + auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0); auto const& tx = ctx.tx; auto const account = tx[sfAccount]; @@ -77,12 +79,21 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) // Cannot transfer a non-transferable Asset if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)) return ret; - // Cannot transfer a frozen Asset - if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) - return ret; - // Pseudo-account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; + + if (fix330Enabled) + { + if (auto const ret = checkDepositFreeze(ctx.view, account, pseudoAccountID, vaultAsset)) + return ret; + } + else + { + if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) + return ret; + + if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return ret; + } + // Cannot transfer unauthorized asset if (auto const ret = requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)) return ret; @@ -92,7 +103,6 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) // `sfCoverAvailable +=` could credit the broker more than the depositor paid Computing it // here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of // failing only in doApply. - bool const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); auto const roundedAmount = [&]() -> STAmount { if (!fix320Enabled) return tx[sfAmount]; diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 426f77cc70..008857a4ad 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -55,6 +55,8 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) TER LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) { + auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); + auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0); auto const& tx = ctx.tx; auto const account = tx[sfAccount]; @@ -103,8 +105,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) // the lsfMPTCanTransfer flag check, so an issuer cannot trap a broker's // first-loss capital. Other transferability checks (IOU NoRipple, freeze, // requireAuth) still apply. - auto const waive = ctx.view.rules().enabled(fixCleanup3_2_0) ? WaiveMPTCanTransfer::Yes - : WaiveMPTCanTransfer::No; + auto const waive = fix320Enabled ? WaiveMPTCanTransfer::Yes : WaiveMPTCanTransfer::No; if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct, waive)) return ret; @@ -125,22 +126,30 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) return ter; - // Check for freezes, unless sending directly to the issuer - if (dstAcct != vaultAsset.getIssuer()) + if (fix330Enabled) { - // Cannot send a frozen Asset - if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; - // Destination account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) + if (auto const ret = + checkWithdrawFreeze(ctx.view, pseudoAccountID, account, dstAcct, vaultAsset)) return ret; } + else + { // Check for freezes, unless sending directly to the issuer + if (dstAcct != vaultAsset.getIssuer()) + { + // Cannot send a frozen Asset + if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return ret; + // Destination account cannot receive if asset is deep frozen + if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) + return ret; + } + } auto const coverAvail = sleBroker->at(sfCoverAvailable); // Cover Rate is in 1/10 bips units auto const currentDebtTotal = sleBroker->at(sfDebtTotal); auto const minimumCover = [&]() { - if (ctx.view.rules().enabled(fixCleanup3_2_0)) + if (fix320Enabled) { return minimumBrokerCover( currentDebtTotal, TenthBips32{sleBroker->at(sfCoverRateMinimum)}, vault); @@ -159,11 +168,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if ((coverAvail - amount) < minimumCover) return tecINSUFFICIENT_FUNDS; + auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer() + ? FreezeHandling::IgnoreFreeze + : FreezeHandling::ZeroIfFrozen; + if (accountHolds( ctx.view, pseudoAccountID, vaultAsset, - FreezeHandling::ZeroIfFrozen, + freezeHandling, AuthHandling::ZeroIfUnauthorized, ctx.j) < amount) return tecINSUFFICIENT_FUNDS; diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp index b70543d280..d5eb80b84d 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp @@ -63,6 +63,9 @@ VaultDeposit::preflight(PreflightContext const& ctx) TER VaultDeposit::preclaim(PreclaimContext const& ctx) { + auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); + auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0); + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); if (!vault) return tecNO_ENTRY; @@ -107,13 +110,21 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - // Cannot deposit inside Vault an Asset frozen for the depositor - if (isFrozen(ctx.view, account, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + if (fix330Enabled) + { + if (auto const ret = checkDepositFreeze(ctx.view, account, vaultAccount, vaultAsset)) + return ret; + } + else + { + // Cannot deposit inside Vault an Asset frozen for the depositor + if (isFrozen(ctx.view, account, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; - // Cannot deposit if the shares of the vault are frozen - if (isFrozen(ctx.view, account, vaultShare)) - return tecLOCKED; + // Cannot deposit if the shares of the vault are frozen + if (isFrozen(ctx.view, account, vaultShare)) + return tecLOCKED; + } if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)) { @@ -141,7 +152,6 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter)) return ter; - bool const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); auto const roundedAmount = fix320Enabled ? roundToVaultScale(amount, vault) : amount; if (fix320Enabled && roundedAmount == beast::kZero) diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 815d8ccd5b..3d30005876 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -65,6 +65,10 @@ VaultWithdraw::preflight(PreflightContext const& ctx) TER VaultWithdraw::preclaim(PreclaimContext const& ctx) { + auto const fix313Enabled = ctx.view.rules().enabled(fixCleanup3_1_3); + auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0); + auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0); + auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID])); if (!vault) return tecNO_ENTRY; @@ -82,8 +86,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // lsfMPTCanTransfer flag check, so an issuer cannot trap depositor funds. // Other transferability checks (IOU NoRipple, freeze, requireAuth) still // apply. - auto const waive = ctx.view.rules().enabled(fixCleanup3_2_0) ? WaiveMPTCanTransfer::Yes - : WaiveMPTCanTransfer::No; + auto const waive = fix320Enabled ? WaiveMPTCanTransfer::Yes : WaiveMPTCanTransfer::No; if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct, waive); !isTesSuccess(ter)) { @@ -100,7 +103,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - if (ctx.view.rules().enabled(fixCleanup3_1_3) && amount.asset() == vaultShare) + if (fix313Enabled && amount.asset() == vaultShare) { // Post-fixCleanup3_1_3: if the user specified shares, convert // to the equivalent asset amount before checking withdrawal @@ -160,15 +163,30 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter)) return ter; - // Cannot withdraw from a Vault an Asset frozen for the destination account - if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) - return ret; - - // Cannot return shares to the vault, if the underlying asset was frozen for - // the submitter - if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare})) - return ret; + if (fix330Enabled) + { + // checkWithdrawFreeze checks the underlying asset on the source + // (vault pseudo-account), the submitter, and the destination. + // A separate share-level freeze check is unnecessary: vault shares + // are issued by the vault pseudo-account, which cannot submit + // MPTokenIssuanceSet to individually lock a holder's MPToken. + // The only way shares become locked is transitively via the + // underlying asset, which checkWithdrawFreeze covers. + if (auto const ret = + checkWithdrawFreeze(ctx.view, vaultAccount, account, dstAcct, vaultAsset)) + return ret; + } + else + { + // Cannot withdraw from a Vault an Asset frozen for the destination account + if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) + return ret; + // Cannot return shares to the vault, if the underlying asset was frozen for + // the submitter + if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare})) + return ret; + } return tesSUCCESS; } @@ -254,8 +272,14 @@ VaultWithdraw::doApply() return tecPATH_DRY; } - if (accountHolds( - view(), accountID_, share, FreezeHandling::ZeroIfFrozen, AuthHandling::IgnoreAuth, j_) < + // Post-fixCleanup3_3_0: preclaim already validated all freeze conditions + // (checkWithdrawFreeze), so IgnoreFreeze avoids a redundant check that + // would incorrectly return zero for vault pseudo-accounts whose shares + // are frozen via a transitively frozen underlying asset. + auto const freezeHandling = view().rules().enabled(fixCleanup3_3_0) + ? FreezeHandling::IgnoreFreeze + : FreezeHandling::ZeroIfFrozen; + if (accountHolds(view(), accountID_, share, freezeHandling, AuthHandling::IgnoreAuth, j_) < sharesRedeemed) { JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares"; @@ -358,10 +382,9 @@ VaultWithdraw::doApply() // else quietly ignore, account balance is not zero } - auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); - associateAsset(*vault, vaultAsset); + auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_); return doWithdraw( view(), ctx_.tx, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_); } diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 5b576b41e4..9ff6c65c17 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -18,10 +18,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -46,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -726,13 +729,14 @@ private: ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED)); - if (!features[featureAMMClawback]) + // Post-fixCleanup3_3_0 a locked holder cannot deposit the other + // (non-locked) token either, matching featureAMMClawback. + if (!features[featureAMMClawback] && !features[fixCleanup3_3_0]) { ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt); } else { - // Carol can not deposit non-frozen token either ammAlice.deposit( carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED)); } @@ -753,8 +757,15 @@ private: gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); env.close(); - // Can deposit non-frozen token - ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); + // Post-fixCleanup3_3_0 the deposit checks both pool assets, so the + // non-frozen token cannot be deposited while the AMM's USD is frozen. + ammAlice.deposit( + carol_, + btc(100), + std::nullopt, + std::nullopt, + std::nullopt, + features[fixCleanup3_3_0] ? Ter(tecFROZEN) : Ter(tesSUCCESS)); // Cannot deposit frozen token ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); @@ -773,8 +784,15 @@ private: // Individually lock AMM btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); - // Can deposit non-frozen token - ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt); + // Post-fixCleanup3_3_0 the non-locked token cannot be deposited + // while the AMM's BTC is locked. + ammAlice.deposit( + carol_, + USD(100), + std::nullopt, + std::nullopt, + std::nullopt, + features[fixCleanup3_3_0] ? Ter(tecLOCKED) : Ter(tesSUCCESS)); // Can not deposit locked token ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED)); @@ -788,7 +806,9 @@ private: ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); } - // Individually lock MPT (AMM) account with MPT/MPT AMM + // Individually lock MPT (AMM) account with MPT/MPT AMM. + // This block always runs with all amendments (incl. fixCleanup3_3_0), + // so the deposit checks both pool assets unconditionally. { Env env{*this}; env.fund(XRP(10'000), gw_, alice_, carol_); @@ -826,8 +846,10 @@ private: // Individually lock MPT BTC (AMM) account btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); - // Can deposit non-locked token USD - ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt); + // Post-fixCleanup3_3_0 the non-locked token USD cannot be deposited + // while the AMM's BTC is locked. + ammAlice.deposit( + carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED)); // Can not deposit locked token BTC ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED)); @@ -843,8 +865,10 @@ private: // Individually Lock MPT USD (AMM) account usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); - // Can deposit non-locked token BTC - ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); + // Post-fixCleanup3_3_0 the non-locked token BTC cannot be deposited + // while the AMM's USD is locked. + ammAlice.deposit( + carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED)); // Can not deposit locked token USD ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED)); @@ -876,7 +900,9 @@ private: AMM amm(env, alice, XRP(10'000), btc(10'000)); env.close(); - if (!features[featureAMMClawback]) + // Post-fixCleanup3_3_0 the deposit requires authorization for both + // pool assets, so the unauthorized MPT blocks the XRP deposit too. + if (!features[featureAMMClawback] && !features[fixCleanup3_3_0]) { amm.deposit(carol, XRP(10), std::nullopt, std::nullopt, std::nullopt); } @@ -6863,6 +6889,173 @@ private: BEAST_EXPECT(!amm.ammExists()); } + void + testAMMWithVaultShares() + { + testcase("AMM with vault shares — underlying freeze blocks share withdrawal"); + using namespace jtx; + // AMMTestBase::testableAmendments() strips featureSingleAssetVault, + // but vault shares require it. Use the global jtx set directly. + FeatureBitset const all{jtx::testableAmendments()}; + + // When alice's underlying asset is individually frozen: + // + // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM + // pseudo-account's underlying, not alice's — deposit is allowed. + // Pre-fix: featureAMMClawback calls isFrozen(alice, share) which + // descends via isVaultPseudoAccountFrozen(alice,...) and finds the + // frozen underlying — deposit is blocked. + // + // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with + // checkDeepFrozen(alice, share) which calls isFrozen(alice, share) + // and finds the frozen underlying — withdrawal is blocked. + // Pre-fix: the old path only checks the AMM account's MPToken lock, + // which is unset — withdrawal succeeds. + + auto runIOU = [&](FeatureBitset const& features) { + bool const fix330 = features[fixCleanup3_3_0]; + Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; + + env.fund(XRP(100'000), gw_, alice_); + env(fset(gw_, asfDefaultRipple)); + env.close(); + + PrettyAsset const iou = gw_["IOU"]; + env.trust(iou(1'000'000), alice_); + env(pay(gw_, alice_, iou(10'000))); + env.close(); + + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); + env(createTx); + env.close(); + + // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6) + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)})); + env.close(); + + auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); + // Use half the shares for the AMM; alice keeps the other half. + STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; + // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8 + AMM amm{env, alice_, XRP(100), shareAmt}; + env.close(); + + // Freeze alice's IOU trustline (individual freeze on underlying). + env(trust(gw_, iou(0), alice_, tfSetFreeze)); + env.close(); + + // post-fix330: checkDepositFreeze checks AMM pseudo's underlying + // (not alice's) → deposit is allowed + // pre-fix330: featureAMMClawback path calls isFrozen(alice, share) + // which descends to alice's frozen IOU → tecLOCKED + amm.deposit( + {.account = alice_, + .asset1In = XRP(1), + .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); + + // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share) + // descends to alice's frozen IOU → tecLOCKED + // pre-fix330: the AMM pseudo-account is not authorized for the + // share's underlying (requireAuth recurses share→IOU + // and the AMM holds no IOU trustline), so + // accountHolds(ZeroIfUnauthorized) reports the pool's + // share balance as 0 and the withdrawal math fails → + // tecAMM_FAILED. Vault shares deposited into an AMM are + // only withdrawable once fixCleanup3_3_0 exempts the + // pseudo-account from the recursive auth check. + amm.withdraw( + {.account = alice_, + .tokens = 1'000, + .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); + + env(trust(gw_, iou(0), alice_, tfClearFreeze)); + env.close(); + + // Lifting the freeze lets the deposit through in both cases. The + // withdrawal only succeeds post-fix330; pre-fix330 the share balance + // remains inaccessible to the unauthorized pseudo-account, so the + // shares stay stuck → tecAMM_FAILED. + amm.deposit({.account = alice_, .asset1In = XRP(1)}); + amm.withdraw( + {.account = alice_, + .tokens = 1'000, + .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); + }; + + runIOU(all); + runIOU(all - fixCleanup3_3_0); + + auto runMPT = [&](FeatureBitset const& features) { + bool const fix330 = features[fixCleanup3_3_0]; + // Expected freeze failures fire invariant checks that log at Error; + // silence them so the test output stays clean. + Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; + + env.fund(XRP(100'000), gw_, alice_); + env.close(); + + MPTTester mptt{env, gw_, kMptInitNoFund}; + mptt.create({.flags = kMptDexFlags | tfMPTCanLock}); + PrettyAsset const mpt = mptt.issuanceID(); + mptt.authorize({.account = alice_}); + env(pay(gw_, alice_, mpt(30'000))); + env.close(); + + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt}); + env(createTx); + env.close(); + + // 20000 MPT → 20000 vault shares (MPT vault scale = 0) + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)})); + env.close(); + + auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); + // Use half the shares for the AMM; alice keeps the other half. + // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6 + STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000}; + AMM amm{env, alice_, XRP(100), shareAmt}; + env.close(); + + // Lock alice's underlying MPT (individual lock). + mptt.set({.holder = alice_, .flags = tfMPTLock}); + + // Same pre/post-fix330 semantics as the IOU case above. + amm.deposit( + {.account = alice_, + .asset1In = XRP(1), + .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); + + // {.tokens = 1'000} → frac = 1000/1e6 = 0.001 + // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10 + // post-fix330: checkWithdrawFreeze sees alice's locked underlying + // MPT via the share → tecLOCKED. + // pre-fix330: the AMM pseudo-account is unauthorized for the + // share's underlying MPT (it holds no underlying + // MPToken), so accountHolds(ZeroIfUnauthorized) zeros + // the pool's share balance and the math fails → + // tecAMM_FAILED. + amm.withdraw( + {.account = alice_, + .tokens = 1'000, + .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); + + mptt.set({.holder = alice_, .flags = tfMPTUnlock}); + + // Unlocking lets the deposit through; the withdrawal only succeeds + // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED). + amm.deposit({.account = alice_, .asset1In = XRP(1)}); + amm.withdraw( + {.account = alice_, + .tokens = 1'000, + .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); + }; + + runMPT(all); + runMPT(all - fixCleanup3_3_0); + } + void testAMMDepositWithFrozenAssets() { @@ -7085,8 +7278,8 @@ private: FeatureBitset const all{jtx::testableAmendments()}; testInstanceCreate(); testInvalidInstance(); - testInvalidDeposit(all); - testInvalidDeposit(all - featureAMMClawback); + for (auto const& f : jtx::amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}, all)) + testInvalidDeposit(f); testDeposit(); testInvalidWithdraw(); testWithdraw(); @@ -7113,6 +7306,7 @@ private: testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); testAMMDepositWithFrozenAssets(); + testAMMWithVaultShares(); testAutoDelete(); } }; diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index b01d58ddff..3672c4a68a 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +86,13 @@ private: return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol; } + // Seed from the local testableAmendments() which strips SAV and Lending. + static std::vector + amendmentCombinations(std::initializer_list features) + { + return jtx::amendmentCombinations(features, testableAmendments()); + } + void testInstanceCreate() { @@ -746,18 +754,19 @@ private: testAMM( [&](AMM& ammAlice, Env& env) { env(fset(gw_, asfGlobalFreeze)); - if (!features[featureAMMClawback]) + auto const freezeBlocksAll = + features[featureAMMClawback] || features[fixCleanup3_3_0]; + if (!freezeBlocksAll) { // If the issuer set global freeze, the holder still can - // deposit the other non-frozen token when AMMClawback is - // not enabled. + // deposit the other non-frozen token when neither + // AMMClawback nor fixCleanup3_3_0 is enabled. ammAlice.deposit(carol_, XRP(100)); } else { // If the issuer set global freeze, the holder cannot - // deposit the other non-frozen token when AMMClawback is - // enabled. + // deposit the other non-frozen token. ammAlice.deposit( carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); } @@ -786,16 +795,18 @@ private: [&](AMM& ammAlice, Env& env) { env(trust(gw_, carol_["USD"](0), tfSetFreeze)); env.close(); - if (!features[featureAMMClawback]) + auto const freezeBlocksAll = + features[featureAMMClawback] || features[fixCleanup3_3_0]; + if (!freezeBlocksAll) { - // Can deposit non-frozen token if AMMClawback is not - // enabled + // Can deposit non-frozen token if neither AMMClawback + // nor fixCleanup3_3_0 is enabled ammAlice.deposit(carol_, XRP(100)); } else { // Cannot deposit non-frozen token if the other token is - // frozen when AMMClawback is enabled + // frozen ammAlice.deposit( carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); } @@ -810,8 +821,18 @@ private: STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); env.close(); - // Can deposit non-frozen token - ammAlice.deposit(carol_, XRP(100)); + // Post-fixCleanup3_3_0: checkDepositFreeze checks both pool + // assets against the AMM account, so depositing the + // non-frozen token is also blocked. + if (!features[fixCleanup3_3_0]) + { + ammAlice.deposit(carol_, XRP(100)); + } + else + { + ammAlice.deposit( + carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); + } ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); @@ -861,11 +882,10 @@ private: AMM amm(env, alice_, XRP(10), gw_["USD"](10), Ter(tesSUCCESS)); env.close(); - if (features[featureAMMClawback]) + if (features[featureAMMClawback] || features[fixCleanup3_3_0]) { - // if featureAMMClawback is enabled, bob_ can not deposit XRP - // because he's not authorized to hold the paired token - // gw_["USD"]. + // bob_ can not deposit XRP because he's not authorized to + // hold the paired token gw_["USD"]. amm.deposit( bob_, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tecNO_AUTH)); } @@ -1734,36 +1754,57 @@ private: }); // Globally frozen asset - testAMM([&](AMM& ammAlice, Env& env) { - ammAlice.deposit({.account = gw_, .asset1In = USD(1'000), .asset2In = XRP(1'000)}); - env(fset(gw_, asfGlobalFreeze)); - env.close(); - // Can withdraw non-frozen token - for (auto const& account : {alice_, gw_}) - { - ammAlice.withdraw(account, XRP(100)); - ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); - ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); - } - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0); + ammAlice.deposit({.account = gw_, .asset1In = USD(1'000), .asset2In = XRP(1'000)}); + env(fset(gw_, asfGlobalFreeze)); + env.close(); + // Can withdraw non-frozen token + for (auto const& account : {alice_, gw_}) + { + ammAlice.withdraw(account, XRP(100)); + // Post-fixCleanup3_3_0 the issuer can withdraw their own + // frozen token from the pool. + auto const frozenErr = + (fix330 && account == gw_) ? Ter(tesSUCCESS) : Ter(tecFROZEN); + ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, frozenErr); + ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, frozenErr); + } + }, + std::nullopt, + 0, + std::nullopt, + amendmentCombinations({fixCleanup3_3_0})); // Individually frozen (AMM) account - testAMM([&](AMM& ammAlice, Env& env) { - env(trust(gw_, alice_["USD"](0), tfSetFreeze)); - env.close(); - // Can withdraw non-frozen token - ammAlice.withdraw(alice_, XRP(100)); - ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); - ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); - env(trust(gw_, alice_["USD"](0), tfClearFreeze)); - // Individually frozen AMM - env(trust( - gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); - // Can withdraw non-frozen token - ammAlice.withdraw(alice_, XRP(100)); - ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); - ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0); + env(trust(gw_, alice_["USD"](0), tfSetFreeze)); + env.close(); + // Can withdraw non-frozen token + ammAlice.withdraw(alice_, XRP(100)); + // Post-fixCleanup3_3_0 regular freeze no longer blocks + // self-withdrawal; only deep freeze does. + auto const indivFreezeErr = fix330 ? Ter(tesSUCCESS) : Ter(tecFROZEN); + ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, indivFreezeErr); + ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, indivFreezeErr); + env(trust(gw_, alice_["USD"](0), tfClearFreeze)); + // Individually frozen AMM — still blocked regardless of + // fixCleanup3_3_0 because the AMM account itself is frozen. + env(trust( + gw_, + STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, + tfSetFreeze)); + ammAlice.withdraw(alice_, XRP(100)); + ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); + ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); + }, + std::nullopt, + 0, + std::nullopt, + amendmentCombinations({fixCleanup3_3_0})); // Carol withdraws more than she owns testAMM([&](AMM& ammAlice, Env&) { @@ -6659,11 +6700,11 @@ private: }); } - if (features[featureAMMClawback]) + if (features[featureAMMClawback] || features[fixCleanup3_3_0]) { // Deposit one asset which is not the frozen token, - // but the other asset is frozen. We should get tecFROZEN error - // when feature AMMClawback is enabled. + // but the other asset is frozen. tecFROZEN when either + // AMMClawback or fixCleanup3_3_0 is enabled. Env env(*this, features); testAMMDeposit(env, [&](AMM& amm) { amm.deposit( @@ -6673,8 +6714,8 @@ private: else { // Deposit one asset which is not the frozen token, - // but the other asset is frozen. We will get tecSUCCESS - // when feature AMMClawback is not enabled. + // but the other asset is frozen. tesSUCCESS only when + // neither AMMClawback nor fixCleanup3_3_0 is enabled. Env env(*this, features); testAMMDeposit(env, [&](AMM& amm) { amm.deposit( @@ -7197,8 +7238,8 @@ private: FeatureBitset const all{testableAmendments()}; testInvalidInstance(); testInstanceCreate(); - testInvalidDeposit(all); - testInvalidDeposit(all - featureAMMClawback); + for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback})) + testInvalidDeposit(f); testDeposit(); testInvalidWithdraw(); testWithdraw(); @@ -7248,8 +7289,8 @@ private: testAMMClawback(all - featureAMMClawback - featureSingleAssetVault); testAMMClawback(all - featureAMMClawback); testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); - testAMMDepositWithFrozenAssets(all); - testAMMDepositWithFrozenAssets(all - featureAMMClawback); + for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback})) + testAMMDepositWithFrozenAssets(f); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 804cfaebfc..6aa17c7840 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -4598,6 +4599,210 @@ class Invariants_test : public beast::unit_test::Suite } } } + + // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen + // on fixCleanup3_3_0. Pre-amendment, vault-share transfers are allowed + // even when the underlying asset is individually frozen for the sender; + // post-amendment they are blocked. + { + Account const gw{"gw"}; + MPTID shareID{}; + + auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + env.fund(XRP(1'000), gw); + env.trust(gw["IOU"](10'000), a1); + env.trust(gw["IOU"](10'000), a2); + env.close(); + env(pay(gw, a1, gw["IOU"](500))); + env(pay(gw, a2, gw["IOU"](500))); + env.close(); + + PrettyAsset const iou = gw["IOU"]; + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou}); + env(createTx); + env.close(); + // Both a1 and a2 deposit IOU, each receiving vault shares. + env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)})); + env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)})); + env.close(); + + shareID = env.le(vaultKeylet)->at(sfShareMPTID); + + // Freeze a2's IOU trustline from the issuer side. + // a2 is the receiver in the simulated AMM withdraw; the + // distinction under test is that pre-fix330 the invariant + // does not apply the transitive vault freeze to receivers. + env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); + env.close(); + return true; + }; + + // Simulate a vault-share transfer: a1 sends 10 shares to a2. + auto const precheck = + [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool { + auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id())); + auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id())); + if (!sle1 || !sle2) + return false; + (*sle1)[sfMPTAmount] -= 10; + (*sle2)[sfMPTAmount] += 10; + ac.view().update(sle1); + ac.view().update(sle2); + return true; + }; + + // post-fixCleanup3_3_0: full isFrozen() applies to all holders; + // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen → + // invalidTransfer → invariant fires. + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose); + + // pre-fixCleanup3_3_0: legacy AMM withdraw only checked + // checkIndividualFrozen on the destination, not the transitive + // vault freeze; a2 as receiver is exempt → invariant passes. + doInvariantCheck( + Env{*this, defaultAmendments() - fixCleanup3_3_0}, + {}, + precheck, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tesSUCCESS, tesSUCCESS}, + preclose); + } + + // Side-specific vault-share AMM_WITHDRAW invariant tests. + // Both cases use a real vault (IOU underlying) and a real AMM whose + // pool includes vault shares. precheck simulates an AMM_WITHDRAW by + // transferring 10 vault shares from the AMM pseudo-account to a2. + { + MPTID shareID{}; + AccountID ammAcctID{}; + AccountID vaultPseudoID{}; + Account const gw{"gw"}; + + // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares + // to a2. The AMM pseudo is the sender (decreasing balance); + // a2 is the receiver (increasing balance). + auto const precheck2 = + [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool { + auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID)); + auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id())); + if (!sleAMM || !sle2) + return false; + (*sleAMM)[sfMPTAmount] -= 10; + (*sle2)[sfMPTAmount] += 10; + ac.view().update(sleAMM); + ac.view().update(sle2); + return true; + }; + + // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and + // creates an AMM with XRP + 100 vault shares, giving the AMM + // pseudo-account a vault-share MPToken balance. + auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool { + env.fund(XRP(1'000), gw); + env(fset(gw, asfDefaultRipple)); + env.close(); + + env.trust(gw["IOU"](10'000), a1); + env.trust(gw["IOU"](10'000), a2); + env.close(); + env(pay(gw, a1, gw["IOU"](1'000))); + env(pay(gw, a2, gw["IOU"](500))); + env.close(); + + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]}); + env(createTx); + env.close(); + + env(vault.deposit( + {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)})); + env(vault.deposit( + {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)})); + env.close(); + + shareID = env.le(vaultKeylet)->at(sfShareMPTID); + vaultPseudoID = env.le(vaultKeylet)->at(sfAccount); + + // a1 creates AMM with XRP + 100 vault shares; the AMM + // pseudo-account receives an MPToken record for shareID. + AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100}); + ammAcctID = amm.ammAccount(); + return true; + }; + + // Case 1: freeze the vault pseudo-account's IOU trustline. + // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo, + // ammAcct}, IOU); since vaultPseudo is frozen it returns true. The + // AMM sender has a decreasing balance (not a receiver) so it is + // never exempt from the check — invariant fires both pre- and + // post-fixCleanup3_3_0. + auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool { + if (!setupVaultAMM(a1, a2, env)) + return false; + env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze)); + env.close(); + return true; + }; + + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck2, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose3); + + doInvariantCheck( + Env{*this, defaultAmendments() - fixCleanup3_3_0}, + {{"invalid MPToken transfer between holders"}}, + precheck2, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose3); + + // Case 2: freeze a2's (receiver's) IOU trustline. + // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2}, + // IOU) → true. The AMM sender's check passes (vaultPseudo and + // ammAcct are not frozen). Pre-fix330: receiver is exempt from + // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes. + // Post-fix330: full isFrozen() applied to a2 → fires. + auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool { + if (!setupVaultAMM(a1, a2, env)) + return false; + env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); + env.close(); + return true; + }; + + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck2, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose4); + + doInvariantCheck( + Env{*this, defaultAmendments() - fixCleanup3_3_0}, + {}, + precheck2, + XRPAmount{}, + STTx{ttAMM_WITHDRAW, [](STObject&) {}}, + {tesSUCCESS, tesSUCCESS}, + preclose4); + } } void diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 671f98a901..bdc950eeff 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -936,11 +936,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), Ter(tecINSUFFICIENT_FUNDS)); - - // preclaim: tecFROZEN - env(fset(issuer, asfGlobalFreeze)); - env.close(); - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), Ter(tecFROZEN)); + // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes } else { @@ -966,35 +962,20 @@ class LoanBroker_test : public beast::unit_test::Suite // preclaim: tecDST_TAG_NEEDED Account const dest{"dest"}; env.fund(XRP(1'000), dest); + env(fset(dest, asfRequireDest)); - env.close(); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), kDestination(dest), Ter(tecDST_TAG_NEEDED)); + env(fclear(dest, asfRequireDest)); // preclaim: tecNO_PERMISSION - env(fclear(dest, asfRequireDest)); env(fset(dest, asfDepositAuth)); - env.close(); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), kDestination(dest), Ter(tecNO_PERMISSION)); - - // preclaim: tecFROZEN - env(trust(dest, asset(1'000))); env(fclear(dest, asfDepositAuth)); - env(fset(issuer, asfGlobalFreeze)); - env.close(); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - kDestination(dest), - Ter(tecFROZEN)); - - // preclaim:: tecFROZEN (deep frozen) - env(fclear(issuer, asfGlobalFreeze)); - env(trust(issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze)); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - kDestination(dest), - Ter(tecFROZEN)); + // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes // preclaim: tecPSEUDO_ACCOUNT env(coverWithdraw(alice, brokerKeylet.key, asset(10)), @@ -1784,6 +1765,444 @@ class LoanBroker_test : public beast::unit_test::Suite BEAST_EXPECT(aliceBalanceAfter == aliceBalanceBefore); } + void + testCoverDepositFreezes() + { + using namespace jtx; + using namespace loanBroker; + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + + // === IOU === + { + testcase("LoanBrokerCoverDeposit IOU freeze checks"); + Env env(*this); + Vault const vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env(trust(alice, issuer["IOU"](1'000'000))); + env.close(); + PrettyAsset const asset(issuer["IOU"]); + env(pay(issuer, alice, asset(100'000))); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + Account const brokerPseudo("pseudo", broker->at(sfAccount)); + + env(coverDeposit(alice, brokerKeylet.key, asset(10))); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Global freeze + env(fset(issuer, asfGlobalFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN)); + env(fclear(issuer, asfGlobalFreeze)); + + // Source regular freeze + env(trust(issuer, asset(0), alice, tfSetFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN)); + env(trust(issuer, asset(0), alice, tfClearFreeze)); + + // Source deep freeze + env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN)); + env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze)); + + // Pseudo regular freeze — post-fix blocks, pre-fix allows (BUG) + TER const pseudoTer = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS); + env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(pseudoTer)); + env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze)); + + // Pseudo deep freeze + env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN)); + env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze)); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + // === MPT === + { + testcase("LoanBrokerCoverDeposit MPT lock checks"); + Env env(*this); + Vault const vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mpt{mptt.issuanceID()}; + + mptt.authorize({.account = alice}); + env(pay(issuer, alice, mpt(100'000))); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt}); + env(tx); + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + Account const brokerPseudo("pseudo", broker->at(sfAccount)); + + env(coverDeposit(alice, brokerKeylet.key, mpt(10))); + env.close(); + + // For MPT isDeepFrozen == isFrozen, so all locks block in + // both pre- and post-fix. No behavioral difference. + auto runTests = [&]() { + // Global lock + mptt.set({.flags = tfMPTLock}); + env.close(); + env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED)); + mptt.set({.flags = tfMPTUnlock}); + env.close(); + + // Source (alice) individual lock + mptt.set({.holder = alice, .flags = tfMPTLock}); + env.close(); + env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED)); + mptt.set({.holder = alice, .flags = tfMPTUnlock}); + env.close(); + + // Pseudo individual lock + mptt.set({.holder = brokerPseudo, .flags = tfMPTLock}); + env.close(); + env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED)); + mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock}); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + } + + // Focused demonstration: a cover-withdraw submitter under a regular + // individual IOU freeze can still withdraw to themselves (self-withdrawal). + // + // Pre-fixCleanup3_3_0: the old code only checked the pseudo-account source + // and the destination for deep-freeze; it did not check the submitter's + // individual freeze at all. Self-withdrawal therefore always succeeded. + // Post-fixCleanup3_3_0: checkWithdrawFreeze explicitly skips the submitter + // freeze check when submitter == destination, preserving the same result. + void + testCoverWithdrawSelfWhileFrozen() + { + testcase("LoanBrokerCoverWithdraw IOU self-withdrawal while individually frozen"); + + using namespace jtx; + using namespace loanBroker; + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const dest{"dest"}; + Env env{*this}; + Vault const vault{env}; + + env.fund(XRP(100'000), issuer, alice, dest); + env(trust(alice, issuer["IOU"](1'000'000))); + env(trust(dest, issuer["IOU"](1'000'000))); + env.close(); + + PrettyAsset const asset(issuer["IOU"]); + env(pay(issuer, alice, asset(100'000))); + env.close(); + + auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(vaultTx); + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + env(coverDeposit(alice, brokerKeylet.key, asset(10))); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Set a regular individual freeze on alice's IOU trustline. + env(trust(issuer, asset(0), alice, tfSetFreeze)); + env.close(); + + // Self-withdrawal: submitter == destination (no sfDestination in tx). + // Both pre- and post-fixCleanup3_3_0 this succeeds: + // pre-fix: old code never checked the submitter's freeze. + // post-fix: checkWithdrawFreeze skips submitter when submitter==dst. + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS)); + + // Withdrawal to a third party is blocked by the submitter freeze + // under fixCleanup3_3_0; pre-fix it was not checked. + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS))); + + env(trust(issuer, asset(0), alice, tfClearFreeze)); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + void + testCoverWithdrawFreezes() + { + using namespace jtx; + using namespace loanBroker; + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + + // === IOU === + { + testcase("LoanBrokerCoverWithdraw IOU freeze checks"); + Env env(*this); + Vault const vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env(trust(alice, issuer["IOU"](1'000'000))); + env.close(); + PrettyAsset const asset(issuer["IOU"]); + env(pay(issuer, alice, asset(100'000))); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + Account const brokerPseudo("pseudo", broker->at(sfAccount)); + + env(coverDeposit(alice, brokerKeylet.key, asset(10))); + env.close(); + + Account const dest{"dest"}; + env.fund(XRP(1'000), dest); + env(trust(dest, asset(1'000))); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + TER const expectedTec = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS); + + // Global freeze + env(fset(issuer, asfGlobalFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(tecFROZEN)); + env(fclear(issuer, asfGlobalFreeze)); + + // Source (pseudo) regular freeze + env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(tecFROZEN)); + env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze)); + + // Source (pseudo) deep freeze + env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(tecFROZEN)); + env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze)); + + // Submitter regular freeze → dest + env(trust(issuer, asset(0), alice, tfSetFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(expectedTec)); + // Submitter regular freeze → self: always allowed + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS)); + env(trust(issuer, asset(0), alice, tfClearFreeze)); + env(coverDeposit( + alice, brokerKeylet.key, asset(isTesSuccess(expectedTec) ? 2 : 1))); + + // Submitter deep freeze → dest + env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(expectedTec)); + // Submitter deep freeze → self: blocked (checkDeepFrozen) + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN)); + env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze)); + if (isTesSuccess(expectedTec)) + env(coverDeposit(alice, brokerKeylet.key, asset(1))); + + // Destination regular freeze: only deep freeze blocks + env(trust(issuer, asset(0), dest, tfSetFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(tesSUCCESS)); + env(trust(issuer, asset(0), dest, tfClearFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1))); + + // Destination deep freeze + env(trust(issuer, asset(0), dest, tfSetFreeze | tfSetDeepFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(dest), + Ter(tecFROZEN)); + env(trust(issuer, asset(0), dest, tfClearFreeze | tfClearDeepFreeze)); + + // Submitter frozen → issuer: bypasses all freeze checks + env(trust(issuer, asset(0), alice, tfSetFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(1)), + kDestination(issuer), + Ter(tesSUCCESS)); + env(trust(issuer, asset(0), alice, tfClearFreeze)); + env(coverDeposit(alice, brokerKeylet.key, asset(1))); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + // === MPT === + { + testcase("LoanBrokerCoverWithdraw MPT lock checks"); + Env env(*this); + Vault const vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mpt{mptt.issuanceID()}; + + mptt.authorize({.account = alice}); + env(pay(issuer, alice, mpt(100'000))); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt}); + env(tx); + env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + Account const brokerPseudo("pseudo", broker->at(sfAccount)); + + env(coverDeposit(alice, brokerKeylet.key, mpt(10))); + env.close(); + + Account const dest{"dest"}; + env.fund(XRP(1'000), dest); + mptt.authorize({.account = dest}); + env.close(); + + auto runTests = [&]() { + auto const withFix = env.current()->rules().enabled(fixCleanup3_3_0); + // Only submitter-to-dest differs: post-fix blocks, pre-fix + // doesn't (BUG). All other locks block in both because for + // MPT isDeepFrozen == isFrozen. + TER const submitterToDest = withFix ? TER(tecLOCKED) : TER(tesSUCCESS); + + // Global lock + mptt.set({.flags = tfMPTLock}); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), + kDestination(dest), + Ter(tecLOCKED)); + mptt.set({.flags = tfMPTUnlock}); + env.close(); + + // Source (pseudo) individual lock + mptt.set({.holder = brokerPseudo, .flags = tfMPTLock}); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), + kDestination(dest), + Ter(tecLOCKED)); + mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock}); + env.close(); + + // Submitter individual lock → dest + mptt.set({.holder = alice, .flags = tfMPTLock}); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), + kDestination(dest), + Ter(submitterToDest)); + // Submitter individual lock → self: blocked + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED)); + mptt.set({.holder = alice, .flags = tfMPTUnlock}); + env.close(); + if (isTesSuccess(submitterToDest)) + env(coverDeposit(alice, brokerKeylet.key, mpt(1))); + env.close(); + + // Dest individual lock: blocked + mptt.set({.holder = dest, .flags = tfMPTLock}); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), + kDestination(dest), + Ter(tecLOCKED)); + mptt.set({.holder = dest, .flags = tfMPTUnlock}); + env.close(); + + // Submitter locked → issuer: bypasses all freeze checks + mptt.set({.holder = alice, .flags = tfMPTLock}); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), + kDestination(issuer), + Ter(tesSUCCESS)); + mptt.set({.holder = alice, .flags = tfMPTUnlock}); + env(coverDeposit(alice, brokerKeylet.key, mpt(1))); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + } + void testRIPD4274IOU() { @@ -2244,6 +2663,13 @@ public: void run() override { + testInvalidLoanBrokerCoverClawback(); + testInvalidLoanBrokerCoverDeposit(); + testInvalidLoanBrokerCoverWithdraw(); + testCoverDepositFreezes(); + testCoverWithdrawFreezes(); + testCoverWithdrawSelfWhileFrozen(); + testCoverPrecisionGuard(); testLoanBrokerSetDebtMaximum(); @@ -2251,9 +2677,6 @@ public: testDisabled(); testLifecycle(); - testInvalidLoanBrokerCoverClawback(); - testInvalidLoanBrokerCoverDeposit(); - testInvalidLoanBrokerCoverWithdraw(); testInvalidLoanBrokerDelete(); testInvalidLoanBrokerSet(); testRequireAuth(); @@ -2268,7 +2691,6 @@ public: testLoanBrokerDeleteFrozenIOU(all_); testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0); - // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 1b0dd414e1..56a7cab47a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -92,23 +92,6 @@ protected: // even if they are set to unsupported. FeatureBitset const all_{jtx::testableAmendments()}; - - // All 2^N permutations of `all_` with each subset of the given features - // excluded. The first entry is always `all_` itself (empty exclusion); - // the last excludes every feature in the list. - std::vector - amendmentCombinations(std::initializer_list features) const - { - std::vector result{all_}; - for (auto const& f : features) - { - auto const n = result.size(); - for (std::size_t i = 0; i < n; ++i) - result.push_back(result[i] - f); - } - return result; - } - std::string const iouCurrency_{"IOU"}; void @@ -8624,8 +8607,8 @@ public: run() override { runAmendmentIndependent(); - for (auto const& features : - amendmentCombinations({fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2})) + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) runAmendmentSensitive(features); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index bfb80d3e36..323184aa36 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -7461,21 +7461,42 @@ class MPToken_test : public beast::unit_test::Suite // MPTLock is set usd.set({.flags = tfMPTLock}); - // carol and issuer can't withdraw - for (auto const& account : {carol, gw}) - { - amm.withdraw( - {.account = account, - .asset1Out = usd(1), - .asset2Out = eur(1), - .err = Ter(tecLOCKED)}); - amm.withdraw({.account = account, .tokens = 1'000, .err = Ter(tecLOCKED)}); - // can single withdraw another asset - amm.withdraw( - {.account = account, - .asset1Out = eur(1), - .assets = std::make_pair(eur, usd)}); - } + auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0); + + // carol can't withdraw the locked token (any withdrawal type) + amm.withdraw( + {.account = carol, + .asset1Out = usd(1), + .asset2Out = eur(1), + .err = Ter(tecLOCKED)}); + amm.withdraw({.account = carol, .tokens = 1'000, .err = Ter(tecLOCKED)}); + // can single withdraw the non-locked asset + amm.withdraw( + {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + + // post-fixCleanup3_3_0 the issuer can redeem even when locked. + // Each successful withdrawal burns LP tokens, so replenish between + // each type to keep gw's LP balance stable. + auto const gwLockErr = fix330 ? Ter(tesSUCCESS) : Ter(tecLOCKED); + auto const replenish = [&](IOUAmount tokens) { + usd.set({.flags = tfMPTUnlock}); + amm.deposit({.account = gw, .tokens = tokens}); + usd.set({.flags = tfMPTLock}); + }; + + amm.withdraw( + {.account = gw, .asset1Out = usd(1), .asset2Out = eur(1), .err = gwLockErr}); + if (fix330) + replenish(IOUAmount{1}); + + amm.withdraw({.account = gw, .tokens = 1'000, .err = gwLockErr}); + if (fix330) + replenish(IOUAmount{1'000}); + + // can single withdraw the non-locked asset + amm.withdraw( + {.account = gw, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)}); + usd.set({.flags = tfMPTUnlock}); // MPTRequireAuth is set diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 4643bc8555..6fefcfc404 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1573,6 +1573,7 @@ class Vault_test : public beast::unit_test::Suite bool enableClawback = true; bool requireAuth = true; int initialXRP = 1000; + FeatureBitset features = testableAmendments(); }; auto testCase = [this]( @@ -1585,7 +1586,7 @@ class Vault_test : public beast::unit_test::Suite Vault& vault, MPTTester& mptt)> test, CaseArgs args = {}) { - Env env{*this, testableAmendments()}; + Env env{*this, args.features}; Account const issuer{"issuer"}; Account const owner{"owner"}; Account const depositor{"depositor"}; @@ -1632,6 +1633,8 @@ class Vault_test : public beast::unit_test::Suite env(tx, Ter(tecNO_ENTRY)); }); + // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze + testCase([this]( Env& env, Account const& issuer, @@ -1646,81 +1649,6 @@ class Vault_test : public beast::unit_test::Suite env(tx, Ter(tecLOCKED)); }); - testCase([this]( - Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Asset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT global lock blocks deposit"); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - mptt.set({.account = issuer, .flags = tfMPTLock}); - env.close(); - - tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx, Ter{tecLOCKED}); - env.close(); - - // Can delete empty vault, even if global lock - tx = vault.del({.owner = owner, .id = keylet.key}); - env(tx); - }); - - testCase([this]( - Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Asset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT global lock blocks withdrawal"); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx); - env.close(); - - // Check that the OutstandingAmount field of MPTIssuance - // accounts for the issued shares. - auto v = env.le(keylet); - BEAST_EXPECT(v); - MPTID const share = (*v)[sfShareMPTID]; - auto issuance = env.le(keylet::mptokenIssuance(share)); - BEAST_EXPECT(issuance); - Number const outstandingShares = issuance->at(sfOutstandingAmount); - BEAST_EXPECT(outstandingShares == 100); - - mptt.set({.account = issuer, .flags = tfMPTLock}); - env.close(); - - tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx, Ter(tecLOCKED)); - - tx[sfDestination] = issuer.human(); - env(tx, Ter(tecLOCKED)); - - // Clawback is still permitted, even with global lock - tx = vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); - env(tx); - env.close(); - - // Clawback removed shares MPToken - auto const mptSle = env.le(keylet::mptoken(share, depositor.id())); - BEAST_EXPECT(mptSle == nullptr); - - // Can delete empty vault, even if global lock - tx = vault.del({.owner = owner, .id = keylet.key}); - env(tx); - }); - testCase([this]( Env& env, Account const& issuer, @@ -2155,57 +2083,6 @@ class Vault_test : public beast::unit_test::Suite env(vault.del({.owner = owner, .id = keylet.key})); }); - testCase([this]( - Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Asset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT lock of vault pseudo-account"); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - auto const vaultAccount = [&env, keylet = keylet, this]() -> AccountID { - auto const vault = env.le(keylet); - BEAST_EXPECT(vault != nullptr); - return vault->at(sfAccount); - }(); - - tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx); - env.close(); - - tx = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - jv[sfMPTokenIssuanceID] = to_string(asset.get().getMptID()); - jv[jss::Holder] = toBase58(vaultAccount); - jv[jss::TransactionType] = jss::MPTokenIssuanceSet; - jv[jss::Flags] = tfMPTLock; - return jv; - }(); - env(tx); - env.close(); - - tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx, Ter(tecLOCKED)); - - tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx, Ter(tecLOCKED)); - - // Clawback works, even when locked - tx = vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(100)}); - env(tx); - - // Can delete an empty vault even when asset is locked. - tx = vault.del({.owner = owner, .id = keylet.key}); - env(tx); - }); - { testcase("MPT shares to a vault"); @@ -2438,6 +2315,7 @@ class Vault_test : public beast::unit_test::Suite Number initialIOU = 200; double transferRate = 1.0; bool charlieRipple = true; + FeatureBitset features = testableAmendments(); }; auto testCase = [&, this]( @@ -2451,7 +2329,7 @@ class Vault_test : public beast::unit_test::Suite PrettyAsset const& asset, std::function issuanceId)> test, CaseArgs args = {}) { - Env env{*this, testableAmendments()}; + Env env{*this, args.features}; Account const owner{"owner"}; Account const issuer{"issuer"}; Account const charlie{"charlie"}; @@ -2543,83 +2421,6 @@ class Vault_test : public beast::unit_test::Suite env.close(); }); - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto vaultAccount, - Vault& vault, - PrettyAsset const& asset, - auto issuanceId) { - testcase("IOU frozen trust line to vault account"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); - - Asset const share = Asset(issuanceId(keylet)); - - // Freeze the trustline to the vault - auto trustSet = [&, account = vaultAccount(keylet)]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(account); - } - jv[jss::TransactionType] = jss::TrustSet; - jv[jss::Flags] = tfSetFreeze; - return jv; - }(); - env(trustSet); - env.close(); - - { - // Note, the "frozen" state of the trust line to vault account - // is reported as "locked" state of the vault shares, because - // this state is attached to shares by means of the transitive - // isFrozen. - auto tx = - vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(80)}); - env(tx, Ter{tecLOCKED}); - } - - { - auto tx = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)}); - env(tx, Ter{tecLOCKED}); - - // also when trying to withdraw to a 3rd party - tx[sfDestination] = charlie.human(); - env(tx, Ter{tecLOCKED}); - env.close(); - } - - { - // Clawback works, even when locked - auto tx = vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)}); - env(tx); - env.close(); - } - - // Clear the frozen state - trustSet[jss::Flags] = tfClearFreeze; - env(trustSet); - env.close(); - - env(vault.withdraw( - {.depositor = owner, .id = keylet.key, .amount = share(50'000'000)})); - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); - testCase( [&, this]( Env& env, @@ -2681,65 +2482,6 @@ class Vault_test : public beast::unit_test::Suite }, CaseArgs{.transferRate = 1.25}); - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto, - Vault& vault, - PrettyAsset const& asset, - auto&&...) { - testcase("IOU frozen trust line to depositor"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); - - // Withdraw to 3rd party works - auto const withdrawToCharlie = [&](xrpl::Keylet keylet) { - auto tx = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - tx[sfDestination] = charlie.human(); - return tx; - }(keylet); - env(withdrawToCharlie); - - // Freeze the owner - env(trust(issuer, asset(0), owner, tfSetFreeze)); - env.close(); - - // Cannot withdraw - auto const withdraw = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(withdraw, Ter{tecFROZEN}); - - // Cannot withdraw to 3rd party - env(withdrawToCharlie, Ter{tecLOCKED}); - env.close(); - - { - // Cannot deposit some more - auto tx = - vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, Ter{tecFROZEN}); - } - - { - // Clawback still works - auto tx = vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}); - env(tx); - env.close(); - } - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); - testCase([&, this]( Env& env, Account const& owner, @@ -3031,102 +2773,6 @@ class Vault_test : public beast::unit_test::Suite env.close(); }, CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1}); - - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto, - Vault& vault, - PrettyAsset const& asset, - auto&&...) { - testcase("IOU frozen trust line to 3rd party"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); - - // Withdraw to 3rd party works - auto const withdrawToCharlie = [&](xrpl::Keylet keylet) { - auto tx = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - tx[sfDestination] = charlie.human(); - return tx; - }(keylet); - env(withdrawToCharlie); - - // Freeze the 3rd party - env(trust(issuer, asset(0), charlie, tfSetFreeze)); - env.close(); - - // Can withdraw - auto const withdraw = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(withdraw); - env.close(); - - // Cannot withdraw to 3rd party - env(withdrawToCharlie, Ter{tecFROZEN}); - env.close(); - - env(vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)})); - env.close(); - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); - - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto, - Vault& vault, - PrettyAsset const& asset, - auto&&...) { - testcase("IOU global freeze"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); - - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - { - // Cannot withdraw - auto tx = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, Ter{tecFROZEN}); - - // Cannot withdraw to 3rd party - tx[sfDestination] = charlie.human(); - env(tx, Ter{tecFROZEN}); - env.close(); - - // Cannot deposit some more - tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}); - - env(tx, Ter{tecFROZEN}); - } - - // Clawback is permitted - env(vault.clawback( - {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)})); - env.close(); - - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); } void @@ -7824,6 +7470,579 @@ class Vault_test : public beast::unit_test::Suite } } + void + testVaultDepositFreeze() + { + using namespace test::jtx; + + Account const issuer{"issuer"}; + Account const owner{"owner"}; + + // === IOU === + { + testcase("VaultDeposit IOU freeze checks"); + Env env{*this}; + Vault vault{env}; + + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + + // Initial deposit so the vault pseudo-account has a trustline + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Global freeze + env(fset(issuer, asfGlobalFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(tecFROZEN)); + env(fclear(issuer, asfGlobalFreeze)); + + // Depositor regular freeze + env(trust(issuer, asset(0), owner, tfSetFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(tecFROZEN)); + env(trust(issuer, asset(0), owner, tfClearFreeze)); + + // Depositor deep freeze + env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(tecFROZEN)); + env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + + // Vault-account regular freeze + // Post-fix: checkDepositFreeze catches it → tecFROZEN + // Pre-fix: not checked directly, but the transitive share + // check triggers → tecLOCKED + { + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); + + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); + + TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(expected)); + + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } + + // Vault-account deep freeze + { + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); + + trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze; + env(trustSet); + env.close(); + + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + + trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze; + env(trustSet); + env.close(); + } + + // Clawback works while frozen + env(fset(issuer, asfGlobalFreeze)); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); + env(fclear(issuer, asfGlobalFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + // === MPT === + { + testcase("VaultDeposit MPT lock checks"); + Env env{*this}; + Vault vault{env}; + + env.fund(XRP(100'000), issuer, owner); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; + + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + auto const vaultAcctID = env.le(keylet)->at(sfAccount); + Account const vaultAcct("vault", vaultAcctID); + + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); + + // For MPT isDeepFrozen == isFrozen, so all locks block in + // both pre- and post-fix. + auto runTests = [&]() { + // Global lock + mptt.set({.flags = tfMPTLock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + mptt.set({.flags = tfMPTUnlock}); + env.close(); + + // Depositor individual lock + mptt.set({.holder = owner, .flags = tfMPTLock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + mptt.set({.holder = owner, .flags = tfMPTUnlock}); + env.close(); + + // Vault pseudo-account individual lock + mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); + env.close(); + + // Clawback works while locked + mptt.set({.flags = tfMPTLock}); + env.close(); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)})); + mptt.set({.flags = tfMPTUnlock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + } + + // Focused demonstration: a depositor under a regular individual IOU freeze + // can still withdraw to themselves (self-withdrawal), but is blocked from + // withdrawing to a third party. + // + // Pre-fixCleanup3_3_0: both the self-withdrawal AND the third-party + // withdrawal were blocked because the old code checked checkFrozen on the + // destination regardless of whether it was the submitter. + // Post-fixCleanup3_3_0: checkWithdrawFreeze skips the submitter freeze + // check when submitter == destination, so self-withdrawal succeeds. + void + testVaultSelfWithdrawWhileFrozen() + { + testcase("VaultWithdraw IOU self-withdrawal while individually frozen"); + + using namespace test::jtx; + + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Account const charlie{"charlie"}; + Env env{*this}; + Vault vault{env}; + + env.fund(XRP(100'000), issuer, owner, charlie); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env.trust(asset(1'000'000), charlie); + env(pay(issuer, owner, asset(100'000))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)})); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Set a regular individual freeze on the owner's IOU trustline. + env(trust(issuer, asset(0), owner, tfSetFreeze)); + env.close(); + + // Self-withdrawal: submitter == destination, so the submitter + // freeze check is skipped. + // Post-fix: tesSUCCESS. Pre-fix: tecFROZEN. + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); + + // Withdrawal to a third party is blocked: submitter != destination + // so the submitter freeze check applies. + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + // Post-fix: tecFROZEN (checkIndividualFrozen on submitter). + // Pre-fix: tecLOCKED (isFrozen on the vault share). + env(withdrawToCharlie, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + } + + env(trust(issuer, asset(0), owner, tfClearFreeze)); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + void + testVaultWithdrawFreeze() + { + using namespace test::jtx; + + Account const issuer{"issuer"}; + Account const owner{"owner"}; + + // === IOU === + { + testcase("VaultWithdraw IOU freeze checks"); + Env env{*this}; + Vault vault{env}; + + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); + + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.trust(asset(1'000'000), charlie); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + // Post-fix: submitter freeze blocks withdraw to 3rd party + // Pre-fix: submitter's IOU freeze not checked, but + // checkFrozen(depositor, share) may trigger tecLOCKED + TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + + // Global freeze → self-withdraw + env(fset(issuer, asfGlobalFreeze)); + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(tecFROZEN)); + // Global freeze → withdraw to 3rd party + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); + } + env(fclear(issuer, asfGlobalFreeze)); + + // Vault-account regular freeze + { + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); + + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); + + TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + + // Self-withdraw + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(vaultAcctFreeze)); + // Withdraw to 3rd party + { + auto withdrawToCharlie = vault.withdraw( + {.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(vaultAcctFreeze)); + } + + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } + + // Depositor regular freeze → self-withdraw + env(trust(issuer, asset(0), owner, tfSetFreeze)); + // Post-fix: self-withdraw allowed (submitter==dst skip) + // Pre-fix: isFrozen(depositor, iou) catches it + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); + + // Depositor regular freeze → withdraw to 3rd party + { + auto withdrawTo3rd = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawTo3rd[sfDestination] = charlie.human(); + env(withdrawTo3rd, Ter(submitterTo3rd)); + } + env(trust(issuer, asset(0), owner, tfClearFreeze)); + // Replenish what was withdrawn + if (fix330Enabled) + { + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + } + env.close(); + + // Depositor deep freeze → self-withdraw blocked + env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN))); + env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + + // Destination regular freeze → withdraw to 3rd party + env(trust(issuer, asset(0), charlie, tfSetFreeze)); + // Self-withdraw unaffected by charlie's freeze + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + // Post-fix: regular freeze on dst allowed + // Pre-fix: checkFrozen(dst, iou) catches it + env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); + } + env(trust(issuer, asset(0), charlie, tfClearFreeze)); + // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded + env(vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(fix330Enabled ? 2 : 1)})); + env.close(); + + // Destination deep freeze → withdraw to 3rd party blocked + env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze)); + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); + } + // Destination deep freeze → self-withdraw unaffected + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env.close(); + + // Clawback works while frozen + env(fset(issuer, asfGlobalFreeze)); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); + env(fclear(issuer, asfGlobalFreeze)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + + // === MPT === + { + testcase("VaultWithdraw MPT lock checks"); + Env env{*this}; + Vault vault{env}; + + env.fund(XRP(100'000), issuer, owner); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; + + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + Account const vaultAcct("vault", env.le(keylet)->at(sfAccount)); + + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); + + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.close(); + mptt.authorize({.account = charlie}); + mptt.authorize({.account = issuer, .holder = charlie}); + env.close(); + + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Global lock + mptt.set({.flags = tfMPTLock}); + env.close(); + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + + // Global lock → withdraw to issuer + // Post-fix: bypasses freeze checks, but accountHolds + // on the pseudo returns 0 under global lock + // Pre-fix: checkFrozen(dst=issuer) catches global lock + { + auto withdrawToIssuer = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}); + withdrawToIssuer[sfDestination] = issuer.human(); + env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED))); + } + mptt.set({.flags = tfMPTUnlock}); + env.close(); + if (fix330Enabled) + { + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + } + env.close(); + + // Vault pseudo-account individual lock + mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); + env.close(); + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); + env.close(); + + // Depositor individual lock → self-withdraw blocked + // (isDeepFrozen == isFrozen for MPT) + mptt.set({.holder = owner, .flags = tfMPTLock}); + env.close(); + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), + Ter(tecLOCKED)); + // Depositor lock → withdraw to 3rd party also blocked + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecLOCKED)); + } + + // Depositor lock → withdraw to issuer + // Post-fix: issuer bypass in checkWithdrawFreezes + // Pre-fix: checkFrozen(depositor, share) blocks transitively + { + auto withdrawToIssuer = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}); + withdrawToIssuer[sfDestination] = issuer.human(); + env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED))); + } + mptt.set({.holder = owner, .flags = tfMPTUnlock}); + env.close(); + if (fix330Enabled) + { + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + } + env.close(); + + // 3rd party destination lock → withdraw to 3rd party blocked + mptt.set({.holder = charlie, .flags = tfMPTLock}); + env.close(); + { + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter{tecLOCKED}); + } + // 3rd party lock → self-withdraw unaffected + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + mptt.set({.holder = charlie, .flags = tfMPTUnlock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + env.close(); + + // Clawback works while locked + mptt.set({.flags = tfMPTLock}); + env.close(); + env(vault.clawback( + {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)})); + mptt.set({.flags = tfMPTUnlock}); + env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); + env.close(); + }; + + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } + } + public: void run() override @@ -7864,6 +8083,10 @@ public: testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice(); testWithdrawSoleShareholderLoanRepaymentExit(); + testVaultDepositFreeze(); + testVaultWithdrawFreeze(); + testVaultSelfWithdrawWhileFrozen(); + testReferenceHolding(); testHoldingDeletionBlocked(); } diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 3d813d993c..aca6074c4a 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -96,6 +96,26 @@ testableAmendments() return kIds; } +/** + * Returns all 2^N permutations of a seed FeatureBitset with each subset of + * the given features excluded. The seed is included as the first element. + * + * Useful for running a test over every combination of optional amendments + * so that each case is exercised both with and without each feature. + */ +inline std::vector +amendmentCombinations(std::initializer_list features, FeatureBitset seed) +{ + std::vector result{seed}; + for (auto const& f : features) + { + auto const n = result.size(); + for (std::size_t i = 0; i < n; ++i) + result.push_back(result[i] - f); + } + return result; +} + //------------------------------------------------------------------------------ class SuiteLogs : public Logs diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index 9256242417..7da419c6e7 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -100,5 +100,4 @@ cmdToJSONRPC(std::vector const& args, beast::Journal j, unsigned in jv[jss::id] = paramsObj[jss::id]; return jv; } - } // namespace xrpl::test::jtx diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 535e14cf1d..c3ed91f672 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -7,6 +7,7 @@ #include #include +#include namespace xrpl::test::jtx { @@ -50,5 +51,4 @@ fillSeq(json::Value& jv, ReadView const& view); /** Given an xrpld unit test rpc command, return the corresponding JSON. */ json::Value cmdToJSONRPC(std::vector const& args, beast::Journal j, unsigned int apiVersion); - } // namespace xrpl::test::jtx From fd8a9152437c3fa42922428e7029828cf54b1b88 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Fri, 26 Jun 2026 18:26:53 -0400 Subject: [PATCH 03/31] fix: Use trustline balance direction to validate IOU PaymentMint/PaymentBurn (#7584) --- include/xrpl/protocol/Permissions.h | 2 +- src/libxrpl/protocol/Permissions.cpp | 22 +- .../tx/transactors/payment/Payment.cpp | 59 +++- src/test/app/Delegate_test.cpp | 318 ++++++++++++++++-- 4 files changed, 365 insertions(+), 36 deletions(-) diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index eb161ef7ad..c6f464082d 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -106,7 +106,7 @@ public: txToPermissionType(TxType type); // tx type value is permission value minus one - [[nodiscard]] static TxType + [[nodiscard]] static std::optional permissionToTxType(std::uint32_t value); /** diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index 3aa9705b03..80aa1b1f4e 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -152,9 +153,11 @@ Permission::getPermissionName(std::uint32_t value) const return granular; // not a granular permission, check if it maps to a transaction type - auto const txType = permissionToTxType(value); - if (auto const* item = TxFormats::getInstance().findByType(txType); item != nullptr) - return item->getName(); + if (auto const txType = permissionToTxType(value)) + { + if (auto const* item = TxFormats::getInstance().findByType(*txType); item != nullptr) + return item->getName(); + } return std::nullopt; } @@ -231,7 +234,10 @@ Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const } auto const txType = permissionToTxType(permissionValue); - auto const txIt = txDelegationMap_.find(txType); + if (!txType) + return false; + + auto const txIt = txDelegationMap_.find(*txType); // Tx-level permissions require the transaction type itself to be delegable, and // the corresponding amendment enabled. @@ -245,10 +251,14 @@ Permission::txToPermissionType(TxType const type) return static_cast(type) + 1; } -TxType +std::optional Permission::permissionToTxType(uint32_t value) { - XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0"); + // Values outside this range [1, 65536] would silently truncate when cast to + // uint16_t, for example, 65537 would become 1, mapping to the Payment transaction. + if (value == 0 || value > std::numeric_limits::max() + 1u) + return std::nullopt; + return static_cast(value - 1); } diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 9a9a01ec19..7f1e4d8079 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -283,16 +283,59 @@ Payment::checkGranularSemantics( if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) return terNO_DELEGATE_PERMISSION; - // PaymentMint and PaymentBurn apply to both IOU and MPT direct payments. - if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && - amountAsset.getIssuer() == tx[sfAccount]) - return tesSUCCESS; + if (isXRP(amountAsset)) + return terNO_DELEGATE_PERMISSION; - if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && - amountAsset.getIssuer() == tx[sfDestination]) - return tesSUCCESS; + return amountAsset.visit( + [&](MPTIssue const& mptIssue) -> NotTEC { + // For MPT payments, the MPTokenIssuanceID encodes the issuer unambiguously, + // unlike IOU, there is no endpoint aliasing where either side of the + // trustline can appear as the issuer. + if (heldGranularPermissions.contains(PaymentMint) && + mptIssue.getIssuer() == tx[sfAccount]) + return tesSUCCESS; + if (heldGranularPermissions.contains(PaymentBurn) && + mptIssue.getIssuer() == tx[sfDestination]) + return tesSUCCESS; + return terNO_DELEGATE_PERMISSION; + }, + [&](Issue const& issue) -> NotTEC { + // For IOU payments, either endpoint may be encoded as the issuer in + // sfAmount. PaySteps normalizes those endpoint aliases, so sfAmount.issuer + // alone does not reliably identify whether the transaction issues or redeems + // IOUs. We determine PaymentMint vs PaymentBurn from the trustline balance + // direction instead. + auto const account = tx[sfAccount]; + auto const destination = tx[sfDestination]; - return terNO_DELEGATE_PERMISSION; + // Reject if neither endpoint is the issuer. + if (issue.getIssuer() != account && issue.getIssuer() != destination) + return terNO_DELEGATE_PERMISSION; + + auto const sle = view.read(keylet::trustLine(account, destination, issue.currency)); + if (!sle) + return terNO_DELEGATE_PERMISSION; + + bool const accountIsLow = (account < destination); + auto const destLimit = sle->getFieldAmount(accountIsLow ? sfHighLimit : sfLowLimit); + auto const rawBalance = sle->getFieldAmount(sfBalance); + bool const accountIsHolder = + accountIsLow ? rawBalance > beast::kZero : rawBalance < beast::kZero; + + // PaymentMint requires the destination to be the holder and the account to be the + // issuer. destLimit > 0: destination is willing to hold account's IOUs (account is the + // issuer). !accountIsHolder: DirectStepI will issue, not redeem. + if (heldGranularPermissions.contains(PaymentMint) && destLimit > beast::kZero && + !accountIsHolder) + return tesSUCCESS; + + // PaymentBurn requires the source account to be the holder and the destination to be + // the issuer. accountIsHolder: DirectStepI will redeem, not issue. + if (heldGranularPermissions.contains(PaymentBurn) && accountIsHolder) + return tesSUCCESS; + + return terNO_DELEGATE_PERMISSION; + }); } TER diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 6e80577797..f68f813853 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -1105,10 +1106,43 @@ class Delegate_test : public beast::unit_test::Suite env(delegate::set(alice, bob, {"PaymentBurn"})); env.close(); - env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob)); + env(pay(alice, gw, usd(30)), delegate::As(bob)); env.require(Balance(alice, usd(20))); } + // PaymentBurn is authorized by balance direction, not trust limit. + // holder is allowed to burn even if trust limit is 0. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + auto const gwUSD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.trust(gwUSD(200), alice); + env.close(); + + env(pay(gw, alice, gwUSD(50))); + env.require(Balance(alice, gwUSD(50))); + env.close(); + + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + env.trust(gwUSD(0), alice); + env.close(); + BEAST_EXPECT(env.limit(alice, gwUSD.issue()) == gwUSD(0)); + + env(trust(gw, aliceUSD(200))); + env.close(); + + env(pay(alice, gw, gwUSD(30)), delegate::As(bob)); + env.require(Balance(alice, gwUSD(20))); + env.require(Balance(gw, aliceUSD(-20))); + } + // Test invalid fields or flags not allowed in granular permission template { Env env(*this, features); @@ -1184,22 +1218,22 @@ class Delegate_test : public beast::unit_test::Suite mpt.authorize({.account = alice}); mpt.authorize({.account = bob}); - auto const MPT = mpt["MPT"]; // NOLINT(readability-identifier-naming) - env(pay(gw, alice, MPT(500))); - env(pay(gw, bob, MPT(500))); + auto const gwMPT = mpt["MPT"]; + env(pay(gw, alice, gwMPT(500))); + env(pay(gw, bob, gwMPT(500))); env.close(); - auto aliceMPT = env.balance(alice, MPT); - auto bobMPT = env.balance(bob, MPT); + auto aliceMPT = env.balance(alice, gwMPT); + auto bobMPT = env.balance(bob, gwMPT); // PaymentMint { env(delegate::set(gw, bob, {"PaymentMint"})); env.close(); - env(pay(gw, alice, MPT(50)), delegate::As(bob)); - BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT + MPT(50)); - BEAST_EXPECT(env.balance(bob, MPT) == bobMPT); - aliceMPT = env.balance(alice, MPT); + env(pay(gw, alice, gwMPT(50)), delegate::As(bob)); + BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT + gwMPT(50)); + BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT); + aliceMPT = env.balance(alice, gwMPT); } // PaymentBurn @@ -1207,26 +1241,235 @@ class Delegate_test : public beast::unit_test::Suite env(delegate::set(alice, bob, {"PaymentBurn"})); env.close(); - env(pay(alice, gw, MPT(50)), delegate::As(bob)); - BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50)); - BEAST_EXPECT(env.balance(bob, MPT) == bobMPT); - aliceMPT = env.balance(alice, MPT); + env(pay(alice, gw, gwMPT(50)), delegate::As(bob)); + BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50)); + BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT); + aliceMPT = env.balance(alice, gwMPT); } // Grant both granular permissions and tx level permission. { env(delegate::set(alice, bob, {"PaymentBurn", "PaymentMint", "Payment"})); env.close(); - env(pay(alice, gw, MPT(50)), delegate::As(bob)); - BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50)); - BEAST_EXPECT(env.balance(bob, MPT) == bobMPT); - aliceMPT = env.balance(alice, MPT); - env(pay(alice, bob, MPT(100)), delegate::As(bob)); - BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(100)); - BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100)); + env(pay(alice, gw, gwMPT(50)), delegate::As(bob)); + BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50)); + BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT); + aliceMPT = env.balance(alice, gwMPT); + env(pay(alice, bob, gwMPT(100)), delegate::As(bob)); + BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(100)); + BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT + gwMPT(100)); } } + // PaymentMint/PaymentBurn must not trust IOU issuer aliases. + // In a direct IOU payment, sfAmount.issuer may be encoded as either + // endpoint, and PaySteps normalizes those aliases to the same execution. + // These cases ensure a delegate cannot flip the encoded issuer to turn a + // mint into an apparent burn, or a burn into an apparent mint. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + auto const gwUSD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.trust(gwUSD(200), alice); + env.close(); + + // Alice holds 100 USD issued by gw. + env(pay(gw, alice, gwUSD(100))); + env.close(); + env.require(Balance(alice, gwUSD(100))); + + // Delegate with only PaymentBurn tries to mint by encoding + // Amount.issuer as the destination alias, alice. The actual issuer + // is gw, so this requires PaymentMint and must be rejected. + { + env(delegate::set(gw, bob, {"PaymentBurn"})); + env.close(); + + // Amount.issuer = alice (destination), rejected because gw is + // the actual issuer and PaymentMint is required. + env(pay(gw, alice, aliceUSD(50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env.require(Balance(alice, gwUSD(100))); + + // Fails because bob holds PaymentBurn, not PaymentMint. + env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + env.require(Balance(alice, gwUSD(100))); + } + + // Delegate with only PaymentMint tries to burn by encoding + // Amount.issuer as the source alias, alice. The actual issuer is + // gw, so this requires PaymentBurn and must be rejected. + { + env(delegate::set(alice, bob, {"PaymentMint"})); + env.close(); + + // Amount.issuer = alice (account), rejected because gw is the + // actual issuer and PaymentBurn is required. + env(pay(alice, gw, aliceUSD(50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env.require(Balance(alice, gwUSD(100))); + + // Fails because bob holds PaymentMint, not PaymentBurn. + env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + env.require(Balance(alice, gwUSD(100))); + } + } + + // Neither account nor destination is issuer. + // PaymentMint and PaymentBurn do not authorize these payments. + { + // IOU + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + auto const gwUSD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw, gw2); + env.close(); + + env.trust(gwUSD(200), alice); + env.close(); + + env(pay(gw, alice, gwUSD(100))); + env.close(); + + env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"})); + env.close(); + + env(pay(alice, gw, gw2["USD"](50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // MPT + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + + env.fund(XRP(10000), gw2); + env.close(); + + MPTTester mpt(env, gw, {.holders = {alice, bob}}); + mpt.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); + + mpt.authorize({.account = alice}); + mpt.authorize({.account = bob}); + + auto const gwMPT = mpt["MPT"]; + env(pay(gw, alice, gwMPT(500))); + env(pay(gw, bob, gwMPT(500))); + env.close(); + + env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"})); + env.close(); + + env(pay(alice, gw2, gwMPT(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + } + + // IOU issuer is an endpoint, but no trustline exists. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"})); + env.close(); + + env(pay(alice, gw, alice["USD"](50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // Both trust limits (who is the designated issuer) and balance direction + // (which way DirectStepI executes) must be checked. Neither alone is sufficient. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + auto const gwUSD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + + // Alice trusts gw but holds zero gw-issued USD. With balance == 0, + // DirectStepI would issue rather than redeem, so PaymentBurn must + // be rejected even though Alice's trust limit to gw is positive. + { + env.trust(gwUSD(200), alice); + env.close(); + + // Alice has nothing to burn. + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, aliceUSD(50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // Set up a trust line where gw holds alice-issued USD. DirectStepI + // would redeem rather than issue, so PaymentMint must be rejected + // even though the endpoint identity matches. + { + // Gw sets trust to accept alice-issued USD. + env(trust(gw, aliceUSD(200))); + env.close(); + + // Alice issues her own USD to gw; now gw holds alice's IOUs. + env(pay(alice, gw, aliceUSD(100))); + env.close(); + + // In gw's view, accountHolds(gw, USD, alice) > 0, so DirectStepI redeems. + // PaymentMint must be rejected because the step would redeem, not issue. + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + + env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + env(pay(gw, alice, aliceUSD(50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + } + + // Alice trusts gw but gw is not willing to hold alice's IOU (destLimit == 0). + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gateway"}; + env.fund(XRP(10000), alice, bob, gw); + env.trust(gw["USD"](200), alice); + env.close(); + + env(delegate::set(alice, bob, {"PaymentMint"})); + env.close(); + + env(pay(alice, gw, gw["USD"](50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, alice["USD"](50)), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + // Verify granular permissions of different tx types in the same SLE are scoped // correctly. AccountSet permissions don't apply to Payment and vice versa { @@ -2617,6 +2860,38 @@ class Delegate_test : public beast::unit_test::Suite BEAST_EXPECT(granularPermissions.empty()); } + void + testPermissionToTxType() + { + testcase("test Permission to Tx type"); + + // 0 is not a valid permission value + BEAST_EXPECT(!Permission::permissionToTxType(0)); + + // 1 maps to Payment transaction + BEAST_EXPECT(Permission::permissionToTxType(1) == ttPAYMENT); + + // UINT16_MAX+1 is the maximum possible tx-level permission value + constexpr uint32_t maxTxPermission = std::numeric_limits::max() + 1u; + BEAST_EXPECT(Permission::permissionToTxType(maxTxPermission).has_value()); + + // exceeding maximum value should return nullopt + BEAST_EXPECT(!Permission::permissionToTxType(maxTxPermission + 1)); + + // All granular permission values should return nullopt since they do not map to a TxType. + for (auto const gp : { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION +#define GRANULAR_PERMISSION(type, txType, value, ...) GranularPermissionType::type, +#include +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + }) + { + BEAST_EXPECT(!Permission::permissionToTxType(static_cast(gp))); + } + } + void run() override { @@ -2647,6 +2922,7 @@ class Delegate_test : public beast::unit_test::Suite testTxDelegableCount(); testNonDelegableTxWithDelegate(all); testDelegateUtilsNullptrCheck(); + testPermissionToTxType(); } }; BEAST_DEFINE_TESTSUITE(Delegate, app, xrpl); From 768d7603b1633d5d9a83801f04de789640882385 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:20:38 -0400 Subject: [PATCH 04/31] feat: Confidential Transfer for MPT (#5860) Signed-off-by: dependabot[bot] Signed-off-by: chuanshanjida Co-authored-by: Ed Hennis Co-authored-by: Jingchen Co-authored-by: Denis Angell Co-authored-by: Bart Co-authored-by: yinyiqian1 Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Co-authored-by: Bronek Kozicki Co-authored-by: Mayukha Vadari Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Co-authored-by: tequ Co-authored-by: Ayaz Salikhov Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com> Co-authored-by: Alex Kremer Co-authored-by: Sergey Kuznetsov Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gregory Tsipenyuk Co-authored-by: chuanshanjida Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Peter Chen Co-authored-by: Timothy Banks Co-authored-by: Timothy Banks --- CMakeLists.txt | 2 + conan.lock | 1 + conanfile.py | 2 + cspell.config.yaml | 11 + .../xrpl/ledger/helpers/CredentialHelpers.h | 33 + include/xrpl/protocol/ConfidentialTransfer.h | 431 + include/xrpl/protocol/LedgerFormats.h | 6 +- include/xrpl/protocol/Protocol.h | 62 + include/xrpl/protocol/TER.h | 6 + include/xrpl/protocol/TxFlags.h | 11 +- include/xrpl/protocol/detail/features.macro | 2 +- .../xrpl/protocol/detail/ledger_entries.macro | 23 +- include/xrpl/protocol/detail/secp256k1.h | 5 +- include/xrpl/protocol/detail/sfields.macro | 18 + .../xrpl/protocol/detail/transactions.macro | 87 + include/xrpl/protocol/jss.h | 330 +- .../protocol_autogen/ledger_entries/MPToken.h | 210 + .../ledger_entries/MPTokenIssuance.h | 105 + .../transactions/ConfidentialMPTClawback.h | 201 + .../transactions/ConfidentialMPTConvert.h | 336 + .../transactions/ConfidentialMPTConvertBack.h | 310 + .../transactions/ConfidentialMPTMergeInbox.h | 129 + .../transactions/ConfidentialMPTSend.h | 408 + .../transactions/MPTokenIssuanceSet.h | 74 + include/xrpl/tx/Transactor.h | 5 + include/xrpl/tx/invariants/InvariantCheck.h | 1 + include/xrpl/tx/invariants/MPTInvariant.h | 171 +- .../token/ConfidentialMPTClawback.h | 59 + .../token/ConfidentialMPTConvert.h | 61 + .../token/ConfidentialMPTConvertBack.h | 62 + .../token/ConfidentialMPTMergeInbox.h | 63 + .../transactors/token/ConfidentialMPTSend.h | 72 + .../ledger/helpers/CredentialHelpers.cpp | 54 +- src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 9 + src/libxrpl/ledger/helpers/TokenHelpers.cpp | 3 +- src/libxrpl/protocol/ConfidentialTransfer.cpp | 487 + src/libxrpl/protocol/PublicKey.cpp | 3 +- src/libxrpl/protocol/TER.cpp | 2 + src/libxrpl/tx/Transactor.cpp | 9 + src/libxrpl/tx/invariants/MPTInvariant.cpp | 302 + .../token/ConfidentialMPTClawback.cpp | 210 + .../token/ConfidentialMPTConvert.cpp | 350 + .../token/ConfidentialMPTConvertBack.cpp | 319 + .../token/ConfidentialMPTMergeInbox.cpp | 150 + .../transactors/token/ConfidentialMPTSend.cpp | 445 + .../tx/transactors/token/MPTokenAuthorize.cpp | 19 + .../token/MPTokenIssuanceCreate.cpp | 14 +- .../transactors/token/MPTokenIssuanceSet.cpp | 118 +- .../app/ConfidentialTransferExtended_test.cpp | 2595 ++++++ src/test/app/ConfidentialTransfer_test.cpp | 8208 +++++++++++++++++ src/test/app/Delegate_test.cpp | 4 +- src/test/app/Invariants_test.cpp | 249 + src/test/app/MPToken_test.cpp | 3 +- src/test/app/Vault_test.cpp | 42 + src/test/jtx/ConfidentialTransfer.h | 496 + src/test/jtx/batch.h | 40 +- src/test/jtx/impl/batch.cpp | 11 + src/test/jtx/impl/mpt.cpp | 1931 +++- src/test/jtx/impl/utility.cpp | 18 +- src/test/jtx/mpt.h | 442 +- .../ledger_entries/MPTokenIssuanceTests.cpp | 81 + .../ledger_entries/MPTokenTests.cpp | 162 + .../ConfidentialMPTClawbackTests.cpp | 194 + .../ConfidentialMPTConvertBackTests.cpp | 303 + .../ConfidentialMPTConvertTests.cpp | 309 + .../ConfidentialMPTMergeInboxTests.cpp | 146 + .../transactions/ConfidentialMPTSendTests.cpp | 363 + .../transactions/MPTokenIssuanceSetTests.cpp | 42 + 68 files changed, 21177 insertions(+), 253 deletions(-) create mode 100644 include/xrpl/protocol/ConfidentialTransfer.h create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h create mode 100644 include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h create mode 100644 include/xrpl/tx/transactors/token/ConfidentialMPTSend.h create mode 100644 src/libxrpl/protocol/ConfidentialTransfer.cpp create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp create mode 100644 src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp create mode 100644 src/test/app/ConfidentialTransferExtended_test.cpp create mode 100644 src/test/app/ConfidentialTransfer_test.cpp create mode 100644 src/test/jtx/ConfidentialTransfer.h create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bdc62442b3..1e8befcc8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ find_package(ed25519 REQUIRED) find_package(gRPC REQUIRED) find_package(LibArchive REQUIRED) find_package(lz4 REQUIRED) +find_package(mpt-crypto REQUIRED) find_package(nudb REQUIRED) find_package(OpenSSL REQUIRED) find_package(secp256k1 REQUIRED) @@ -102,6 +103,7 @@ target_link_libraries( INTERFACE ed25519::ed25519 lz4::lz4 + mpt-crypto::mpt-crypto OpenSSL::Crypto OpenSSL::SSL secp256k1::secp256k1 diff --git a/conan.lock b/conan.lock index 45dd145914..b6ddfa4e58 100644 --- a/conan.lock +++ b/conan.lock @@ -12,6 +12,7 @@ "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933", "openssl/3.6.3#1163d4ddc603907084d08a6a0c6e580f%1782307150.583886", "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166", + "mpt-crypto/0.4.0-rc2#a580f2f9ad0e795de696aa62d54fb9af%1782425834.488828", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188", "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732", diff --git a/conanfile.py b/conanfile.py index 2733d4fc9c..aec4f9eab0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -30,6 +30,7 @@ class Xrpl(ConanFile): "ed25519/2015.03", "grpc/1.81.1", "libarchive/3.8.7", + "mpt-crypto/0.4.0-rc2", "nudb/2.0.9", "openssl/3.6.3", "secp256k1/0.7.1", @@ -208,6 +209,7 @@ class Xrpl(ConanFile): "grpc::grpc++", "libarchive::libarchive", "lz4::lz4", + "mpt-crypto::mpt-crypto", "nudb::nudb", "openssl::crypto", "protobuf::libprotobuf", diff --git a/cspell.config.yaml b/cspell.config.yaml index 8273df6c98..c120c31855 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -60,6 +60,7 @@ words: - autobridging - bimap - bindir + - blindings - bookdir - Bougalis - Britto @@ -95,6 +96,7 @@ words: - daria - dcmake - dearmor + - decryptor - dedented - deleteme - demultiplexer @@ -106,6 +108,7 @@ words: - distro - doxyfile - dxrpl + - elgamal - enabled - enablerepo - endmacro @@ -119,6 +122,7 @@ words: - fmtdur - fsanitize - funclets + - Gamal - gcov - gcovr - ghead @@ -216,6 +220,7 @@ words: - partitioner - paychan - paychans + - Pedersen - permdex - perminute - permissioned @@ -239,6 +244,10 @@ words: - Raphson - rcflags - replayer + - rerandomize + - rerandomization + - rerandomized + - rerandomizes - rerere - retriable - RIPD @@ -255,6 +264,7 @@ words: - sahyadri - Satoshi - scons + - Schnorr - secp - sendq - seqit @@ -285,6 +295,7 @@ words: - stvar - stvector - stxchainattestations + - summands - superpeer - superpeers - takergets diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 0cfbbde538..d6b797ce34 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -63,6 +63,39 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j); TER verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j); +/** + * @brief Check whether src is authorized to deposit to dst. + * + * @param tx Transaction containing optional credential IDs. + * @param view Read-only ledger view. + * @param src Source account. + * @param dst Destination account. + * @param sleDst Destination AccountRoot, if it exists. + * @param j Journal for diagnostics. + * @return tesSUCCESS if the deposit is allowed, otherwise an authorization + * error. + */ +TER +checkDepositPreauth( + STTx const& tx, + ReadView const& view, + AccountID const& src, + AccountID const& dst, + std::shared_ptr const& sleDst, + beast::Journal j); + +/** + * @brief Remove expired credentials referenced by the transaction. + * + * @param tx Transaction containing optional sfCredentialIDs. + * @param view Mutable ledger view. + * @param j Journal for diagnostics. + * @return tesSUCCESS if no referenced credentials expired, tecEXPIRED if any + * were removed, or an error from credential deletion. + */ +TER +cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j); + // Check expired credentials and for existing DepositPreauth ledger object TER verifyDepositPreauth( diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h new file mode 100644 index 0000000000..5b1bcbf606 --- /dev/null +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -0,0 +1,431 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Bundles an ElGamal public key with its associated encrypted amount. + * + * Used to represent a recipient in confidential transfers, containing both + * the recipient's ElGamal public key and the ciphertext encrypting the + * transfer amount under that key. + */ +struct ConfidentialRecipient +{ + /** @brief The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). */ + Slice publicKey; + + /** + * @brief The encrypted amount ciphertext + * (size=xrpl::kEcGamalEncryptedTotalLength). + */ + Slice encryptedAmount; +}; + +/** + * @brief Holds two secp256k1 public key components representing an ElGamal + * ciphertext (C1, C2). + */ +struct EcPair +{ + /** @brief First ElGamal ciphertext component. */ + secp256k1_pubkey c1; + + /** @brief Second ElGamal ciphertext component. */ + secp256k1_pubkey c2; +}; + +/** + * @brief Increments the confidential balance version counter on an MPToken. + * + * The version counter is used to prevent replay attacks by binding proofs + * to a specific state of the account's confidential balance. Wraps to 0 + * on overflow (defined behavior for unsigned integers). + * + * @param mptoken The MPToken ledger entry to update. + */ +inline void +incrementConfidentialVersion(STObject& mptoken) +{ + // Retrieve current version and increment, wrapping back to 0 at UINT32_MAX. + // The wrap is computed explicitly rather than relying on unsigned overflow + // of `+ 1u`, as it trips the unsigned-integer-overflow sanitizer in the UBSan CI build. + auto const current = mptoken[~sfConfidentialBalanceVersion].valueOr(0u); + mptoken[sfConfidentialBalanceVersion] = + current == std::numeric_limits::max() ? 0u : current + 1u; +} + +/** + * @brief Generates the context hash for ConfidentialMPTSend transactions. + * + * Creates a unique 256-bit hash that binds the zero-knowledge proofs to + * this specific send transaction, preventing proof reuse across transactions. + * + * @param account The sender's account ID. + * @param issuanceID The MPToken Issuance ID. + * @param sequence The transaction sequence number or ticket number. + * @param destination The destination account ID. + * @param version The sender's confidential balance version. + * @return A 256-bit context hash unique to this transaction. + */ +uint256 +getSendContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + AccountID const& destination, + std::uint32_t version); + +/** + * @brief Generates the context hash for ConfidentialMPTClawback transactions. + * + * Creates a unique 256-bit hash that binds the equality proof to this + * specific clawback transaction. + * + * @param account The issuer's account ID. + * @param issuanceID The MPToken Issuance ID. + * @param sequence The transaction sequence number or ticket number. + * @param holder The holder's account ID being clawed back from. + * @return A 256-bit context hash unique to this transaction. + */ +uint256 +getClawbackContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + AccountID const& holder); + +/** + * @brief Generates the context hash for ConfidentialMPTConvert transactions. + * + * Creates a unique 256-bit hash that binds the Schnorr proof (for key + * registration) to this specific convert transaction. + * + * @param account The holder's account ID. + * @param issuanceID The MPToken Issuance ID. + * @param sequence The transaction sequence number or a ticket number. + * @return A 256-bit context hash unique to this transaction. + */ +uint256 +getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence); + +/** + * @brief Generates the context hash for ConfidentialMPTConvertBack transactions. + * + * Creates a unique 256-bit hash that binds the zero-knowledge proofs to + * this specific convert-back transaction. + * + * @param account The holder's account ID. + * @param issuanceID The MPToken Issuance ID. + * @param sequence The transaction sequence number or a ticket number. + * @param version The holder's confidential balance version. + * @return A 256-bit context hash unique to this transaction. + */ +uint256 +getConvertBackContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + std::uint32_t version); + +/** + * @brief Parses an ElGamal ciphertext into two secp256k1 public key components. + * + * Breaks an encrypted amount (size=xrpl::kEcGamalEncryptedTotalLength, two + * compressed EC points of size=xrpl::kEcCiphertextComponentLength) into + * a pair containing (C1, C2) for use in cryptographic operations. + * + * @param buffer The buffer containing the compressed ciphertext + * (size=xrpl::kEcGamalEncryptedTotalLength). + * @return The parsed pair (c1, c2) if successful, std::nullopt if the buffer is invalid. + */ +std::optional +makeEcPair(Slice const& buffer); + +/** + * @brief Serializes an EcPair into compressed form. + * + * Converts an EcPair (C1, C2) back into a buffer + * (size=xrpl::kEcGamalEncryptedTotalLength) containing two compressed EC + * points (size=xrpl::kEcCiphertextComponentLength each). + * + * @param pair The EcPair to serialize. + * @return The buffer (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt + * if serialization fails. + */ +std::optional +serializeEcPair(EcPair const& pair); + +/** + * @brief Verifies that a buffer contains two valid, parsable EC public keys. + * + * @param buffer The input buffer containing two concatenated components. + * @return true if both components can be parsed successfully, false otherwise. + */ +bool +isValidCiphertext(Slice const& buffer); + +/** + * @brief Verifies that a buffer contains a valid, parsable compressed EC point. + * + * Can be used to validate both compressed public keys and Pedersen commitments. + * Fails early if the prefix byte is not 0x02 or 0x03. + * + * @param buffer The input buffer containing a compressed EC point + * (size=xrpl::kCompressedEcPointLength). + * @return true if the point can be parsed successfully, false otherwise. + */ +bool +isValidCompressedECPoint(Slice const& buffer); + +/** + * @brief Homomorphically adds two ElGamal ciphertexts. + * + * Uses the additive homomorphic property of ElGamal encryption to compute + * Enc(a + b) from Enc(a) and Enc(b) without decryption. + * + * @param a The first ciphertext (size=xrpl::kEcGamalEncryptedTotalLength). + * @param b The second ciphertext (size=xrpl::kEcGamalEncryptedTotalLength). + * @return The resulting ciphertext Enc(a + b), or std::nullopt on failure. + */ +std::optional +homomorphicAdd(Slice const& a, Slice const& b); + +/** + * @brief Homomorphically subtracts two ElGamal ciphertexts. + * + * Uses the additive homomorphic property of ElGamal encryption to compute + * Enc(a - b) from Enc(a) and Enc(b) without decryption. + * + * @param a The minuend ciphertext (size=xrpl::kEcGamalEncryptedTotalLength). + * @param b The subtrahend ciphertext (size=xrpl::kEcGamalEncryptedTotalLength). + * @return The resulting ciphertext Enc(a - b), or std::nullopt on failure. + */ +std::optional +homomorphicSubtract(Slice const& a, Slice const& b); + +/** + * @brief Re-randomizes an ElGamal ciphertext without changing its plaintext. + * + * Adds Enc(0; randomness) under the supplied public key to the ciphertext. + * This is used when a public, deterministic scalar must perturb ciphertext + * randomness while preserving ledger reproducibility. + * + * @param ciphertext The ciphertext to re-randomize + * (size=xrpl::kEcGamalEncryptedTotalLength). + * @param pubKeySlice The ElGamal public key matching the ciphertext recipient. + * @param randomness The scalar used as zero-encryption randomness + * (size=xrpl::kEcScalarLength). + * @return The re-randomized ciphertext, or std::nullopt on failure. + */ +std::optional +rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness); + +/** + * @brief Encrypts an amount using ElGamal encryption. + * + * Produces a ciphertext C = (C1, C2) where C1 = r*G and C2 = m*G + r*Pk, + * using the provided blinding factor r. + * + * @param amt The plaintext amount to encrypt. + * @param pubKeySlice The recipient's ElGamal public key (size=xrpl::kEcPubKeyLength). + * @param blindingFactor The randomness used as blinding factor r + * (size=xrpl::ecBlindingFactorLength). + * @return The ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt on failure. + */ +std::optional +encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor); + +/** + * @brief Generates the canonical zero encryption for a specific MPToken. + * + * Creates a deterministic encryption of zero that is unique to the account + * and MPT issuance. Used to initialize confidential balance fields. + * + * @param pubKeySlice The holder's ElGamal public key (size=xrpl::kEcPubKeyLength). + * @param account The account ID of the token holder. + * @param mptId The MPToken Issuance ID. + * @return The canonical zero ciphertext (size=xrpl::kEcGamalEncryptedTotalLength), or std::nullopt + * on failure. + */ +std::optional +encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId); + +/** + * @brief Verifies a Schnorr proof of knowledge of an ElGamal private key. + * + * Proves that the submitter knows the secret key corresponding to the + * provided public key, without revealing the secret key itself. + * + * @param pubKeySlice The ElGamal public key (size=xrpl::kEcPubKeyLength). + * @param proofSlice The Schnorr proof (size=xrpl::ecSchnorrProofLength). + * @param contextHash The 256-bit context hash binding the proof. + * @return tesSUCCESS if valid, or an error code otherwise. + */ +TER +verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash); + +/** + * @brief Validates the format of encrypted amount fields in a transaction. + * + * Checks that all ciphertext fields in the transaction object have the + * correct length and contain valid EC points. This function is only used + * by ConfidentialMPTConvert and ConfidentialMPTConvertBack transactions. + * + * @param object The transaction object containing encrypted amount fields. + * @return tesSUCCESS if all formats are valid, temMALFORMED if required fields + * are missing, or temBAD_CIPHERTEXT if format validation fails. + */ +NotTEC +checkEncryptedAmountFormat(STObject const& object); + +/** + * @brief Verifies revealed amount encryptions for all recipients. + * + * Validates that the same amount was correctly encrypted for the holder, + * issuer, and optionally the auditor using their respective public keys. + * + * @param amount The revealed plaintext amount. + * @param blindingFactor The blinding factor used in all encryptions + * (size=xrpl::ecBlindingFactorLength). + * @param holder The holder's public key and encrypted amount. + * @param issuer The issuer's public key and encrypted amount. + * @param auditor Optional auditor's public key and encrypted amount. + * @return tesSUCCESS if all encryptions are valid, or an error code otherwise. + */ +TER +verifyRevealedAmount( + uint64_t const amount, + Slice const& blindingFactor, + ConfidentialRecipient const& holder, + ConfidentialRecipient const& issuer, + std::optional const& auditor); + +/** + * @brief Returns the number of recipients in a confidential transfer. + * + * Returns 4 if an auditor is present (sender, destination, issuer, auditor), + * or 3 if no auditor (sender, destination, issuer). + * + * @param hasAuditor Whether the issuance has an auditor configured. + * @return The number of recipients (3 or 4). + */ +constexpr uint8_t +getConfidentialRecipientCount(bool hasAuditor) +{ + return hasAuditor ? 4 : 3; +} + +/** + * @brief Verifies a compact sigma clawback proof. + * + * Proves that the issuer knows the exact amount encrypted in the holder's + * balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer + * can decrypt the balance using their private key. + * + * @param amount The revealed plaintext amount. + * @param proof The zero-knowledge proof bytes (ecClawbackProofLength). + * @param pubKeySlice The issuer's ElGamal public key (kEcPubKeyLength bytes). + * @param ciphertext The issuer's encrypted balance on the holder's account + * (kEcGamalEncryptedTotalLength bytes). + * @param contextHash The 256-bit context hash binding the proof. + * @return tesSUCCESS if the proof is valid, or an error code otherwise. + */ +TER +verifyClawbackProof( + uint64_t const amount, + Slice const& proof, + Slice const& pubKeySlice, + Slice const& ciphertext, + uint256 const& contextHash); + +/** + * @brief Generates a cryptographically secure blinding factor + * (size=xrpl::kEcBlindingFactorLength). + * + * Produces random bytes suitable for use as an ElGamal blinding factor + * or Pedersen commitment randomness. + * + * @return A buffer containing the random blinding factor + * (size=xrpl::kEcBlindingFactorLength). + */ +Buffer +generateBlindingFactor(); + +/** + * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTSend transaction. + * + * This function calls mpt_verify_send_proof API in the mpt-crypto utility lib, which verifies the + * equality proof, amount linkage, balance linkage, and range proof. + * Equality proof: Proves the same value is encrypted for the sender, receiver, issuer, and auditor. + * Amount linkage: Proves the send amount matches the amount Pedersen commitment. + * Balance linkage: Proves the sender's balance matches the balance Pedersen + * commitment. + * Range proof: Proves the amount and the remaining balance are within range [0, 2^64-1]. + * + * @param proof The full proof blob. + * @param sender The sender's public key and encrypted amount. + * @param destination The destination's public key and encrypted amount. + * @param issuer The issuer's public key and encrypted amount. + * @param auditor The auditor's public key and encrypted amount if present. + * @param spendingBalance The sender's current spending balance ciphertext. + * @param amountCommitment The Pedersen commitment to the send amount. + * @param balanceCommitment The Pedersen commitment to the sender's balance. + * @param contextHash The context hash binding the proof. + * @return tesSUCCESS if all proofs are valid, or an error code otherwise. + */ +TER +verifySendProof( + Slice const& proof, + ConfidentialRecipient const& sender, + ConfidentialRecipient const& destination, + ConfidentialRecipient const& issuer, + std::optional const& auditor, + Slice const& spendingBalance, + Slice const& amountCommitment, + Slice const& balanceCommitment, + uint256 const& contextHash); + +/** + * @brief Verifies all zero-knowledge proofs for a ConfidentialMPTConvertBack transaction. + * + * This function calls mpt_verify_convert_back_proof API in the mpt-crypto utility lib, which + * verifies the balance linkage proof and range proof. Balance linkage proof: proves the balance + * commitment matches the spending ciphertext. Range proof: proves the remaining balance after + * convert back is within range [0, 2^64-1]. + * + * @param proof The full proof blob. + * @param pubKeySlice The holder's public key. + * @param spendingBalance The holder's spending balance ciphertext. + * @param balanceCommitment The Pedersen commitment to the balance. + * @param amount The amount being converted back to public. + * @param contextHash The context hash binding the proof. + * @return tesSUCCESS if all proofs are valid, or an error code otherwise. + */ +TER +verifyConvertBackProof( + Slice const& proof, + Slice const& pubKeySlice, + Slice const& spendingBalance, + Slice const& balanceCommitment, + uint64_t amount, + uint256 const& contextHash); + +} // namespace xrpl diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index c1274e9e91..70afd12f34 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -177,7 +177,8 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfMPTCanEscrow, 0x00000008) \ LSF_FLAG(lsfMPTCanTrade, 0x00000010) \ LSF_FLAG(lsfMPTCanTransfer, 0x00000020) \ - LSF_FLAG(lsfMPTCanClawback, 0x00000040)) \ + LSF_FLAG(lsfMPTCanClawback, 0x00000040) \ + LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080)) \ \ LEDGER_OBJECT(MPTokenIssuanceMutable, \ LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \ @@ -186,8 +187,9 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \ LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \ LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \ + LSF_FLAG(lsmfMPTCannotEnableCanHoldConfidentialBalance, 0x00000080) \ LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \ - LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \ + LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \ \ LEDGER_OBJECT(MPToken, \ LSF_FLAG2(lsfMPTLocked, 0x00000001) \ diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 6a96b2ccbe..7eac92e83c 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -4,6 +4,10 @@ #include #include +#include +#include + +#include #include namespace xrpl { @@ -307,4 +311,62 @@ constexpr std::size_t kPermissionMaxSize = 10; /** The maximum number of transactions that can be in a batch. */ constexpr std::size_t kMaxBatchTxCount = 8; +/** Length of a secp256k1 scalar in bytes. */ +constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE; + +/** Length of EC point (compressed) */ +constexpr std::size_t kCompressedEcPointLength = 33; + +/** Length of one compressed EC point component in an EC ElGamal ciphertext. */ +constexpr std::size_t kEcCiphertextComponentLength = kMPT_ELGAMAL_CIPHER_SIZE; + +/** EC ElGamal ciphertext length: two compressed EC points concatenated. */ +constexpr std::size_t kEcGamalEncryptedTotalLength = kMPT_ELGAMAL_TOTAL_SIZE; + +/** Length of EC public key (compressed) */ +constexpr std::size_t kEcPubKeyLength = kMPT_PUBKEY_SIZE; + +/** Length of EC private key in bytes */ +constexpr std::size_t kEcPrivKeyLength = kMPT_PRIVKEY_SIZE; + +/** Length of the EC blinding factor in bytes */ +constexpr std::size_t kEcBlindingFactorLength = kMPT_BLINDING_FACTOR_SIZE; + +/** Length of Schnorr ZKProof for public key registration (compact form) in bytes */ +constexpr std::size_t kEcSchnorrProofLength = kMPT_SCHNORR_PROOF_SIZE; + +/** Length of Pedersen Commitment (compressed) */ +constexpr std::size_t kEcPedersenCommitmentLength = kMPT_PEDERSEN_COMMIT_SIZE; + +/** Length of single bulletproof (range proof for 1 commitment) in bytes */ +constexpr std::size_t kEcSingleBulletproofLength = kMPT_SINGLE_BULLETPROOF_SIZE; + +/** Length of double bulletproof (range proof for 2 commitments) in bytes */ +constexpr std::size_t kEcDoubleBulletproofLength = kMPT_DOUBLE_BULLETPROOF_SIZE; + +/** Length of the compact sigma proof component for ConfidentialMPTSend. */ +constexpr std::size_t kEcSendSigmaProofLength = SECP256K1_COMPACT_STANDARD_PROOF_SIZE; + +/** 192 bytes compact sigma proof + 754 bytes double bulletproof. */ +constexpr std::size_t kEcSendProofLength = kEcSendSigmaProofLength + kEcDoubleBulletproofLength; + +/** Length of the compact sigma proof component for ConfidentialMPTConvertBack. */ +constexpr std::size_t kEcConvertBackSigmaProofLength = SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE; + +/** 128 bytes compact sigma proof + 688 bytes single bulletproof. */ +constexpr std::size_t kEcConvertBackProofLength = + kEcConvertBackSigmaProofLength + kEcSingleBulletproofLength; + +/** Length of the ZKProof for ConfidentialMPTClawback. */ +constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE; + +/** Extra base fee multiplier charged to confidential MPT transactions. */ +constexpr std::uint32_t kConfidentialFeeMultiplier = 9; + +/** Compressed EC point prefix for even y-coordinate */ +constexpr std::uint8_t kEcCompressedPrefixEvenY = 0x02; + +/** Compressed EC point prefix for odd y-coordinate */ +constexpr std::uint8_t kEcCompressedPrefixOddY = 0x03; + } // namespace xrpl diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 072bd4778f..84c344ea76 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -128,6 +128,7 @@ enum TEMcodes : TERUnderlyingType { temBAD_TRANSFER_FEE, temINVALID_INNER_BATCH, temBAD_MPT, + temBAD_CIPHERTEXT, }; //------------------------------------------------------------------------------ @@ -358,6 +359,11 @@ enum TECcodes : TERUnderlyingType { tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, + // DEPRECATED: This error code tecNO_DELEGATE_PERMISSION is reserved for + // backward compatibility with historical data on non-prod networks, can be + // reclaimed after those networks reset. + tecNO_DELEGATE_PERMISSION = 198, + tecBAD_PROOF = 199, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f9c7bc1a5d..461afd24e7 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -140,7 +140,8 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfMPTCanEscrow, lsfMPTCanEscrow) \ TF_FLAG(tfMPTCanTrade, lsfMPTCanTrade) \ TF_FLAG(tfMPTCanTransfer, lsfMPTCanTransfer) \ - TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback), \ + TF_FLAG(tfMPTCanClawback, lsfMPTCanClawback) \ + TF_FLAG(tfMPTCanHoldConfidentialBalance, lsfMPTCanHoldConfidentialBalance), \ MASK_ADJ(0)) \ \ TRANSACTION(MPTokenAuthorize, \ @@ -349,10 +350,13 @@ inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTrans inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback; inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; +inline constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance = + lsmfMPTCannotEnableCanHoldConfidentialBalance; inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = ~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow | tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback | - tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee); + tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee | + tmfMPTCannotEnableCanHoldConfidentialBalance); // MPTokenIssuanceSet MutableFlags: // Enable mutable capability flags. These flags are one-way: once enabled, @@ -364,9 +368,10 @@ inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004; inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008; inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010; inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020; +inline constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance = 0x00000040; inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade | - tmfMPTSetCanTransfer | tmfMPTSetCanClawback); + tmfMPTSetCanTransfer | tmfMPTSetCanClawback | tmfMPTSetCanHoldConfidentialBalance); // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a // TrustLine to be added to the issuer of that token without explicit permission from that issuer. diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 2b6beaa671..a9b237be32 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -14,7 +14,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. - +XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) 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) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index e0ea1a61c3..2f403708c3 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -401,19 +401,28 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfDomainID, SoeOptional}, {sfMutableFlags, SoeDefault}, {sfReferenceHolding, SoeOptional}, + {sfIssuerEncryptionKey, SoeOptional}, + {sfAuditorEncryptionKey, SoeOptional}, + {sfConfidentialOutstandingAmount, SoeDefault}, })) /** A ledger object which tracks MPToken \sa keylet::mptoken */ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ - {sfAccount, SoeRequired}, - {sfMPTokenIssuanceID, SoeRequired}, - {sfMPTAmount, SoeDefault}, - {sfLockedAmount, SoeOptional}, - {sfOwnerNode, SoeRequired}, - {sfPreviousTxnID, SoeRequired}, - {sfPreviousTxnLgrSeq, SoeRequired}, + {sfAccount, SoeRequired}, + {sfMPTokenIssuanceID, SoeRequired}, + {sfMPTAmount, SoeDefault}, + {sfLockedAmount, SoeOptional}, + {sfOwnerNode, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfConfidentialBalanceInbox, SoeOptional}, + {sfConfidentialBalanceSpending, SoeOptional}, + {sfConfidentialBalanceVersion, SoeDefault}, + {sfIssuerEncryptedBalance, SoeOptional}, + {sfAuditorEncryptedBalance, SoeOptional}, + {sfHolderEncryptionKey, SoeOptional}, })) /** A ledger object which tracks Oracle diff --git a/include/xrpl/protocol/detail/secp256k1.h b/include/xrpl/protocol/detail/secp256k1.h index 17dfa3ff25..5ca9033eae 100644 --- a/include/xrpl/protocol/detail/secp256k1.h +++ b/include/xrpl/protocol/detail/secp256k1.h @@ -11,7 +11,10 @@ secp256k1Context() struct Holder { secp256k1_context* impl; - Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN)) + // SECP256K1_CONTEXT_SIGN and SECP256K1_CONTEXT_VERIFY were deprecated. + // All contexts support both signing and verification, so + // SECP256K1_CONTEXT_NONE is the correct flag to use. + Holder() : impl(secp256k1_context_create(SECP256K1_CONTEXT_NONE)) { } diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 01bb4fc480..0d453eea11 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -113,6 +113,7 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips) TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) +TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -146,6 +147,7 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28) TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::kSmdBaseTen|SField::kSmdDefault) TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) +TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -206,6 +208,7 @@ TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, SField::kSmdPseudoAccount | SField::kSmdDefault) TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) +TYPED_SFIELD(sfBlindingFactor, UINT256, 40) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -299,6 +302,21 @@ TYPED_SFIELD(sfAssetClass, VL, 28) TYPED_SFIELD(sfProvider, VL, 29) TYPED_SFIELD(sfMPTokenMetadata, VL, 30) TYPED_SFIELD(sfCredentialType, VL, 31) +TYPED_SFIELD(sfConfidentialBalanceInbox, VL, 32) +TYPED_SFIELD(sfConfidentialBalanceSpending, VL, 33) +TYPED_SFIELD(sfIssuerEncryptedBalance, VL, 34) +TYPED_SFIELD(sfIssuerEncryptionKey, VL, 35) +TYPED_SFIELD(sfHolderEncryptionKey, VL, 36) +TYPED_SFIELD(sfZKProof, VL, 37) +TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38) +TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39) +TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40) +TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41) +TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42) +TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43) +TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44) +TYPED_SFIELD(sfAmountCommitment, VL, 45) +TYPED_SFIELD(sfBalanceCommitment, VL, 46) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dbaaf46083..8a3b0ff2ce 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -735,6 +735,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, {sfMPTokenMetadata, SoeOptional}, {sfTransferFee, SoeOptional}, {sfMutableFlags, SoeOptional}, + {sfIssuerEncryptionKey, SoeOptional}, + {sfAuditorEncryptionKey, SoeOptional}, })) /** This transaction type authorizes a MPToken instance */ @@ -1077,6 +1079,91 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, {sfAmount, SoeRequired, SoeMptSupported}, })) +/** This transaction type converts into confidential MPT balance. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, + Delegation::Delegable, + featureConfidentialTransfer, + NoPriv, + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfMPTAmount, SoeRequired}, + {sfHolderEncryptionKey, SoeOptional}, + {sfHolderEncryptedAmount, SoeRequired}, + {sfIssuerEncryptedAmount, SoeRequired}, + {sfAuditorEncryptedAmount, SoeOptional}, + {sfBlindingFactor, SoeRequired}, + {sfZKProof, SoeOptional}, +})) + +/** This transaction type merges MPT inbox. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, + Delegation::Delegable, + featureConfidentialTransfer, + NoPriv, + ({ + {sfMPTokenIssuanceID, SoeRequired}, +})) + +/** This transaction type converts back into public MPT balance. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, + Delegation::Delegable, + featureConfidentialTransfer, + NoPriv, + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfMPTAmount, SoeRequired}, + {sfHolderEncryptedAmount, SoeRequired}, + {sfIssuerEncryptedAmount, SoeRequired}, + {sfAuditorEncryptedAmount, SoeOptional}, + {sfBlindingFactor, SoeRequired}, + {sfZKProof, SoeRequired}, + {sfBalanceCommitment, SoeRequired}, +})) + +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, + Delegation::Delegable, + featureConfidentialTransfer, + NoPriv, + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfDestination, SoeRequired}, + {sfDestinationTag, SoeOptional}, + {sfSenderEncryptedAmount, SoeRequired}, + {sfDestinationEncryptedAmount, SoeRequired}, + {sfIssuerEncryptedAmount, SoeRequired}, + {sfAuditorEncryptedAmount, SoeOptional}, + {sfZKProof, SoeRequired}, + {sfAmountCommitment, SoeRequired}, + {sfBalanceCommitment, SoeRequired}, + {sfCredentialIDs, SoeOptional}, +})) + +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, + Delegation::Delegable, + featureConfidentialTransfer, + NoPriv, + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeRequired}, + {sfMPTAmount, SoeRequired}, + {sfZKProof, SoeRequired}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 8a2a112542..191ed385f3 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -137,6 +137,7 @@ JSS(authorized_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo JSS(auth_change_queued); // out: AccountInfo +JSS(auditor_encrypted_balance); // out: mpt_holders (confidential MPT) JSS(available); // out: ValidatorList JSS(avg_bps_recv); // out: Peers JSS(avg_bps_sent); // out: Peers @@ -161,9 +162,6 @@ JSS(build_path); // in: TransactionSign JSS(build_version); // out: NetworkOPs JSS(cancel_after); // out: AccountChannels JSS(can_delete); // out: CanDelete -JSS(mpt_amount); // out: mpt_holders -JSS(mpt_issuance_id); // in: Payment, mpt_holders -JSS(mptoken_index); // out: mpt_holders JSS(changes); // out: BookChanges JSS(channel_id); // out: AccountChannels JSS(channels); // out: AccountChannels @@ -185,165 +183,170 @@ JSS(command); // in: RPCHandler JSS(common); // out: RPC server_definitions JSS(complete); // out: NetworkOPs, InboundLedger JSS(complete_ledgers); // out: NetworkOPs, PeerImp -JSS(consensus); // out: NetworkOPs, LedgerConsensus -JSS(converge_time); // out: NetworkOPs -JSS(converge_time_s); // out: NetworkOPs -JSS(cookie); // out: NetworkOPs -JSS(count); // in: AccountTx*, ValidatorList -JSS(counters); // in/out: retrieve counters -JSS(credentials); // in: deposit_authorized -JSS(credential_type); // in: LedgerEntry DepositPreauth -JSS(ctid); // in/out: Tx RPC -JSS(currency_a); // out: BookChanges -JSS(currency_b); // out: BookChanges -JSS(currency); // in: paths/PathRequest, STAmount - // out: STPathSet, STAmount, AccountLines -JSS(current); // out: OwnerInfo -JSS(current_activities); // -JSS(current_ledger_size); // out: TxQ -JSS(current_queue_size); // out: TxQ -JSS(data); // out: LedgerData -JSS(date); // out: tx/Transaction, NetworkOPs -JSS(dbKBLedger); // out: getCounts -JSS(dbKBTotal); // out: getCounts -JSS(dbKBTransaction); // out: getCounts -JSS(debug_signing); // in: TransactionSign -JSS(deletion_blockers_only); // in: AccountObjects -JSS(delivered_amount); // out: insertDeliveredAmount -JSS(deposit_authorized); // out: deposit_authorized -JSS(deprecated); // -JSS(descending); // in: AccountTx* -JSS(description); // in/out: Reservations -JSS(destination); // in: nft_buy_offers, nft_sell_offers -JSS(destination_account); // in: PathRequest, RipplePathFind, account_lines - // out: AccountChannels -JSS(destination_amount); // in: PathRequest, RipplePathFind -JSS(destination_currencies); // in: PathRequest, RipplePathFind -JSS(destination_tag); // in: PathRequest - // out: AccountChannels -JSS(details); // out: Manifest, server_info -JSS(dir_entry); // out: DirectoryEntryIterator -JSS(dir_index); // out: DirectoryEntryIterator -JSS(dir_root); // out: DirectoryEntryIterator -JSS(discounted_fee); // out: amm_info -JSS(domain); // out: ValidatorInfo, Manifest -JSS(drops); // out: TxQ -JSS(duration_us); // out: NetworkOPs -JSS(effective); // out: ValidatorList - // in: UNL -JSS(enabled); // out: AmendmentTable -JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit -JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit -JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit -JSS(entire_set); // out: get_aggregate_price -JSS(ephemeral_key); // out: ValidatorInfo - // in/out: Manifest -JSS(error); // out: error -JSS(errored); // -JSS(error_code); // out: error -JSS(error_exception); // out: Submit -JSS(error_message); // out: error -JSS(expand); // in: handler/Ledger -JSS(expected_date); // out: any (warnings) -JSS(expected_date_UTC); // out: any (warnings) -JSS(expected_ledger_size); // out: TxQ -JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info -JSS(fail_hard); // in: Sign, Submit -JSS(failed); // out: InboundLedger -JSS(feature); // in: Feature -JSS(features); // out: Feature -JSS(fee_base); // out: NetworkOPs -JSS(fee_div_max); // in: TransactionSign -JSS(fee_level); // out: AccountInfo -JSS(fee_mult_max); // in: TransactionSign -JSS(fee_ref); // out: NetworkOPs, DEPRECATED -JSS(fetch_pack); // out: NetworkOPs -JSS(FIELDS); // out: RPC server_definitions - // matches definitions.json format -JSS(first); // out: rpc/Version -JSS(finished); // -JSS(fix_txns); // in: LedgerCleaner -JSS(flags); // out: AccountOffers, NetworkOPs -JSS(forward); // in: AccountTx -JSS(freeze); // out: AccountLines -JSS(freeze_peer); // out: AccountLines -JSS(deep_freeze); // out: AccountLines -JSS(deep_freeze_peer); // out: AccountLines -JSS(frozen_balances); // out: GatewayBalances -JSS(full); // in: LedgerClearer, handlers/Ledger -JSS(full_reply); // out: PathFind -JSS(fullbelow_size); // out: GetCounts -JSS(git); // out: server_info -JSS(good); // out: RPCVersion -JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field -JSS(have_header); // out: InboundLedger -JSS(have_state); // out: InboundLedger -JSS(have_transactions); // out: InboundLedger -JSS(high); // out: BookChanges -JSS(highest_sequence); // out: AccountInfo -JSS(highest_ticket); // out: AccountInfo -JSS(historical_perminute); // historical_perminute. -JSS(holders); // out: MPTHolders -JSS(hostid); // out: NetworkOPs -JSS(hotwallet); // in: GatewayBalances -JSS(id); // websocket. -JSS(ident); // in: AccountCurrencies, AccountInfo, OwnerInfo -JSS(ignore_default); // in: AccountLines -JSS(in); // out: OverlayImpl -JSS(inLedger); // out: tx/Transaction -JSS(inbound); // out: PeerImp -JSS(index); // in: LedgerEntry - // out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData -JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo -JSS(initial_sync_duration_us); // -JSS(internal_command); // in: Internal -JSS(invalid_API_version); // out: Many, when a request has an invalid version -JSS(io_latency_ms); // out: NetworkOPs -JSS(ip); // in: Connect, out: OverlayImpl -JSS(is_burned); // out: nft_info (clio) -JSS(isSerialized); // out: RPC server_definitions - // matches definitions.json format -JSS(isSigningField); // out: RPC server_definitions - // matches definitions.json format -JSS(isVLEncoded); // out: RPC server_definitions - // matches definitions.json format -JSS(issuer); // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers - // out: STPathSet, STAmount -JSS(job); // -JSS(job_queue); // -JSS(jobs); // -JSS(jsonrpc); // json version -JSS(jq_trans_overflow); // JobQueue transaction limit overflow. -JSS(kept); // out: SubmitTransaction -JSS(key); // out -JSS(key_type); // in/out: WalletPropose, TransactionSign -JSS(latency); // out: PeerImp -JSS(last); // out: RPCVersion -JSS(last_close); // out: NetworkOPs -JSS(last_refresh_time); // out: ValidatorSite -JSS(last_refresh_status); // out: ValidatorSite -JSS(last_refresh_message); // out: ValidatorSite -JSS(ledger); // in: NetworkOPs, LedgerCleaner, RPCHelpers - // out: NetworkOPs, PeerImp -JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept, - // AccountLines -JSS(ledger_data); // out: LedgerHeader -JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, RipplePathFind, - // TransactionEntry, handlers/Ledger - // out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData, - // AccountLines -JSS(ledger_hit_rate); // out: GetCounts -JSS(ledger_index); // in/out: many -JSS(ledger_index_max); // in, out: AccountTx* -JSS(ledger_index_min); // in, out: AccountTx* -JSS(ledger_max); // in, out: AccountTx* -JSS(ledger_min); // in, out: AccountTx* -JSS(ledger_time); // out: NetworkOPs -JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions - // matches definitions.json format -JSS(LEDGER_ENTRY_FLAGS); // out: RPC server_definitions -JSS(LEDGER_ENTRY_FORMATS); // out: RPC server_definitions -JSS(levels); // LogLevels +JSS(confidential_balance_inbox); // out: mpt_holders (confidential MPT) +JSS(confidential_balance_spending); // out: mpt_holders (confidential MPT) +JSS(confidential_balance_version); // out: mpt_holders (confidential MPT) +JSS(consensus); // out: NetworkOPs, LedgerConsensus +JSS(converge_time); // out: NetworkOPs +JSS(converge_time_s); // out: NetworkOPs +JSS(cookie); // out: NetworkOPs +JSS(count); // in: AccountTx*, ValidatorList +JSS(counters); // in/out: retrieve counters +JSS(credentials); // in: deposit_authorized +JSS(credential_type); // in: LedgerEntry DepositPreauth +JSS(ctid); // in/out: Tx RPC +JSS(currency_a); // out: BookChanges +JSS(currency_b); // out: BookChanges +JSS(currency); // in: paths/PathRequest, STAmount + // out: STPathSet, STAmount, AccountLines +JSS(current); // out: OwnerInfo +JSS(current_activities); // +JSS(current_ledger_size); // out: TxQ +JSS(current_queue_size); // out: TxQ +JSS(data); // out: LedgerData +JSS(date); // out: tx/Transaction, NetworkOPs +JSS(dbKBLedger); // out: getCounts +JSS(dbKBTotal); // out: getCounts +JSS(dbKBTransaction); // out: getCounts +JSS(debug_signing); // in: TransactionSign +JSS(deletion_blockers_only); // in: AccountObjects +JSS(delivered_amount); // out: insertDeliveredAmount +JSS(deposit_authorized); // out: deposit_authorized +JSS(deprecated); // +JSS(descending); // in: AccountTx* +JSS(description); // in/out: Reservations +JSS(destination); // in: nft_buy_offers, nft_sell_offers +JSS(destination_account); // in: PathRequest, RipplePathFind, account_lines + // out: AccountChannels +JSS(destination_amount); // in: PathRequest, RipplePathFind +JSS(destination_currencies); // in: PathRequest, RipplePathFind +JSS(destination_tag); // in: PathRequest + // out: AccountChannels +JSS(details); // out: Manifest, server_info +JSS(dir_entry); // out: DirectoryEntryIterator +JSS(dir_index); // out: DirectoryEntryIterator +JSS(dir_root); // out: DirectoryEntryIterator +JSS(discounted_fee); // out: amm_info +JSS(domain); // out: ValidatorInfo, Manifest +JSS(drops); // out: TxQ +JSS(duration_us); // out: NetworkOPs +JSS(effective); // out: ValidatorList + // in: UNL +JSS(enabled); // out: AmendmentTable +JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit +JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit +JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit +JSS(entire_set); // out: get_aggregate_price +JSS(ephemeral_key); // out: ValidatorInfo + // in/out: Manifest +JSS(error); // out: error +JSS(errored); // +JSS(error_code); // out: error +JSS(error_exception); // out: Submit +JSS(error_message); // out: error +JSS(expand); // in: handler/Ledger +JSS(expected_date); // out: any (warnings) +JSS(expected_date_UTC); // out: any (warnings) +JSS(expected_ledger_size); // out: TxQ +JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info +JSS(fail_hard); // in: Sign, Submit +JSS(failed); // out: InboundLedger +JSS(feature); // in: Feature +JSS(features); // out: Feature +JSS(fee_base); // out: NetworkOPs +JSS(fee_div_max); // in: TransactionSign +JSS(fee_level); // out: AccountInfo +JSS(fee_mult_max); // in: TransactionSign +JSS(fee_ref); // out: NetworkOPs, DEPRECATED +JSS(fetch_pack); // out: NetworkOPs +JSS(FIELDS); // out: RPC server_definitions + // matches definitions.json format +JSS(first); // out: rpc/Version +JSS(finished); // +JSS(fix_txns); // in: LedgerCleaner +JSS(flags); // out: AccountOffers, NetworkOPs +JSS(forward); // in: AccountTx +JSS(freeze); // out: AccountLines +JSS(freeze_peer); // out: AccountLines +JSS(deep_freeze); // out: AccountLines +JSS(deep_freeze_peer); // out: AccountLines +JSS(frozen_balances); // out: GatewayBalances +JSS(full); // in: LedgerClearer, handlers/Ledger +JSS(full_reply); // out: PathFind +JSS(fullbelow_size); // out: GetCounts +JSS(git); // out: server_info +JSS(good); // out: RPCVersion +JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field +JSS(have_header); // out: InboundLedger +JSS(have_state); // out: InboundLedger +JSS(have_transactions); // out: InboundLedger +JSS(high); // out: BookChanges +JSS(highest_sequence); // out: AccountInfo +JSS(highest_ticket); // out: AccountInfo +JSS(historical_perminute); // historical_perminute. +JSS(holders); // out: MPTHolders +JSS(holder_encryption_key); // out: mpt_holders (confidential MPT) +JSS(hostid); // out: NetworkOPs +JSS(hotwallet); // in: GatewayBalances +JSS(id); // websocket. +JSS(ident); // in: AccountCurrencies, AccountInfo, OwnerInfo +JSS(ignore_default); // in: AccountLines +JSS(in); // out: OverlayImpl +JSS(inLedger); // out: tx/Transaction +JSS(inbound); // out: PeerImp +JSS(index); // in: LedgerEntry + // out: STLedgerEntry, LedgerEntry, TxHistory, LedgerData +JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo +JSS(initial_sync_duration_us); // +JSS(internal_command); // in: Internal +JSS(invalid_API_version); // out: Many, when a request has an invalid version +JSS(io_latency_ms); // out: NetworkOPs +JSS(ip); // in: Connect, out: OverlayImpl +JSS(is_burned); // out: nft_info (clio) +JSS(isSerialized); // out: RPC server_definitions + // matches definitions.json format +JSS(isSigningField); // out: RPC server_definitions + // matches definitions.json format +JSS(isVLEncoded); // out: RPC server_definitions + // matches definitions.json format +JSS(issuer); // in: RipplePathFind, Subscribe, Unsubscribe, BookOffers + // out: STPathSet, STAmount +JSS(issuer_encrypted_balance); // out: mpt_holders (confidential MPT) +JSS(job); // +JSS(job_queue); // +JSS(jobs); // +JSS(jsonrpc); // json version +JSS(jq_trans_overflow); // JobQueue transaction limit overflow. +JSS(kept); // out: SubmitTransaction +JSS(key); // out +JSS(key_type); // in/out: WalletPropose, TransactionSign +JSS(latency); // out: PeerImp +JSS(last); // out: RPCVersion +JSS(last_close); // out: NetworkOPs +JSS(last_refresh_time); // out: ValidatorSite +JSS(last_refresh_status); // out: ValidatorSite +JSS(last_refresh_message); // out: ValidatorSite +JSS(ledger); // in: NetworkOPs, LedgerCleaner, RPCHelpers + // out: NetworkOPs, PeerImp +JSS(ledger_current_index); // out: NetworkOPs, RPCHelpers, LedgerCurrent, LedgerAccept, + // AccountLines +JSS(ledger_data); // out: LedgerHeader +JSS(ledger_hash); // in: RPCHelpers, LedgerRequest, RipplePathFind, + // TransactionEntry, handlers/Ledger + // out: NetworkOPs, RPCHelpers, LedgerClosed, LedgerData, + // AccountLines +JSS(ledger_hit_rate); // out: GetCounts +JSS(ledger_index); // in/out: many +JSS(ledger_index_max); // in, out: AccountTx* +JSS(ledger_index_min); // in, out: AccountTx* +JSS(ledger_max); // in, out: AccountTx* +JSS(ledger_min); // in, out: AccountTx* +JSS(ledger_time); // out: NetworkOPs +JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions + // matches definitions.json format +JSS(LEDGER_ENTRY_FLAGS); // out: RPC server_definitions +JSS(LEDGER_ENTRY_FORMATS); // out: RPC server_definitions +JSS(levels); // LogLevels JSS(limit); // in/out: AccountTx*, AccountOffers, AccountLines, AccountObjects // in: LedgerData, BookOffers JSS(limit_peer); // out: AccountLines @@ -401,6 +404,9 @@ JSS(min_ledger); // in: LedgerCleaner JSS(minimum_fee); // out: TxQ JSS(minimum_level); // out: TxQ JSS(missingCommand); // error +JSS(mpt_amount); // out: mpt_holders +JSS(mpt_issuance_id); // in: Payment, mpt_holders +JSS(mptoken_index); // out: mpt_holders JSS(mpt_issuance_id_a); // out: BookChanges JSS(mpt_issuance_id_b); // out: BookChanges JSS(name); // out: AmendmentTableImpl, PeerImp diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h index 0d394020f7..379cfe53f5 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h @@ -147,6 +147,150 @@ public: { return this->sle_->at(sfPreviousTxnLgrSeq); } + + /** + * @brief Get sfConfidentialBalanceInbox (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getConfidentialBalanceInbox() const + { + if (hasConfidentialBalanceInbox()) + return this->sle_->at(sfConfidentialBalanceInbox); + return std::nullopt; + } + + /** + * @brief Check if sfConfidentialBalanceInbox is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasConfidentialBalanceInbox() const + { + return this->sle_->isFieldPresent(sfConfidentialBalanceInbox); + } + + /** + * @brief Get sfConfidentialBalanceSpending (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getConfidentialBalanceSpending() const + { + if (hasConfidentialBalanceSpending()) + return this->sle_->at(sfConfidentialBalanceSpending); + return std::nullopt; + } + + /** + * @brief Check if sfConfidentialBalanceSpending is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasConfidentialBalanceSpending() const + { + return this->sle_->isFieldPresent(sfConfidentialBalanceSpending); + } + + /** + * @brief Get sfConfidentialBalanceVersion (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getConfidentialBalanceVersion() const + { + if (hasConfidentialBalanceVersion()) + return this->sle_->at(sfConfidentialBalanceVersion); + return std::nullopt; + } + + /** + * @brief Check if sfConfidentialBalanceVersion is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasConfidentialBalanceVersion() const + { + return this->sle_->isFieldPresent(sfConfidentialBalanceVersion); + } + + /** + * @brief Get sfIssuerEncryptedBalance (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerEncryptedBalance() const + { + if (hasIssuerEncryptedBalance()) + return this->sle_->at(sfIssuerEncryptedBalance); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerEncryptedBalance is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerEncryptedBalance() const + { + return this->sle_->isFieldPresent(sfIssuerEncryptedBalance); + } + + /** + * @brief Get sfAuditorEncryptedBalance (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptedBalance() const + { + if (hasAuditorEncryptedBalance()) + return this->sle_->at(sfAuditorEncryptedBalance); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedBalance is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedBalance() const + { + return this->sle_->isFieldPresent(sfAuditorEncryptedBalance); + } + + /** + * @brief Get sfHolderEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHolderEncryptionKey() const + { + if (hasHolderEncryptionKey()) + return this->sle_->at(sfHolderEncryptionKey); + return std::nullopt; + } + + /** + * @brief Check if sfHolderEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHolderEncryptionKey() const + { + return this->sle_->isFieldPresent(sfHolderEncryptionKey); + } }; /** @@ -270,6 +414,72 @@ public: return *this; } + /** + * @brief Set sfConfidentialBalanceInbox (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setConfidentialBalanceInbox(std::decay_t const& value) + { + object_[sfConfidentialBalanceInbox] = value; + return *this; + } + + /** + * @brief Set sfConfidentialBalanceSpending (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setConfidentialBalanceSpending(std::decay_t const& value) + { + object_[sfConfidentialBalanceSpending] = value; + return *this; + } + + /** + * @brief Set sfConfidentialBalanceVersion (SoeDefault) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setConfidentialBalanceVersion(std::decay_t const& value) + { + object_[sfConfidentialBalanceVersion] = value; + return *this; + } + + /** + * @brief Set sfIssuerEncryptedBalance (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setIssuerEncryptedBalance(std::decay_t const& value) + { + object_[sfIssuerEncryptedBalance] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptedBalance (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setAuditorEncryptedBalance(std::decay_t const& value) + { + object_[sfAuditorEncryptedBalance] = value; + return *this; + } + + /** + * @brief Set sfHolderEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setHolderEncryptionKey(std::decay_t const& value) + { + object_[sfHolderEncryptionKey] = value; + return *this; + } + /** * @brief Build and return the completed MPToken wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h index d493ac779c..b6c77093ac 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h @@ -302,6 +302,78 @@ public: { return this->sle_->isFieldPresent(sfReferenceHolding); } + + /** + * @brief Get sfIssuerEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerEncryptionKey() const + { + if (hasIssuerEncryptionKey()) + return this->sle_->at(sfIssuerEncryptionKey); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerEncryptionKey() const + { + return this->sle_->isFieldPresent(sfIssuerEncryptionKey); + } + + /** + * @brief Get sfAuditorEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptionKey() const + { + if (hasAuditorEncryptionKey()) + return this->sle_->at(sfAuditorEncryptionKey); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptionKey() const + { + return this->sle_->isFieldPresent(sfAuditorEncryptionKey); + } + + /** + * @brief Get sfConfidentialOutstandingAmount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getConfidentialOutstandingAmount() const + { + if (hasConfidentialOutstandingAmount()) + return this->sle_->at(sfConfidentialOutstandingAmount); + return std::nullopt; + } + + /** + * @brief Check if sfConfidentialOutstandingAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasConfidentialOutstandingAmount() const + { + return this->sle_->isFieldPresent(sfConfidentialOutstandingAmount); + } }; /** @@ -504,6 +576,39 @@ public: return *this; } + /** + * @brief Set sfIssuerEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceBuilder& + setIssuerEncryptionKey(std::decay_t const& value) + { + object_[sfIssuerEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceBuilder& + setAuditorEncryptionKey(std::decay_t const& value) + { + object_[sfAuditorEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfConfidentialOutstandingAmount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceBuilder& + setConfidentialOutstandingAmount(std::decay_t const& value) + { + object_[sfConfidentialOutstandingAmount] = value; + return *this; + } + /** * @brief Build and return the completed MPTokenIssuance wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h new file mode 100644 index 0000000000..2b16590649 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h @@ -0,0 +1,201 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTClawbackBuilder; + +/** + * @brief Transaction: ConfidentialMPTClawback + * + * Type: ttCONFIDENTIAL_MPT_CLAWBACK (89) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialTransfer + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTClawbackBuilder to construct new transactions. + */ +class ConfidentialMPTClawback : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CLAWBACK; + + /** + * @brief Construct a ConfidentialMPTClawback transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTClawback(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawback"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfHolder (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getHolder() const + { + return this->tx_->at(sfHolder); + } + + /** + * @brief Get sfMPTAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getMPTAmount() const + { + return this->tx_->at(sfMPTAmount); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } +}; + +/** + * @brief Builder for ConfidentialMPTClawback transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTClawbackBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTClawbackBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param holder The sfHolder field value. + * @param mPTAmount The sfMPTAmount field value. + * @param zKProof The sfZKProof field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTClawbackBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& holder, std::decay_t const& mPTAmount, std::decay_t const& zKProof, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CLAWBACK, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setHolder(holder); + setMPTAmount(mPTAmount); + setZKProof(zKProof); + } + + /** + * @brief Construct a ConfidentialMPTClawbackBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTClawbackBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CLAWBACK) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTClawbackBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTClawbackBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfHolder (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTClawbackBuilder& + setHolder(std::decay_t const& value) + { + object_[sfHolder] = value; + return *this; + } + + /** + * @brief Set sfMPTAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTClawbackBuilder& + setMPTAmount(std::decay_t const& value) + { + object_[sfMPTAmount] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTClawbackBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTClawback wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTClawback + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTClawback{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h new file mode 100644 index 0000000000..f7a4cb601a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h @@ -0,0 +1,336 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTConvertBuilder; + +/** + * @brief Transaction: ConfidentialMPTConvert + * + * Type: ttCONFIDENTIAL_MPT_CONVERT (85) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialTransfer + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTConvertBuilder to construct new transactions. + */ +class ConfidentialMPTConvert : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT; + + /** + * @brief Construct a ConfidentialMPTConvert transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTConvert(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvert"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfMPTAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getMPTAmount() const + { + return this->tx_->at(sfMPTAmount); + } + + /** + * @brief Get sfHolderEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHolderEncryptionKey() const + { + if (hasHolderEncryptionKey()) + { + return this->tx_->at(sfHolderEncryptionKey); + } + return std::nullopt; + } + + /** + * @brief Check if sfHolderEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHolderEncryptionKey() const + { + return this->tx_->isFieldPresent(sfHolderEncryptionKey); + } + + /** + * @brief Get sfHolderEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getHolderEncryptedAmount() const + { + return this->tx_->at(sfHolderEncryptedAmount); + } + + /** + * @brief Get sfIssuerEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getIssuerEncryptedAmount() const + { + return this->tx_->at(sfIssuerEncryptedAmount); + } + + /** + * @brief Get sfAuditorEncryptedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptedAmount() const + { + if (hasAuditorEncryptedAmount()) + { + return this->tx_->at(sfAuditorEncryptedAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedAmount() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptedAmount); + } + + /** + * @brief Get sfBlindingFactor (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBlindingFactor() const + { + return this->tx_->at(sfBlindingFactor); + } + + /** + * @brief Get sfZKProof (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getZKProof() const + { + if (hasZKProof()) + { + return this->tx_->at(sfZKProof); + } + return std::nullopt; + } + + /** + * @brief Check if sfZKProof is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasZKProof() const + { + return this->tx_->isFieldPresent(sfZKProof); + } +}; + +/** + * @brief Builder for ConfidentialMPTConvert transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTConvertBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTConvertBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param mPTAmount The sfMPTAmount field value. + * @param holderEncryptedAmount The sfHolderEncryptedAmount field value. + * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value. + * @param blindingFactor The sfBlindingFactor field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTConvertBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& mPTAmount, std::decay_t const& holderEncryptedAmount, std::decay_t const& issuerEncryptedAmount, std::decay_t const& blindingFactor, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CONVERT, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setMPTAmount(mPTAmount); + setHolderEncryptedAmount(holderEncryptedAmount); + setIssuerEncryptedAmount(issuerEncryptedAmount); + setBlindingFactor(blindingFactor); + } + + /** + * @brief Construct a ConfidentialMPTConvertBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTConvertBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfMPTAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setMPTAmount(std::decay_t const& value) + { + object_[sfMPTAmount] = value; + return *this; + } + + /** + * @brief Set sfHolderEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setHolderEncryptionKey(std::decay_t const& value) + { + object_[sfHolderEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfHolderEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setHolderEncryptedAmount(std::decay_t const& value) + { + object_[sfHolderEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfIssuerEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setIssuerEncryptedAmount(std::decay_t const& value) + { + object_[sfIssuerEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setAuditorEncryptedAmount(std::decay_t const& value) + { + object_[sfAuditorEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfBlindingFactor (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setBlindingFactor(std::decay_t const& value) + { + object_[sfBlindingFactor] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTConvert wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTConvert + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTConvert{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h new file mode 100644 index 0000000000..68bf326645 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h @@ -0,0 +1,310 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTConvertBackBuilder; + +/** + * @brief Transaction: ConfidentialMPTConvertBack + * + * Type: ttCONFIDENTIAL_MPT_CONVERT_BACK (87) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialTransfer + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTConvertBackBuilder to construct new transactions. + */ +class ConfidentialMPTConvertBack : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_CONVERT_BACK; + + /** + * @brief Construct a ConfidentialMPTConvertBack transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTConvertBack(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBack"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfMPTAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getMPTAmount() const + { + return this->tx_->at(sfMPTAmount); + } + + /** + * @brief Get sfHolderEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getHolderEncryptedAmount() const + { + return this->tx_->at(sfHolderEncryptedAmount); + } + + /** + * @brief Get sfIssuerEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getIssuerEncryptedAmount() const + { + return this->tx_->at(sfIssuerEncryptedAmount); + } + + /** + * @brief Get sfAuditorEncryptedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptedAmount() const + { + if (hasAuditorEncryptedAmount()) + { + return this->tx_->at(sfAuditorEncryptedAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedAmount() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptedAmount); + } + + /** + * @brief Get sfBlindingFactor (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBlindingFactor() const + { + return this->tx_->at(sfBlindingFactor); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } + + /** + * @brief Get sfBalanceCommitment (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getBalanceCommitment() const + { + return this->tx_->at(sfBalanceCommitment); + } +}; + +/** + * @brief Builder for ConfidentialMPTConvertBack transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTConvertBackBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTConvertBackBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param mPTAmount The sfMPTAmount field value. + * @param holderEncryptedAmount The sfHolderEncryptedAmount field value. + * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value. + * @param blindingFactor The sfBlindingFactor field value. + * @param zKProof The sfZKProof field value. + * @param balanceCommitment The sfBalanceCommitment field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTConvertBackBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& mPTAmount, std::decay_t const& holderEncryptedAmount, std::decay_t const& issuerEncryptedAmount, std::decay_t const& blindingFactor, std::decay_t const& zKProof, std::decay_t const& balanceCommitment, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_CONVERT_BACK, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setMPTAmount(mPTAmount); + setHolderEncryptedAmount(holderEncryptedAmount); + setIssuerEncryptedAmount(issuerEncryptedAmount); + setBlindingFactor(blindingFactor); + setZKProof(zKProof); + setBalanceCommitment(balanceCommitment); + } + + /** + * @brief Construct a ConfidentialMPTConvertBackBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTConvertBackBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_CONVERT_BACK) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTConvertBackBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfMPTAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setMPTAmount(std::decay_t const& value) + { + object_[sfMPTAmount] = value; + return *this; + } + + /** + * @brief Set sfHolderEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setHolderEncryptedAmount(std::decay_t const& value) + { + object_[sfHolderEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfIssuerEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setIssuerEncryptedAmount(std::decay_t const& value) + { + object_[sfIssuerEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setAuditorEncryptedAmount(std::decay_t const& value) + { + object_[sfAuditorEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfBlindingFactor (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setBlindingFactor(std::decay_t const& value) + { + object_[sfBlindingFactor] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Set sfBalanceCommitment (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTConvertBackBuilder& + setBalanceCommitment(std::decay_t const& value) + { + object_[sfBalanceCommitment] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTConvertBack wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTConvertBack + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTConvertBack{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h new file mode 100644 index 0000000000..bb932080d8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h @@ -0,0 +1,129 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTMergeInboxBuilder; + +/** + * @brief Transaction: ConfidentialMPTMergeInbox + * + * Type: ttCONFIDENTIAL_MPT_MERGE_INBOX (86) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialTransfer + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTMergeInboxBuilder to construct new transactions. + */ +class ConfidentialMPTMergeInbox : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_MERGE_INBOX; + + /** + * @brief Construct a ConfidentialMPTMergeInbox transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTMergeInbox(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInbox"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } +}; + +/** + * @brief Builder for ConfidentialMPTMergeInbox transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTMergeInboxBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTMergeInboxBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTMergeInboxBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_MERGE_INBOX, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + } + + /** + * @brief Construct a ConfidentialMPTMergeInboxBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTMergeInboxBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_MERGE_INBOX) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTMergeInboxBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMergeInboxBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTMergeInbox wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTMergeInbox + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTMergeInbox{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h new file mode 100644 index 0000000000..2d8a77d56f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h @@ -0,0 +1,408 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTSendBuilder; + +/** + * @brief Transaction: ConfidentialMPTSend + * + * Type: ttCONFIDENTIAL_MPT_SEND (88) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialTransfer + * Privileges: NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTSendBuilder to construct new transactions. + */ +class ConfidentialMPTSend : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_SEND; + + /** + * @brief Construct a ConfidentialMPTSend transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTSend(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTSend"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfDestination (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getDestination() const + { + return this->tx_->at(sfDestination); + } + + /** + * @brief Get sfDestinationTag (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDestinationTag() const + { + if (hasDestinationTag()) + { + return this->tx_->at(sfDestinationTag); + } + return std::nullopt; + } + + /** + * @brief Check if sfDestinationTag is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDestinationTag() const + { + return this->tx_->isFieldPresent(sfDestinationTag); + } + + /** + * @brief Get sfSenderEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getSenderEncryptedAmount() const + { + return this->tx_->at(sfSenderEncryptedAmount); + } + + /** + * @brief Get sfDestinationEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getDestinationEncryptedAmount() const + { + return this->tx_->at(sfDestinationEncryptedAmount); + } + + /** + * @brief Get sfIssuerEncryptedAmount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getIssuerEncryptedAmount() const + { + return this->tx_->at(sfIssuerEncryptedAmount); + } + + /** + * @brief Get sfAuditorEncryptedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptedAmount() const + { + if (hasAuditorEncryptedAmount()) + { + return this->tx_->at(sfAuditorEncryptedAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedAmount() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptedAmount); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } + + /** + * @brief Get sfAmountCommitment (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getAmountCommitment() const + { + return this->tx_->at(sfAmountCommitment); + } + + /** + * @brief Get sfBalanceCommitment (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getBalanceCommitment() const + { + return this->tx_->at(sfBalanceCommitment); + } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } +}; + +/** + * @brief Builder for ConfidentialMPTSend transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTSendBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTSendBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param destination The sfDestination field value. + * @param senderEncryptedAmount The sfSenderEncryptedAmount field value. + * @param destinationEncryptedAmount The sfDestinationEncryptedAmount field value. + * @param issuerEncryptedAmount The sfIssuerEncryptedAmount field value. + * @param zKProof The sfZKProof field value. + * @param amountCommitment The sfAmountCommitment field value. + * @param balanceCommitment The sfBalanceCommitment field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTSendBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& destination, std::decay_t const& senderEncryptedAmount, std::decay_t const& destinationEncryptedAmount, std::decay_t const& issuerEncryptedAmount, std::decay_t const& zKProof, std::decay_t const& amountCommitment, std::decay_t const& balanceCommitment, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_SEND, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setDestination(destination); + setSenderEncryptedAmount(senderEncryptedAmount); + setDestinationEncryptedAmount(destinationEncryptedAmount); + setIssuerEncryptedAmount(issuerEncryptedAmount); + setZKProof(zKProof); + setAmountCommitment(amountCommitment); + setBalanceCommitment(balanceCommitment); + } + + /** + * @brief Construct a ConfidentialMPTSendBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTSendBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_SEND) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTSendBuilder"); + } + object_ = *tx; + } + + /** @brief Transaction-specific field setters */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfDestination (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setDestination(std::decay_t const& value) + { + object_[sfDestination] = value; + return *this; + } + + /** + * @brief Set sfDestinationTag (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setDestinationTag(std::decay_t const& value) + { + object_[sfDestinationTag] = value; + return *this; + } + + /** + * @brief Set sfSenderEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setSenderEncryptedAmount(std::decay_t const& value) + { + object_[sfSenderEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfDestinationEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setDestinationEncryptedAmount(std::decay_t const& value) + { + object_[sfDestinationEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfIssuerEncryptedAmount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setIssuerEncryptedAmount(std::decay_t const& value) + { + object_[sfIssuerEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setAuditorEncryptedAmount(std::decay_t const& value) + { + object_[sfAuditorEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Set sfAmountCommitment (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setAmountCommitment(std::decay_t const& value) + { + object_[sfAmountCommitment] = value; + return *this; + } + + /** + * @brief Set sfBalanceCommitment (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setBalanceCommitment(std::decay_t const& value) + { + object_[sfBalanceCommitment] = value; + return *this; + } + + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTSendBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTSend wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTSend + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTSend{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h index a92521173b..8099af1148 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h @@ -187,6 +187,58 @@ public: { return this->tx_->isFieldPresent(sfMutableFlags); } + + /** + * @brief Get sfIssuerEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerEncryptionKey() const + { + if (hasIssuerEncryptionKey()) + { + return this->tx_->at(sfIssuerEncryptionKey); + } + return std::nullopt; + } + + /** + * @brief Check if sfIssuerEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerEncryptionKey() const + { + return this->tx_->isFieldPresent(sfIssuerEncryptionKey); + } + + /** + * @brief Get sfAuditorEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptionKey() const + { + if (hasAuditorEncryptionKey()) + { + return this->tx_->at(sfAuditorEncryptionKey); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptionKey() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptionKey); + } }; /** @@ -297,6 +349,28 @@ public: return *this; } + /** + * @brief Set sfIssuerEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceSetBuilder& + setIssuerEncryptionKey(std::decay_t const& value) + { + object_[sfIssuerEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceSetBuilder& + setAuditorEncryptionKey(std::decay_t const& value) + { + object_[sfAuditorEncryptionKey] = value; + return *this; + } + /** * @brief Build and return the MPTokenIssuanceSet wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 470571eb48..02fedfe970 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -186,6 +187,10 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + // Returns the base fee plus extra base fee units, not scaled for load. + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx, std::uint32_t extraBaseFeeMultiplier); + /* Do NOT define an invokePreflight function in a derived class. Instead, define: diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index 9378062726..8931d189fd 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -413,6 +413,7 @@ using InvariantChecks = std::tuple< ValidLoanBroker, ValidLoan, ValidVault, + ValidConfidentialMPToken, ValidMPTPayment, ValidAmounts, ValidMPTTransfer>; diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h index b4b76a290f..aa90f1b8ef 100644 --- a/include/xrpl/tx/invariants/MPTInvariant.h +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -36,17 +36,42 @@ class ValidMPTIssuance std::vector> deletedHoldings_; public: + /** + * @brief Track MPT issuance and holding creations, deletions, and + * mutations. + * + * @param isDelete Whether the ledger entry is being deleted. + * @param before The ledger entry before transaction application. + * @param after The ledger entry after transaction application. + */ void - visitEntry(bool, SLE::const_ref, SLE::const_ref); + visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after); + /** + * @brief Verify MPT issuance invariants after transaction application. + * + * @param tx The transaction being checked. + * @param result The transaction result code. + * @param fee The fee charged by the transaction. + * @param view The ledger view after transaction application. + * @param j Journal used for diagnostics. + * @return true if the invariant checks pass, otherwise false. + */ [[nodiscard]] bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; + finalize( + STTx const& tx, + TER const result, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) const; }; -/** Verify: - * - OutstandingAmount <= MaximumAmount for any MPT - * - OutstandingAmount after = OutstandingAmount before + - * sum (MPT after - MPT before) - this is total MPT credit/debit +/** + * @brief Verify public MPT amount and outstanding amount accounting. + * + * Checks that OutstandingAmount does not exceed MaximumAmount and that + * OutstandingAmount after application equals OutstandingAmount before + * application plus the net holder balance delta. */ class ValidMPTPayment { @@ -64,11 +89,104 @@ class ValidMPTPayment hash_map data_; public: + /** + * @brief Track MPT amount and outstanding amount changes. + * + * @param isDelete Whether the ledger entry is being deleted. + * @param before The ledger entry before transaction application. + * @param after The ledger entry after transaction application. + */ void - visitEntry(bool, SLE::const_ref, SLE::const_ref); + visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after); + /** + * @brief Verify public MPT payment accounting invariants. + * + * @param tx The transaction being checked. + * @param result The transaction result code. + * @param fee The fee charged by the transaction. + * @param view The ledger view after transaction application. + * @param j Journal used for diagnostics. + * @return true if the invariant checks pass, otherwise false. + */ bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + finalize( + STTx const& tx, + TER const result, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); +}; + +/** + * @brief Invariants: Confidential MPToken consistency + * + * - Convert/ConvertBack symmetry: + * Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X) + * - Cannot delete MPToken with non-zero confidential state: + * Cannot delete if sfIssuerEncryptedBalance exists + * Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist + * - Privacy flag consistency: + * MPToken confidential balance fields can only be created or changed if + * lsfMPTCanHoldConfidentialBalance is set on the issuance. + * - Encrypted field existence consistency: + * If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then + * sfIssuerEncryptedBalance must also exist (and vice versa). If + * sfAuditorEncryptedBalance exists, then those core encrypted balance fields + * must also exist. + * - COA <= OutstandingAmount: + * Confidential outstanding balance cannot exceed total outstanding. + * - Verifies sfConfidentialBalanceVersion is changed whenever sfConfidentialBalanceSpending is + * modified on an MPToken. + */ +class ValidConfidentialMPToken +{ + struct Changes + { + std::int64_t mptAmountDelta = 0; + std::int64_t coaDelta = 0; + std::int64_t outstandingDelta = 0; + SLE::const_pointer issuance; + bool deletedWithEncrypted = false; + bool badConsistency = false; + bool badCOA = false; + bool changesConfidentialFields = false; + bool badVersion = false; + }; + std::map changes_; + +public: + /** + * @brief Track confidential MPT balance, issuance, and version changes. + * + * @param isDelete Whether the ledger entry is being deleted. + * @param before The ledger entry before transaction application. + * @param after The ledger entry after transaction application. + */ + void + visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after); + + /** + * @brief Verify confidential MPT accounting and encrypted-field + * invariants. + * + * @param tx The transaction being checked. + * @param result The transaction result code. + * @param fee The fee charged by the transaction. + * @param view The ledger view after transaction application. + * @param j Journal used for diagnostics. + * @return true if the invariant checks pass, otherwise false. + */ + bool + finalize( + STTx const& tx, + TER const result, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); }; class ValidMPTTransfer @@ -85,11 +203,36 @@ class ValidMPTTransfer hash_map deletedAuthorized_; public: + /** + * @brief Track MPT balance changes and deleted authorization state. + * + * @param isDelete Whether the ledger entry is being deleted. + * @param before The ledger entry before transaction application. + * @param after The ledger entry after transaction application. + */ void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after); + /** + * @brief Verify MPT transfer authorization invariants. + * + * @param tx The transaction being checked. + * @param result The transaction result code. + * @param fee The fee charged by the transaction. + * @param view The ledger view after transaction application. + * @param j Journal used for diagnostics. + * @return true if the invariant checks pass, otherwise false. + */ bool - finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); + finalize( + STTx const& tx, + TER const result, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); private: /** @@ -99,7 +242,13 @@ private: * finalize() runs, so their authorization state is captured during * visitEntry() and stored in deletedAuthorized_. For deleted MPTokens, * returns true if reqAuth is false or lsfMPTAuthorized was set at deletion. - * For existing MPTokens, returns the result of requireAuth() + * For existing MPTokens, returns the result of requireAuth(). + * + * @param view The ledger view after transaction application. + * @param mptid The MPToken issuance ID. + * @param holder The holder account being checked. + * @param requireAuth Whether the issuance requires explicit authorization. + * @return true if the holder is authorized, otherwise false. */ [[nodiscard]] bool isAuthorized( diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h new file mode 100644 index 0000000000..ff6f76e8ea --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +namespace xrpl { + +/** + * @brief Allows an MPT issuer to clawback confidential balances from a holder. + * + * This transaction enables the issuer of an MPToken Issuance (with clawback + * enabled) to reclaim confidential tokens from a holder's account. Unlike + * regular clawback, the issuer cannot see the holder's balance directly. + * Instead, the issuer must provide a zero-knowledge proof that demonstrates + * they know the exact encrypted balance amount. + * + * @par Cryptographic Operations: + * - **Equality Proof Verification**: Verifies that the issuer's revealed + * amount matches the holder's encrypted balance using the issuer's + * ElGamal private key. + * + * @see ConfidentialMPTSend, ConfidentialMPTConvert + */ +class ConfidentialMPTClawback : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h new file mode 100644 index 0000000000..2e2591844f --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +namespace xrpl { + +/** + * @brief Converts public (plaintext) MPT balance to confidential (encrypted) + * balance. + * + * This transaction allows a token holder to convert their publicly visible + * MPToken balance into an encrypted confidential balance. Once converted, + * the balance can only be spent using ConfidentialMPTSend transactions and + * remains hidden from public view on the ledger. + * + * @par Cryptographic Operations: + * - **Schnorr Proof Verification**: When registering a new ElGamal public key, + * verifies proof of knowledge of the corresponding private key. + * - **Revealed Amount Verification**: Verifies that the provided encrypted + * amounts (for holder, issuer, and optionally auditor) all encrypt the + * same plaintext amount using the provided blinding factor. + * + * @see ConfidentialMPTConvertBack, ConfidentialMPTSend + */ +class ConfidentialMPTConvert : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h new file mode 100644 index 0000000000..cb4b6295a0 --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +namespace xrpl { + +/** + * @brief Converts confidential (encrypted) MPT balance back to public + * (plaintext) balance. + * + * This transaction allows a token holder to convert their encrypted + * confidential balance back into a publicly visible MPToken balance. The + * holder must prove they have sufficient confidential balance without + * revealing the actual balance amount. + * + * @par Cryptographic Operations: + * - **Revealed Amount Verification**: Verifies that the provided encrypted + * amounts correctly encrypt the conversion amount. + * - **Pedersen Linkage Proof**: Verifies that the provided balance commitment + * correctly links to the holder's encrypted spending balance. + * - **Bulletproof Range Proof**: Verifies that the remaining balance (after + * conversion) is non-negative, ensuring the holder has sufficient funds. + * + * @see ConfidentialMPTConvert, ConfidentialMPTSend + */ +class ConfidentialMPTConvertBack : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h new file mode 100644 index 0000000000..585c273e12 --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +namespace xrpl { + +/** + * @brief Merges the confidential inbox balance into the spending balance. + * + * In the confidential transfer system, incoming funds are deposited into an + * "inbox" balance that the recipient cannot immediately spend. This prevents + * front-running attacks where an attacker could invalidate a pending + * transaction by sending funds to the sender. This transaction merges the + * inbox into the spending balance, making those funds available for spending. + * + * @par Cryptographic Operations: + * - **Homomorphic Addition**: Adds the encrypted inbox balance to the + * encrypted spending balance using ElGamal homomorphic properties. + * - **Zero Encryption**: Resets the inbox to an encryption of zero. + * + * @note This transaction requires no zero-knowledge proofs because it only + * combines encrypted values that the holder already owns. The + * homomorphic properties of ElGamal encryption ensure correctness. + * + * @see ConfidentialMPTSend, ConfidentialMPTConvert + */ +class ConfidentialMPTMergeInbox : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h new file mode 100644 index 0000000000..72599ab987 --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h @@ -0,0 +1,72 @@ +#pragma once + +#include + +namespace xrpl { + +/** + * @brief Transfers confidential MPT tokens between holders privately. + * + * This transaction enables private token transfers where the transfer amount + * is hidden from public view. Both sender and recipient must have initialized + * confidential balances. The transaction provides encrypted amounts for all + * parties (sender, destination, issuer, and optionally auditor) along with + * zero-knowledge proofs that verify correctness without revealing the amount. + * + * @par Cryptographic Operations: + * - **Multi-Ciphertext Equality Proof**: Verifies that all encrypted amounts + * (sender, destination, issuer, auditor) encrypt the same plaintext value. + * - **Amount Pedersen Linkage Proof**: Verifies that the amount commitment + * correctly links to the sender's encrypted amount. + * - **Balance Pedersen Linkage Proof**: Verifies that the balance commitment + * correctly links to the sender's encrypted spending balance. + * - **Bulletproof Range Proof**: Verifies remaining balance and + * transfer amount are non-negative. + * + * @note Funds are deposited into the destination's inbox, not spending + * balance. The recipient must call ConfidentialMPTMergeInbox to make + * received funds spendable. + * + * @see ConfidentialMPTMergeInbox, ConfidentialMPTConvert, + * ConfidentialMPTConvertBack + */ +class ConfidentialMPTSend : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index ca5876f88a..6fc2faf03e 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -346,9 +346,9 @@ verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, b } TER -verifyDepositPreauth( +checkDepositPreauth( STTx const& tx, - ApplyView& view, + ReadView const& view, AccountID const& src, AccountID const& dst, SLE::const_ref sleDst, @@ -360,9 +360,27 @@ verifyDepositPreauth( // 2. If src is deposit preauthorized by dst (either by account or by // credentials). - bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs); + if (sleDst && ((sleDst->getFlags() & lsfDepositAuth) != 0u)) + { + if (src != dst) + { + if (!view.exists(keylet::depositPreauth(dst, src))) + { + return !tx.isFieldPresent(sfCredentialIDs) + ? tecNO_PERMISSION + : credentials::authorizedDepositPreauth( + view, tx.getFieldV256(sfCredentialIDs), dst); + } + } + } - if (credentialsPresent) + return tesSUCCESS; +} + +TER +cleanupExpiredCredentials(STTx const& tx, ApplyView& view, beast::Journal j) +{ + if (tx.isFieldPresent(sfCredentialIDs)) { auto const foundExpired = credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j); @@ -372,20 +390,22 @@ verifyDepositPreauth( return tecEXPIRED; } - if (sleDst && sleDst->isFlag(lsfDepositAuth)) - { - if (src != dst) - { - if (!view.exists(keylet::depositPreauth(dst, src))) - { - return !credentialsPresent ? tecNO_PERMISSION - : credentials::authorizedDepositPreauth( - view, tx.getFieldV256(sfCredentialIDs), dst); - } - } - } - return tesSUCCESS; } +TER +verifyDepositPreauth( + STTx const& tx, + ApplyView& view, + AccountID const& src, + AccountID const& dst, + SLE::const_ref sleDst, + beast::Journal j) +{ + if (auto const err = cleanupExpiredCredentials(tx, view, j); !isTesSuccess(err)) + return err; + + return checkDepositPreauth(tx, view, src, dst, sleDst, j); +} + } // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 2781902f5b..3c4e55a16e 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -290,6 +290,15 @@ removeEmptyHolding( (view.rules().enabled(fixCleanup3_1_3) && (*mptoken)[~sfLockedAmount].valueOr(0) != 0)) return tecHAS_OBLIGATIONS; + // Don't delete if the token still has confidential balances + if (mptoken->isFieldPresent(sfConfidentialBalanceInbox) || + mptoken->isFieldPresent(sfConfidentialBalanceSpending) || + mptoken->isFieldPresent(sfIssuerEncryptedBalance) || + mptoken->isFieldPresent(sfAuditorEncryptedBalance)) + { + return tecHAS_OBLIGATIONS; + } + return authorizeMPToken( view, {}, // priorBalance diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index b9adfbd3e6..2ecde25754 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -426,7 +426,8 @@ accountHolds( // Only if auth check is needed, as it needs to do an additional read // operation. Note featureSingleAssetVault will affect error codes. if (zeroIfUnauthorized == AuthHandling::ZeroIfUnauthorized && - view.rules().enabled(featureSingleAssetVault)) + (view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureConfidentialTransfer))) { if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth); !isTesSuccess(err)) diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp new file mode 100644 index 0000000000..0baff1e33a --- /dev/null +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -0,0 +1,487 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace xrpl { +namespace { + +account_id +toAccountId(AccountID const& account) +{ + account_id res; + std::memcpy(res.bytes, account.data(), kMPT_ACCOUNT_ID_SIZE); + return res; +} + +mpt_issuance_id +toIssuanceId(uint192 const& issuance) +{ + mpt_issuance_id res; + std::memcpy(res.bytes, issuance.data(), kMPT_ISSUANCE_ID_SIZE); + return res; +} + +/** + * @brief Pack a ConfidentialRecipient (public key + ElGamal ciphertext) into the + * secp256k1-mpt participant struct. Callers MUST have already validated that + * r.publicKey.size() == kEcPubKeyLength and + * r.encryptedAmount.size() == kEcGamalEncryptedTotalLength; + */ +mpt_confidential_participant +toParticipant(ConfidentialRecipient const& r) +{ + mpt_confidential_participant p{}; + std::memcpy(p.pubkey, r.publicKey.data(), kEcPubKeyLength); + std::memcpy(p.ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength); + return p; +} + +} // namespace + +uint256 +getSendContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + AccountID const& destination, + std::uint32_t version) +{ + uint256 result; + mpt_get_send_context_hash( + toAccountId(account), + toIssuanceId(issuanceID), + sequence, + toAccountId(destination), + version, + result.data()); + return result; +} + +uint256 +getClawbackContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + AccountID const& holder) +{ + uint256 result; + mpt_get_clawback_context_hash( + toAccountId(account), + toIssuanceId(issuanceID), + sequence, + toAccountId(holder), + result.data()); + return result; +} + +uint256 +getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence) +{ + uint256 result; + mpt_get_convert_context_hash( + toAccountId(account), toIssuanceId(issuanceID), sequence, result.data()); + return result; +} + +uint256 +getConvertBackContextHash( + AccountID const& account, + uint192 const& issuanceID, + std::uint32_t sequence, + std::uint32_t version) +{ + uint256 result; + mpt_get_convert_back_context_hash( + toAccountId(account), toIssuanceId(issuanceID), sequence, version, result.data()); + return result; +} + +std::optional +makeEcPair(Slice const& buffer) +{ + if (buffer.length() != 2 * kEcCiphertextComponentLength) + return std::nullopt; // LCOV_EXCL_LINE + + auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) { + return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length()); + }; + + Slice const s1{buffer.data(), kEcCiphertextComponentLength}; + Slice const s2{buffer.data() + kEcCiphertextComponentLength, kEcCiphertextComponentLength}; + + EcPair pair{}; + if (parsePubKey(s1, pair.c1) != 1 || parsePubKey(s2, pair.c2) != 1) + return std::nullopt; + + return pair; +} + +std::optional +serializeEcPair(EcPair const& pair) +{ + auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) { + size_t outLen = kEcCiphertextComponentLength; // 33 bytes + auto const ret = secp256k1_ec_pubkey_serialize( + secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED); + return ret == 1 && outLen == kEcCiphertextComponentLength; + }; + + Buffer buffer(kEcGamalEncryptedTotalLength); + auto const ptr = buffer.data(); + bool const res1 = serializePubKey(pair.c1, ptr); + bool const res2 = serializePubKey(pair.c2, ptr + kEcCiphertextComponentLength); + + if (!res1 || !res2) + return std::nullopt; + + return buffer; +} + +bool +isValidCiphertext(Slice const& buffer) +{ + return makeEcPair(buffer).has_value(); +} + +bool +isValidCompressedECPoint(Slice const& buffer) +{ + if (buffer.size() != kCompressedEcPointLength) + return false; + + // Compressed EC points must start with 0x02 or 0x03 + if (buffer[0] != kEcCompressedPrefixEvenY && buffer[0] != kEcCompressedPrefixOddY) + return false; + + secp256k1_pubkey point; + return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1; +} + +std::optional +homomorphicAdd(Slice const& a, Slice const& b) +{ + if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength) + return std::nullopt; + + auto const pairA = makeEcPair(a); + auto const pairB = makeEcPair(b); + + if (!pairA || !pairB) + return std::nullopt; + + EcPair sum{}; + if (auto res = secp256k1_elgamal_add( + secp256k1Context(), &sum.c1, &sum.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2); + res != 1) + { + return std::nullopt; + } + + return serializeEcPair(sum); +} + +std::optional +homomorphicSubtract(Slice const& a, Slice const& b) +{ + if (a.length() != kEcGamalEncryptedTotalLength || b.length() != kEcGamalEncryptedTotalLength) + return std::nullopt; + + auto const pairA = makeEcPair(a); + auto const pairB = makeEcPair(b); + + if (!pairA || !pairB) + return std::nullopt; + + EcPair diff{}; + if (auto const res = secp256k1_elgamal_subtract( + secp256k1Context(), &diff.c1, &diff.c2, &pairA->c1, &pairA->c2, &pairB->c1, &pairB->c2); + res != 1) + { + return std::nullopt; + } + + return serializeEcPair(diff); +} + +std::optional +rerandomizeCiphertext(Slice const& ciphertext, Slice const& pubKeySlice, Slice const& randomness) +{ + auto zero = encryptAmount(0, pubKeySlice, randomness); + if (!zero) + return std::nullopt; + + return homomorphicAdd(ciphertext, *zero); +} + +Buffer +generateBlindingFactor() +{ + unsigned char blindingFactor[kEcBlindingFactorLength]; + + // todo: might need to be updated using another RNG + if (RAND_bytes(blindingFactor, kEcBlindingFactorLength) != 1) + Throw("Failed to generate random number"); + + return Buffer(blindingFactor, kEcBlindingFactorLength); +} + +std::optional +encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor) +{ + if (blindingFactor.size() != kEcBlindingFactorLength || pubKeySlice.size() != kEcPubKeyLength) + return std::nullopt; + + Buffer out(kEcGamalEncryptedTotalLength); + if (mpt_encrypt_amount(amt, pubKeySlice.data(), blindingFactor.data(), out.data()) != 0) + return std::nullopt; + + return out; +} + +std::optional +encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId) +{ + if (pubKeySlice.size() != kEcPubKeyLength) + return std::nullopt; // LCOV_EXCL_LINE + + EcPair pair{}; + secp256k1_pubkey pubKey; + if (auto res = secp256k1_ec_pubkey_parse( + secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength); + res != 1) + { + return std::nullopt; // LCOV_EXCL_LINE + } + + if (auto res = generate_canonical_encrypted_zero( + secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data()); + res != 1) + { + return std::nullopt; // LCOV_EXCL_LINE + } + + return serializeEcPair(pair); +} + +TER +verifyRevealedAmount( + uint64_t const amount, + Slice const& blindingFactor, + ConfidentialRecipient const& holder, + ConfidentialRecipient const& issuer, + std::optional const& auditor) +{ + if (blindingFactor.size() != kEcBlindingFactorLength || + holder.publicKey.size() != kEcPubKeyLength || + holder.encryptedAmount.size() != kEcGamalEncryptedTotalLength || + issuer.publicKey.size() != kEcPubKeyLength || + issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const holderP = toParticipant(holder); + auto const issuerP = toParticipant(issuer); + mpt_confidential_participant auditorP{}; + mpt_confidential_participant const* auditorPtr = nullptr; + if (auditor) + { + if (auditor->publicKey.size() != kEcPubKeyLength || + auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + auditorP = toParticipant(*auditor); + auditorPtr = &auditorP; + } + + if (mpt_verify_revealed_amount(amount, blindingFactor.data(), &holderP, &issuerP, auditorPtr) != + 0) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +NotTEC +checkEncryptedAmountFormat(STObject const& object) +{ + // Current usage of this function is only for ConfidentialMPTConvert and + // ConfidentialMPTConvertBack transactions, which already enforce that these fields + // are present. + if (!object.isFieldPresent(sfHolderEncryptedAmount) || + !object.isFieldPresent(sfIssuerEncryptedAmount)) + { + return temMALFORMED; // LCOV_EXCL_LINE + } + + if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength || + object[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + { + return temBAD_CIPHERTEXT; + } + + bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount); + if (hasAuditor && object[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + if (!isValidCiphertext(object[sfHolderEncryptedAmount]) || + !isValidCiphertext(object[sfIssuerEncryptedAmount])) + { + return temBAD_CIPHERTEXT; + } + + if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount])) + return temBAD_CIPHERTEXT; + + return tesSUCCESS; +} + +TER +verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) +{ + if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0) + return tecBAD_PROOF; + + return tesSUCCESS; +} + +TER +verifyClawbackProof( + uint64_t const amount, + Slice const& proof, + Slice const& pubKeySlice, + Slice const& ciphertext, + uint256 const& contextHash) +{ + if (ciphertext.size() != kEcGamalEncryptedTotalLength || + pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (mpt_verify_clawback_proof( + proof.data(), amount, pubKeySlice.data(), ciphertext.data(), contextHash.data()) != 0) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +TER +verifySendProof( + Slice const& proof, + ConfidentialRecipient const& sender, + ConfidentialRecipient const& destination, + ConfidentialRecipient const& issuer, + std::optional const& auditor, + Slice const& spendingBalance, + Slice const& amountCommitment, + Slice const& balanceCommitment, + uint256 const& contextHash) +{ + auto const recipientCount = getConfidentialRecipientCount(auditor.has_value()); + if (proof.size() != kEcSendProofLength || sender.publicKey.size() != kEcPubKeyLength || + sender.encryptedAmount.size() != kEcGamalEncryptedTotalLength || + destination.publicKey.size() != kEcPubKeyLength || + destination.encryptedAmount.size() != kEcGamalEncryptedTotalLength || + issuer.publicKey.size() != kEcPubKeyLength || + issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength || + spendingBalance.size() != kEcGamalEncryptedTotalLength || + amountCommitment.size() != kEcPedersenCommitmentLength || + balanceCommitment.size() != kEcPedersenCommitmentLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + std::vector participants; + participants.reserve(recipientCount); + participants.push_back(toParticipant(sender)); + participants.push_back(toParticipant(destination)); + participants.push_back(toParticipant(issuer)); + if (auditor) + { + if (auditor->publicKey.size() != kEcPubKeyLength || + auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + participants.push_back(toParticipant(*auditor)); + } + if (participants.size() != recipientCount) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (mpt_verify_send_proof( + proof.data(), + participants.data(), + recipientCount, + spendingBalance.data(), + amountCommitment.data(), + balanceCommitment.data(), + contextHash.data()) != 0) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +TER +verifyConvertBackProof( + Slice const& proof, + Slice const& pubKeySlice, + Slice const& spendingBalance, + Slice const& balanceCommitment, + uint64_t amount, + uint256 const& contextHash) +{ + if (proof.size() != kEcConvertBackProofLength || pubKeySlice.size() != kEcPubKeyLength || + spendingBalance.size() != kEcGamalEncryptedTotalLength || + balanceCommitment.size() != kEcPedersenCommitmentLength) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (mpt_verify_convert_back_proof( + proof.data(), + pubKeySlice.data(), + spendingBalance.data(), + balanceCommitment.data(), + amount, + contextHash.data()) != 0) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +} // namespace xrpl diff --git a/src/libxrpl/protocol/PublicKey.cpp b/src/libxrpl/protocol/PublicKey.cpp index 7472f059e9..c6e6c1d324 100644 --- a/src/libxrpl/protocol/PublicKey.cpp +++ b/src/libxrpl/protocol/PublicKey.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -211,7 +212,7 @@ publicKeyType(Slice const& slice) if (slice[0] == 0xED) return KeyType::Ed25519; - if (slice[0] == 0x02 || slice[0] == 0x03) + if (slice[0] == kEcCompressedPrefixEvenY || slice[0] == kEcCompressedPrefixOddY) return KeyType::Secp256k1; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 6b8dfc6811..e5c1d17b1a 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -106,6 +106,7 @@ transResults() MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), + MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -199,6 +200,7 @@ transResults() MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."), + MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 3a10b7124f..9ab8779f03 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -356,6 +356,15 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) return baseFee + (signerCount * baseFee); } +XRPAmount +Transactor::calculateBaseFee( + ReadView const& view, + STTx const& tx, + std::uint32_t extraBaseFeeMultiplier) +{ + return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier; +} + // Returns the fee in fee units, not scaled for load. XRPAmount Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx) diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 278c7a7858..26bee4effb 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -1,7 +1,9 @@ #include #include +#include #include +#include #include #include #include @@ -23,11 +25,46 @@ #include #include +#include +#include #include #include #include + namespace xrpl { +namespace { +constexpr auto kConfidentialMptTxTypes = std::to_array({ + ttCONFIDENTIAL_MPT_SEND, + ttCONFIDENTIAL_MPT_CONVERT, + ttCONFIDENTIAL_MPT_CONVERT_BACK, + ttCONFIDENTIAL_MPT_MERGE_INBOX, + ttCONFIDENTIAL_MPT_CLAWBACK, +}); + +// Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant +// tests can inject INT64_MAX + 1, which would result in undefined behavior +// under UBSan if converted directly. +std::int64_t +toSignedMPTAmount(std::uint64_t amount) +{ + return static_cast(std::min(amount, kMaxMpTokenAmount)); +} + +std::int64_t +addMPTAmountDelta(std::int64_t delta, std::uint64_t amount) +{ + return delta + toSignedMPTAmount(amount); +} + +std::int64_t +subtractMPTAmountDelta(std::int64_t delta, std::uint64_t amount) +{ + return delta - toSignedMPTAmount(amount); +} + +} // namespace + void ValidMPTIssuance::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { @@ -438,6 +475,16 @@ ValidMPTPayment::finalize( { if (isTesSuccess(result)) { + // Confidential transactions are validated by ValidConfidentialMPToken. + // They modify encrypted fields and sfConfidentialOutstandingAmount + // rather than sfMPTAmount/sfOutstandingAmount in the standard way, + // so ValidMPTPayment's accounting does not apply to them. + if (std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) != + kConfidentialMptTxTypes.end()) + { + return true; + } + bool const invariantPasses = !view.rules().enabled(featureMPTokensV2); if (overflow_) { @@ -468,6 +515,261 @@ ValidMPTPayment::finalize( return true; } +void +ValidConfidentialMPToken::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // Helper to get MPToken Issuance ID safely + auto const getMptID = [](std::shared_ptr const& sle) -> uint192 { + if (!sle) + return beast::kZero; + if (sle->getType() == ltMPTOKEN) + return sle->getFieldH192(sfMPTokenIssuanceID); + if (sle->getType() == ltMPTOKEN_ISSUANCE) + return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer)); + return beast::kZero; + }; + + if (before && before->getType() == ltMPTOKEN) + { + uint192 const id = getMptID(before); + auto& change = changes_[id]; + change.mptAmountDelta = + subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount)); + + // Cannot delete MPToken with non-zero confidential state or non-zero public amount + if (isDelete) + { + bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0; + bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) || + before->isFieldPresent(sfConfidentialBalanceInbox) || + before->isFieldPresent(sfIssuerEncryptedBalance) || + before->isFieldPresent(sfAuditorEncryptedBalance); + + if (hasPublicBalance || hasEncryptedFields) + changes_[id].deletedWithEncrypted = true; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + uint192 const id = getMptID(after); + auto& change = changes_[id]; + change.mptAmountDelta = + addMPTAmountDelta(change.mptAmountDelta, after->getFieldU64(sfMPTAmount)); + + // Encrypted field existence consistency + bool const hasIssuerBalance = after->isFieldPresent(sfIssuerEncryptedBalance); + bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox); + bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending); + bool const hasAuditorBalance = after->isFieldPresent(sfAuditorEncryptedBalance); + + // The core encrypted balances must all exist or not exist at the same time. The auditor + // balance is optional, but cannot exist without the core fields. + if (hasHolderInbox != hasHolderSpending || hasHolderInbox != hasIssuerBalance || + (hasAuditorBalance && !hasIssuerBalance)) + changes_[id].badConsistency = true; + + auto const confidentialBalanceFieldChanged = [&before, &after](auto const& field) { + auto const afterValue = (*after)[~field]; + if (!afterValue) + return false; + + if (!before || before->getType() != ltMPTOKEN) + return true; // LCOV_EXCL_LINE + + return (*before)[~field] != afterValue; + }; + + if (confidentialBalanceFieldChanged(sfConfidentialBalanceInbox) || + confidentialBalanceFieldChanged(sfConfidentialBalanceSpending) || + confidentialBalanceFieldChanged(sfIssuerEncryptedBalance) || + confidentialBalanceFieldChanged(sfAuditorEncryptedBalance)) + { + changes_[id].changesConfidentialFields = true; + } + } + + if (before && before->getType() == ltMPTOKEN_ISSUANCE) + { + uint192 const id = getMptID(before); + auto& change = changes_[id]; + if (before->isFieldPresent(sfConfidentialOutstandingAmount)) + { + change.coaDelta = subtractMPTAmountDelta( + change.coaDelta, before->getFieldU64(sfConfidentialOutstandingAmount)); + } + change.outstandingDelta = subtractMPTAmountDelta( + change.outstandingDelta, before->getFieldU64(sfOutstandingAmount)); + } + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + uint192 const id = getMptID(after); + auto& change = changes_[id]; + + bool const hasCOA = after->isFieldPresent(sfConfidentialOutstandingAmount); + std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0); + std::uint64_t const oa = after->getFieldU64(sfOutstandingAmount); + + if (hasCOA) + change.coaDelta = addMPTAmountDelta(change.coaDelta, coa); + + change.outstandingDelta = addMPTAmountDelta(change.outstandingDelta, oa); + change.issuance = after; + + // COA <= OutstandingAmount + if (coa > oa) + change.badCOA = true; + } + + if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN) + { + uint192 const id = getMptID(after); + + // sfConfidentialBalanceVersion must change when spending changes + auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending]; + auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending]; + auto const versionBefore = (*before)[~sfConfidentialBalanceVersion]; + auto const versionAfter = (*after)[~sfConfidentialBalanceVersion]; + + if (spendingBefore.has_value() && spendingBefore != spendingAfter) + { + if (versionBefore == versionAfter) + changes_[id].badVersion = true; + } + } +} + +bool +ValidConfidentialMPToken::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + if (result != tesSUCCESS) + return true; + + for (auto const& [id, checks] : changes_) + { + // Find the MPTokenIssuance + auto const issuance = [&]() -> std::shared_ptr { + if (checks.issuance) + return checks.issuance; + return view.read(keylet::mptokenIssuance(id)); + }(); + + // Skip all invariance checks if issuance doesn't exist because that means the MPT has been + // deleted + if (!issuance) + continue; + + // Cannot delete MPToken with non-zero confidential state + if (checks.deletedWithEncrypted) + { + if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0) + { + JLOG(j.fatal()) + << "Invariant failed: MPToken deleted with encrypted fields while COA > 0"; + return false; + } + } + + // Encrypted field existence consistency + if (checks.badConsistency) + { + JLOG(j.fatal()) << "Invariant failed: MPToken encrypted field " + "existence inconsistency"; + return false; + } + + // COA <= OutstandingAmount + if (checks.badCOA) + { + JLOG(j.fatal()) << "Invariant failed: Confidential outstanding amount " + "exceeds total outstanding amount"; + return false; + } + + // Confidential balance fields may remain on a holder MPToken after all + // confidential balances have returned to zero. Only creating or + // changing those fields requires the issuance privacy flag. + if (checks.changesConfidentialFields) + { + if (!issuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + { + JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted " + "fields but Issuance does not have " + "lsfMPTCanHoldConfidentialBalance set"; + return false; + } + } + + // We only enforce this when Confidential Outstanding Amount changes (Convert, ConvertBack, + // ConfidentialClawback). This avoids falsely failing on Escrow or AMM operations that lock + // public tokens outside of ltMPTOKEN. Convert / ConvertBack: + // - COA and MPTAmount must have opposite deltas, which cancel each other out to zero. + // - OA remains unchanged. + // - Therefore, the net delta on both sides of the equation is zero. + // + // Clawback: + // - MPTAmount remains unchanged. + // - COA and OA must have identical deltas (mirrored on each side). + // - The equation remains balanced as both sides have equal offsets. + if (checks.coaDelta != 0) + { + if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta) + { + JLOG(j.fatal()) << "Invariant failed: Token conservation " + "violation for MPT " + << to_string(id); + return false; + } + } + else if ( + std::ranges::find(kConfidentialMptTxTypes, tx.getTxnType()) != + kConfidentialMptTxTypes.end()) + { + // Confidential Txns should not modify public MPTAmount balance + // if Confidential Amount Delta is 0 + if (checks.mptAmountDelta != 0) + { + JLOG(j.fatal()) << "Invariant failed: MPTAmount changed by confidential " + "transaction that should not modify this field." + << to_string(id); + return false; + } + + // Among confidential MPT transactions, only ConfidentialMPTSend and + // ConfidentialMPTMergeInbox leave coaDelta unmodified. Therefore, if a confidential MPT + // transaction reaches here, it must be one of these two types, neither of which will + // modify sfOutstandingAmount + if (checks.outstandingDelta != 0) + { + JLOG(j.fatal()) << "Invariant failed: OutstandingAmount changed " + "by confidential transaction that should not " + "modify it for MPT " + << to_string(id); + return false; + } + } + + if (checks.badVersion) + { + JLOG(j.fatal()) + << "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when " + "sfConfidentialBalanceSpending changed"; + return false; + } + } + + return true; +} + void ValidMPTTransfer::visitEntry( bool isDelete, diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp new file mode 100644 index 0000000000..e0d1d28a8f --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -0,0 +1,210 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +NotTEC +ConfidentialMPTClawback::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + auto const account = ctx.tx[sfAccount]; + + // Only issuer can clawback + if (account != MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer()) + return temMALFORMED; + + // Cannot clawback from self + if (account == ctx.tx[sfHolder]) + return temMALFORMED; + + // Check invalid claw amount + auto const clawAmount = ctx.tx[sfMPTAmount]; + if (clawAmount == 0 || clawAmount > kMaxMpTokenAmount) + return temBAD_AMOUNT; + + // Verify proof length + if (ctx.tx[sfZKProof].length() != kEcClawbackProofLength) + return temMALFORMED; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTClawback::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx) +{ + // Check if sender account exists + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + // Check if holder account exists + auto const holder = ctx.tx[sfHolder]; + if (!ctx.view.exists(keylet::account(holder))) + return tecNO_TARGET; + + // Check if MPT issuance exists + auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // Sanity check: account must be the same as issuer + if (sleIssuance->getAccountID(sfIssuer) != account) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Check if issuance has issuer ElGamal public key + if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) + return tecNO_PERMISSION; + + // Check if clawback is allowed + if (!sleIssuance->isFlag(lsfMPTCanClawback)) + return tecNO_PERMISSION; + + // Check if issuance allows confidential transfer + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + return tecNO_PERMISSION; + + // Check holder's MPToken + auto const sleHolderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, holder)); + if (!sleHolderMPToken) + return tecOBJECT_NOT_FOUND; + + // Check if holder has confidential balances to claw back + if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance)) + return tecNO_PERMISSION; + + // Check if Holder has ElGamal public Key + if (!sleHolderMPToken->isFieldPresent(sfHolderEncryptionKey)) + return tecNO_PERMISSION; + + // Sanity check: claw amount can not exceed confidential outstanding amount + // or total outstanding amount (prevents underflow in doApply) + auto const amount = ctx.tx[sfMPTAmount]; + if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) || + amount > (*sleIssuance)[sfOutstandingAmount]) + return tecINSUFFICIENT_FUNDS; + + auto const contextHash = + getClawbackContextHash(account, mptIssuanceID, ctx.tx.getSeqProxy().value(), holder); + + // Verify the revealed confidential amount by the issuer matches the exact + // confidential balance of the holder. + return verifyClawbackProof( + amount, + ctx.tx[sfZKProof], + (*sleIssuance)[sfIssuerEncryptionKey], + (*sleHolderMPToken)[sfIssuerEncryptedBalance], + contextHash); +} + +TER +ConfidentialMPTClawback::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const holder = ctx_.tx[sfHolder]; + + auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); + auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder)); + + if (!sleIssuance || !sleHolderMPToken) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const clawAmount = ctx_.tx[sfMPTAmount]; + + auto const holderPubKey = (*sleHolderMPToken)[sfHolderEncryptionKey]; + auto const issuerPubKey = (*sleIssuance)[sfIssuerEncryptionKey]; + + // After clawback, the balance should be encrypted zero. + auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID); + if (!encZeroForHolder) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID); + if (!encZeroForIssuer) + return tecINTERNAL; // LCOV_EXCL_LINE + + // Set holder's confidential balances to encrypted zero + (*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder; + (*sleHolderMPToken)[sfConfidentialBalanceSpending] = *encZeroForHolder; + (*sleHolderMPToken)[sfIssuerEncryptedBalance] = std::move(*encZeroForIssuer); + incrementConfidentialVersion(*sleHolderMPToken); + + if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance)) + { + // Sanity check: the issuance must have an auditor public key if + // auditing is enabled. + if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey)) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey]; + + auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID); + + if (!encZeroForAuditor) + return tecINTERNAL; // LCOV_EXCL_LINE + + (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor); + } + + // Decrease Global Confidential Outstanding Amount + auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount]; + if (clawAmount > oldCOA) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount; + + // Decrease Global Total Outstanding Amount + auto const oldOA = (*sleIssuance)[sfOutstandingAmount]; + if (clawAmount > oldOA) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount; + + view().update(sleHolderMPToken); + view().update(sleIssuance); + + return tesSUCCESS; +} + +void +ConfidentialMPTClawback::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +ConfidentialMPTClawback::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp new file mode 100644 index 0000000000..44e2596325 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -0,0 +1,350 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +NotTEC +ConfidentialMPTConvert::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot convert + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + if (ctx.tx[sfMPTAmount] > kMaxMpTokenAmount) + return temBAD_AMOUNT; + + if (ctx.tx.isFieldPresent(sfHolderEncryptionKey)) + { + if (!isValidCompressedECPoint(ctx.tx[sfHolderEncryptionKey])) + return temMALFORMED; + + // proof of knowledge of the secret key corresponding to the provided + // public key is needed when holder ec public key is being set. + if (!ctx.tx.isFieldPresent(sfZKProof)) + return temMALFORMED; + + // verify schnorr proof length when registering holder ec public key + if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength) + return temMALFORMED; + } + else + { + // Either both sfHolderEncryptionKey and sfZKProof should be present, or both should be + // absent. + if (ctx.tx.isFieldPresent(sfZKProof)) + return temMALFORMED; + } + + // check encrypted amount format after the above basic checks + // this check is more expensive so put it at the end + if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res)) + return res; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTConvert::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const issuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const amount = ctx.tx[sfMPTAmount]; + + // ensure that issuance exists + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(issuanceID)); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) || + !sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) + { + return tecNO_PERMISSION; + } + + // already checked in preflight, but should also check that issuer on the + // issuance isn't the account either + if (sleIssuance->getAccountID(sfIssuer) == account) + return tefINTERNAL; // LCOV_EXCL_LINE + + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey); + + // tx must include auditor ciphertext if the issuance has enabled + // auditing, and must not include it if auditing is not enabled + if (requiresAuditor != hasAuditor) + return tecNO_PERMISSION; + + auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account)); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + auto const mptIssue = MPTIssue{issuanceID}; + + // Explicit freeze and auth checks are required because accountHolds + // with ZeroIfFrozen/ZeroIfUnauthorized only implicitly rejects + // non-zero amounts. A zero-amount convert would bypass those implicit + // checks, allowing frozen or unauthorized accounts to register ElGamal + // keys and initialize confidential balance fields. + + // Check lock + if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter)) + return ter; + + // Check auth + if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter)) + return ter; + + auto const mptAmount = + STAmount(MPTAmount{static_cast(amount)}, mptIssue); + if (accountHolds( + ctx.view, + account, + mptIssue, + FreezeHandling::ZeroIfFrozen, + AuthHandling::ZeroIfUnauthorized, + ctx.j) < mptAmount) + { + return tecINSUFFICIENT_FUNDS; + } + + auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderEncryptionKey); + auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderEncryptionKey); + + // must have pk to convert + if (!hasHolderKeyOnLedger && !hasHolderKeyInTx) + return tecNO_PERMISSION; + + // can't update if there's already a pk + if (hasHolderKeyOnLedger && hasHolderKeyInTx) + return tecDUPLICATE; + + // Run all verifications before returning any error to prevent timing attacks + // that could reveal which proof failed. + bool valid = true; + + Slice holderPubKey; + if (hasHolderKeyInTx) + { + holderPubKey = ctx.tx[sfHolderEncryptionKey]; + + auto const contextHash = + getConvertContextHash(account, issuanceID, ctx.tx.getSeqProxy().value()); + + if (auto const ter = verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash); + !isTesSuccess(ter)) + { + valid = false; + } + } + else + { + holderPubKey = (*sleMptoken)[sfHolderEncryptionKey]; + } + + std::optional auditor; + if (hasAuditor) + { + auditor.emplace( + ConfidentialRecipient{ + .publicKey = (*sleIssuance)[sfAuditorEncryptionKey], + .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount], + }); + } + + auto const blindingFactor = ctx.tx[sfBlindingFactor]; + if (auto const ter = verifyRevealedAmount( + amount, + Slice(blindingFactor.data(), blindingFactor.size()), + { + .publicKey = holderPubKey, + .encryptedAmount = ctx.tx[sfHolderEncryptedAmount], + }, + { + .publicKey = (*sleIssuance)[sfIssuerEncryptionKey], + .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount], + }, + auditor); + !isTesSuccess(ter)) + { + valid = false; + } + + if (!valid) + return tecBAD_PROOF; + + return tesSUCCESS; +} + +TER +ConfidentialMPTConvert::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); + if (!sleMptoken) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const amtToConvert = ctx_.tx[sfMPTAmount]; + auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); + + if (ctx_.tx.isFieldPresent(sfHolderEncryptionKey)) + (*sleMptoken)[sfHolderEncryptionKey] = ctx_.tx[sfHolderEncryptionKey]; + + // Converting decreases regular balance and increases confidential outstanding. + // The confidential outstanding tracks total tokens in confidential form globally. + auto const currentCOA = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0); + if (amtToConvert > kMaxMpTokenAmount - currentCOA) + return tecINTERNAL; // LCOV_EXCL_LINE + + (*sleMptoken)[sfMPTAmount] = amt - amtToConvert; + (*sleIssuance)[sfConfidentialOutstandingAmount] = currentCOA + amtToConvert; + + auto const holderEc = ctx_.tx[sfHolderEncryptedAmount]; + auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount]; + auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + + // Two cases for Convert: + // 1. Holder already has confidential balances -> homomorphically add to inbox + // 2. First-time convert -> initialize all confidential balance fields + if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && + sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) && + sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) + { + // Case 1: Add to existing inbox balance (holder will merge later) + { + auto sum = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox]); + if (!sum) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvert failed homomorphic add for holder inbox."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*sum); + } + + // homomorphically add issuer's encrypted balance + { + auto sum = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance]); + if (!sum) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvert failed homomorphic add for issuer balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*sum); + } + + // homomorphically add auditor's encrypted balance + if (auditorEc) + { + if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]); + if (!sum) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvert failed homomorphic add for auditor balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*sum); + } + } + else if ( + !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && + !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) && + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) && + !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) + { + // Case 2: First-time convert - initialize all confidential fields + (*sleMptoken)[sfConfidentialBalanceInbox] = holderEc; + (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc; + (*sleMptoken)[sfConfidentialBalanceVersion] = 0; + + if (auditorEc) + (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc; + + // Spending balance starts at zero. Must use canonical zero encryption + // (deterministic ciphertext) so the ledger state is reproducible. + auto zeroBalance = encryptCanonicalZeroAmount( + (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); + + if (!zeroBalance) + return tecINTERNAL; // LCOV_EXCL_LINE + + (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance); + } + else + { + // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should + // exist together + return tecINTERNAL; // LCOV_EXCL_LINE + } + + view().update(sleIssuance); + view().update(sleMptoken); + return tesSUCCESS; +} + +void +ConfidentialMPTConvert::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +ConfidentialMPTConvert::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp new file mode 100644 index 0000000000..d6fed78833 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -0,0 +1,319 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +NotTEC +ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot convert back + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > kMaxMpTokenAmount) + return temBAD_AMOUNT; + + if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment])) + return temMALFORMED; + + // check encrypted amount format after the above basic checks + // this check is more expensive so put it at the end + if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res)) + return res; + + // ConvertBack proof = compact sigma proof (128 bytes) + single bulletproof (688 bytes) + if (ctx.tx[sfZKProof].size() != kEcConvertBackProofLength) + return temMALFORMED; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTConvertBack::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +/** + * Verifies the cryptographic proofs for a ConvertBack transaction. + * + * This function verifies three proofs: + * 1. Revealed amount proof: verifies the encrypted amounts (holder, issuer, + * auditor) all encrypt the same revealed amount using the blinding factor. + * 2. Pedersen linkage proof: verifies the balance commitment is derived from + * the holder's encrypted spending balance. + * 3. Bulletproof (range proof): verifies the remaining balance (balance - amount) + * is non-negative, preventing overdrafts. + * + * All proofs are verified before returning any error to prevent timing attacks. + */ +static TER +verifyProofs( + STTx const& tx, + std::shared_ptr const& issuance, + std::shared_ptr const& mptoken) +{ + if (!mptoken->isFieldPresent(sfHolderEncryptionKey)) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const mptIssuanceID = tx[sfMPTokenIssuanceID]; + auto const account = tx[sfAccount]; + auto const amount = tx[sfMPTAmount]; + auto const blindingFactor = tx[sfBlindingFactor]; + auto const holderPubKey = (*mptoken)[sfHolderEncryptionKey]; + + auto const contextHash = getConvertBackContextHash( + account, + mptIssuanceID, + tx.getSeqProxy().value(), + (*mptoken)[~sfConfidentialBalanceVersion].value_or(0)); + + // Prepare Auditor Info + std::optional auditor; + bool const hasAuditor = issuance->isFieldPresent(sfAuditorEncryptionKey); + if (hasAuditor) + { + auditor.emplace( + ConfidentialRecipient{ + .publicKey = (*issuance)[sfAuditorEncryptionKey], + .encryptedAmount = tx[sfAuditorEncryptedAmount], + }); + } + + // Run all verifications before returning any error to prevent timing attacks + // that could reveal which proof failed. + bool valid = true; + + if (auto const ter = verifyRevealedAmount( + amount, + Slice(blindingFactor.data(), blindingFactor.size()), + { + .publicKey = holderPubKey, + .encryptedAmount = tx[sfHolderEncryptedAmount], + }, + { + .publicKey = (*issuance)[sfIssuerEncryptionKey], + .encryptedAmount = tx[sfIssuerEncryptedAmount], + }, + auditor); + !isTesSuccess(ter)) + { + valid = false; + } + + if (auto const ter = verifyConvertBackProof( + tx[sfZKProof], + holderPubKey, + (*mptoken)[sfConfidentialBalanceSpending], + tx[sfBalanceCommitment], + amount, + contextHash); + !isTesSuccess(ter)) + { + valid = false; + } + + if (!valid) + return tecBAD_PROOF; + + return tesSUCCESS; +} + +TER +ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) +{ + auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const account = ctx.tx[sfAccount]; + auto const amount = ctx.tx[sfMPTAmount]; + + // ensure that issuance exists + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) || + !sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) + return tecNO_PERMISSION; + + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey); + + // tx must include auditor ciphertext if the issuance has enabled + // auditing + if (requiresAuditor && !hasAuditor) + return tecNO_PERMISSION; + + // if auditing is not supported then user should not upload auditor + // ciphertext + if (!requiresAuditor && hasAuditor) + return tecNO_PERMISSION; + + // already checked in preflight, but should also check that issuer on + // the issuance isn't the account either + if (sleIssuance->getAccountID(sfIssuer) == account) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) || + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || + !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance)) + { + return tecNO_PERMISSION; + } + + // Sanity check: holder's MPToken must have auditor balance field if auditing + // is enabled + if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) + return tefINTERNAL; // LCOV_EXCL_LINE + + // if the total circulating confidential balance is smaller than what the + // holder is trying to convert back, we know for sure this txn should + // fail + if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) < amount) + return tecINSUFFICIENT_FUNDS; + + // Check lock + MPTIssue const mptIssue(mptIssuanceID); + if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter)) + return ter; + + // Check auth + if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter)) + return ter; + + if (auto const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken); !isTesSuccess(res)) + return res; + + return tesSUCCESS; +} + +TER +ConfidentialMPTConvertBack::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); + if (!sleMptoken) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const amtToConvertBack = ctx_.tx[sfMPTAmount]; + auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); + + // Converting back increases regular balance and decreases confidential + // outstanding. This is the inverse of Convert. + if (amt > kMaxMpTokenAmount - amtToConvertBack) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack; + + auto const coa = (*sleIssuance)[~sfConfidentialOutstandingAmount].valueOr(0); + if (coa < amtToConvertBack) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleIssuance)[sfConfidentialOutstandingAmount] = coa - amtToConvertBack; + + std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + + // homomorphically subtract holder's encrypted balance + { + auto res = homomorphicSubtract( + (*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount]); + if (!res) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvertBack failed homomorphic subtract for holder spending " + "balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*res); + } + + // homomorphically subtract issuer's encrypted balance + { + auto res = homomorphicSubtract( + (*sleMptoken)[sfIssuerEncryptedBalance], ctx_.tx[sfIssuerEncryptedAmount]); + if (!res) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvertBack failed homomorphic subtract for issuer balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfIssuerEncryptedBalance] = std::move(*res); + } + + if (auditorEc) + { + auto res = homomorphicSubtract( + (*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount]); + if (!res) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTConvertBack failed homomorphic subtract for auditor balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfAuditorEncryptedBalance] = std::move(*res); + } + + incrementConfidentialVersion(*sleMptoken); + + view().update(sleIssuance); + view().update(sleMptoken); + return tesSUCCESS; +} + +void +ConfidentialMPTConvertBack::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +ConfidentialMPTConvertBack::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp new file mode 100644 index 0000000000..02c759c521 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -0,0 +1,150 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +NotTEC +ConfidentialMPTMergeInbox::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot merge + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTMergeInbox::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx) +{ + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + return tecNO_PERMISSION; + + // already checked in preflight, but should also check that issuer on the + // issuance isn't the account either + if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const sleMptoken = + ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) || + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || + !sleMptoken->isFieldPresent(sfHolderEncryptionKey)) + { + return tecNO_PERMISSION; + } + + // Check lock + auto const account = ctx.tx[sfAccount]; + MPTIssue const mptIssue(ctx.tx[sfMPTokenIssuanceID]); + if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter)) + return ter; + + // Check auth + if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter)) + return ter; + + return tesSUCCESS; +} + +TER +ConfidentialMPTMergeInbox::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); + if (!sleMptoken) + return tecINTERNAL; // LCOV_EXCL_LINE + + // sanity check + if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || + !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) || + !sleMptoken->isFieldPresent(sfHolderEncryptionKey)) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // Merge inbox into spending: spending = spending + inbox + // This allows holder to use received funds. Without merging, incoming + // transfers sit in inbox and cannot be spent or converted back. + auto sum = homomorphicAdd( + (*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox]); + if (!sum) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTMergeInbox failed homomorphic add for inbox merge."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*sum); + + // Reset inbox to encrypted zero. Must use canonical zero encryption + // (deterministic ciphertext) so the ledger state is reproducible. + auto zeroEncryption = + encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); + + if (!zeroEncryption) + return tecINTERNAL; // LCOV_EXCL_LINE + + (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption); + + incrementConfidentialVersion(*sleMptoken); + + view().update(sleMptoken); + return tesSUCCESS; +} + +void +ConfidentialMPTMergeInbox::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +ConfidentialMPTMergeInbox::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp new file mode 100644 index 0000000000..302b7d239b --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -0,0 +1,445 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +bool +ConfidentialMPTSend::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); +} + +NotTEC +ConfidentialMPTSend::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + auto const account = ctx.tx[sfAccount]; + auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer(); + + // ConfidentialMPTSend only allows holder to holder, holder to second account, + // and second account to holder transfers. So issuer cannot be the sender. + if (account == issuer) + return temMALFORMED; + + // Can not send to self + if (account == ctx.tx[sfDestination]) + return temMALFORMED; + + // Issuer cannot be the destination + if (ctx.tx[sfDestination] == issuer) + return temMALFORMED; + + // Check the length of the encrypted amounts + if (ctx.tx[sfSenderEncryptedAmount].length() != kEcGamalEncryptedTotalLength || + ctx.tx[sfDestinationEncryptedAmount].length() != kEcGamalEncryptedTotalLength || + ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + { + return temBAD_CIPHERTEXT; + } + + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + // Check the length of the ZKProof (fixed size regardless of recipient count) + if (ctx.tx[sfZKProof].length() != kEcSendProofLength) + return temMALFORMED; + + // Check the Pedersen commitments are valid + if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) || + !isValidCompressedECPoint(ctx.tx[sfAmountCommitment])) + { + return temMALFORMED; + } + + // Check the encrypted amount formats, this is more expensive so put it at + // the end + if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) || + !isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) || + !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount])) + { + return temBAD_CIPHERTEXT; + } + + if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) + return temBAD_CIPHERTEXT; + + if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTSend::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +namespace detail { + +static TER +verifySendProofs( + PreclaimContext const& ctx, + std::shared_ptr const& sleSenderMPToken, + std::shared_ptr const& sleDestinationMPToken, + std::shared_ptr const& sleIssuance) +{ + // Sanity check + if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + + std::optional auditor; + if (hasAuditor) + { + auditor.emplace( + ConfidentialRecipient{ + .publicKey = (*sleIssuance)[sfAuditorEncryptionKey], + .encryptedAmount = ctx.tx[sfAuditorEncryptedAmount], + }); + } + + auto const contextHash = getSendContextHash( + ctx.tx[sfAccount], + ctx.tx[sfMPTokenIssuanceID], + ctx.tx.getSeqProxy().value(), + ctx.tx[sfDestination], + (*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0)); + + return verifySendProof( + ctx.tx[sfZKProof], + { + .publicKey = (*sleSenderMPToken)[sfHolderEncryptionKey], + .encryptedAmount = ctx.tx[sfSenderEncryptedAmount], + }, + { + .publicKey = (*sleDestinationMPToken)[sfHolderEncryptionKey], + .encryptedAmount = ctx.tx[sfDestinationEncryptedAmount], + }, + { + .publicKey = (*sleIssuance)[sfIssuerEncryptionKey], + .encryptedAmount = ctx.tx[sfIssuerEncryptedAmount], + }, + auditor, + (*sleSenderMPToken)[sfConfidentialBalanceSpending], + ctx.tx[sfAmountCommitment], + ctx.tx[sfBalanceCommitment], + contextHash); +} + +} // namespace detail + +TER +ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) +{ + // Check if sender account exists + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + // Check if destination account exists + auto const destination = ctx.tx[sfDestination]; + auto const sleDst = ctx.view.read(keylet::account(destination)); + if (!sleDst) + return tecNO_TARGET; + + // Check destination tag + if (((sleDst->getFlags() & lsfRequireDestTag) != 0u) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + { + return tecDST_TAG_NEEDED; + } + + // Check if MPT issuance exists + auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // Check if the issuance allows transfer + if (!sleIssuance->isFlag(lsfMPTCanTransfer)) + return tecNO_AUTH; + + // Check if issuance allows confidential transfer + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + return tecNO_PERMISSION; + + // Sanity check: transfer fee must be 0 for confidential MPTs. This should + // be unreachable in valid ledger state because MPTokenIssuanceCreate and + // MPTokenIssuanceSet enforce it. + if ((*sleIssuance)[~sfTransferFee].value_or(0) > 0) + return tecNO_PERMISSION; + + // Check if issuance has issuer ElGamal public key + if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) + return tecNO_PERMISSION; + + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey); + + // Tx must include auditor ciphertext if the issuance has enabled + // auditing, and must not include it if auditing is not enabled + if (requiresAuditor != hasAuditor) + return tecNO_PERMISSION; + + // Sanity check: issuer isn't the sender + if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) + return tefINTERNAL; // LCOV_EXCL_LINE + + // Check sender's MPToken existence + auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); + if (!sleSenderMPToken) + return tecOBJECT_NOT_FOUND; + + // Check sender's MPToken has necessary fields for confidential send + if (!sleSenderMPToken->isFieldPresent(sfHolderEncryptionKey) || + !sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) || + !sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance)) + { + return tecNO_PERMISSION; + } + + // Check destination's MPToken existence + auto const sleDestinationMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, destination)); + if (!sleDestinationMPToken) + return tecOBJECT_NOT_FOUND; + + // Check destination's MPToken has necessary fields for confidential send + if (!sleDestinationMPToken->isFieldPresent(sfHolderEncryptionKey) || + !sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) || + !sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance)) + { + return tecNO_PERMISSION; + } + + // Sanity check: Both MPTokens' auditor fields must be present if auditing + // is enabled + if (requiresAuditor && + (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) || + !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance))) + { + return tefINTERNAL; // LCOV_EXCL_LINE + } + + // Check lock + MPTIssue const mptIssue(mptIssuanceID); + if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter)) + return ter; + + if (auto const ter = checkFrozen(ctx.view, destination, mptIssue); !isTesSuccess(ter)) + return ter; + + // Check auth + if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter)) + return ter; + + if (auto const ter = requireAuth(ctx.view, mptIssue, destination); !isTesSuccess(ter)) + return ter; + + if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j); + !isTesSuccess(err)) + return err; + + // Check deposit preauth before the expensive ZK proof verification. + // Uses read-only view. + auto const preauthErr = + checkDepositPreauth(ctx.tx, ctx.view, account, destination, sleDst, ctx.j); + if (!isTesSuccess(preauthErr)) + return preauthErr; + + return detail::verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance); +} + +TER +ConfidentialMPTSend::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + auto const destination = ctx_.tx[sfDestination]; + + auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); + auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination)); + auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID)); + + auto const sleDestAcct = view().read(keylet::account(destination)); + + if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct) + return tecINTERNAL; // LCOV_EXCL_LINE + + // Deposit preauth authorization was already verified in preclaim. + // Remove any expired credentials. + if (auto err = cleanupExpiredCredentials(ctx_.tx, ctx_.view(), ctx_.journal); + !isTesSuccess(err)) + return err; + + auto const senderEc = ctx_.tx[sfSenderEncryptedAmount]; + auto const destEc = ctx_.tx[sfDestinationEncryptedAmount]; + auto const issuerEc = ctx_.tx[sfIssuerEncryptedAmount]; + auto const proof = ctx_.tx[sfZKProof]; + Slice const sendChallenge{proof.data(), kEcBlindingFactorLength}; + + auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + + // Subtract from sender's spending balance + { + auto const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending]; + auto newSpending = homomorphicSubtract(curSpending, senderEc); + if (!newSpending) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic subtract for sender spending balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleSenderMPToken)[sfConfidentialBalanceSpending] = std::move(*newSpending); + } + + // Subtract from issuer's balance + { + auto const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance]; + auto newIssuerEnc = homomorphicSubtract(curIssuerEnc, issuerEc); + if (!newIssuerEnc) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic subtract for sender issuer balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleSenderMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc); + } + + // Subtract from auditor's balance if present + if (auditorEc) + { + auto const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance]; + auto newAuditorEnc = homomorphicSubtract(curAuditorEnc, *auditorEc); + if (!newAuditorEnc) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic subtract for sender auditor balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleSenderMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc); + } + + // Add to destination's inbox balance + { + auto rerandomizedDestEc = rerandomizeCiphertext( + destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge); + if (!rerandomizedDestEc) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox]; + auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc); + if (!newInbox) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic add for destination inbox."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleDestinationMPToken)[sfConfidentialBalanceInbox] = std::move(*newInbox); + } + + // Add to issuer's balance + { + auto rerandomizedIssuerEc = + rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge); + if (!rerandomizedIssuerEc) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance]; + auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc); + if (!newIssuerEnc) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic add for destination issuer balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleDestinationMPToken)[sfIssuerEncryptedBalance] = std::move(*newIssuerEnc); + } + + // Add to auditor's balance if present + if (auditorEc) + { + auto rerandomizedAuditorEc = rerandomizeCiphertext( + *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge); + if (!rerandomizedAuditorEc) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance]; + auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc); + if (!newAuditorEnc) + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed homomorphic add for destination auditor balance."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + (*sleDestinationMPToken)[sfAuditorEncryptedBalance] = std::move(*newAuditorEnc); + } + + // increment sender version only; receiver version is not modified by incoming sends + incrementConfidentialVersion(*sleSenderMPToken); + + view().update(sleSenderMPToken); + view().update(sleDestinationMPToken); + return tesSUCCESS; +} + +void +ConfidentialMPTSend::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +ConfidentialMPTSend::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index e1d9daaada..6db37515cb 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -83,6 +83,25 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked)) return tecNO_PERMISSION; + if (ctx.view.rules().enabled(featureConfidentialTransfer)) + { + auto const sleMptIssuance = + ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID])); + + // if there still existing encrypted balances of MPT in + // circulation + if (sleMptIssuance && + (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) != 0) + { + // this MPT still has encrypted balance, since we don't know + // if it's non-zero or not, we won't allow deletion of + // MPToken + if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) || + sleMpt->isFieldPresent(sfConfidentialBalanceSpending)) + return tecHAS_OBLIGATIONS; + } + } + return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 68956a533d..e30127b688 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -37,7 +37,15 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT)) return false; - return true; + if (ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance) && + !ctx.rules.enabled(featureConfidentialTransfer)) + return false; + + // can not set tmfMPTCannotEnableCanHoldConfidentialBalance without featureConfidentialTransfer + auto const mutableFlags = ctx.tx[~sfMutableFlags]; + return !mutableFlags || + ((*mutableFlags & tmfMPTCannotEnableCanHoldConfidentialBalance) == 0u) || + ctx.rules.enabled(featureConfidentialTransfer); } std::uint32_t @@ -70,6 +78,10 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) // must also be set. if (fee > 0u && !ctx.tx.isFlag(tfMPTCanTransfer)) return temMALFORMED; + + // Confidential amounts are encrypted so transfer rate is disallowed. + if (fee > 0u && ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance)) + return temBAD_TRANSFER_FEE; } if (auto const domain = ctx.tx[~sfDomainID]) diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index e200c9762a..d526251069 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -74,11 +75,27 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) auto const metadata = ctx.tx[~sfMPTokenMetadata]; auto const transferFee = ctx.tx[~sfTransferFee]; auto const isMutate = mutableFlags || metadata || transferFee; + auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey); + auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey); + auto const txFlags = ctx.tx.getFlags(); + + bool const enablePrivacy = + mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u; + + auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID); + auto const hasHolder = ctx.tx.isFieldPresent(sfHolder); if (isMutate && !ctx.rules.enabled(featureDynamicMPT)) return temDISABLED; - if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder)) + if ((hasIssuerElGamalKey || hasAuditorElGamalKey || enablePrivacy) && + !ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + if (hasDomain && hasHolder) + return temMALFORMED; + + if (enablePrivacy && hasHolder) return temMALFORMED; // fails if both flags are set @@ -90,10 +107,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (holderID && accountID == holderID) return temMALFORMED; - if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT)) + if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT) || + ctx.rules.enabled(featureConfidentialTransfer)) { // Is this transaction actually changing anything ? - if (ctx.tx.getFlags() == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate) + if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey && + !isMutate) return temMALFORMED; } @@ -110,6 +129,9 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (transferFee && *transferFee > kMaxTransferFee) return temBAD_TRANSFER_FEE; + if (transferFee && *transferFee > 0u && enablePrivacy) + return temBAD_TRANSFER_FEE; + if (metadata && metadata->length() > kMaxMpTokenMetadataLength) return temMALFORMED; @@ -120,6 +142,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) } } + if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey)) + return temMALFORMED; + + if (hasAuditorElGamalKey && !hasIssuerElGamalKey) + return temMALFORMED; + + if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey])) + return temMALFORMED; + + if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorEncryptionKey])) + return temMALFORMED; + return tesSUCCESS; } @@ -181,12 +215,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) return currentMutableFlags & mutableFlag; }; - if (auto const mutableFlags = ctx.tx[~sfMutableFlags]) + auto const mutableFlags = ctx.tx[~sfMutableFlags]; + // Whether the transaction is enabling confidential amounts. + bool const enablesConfidentialAmount = + mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u; + if (mutableFlags) { if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) { return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u); })) return tecNO_PERMISSION; + + if (enablesConfidentialAmount && + isMutableFlag(lsmfMPTCannotEnableCanHoldConfidentialBalance)) + return tecNO_PERMISSION; } if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata)) @@ -201,10 +243,55 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer)) return tecNO_PERMISSION; + // Cannot set a non-zero TransferFee on an issuance that has confidential + // transfer enabled + if (fee > 0u && sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance)) + return tecNO_PERMISSION; + if (!isMutableFlag(lsmfMPTCanMutateTransferFee)) return tecNO_PERMISSION; } + // cannot update issuer public key + if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) && + sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey)) + { + return tecNO_PERMISSION; + } + + // cannot update auditor public key + if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) && + sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey)) + { + return tecNO_PERMISSION; // LCOV_EXCL_LINE + } + + if (enablesConfidentialAmount && sleMptIssuance->isFieldPresent(sfTransferFee) && + (*sleMptIssuance)[sfTransferFee] > 0u) + return tecNO_PERMISSION; + + // Encryption keys can only be set if confidential amounts are already + // enabled on the issuance OR if the transaction is enabling it + if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) && + !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount) + { + return tecNO_PERMISSION; + } + + if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) && + !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount) + { + return tecNO_PERMISSION; + } + + // cannot upload key if there's circulating supply of COA + if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) || + ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialAmount) && + (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0) + { + return tecNO_PERMISSION; // LCOV_EXCL_LINE + } + return tesSUCCESS; } @@ -249,6 +336,9 @@ MPTokenIssuanceSet::doApply() flagsOut |= f.ledgerFlag; } } + + if ((mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u) + flagsOut |= lsfMPTCanHoldConfidentialBalance; } if (flagsIn != flagsOut) @@ -300,6 +390,26 @@ MPTokenIssuanceSet::doApply() } } + if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey]) + { + // This is enforced in preflight. + XRPL_ASSERT( + sle->getType() == ltMPTOKEN_ISSUANCE, + "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance"); + + sle->setFieldVL(sfIssuerEncryptionKey, *pubKey); + } + + if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey]) + { + // This is enforced in preflight. + XRPL_ASSERT( + sle->getType() == ltMPTOKEN_ISSUANCE, + "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance"); + + sle->setFieldVL(sfAuditorEncryptionKey, *pubKey); + } + view().update(sle); return tesSUCCESS; diff --git a/src/test/app/ConfidentialTransferExtended_test.cpp b/src/test/app/ConfidentialTransferExtended_test.cpp new file mode 100644 index 0000000000..0aee7516c9 --- /dev/null +++ b/src/test/app/ConfidentialTransferExtended_test.cpp @@ -0,0 +1,2595 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { + +class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase +{ + void + testSendDepositPreauth(FeatureBitset features) + { + testcase("Send deposit preauth"); + using namespace test::jtx; + + // When an account enables lsfDepositAuth (via asfDepositAuth flag), + // it requires explicit authorization before accepting incoming payments. + // + // There are two authorization mechanisms: + // + // 1. DIRECT ACCOUNT AUTHORIZATION (deposit::auth) + // - Bob directly authorizes Carol: deposit::auth(bob, carol) + // - Simple 1-to-1 trust relationship + // - Carol can send to Bob without credentials + // + // 2. CREDENTIAL-BASED AUTHORIZATION (deposit::authCredentials) + // - A trusted third party (dpIssuer) issues credentials + // - Bob authorizes a credential TYPE from an issuer + // - Anyone holding that credential can send to Bob + // - Requires sender to include credential ID in transaction + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dpIssuer("dpIssuer"); + char const credType[] = "KYC_VERIFIED"; + + // Create and accept credential for an account + auto createCredential = [&](Env& env, Account const& subject) -> std::string { + env(credentials::create(subject, dpIssuer, credType)); + env.close(); + env(credentials::accept(subject, dpIssuer, credType)); + env.close(); + auto const jv = credentials::ledgerEntry(env, subject, dpIssuer, credType); + return jv[jss::result][jss::index].asString(); + }; + + // TEST 1: Direct Account Authorization + { + Env env(*this, features); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + env(fset(bob, asfDepositAuth)); + env.close(); + + // Carol cannot send to Bob without authorization + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // Bob directly authorizes Carol + env(deposit::auth(bob, carol)); + env.close(); + + // Now Carol can send to Bob + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + }); + mpt.mergeInbox({ + .account = bob, + }); + + // Bob revokes Carol's authorization + env(deposit::unauth(bob, carol)); + env.close(); + + // Carol can no longer send to Bob + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + } + + // TEST 2: Credential-Based Authorization + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const credIdx = createCredential(env, carol); + + // Carol cannot send yet - Bob hasn't authorized this credential type + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecNO_PERMISSION, + }); + + // Bob authorizes the credential type from dpIssuer + env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}})); + env.close(); + + // Carol still cannot send without including credential + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // Carol CAN send when including her credential + mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}}); + mpt.mergeInbox({ + .account = bob, + }); + } + + // TEST 3: Direct Auth Takes Precedence Over Credentials + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const credIdx = createCredential(env, carol); + + // Bob directly authorizes Carol (no credential needed) + env(deposit::auth(bob, carol)); + env.close(); + + // Carol can send without credentials (direct auth) + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + }); + mpt.mergeInbox({ + .account = bob, + }); + + // Carol can also send WITH credentials (still works) + mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}}); + mpt.mergeInbox({ + .account = bob, + }); + + // Bob revokes direct authorization + env(deposit::unauth(bob, carol)); + env.close(); + + // Carol cannot send without credentials anymore + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // But credential-based auth not set up, so this also fails + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecNO_PERMISSION, + }); + + // Bob authorizes the credential type + env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}})); + env.close(); + + // Now Carol can send with credentials + mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}}); + } + + auto const expireTime = 30; + + // Lambda function that returns the credential index after creating a + // credential that expires shortly after the current ledger time. + auto createExpiringCredential = [&](Env& env, Account const& subject) -> std::string { + auto jv = credentials::create(subject, dpIssuer, credType); + auto const expiry = + env.current()->header().parentCloseTime.time_since_epoch().count() + expireTime; + jv[sfExpiration.jsonName] = expiry; + env(jv); + env.close(); + env(credentials::accept(subject, dpIssuer, credType)); + env.close(); + auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType); + return credentials[jss::result][jss::index].asString(); + }; + + auto credentialDeleted = [&](Env& env, Account const& subject) -> bool { + auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType); + return credentials[jss::result].isMember(jss::error) && + credentials[jss::result][jss::error] == "entryNotFound"; + }; + + // TEST 4: Expired credential with matching depositPreauth entry. + // checkDepositPreauth in preclaim returns tesSUCCESS (the expired + // credential still exists and matches the depositPreauth key), so ZK + // proofs run. cleanupExpiredCredentials in doApply then removes the + // expired credential and returns tecEXPIRED. + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const credIdx = createExpiringCredential(env, carol); + + // Bob authorizes carol's credential type + env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}})); + env.close(); + + // Advance ledger past credential expiration + env.close(std::chrono::seconds(expireTime)); + + // Send fails with tecEXPIRED; the expired credential is cleaned up + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecEXPIRED, + }); + env.close(); + + BEAST_EXPECT(credentialDeleted(env, carol)); + } + + // TEST 5: Expired credential, destination has no depositAuth. + // checkDepositPreauth in preclaim returns tesSUCCESS even with expired credentials, + // because we want to keep the checkDepositPreauth part before the expensive proof + // verification. cleanupExpiredCredentials in doApply removes the expired credential and + // returns tecEXPIRED. + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + auto const credIdx = createExpiringCredential(env, carol); + + // Advance ledger past credential expiration + env.close(std::chrono::seconds(expireTime)); + + // Send fails with tecEXPIRED; the expired credential is cleaned up + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecEXPIRED, + }); + env.close(); + + BEAST_EXPECT(credentialDeleted(env, carol)); + } + + // TEST 6: Expired credential, depositAuth enabled but credential + // not authorized by bob. + // checkDepositPreauth in preclaim calls checkDepositPreauth which + // finds no match and returns tecNO_PERMISSION. doApply never runs, so + // the expired credential is not cleaned up by this transaction. This is + // a deliberate tradeoff: allowing doApply to run solely for cleanup + // would require bypassing the preclaim short-circuit, forcing every + // validator to run the expensive ZK proof verification before + // discovering the authorization failure. Expired credentials here will + // be cleaned up opportunistically by a future transaction that + // references them. + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const credIdx = createExpiringCredential(env, carol); + + // Advance ledger past credential expiration + env.close(std::chrono::seconds(expireTime)); + + // Fails with tecNO_PERMISSION. + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecNO_PERMISSION, + }); + env.close(); + + // Expired credential is not deleted + BEAST_EXPECT(!credentialDeleted(env, carol)); + } + } + + void + testSendCredentialValidation(FeatureBitset features) + { + testcase("Send credential validation"); + using namespace test::jtx; + + // Tests for credentials::checkFields (preflight) and + // credentials::valid (preclaim) validation. + // + // Preflight checks (temMALFORMED): + // - Empty credentials array + // - Array size exceeds maxCredentialsArraySize (8) + // - Duplicate credential IDs in array + // + // Preclaim checks (tecBAD_CREDENTIALS): + // - Credential doesn't exist + // - Credential doesn't belong to source account + // - Credential not accepted (lsfAccepted flag not set) + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dpIssuer("dpIssuer"); + char const credType[] = "KYC"; + + // TEST 1: Preflight - Empty Credentials Array + { + Env env(*this, features); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = std::vector{}, + .err = temMALFORMED, + }); + } + + // TEST 2: Preflight - Credentials Array Too Large + { + Env env(*this, features); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + std::vector tooManyCredentials; + tooManyCredentials.reserve(9); + for (int i = 0; i < 9; ++i) + tooManyCredentials.push_back(to_string(uint256(i))); + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = tooManyCredentials, + .err = temMALFORMED, + }); + } + + // TEST 3: Preflight - Duplicate Credentials + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + env(credentials::create(carol, dpIssuer, credType)); + env.close(); + env(credentials::accept(carol, dpIssuer, credType)); + env.close(); + + auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx, credIdx}}, + .err = temMALFORMED, + }); + } + + // TEST 4: Preclaim - Credential Doesn't Exist + { + Env env(*this, features); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + std::string const fakeCredIdx = to_string(uint256(999)); + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{fakeCredIdx}}, + .err = tecBAD_CREDENTIALS, + }); + } + + // TEST 5: Preclaim - Credential Doesn't Belong to Source Account + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + // Create credential for BOB (not carol) + env(credentials::create(bob, dpIssuer, credType)); + env.close(); + env(credentials::accept(bob, dpIssuer, credType)); + env.close(); + + auto const jv = credentials::ledgerEntry(env, bob, dpIssuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecBAD_CREDENTIALS, + }); + } + + // TEST 6: Preclaim - Credential Not Accepted + { + Env env(*this, features); + env.fund(XRP(50000), dpIssuer); + env.close(); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + // Create credential but DON'T accept it + env(credentials::create(carol, dpIssuer, credType)); + env.close(); + + auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{credIdx}}, + .err = tecBAD_CREDENTIALS, + }); + } + + // TEST 7: Preflight - sfCredentialIDs requires featureCredentials. + // Even with featureConfidentialTransfer enabled, supplying + // CredentialIDs while featureCredentials is disabled must be + // rejected in preflight via checkExtraFeatures. + { + Env env(*this, features - featureCredentials); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 50}}}; + auto& mpt = confEnv.mpt; + + auto constexpr kCredIdx = + "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288BE4"; + + mpt.send({ + .account = carol, + .dest = bob, + .amt = 10, + .credentials = {{kCredIdx}}, + .err = temDISABLED, + }); + } + } + + // Bob creates the AMM, but Bob is not the MPT holder checked below. + // The AMM has its own pseudo-account (`ammHolder`) that can hold the + // public MPT pool balance. That pseudo-account cannot normally + // initialize confidential state because the confidential txn's must be + // signed by sfAccount, and the AMM pseudo-account has no signing key. + // So this is a construction/impossibility test: public AMM MPT state exists + // but the corresponding confidential AMM clawback flow is not normally reachable. + void + testAMMHolderCannotHaveConfidentialStateClawback(FeatureBitset features) + { + testcase("AMM holder cannot have confidential state"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + + for (bool const enablePseudoAccount : {false, true}) + { + Env env{ + *this, + enablePseudoAccount ? features | featureSingleAssetVault + : features - featureSingleAssetVault}; + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .flags = kMptDexFlags | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 1'000); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + AMM const amm(env, bob, XRP(100), mptAlice(100)); + Account const ammHolder("amm", amm.ammAccount()); + auto const ammSle = env.le(keylet::account(ammHolder.id())); + + BEAST_EXPECT(ammSle && ammSle->isFieldPresent(sfAMMID)); + BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100); + + BEAST_EXPECT(!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox)); + BEAST_EXPECT( + !mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending)); + BEAST_EXPECT( + !mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance)); + BEAST_EXPECT( + !mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance)); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = ammHolder, + .amt = 100, + .proof = strHex(gMakeZeroBuffer(kEcClawbackProofLength)), + .err = tecNO_PERMISSION, + }); + } + } + + // Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet, + // Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account + // sequence numbers. + void + testWithTickets(FeatureBitset features) + { + testcase("Confidential transfer with tickets"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + + // MPTokenIssuanceSet with ticket, registers alice's issuer key. + { + std::uint32_t const ticketSeq = env.seq(alice) + 1; + env(ticket::create(alice, 1)); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice), .ticketSeq = ticketSeq}); + } + + // ConfidentialMPTConvert with ticket, first convert registers bob's key. + { + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + .ticketSeq = ticketSeq, + }); + env.require(MptBalance(mptAlice, bob, 50)); + } + + // ConfidentialMPTConvert with ticket + { + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + mptAlice.convert({.account = bob, .amt = 20, .ticketSeq = ticketSeq}); + env.require(MptBalance(mptAlice, bob, 30)); + } + + // ConfidentialMPTMergeInbox with ticket. + { + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq}); + } + + mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)}); + mptAlice.mergeInbox({.account = carol}); + + // ConfidentialMPTSend with ticket. + { + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .ticketSeq = ticketSeq}); + } + + // Merge carol's inbox so her spending balance includes the received send. + mptAlice.mergeInbox({.account = carol}); + + // ConfidentialMPTConvertBack with ticket. + // The convertBack proof context hash must use the ticket sequence. + { + std::uint32_t const ticketSeq = env.seq(carol) + 1; + env(ticket::create(carol, 1)); + mptAlice.convertBack({.account = carol, .amt = 10, .ticketSeq = ticketSeq}); + // carol converted 50, received 10 from bob, then converted back 10 → public 60 + env.require(MptBalance(mptAlice, carol, 60)); + } + } + + // Verifies that cryptographic proofs in Convert transactions are bound to + // the ticket sequence rather than the account sequence. + // A proof built with the ticket sequence passes. + void + testConvertTicketProofBinding(FeatureBitset features) + { + testcase("Convert proof binds to ticket sequence"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + mptAlice.generateKeyPair(bob); + + uint64_t const amt = 30; + Buffer const bf = generateBlindingFactor(); + Buffer const holderCt = mptAlice.encryptAmount(bob, amt, bf); + Buffer const issuerCt = mptAlice.encryptAmount(alice, amt, bf); + + std::uint32_t const ticketSeq1 = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + + // Invalid: Schnorr proof built with the account seq (env.seq(bob)) rather + // than the ticket seq (ticketSeq1). + { + BEAST_EXPECT(env.seq(bob) != ticketSeq1); + uint256 const badCtxHash = + getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob)); + auto const badProof = requireOptional( + mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof."); + + mptAlice.convert({ + .account = bob, + .amt = amt, + .proof = strHex(badProof), + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .ticketSeq = ticketSeq1, + .err = tecBAD_PROOF, + }); + } + + std::uint32_t const ticketSeq2 = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + + // Valid: proof auto-generated by convert() using ticketSeq2; context hashes match. + mptAlice.convert({ + .account = bob, + .amt = amt, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .ticketSeq = ticketSeq2, + }); + env.require(MptBalance(mptAlice, bob, 70)); + } + + // Exercises ticket-specific error codes for confidential transfer transactions: + void + testDestinationTag(FeatureBitset features) + { + testcase("test Destination Tag"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + // Set RequireDest on carol + env(fset(carol, asfRequireDest)); + env.close(); + + // Send without destination tag — rejected + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecDST_TAG_NEEDED, + }); + + // Send with destination tag — succeeds (passes preclaim, + // reaches ZKP verification with the real proof) + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .destinationTag = 42}); + + // Verify the destination tag is in the confirmed transaction + auto const tx = env.tx(); + BEAST_EXPECT(tx); + BEAST_EXPECT(tx->isFieldPresent(sfDestinationTag)); + BEAST_EXPECT((*tx)[sfDestinationTag] == 42); + + env(fclear(carol, asfRequireDest)); + env.close(); + + // Send without destination tag when not required — succeeds + mptAlice.mergeInbox({.account = carol}); + mptAlice.send({.account = bob, .dest = carol, .amt = 10}); + } + + // terPRE_TICKET when the ticket doesn't exist yet, and tefNO_TICKET when + // the ticket has already been consumed or was never created. + void + testTicketErrors(FeatureBitset features) + { + testcase("Confidential transfer ticket errors"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + mptAlice.generateKeyPair(bob); + + // Give bob an inbox balance so MergeInbox has something to merge. + mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)}); + + // Use MergeInbox as the confidential transfer transaction under test + // so that ticket errors are isolated from cryptographic verification. + + // terPRE_TICKET: ticket sequence is far in the future and hasn't been created. + mptAlice.mergeInbox( + {.account = bob, .ticketSeq = env.seq(bob) + 100, .err = terPRE_TICKET}); + + // Create one ticket and use it successfully. + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq}); + + // tefNO_TICKET: attempt to reuse the same (already-consumed) ticket. + mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq, .err = tefNO_TICKET}); + + // tefNO_TICKET: ticket sequence is in the past but was never created. + mptAlice.mergeInbox({.account = bob, .ticketSeq = 1, .err = tefNO_TICKET}); + } + + // Bob sends 100 MPT to Carol. Carol Merge Inbox. Carol sends 50 MPT to Dave. + // Inner 3rd txn (Carol sends to Dave) fails because the proof is built with + // when Carols's spending balance is 0. (before she received funds from Bob) + // + // Also tests Bob sending to two recipients (Carol and Dave) in a single + // batch. Even though Bob has enough balance for both, the second send's + // balance-linkage proof becomes incorrect once inner 1 updates Bob's encrypted + // spending, so fails + void + testBatchConfidentialSend(FeatureBitset features) + { + testcase("Batch confidential send - merge inbox dependency"); + using namespace test::jtx; + + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob = A (100 spending), carol = B (0), dave = C (0) + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + // Build the batch: + // Batch Txn 1 bob -> carol 100 : valid proof, bob spending=100 + // Batch Txn 2 carol -> mergeInbox : valid JV + // Batch Txn 3 carol->dave 50 : Invalid + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + // 3 signers, Bob, Carol, Dave + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1); + auto const jv2 = mpt.mergeInboxJV({.account = carol}); + auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 50}, carolSeq + 1); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Inner(jv3, carolSeq + 1), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // AllOrNothing: inner 3 fails + // bob's spending must remain 100; carol's inbox must remain 0. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + + // Bob sends to two recipients (Carol and Dave) in one batch. + // Bob has 150, enough for both sends individually. However, batch txn 1 + // changes Bob's encrypted spending on the ledger; batch txn 2 was built + // against the old enc(150) so its balance-linkage proof is stale. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 150, 0); + + // tfAllOrNothing — rejects the whole batch as 2nd txn proof is incorrect + { + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // Nothing applied: bob stays 150, carol and dave inbox stay 0. + BEAST_EXPECT( + mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 150); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + + // If we change batch mode to be tfIndependent — txn 1 applies, inner 2 fails. + { + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2); + + env(batch::outer(bob, bobSeq, batchFee, tfIndependent), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // bob 150→100, carol inbox 0→50 + BEAST_EXPECT( + mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 50); + // dave gets nothing + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + } + + // Now, Bob sends Confidential MPT to 2 accounts in one batch. + // However this time, the second txn proof is calculated using the + // correct encrypted(spending) proof, so it should pass. + { + // bob has exactly enough for both sends. + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 200, 0); + + { + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1 is built against the current ledger state (spending=200). + auto const jv1 = + mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1); + + // Compute post-jv1 state without touching the ledger. + auto const chain1 = mpt.chainAfterSend(bob, 100, jv1); + + // jv2 proof is built against predicted spending=100, version=N+1. + auto const jv2 = + mpt.sendJV({.account = bob, .dest = dave, .amt = 100}, bobSeq + 2, chain1); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // Both txns applied: bob 200→0, carol inbox=100, dave inbox=100. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 0); + BEAST_EXPECT( + mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 100); + } + + // Now Bob has 150, but tries to send two 100 in one batch. + // This fails because Bob doesn't have enough MPT balance. + { + Env env2{*this, features}; + Account const alice2("alice"); + Account const bob2("bob"); + Account const carol2("carol"); + Account const dave2("dave"); + + MPTTester mpt2(env2, alice2, {.holders = {bob2, carol2, dave2}}); + setupBatchEnv(mpt2, alice2, bob2, carol2, dave2, 150, 0); + + auto const bobSeq = env2.seq(bob2); + auto const batchFee = batch::calcConfidentialBatchFee(env2, 0, 2); + + auto const jv1 = + mpt2.sendJV({.account = bob2, .dest = carol2, .amt = 100}, bobSeq + 1); + auto const chain1 = mpt2.chainAfterSend(bob2, 100, jv1); + + auto const jv2 = + mpt2.sendJV({.account = bob2, .dest = dave2, .amt = 100}, bobSeq + 2, chain1); + + env2( + batch::outer(bob2, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env2.close(); + + // AllOrNothing: inner 2 fails → nothing applied. + BEAST_EXPECT( + mpt2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 150); + BEAST_EXPECT( + mpt2.getDecryptedBalance(carol2, MPTTester::holderEncryptedInbox) == 0); + BEAST_EXPECT(mpt2.getDecryptedBalance(dave2, MPTTester::holderEncryptedInbox) == 0); + } + } + } + void + testBatchConfidentialConvertAndConvertBack(FeatureBitset features) + { + testcase("Batch confidential convert and convertBack"); + using namespace test::jtx; + + // convert + convertBack in one AllOrNothing batch, both valid. + // + // Bob has regular=50, spending=100. + // jv1: convert 50 regular → inbox (Schnorr proof; does NOT touch spending/version) + // jv2: convertBack 30 spending → regular (proof against spending=100, version=V) + // + // Since jv1 leaves spending and version unchanged, jv2's proof is still + // valid when it executes, so both inner txns succeed. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob: spending=100, regular=0 after setupBatchEnv; + // pay 50 more to give bob regular MPT to convert in the batch. + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + mpt.pay(alice, bob, 50); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1: convert 50 regular MPT into confidential inbox + auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1); + // jv2: convert 30 spending back to regular MPT + auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 2); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // regular (mptAmount): 50 (pre) - 50 (convert) + 30 (convertBack) = 30 + // spending balance: 100 - 30 = 70 + // inbox: 0 + 50 (from convert) = 50 + env.require(MptBalance(mpt, bob, 30)); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 50); + } + + // convert + mergeInbox + convertBack, stale convertBack proof. + // + // jv1: convert 50 regular → inbox + // jv2: mergeInbox (inbox 50 → spending, version V → V+1) + // jv3: convertBack 30 (proof built against spending=100, version=V) + // + // After jv2 applies, spending=150 and version=V+1, so jv3's + // proof is stale. AllOrNothing rejects the whole batch. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + mpt.pay(alice, bob, 50); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 3); + + auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1); + auto const jv2 = mpt.mergeInboxJV({.account = bob}); + // jv3 proof is built against spending=100, version=V (pre-batch) + auto const jv3 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 3); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + batch::Inner(jv3, bobSeq + 3), + Ter(tesSUCCESS)); + env.close(); + + // jv3 fails so nothing is applied. + env.require(MptBalance(mpt, bob, 50)); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0); + } + } + + // Tests a batch containing all four confidential MPT operations, Send, + // Convert, ConvertBack, and MergeInbox in a single AllOrNothing batch. + void + testBatchConfidentialMixTransactions(FeatureBitset features) + { + testcase("Batch confidential mixed operations"); + using namespace test::jtx; + + // send(bob→carol) + convert(carol) + convertBack(dave) + // + mergeInbox(carol) in one AllOrNothing batch. + // + // Setup: + // bob: spending=100, regular=0 + // carol: spending=0, regular=50 + // dave: spending=50, regular=0 + // + // After the batch: + // bob spending: 100 -> 70 (sent 30 to carol) + // carol inbox: 0+30(send)+50(convert)=80 -> merged -> spending=80, inbox=0 + // dave spending: 50 -> 30; regular: 0 -> 20 + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob: spending=100. carol: key registered, spending=0. + // dave: key registered, spending=0 initially. + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + // Give carol 50 regular MPT to convert in the batch. + mpt.pay(alice, carol, 50); + // Give dave 50 regular MPT then convert to confidential spending. + mpt.pay(alice, dave, 50); + mpt.convert({.account = dave, .amt = 50}); + mpt.mergeInbox({.account = dave}); + + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const daveSeq = env.seq(dave); + // 2 extra signers (carol, dave), 4 inner txns + auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 4); + + // jv1: bob sends 30 to carol + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1); + // jv2: carol converts her 50 regular MPT to confidential + auto const jv2 = mpt.convertJV({.account = carol, .amt = 50}, carolSeq); + // jv3: dave converts 20 spending back to regular MPT + auto const jv3 = mpt.convertBackJV({.account = dave, .amt = 20}, daveSeq); + // jv4: carol merges inbox into spending + // (inbox = 30 from jv1 + 50 from jv2 = 80 at execution time) + auto const jv4 = mpt.mergeInboxJV({.account = carol}); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Inner(jv3, daveSeq), + batch::Inner(jv4, carolSeq + 1), + batch::Sig(carol, dave), + Ter(tesSUCCESS)); + env.close(); + + // All four applied: + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70); + // carol's inbox was merged: spending=80, inbox=0 + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 80); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + // dave: spending=30, regular=20 + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 30); + env.require(MptBalance(mpt, dave, 20)); + } + + // bob send + bob convertBack in one AllOrNothing batch. + // + // The Send applies first and increments Bob's version counter. + // The ConvertBack proof was built against the pre-Send (spending=100, + // version=V), so batch txn is rejected. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1: bob sends 30 to carol (spending 100->70, version V->V+1) + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1); + // jv2: bob convertBack 40 , proof built against spending=100, version=V + auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 40}, bobSeq + 2); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // AllOrNothing: jv2 fails (stale proof) → nothing applied. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + } + + // Verifies that batch transactions work correctly when tickets are used instead + // of sequence numbers + void + testBatchAllOrNothing(FeatureBitset features) + { + testcase("Batch confidential MPT - all or nothing"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob=100 spending, carol=60 spending, dave=0 + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // bob sends dave 10, carol sends dave 5, independent, both valid. + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // Both txn applied: bob's balance 100→90, carol 60→55, dave inbox 0→15 + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15); + } + } + + void + testBatchOnlyOne(FeatureBitset features) + { + testcase("Batch confidential MPT - only one"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob=100 spending, carol=60 spending, dave=0 + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // bob sends dave 200 (invalid), carol sends dave 300 (invalid) + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + // Both proofs fail range check (amount > balance) + auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // No success found → nothing applied; balances unchanged + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + + // bob sends dave 200 (invalid), carol sends dave 5 (valid) + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1); + auto jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // Only carol's send applied: carol 60→55, dave inbox 0→5, bob unchanged + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 5); + } + } + + void + testBatchUntilFailure(FeatureBitset features) + { + testcase("Batch confidential MPT - until failure"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob=100 spending, carol=60 spending, dave=0 + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // first fails → none applied + // Bob sends Dave 200 (invalid — stops immediately) + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60); + } + + // Bob sends dave 10, Carol sends dave 5 — both valid and independent + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // Both applied: bob 100→90, carol 60→55, dave inbox 0→15 + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15); + } + } + + void + testBatchIndependent(FeatureBitset features) + { + testcase("Batch confidential MPT - independent"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + // bob=100 spending, carol=60 spending, dave=0 + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // Bob sends dave 10 (valid), Carol sends dave 300 + // (invalid), Carol sends Dave 5 (valid). Carol's + // balance is still 60 because the preceding send failed). + { + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1); + + // Carol trying to send dave 300 but own balance only 60 + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq); + auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq + 1); + + env(batch::outer(bob, bobSeq, batchFee, tfIndependent), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Inner(jv3, carolSeq + 1), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // inner 1 (bob→dave 10) applied: bob 100→90 + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90); + // inner 2 failed (carol not changed), inner 3 applied: carol 60→55 + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55); + // dave inbox: 10 (from bob) + 5 (from carol inner 3) = 15 + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15); + } + } + + // Tests batching ConfidentialMPTConvert and a ConfidentialMPTConvertBack + // in the same batch transaction. Because Convert only modifies the inbox + // (never the spending balance or the version counter), a ConvertBack proof + // built against the pre-batch spending balance is still valid when both + // appear in the same batch. + void + testBatchWithTickets(FeatureBitset features) + { + testcase("Batch confidential MPT with tickets"); + using namespace test::jtx; + + // outer batch uses a ticket. + // The inner send proofs are still bound to regular account sequences. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + // Bob creates one ticket to use for the outer batch. + std::uint32_t const outerTicketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + env.close(); + + auto const bobSeq = env.seq(bob); + // 0 extra signers: all inner txns are from bob; + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // When the outer uses a ticket (seq=0), inner txns start from bobSeq, bobSeq+1. + // jv2 must use chain state predicted after jv1 since both sends are from bob. + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq); + auto const chain1 = mpt.chainAfterSend(bob, 40, jv1); + auto const jv2 = + mpt.sendJV({.account = bob, .dest = dave, .amt = 20}, bobSeq + 1, chain1); + + env(batch::outer(bob, 0, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq), + batch::Inner(jv2, bobSeq + 1), + ticket::Use(outerTicketSeq), + Ter(tesSUCCESS)); + env.close(); + + // Both sends applied: bob 100→40, carol inbox=40, dave inbox=20. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 40); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 20); + } + + // inner transactions each consume their own ticket. + // The send proof context hash must be bound to the ticket sequence, not the + // account sequence. sendJV receives the ticket seq as its `seq` parameter. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + // Bob creates two tickets for the two inner sends. + std::uint32_t const ticketSeq1 = env.seq(bob) + 1; + std::uint32_t const ticketSeq2 = env.seq(bob) + 2; + env(ticket::create(bob, 2)); + env.close(); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1: proof bound to ticketSeq1. + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, ticketSeq1); + // jv2: proof bound to ticketSeq2, spending state predicted after jv1. + auto const chain1 = mpt.chainAfterSend(bob, 40, jv1); + auto const jv2 = + mpt.sendJV({.account = bob, .dest = dave, .amt = 30}, ticketSeq2, chain1); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, 0, ticketSeq1), + batch::Inner(jv2, 0, ticketSeq2), + Ter(tesSUCCESS)); + env.close(); + + // Both sends applied: bob 100→30, carol inbox=40, dave inbox=30. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30); + } + + // inner send uses wrong sequence (account seq instead of ticket seq) + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + std::uint32_t const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + env.close(); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // Proof intentionally built with account seq (bobSeq+1) instead of ticketSeq. + auto const badJV = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1); + auto const jv2 = mpt.mergeInboxJV({.account = bob}); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(badJV, 0, ticketSeq), + batch::Inner(jv2, bobSeq + 1), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + } + + // Basic tests of confidential transfer through delegation. Verifies that a delegated account + // with the appropriate permissions can execute confidential transfer transactions + // on behalf of the delegator. + void + testConfidentialDelegation(FeatureBitset features) + { + testcase("Confidential transfers through delegation"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dave{"dave"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback | + tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 200); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Bob delegates Convert, MergeInbox to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + env.close(); + + // Carol has no permission from bob to convert on his behalf. + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = carol, + .err = terNO_DELEGATE_PERMISSION, + }); + + // Dave executes Convert on behalf of bob, registering bob's key. + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = dave, + }); + env.require(MptBalance(mptAlice, bob, 100)); + + // Dave executes Convert again on behalf of bob (no key registration). + mptAlice.convert({.account = bob, .amt = 50, .delegate = dave}); + + // Dave executes MergeInbox on behalf of bob. + mptAlice.mergeInbox({.account = bob, .delegate = dave}); + + // Carol converts and merge inbox. + mptAlice.convert({ + .account = carol, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Dave does not have permission to send on behalf of bob. + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, + .delegate = dave, + .err = terNO_DELEGATE_PERMISSION}); + + // Bob delegates ConfidentialMPTSend to dave. + env(delegate::set( + bob, + dave, + {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox", "ConfidentialMPTSend"})); + env.close(); + + // Dave executes Send on behalf of bob. + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave}); + mptAlice.mergeInbox({.account = carol}); + + // Dave does not have permission to convert back on behalf of bob. + mptAlice.convertBack( + {.account = bob, .amt = 10, .delegate = dave, .err = terNO_DELEGATE_PERMISSION}); + + // Bob delegates ConfidentialMPTConvertBack to dave. + env(delegate::set( + bob, + dave, + {"ConfidentialMPTConvert", + "ConfidentialMPTMergeInbox", + "ConfidentialMPTSend", + "ConfidentialMPTConvertBack"})); + env.close(); + + // Dave executes ConvertBack on behalf of bob. + mptAlice.convertBack({.account = bob, .amt = 10, .delegate = dave}); + + // Dave does not have permission to clawback on behalf of alice. + mptAlice.confidentialClaw( + {.holder = bob, .amt = 130, .delegate = dave, .err = terNO_DELEGATE_PERMISSION}); + + // Alice delegates ConfidentialMPTClawback to dave. + env(delegate::set(alice, dave, {"ConfidentialMPTClawback"})); + env.close(); + + // Dave executes Clawback on behalf of alice. + mptAlice.confidentialClaw({.holder = bob, .amt = 130, .delegate = dave}); + } + + // Verifies that revoking delegation prevents further delegated operations. + void + testDelegationRevocation(FeatureBitset features) + { + testcase("Confidential delegation revocation"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + + MPTTester mptAlice(env, alice, {.holders = {bob}}); + env.fund(XRP(10000), carol); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Creating the Delegate SLE consumes one owner reserve slot for bob. + auto const bobOwnersBefore = ownerCount(env, bob); + env(delegate::set(bob, carol, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + env.close(); + env.require(Owners(bob, bobOwnersBefore + 1)); + + // Carol converts and merge inbox on behalf of bob. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = carol, + }); + mptAlice.mergeInbox({.account = bob, .delegate = carol}); + + // Bob revokes all permissions, deletes the Delegate SLE, releasing the reserve. + env(delegate::set(bob, carol, std::vector{})); + env.close(); + env.require(Owners(bob, bobOwnersBefore)); + + // Carol can no longer convert on behalf of bob. + mptAlice.convert({ + .account = bob, + .amt = 30, + .delegate = carol, + .err = terNO_DELEGATE_PERMISSION, + }); + + // Bob can still convert by himself. + mptAlice.convert({.account = bob, .amt = 30}); + } + + // Verifies that a delegated confidential transfer works correctly when an + // auditor is configured on the issuance. + void + testDelegationWithAuditor(FeatureBitset features) + { + testcase("Confidential delegation with auditor"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dave{"dave"}; + Account const auditor{"auditor"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(auditor); + mptAlice.set({ + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // Bob delegates Convert and Send permissions to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTSend", "ConfidentialMPTConvert"})); + env.close(); + + // Dave converts on behalf of bob. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = dave, + }); + mptAlice.mergeInbox({.account = bob}); + + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Dave sends on behalf of bob. + mptAlice.send({.account = bob, .dest = carol, .amt = 20, .delegate = dave}); + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave}); + + // Bob delegates ConvertBack and Send permissions to auditor. + env(delegate::set(bob, auditor, {"ConfidentialMPTSend", "ConfidentialMPTConvertBack"})); + env.close(); + + // auditor can send and convert back on behalf of bob as well. + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = auditor}); + mptAlice.convertBack({.account = bob, .amt = 10, .delegate = auditor}); + } + + // Verifies that a non-issuer delegating clawback to a third party does not + // allow that party to execute clawback, since clawback is issuer-only. + void + testDelegationClawbackIssuerOnly(FeatureBitset features) + { + testcase("Confidential clawback delegation requires issuer"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dave{"dave"}; + + ConfidentialEnv const confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 100, .convertAmount = 100}}, + tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + env.fund(XRP(10000), dave); + env.close(); + + // Bob delegates Clawback permission to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTClawback"})); + env.close(); + + // Dave attempts clawback on behalf of bob targetting bob, but since bob is not the issuer, + // the transaction should be rejected. + { + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfHolder] = bob.human(); + jv[sfMPTAmount.jsonName] = "50"; + jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0'); + env(jv, delegate::As(dave), Ter(temMALFORMED)); + } + + // Dave attempts clawback on behalf of bob targeting carol, but since bob is not the issuer, + // the transaction should be rejected. + { + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfHolder] = carol.human(); + jv[sfMPTAmount.jsonName] = "100"; + jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0'); + env(jv, delegate::As(dave), Ter(temMALFORMED)); + } + } + + // Batch with delegated ConfidentialMPTSend txs, covering stale and updated inner + // send proofs. + void + testBatchDelegatedSend(FeatureBitset features) + { + testcase("Batch ConfidentialMPTSend with delegation"); + using namespace test::jtx; + + // AllOrNothing: two delegated sends from bob via dave, second proof is + // stale once the first send updates bob's spending, whole batch rolls back. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + env(delegate::set(bob, dave, {"ConfidentialMPTSend"})); + env.close(); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1: proof against spending balance 100 + auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1); + jv1[jss::Delegate] = dave.human(); + // jv2: proof also against spending balance 100, which is stale once jv1 applies + auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2); + jv2[jss::Delegate] = dave.human(); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // Stale proof on jv2, AllOrNothing rolls back everything. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + + // AllOrNothing: two delegated sends with correctly chained proofs both apply. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + env(delegate::set(bob, dave, {"ConfidentialMPTSend"})); + env.close(); + + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + // jv1: proof against spending balance 100. + auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1); + jv1[jss::Delegate] = dave.human(); + auto const chain1 = mpt.chainAfterSend(bob, 40, jv1); + // jv2: proof against predicted spending balance 60. + auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 40}, bobSeq + 2, chain1); + jv2[jss::Delegate] = dave.human(); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, bobSeq + 2), + Ter(tesSUCCESS)); + env.close(); + + // Both inner tx applied + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 20); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 40); + } + } + + // Test missing delegation permission inside a batch. + void + testBatchDelegationMissingPermission(FeatureBitset features) + { + testcase("Batch delegation missing permission"); + using namespace test::jtx; + + // AllOrNothing: dave has no Send permission from bob, so the delegated + // inner send fails. The whole batch rolls back. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // Bob grants dave only MergeInbox, not Send. + env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"})); + env.close(); + + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + // jv1: direct send from carol (valid proof). + auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq); + // jv2: delegated send, fails because dave has no Send permission. + auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1); + jv2[jss::Delegate] = dave.human(); + + env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, carolSeq), + batch::Inner(jv2, bobSeq + 1), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // jv1 applied in the batch view, then jv2 failed, so + // AllOrNothing discards both inner effects. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + + // Independent: the delegated confidential send is skipped because lack of permission. The + // send from carol still applies. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // Bob does not grant dave any permissions. + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1); + jv1[jss::Delegate] = dave.human(); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq); + + env(batch::outer(bob, bobSeq, batchFee, tfIndependent), + batch::Inner(jv1, bobSeq + 1), + batch::Inner(jv2, carolSeq), + batch::Sig(carol), + Ter(tesSUCCESS)); + env.close(); + + // jv1 failed and jv2 applied. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 30); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30); + } + } + + // Test batch outer signer is the delegated account. + void + testBatchDelegatedSendWithDelegateAsOuterAccount(FeatureBitset features) + { + testcase("Test batch delegated send with delegate as outer account"); + using namespace test::jtx; + + // Dave has delegation permission, but the inner Account is bob. + // Without bob's BatchSigner, the batch is rejected. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + env(delegate::set(bob, dave, {"ConfidentialMPTSend"})); + env.close(); + + auto const daveSeq = env.seq(dave); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + + auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq); + jv1[jss::Delegate] = dave.human(); + auto const jv2 = mpt.mergeInboxJV({.account = dave}); + + env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq), + batch::Inner(jv2, daveSeq + 1), + Ter(temBAD_SIGNER)); + env.close(); + + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + + // Dave submits a mixed batch: bob signs inner tx1, and + // dave is the Delegate account signing for inner tx2. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0); + + env(delegate::set(bob, dave, {"ConfidentialMPTSend"})); + env.close(); + + auto const daveSeq = env.seq(dave); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + + auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq); + auto const chain1 = mpt.chainAfterSend(bob, 40, jv1); + auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1, chain1); + jv2[jss::Delegate] = dave.human(); + + // Dave is outer; bob signs because his account appears in inner txns. + env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq), + batch::Inner(jv2, bobSeq + 1), + batch::Sig(bob), + Ter(tesSUCCESS)); + env.close(); + + // Both sends applied: bob 100→30, carol inbox=70. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70); + } + + // Verify the delegator Bob's BatchSigner does not bypass the missing delegation permission. + // The delegated inner send fails. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + + // Bob does not grant dave any permissions. + auto const daveSeq = env.seq(dave); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2); + + auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq); + jv1[jss::Delegate] = dave.human(); + auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq); + + env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq), + batch::Inner(jv2, carolSeq), + batch::Sig(bob, carol), + Ter(tesSUCCESS)); + env.close(); + + // jv1 fails before jv2 is attempted. + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + } + } + + // Mixed batch with delegated and non-delegated inner confidential MPT transactions. + void + testBatchDelegatedConfidentialMix(FeatureBitset features) + { + testcase("Batch delegated confidential multiple operations"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + Account const erin("erin"); + Account const frank("frank"); + + MPTTester mpt(env, alice, {.holders = {bob, carol, dave, frank}}); + setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60); + mpt.pay(alice, bob, 50); + env.fund(XRP(10000), erin); + env.close(); + + mpt.authorize({.account = frank}); + mpt.pay(alice, frank, 40); + mpt.generateKeyPair(frank); + + env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTConvertBack"})); + env(delegate::set(carol, erin, {"ConfidentialMPTSend"})); + env(delegate::set(bob, erin, {"ConfidentialMPTMergeInbox"})); + env.close(); + + auto const daveSeq = env.seq(dave); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const frankSeq = env.seq(frank); + auto const batchFee = batch::calcConfidentialBatchFee(env, 3, 6); + + // Dave submits the batch. Bob's convert and convertback use Dave as Delegate; + // Carol's send and Bob's mergeInbox use Erin as Delegate. Frank's + // convert and mergeInbox are non-delegated. + auto jv1 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq); + jv1[jss::Delegate] = dave.human(); + auto jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1); + jv2[jss::Delegate] = dave.human(); + auto jv3 = mpt.sendJV({.account = carol, .dest = bob, .amt = 15}, carolSeq); + jv3[jss::Delegate] = erin.human(); + auto const jv4 = mpt.convertJV( + {.account = frank, .amt = 25, .holderPubKey = mpt.getPubKey(frank)}, frankSeq); + auto const jv5 = mpt.mergeInboxJV({.account = frank}); + auto jv6 = mpt.mergeInboxJV({.account = bob}); + jv6[jss::Delegate] = erin.human(); + + env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing), + batch::Inner(jv1, bobSeq), + batch::Inner(jv2, bobSeq + 1), + batch::Inner(jv3, carolSeq), + batch::Inner(jv4, frankSeq), + batch::Inner(jv5, frankSeq + 1), + batch::Inner(jv6, bobSeq + 2), + batch::Sig(bob, carol, frank), + Ter(tesSUCCESS)); + env.close(); + + env.require(MptBalance(mpt, bob, 60)); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 105); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0); + env.require(MptBalance(mpt, carol, 0)); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 45); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + env.require(MptBalance(mpt, frank, 15)); + BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedSpending) == 25); + BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedInbox) == 0); + env.require(MptBalance(mpt, dave, 0)); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 0); + BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0); + auto const outstandingBalance = mpt.getIssuanceOutstandingBalance(); + BEAST_EXPECT(outstandingBalance && *outstandingBalance == 250); + BEAST_EXPECT(mpt.getIssuanceConfidentialBalance() == 175); + } + + // Test invalid scenarios for delegation with tickets. + void + testInvalidDelegationWithTickets(FeatureBitset features) + { + testcase("Invalid cases for delegation with tickets"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + env.fund(XRP(10000), carol); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 200); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Bob grants carol permissions. + env(delegate::set(bob, carol, {"ConfidentialMPTConvert"})); + env.close(); + + uint64_t const amt = 10; + auto const bf = generateBlindingFactor(); + auto const holderCt = mptAlice.encryptAmount(bob, amt, bf); + auto const issuerCt = mptAlice.encryptAmount(alice, amt, bf); + + // Invalid: proof built with wrong ticket sequence (ticketSeq + 1). + { + auto const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + + auto const badCtxHash = + getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq + 1); + auto const badProof = requireOptional( + mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof."); + + mptAlice.convert({ + .account = bob, + .amt = amt, + .proof = strHex(badProof), + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .ticketSeq = ticketSeq, + .err = tecBAD_PROOF, + }); + } + + // Invalid: proof built with account sequence instead of ticket sequence. + { + auto const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + auto const badCtxHash = getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob)); + auto const badProof = requireOptional( + mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof."); + + mptAlice.convert({ + .account = bob, + .amt = amt, + .proof = strHex(badProof), + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .ticketSeq = ticketSeq, + .err = tecBAD_PROOF, + }); + } + + // Invalid: ticket sequence is far in the future and hasn't been created yet. + { + mptAlice.convert({ + .account = bob, + .amt = amt, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .ticketSeq = env.seq(bob) + 100, + .err = terPRE_TICKET, + }); + } + + // Invalid: ticket sequence is in the past but was never created. + { + mptAlice.convert({ + .account = bob, + .amt = amt, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .ticketSeq = 1, + .err = tefNO_TICKET, + }); + } + + // Invalid: the delegated account, carol, creates a ticket and uses it. + { + auto const carolTicketSeq = env.seq(carol) + 1; + env(ticket::create(carol, 1)); + + mptAlice.convert({ + .account = bob, + .amt = amt, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .ticketSeq = carolTicketSeq, + .err = tefNO_TICKET, + }); + } + + // Invalid: proof bound to a ticket sequence but submitted without a ticket, + // using account sequence. + { + auto const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + + // Build proof using ticketSeq. + auto const ctxHashForTicket = + getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq); + auto const proof = requireOptional( + mptAlice.getSchnorrProof(bob, ctxHashForTicket), "Missing Schnorr Proof."); + + // Submit without ticket. + mptAlice.convert({ + .account = bob, + .amt = amt, + .proof = strHex(proof), + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCt, + .issuerEncryptedAmt = issuerCt, + .blindingFactor = bf, + .delegate = carol, + .err = tecBAD_PROOF, + }); + } + } + + // Verifies that delegation works correctly when the delegating account uses + // tickets instead of regular sequence numbers. The proof must bind to the + // ticket sequence, not the account sequence. + void + testDelegationWithTickets(FeatureBitset features) + { + testcase("Confidential delegation with tickets"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 200); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Bob grants dave permissions. + env(delegate::set( + bob, + dave, + {"ConfidentialMPTConvert", + "ConfidentialMPTMergeInbox", + "ConfidentialMPTSend", + "ConfidentialMPTConvertBack"})); + // Alice grants dave permission to clawback on her behalf. + env(delegate::set(alice, dave, {"ConfidentialMPTClawback"})); + env.close(); + + // Dave executes Convert on behalf of bob using ticket. + auto ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + BEAST_EXPECT(env.seq(bob) != ticketSeq); + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = dave, + .ticketSeq = ticketSeq, + }); + env.require(MptBalance(mptAlice, bob, 100)); + + // MergeInbox using ticket with delegation. + ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + BEAST_EXPECT(env.seq(bob) != ticketSeq); + mptAlice.mergeInbox({.account = bob, .delegate = dave, .ticketSeq = ticketSeq}); + + // Carol converts and merges inbox to receive from bob. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Send using ticket with delegation. + ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + BEAST_EXPECT(env.seq(bob) != ticketSeq); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 20, + .delegate = dave, + .ticketSeq = ticketSeq, + }); + + // ConvertBack using ticket with delegation. + ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + BEAST_EXPECT(env.seq(bob) != ticketSeq); + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .delegate = dave, + .ticketSeq = ticketSeq, + }); + + // Clawback using ticket with delegation. + ticketSeq = env.seq(alice) + 1; + env(ticket::create(alice, 1)); + BEAST_EXPECT(env.seq(alice) != ticketSeq); + mptAlice.confidentialClaw({ + .holder = bob, + .amt = 70, + .delegate = dave, + .ticketSeq = ticketSeq, + }); + } + + void + testWithFeats(FeatureBitset features) + { + // DepositAuth, credentials, and destination tag interactions. + testSendDepositPreauth(features); + testSendCredentialValidation(features); + testDestinationTag(features); + + // AMM/pseudo-account interaction. + testAMMHolderCannotHaveConfidentialStateClawback(features); + + // Ticket interactions. + testWithTickets(features); + testConvertTicketProofBinding(features); + testTicketErrors(features); + + // Batch interactions. + testBatchConfidentialSend(features); + testBatchConfidentialConvertAndConvertBack(features); + testBatchConfidentialMixTransactions(features); + testBatchAllOrNothing(features); + testBatchOnlyOne(features); + testBatchUntilFailure(features); + testBatchIndependent(features); + testBatchWithTickets(features); + + // Permission delegation interactions. + testConfidentialDelegation(features); + testDelegationRevocation(features); + testDelegationWithAuditor(features); + testDelegationClawbackIssuerOnly(features); + testBatchDelegatedSend(features); + testBatchDelegationMissingPermission(features); + testBatchDelegatedSendWithDelegateAsOuterAccount(features); + testBatchDelegatedConfidentialMix(features); + testInvalidDelegationWithTickets(features); + testDelegationWithTickets(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testableAmendments()}; + + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(ConfidentialTransferExtended, app, xrpl); + +} // namespace xrpl diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp new file mode 100644 index 0000000000..e6faf0a2ae --- /dev/null +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -0,0 +1,8208 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class ConfidentialTransfer_test : public ConfidentialTransferTestBase +{ + void + testConvert(FeatureBitset features) + { + testcase("Convert"); + using namespace test::jtx; + + // Basic convert test + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = bob, + .amt = 20, + }); + + mptAlice.convert({ + .account = bob, + .amt = 40, + }); + + mptAlice.convert({ + .account = bob, + .amt = 40, + }); + } + + // Edge case: minimum amount (1) + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 1); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = bob, + .amt = 1, + }); + } + + // Edge case: kMaxMpTokenAmount + // Using raw JSON to avoid automatic decryption checks in MPTTester + // which don't work for very large amounts (brute-force decryption is slow) + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, kMaxMpTokenAmount); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + // First convert with amt=0 to register public key (uses MPTTester) + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // Second convert with kMaxMpTokenAmount using raw JSON + Buffer const blindingFactor = generateBlindingFactor(); + auto const holderCiphertext = + mptAlice.encryptAmount(bob, kMaxMpTokenAmount, blindingFactor); + auto const issuerCiphertext = + mptAlice.encryptAmount(alice, kMaxMpTokenAmount, blindingFactor); + + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTConvert; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount); + jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext); + jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext); + jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); + + env(jv, Ter(tesSUCCESS)); + + // Verify the public balance was reduced + env.require(MptBalance(mptAlice, bob, 0)); + } + } + + void + testConvertWithAuditor(FeatureBitset features) + { + testcase("Convert with auditor"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob}, + .auditor = auditor, + }); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = bob, + .amt = 20, + }); + + mptAlice.convert({ + .account = bob, + .amt = 30, + }); + } + + void + testConvertPreflight(FeatureBitset features) + { + testcase("Convert preflight"); + using namespace test::jtx; + + // Alice (issuer) tries to convert her own tokens - should fail + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.convert({ + .account = alice, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(alice), + .err = temMALFORMED, + }); + } + + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = temDISABLED, + }); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = temDISABLED, + }); + } + + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = alice, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = temMALFORMED, + }); + + // Holder encrypted amount is empty (length 0) + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = Buffer{}, + .err = temBAD_CIPHERTEXT, + }); + + // Issuer encrypted amount is empty (length 0) + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .issuerEncryptedAmt = Buffer{}, + .err = temBAD_CIPHERTEXT, + }); + + // Auditor encrypted amount has invalid length (must be 66 bytes) + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .auditorEncryptedAmt = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // Auditor encrypted amount has correct length but invalid data + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .auditorEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + // Amount exceeds maximum allowed MPT amount + mptAlice.convert({ + .account = bob, + .amt = kMaxMpTokenAmount + 1, + .holderPubKey = mptAlice.getPubKey(bob), + .err = temBAD_AMOUNT, + }); + + // Holder encrypted amount has correct length but invalid data + mptAlice.convert({ + .account = bob, + .amt = 1, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + // Issuer encrypted amount has correct length but invalid data (not + // a valid EC point) + mptAlice.convert({ + .account = bob, + .amt = 1, + .holderPubKey = mptAlice.getPubKey(bob), + .issuerEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + // Holder public key is invalid (empty buffer) + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = Buffer{}, + .err = temMALFORMED, + }); + + // Holder public key has correct length but invalid EC point data + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength), + .err = temMALFORMED, + }); + } + + // when registering holder pub key, the transaction must include a + // Schnorr proof of knowledge for the corresponding secret key + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .fillSchnorrProof = false, + .holderPubKey = mptAlice.getPubKey(bob), + .err = temMALFORMED, + }); + + mptAlice.convert({ + .account = bob, + .amt = 0, + .fillSchnorrProof = false, + .holderPubKey = mptAlice.getPubKey(bob), + .err = temMALFORMED, + }); + + // proof length is invalid + mptAlice.convert({ + .account = bob, + .amt = 10, + .proof = std::string(10, 'A'), + .holderPubKey = mptAlice.getPubKey(bob), + .err = temMALFORMED, + }); + } + + // when holder pub key already registered, Schnorr proof must not be + // provided + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // this will register bob's pub key, + // and convert 10 to confidential balance + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // proof must not be provided after pub key was registered + mptAlice.convert({ + .account = bob, + .amt = 20, + .fillSchnorrProof = true, + .err = temMALFORMED, + }); + } + } + + void + testConvertInvalidProofContextBinding(FeatureBitset features) + { + testcase("Convert proof context binding"); + using namespace test::jtx; + + auto runBadProof = [&](auto makeContextHash) { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + auto const proof = + mptAlice.getSchnorrProof(bob, makeContextHash(env, mptAlice, alice, bob, carol)); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.convert({ + .account = bob, + .amt = 10, + .proof = strHex(requireOptional(proof, "Missing proof")), + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecBAD_PROOF, + }); + }; + + // Wrong account in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const& carol) { + return getConvertContextHash(carol.id(), mpt.issuanceID(), env.seq(bob)); + }); + + // Wrong issuance ID in the proof context. + runBadProof([&](Env& env, + MPTTester const&, + Account const& alice, + Account const& bob, + Account const&) { + return getConvertContextHash( + bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob)); + }); + + // Wrong transaction sequence in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const&) { + return getConvertContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1); + }); + } + + void + testSet(FeatureBitset features) + { + testcase("Set"); + using namespace test::jtx; + + // Set keys on issuance that already has confidential amounts enabled + { + Env env{*this, features}; + Account const alice("alice"); + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + } + + // Enable confidential amounts flag only (no keys) + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + }); + } + + // Set keys when enabling confidential amounts in the same tx + { + Env env{*this, features}; + Account const alice("alice"); + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // Verify lsfMPTCanHoldConfidentialBalance flag is set + BEAST_EXPECT(mptAlice.checkFlags( + lsfMPTCanTransfer | lsfMPTCanLock | lsfMPTCanHoldConfidentialBalance)); + + // Verify keys are persisted on the issuance + auto const sle = env.le(keylet::mptokenIssuance(mptAlice.issuanceID())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfIssuerEncryptionKey)); + BEAST_EXPECT(sle->isFieldPresent(sfAuditorEncryptionKey)); + } + } + + void + testSetPreflight(FeatureBitset features) + { + testcase("Set preflight"); + using namespace test::jtx; + + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = temDISABLED, + }); + } + + // pub key is invalid + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + // Issuer pub key is invalid (empty) + mptAlice.set({ + .account = alice, + .issuerPubKey = Buffer{}, + .err = temMALFORMED, + }); + + // Issuer pub key has correct length but invalid EC point data + mptAlice.set({ + .account = alice, + .issuerPubKey = gMakeZeroBuffer(kEcPubKeyLength), + .err = temMALFORMED, + }); + + // Auditor key is invalid length + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = gMakeZeroBuffer(10), + .err = temMALFORMED, + }); + + // Auditor key has correct length but invalid EC point data + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = gMakeZeroBuffer(kEcPubKeyLength), + .err = temMALFORMED, + }); + + // Cannot set auditor key without issuer key + mptAlice.set({ + .account = alice, + .auditorPubKey = mptAlice.getPubKey(alice), + .err = temMALFORMED, + }); + + // Cannot set Holder and issuer Keys in the same transaction + mptAlice.set({ + .account = alice, + .holder = bob, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = temMALFORMED, + }); + + // Cannot set Holder and auditor Keys in the same transaction + mptAlice.set({ + .account = alice, + .holder = bob, + .auditorPubKey = mptAlice.getPubKey(alice), + .err = temMALFORMED, + }); + } + } + + void + testSetPreclaim(FeatureBitset features) + { + testcase("Set preclaim"); + using namespace test::jtx; + + // Cannot set issuer key if confidential amounts not enabled + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = tecNO_PERMISSION, + }); + } + + // Cannot update issuer public key once set + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + // First set issuer key - should succeed + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + }); + + // Try to update issuer key - should fail + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(bob), + .err = tecNO_PERMISSION, + }); + } + + // Cannot update issuer and auditor public keys once set + // Note: trying to set only auditor key fails in preflight (temMALFORMED) + // so we must provide both keys, which fails on issuer key check first + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(auditor); + + // Set issuer and auditor keys - should succeed + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // Try to update both keys - fails on issuer key check first + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(bob), + .auditorPubKey = mptAlice.getPubKey(alice), + .err = tecNO_PERMISSION, + }); + } + + // Cannot set auditor key if confidential amounts not enabled + { + Env env{*this, features}; + Account const alice("alice"); + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + .err = tecNO_PERMISSION, + }); + } + + // Cannot set keys when mutation of canConfidentialAmount is disallowed + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + // Create with tmfMPTCannotEnableCanHoldConfidentialBalance + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + + // Trying to enable confidential amounts and set keys fails + // because the issuance cannot mutate canConfidentialAmount + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = tecNO_PERMISSION, + }); + } + + // Set issuer key first, then auditor key in a separate tx + { + Env env{*this, features}; + Account const alice("alice"); + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + + // Set issuer key only + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + }); + + // Set auditor key in a separate tx - requires issuer key in tx + // (preflight enforces auditor key requires issuer key) + // This fails because issuer key is already set on ledger + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + .err = tecNO_PERMISSION, + }); + } + } + + void + testTransferFee(FeatureBitset features) + { + testcase("test transfer fee"); + using namespace test::jtx; + + // MPTokenIssuanceCreate: cannot create with both TransferFee > 0 and + // tfMPTCanHoldConfidentialBalance + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .transferFee = 100, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + .err = temBAD_TRANSFER_FEE, + }); + + // transferFee being 0 is allowed, even with tfMPTCanHoldConfidentialBalance + mptAlice.create({ + .transferFee = 0, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + } + + // MPTokenIssuanceSet (preflight): cannot enable confidential amounts and + // set TransferFee > 0 in the same transaction + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanMutateTransferFee, + }); + + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .transferFee = 100, + .err = temBAD_TRANSFER_FEE, + }); + } + + // MPTokenIssuanceSet (preclaim): cannot enable confidential amounts on + // an issuance that already has a non-zero TransferFee + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .transferFee = 100, + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanMutateTransferFee, + }); + + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .err = tecNO_PERMISSION, + }); + } + + // MPTokenIssuanceSet (preclaim): cannot set TransferFee > 0 on an + // issuance that already has lsfMPTCanHoldConfidentialBalance + { + Env env{*this, features}; + Account const alice("alice"); + MPTTester mptAlice(env, alice, {.holders = {}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + .mutableFlags = tmfMPTCanMutateTransferFee, + }); + + mptAlice.set({ + .account = alice, + .transferFee = 100, + .err = tecNO_PERMISSION, + }); + + // Setting transfer fee to 0 is allowed, but have no effect. + mptAlice.set({ + .account = alice, + .transferFee = 0, + }); + } + } + + void + testConvertPreclaim(FeatureBitset features) + { + testcase("Convert preclaim"); + using namespace test::jtx; + + // tfMPTCanHoldConfidentialBalance is not set on issuance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecNO_PERMISSION, + }); + } + + // issuer has not uploaded their sfIssuerEncryptionKey + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecNO_PERMISSION, + }); + } + + // issuance does not exist + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.destroy(); + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecOBJECT_NOT_FOUND, + }); + } + + // bob has not created MPToken + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecOBJECT_NOT_FOUND, + }); + } + + // Verification of Issuer and and holder ciphertexts + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .issuerEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + + std::uint64_t const amount = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const holderCiphertext = mptAlice.encryptAmount(bob, amount, blindingFactor); + + // Holder ciphertext is valid for the amount and + // blinding factor, but the issuer ciphertext is encrypted under a + // different public key than the registered issuer key. + Buffer const wrongIssuerCiphertext = + mptAlice.encryptAmount(carol, amount, blindingFactor); + + mptAlice.convert({ + .account = bob, + .amt = amount, + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderCiphertext, + .issuerEncryptedAmt = wrongIssuerCiphertext, + .blindingFactor = blindingFactor, + .err = tecBAD_PROOF, + }); + } + + // trying to convert more than what bob has + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 200, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecINSUFFICIENT_FUNDS, + }); + } + + // holder cannot upload pk again + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)}); + + // cannot upload pk again + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecDUPLICATE, + }); + } + + // cannot convert if locked + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecLOCKED, + }); + + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTUnlock, + }); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + }); + } + + // cannot convert if unauth + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth | + tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + // Unauthorize bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecNO_AUTH, + }); + + // auth bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + }); + } + + // frozen account cannot bypass freeze check with amount=0 + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // lock bob + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + + mptAlice.generateKeyPair(bob); + + // amount=0 should still be rejected when locked + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecLOCKED, + }); + } + + // unauthorized account cannot bypass auth check with amount=0 + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth | + tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + // Unauthorize bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + + // amount=0 should still be rejected when unauthorized + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecNO_AUTH, + }); + } + + // cannot convert if auditor key is set, but auditor amount is not + // provided + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob}, + .auditor = auditor, + }); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(auditor); + + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + // no auditor encrypted amt provided + mptAlice.convert({ + .account = bob, + .amt = 10, + .fillAuditorEncryptedAmt = false, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecNO_PERMISSION, + }); + } + + // cannot convert if tx include auditor ciphertext, but does not have + // auditing enabled + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + // there is no auditor key set + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .auditorEncryptedAmt = getTrivialCiphertext(), + .err = tecNO_PERMISSION, + }); + } + + // Auditor key set successfully, auditor ciphertext mathematically + // correct, but contains invalid data (mismatching amount). + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob}, + .auditor = auditor, + }); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(auditor); + + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .auditorEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + } + + // invalid proof when registering holder pub key + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .proof = std::string(kEcSchnorrProofLength * 2, 'A'), + .holderPubKey = mptAlice.getPubKey(bob), + .err = tecBAD_PROOF, + }); + } + + // no holder key on ledger and no key in tx + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob has not registered a holder key, and doesn't provide one + mptAlice.convert({ + .account = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + } + + // all public balance already converted, try to convert more + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + // convert entire public balance + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + env.require(MptBalance(mptAlice, bob, 0)); + + // try to convert 1 more — no public balance left + mptAlice.convert({ + .account = bob, + .amt = 1, + .err = tecINSUFFICIENT_FUNDS, + }); + } + } + + void + testMergeInbox(FeatureBitset features) + { + testcase("Merge inbox"); + using namespace test::jtx; + + // Merge with an empty inbox should succeed as a no-op. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + }); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({.account = bob}); + // Inbox is empty after the first merge; the second merge is a no-op. + mptAlice.mergeInbox({.account = bob}); + } + + // Makes sure if merge inbox version is UINT32_MAX, the next merge wraps + // the version back to 0. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + }); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // Force the on-ledger version to UINT32_MAX, then apply a merge and + // confirm the version wraps around to 0. + auto const wrappedFrom = std::numeric_limits::max(); + auto const jt = env.jt(mptAlice.mergeInboxJV({.account = bob})); + BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) { + auto const sle = std::const_pointer_cast( + view.read(keylet::mptoken(mptAlice.issuanceID(), bob.id()))); + if (!sle) + return false; + + (*sle)[sfConfidentialBalanceVersion] = wrappedFrom; + view.rawReplace(sle); + + auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, env.journal); + BEAST_EXPECT(result.ter == tesSUCCESS); + return result.applied; + })); + + BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) == 0); + } + } + + void + testMergeInboxPreflight(FeatureBitset features) + { + testcase("Merge inbox preflight"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({ + .account = alice, + .err = temMALFORMED, + }); + + env.disableFeature(featureConfidentialTransfer); + env.close(); + + mptAlice.mergeInbox({ + .account = bob, + .err = temDISABLED, + }); + } + + void + testMergeInboxPreclaim(FeatureBitset features) + { + testcase("Merge inbox preclaim"); + using namespace test::jtx; + + // issuance does not exist + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.destroy(); + mptAlice.generateKeyPair(bob); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // tfMPTCanHoldConfidentialBalance is not set on issuance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecNO_PERMISSION, + }); + } + + // no mptoken + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // bob doesn't have encrypted balances + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecNO_PERMISSION, + }); + } + + // holder is locked + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // lock bob + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecLOCKED, + }); + + // unlock bob + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTUnlock, + }); + + // should succeed now + mptAlice.mergeInbox({ + .account = bob, + }); + } + + // holder not authorized + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance | + tfMPTRequireAuth, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // unauthorize bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + + mptAlice.mergeInbox({ + .account = bob, + .err = tecNO_AUTH, + }); + + // authorize bob again + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + + // should succeed now + mptAlice.mergeInbox({ + .account = bob, + }); + } + } + + void + testSend(FeatureBitset features) + { + testcase("test confidential send"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 20}}}; + auto& mptAlice = confEnv.mpt; + + // bob sends 10 to carol + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + }); + + // bob sends 1 to carol again + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 1, + }); + + mptAlice.mergeInbox({ + .account = carol, + }); + + // carol sends 15 back to bob + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 15, + }); + } + + void + testSendWithAuditor(FeatureBitset features) + { + testcase("test confidential send with auditor"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 20}}, + tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + auditor}; + auto& mptAlice = confEnv.mpt; + + // bob sends 10 to carol + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + }); + + // bob sends 1 to carol again + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 1, + }); + + mptAlice.mergeInbox({ + .account = carol, + }); + + // carol sends 15 back to bob + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 15, + }); + } + + void + testSendPreflight(FeatureBitset features) + { + testcase("test ConfidentialMPTSend Preflight"); + using namespace test::jtx; + + // test disabled + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create(); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .err = temDISABLED, + }); + } + + // test malformed + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = carol, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + // issuer can not be the same as sender + mptAlice.send({ + .account = alice, + .dest = carol, + .amt = 10, + .err = temMALFORMED, + }); + + // can not send to self + mptAlice.send({ + .account = bob, + .dest = bob, + .amt = 10, + .err = temMALFORMED, + }); + + // can not send to issuer + mptAlice.send({ + .account = bob, + .dest = alice, + .amt = 10, + .err = temMALFORMED, + }); + + // sender encrypted amount wrong length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .senderEncryptedAmt = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // dest encrypted amount wrong length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .destEncryptedAmt = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // issuer encrypted amount wrong length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .issuerEncryptedAmt = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // sender encrypted amount malformed + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // dest encrypted amount malformed + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // issuer encrypted amount malformed + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // invalid proof length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = std::string(10, 'A'), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temMALFORMED, + }); + + // invalid amount Pedersen commitment length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = gMakeZeroBuffer(100), + .balanceCommitment = getTrivialCommitment(), + .err = temMALFORMED, + }); + + // invalid balance Pedersen commitment length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = gMakeZeroBuffer(100), + .err = temMALFORMED, + }); + + // amount Pedersen commitment has correct length but invalid EC point data + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength), + .balanceCommitment = getTrivialCommitment(), + .err = temMALFORMED, + }); + + // balance Pedersen commitment has correct length but invalid EC point data + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength), + .err = temMALFORMED, + }); + } + + // test bad ciphertext + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob, carol}, + .auditor = auditor, + }); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(auditor); + + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = carol, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + // auditor encrypted amount wrong length + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .auditorEncryptedAmt = gMakeZeroBuffer(10), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // auditor encrypted amount (correct length, invalid data) + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .auditorEncryptedAmt = getBadCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + } + } + + void + testSendPreclaim(FeatureBitset features) + { + testcase("test ConfidentialMPTSend Preclaim"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + Account const eve("eve"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave, eve}}); + + // authorize bob, carol, dave (not eve) + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth | + tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.authorize({ + .account = alice, + .holder = carol, + }); + mptAlice.authorize({ + .account = dave, + }); + mptAlice.authorize({ + .account = alice, + .holder = dave, + }); + + // fund bob, carol (not dave or eve) + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(dave); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob and carol convert some funds to confidential + mptAlice.convert({ + .account = bob, + .amt = 60, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tesSUCCESS, + }); + mptAlice.convert({ + .account = carol, + .amt = 20, + .holderPubKey = mptAlice.getPubKey(carol), + .err = tesSUCCESS, + }); + + // bob and carol merge inbox + mptAlice.mergeInbox({ + .account = bob, + }); + mptAlice.mergeInbox({ + .account = carol, + }); + + // issuance not found + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // destroy the issuance + mptAlice.destroy(); + + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::Destination] = carol.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTSend; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext()); + jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext()); + jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext()); + jv[sfAmountCommitment] = strHex(getTrivialCommitment()); + jv[sfBalanceCommitment] = strHex(getTrivialCommitment()); + jv[sfZKProof] = getTrivialSendProofHex(); + + env(jv, Ter(tecOBJECT_NOT_FOUND)); + } + + // destination does not exist + { + Account const unknown("unknown"); + mptAlice.send({ + .account = bob, + .dest = unknown, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecNO_TARGET, + }); + } + + // destination requires destination tag but none provided + { + env(fset(carol, asfRequireDest)); + env.close(); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecDST_TAG_NEEDED, + }); + + env(fclear(carol, asfRequireDest)); + env.close(); + } + + // dave exists, but has no confidential fields (never converted) + { + mptAlice.send({ + .account = bob, + .dest = dave, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecNO_PERMISSION, + }); + mptAlice.send({ + .account = dave, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecNO_PERMISSION, + }); + } + + // destination exists but has no MPT object. + { + mptAlice.send({ + .account = bob, + .dest = eve, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getTrivialCiphertext(), + .destEncryptedAmt = getTrivialCiphertext(), + .issuerEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecOBJECT_NOT_FOUND, + }); + } + + // issuance is locked globally + { + // lock issuance + mptAlice.set({ + .account = alice, + .flags = tfMPTLock, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecLOCKED, + }); + // unlock issuance + mptAlice.set({ + .account = alice, + .flags = tfMPTUnlock, + }); + // now can send + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 1, + }); + } + + // sender is locked + { + // lock bob + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecLOCKED, + }); + // unlock bob + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTUnlock, + }); + // now can send + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 2, + }); + } + + // destination is locked + { + // lock carol + mptAlice.set({ + .account = alice, + .holder = carol, + .flags = tfMPTLock, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecLOCKED, + }); + // unlock carol + mptAlice.set({ + .account = alice, + .holder = carol, + .flags = tfMPTUnlock, + }); + // now can send + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 3, + }); + } + + // sender not authorized + { + // unauthorize bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecNO_AUTH, + }); + // authorize bob again + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + // now can send + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 4, + }); + } + + // destination not authorized + { + // unauthorize carol + mptAlice.authorize({ + .account = alice, + .holder = carol, + .flags = tfMPTUnauthorize, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecNO_AUTH, + }); + // authorize carol again + mptAlice.authorize({ + .account = alice, + .holder = carol, + }); + // now can send + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 5, + }); + } + + // cannot send when MPTCanTransfer is not set + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 20}}, + tfMPTCanLock | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + // bob sends 10 to carol + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, // will be encrypted internally + .err = tecNO_AUTH, + }); + } + + // Confidential MPTs should not have a transfer fee. Force malformed + // ledger state to cover the defensive preclaim check. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 20}}}; + auto& mptAlice = confEnv.mpt; + + BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) { + auto const issuance = std::const_pointer_cast( + view.read(keylet::mptokenIssuance(mptAlice.issuanceID()))); + if (!issuance) + return false; + + issuance->setFieldU16(sfTransferFee, 1); + view.rawReplace(issuance); + return true; + })); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .err = tecNO_PERMISSION, + }); + } + + // bad proof + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 20}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .err = tecBAD_PROOF, + }); + } + + // No Auditor key set, but auditor encrypted amt provided + { + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .auditorEncryptedAmt = getTrivialCiphertext(), + .err = tecNO_PERMISSION, + }); + } + + // Auditor CipherText is Valid, but does not match the Txn Amount + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob, carol}, + .auditor = auditor, + }); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(auditor); + + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = carol, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .auditorEncryptedAmt = getTrivialCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + } + } + + void + testSendRangeProof(FeatureBitset features) + { + testcase("test ConfidentialMPTSend Range Proof"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 1000, .convertAmount = 60}, + {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + { + // Bob has 60, tries to send 70. Invalid remaining balance. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 70, + .err = tecBAD_PROOF, + }); + + // Bob has 60, tries to send 61. Invalid remaining balance. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 61, + .err = tecBAD_PROOF, + }); + + // Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 60, + .err = tesSUCCESS, + }); + } + + { + // Bob converts 100. + mptAlice.convert({ + .account = bob, + .amt = 100, + }); + mptAlice.mergeInbox({ + .account = bob, + }); + + // Bob has 100, tries to send 2^64-1. Invalid remaining balance. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = std::numeric_limits::max(), + .err = tecBAD_PROOF, + }); + + // Bob sends 1, remaining 99. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 1, + .err = tesSUCCESS, + }); + + // Bob sends 100, but only has 99. Invalid remaining balance. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 100, + .err = tecBAD_PROOF, + }); + } + + // send when spending balance is 0 (key registered, inbox merged, but nothing converted) + { + // Register keys only (amt=0) for both parties — spending stays 0. + Env env2{*this, features}; + Account const alice2("alice"), bob2("bob"), carol2("carol"); + ConfidentialEnv zeroEnv{ + env2, + alice2, + {{.account = bob2, .payAmount = 100, .convertAmount = 0}, + {.account = carol2, .payAmount = 50, .convertAmount = 0}}}; + auto& mptAlice2 = zeroEnv.mpt; + + // Trying to send any amount with 0 spending balance must fail: + // the range proof for < 0 is invalid. + mptAlice2.send({ + .account = bob2, + .dest = carol2, + .amt = 1, + .err = tecBAD_PROOF, + }); + + BEAST_EXPECT( + mptAlice2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 0); + } + + // todo: test m exceeding range, require using scala and refactor + } + + /* The equality proof library and range proof library do not + * support generating proofs for amt=0 (they require a positive witness). + * To test the VERIFIER without crashing the helper, we bypass normal proof + * generation by supplying explicit ciphertexts, commitments, and a dummy + * (all-zero) proof. The preflight has no temBAD_AMOUNT guard for + * ConfidentialMPTSend, so all validation occurs in verifySendProofs. + */ + void + testSendZeroAmount(FeatureBitset features) + { + testcase("Send: zero amount — equality and range proof verifier behavior"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({.account = bob}); + + mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)}); + mptAlice.mergeInbox({.account = carol}); + + Buffer const bf = generateBlindingFactor(); + + // equality proof verification for amt=0. + // Encrypt 0 under each participant's key. The amount commitment is + // getTrivialCommitment() — a valid EC point that passes preflight's + // isValidCompressedECPoint check but is not the true PC for amt=0. + // The dummy ZKProof's equality component must be rejected by + // verifyMultiCiphertextEqualityProof. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 0, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf), + .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf), + .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // range proof verification for amt=0. + // Identical construction; focuses on the bulletproof range check + // embedded in ZKProof. The range proof for amount=0 with a dummy + // (all-zero) proof must also be rejected. + Buffer const bf2 = generateBlindingFactor(); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 0, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf2), + .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf2), + .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf2), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // All rejected sends must leave balances unchanged. + BEAST_EXPECT(mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); + BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + + void + testDelete(FeatureBitset features) + { + testcase("Delete"); + using namespace test::jtx; + + // cannot delete mptoken where it has encrypted balance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.authorize({ + .account = bob, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS, + }); + } + + // cannot delete mptoken where it has encrypted balance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.convert({ + .account = carol, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + // carol cannot delete even if he has encrypted zero amount + mptAlice.authorize({ + .account = carol, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS, + }); + } + + // can delete mptoken if outstanding confidential balance is zero + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.authorize({ + .account = bob, + .flags = tfMPTUnauthorize, + }); + } + + // can delete mptoken if issuance has been destroyed and has + // encrypted zero balance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.destroy(); + + mptAlice.authorize({ + .account = bob, + .flags = tfMPTUnauthorize, + }); + } + // test with convert back and delete + // can delete mptoken if converted back (COA returns to zero) + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 100}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = bob, + .amt = 100, + }); + + mptAlice.pay(bob, alice, 100); + + // Should be able to delete as Confidential Outstanding amount is 0 + mptAlice.authorize({ + .account = bob, + .flags = tfMPTUnauthorize, + }); + } + + // removeEmptyHolding: vault share MPToken with confidential balance + // fields should not be deleted on VaultWithdraw + { + Env env{*this, features | featureSingleAssetVault}; + Account const issuer("issuer"); + Account const owner("owner"); + Account const depositor("depositor"); + + MPTTester mptt{env, issuer, {.holders = {owner, depositor}}}; + mptt.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback, + }); + PrettyAsset const asset = mptt.issuanceID(); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + + test::jtx::Vault const vault{env}; + auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // Get the share MPTID from vault + auto const vaultSle = env.le(vaultKeylet); + BEAST_EXPECT(vaultSle != nullptr); + auto const share = vaultSle->at(sfShareMPTID); + + // Depositor deposits into vault + tx = vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}); + env(tx); + env.close(); + + // Verify depositor has share tokens + auto shareMpt = env.le(keylet::mptoken(share, depositor.id())); + BEAST_EXPECT(shareMpt != nullptr); + + // Inject confidential balance fields on the share MPToken + // to simulate a scenario where vault shares somehow have + // confidential balances + env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) { + // Set lsfMPTCanHoldConfidentialBalance on the share issuance + // so the invariant allows encrypted fields on the MPToken + auto issuance = + std::const_pointer_cast(view.read(keylet::mptokenIssuance(share))); + if (!issuance) + return false; + issuance->setFlag(lsfMPTCanHoldConfidentialBalance); + view.rawReplace(issuance); + + auto const k = keylet::mptoken(share, depositor.id()); + auto const sle = std::const_pointer_cast(view.read(k)); + if (!sle) + return false; + // Inject dummy confidential balance fields + Buffer dummyCiphertext(kEcGamalEncryptedTotalLength); + std::memset(dummyCiphertext.data(), 0, kEcGamalEncryptedTotalLength); + dummyCiphertext.data()[0] = kEcCompressedPrefixEvenY; + dummyCiphertext.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + dummyCiphertext.data()[kEcCiphertextComponentLength - 1] = 0x01; + dummyCiphertext.data()[kEcGamalEncryptedTotalLength - 1] = 0x01; + sle->setFieldVL(sfConfidentialBalanceSpending, dummyCiphertext); + sle->setFieldVL(sfConfidentialBalanceInbox, dummyCiphertext); + sle->setFieldVL(sfIssuerEncryptedBalance, dummyCiphertext); + view.rawReplace(sle); + return true; + }); + + // Withdraw everything - which should fail because of the confidential balance fields + tx = vault.withdraw( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}); + env(tx); + + // The share MPToken should still exist because the + // withdrawal failed due to confidential balance obligations + shareMpt = env.le(keylet::mptoken(share, depositor.id())); + BEAST_EXPECT(shareMpt != nullptr); + } + } + + void + testConvertBack(FeatureBitset features) + { + testcase("Convert back"); + using namespace test::jtx; + + // Basic convert back test + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + }); + } + + // Edge case: minimum amount (1) + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 2, .convertAmount = 2}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = bob, + .amt = 1, + }); + } + + // Edge case: kMaxMpTokenAmount + // Using raw JSON to avoid automatic decryption checks in MPTTester + // which don't work for very large amounts (brute-force decryption is slow) + // TODO: improve this test once there is bounded decryption or optimized decryption for + // large amounts + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, kMaxMpTokenAmount); + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + // Convert kMaxMpTokenAmount to confidential using raw JSON + Buffer const convertBlindingFactor = generateBlindingFactor(); + auto const convertHolderCiphertext = + mptAlice.encryptAmount(bob, kMaxMpTokenAmount, convertBlindingFactor); + auto const convertIssuerCiphertext = + mptAlice.encryptAmount(alice, kMaxMpTokenAmount, convertBlindingFactor); + auto const convertContextHash = + getConvertContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob)); + auto const schnorrProof = requireOptional( + mptAlice.getSchnorrProof(bob, convertContextHash), "Missing schnorr proof"); + + { + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTConvert; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount); + jv[sfHolderEncryptionKey.jsonName] = + strHex(requireOptional(mptAlice.getPubKey(bob), "Missing holder public key")); + jv[sfHolderEncryptedAmount.jsonName] = strHex(convertHolderCiphertext); + jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertIssuerCiphertext); + jv[sfBlindingFactor.jsonName] = strHex(convertBlindingFactor); + jv[sfZKProof.jsonName] = strHex(schnorrProof); + + env(jv, Ter(tesSUCCESS)); + } + + // Merge inbox using raw JSON - moves funds from inbox to spending balance + { + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTMergeInbox; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + + env(jv, Ter(tesSUCCESS)); + } + + // ConvertBack kMaxMpTokenAmount - 1 using raw JSON + // After convert + merge, spending balance = kMaxMpTokenAmount + // We convert back kMaxMpTokenAmount - 1 to leave remainder of 1 + std::uint64_t const convertBackAmt = kMaxMpTokenAmount - 1; + + Buffer const convertBackBlindingFactor = generateBlindingFactor(); + auto const convertBackHolderCiphertext = + mptAlice.encryptAmount(bob, convertBackAmt, convertBackBlindingFactor); + auto const convertBackIssuerCiphertext = + mptAlice.encryptAmount(alice, convertBackAmt, convertBackBlindingFactor); + + // Get the encrypted spending balance from ledger (no decryption needed) + auto const encryptedSpendingBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + + // Generate pedersen commitment for the known spending balance + Buffer const pcBlindingFactor = generateBlindingFactor(); + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(kMaxMpTokenAmount, pcBlindingFactor); + + // Generate the proof using known spending balance value + auto const version = mptAlice.getMPTokenVersion(bob); + uint256 const convertBackContextHash = + getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + convertBackAmt, + convertBackContextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = kMaxMpTokenAmount, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + { + json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfMPTAmount.jsonName] = std::to_string(convertBackAmt); + jv[sfHolderEncryptedAmount.jsonName] = strHex(convertBackHolderCiphertext); + jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertBackIssuerCiphertext); + jv[sfBlindingFactor.jsonName] = strHex(convertBackBlindingFactor); + jv[sfBalanceCommitment.jsonName] = strHex(pedersenCommitment); + jv[sfZKProof.jsonName] = strHex(proof); + + env(jv, Ter(tesSUCCESS)); + } + + // Verify the public balance was restored (minus 1 remaining in confidential) + env.require(MptBalance(mptAlice, bob, convertBackAmt)); + } + } + + void + testConvertBackWithAuditor(FeatureBitset features) + { + testcase("Convert back with auditor"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 40}}, + tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + auditor}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + }); + } + + void + testConvertBackPreflight(FeatureBitset features) + { + testcase("Convert back preflight"); + using namespace test::jtx; + + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .err = temDISABLED, + }); + } + + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = alice, + .amt = 30, + .err = temMALFORMED, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 0, + .err = temBAD_AMOUNT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = kMaxMpTokenAmount + 1, + .err = temBAD_AMOUNT, + }); + + // Balance commitment has correct length but invalid EC point data + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .pedersenCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength), + .err = temMALFORMED, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .holderEncryptedAmt = Buffer{}, + .err = temBAD_CIPHERTEXT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .issuerEncryptedAmt = Buffer{}, + .err = temBAD_CIPHERTEXT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .holderEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .issuerEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .auditorEncryptedAmt = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .auditorEncryptedAmt = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + // invalid proof length + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .proof = Buffer{}, + .err = temMALFORMED, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .proof = gMakeZeroBuffer(100), + .err = temMALFORMED, + }); + } + } + + void + testConvertBackPreclaim(FeatureBitset features) + { + testcase("Convert back preclaim"); + using namespace test::jtx; + + // issuance does not exist + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.destroy(); + mptAlice.generateKeyPair(bob); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // tfMPTCanHoldConfidentialBalance is not set on issuance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .err = tecNO_PERMISSION, + }); + } + + // no mptoken + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // mptoken exists but lacks confidential fields + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + + mptAlice.pay(alice, bob, 100); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // Bob's MPToken lacks the confidential fields + auto const sleBobMpt = env.le(keylet::mptoken(mptAlice.issuanceID(), bob.id())); + BEAST_EXPECT(sleBobMpt); + BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfHolderEncryptionKey)); + BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfConfidentialBalanceSpending)); + BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfIssuerEncryptedBalance)); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .err = tecNO_PERMISSION, + }); + } + + // bob tries to convert back more than COA + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({ + .account = bob, + }); + + mptAlice.convert({ + .account = carol, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 300, + .err = tecINSUFFICIENT_FUNDS, + }); + } + + // cannot convert if locked or unauth + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth | + tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + mptAlice.convert({ + .account = bob, + .amt = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({ + .account = bob, + }); + + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .err = tecLOCKED, + }); + + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTUnlock, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + }); + + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .err = tecNO_AUTH, + }); + + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + }); + } + + // Verification of holder and issuer ciphertexts during convertBack + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + // Holder encrypted amount is valid format but mathematically incorrect for this + // convertBack + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .holderEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + + // Issuer encrypted amount is valid format but mathematically incorrect for this + // convertBack + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .issuerEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + } + + // Alice has NOT set an auditor key, but Bob provides + // auditorEncryptedAmt + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + // Provide valid ciphertext to pass preflight + .auditorEncryptedAmt = getTrivialCiphertext(), + .err = tecNO_PERMISSION, + }); + } + + // we set the auditor key, but convertBack omits auditorEncryptedAmt + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + auditor}; + auto& mptAlice = confEnv.mpt; + + // ConvertBack WITHOUT auditorEncryptedAmt + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .fillAuditorEncryptedAmt = false, + .err = tecNO_PERMISSION, + }); + + // ConvertBack where auditor ciphertext mathematically + // correct, but contains invalid data (mismatching amount). + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .auditorEncryptedAmt = getTrivialCiphertext(), + .err = tecBAD_PROOF, + }); + } + } + + void + testClawback(FeatureBitset features) + { + testcase("test ConfidentialMPTClawback"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback | + tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.pay(alice, carol, 200); + mptAlice.authorize({ + .account = dave, + }); + mptAlice.pay(alice, dave, 300); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(dave); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // setup bob. + // after setup, bob's spending balance is 60, inbox balance is 0. + { + // bob converts 60 to confidential + mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)}); + + // bob merge inbox + mptAlice.mergeInbox({ + .account = bob, + }); + } + + // setup carol. + // after setup, carol's spending balance is 120, inbox balance is 0. + { + // carol converts 120 to confidential + mptAlice.convert( + {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)}); + + // carol merge inbox + mptAlice.mergeInbox({ + .account = carol, + }); + } + + // setup dave. + // dave will not merge inbox. + // after setup, dave's inbox balance is 200, spending balance is 0. + mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)}); + + // setup: carol confidential send 50 to bob. + // after send, bob's inbox balance is 50, spending balance + // remains 60. carol's inbox balance remains 0, spending balance + // drops to 70. + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 50, + }); + + // Confidential clawback is burn/reduce outstanding amount. + // The holder public balance is unchanged, and OA/COA decrease. + auto const preBobPublicBalance = mptAlice.getBalance(bob); + auto const preOutstandingAmount = mptAlice.getIssuanceOutstandingBalance(); + auto const preConfidentialOutstandingAmount = mptAlice.getIssuanceConfidentialBalance(); + BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id()))); + + // alice clawback all confidential balance from bob, 110 in total. + // bob has balance in both inbox and spending. These balances should + // become zero after clawback, which is verified in the + // confidentialClaw function. + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 110, + }); + BEAST_EXPECT(mptAlice.getBalance(bob) == preBobPublicBalance); + auto const postOutstandingAmount = mptAlice.getIssuanceOutstandingBalance(); + BEAST_EXPECT( + preOutstandingAmount && postOutstandingAmount && + *postOutstandingAmount == *preOutstandingAmount - 110); + BEAST_EXPECT( + mptAlice.getIssuanceConfidentialBalance() == preConfidentialOutstandingAmount - 110); + BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id()))); + + // alice clawback all confidential balance from carol, which is 70. + // carol only has balance in spending. + mptAlice.confidentialClaw({ + .account = alice, + .holder = carol, + .amt = 70, + }); + + // alice clawback all confidential balance from dave, which is 200. + // dave only has balance in inbox. + mptAlice.confidentialClaw({ + .account = alice, + .holder = dave, + .amt = 200, + }); + } + + void + testClawbackWithAuditor(FeatureBitset features) + { + testcase("test ConfidentialMPTClawback with auditor"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + Account const auditor("auditor"); + MPTTester mptAlice( + env, + alice, + { + .holders = {bob, carol, dave}, + .auditor = auditor, + }); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback | + tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.pay(alice, carol, 200); + mptAlice.authorize({ + .account = dave, + }); + mptAlice.pay(alice, dave, 300); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(dave); + mptAlice.generateKeyPair(auditor); + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + // setup bob. + // after setup, bob's spending balance is 60, inbox balance is 0. + { + // bob converts 60 to confidential + mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)}); + + // bob merge inbox + mptAlice.mergeInbox({ + .account = bob, + }); + } + + // setup carol. + // after setup, carol's spending balance is 120, inbox balance is 0. + { + // carol converts 120 to confidential + mptAlice.convert( + {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)}); + + // carol merge inbox + mptAlice.mergeInbox({ + .account = carol, + }); + } + + // setup dave. + // dave will not merge inbox. + // after setup, dave's inbox balance is 200, spending balance is 0. + mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)}); + + // setup: carol confidential send 50 to bob. + // after send, bob's inbox balance is 50, spending balance + // remains 60. carol's inbox balance remains 0, spending balance + // drops to 70. + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 50, + }); + + // alice clawback all confidential balance from bob, 110 in total. + // bob has balance in both inbox and spending. These balances should + // become zero after clawback, which is verified in the + // confidentialClaw function. + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 110, + }); + + // alice clawback all confidential balance from carol, which is 70. + // carol only has balance in spending. + mptAlice.confidentialClaw({ + .account = alice, + .holder = carol, + .amt = 70, + }); + + // alice clawback all confidential balance from dave, which is 200. + // dave only has balance in inbox. + mptAlice.confidentialClaw({ + .account = alice, + .holder = dave, + .amt = 200, + }); + } + + void + testClawbackInvalidProofContextBinding(FeatureBitset features) + { + testcase("ConfidentialMPTClawback context binding"); + using namespace test::jtx; + + auto runBadProof = [&](auto makeContextHash) { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}}, + tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback | + tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + auto const privKey = mptAlice.getPrivKey(alice); + if (!BEAST_EXPECT(privKey.has_value())) + return; + + auto const proof = mptAlice.getClawbackProof( + bob, + 60, + requireOptionalRef(privKey, "Missing private key"), + makeContextHash(env, mptAlice, alice, bob, carol)); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + .proof = strHex(requireOptional(proof, "Missing proof")), + .err = tecBAD_PROOF, + }); + }; + + // Wrong account (issuer) in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const& alice, + Account const& bob, + Account const& carol) { + return getClawbackContextHash(carol.id(), mpt.issuanceID(), env.seq(alice), bob.id()); + }); + + // Wrong issuance ID in the proof context. + runBadProof([&](Env& env, + MPTTester const&, + Account const& alice, + Account const& bob, + Account const&) { + return getClawbackContextHash( + alice.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(alice), bob.id()); + }); + + // Wrong transaction sequence in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const& alice, + Account const& bob, + Account const&) { + return getClawbackContextHash( + alice.id(), mpt.issuanceID(), env.seq(alice) + 1, bob.id()); + }); + + // Wrong holder in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const& alice, + Account const&, + Account const& carol) { + return getClawbackContextHash(alice.id(), mpt.issuanceID(), env.seq(alice), carol.id()); + }); + } + + // Bob creates the AMM, but Bob is not the MPT holder checked below. + // The AMM has its own pseudo-account (`ammHolder`) that can hold the + // public MPT pool balance. That pseudo-account cannot normally + // initialize confidential state because the confidential txn's must be + // signed by sfAccount, and the AMM pseudo-account has no signing key. + // So this is a construction/impossibility test: public AMM MPT state exists + // but the corresponding confidential AMM clawback flow is not normally reachable. + void + testClawbackPreflight(FeatureBitset features) + { + testcase("test ConfidentialMPTClawback Preflight"); + using namespace test::jtx; + + // test feature disabled + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create(); + mptAlice.authorize({ + .account = bob, + }); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 10, + .proof = "123", + .err = temDISABLED, + }); + } + + // test malformed + { + // set up + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + // only issuer can clawback + mptAlice.confidentialClaw({ + .account = carol, + .holder = bob, + .amt = 10, + .err = temMALFORMED, + }); + + // invalid issuance ID, whose issuer is not alice + { + json::Value jv; + jv[jss::Account] = alice.human(); + jv[sfHolder] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTAmount] = std::to_string(10); + jv[sfZKProof] = "123"; + + // wrong issuance ID + jv[sfMPTokenIssuanceID] = "00000004AE123A8556F3CF91154711376AFB0F894F832B3E"; + + env(jv, Ter(temMALFORMED)); + } + + // issuer cannot clawback from self + mptAlice.confidentialClaw({ + .account = alice, + .holder = alice, + .amt = 10, + .err = temMALFORMED, + }); + + // invalid amount + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 0, + .err = temBAD_AMOUNT, + }); + + // invalid proof length + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 10, + .proof = "123", + .err = temMALFORMED, + }); + } + } + + void + testClawbackPreclaim(FeatureBitset features) + { + testcase("Clawback Preclaim Errors"); + using namespace test::jtx; + + { + // set up, alice is the issuer, bob and carol are authorized + // holders. dave is not authorized. bob has confidential + // balance, carol does not. + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const dave("dave"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth | + tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.authorize({ + .account = alice, + .holder = bob, + }); + mptAlice.authorize({ + .account = carol, + }); + mptAlice.authorize({ + .account = alice, + .holder = carol, + }); + + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 60, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({ + .account = bob, + }); + + // holder does not exist + { + Account const unknown("unknown"); + mptAlice.confidentialClaw({ + .account = alice, + .holder = unknown, + .amt = 10, + .err = tecNO_TARGET, + }); + } + + // dave does not hold mpt at all, no MPT object + { + mptAlice.confidentialClaw({ + .account = alice, + .holder = dave, + .amt = 10, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // carol has no confidential balance + { + mptAlice.confidentialClaw({ + .account = alice, + .holder = carol, + .amt = 10, + .err = tecNO_PERMISSION, + }); + } + } + + // lsfMPTCanClawback not set + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + } + + // no issuer key + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create({ + .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + } + + // issuance not found + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create({ + .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({ + .account = bob, + }); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // destroy the issuance + mptAlice.destroy(); + + json::Value jv; + jv[jss::Account] = alice.human(); + jv[sfHolder] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTAmount] = std::to_string(10); + std::string const dummyProof(kEcClawbackProofLength * 2, '0'); + jv[sfZKProof] = dummyProof; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + + env(jv, Ter(tecOBJECT_NOT_FOUND)); + } + + // After setup, bob has confidential balance 60 in spending. + std::uint32_t const setupFlags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth | + tfMPTCanLock | tfMPTCanHoldConfidentialBalance; + std::string const dummyClawbackProof(kEcClawbackProofLength * 2, '0'); + + auto removeMPTokenField = + [&](Env& env, MPTTester const& mpt, Account const& holder, SField const& field) { + BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) { + auto const sle = std::const_pointer_cast( + view.read(keylet::mptoken(mpt.issuanceID(), holder.id()))); + if (!sle) + return false; + + sle->makeFieldAbsent(field); + view.rawReplace(sle); + return true; + })); + }; + + // After global COA is drained to zero, a further confidential clawback + // fails because the amount exceeds the remaining confidential + // outstanding amount. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + }); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 1, + .proof = dummyClawbackProof, + .err = tecINSUFFICIENT_FUNDS, + }); + } + + // Missing issuer encrypted balance should fail before proof + // verification. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + + removeMPTokenField(env, mptAlice, bob, sfIssuerEncryptedBalance); + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + .proof = dummyClawbackProof, + .err = tecNO_PERMISSION, + }); + } + + // Missing holder encryption key should fail before proof verification. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + + removeMPTokenField(env, mptAlice, bob, sfHolderEncryptionKey); + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + .proof = dummyClawbackProof, + .err = tecNO_PERMISSION, + }); + } + + // lock should not block clawback. lock bob individually + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + mptAlice.set({ + .account = alice, + .holder = bob, + .flags = tfMPTLock, + }); + + // clawback should still work + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + }); + } + + // lock globally + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + mptAlice.set({ + .account = alice, + .flags = tfMPTLock, + }); + + // clawback should still work + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + }); + } + + // unauthorize should not block clawback + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + + // unauthorize bob + mptAlice.authorize({ + .account = alice, + .holder = bob, + .flags = tfMPTUnauthorize, + }); + // clawback should still work + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 60, + }); + } + + // insufficient funds, clawback amount exceeding confidential + // outstanding amount + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags}; + auto& mptAlice = confEnv.mpt; + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 10000, + .err = tecINSUFFICIENT_FUNDS, + }); + } + } + + void + testClawbackProof(FeatureBitset features) + { + testcase("ConfidentialMPTClawback Proof"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // lambda function to set up MPT with alice as issuer, bob and carol + // as authorized holders, and fund 1000 mpt to bob and 2000 mpt to + // carol. + auto setupEnv = [&](Env& env) -> MPTTester { + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance, + }); + + for (auto const& [acct, amt] : {std::pair{bob, 1000}, {carol, 2000}}) + { + mptAlice.authorize({ + .account = acct, + }); + mptAlice.pay(alice, acct, amt); + mptAlice.generateKeyPair(acct); + } + + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + return mptAlice; + }; + + // lambda function to test a set of bad clawback amounts that should + // return tecBAD_PROOF + auto checkBadProofs = + [&](MPTTester& mpt, Account const& holder, std::initializer_list amts) { + for (auto const badAmt : amts) + { + mpt.confidentialClaw({ + .account = alice, + .holder = holder, + .amt = badAmt, + .err = tecBAD_PROOF, + }); + } + }; + + // SCENARIO 1: clawback from inbox only or spending only balances. + // bob converts 500 and merge inbox, + // carol converts 1000, but not merge inbox. + // after setup, bob has 500 in spending, carol has 1000 in inbox. + { + Env env{*this, features}; + auto mptAlice = setupEnv(env); + + // bob converts and merges + mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({ + .account = bob, + }); + // carol converts without merge + mptAlice.convert( + {.account = carol, .amt = 1000, .holderPubKey = mptAlice.getPubKey(carol)}); + + // verify proof fails with invalid clawback amount + // bob: 500 in Spending, 0 in Inbox + checkBadProofs( + mptAlice, + bob, + { + 1, + 10, + 70, + 100, + 110, + 200, + 499, + 501, + 600, + }); + + // carol: 1000 in Inbox, 0 in Spending + checkBadProofs( + mptAlice, + carol, + { + 1, + 10, + 50, + 500, + 777, + 850, + 999, + 1001, + 1200, + }); + + // clawback with correct amount that passes proof verification + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 500, + }); + mptAlice.confidentialClaw({ + .account = alice, + .holder = carol, + .amt = 1000, + }); + } + + // SCENARIO 2: clawback from mixed inbox and spending balances. + // bob converts 300 to confidential and merge inbox, + // carol converts 400 to confidential and merge inbox, + // bob sends 100 to carol, carol sends 100 to bob. + // After setup, bob has 100 in inbox and 200 in spending; + // carol has 100 in inbox and 300 in spending. + { + Env env{*this, features}; + auto mptAlice = setupEnv(env); + + mptAlice.convert({.account = bob, .amt = 300, .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({ + .account = bob, + }); + mptAlice.convert( + {.account = carol, .amt = 400, .holderPubKey = mptAlice.getPubKey(carol)}); + mptAlice.mergeInbox({ + .account = carol, + }); + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 100, + }); + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 100, + }); + + // verify proof fails with invalid clawback amount + // bob: 100 in inbox, 200 in spending + checkBadProofs( + mptAlice, + bob, + { + 1, + 10, + 50, + 100, + 200, + 299, + 301, + 400, + }); + + // proof failure for incorrect amount when clawbacking from + // carol carol: 100 in inbox, 300 in spending + checkBadProofs( + mptAlice, + carol, + { + 1, + 10, + 50, + 100, + 300, + 399, + 401, + 501, + }); + + // clawback with correct amount that passes proof verification + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 300, + }); + mptAlice.confidentialClaw({ + .account = alice, + .holder = carol, + .amt = 400, + }); + } + + // SCENARIO 3: the clawback proof omits the holder's confidential + // balance version. A proof generated before the version advances is + // still accepted, because getClawbackContextHash has no version + // component. + { + Env env{*this, features}; + auto mptAlice = setupEnv(env); + + mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({ + .account = bob, + }); + + auto const privKey = mptAlice.getPrivKey(alice); + if (!BEAST_EXPECT(privKey.has_value())) + return; + + auto const proof = mptAlice.getClawbackProof( + bob, + 500, + requireOptionalRef(privKey, "Missing private key"), + getClawbackContextHash( + alice.id(), mptAlice.issuanceID(), env.seq(alice), bob.id())); + if (!BEAST_EXPECT(proof.has_value())) + return; + + // Advance bob's balance version after the proof is generated. An + // empty-inbox merge leaves the balance unchanged but still bumps + // sfConfidentialBalanceVersion. + auto const versionBefore = mptAlice.getMPTokenVersion(bob); + mptAlice.mergeInbox({.account = bob}); + BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) != versionBefore); + + // The stale-version proof is still accepted. + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 500, + .proof = strHex(requireOptional(proof, "Missing proof")), + }); + } + } + + void + testPublicTransfersAfterClearingConfidentialFlag(FeatureBitset features) + { + testcase("Public transfers after clearing Confidential Flag"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + + // After clearing the confidential flag, all four public MPT operations + // must succeed regardless of which confidential path left encrypted-zero + // fields on bob's MPToken. + auto runPublicPayments = [&](MPTTester& mpt) { + mpt.pay(bob, carol, 10); + mpt.pay(carol, bob, 5); + mpt.pay(alice, bob, 1); + mpt.pay(carol, alice, 5); + }; + + auto drainAndDeleteBobMPToken = [&](Env& env, MPTTester& mpt) { + auto const bobBalance = mpt.getBalance(bob); + BEAST_EXPECT(bobBalance > 0); + + mpt.pay(bob, alice, bobBalance); + BEAST_EXPECT(mpt.getBalance(bob) == 0); + + mpt.authorize({.account = bob, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(mpt.issuanceID(), bob.id()))); + }; + + // Alice pays Bob 100 public, Bob converts 50 confidential + // Bob converts 50 back to public, and make sure can receive public payments + { + Env env{*this, features}; + ConfidentialEnv ct{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + + env.fund(XRP(1'000), carol); + ct.mpt.authorize({.account = carol}); + ct.mpt.pay(alice, carol, 50); + + ct.mpt.convertBack({.account = bob, .amt = 50}); + + runPublicPayments(ct.mpt); + drainAndDeleteBobMPToken(env, ct.mpt); + } + + // Same path as above but with Auditor + { + Env env{*this, features}; + Account const auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(auditor); + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + mptAlice.convertBack({.account = bob, .amt = 50}); + + runPublicPayments(mptAlice); + drainAndDeleteBobMPToken(env, mptAlice); + } + + // Confidential clawback leaves encrypted-zero fields; + // the public balance remaining after the clawback must stay usable. + { + Env env{*this, features}; + ConfidentialEnv ct{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance}; + + env.fund(XRP(1'000), carol); + ct.mpt.authorize({.account = carol}); + ct.mpt.pay(alice, carol, 50); + + ct.mpt.confidentialClaw({.account = alice, .holder = bob, .amt = 50}); + + runPublicPayments(ct.mpt); + drainAndDeleteBobMPToken(env, ct.mpt); + } + } + + void + testMutatePrivacy(FeatureBitset features) + { + testcase("mutate lsfMPTCanHoldConfidentialBalance"); + using namespace test::jtx; + + // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance + // when featureDynamicMPT is disabled + { + Env env{*this, features - featureDynamicMPT}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 0, + .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .err = temDISABLED, + }); + } + + // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance when + // featureConfidentialTransfer is disabled + { + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 0, + .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .err = temDISABLED, + }); + } + + // if lsmfMPTCannotEnableCanHoldConfidentialBalance is set, can not set/clear + // lsfMPTCanHoldConfidentialBalance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + }); + + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .err = tecNO_PERMISSION, + }); + } + + // Toggle lsfMPTCanHoldConfidentialBalance + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + .mutableFlags = tmfMPTCanEnableCanLock, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + auto holderPubKeySet = false; + auto verifyToggle = [&](TER expectedResult, uint64_t amt) { + if (!holderPubKeySet) + { + mptAlice.convert({ + .account = bob, + .amt = amt, + .holderPubKey = mptAlice.getPubKey(bob), + .err = expectedResult, + }); + } + else + { + mptAlice.convert({ + .account = bob, + .amt = amt, + .err = expectedResult, + }); + } + + if (expectedResult == tesSUCCESS) + { + holderPubKeySet = true; + mptAlice.mergeInbox({ + .account = bob, + }); + + // make sure there's no confidential outstanding balance + // for the next toggle test + mptAlice.convertBack({ + .account = bob, + .amt = amt, + }); + } + }; + + // set lsfMPTCanHoldConfidentialBalance, but no effect because + // lsfMPTCanHoldConfidentialBalance was already set + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + }); + verifyToggle(tesSUCCESS, 10); + + // set tmfMPTSetCanHoldConfidentialBalance again + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + }); + verifyToggle(tesSUCCESS, 30); + } + + // can not mutate lsfPrivacy when there's confidential + // outstanding amount + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + // lsmfMPTCannotEnableCanHoldConfidentialBalance is false by default, + // so that lsfMPTCanHoldConfidentialBalance can be mutated + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({ + .account = bob, + }); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob convert 50 to confidential + mptAlice.convert({.account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob)}); + + // set lsfMPTCanHoldConfidentialBalance should fail because of + // confidential outstanding balance + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .err = tecNO_PERMISSION, + }); + } + } + + void + testConvertBackPedersenProof(FeatureBitset features) + { + testcase("Convert back pedersen proof"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}}; + auto& mptAlice = confEnv.mpt; + + // for ease of understanding, generate all the fields here instead of + // autofilling + uint64_t const amt = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + + auto const spendingBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const encryptedSpendingBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + BEAST_EXPECT(!encryptedSpendingBalance.empty()); + + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor); + Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor); + auto const version = mptAlice.getMPTokenVersion(bob); + + // These tests verify that the compact ConvertBack proof validation + // correctly rejects proofs generated with incorrect parameters. + // The compact proof simultaneously verifies balance ownership, + // commitment linkage, and that remaining balance is non-negative. + + // Test 1: Proof generated with wrong pedersen commitment value. + // The proof uses PC(1, rho) but the transaction submits PC(balance, rho). + // Verification fails because the proof doesn't match the submitted commitment. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + Buffer const badPedersenCommitment = + mptAlice.getPedersenCommitment(1, pcBlindingFactor); + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = badPedersenCommitment, // wrong pedersen commitment + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 2: Proof generated with wrong blinding factor (rho). + // The pedersen commitment PC = balance*G + rho*H requires the same rho + // used in proof generation. Using a different rho breaks the linkage. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = generateBlindingFactor(), // wrong blinding factor + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 3: Proof generated with wrong balance value. + // The proof claims balance=1 but the encrypted spending balance contains + // the actual balance. Verification fails because the values don't match. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = 1, // wrong balance + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 4: Correct proof but wrong pedersen commitment in transaction. + // The proof is generated correctly, but the transaction submits a + // different pedersen commitment. Verification fails because the + // submitted commitment doesn't match what the proof was generated for. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + Buffer const badPedersenCommitment = + mptAlice.getPedersenCommitment(1, pcBlindingFactor); + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = badPedersenCommitment, // wrong pedersen commitment + .err = tecBAD_PROOF, + }); + } + + // Test 5: Proof generated with wrong context hash. + // The context hash binds the proof to a specific transaction (account, + // sequence, issuanceID, amount, version). Using a different context hash + // makes the proof invalid for this transaction, preventing replay attacks. + { + uint256 const badContextHash{1}; + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + badContextHash, // wrong context hash + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 6: Correct proof to verify the test setup is valid. + // All parameters are correct, so the transaction should succeed. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + }); + } + } + + void + testConvertBackBulletproof(FeatureBitset features) + { + testcase("Convert back bulletproof"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}}; + auto& mptAlice = confEnv.mpt; + + // for ease of understanding, generate all the fields here instead of + // autofilling + uint64_t const amt = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + + auto const spendingBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const encryptedSpendingBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + BEAST_EXPECT(!encryptedSpendingBalance.empty()); + + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor); + Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor); + auto const version = mptAlice.getMPTokenVersion(bob); + + // These tests verify that the compact ConvertBack proof (sigma + bulletproof) + // correctly rejects proofs generated with incorrect parameters. + // The compact proof simultaneously verifies balance ownership, commitment + // linkage, and that the remaining balance is non-negative. + + // Test 1: Proof generated with wrong balance value. + // The sigma proof claims balance=1 but the spending balance contains the + // actual balance. The compact proof's balance-linkage check fails. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = 1, // wrong balance (actual balance is ~40) + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 2: Proof generated with wrong blinding factor (rho). + // The compact sigma proof must use the same blinding factor (rho) as the + // Pedersen commitment PC = balance*G + rho*H. Using a different rho + // creates an inconsistency the verifier detects. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = generateBlindingFactor(), // wrong blinding factor + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 3: Proof generated with wrong context hash. + // The context hash binds the proof to a specific transaction (account, + // sequence, issuanceID, amount, version). Using a different context hash + // makes the proof invalid for this transaction, preventing replay attacks. + { + uint256 const badContextHash{1}; + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + badContextHash, // wrong context hash + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + // Test 4: Correct proof to verify the test setup is valid. + // All parameters are correct, so the transaction should succeed. + { + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + }); + } + } + + // A convert-back proof is bound to (account, issuance, sequence, version) via + // the Fiat-Shamir context hash. Crafting a proof against any single wrong + // variable and submitting it with the real parameters must be rejected + // with tecBAD_PROOF + void + testConvertBackInvalidProofContextBinding(FeatureBitset features) + { + testcase("ConvertBack proof context binding"); + using namespace test::jtx; + + auto runBadProof = [&](auto makeContextHash) { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}}; + auto& mptAlice = confEnv.mpt; + + std::uint64_t const amt = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + + auto const spendingBalance = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + auto const encryptedSpendingBalance = + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending); + if (!BEAST_EXPECT(spendingBalance && encryptedSpendingBalance)) + return; + + Buffer const pedersenCommitment = mptAlice.getPedersenCommitment( + requireOptional(spendingBalance, "Missing spending balance"), pcBlindingFactor); + Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor); + Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor); + auto const version = mptAlice.getMPTokenVersion(bob); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + makeContextHash(env, mptAlice, alice, bob, carol, version), + { + .pedersenCommitment = pedersenCommitment, + .amt = requireOptional(spendingBalance, "Missing spending balance"), + .encryptedAmt = requireOptionalRef( + encryptedSpendingBalance, "Missing encrypted spending balance"), + .blindingFactor = pcBlindingFactor, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + }; + + // Wrong account in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const& carol, + std::uint32_t version) { + return getConvertBackContextHash(carol.id(), mpt.issuanceID(), env.seq(bob), version); + }); + + // Wrong issuance ID in the proof context. + runBadProof([&](Env& env, + MPTTester const&, + Account const& alice, + Account const& bob, + Account const&, + std::uint32_t version) { + return getConvertBackContextHash( + bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob), version); + }); + + // Wrong transaction sequence in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const&, + std::uint32_t version) { + return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1, version); + }); + + // Wrong balance version in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const&, + std::uint32_t version) { + return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), version + 1); + }); + } + + // This test simulates a valid proof π extracted from a transaction + // for amount m1 is reused in a new transaction for a different + // amount m2 with different ciphertexts. It confirms the context hash + // recomputation fails due to the ciphertext binding mismatch, resulting + // in tecBAD_PROOF. + void + testConvertBackProofCiphertextBinding(FeatureBitset features) + { + testcase("ConvertBack: proof ciphertext binding"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + auto const spendingBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const encryptedSpendingBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + auto const version = mptAlice.getMPTokenVersion(bob); + Buffer const pcBlindingFactor = generateBlindingFactor(); + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + + // Generate a valid proof pi for Amount m1 = 10 + uint64_t const amtA = 10; + uint32_t const currentSeq = env.seq(bob); + uint256 const contextHashA = + getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, version); + + Buffer const proofA = mptAlice.getConvertBackProof( + bob, + amtA, + contextHashA, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + + // Construct Transaction B with Amount m2 = 20 and attach Proof pi + uint64_t const amtB = 20; + Buffer const blindingFactorB = generateBlindingFactor(); + Buffer const bobCiphertextB = mptAlice.encryptAmount(bob, amtB, blindingFactorB); + Buffer const issuerCiphertextB = mptAlice.encryptAmount(alice, amtB, blindingFactorB); + + // We attempt to verify the proof pi (for amt 10) against the new ciphertexts (for amt 20). + mptAlice.convertBack({ + .account = bob, + .amt = amtB, + .proof = proofA, // Extracted/Reused proof from Transaction A + .holderEncryptedAmt = bobCiphertextB, + .issuerEncryptedAmt = issuerCiphertextB, + .blindingFactor = blindingFactorB, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, // Expected failure + }); + } + + // This test simulates a valid proof π and ciphertext are + // tied to version v, but are reused after an inbox merge has incremented + // the CBS version to v+1. It confirms the validator rejects the transaction + // before acceptance due to the ContextID mismatch. + void + testConvertBackProofVersionMismatch(FeatureBitset features) + { + testcase("ConvertBack: proof version mismatch"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 1000, .convertAmount = 100}}}; + auto& mptAlice = confEnv.mpt; + + auto const versionV = mptAlice.getMPTokenVersion(bob); + auto const spendingBalanceV = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const encryptedSpendingBalanceV = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + + // Parameters for the intended ConvertBack transaction + uint64_t const amt = 10; + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalanceV, pcBlindingFactor); + Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor); + Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor); + + // State Change: Increment version to v+1 + // Converting more funds and merging increments the sfConfidentialBalanceVersion + mptAlice.convert({ + .account = bob, + .amt = 50, + }); + mptAlice.mergeInbox({ + .account = bob, + }); + + BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) > versionV); + + // Attack: Attempt to reuse proof tied to Version v at ledger Version v+1 + uint32_t const currentSeq = env.seq(bob); + // Proof is explicitly generated using the outdated Version v + uint256 const oldContextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, versionV); + + Buffer const oldProof = mptAlice.getConvertBackProof( + bob, + amt, + oldContextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalanceV, + .encryptedAmt = encryptedSpendingBalanceV, + .blindingFactor = pcBlindingFactor, + }); + + // Submit and verify failure + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = oldProof, + .holderEncryptedAmt = bobCiphertext, + .issuerEncryptedAmt = issuerCiphertext, + .blindingFactor = blindingFactor, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, // Fails because TransactionContextID differs + }); + } + + /* This test simulates an attack where the holder ciphertext is modified + * via homomorphic addition (adding Encrypted_amt(1)) while leaving the issuer + * ciphertext unchanged. It confirms that the validator detects the + * mismatch between the re-computed ciphertexts and the submitted ones, + * resulting in tecBAD_PROOF. */ + void + testConvertBackHomomorphicCiphertextModification(FeatureBitset features) + { + testcase("ConvertBack: homomorphic ciphertext modification"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + // Prepare valid parameters for a ConvertBack of 10 + uint64_t const amt = 10; + Buffer const bf = generateBlindingFactor(); + + auto const holderCipherText = mptAlice.encryptAmount(bob, amt, bf); + auto const issuerCipherText = mptAlice.encryptAmount(alice, amt, bf); + + // Generate a "Delta" ciphertext (Encrypting 1) + // We use Bob's key because we are tampering with Bob's (Holder's) field + Buffer const deltaBf = generateBlindingFactor(); + auto const deltaCipherText = mptAlice.encryptAmount(bob, 1, deltaBf); + + // Homomorphically add Delta to HolderCipherText: Tampered = Enc(10) + Enc(1) = Enc(11) + Buffer tamperedHolderCipherText = requireOptional( + homomorphicAdd(holderCipherText, deltaCipherText), "Missing tampered ciphertext"); + + // Generate a valid proof for the ORIGINAL amount (10) + auto const spendingBal = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const spendingBalEnc = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + Buffer const pcBf = generateBlindingFactor(); + auto const pedersenCommitment = mptAlice.getPedersenCommitment(spendingBal, pcBf); + + auto const currentVersion = mptAlice.getMPTokenVersion(bob); + // Uses the new signature: Account, IssuanceID, Sequence, Version + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + amt, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBal, + .encryptedAmt = spendingBalEnc, + .blindingFactor = pcBf, + }); + + // Submit transaction with Divergent Ciphertexts + // Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10. + // The consistency check (re-encryption of `amt` with `bf`) will match Issuer but FAIL for + // Holder. + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .proof = proof, + .holderEncryptedAmt = tamperedHolderCipherText, // Tampered (11) + .issuerEncryptedAmt = issuerCipherText, // Original (10) + .blindingFactor = bf, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + } + + /* This test verifies that xrpld correctly rejects attempts to + * overflow the maximum allowable token amount via homomorphic manipulation. + * It simulates an attack where an individual takes a valid ciphertext encrypting + * the maximum amount (kMaxMpTokenAmount) and homomorphically adds an encryption of + * 1 to it, producing a ciphertext for MAX+1. The test confirms that the Bulletproof + * range proof or inner-product constraints detect this overflow and invalidate the + * transaction, preserving the supply invariant. */ + void + testSendHomomorphicOverflow(FeatureBitset features) + { + testcase("Send: homomorphic overflow attack via Enc(MAX) + Enc(1)"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 100}, + {.account = carol, .payAmount = 50, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + // Bob sends 10 to carol. The send amount (10) and Bob's remaining balance + // (90) are both within [0, kMaxMpTokenAmount]. Range proof passes. + mptAlice.send({.account = bob, .dest = carol, .amt = 10}); + + // Bob's spending balance is 90 after the baseline send. + auto const bobSpendingBefore = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + BEAST_EXPECT(bobSpendingBefore == 90); + + // Construct Enc(kMaxMpTokenAmount) with Bob's public key. + Buffer const bf1 = generateBlindingFactor(); + Buffer const encMax = mptAlice.encryptAmount(bob, kMaxMpTokenAmount, bf1); + + // Construct Enc(1) with a separate blinding factor. + Buffer const bf2 = generateBlindingFactor(); + Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2); + + // Homomorphically add to produce CB_S_holder' = Enc(MAX) + Enc(1) + Buffer overflowedCt = + requireOptional(homomorphicAdd(encMax, encOne), "Missing overflowed ciphertext"); + + // Submit the send transaction with the tampered ciphertext. + // Setting amt = kMaxMpTokenAmount + 1 drives proof generation for the + // overflowed value. The bulletproof range check [0, kMaxMpTokenAmount] + // rejects MAX+1; the validator must return tecBAD_PROOF. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = kMaxMpTokenAmount + 1, + .senderEncryptedAmt = overflowedCt, + .err = tecBAD_PROOF, + }); + + auto const bobSpendingAfter = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter); + } + + /* This test ensures that the system prevents underflow attacks where a user + * attempts to create a negative balance through homomorphic subtraction. It + * simulates a scenario where an attacker takes a ciphertext encrypting zero + * and subtracts an encryption of 1, resulting in a value of -1. + * The test asserts that the range proof verification fails because the resulting + * value falls outside the valid non-negative range [0, kMaxMpTokenAmount], + * causing the validator to reject the transaction with tecBAD_PROOF. */ + void + testConvertBackHomomorphicUnderflow(FeatureBitset features) + { + testcase("ConvertBack: homomorphic underflow attack via Enc(0) - Enc(1)"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"); + ConfidentialEnv confEnv{ + env, alice, {{.account = bob, .payAmount = 10, .convertAmount = 10}}}; + auto& mptAlice = confEnv.mpt; + + // Converting back 1 from 10 leaves remaining balance = 9 (non-negative). + // Range proof [0, kMaxMpTokenAmount] passes. + mptAlice.convertBack({.account = bob, .amt = 1}); + + // Bob's spending balance is now 9; public balance is 1. + auto const bobSpendingBefore = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + BEAST_EXPECT(bobSpendingBefore == 9); + auto const bobPublicBefore = mptAlice.getBalance(bob); + BEAST_EXPECT(bobPublicBefore == 1); + + // Construct Enc(0) — the zero encrypted balance using Bob's key. + Buffer const bf1 = generateBlindingFactor(); + Buffer const encZero = mptAlice.encryptAmount(bob, 0, bf1); + + // Construct Enc(1) with a separate blinding factor. + Buffer const bf2 = generateBlindingFactor(); + Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2); + + // Homomorphically subtract to produce CB_S_holder' = Enc(0) − Enc(1) + // = Enc(−1), which lies below [0, kMaxMpTokenAmount]. + Buffer underflowedCt = + requireOptional(homomorphicSubtract(encZero, encOne), "Missing underflowed ciphertext"); + + // The underflowed value as uint64_t: 0 - 1 wraps to 0xFFFFFFFFFFFFFFFF. + // Generate a real proof using this wrapped value. The validator must still reject it + // because 0xFFFFFFFFFFFFFFFE (remaining balance) is outside [0, kMaxMpTokenAmount]. + constexpr std::uint64_t kUnderflowedAmt = + static_cast(0) - static_cast(1); + + Buffer const pcBf = generateBlindingFactor(); + Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(kUnderflowedAmt, pcBf); + + auto const currentVersion = mptAlice.getMPTokenVersion(bob); + uint256 const contextHash = + getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion); + + Buffer const proof = mptAlice.getConvertBackProof( + bob, + 1, + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = kUnderflowedAmt, + .encryptedAmt = underflowedCt, + .blindingFactor = pcBf, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 1, + .proof = proof, + .holderEncryptedAmt = underflowedCt, + .pedersenCommitment = pedersenCommitment, + .err = tecBAD_PROOF, + }); + + // Supply invariant: both public and confidential balances must be unchanged + // after the rejected attack. + BEAST_EXPECT(mptAlice.getBalance(bob) == bobPublicBefore); + auto const bobSpendingAfter = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter); + } + + // Confidential sends carry encrypted amounts and a zero-knowledge proof. + // Both are built from elliptic-curve math, so every coordinate in the + // transaction must be a real point on the secp256k1 curve. These three + // variants confirm the validator rejects garbage coordinates at the right + // stage before any expensive cryptographic verification runs. + void + testSendInvalidCurvePoints(FeatureBitset features) + { + testcase("Send: off-curve EC points"); + using namespace test::jtx; + + // Variant A: garbage coordinate in ciphertext / commitment fields + // getBadCiphertext() looks structurally valid (correct length, right + // prefix byte 0x02) but its x-coordinate is 0xFF...FF, which does not + // lie on secp256k1. Preflight must reject before any ledger access. + { + Account const alice("alice"), bob("bob"), carol("carol"); + Env env{*this, features}; + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 30}}}; + auto& mptAlice = confEnv.mpt; + + // sender's encrypted amount has an invalid coordinate + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = getBadCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // recipient's encrypted amount has an invalid coordinate + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .destEncryptedAmt = getBadCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // issuer's encrypted amount has an invalid coordinate + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .issuerEncryptedAmt = getBadCiphertext(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // The amount and balance commitments are single curve coordinates + // used to tie the proof to the transfer amount and sender balance. + // A commitment with a valid-looking prefix but an impossible + // x-coordinate must also be rejected. + Buffer badCommitment(kEcPedersenCommitmentLength); + std::memset(badCommitment.data(), 0xFF, kEcPedersenCommitmentLength); + badCommitment.data()[0] = kEcCompressedPrefixEvenY; + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = badCommitment, + .balanceCommitment = getTrivialCommitment(), + .err = temMALFORMED, + }); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = badCommitment, + .err = temMALFORMED, + }); + } + + // Variant B: garbage coordinates inside the ZKP proof blob + // The proof blob has the right total byte length (so it passes the + // length check at preflight), but every embedded coordinate is + // 0xFF...FF — impossible on secp256k1. The proof verifier must detect + // this and return tecBAD_PROOF without crashing. + { + Account const alice("alice"), bob("bob"), carol("carol"); + Env env{*this, features}; + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 30}}}; + auto& mptAlice = confEnv.mpt; + + Buffer badProof(kEcSendProofLength); + std::memset(badProof.data(), 0xFF, kEcSendProofLength); + badProof.data()[0] = kEcCompressedPrefixEvenY; + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = strHex(badProof), + .err = tecBAD_PROOF, + }); + } + + // Variant C: only one of the two ciphertext coordinates is bad + // Each encrypted amount is two coordinates back-to-back: C1 then C2. + // Both must be valid. These tests corrupt only one at a time to + // confirm both are checked independently. + { + Account const alice("alice"), bob("bob"), carol("carol"); + Env env{*this, features}; + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 30}}}; + auto& mptAlice = confEnv.mpt; + + // getTrivialCiphertext() has both C1 and C2 as valid (but trivial) + // curve coordinates. We replace one half at a time with 0xFF...FF. + auto const& tc = getTrivialCiphertext(); + + // C1 = bad (0xFF...FF), C2 = valid trivial point + Buffer badC1goodC2(kEcGamalEncryptedTotalLength); + std::memset(badC1goodC2.data(), 0xFF, kEcGamalEncryptedTotalLength); + badC1goodC2.data()[0] = kEcCompressedPrefixEvenY; + std::memcpy( + badC1goodC2.data() + kEcCiphertextComponentLength, + tc.data() + kEcCiphertextComponentLength, + kEcCiphertextComponentLength); + + // C1 = valid trivial point, C2 = bad (0xFF...FF) + Buffer goodC1badC2(kEcGamalEncryptedTotalLength); + std::memset(goodC1badC2.data(), 0xFF, kEcGamalEncryptedTotalLength); + std::memcpy(goodC1badC2.data(), tc.data(), kEcCiphertextComponentLength); + goodC1badC2.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + + // sender's encrypted amount — bad C1 + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = badC1goodC2, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // sender's encrypted amount — bad C2 + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = goodC1badC2, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // recipient's encrypted amount — bad C1 + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .destEncryptedAmt = badC1goodC2, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + + // recipient's encrypted amount — bad C2 + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .destEncryptedAmt = goodC1badC2, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = temBAD_CIPHERTEXT, + }); + } + } + + // Reject points from the wrong elliptic curve (wrong-group injection). + // + // An attacker might submit coordinates that come from a completely + // different elliptic curve, for example, the one used in TLS + // certificates (NIST P-256). If those coordinates happen to also be + // valid points on secp256k1 (which is possible since both curves use + // 256-bit fields), the format check at preflight will pass. However, + // the zero-knowledge proof is built specifically for secp256k1: the + // math inside the proof only holds for the right curve, so any + // transaction carrying cross-curve data will still be rejected at + // proof verification (tecBAD_PROOF). + void + testSendWrongGroupPointInjection(FeatureBitset features) + { + testcase("Send: wrong-group point injection rejected"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 60}, + {.account = carol, .payAmount = 50, .convertAmount = 30}}}; + auto& mptAlice = confEnv.mpt; + + // The x-coordinate of the NIST P-256 generator point — a real, + // well-known value from a different elliptic curve (used in TLS + // and certificates). This x-coordinate is also a valid secp256k1 + // point, so it passes preflight. Rejection happens at proof + // verification because the ZKP is secp256k1-specific. + // + // P-256 generator x: + // 6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296 + static constexpr std::uint8_t kP256GeneratorX[32] = { + 0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6, + 0xE5, 0x63, 0xA4, 0x40, 0xF2, 0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, + 0x33, 0xA0, 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96, + }; + + // A 66-byte encrypted amount using the P-256 x-coordinate for both halves. + Buffer wrongGroupCt(kEcGamalEncryptedTotalLength); + wrongGroupCt.data()[0] = kEcCompressedPrefixEvenY; + std::memcpy(wrongGroupCt.data() + 1, kP256GeneratorX, 32); + wrongGroupCt.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + std::memcpy(wrongGroupCt.data() + kEcCiphertextComponentLength + 1, kP256GeneratorX, 32); + + // A 33-byte commitment using the same wrong-curve x-coordinate. + Buffer wrongGroupCommitment(kEcPedersenCommitmentLength); + wrongGroupCommitment.data()[0] = kEcCompressedPrefixEvenY; + std::memcpy(wrongGroupCommitment.data() + 1, kP256GeneratorX, 32); + + // sender's encrypted amount uses a coordinate from the wrong curve + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .senderEncryptedAmt = wrongGroupCt, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // recipient's encrypted amount uses a coordinate from the wrong curve + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .destEncryptedAmt = wrongGroupCt, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // issuer's encrypted amount uses a coordinate from the wrong curve + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .issuerEncryptedAmt = wrongGroupCt, + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // amount commitment uses a coordinate from the wrong curve + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = wrongGroupCommitment, + .balanceCommitment = getTrivialCommitment(), + .err = tecBAD_PROOF, + }); + + // balance commitment uses a coordinate from the wrong curve + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .proof = getTrivialSendProofHex(), + .amountCommitment = getTrivialCommitment(), + .balanceCommitment = wrongGroupCommitment, + .err = tecBAD_PROOF, + }); + } + + // Reject an all-zero "null" public key. + // + // Every account in a confidential transfer needs a real public key — + // a specific point on the secp256k1 curve derived from a secret number + // only that account knows. An all-zero key (33 bytes of 0x00) is not + // a real key. It has no secret behind it, and encrypting data to it + // would not actually hide anything. The validator must reject it at + // preflight so no account can ever register a broken key. + void + testConvertIdentityElementRejection(FeatureBitset features) + { + testcase("Convert: all-zero public key rejected"); + using namespace test::jtx; + + // 33 zero bytes — not a real public key; no valid secret maps to this. + Buffer const nullKey = gMakeZeroBuffer(kEcPubKeyLength); + + // Recipient (holder) tries to register an all-zero key. + // Must be rejected so no account ends up with an unprotected balance. + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 50); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // recipient (carol) tries to register an all-zero key + mptAlice.convert({ + .account = carol, + .amt = 10, + .holderPubKey = nullKey, + .err = temMALFORMED, + }); + + // sender (bob) tries to register an all-zero key + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = nullKey, + .err = temMALFORMED, + }); + } + + // Issuer tries to register an all-zero key. + // The issuer's key is used to encrypt the issuer's copy of every + // transfer amount. + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set({ + .account = alice, + .issuerPubKey = nullKey, + .err = temMALFORMED, + }); + } + } + + /* This test ensures that when sending confidential tokens, the encrypted + * amounts are securely locked to the correct accounts' official public keys. + * + * Attack scenario — Encrypting the issuer's copy with the wrong key: + * A sender correctly encrypts the hidden transfer amount for themselves + * and the receiver. However, they intentionally encrypt the issuer's + * copy of the data using the wrong public key (for example, using the + * receiver's key instead of the official issuer's key). */ + void + testSendWrongIssuerPublicKey(FeatureBitset features) + { + testcase("Send: issuer ciphertext encrypted under wrong public key"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 100}, + {.account = carol, .payAmount = 50, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + auto const bobSpendingBefore = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + + // issuer ciphertext encrypted under carol's holder key + // (should be under alice's registered issuer key). + { + Buffer const bf = generateBlindingFactor(); + Buffer const wrongIssuerCt = mptAlice.encryptAmount(carol, 10, bf); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .issuerEncryptedAmt = wrongIssuerCt, + .err = tecBAD_PROOF, + }); + } + + // issuer ciphertext encrypted under bob's holder key + // (the sender's own key — still not the registered issuer key). + { + Buffer const bf = generateBlindingFactor(); + Buffer const wrongIssuerCt = mptAlice.encryptAmount(bob, 10, bf); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .issuerEncryptedAmt = wrongIssuerCt, + .err = tecBAD_PROOF, + }); + } + + // all balances unchanged + BEAST_EXPECT( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == + bobSpendingBefore); + BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + } + + // This test verifies that the compact AND-composed Send sigma proof + // enforces the shared-randomness invariant across participants. + void + testSendSharedRandomnessViolation(FeatureBitset features) + { + testcase("divergent C1 across participants in ConfidentialMPTSend"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 50}, + {.account = carol, .payAmount = 50, .convertAmount = 50}}, + tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + auto& mptAlice = confEnv.mpt; + + // Send amount is 10. + uint64_t const amt = 10; + + enum class Participant { Sender, Dest, Issuer, Auditor }; + + // This lambda submits a send transaction where one of the four ciphertexts + // is encrypted with different randomness than the one used to build the proof. + // Note: When divergent is nullopt, all participants + // will use the same randomness and expected to succeed, this is the + // control case that confirms the test setup itself is sound, the bad proof + // is actually from divergent randomness, not other causes. + auto submitWithDivergentC1 = [&](std::optional divergent) { + ConfidentialSendSetup setup(mptAlice, bob, carol, alice, amt, std::cref(auditor)); + + auto const proofOpt = + requireOptional(setup.generateProof(mptAlice, env, bob, carol), "Missing proof"); + + // Re-encrypt one participant's ciphertext with divergent randomness. + Buffer senderCt = setup.senderAmt; + Buffer destCt = setup.destAmt; + Buffer issuerCt = setup.issuerAmt; + Buffer auditorCt = + requireOptionalRef(setup.auditorAmt, "Missing auditor encrypted amount"); + if (divergent) + { + Buffer const bfDivergent = generateBlindingFactor(); + switch (*divergent) + { + case Participant::Sender: + senderCt = mptAlice.encryptAmount(bob, amt, bfDivergent); + break; + case Participant::Dest: + destCt = mptAlice.encryptAmount(carol, amt, bfDivergent); + break; + case Participant::Issuer: + issuerCt = mptAlice.encryptAmount(alice, amt, bfDivergent); + break; + case Participant::Auditor: + auditorCt = mptAlice.encryptAmount(auditor, amt, bfDivergent); + break; + } + } + + TER const expectedErr = divergent ? TER{tecBAD_PROOF} : TER{tesSUCCESS}; + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = amt, + .proof = strHex(proofOpt), + .senderEncryptedAmt = senderCt, + .destEncryptedAmt = destCt, + .issuerEncryptedAmt = issuerCt, + .auditorEncryptedAmt = auditorCt, + .blindingFactor = setup.blindingFactor, + .amountCommitment = setup.amountCommitment, + .balanceCommitment = setup.balanceCommitment, + .err = expectedErr, + }); + + // Verify balances. + auto const spendingAfter = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + if (divergent) + { + BEAST_EXPECT(spendingAfter == setup.prevSpending); + } + else + { + BEAST_EXPECT(spendingAfter == setup.prevSpending - amt); + } + }; + + // This confirms the test setup is sound, if any of the divergent cases below + // fail, it is due to the C1 mismatch and not a setup bug. + submitWithDivergentC1(std::nullopt); + + // Divergent C1 for different participants should all fail with tecBAD_PROOF: + submitWithDivergentC1(Participant::Sender); + submitWithDivergentC1(Participant::Dest); + submitWithDivergentC1(Participant::Issuer); + submitWithDivergentC1(Participant::Auditor); + } + + void + testConfidentialMPTBaseFee(FeatureBitset features) + { + testcase("test confidential transactions fee"); + using namespace test::jtx; + + auto setup = + [&](MPTTester& mpt, Account const& alice, Account const& bob, Account const& carol) { + mpt.create({ + .ownerCount = 1, + .flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer | + tfMPTCanClawback, + }); + mpt.authorize({.account = bob}); + mpt.authorize({.account = carol}); + mpt.pay(alice, bob, 100); + mpt.pay(alice, carol, 50); + mpt.generateKeyPair(alice); + mpt.generateKeyPair(bob); + mpt.generateKeyPair(carol); + mpt.set({.account = alice, .issuerPubKey = mpt.getPubKey(alice)}); + }; + + // test expected base fee for confidential transactions + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + setup(mptAlice, alice, bob, carol); + + auto const baseFee = env.current()->fees().base; + auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1); + + // lambda function to submit confidential transaction and check fee charged to the + // account + auto checkFee = [&](Account const& acct, auto&& submitFn) { + auto const before = env.balance(acct); + submitFn(); + auto const after = env.balance(acct); + BEAST_EXPECT(before - after == expectedFee); + }; + + checkFee(bob, [&]() { + mptAlice.convert( + {.account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + .fee = expectedFee}); + }); + checkFee(carol, [&]() { + mptAlice.convert( + {.account = carol, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(carol), + .fee = expectedFee}); + }); + checkFee(bob, [&]() { mptAlice.mergeInbox({.account = bob, .fee = expectedFee}); }); + checkFee(carol, [&]() { mptAlice.mergeInbox({.account = carol, .fee = expectedFee}); }); + checkFee(bob, [&]() { + mptAlice.send({.account = bob, .dest = carol, .amt = 5, .fee = expectedFee}); + }); + checkFee(bob, [&]() { + mptAlice.convertBack({.account = bob, .amt = 5, .fee = expectedFee}); + }); + checkFee(alice, [&]() { + mptAlice.confidentialClaw( + {.account = alice, .holder = carol, .amt = 15, .fee = expectedFee}); + }); + } + + // test insufficient fee for confidential transactions + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + setup(mptAlice, alice, bob, carol); + auto const baseFee = env.current()->fees().base; + auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1); + + mptAlice.convert( + {.account = bob, + .amt = 1, + .holderPubKey = mptAlice.getPubKey(bob), + .fee = expectedFee - 1, + .err = telINSUF_FEE_P}); + mptAlice.mergeInbox({.account = bob, .fee = baseFee, .err = telINSUF_FEE_P}); + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 1, + .fee = baseFee * kConfidentialFeeMultiplier, + .err = telINSUF_FEE_P}); + mptAlice.convertBack({.account = bob, .amt = 1, .fee = baseFee, .err = telINSUF_FEE_P}); + mptAlice.confidentialClaw( + {.account = alice, + .holder = carol, + .amt = 1, + .fee = baseFee, + .err = telINSUF_FEE_P}); + } + + // test excessive fee for confidential transactions + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + setup(mptAlice, alice, bob, carol); + + auto const baseFee = env.current()->fees().base; + auto const highFee = baseFee * (kConfidentialFeeMultiplier + 1) * 2; + auto const bobBefore = env.balance(bob); + mptAlice.convert( + {.account = bob, + .amt = 1, + .holderPubKey = mptAlice.getPubKey(bob), + .fee = highFee}); + BEAST_EXPECT(env.balance(bob) == bobBefore - highFee); + } + } + + void + testSendForgedEqualityProof(FeatureBitset features) + { + testcase("Send: forged equality proof"); + + // Test that modifying a ciphertext after proof generation causes + // verification to fail. The Fiat-Shamir challenge binds ciphertexts + // to the proof, so any modification invalidates the proof. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10); + + // Forge destination ciphertext (Enc(20) instead of Enc(10)) + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer const forgedBlindingFactor = generateBlindingFactor(); + auto const forgedDestAmt = mptAlice.encryptAmount(carol, 20, forgedBlindingFactor); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF); + args.destEncryptedAmt = forgedDestAmt; + mptAlice.send(args); + } + + // Forge sender's ciphertext (Enc(5) instead of Enc(10)) + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer const forgedBlindingFactor = generateBlindingFactor(); + auto const forgedSenderAmt = mptAlice.encryptAmount(bob, 5, forgedBlindingFactor); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF); + args.senderEncryptedAmt = forgedSenderAmt; + mptAlice.send(args); + } + + // Forge issuer's ciphertext (Enc(100) instead of Enc(10)) + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer const forgedBlindingFactor = generateBlindingFactor(); + auto const forgedIssuerAmt = mptAlice.encryptAmount(alice, 100, forgedBlindingFactor); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF); + args.issuerEncryptedAmt = forgedIssuerAmt; + mptAlice.send(args); + } + } + + void + testSendForgedRangeProof(FeatureBitset features) + { + testcase("Send: forged range proof"); + + // Attack: send uint64_max tokens using Enc(uint64_max) ciphertexts + // and a corrupted bulletproof. Verifier rejects due to inner-product + // mismatch and Fiat-Shamir transcript divergence. Supply invariant + // is preserved. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + uint64_t const badAmount = std::numeric_limits::max(); + Buffer const blindingFactor = generateBlindingFactor(); + + // Construct Enc(uint64_max) ciphertexts and commitment. + auto const senderAmt = mptAlice.encryptAmount(bob, badAmount, blindingFactor); + auto const destAmt = mptAlice.encryptAmount(carol, badAmount, blindingFactor); + auto const issuerAmt = mptAlice.encryptAmount(alice, badAmount, blindingFactor); + auto const amountCommitment = mptAlice.getPedersenCommitment(badAmount, blindingFactor); + + // Balance commitment for Bob's actual balance. + auto const prevSpending = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing previous spending balance"); + auto const balanceBlindingFactor = generateBlindingFactor(); + auto const balanceCommitment = + mptAlice.getPedersenCommitment(prevSpending, balanceBlindingFactor); + + // Generate a valid proof for a legitimate amount, then corrupt + // the bulletproof segment to simulate a forged range proof. + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10); + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Corrupt bulletproof bytes. + Buffer forgedProof = requireOptional(validProof, "Missing valid proof"); + for (size_t i = kBulletproofOffset; i < forgedProof.size(); i += 7) + forgedProof.data()[i] ^= 0xFF; + + // Submit — rejected due to commitment mismatch. + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = badAmount, + .proof = strHex(forgedProof), + .senderEncryptedAmt = senderAmt, + .destEncryptedAmt = destAmt, + .issuerEncryptedAmt = issuerAmt, + .amountCommitment = amountCommitment, + .balanceCommitment = balanceCommitment, + .err = tecBAD_PROOF}); + + // Supply invariant: Bob's balance unchanged. + auto const postSpending = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing post spending balance"); + BEAST_EXPECT(postSpending == prevSpending); + } + + void + testSendNegativeValueMalleability(FeatureBitset features) + { + testcase("Send: negative value malleability"); + + // Attack: forge a bulletproof claiming remaining = (uint64_t)(-10). + // Bob has 10 tokens, sends 10. Honest remaining is 0, but the + // forged proof claims 0xFFFFFFFFFFFFFFF6. Rejected because + // PC(0) != PC(0xFFFFFFFFFFFFFFF6). + + using namespace test::jtx; + // Bob converts exactly 10 tokens, leaving honest remaining = 0. + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 1000, .convertAmount = 10}, + {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + uint64_t const negativeRemaining = static_cast(-10); // 0xFFFFFFFFFFFFFFF6 + + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const ctxHash = getSendContextHash( + bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version); + + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Forge bulletproof for {10, 0xFFFFFFFFFFFFFFF6} and splice it in. + auto const forgedBulletproof = getForgedBulletproof( + {sendAmount, negativeRemaining}, + {setup.amountBlindingFactor, setup.balanceBlindingFactor}, + ctxHash); + + Buffer forgedProof(requireOptionalRef(validProof, "Missing valid proof").size()); + std::memcpy( + forgedProof.data(), + requireOptionalRef(validProof, "Missing valid proof").data(), + kBulletproofOffset); + std::memcpy( + forgedProof.data() + kBulletproofOffset, + forgedBulletproof.data(), + kEcDoubleBulletproofLength); + + mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF)); + + // Supply invariant: Bob's balance unchanged. + auto const postSpending = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing post spending balance"); + BEAST_EXPECT(postSpending == setup.prevSpending); + } + + void + testSendInvalidProofContextBinding(FeatureBitset features) + { + testcase("Send proof context binding"); + using namespace test::jtx; + + auto runBadProof = [&](auto makeContextHash) { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 100, .convertAmount = 40}, {.account = carol}}}; + auto& mptAlice = confEnv.mpt; + + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10); + + auto const proof = mptAlice.getConfidentialSendProof( + bob, + setup.sendAmount, + setup.recipients, + setup.blindingFactor, + makeContextHash(env, mptAlice, alice, bob, carol, setup.version), + { + .pedersenCommitment = setup.amountCommitment, + .amt = setup.sendAmount, + .encryptedAmt = setup.senderAmt, + .blindingFactor = setup.amountBlindingFactor, + }, + { + .pedersenCommitment = setup.balanceCommitment, + .amt = setup.prevSpending, + .encryptedAmt = setup.prevEncryptedSpending, + .blindingFactor = setup.balanceBlindingFactor, + }); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.send(setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF)); + }; + + // Wrong sender account in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const& carol, + std::uint32_t version) { + return getSendContextHash( + carol.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version); + }); + + // Wrong issuance ID in the proof context. + runBadProof([&](Env& env, + MPTTester const&, + Account const& alice, + Account const& bob, + Account const& carol, + std::uint32_t version) { + return getSendContextHash( + bob.id(), + makeMptID(env.seq(alice) + 100, alice), + env.seq(bob), + carol.id(), + version); + }); + + // Wrong transaction sequence in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const& carol, + std::uint32_t version) { + return getSendContextHash( + bob.id(), mpt.issuanceID(), env.seq(bob) + 1, carol.id(), version); + }); + + // Wrong destination in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const&, + std::uint32_t version) { + return getSendContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), bob.id(), version); + }); + + // Wrong balance version in the proof context. + runBadProof([&](Env& env, + MPTTester const& mpt, + Account const&, + Account const& bob, + Account const& carol, + std::uint32_t version) { + return getSendContextHash( + bob.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version + 1); + }); + } + + void + testSendFiatShamirBinding(FeatureBitset features) + { + testcase("Send: Fiat-Shamir Binding"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10); + + // Variant A: forged amount commitment. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + auto const forgedBlindingFactor = generateBlindingFactor(); + auto const forgedCommitment = + mptAlice.getPedersenCommitment(setup.sendAmount + 5, forgedBlindingFactor); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF); + args.amountCommitment = forgedCommitment; + mptAlice.send(args); + } + + // Variant B: proof replay at a different sequence. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.pay(bob, carol, 1); + env.close(); + + mptAlice.send(setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF)); + } + + // Variant C: tampered response scalars. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + auto const& proofRef = requireOptionalRef(proof, "Missing proof"); + Buffer tamperedProof(proofRef.size()); + std::memcpy(tamperedProof.data(), proofRef.data(), proofRef.size()); + size_t const tamperOffset = tamperedProof.size() / 2; + tamperedProof.data()[tamperOffset] ^= 0xFF; + + mptAlice.send(setup.sendArgs(bob, carol, tamperedProof, tecBAD_PROOF)); + } + } + + void + testSendProofComponentReuse(FeatureBitset features) + { + testcase("Send: Proof Component Reuse"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"), dan("dan"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, + {.account = carol, .payAmount = 1000, .convertAmount = 50}, + {.account = dan, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // Variant A: replay proof to same destination after sequence changes. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof"))); + mptAlice.mergeInbox({.account = carol}); + + mptAlice.send(setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF)); + } + + // Variant B: replay proof to a different destination. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof"))); + mptAlice.mergeInbox({.account = carol}); + + auto const destAmtDan = mptAlice.encryptAmount(dan, sendAmount, setup.blindingFactor); + auto const issuerAmtDan = + mptAlice.encryptAmount(alice, sendAmount, setup.blindingFactor); + + auto args = + setup.sendArgs(bob, dan, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF); + args.destEncryptedAmt = destAmtDan; + args.issuerEncryptedAmt = issuerAmtDan; + mptAlice.send(args); + } + } + + void + testSendSpecialWitnessValues(FeatureBitset features) + { + testcase("Send: special witness values"); + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}}; + auto& mptAlice = confEnv.mpt; + + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10); + + // Variant A: zero-valued response scalars. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer forgedProof = requireOptionalRef(proof, "Missing proof"); + + static constexpr size_t kSigmaScalarSize = 32; + static constexpr size_t kChallengeOffset = 0; + static constexpr size_t kResponseOffset = kChallengeOffset + kSigmaScalarSize; + static constexpr size_t kResponseSize = 5 * kSigmaScalarSize; // z_m..z_sk + std::memset(forgedProof.data() + kResponseOffset, 0, kResponseSize); + + mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF)); + } + + // Variant B: identity element in ciphertext. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer invalidCiphertext(kEcGamalEncryptedTotalLength); + std::memset(invalidCiphertext.data(), 0, kEcGamalEncryptedTotalLength); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), temBAD_CIPHERTEXT); + args.senderEncryptedAmt = invalidCiphertext; + mptAlice.send(args); + } + + // Variant B2: identity element in commitment. + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer invalidCommitment(kEcPedersenCommitmentLength); + std::memset(invalidCommitment.data(), 0, kEcPedersenCommitmentLength); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(proof, "Missing proof"), temMALFORMED); + args.amountCommitment = invalidCommitment; + mptAlice.send(args); + } + + // Variant C: boundary scalar (curve order). + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer forgedProof = requireOptionalRef(proof, "Missing proof"); + + static constexpr unsigned char kCurveOrder[32] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, // + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, // + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41 // + }; + + std::memcpy(forgedProof.data() + 32, kCurveOrder, 32); + + mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF)); + } + + // Variant C2: overflow scalar (curve order + 1). + { + auto const proof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof.has_value())) + return; + + Buffer forgedProof = requireOptionalRef(proof, "Missing proof"); + + static constexpr unsigned char kOverflowScalar[32] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, // + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, // + 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42 // + }; + + std::memcpy(forgedProof.data() + 32, kOverflowScalar, 32); + + mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF)); + } + } + + void + testSendCrossStatementProofSubstitution(FeatureBitset features) + { + testcase("Send: cross-statement proof substitution"); + + // This test verifies that proofs generated for one protocol component + // cannot be used in place of another, and that proofs bound to + // different public parameters are rejected. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer | tfMPTCanClawback}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // Variant A: Swap proof type (cross-statement substitution) + // ----------------------------------------------------------------- + // Attack: Generate a valid convertBack proof (compact sigma + + // single bulletproof) and attempt to use it as the ZK proof in a + // ConfidentialMPTSend transaction. + // + // Expected: The send proof has a different structure + // (equality + 2×pedersen + double bulletproof). Even if sized to + // match, the domain-separated Fiat-Shamir transcript differs, + // so verification equations fail. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + // Generate a valid convertBack proof for bob + auto const spendingBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance"); + auto const encryptedSpending = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance"); + + Buffer const pcBlindingFactor = generateBlindingFactor(); + Buffer const pedersenCommitment = + mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor); + + auto const version = mptAlice.getMPTokenVersion(bob); + uint256 const convertBackCtxHash = + getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); + + Buffer const convertBackProof = mptAlice.getConvertBackProof( + bob, + sendAmount, + convertBackCtxHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = spendingBalance, + .encryptedAmt = encryptedSpending, + .blindingFactor = pcBlindingFactor, + }); + + // Resize the convertBack proof to match the expected send proof + // size so it passes preflight's size check and reaches the actual + // ZK verification in doApply. + auto const expectedSendSize = kEcSendProofLength; + Buffer resizedProof(expectedSendSize); + auto const copyLen = std::min(convertBackProof.size(), expectedSendSize); + std::memcpy(resizedProof.data(), convertBackProof.data(), copyLen); + // Zero-pad the rest (if convertBack proof is shorter) + if (copyLen < expectedSendSize) + std::memset(resizedProof.data() + copyLen, 0, expectedSendSize - copyLen); + + mptAlice.send(setup.sendArgs(bob, carol, resizedProof, tecBAD_PROOF)); + } + + // Variant B: Valid proof bound to wrong public parameters + // ----------------------------------------------------------------- + // Attack: Generate a valid send proof using a wrong context hash + // (computed with a different issuanceID). The proof is + // mathematically valid for the wrong statement, but when the + // verifier recomputes the Fiat-Shamir challenge using the correct + // issuanceID, the challenge differs and verification fails. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + // Compute context hash with a fabricated (wrong) issuanceID + uint192 const fakeIssuanceID{1}; + auto const wrongCtxHash = getSendContextHash( + bob.id(), fakeIssuanceID, env.seq(bob), carol.id(), setup.version); + + // Generate a proof that is valid for the wrong issuanceID + auto const wrongProof = mptAlice.getConfidentialSendProof( + bob, + sendAmount, + setup.recipients, + setup.blindingFactor, + wrongCtxHash, + { + .pedersenCommitment = setup.amountCommitment, + .amt = sendAmount, + .encryptedAmt = setup.senderAmt, + .blindingFactor = setup.amountBlindingFactor, + }, + { + .pedersenCommitment = setup.balanceCommitment, + .amt = setup.prevSpending, + .encryptedAmt = setup.prevEncryptedSpending, + .blindingFactor = setup.balanceBlindingFactor, + }); + + if (!BEAST_EXPECT(wrongProof.has_value())) + return; + + // Submit with the correct issuanceID — verifier recomputes + // the challenge using the real issuanceID, which differs from + // the one baked into the proof. + mptAlice.send(setup.sendArgs( + bob, carol, requireOptionalRef(wrongProof, "Missing wrong proof"), tecBAD_PROOF)); + } + } + + void + testSendCiphertextMalleability(FeatureBitset features) + { + testcase("Send: ciphertext malleability"); + + // Attack: replace ElGamal ciphertext Enc(m) with Enc(2m) to inflate + // the amount credited to the recipient. ElGamal is homomorphic, so + // scalar multiplication (C1, C2) → (k*C1, k*C2) decrypts to k*m. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // Variant A: Post-signature tampering. + // Build a valid signed transaction, then replace the destination + // ciphertext with Enc(2m) in the serialized blob. The original + // signature no longer covers the modified data. + { + auto const seq = env.seq(bob); + auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq); + auto jtx = env.jt(jv); + BEAST_EXPECT(jtx.stx); + + // Serialize signed tx, deserialize into mutable STObject + Serializer s; + jtx.stx->add(s); + SerialIter sit(s.slice()); + STObject obj(sit, sfTransaction); + + // Replace dest ciphertext with Enc(2m) — a valid EC point + // encrypting an inflated amount under carol's key + Buffer const bf = generateBlindingFactor(); + auto const inflatedCiphertext = mptAlice.encryptAmount(carol, sendAmount * 2, bf); + obj.setFieldVL(sfDestinationEncryptedAmount, inflatedCiphertext); + + // Re-serialize with the original (now-stale) signature + Serializer tampered; + obj.add(tampered); + + // Signature verification fails — rejected before ZKP check + auto const jr = env.rpc("submit", strHex(tampered.slice())); + BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction"); + } + + // Variant B: Re-signed with inflated ciphertext. + // Generate a valid proof for amount m, then replace the destination + // ciphertext with Enc(2m) and re-sign. Signature passes, but the + // compact sigma proof fails: the proof binds Enc(m) to the Pedersen + // commitment PC(m, r), so substituting Enc(2m) breaks the linkage. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const ctxHash = getSendContextHash( + bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version); + + auto const validProof = mptAlice.getConfidentialSendProof( + bob, + sendAmount, + setup.recipients, + setup.blindingFactor, + ctxHash, + { + .pedersenCommitment = setup.amountCommitment, + .amt = sendAmount, + .encryptedAmt = setup.senderAmt, + .blindingFactor = setup.amountBlindingFactor, + }, + { + .pedersenCommitment = setup.balanceCommitment, + .amt = setup.prevSpending, + .encryptedAmt = setup.prevEncryptedSpending, + .blindingFactor = setup.balanceBlindingFactor, + }); + + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Replace dest ciphertext with Enc(2m) using the same blinding + // factor — even with matching randomness the proof rejects + // because the committed plaintext differs + auto const inflatedDestAmt = + mptAlice.encryptAmount(carol, sendAmount * 2, setup.blindingFactor); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF); + args.destEncryptedAmt = inflatedDestAmt; + mptAlice.send(args); + } + } + + void + testSendCiphertextNegation(FeatureBitset features) + { + testcase("Send: ciphertext negation"); + + // Attack: negate ciphertext -Enc(m) = (-C1, -C2) to reverse the + // transaction direction. Negation decrypts to the group-level + // additive inverse of m*G, effectively turning a credit into a debit. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // Negate an ElGamal ciphertext by flipping the y-coordinate parity + // of both compressed EC points. For secp256k1 compressed form, + // prefix 0x02 means even-y and 0x03 means odd-y; negation + // swaps them: -P has the same x but opposite y. + auto negateCiphertext = [](Buffer const& ct) -> Buffer { + Buffer neg = ct; + neg.data()[0] ^= 0x01; // negate C1 + neg.data()[kEcCiphertextComponentLength] ^= 0x01; // negate C2 + return neg; + }; + + // Variant A: Post-signature negation. + // Negate the destination ciphertext in the signed blob. + // Signature no longer covers the modified field. + { + auto const seq = env.seq(bob); + auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq); + auto jtx = env.jt(jv); + BEAST_EXPECT(jtx.stx); + + Serializer s; + jtx.stx->add(s); + + SerialIter sit(s.slice()); + STObject obj(sit, sfTransaction); + + auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount); + Buffer const origBuf(origDestAmt.data(), origDestAmt.size()); + auto const negDestAmt = negateCiphertext(origBuf); + obj.setFieldVL( + sfDestinationEncryptedAmount, Slice(negDestAmt.data(), negDestAmt.size())); + + Serializer tampered; + obj.add(tampered); + + auto const jr = env.rpc("submit", strHex(tampered.slice())); + BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction"); + } + + // Variant B: Re-signed with all negated ciphertexts. + // Signature passes, but the compact sigma proof fails — the proof + // was generated for Enc(m), not Enc(-m). + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Negate all three ciphertexts: Enc(m) -> Enc(-m) + auto const negSenderAmt = negateCiphertext(setup.senderAmt); + auto const negDestAmt = negateCiphertext(setup.destAmt); + auto const negIssuerAmt = negateCiphertext(setup.issuerAmt); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF); + args.senderEncryptedAmt = negSenderAmt; + args.destEncryptedAmt = negDestAmt; + args.issuerEncryptedAmt = negIssuerAmt; + mptAlice.send(args); + } + + // Variant C: Negate only the sender ciphertext. + // The verifier uses the sender ciphertext to derive the remainder + // commitment: Enc(b) - Enc(m) becomes Enc(b) - (-Enc(m)) = Enc(b+m). + // The bulletproof was generated for (b - m), not (b + m), so the + // aggregated range proof fails. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + auto const negSenderAmt = negateCiphertext(setup.senderAmt); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF); + args.senderEncryptedAmt = negSenderAmt; + mptAlice.send(args); + } + } + + void + testSendCiphertextCombination(FeatureBitset features) + { + testcase("Send: ciphertext combination"); + + // Attack: exploit ElGamal homomorphism to combine ciphertexts + // Enc(m1) + Enc(m2) = Enc(m1+m2), inflating the credited amount + // without knowing the private keys. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob, .payAmount = 1000, .convertAmount = 200}, + {.account = carol, .payAmount = 1000, .convertAmount = 100}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + uint64_t const m1 = 10; + uint64_t const m2 = 5; + + // Variant A: Post-signature combination. + // Add Enc(m2) to the signed destination ciphertext Enc(m1). + // The original signature doesn't cover the combined ciphertext. + { + auto const seq = env.seq(bob); + auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = m1}, seq); + auto jtx = env.jt(jv); + BEAST_EXPECT(jtx.stx); + + Serializer s; + jtx.stx->add(s); + + SerialIter sit(s.slice()); + STObject obj(sit, sfTransaction); + + auto const origDestCt = obj.getFieldVL(sfDestinationEncryptedAmount); + + // Homomorphically add Enc(m2) to the original Enc(m1) + Buffer const bf2 = generateBlindingFactor(); + auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2); + auto const combined = requireOptional( + homomorphicAdd( + Slice(origDestCt.data(), origDestCt.size()), Slice(encM2.data(), encM2.size())), + "Missing combined ciphertext"); + + obj.setFieldVL(sfDestinationEncryptedAmount, combined); + + Serializer tampered; + obj.add(tampered); + + auto const jr = env.rpc("submit", strHex(tampered.slice())); + BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction"); + } + + // Variant B: Re-signed with combined ciphertext. + // Generate a valid proof for m1, then replace dest ciphertext with + // Enc(m1) + Enc(m2). Sigma proof fails because the proof was + // generated for Enc(m1) only — the combined ciphertext has + // different randomness. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, m1); + + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Homomorphically add Enc(m2) to the valid dest ciphertext + Buffer const bf2 = generateBlindingFactor(); + auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2); + auto const combinedDest = homomorphicAdd(setup.destAmt, encM2); + BEAST_EXPECT(combinedDest.has_value()); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF); + args.destEncryptedAmt = combinedDest; + mptAlice.send(args); + } + + // Variant C: Cross-transaction ciphertext reuse. + // Execute a valid send of m1, then build a new send for m2 using + // a combined ciphertext oldEnc(m1) + newEnc(m2) = Enc(m1+m2), + // where oldEnc(m1) is the actual ciphertext from the previous tx. + // The proof was generated for the new transaction's context, but + // the ciphertext includes stale randomness from the old Enc(m1). + { + // Execute a valid send of m1, capturing the actual ciphertext used + ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, m1); + auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof1.has_value())) + return; + mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof"))); + + ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, m2); + + auto const proof2 = setup2.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof2.has_value())) + return; + + // Combine the actual prior-tx Enc(m1) with the new Enc(m2) + auto const crossCombined = homomorphicAdd(setup1.destAmt, setup2.destAmt); + BEAST_EXPECT(crossCombined.has_value()); + + auto args = setup2.sendArgs( + bob, carol, requireOptionalRef(proof2, "Missing proof"), tecBAD_PROOF); + args.destEncryptedAmt = crossCombined; + mptAlice.send(args); + } + } + + void + testSendCiphertextRerandomization(FeatureBitset features) + { + testcase("Send: ciphertext rerandomization"); + + // Attack: substitute the randomness component C1 of an ElGamal + // ciphertext (C1, C2) while keeping the message component C2 + // unchanged. This "rerandomizes" the ciphertext to break + // linkability or forge fresh-looking ciphertexts. + // + // The compact sigma proof binds C1 to the shared randomness used + // across all recipients, so any C1 substitution breaks the proof. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // Helper: replace C1 in a ciphertext with C1 from another + // ciphertext, keeping C2 unchanged. Returns a rerandomized + // ciphertext (C1', C2). + auto substituteC1 = [](Buffer const& target, Buffer const& source) -> Buffer { + Buffer result = target; + // Copy C1 (the first ciphertext component) from source. + std::memcpy(result.data(), source.data(), kEcCiphertextComponentLength); + return result; + }; + + // Variant A: Post-signature C1 substitution. + // Replace C1 in the dest ciphertext after signing. + // Signature no longer covers the modified ciphertext. + { + auto const seq = env.seq(bob); + auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq); + auto jtx = env.jt(jv); + BEAST_EXPECT(jtx.stx); + + Serializer s; + jtx.stx->add(s); + SerialIter sit(s.slice()); + STObject obj(sit, sfTransaction); + + // Generate a random C1' by encrypting a different amount + Buffer const bf2 = generateBlindingFactor(); + auto const otherCt = mptAlice.encryptAmount(carol, 99, bf2); + + // Replace C1 in the dest ciphertext + auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount); + Buffer const origBuf(origDestAmt.data(), origDestAmt.size()); + auto const rerandomized = substituteC1(origBuf, otherCt); + obj.setFieldVL( + sfDestinationEncryptedAmount, Slice(rerandomized.data(), rerandomized.size())); + + Serializer tampered; + obj.add(tampered); + + // Signature verification fails + auto const jr = env.rpc("submit", strHex(tampered.slice())); + BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction"); + } + + // Variant B: Re-signed C1 substitution. + // Replace C1 in the dest ciphertext with a fresh random point + // and re-sign. Sigma proof fails because the shared-randomness + // binding no longer holds — C1' wasn't generated with the same r + // used in the proof. + { + ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); + + auto const validProof = setup.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(validProof.has_value())) + return; + + // Create a ciphertext with different randomness to get C1' + Buffer const bf2 = generateBlindingFactor(); + auto const otherCt = mptAlice.encryptAmount(carol, sendAmount, bf2); + + // Replace C1 in dest ciphertext, keep C2 + auto const rerandomizedDest = substituteC1(setup.destAmt, otherCt); + + auto args = setup.sendArgs( + bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF); + args.destEncryptedAmt = rerandomizedDest; + mptAlice.send(args); + } + } + + void + testSendZeroRandomnessCiphertext(FeatureBitset features) + { + testcase("Send: zero randomness ciphertext"); + + // Setting r = 0 in ElGamal yields C1 = O (identity), C2 = mG — + // a deterministic ciphertext that reveals the plaintext. + + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + ConfidentialEnv confEnv{ + env, + alice, + {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}, + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}; + auto& mptAlice = confEnv.mpt; + + uint64_t const sendAmount = 10; + + // ----------------------------------------------------------------- + // Variant A: Post-signature zero-randomness substitution + // ----------------------------------------------------------------- + // Construct a valid ConfidentialMPTSend transaction with proper + // ciphertexts and ZKPs, sign it, then replace the sender ciphertext + // with a deterministic form (C1 = 0x00...00, C2 = arbitrary). + // Since the identity element has no valid compressed encoding, + // the modified blob fails deserialization / signature check. + { + auto const seq = env.seq(bob); + auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq); + auto jtx = env.jt(jv); + BEAST_EXPECT(jtx.stx); + + // Serialize the signed transaction + Serializer s; + jtx.stx->add(s); + SerialIter sit(s.slice()); + STObject obj(sit, sfTransaction); + + // Replace sender ciphertext with zero-randomness form: + // C1 = all zeros (identity element — invalid encoding) + // C2 = valid trivial point (simulating mG) + Buffer zeroCiphertext(kEcGamalEncryptedTotalLength); + std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength); + // C2 half: use a valid point so only C1 is the problem + auto const& tc = getTrivialCiphertext(); + std::memcpy( + zeroCiphertext.data() + kEcCiphertextComponentLength, + tc.data() + kEcCiphertextComponentLength, + kEcCiphertextComponentLength); + obj.setFieldVL(sfSenderEncryptedAmount, zeroCiphertext); + + // Re-serialize with the original (now-stale) signature + Serializer tampered; + obj.add(tampered); + + // Signature verification fails because ciphertext fields are + // signed — transaction rejected before ZKP verification. + auto const jr = env.rpc("submit", strHex(tampered.slice())); + BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction"); + } + + // ----------------------------------------------------------------- + // Variant B: Re-signed zero-randomness ciphertext + // ----------------------------------------------------------------- + // Same zero-randomness ciphertext as Variant A (C1 = 0, C2 = mG), + // but submitted normally via send() which re-signs the transaction. + // Signature verification passes, but preflight's isValidCiphertext + // rejects it: the identity element has no valid compressed encoding + // on secp256k1, so secp256k1_ec_pubkey_parse fails on C1 = 0. + { + // Build zero-randomness ciphertext: C1 = all zeros (identity), + // C2 = valid trivial point (simulating mG) + Buffer zeroCiphertext(kEcGamalEncryptedTotalLength); + std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength); + auto const& tc = getTrivialCiphertext(); + std::memcpy( + zeroCiphertext.data() + kEcCiphertextComponentLength, + tc.data() + kEcCiphertextComponentLength, + kEcCiphertextComponentLength); + + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = sendAmount, + .senderEncryptedAmt = zeroCiphertext, + .err = temBAD_CIPHERTEXT}); + } + + // ----------------------------------------------------------------- + // Variant C: Deterministic ciphertext reuse across transactions + // ----------------------------------------------------------------- + // Construct two transactions using identical deterministic + // ciphertexts (same fixed blinding factor). Even if a valid + // proof could be generated for one, it cannot be reused because + // the TransactionContextID (which includes account sequence) + // differs between transactions. + { + // First transaction: generate valid proof for sendAmount + ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, sendAmount); + + auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol); + if (!BEAST_EXPECT(proof1.has_value())) + return; + + // Submit first transaction successfully + mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof"))); + + mptAlice.mergeInbox({.account = carol}); + + // Second transaction: reuse the same proof from tx1. + // The context hash includes the new account sequence, so the + // proof generated for the old sequence is invalid. + ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, sendAmount); + + mptAlice.send(setup2.sendArgs( + bob, carol, requireOptionalRef(proof1, "Missing proof"), tecBAD_PROOF)); + } + } + + void + testSendRerandomizesRecipientInboxAgainstMergeCancellation(FeatureBitset features) + { + testcase("Send: recipient inbox rerandomization prevents merge cancellation"); + + using namespace test::jtx; + + // Derive the deterministic canonical-zero randomness r0 used for + // Bob's first spending balance. + auto getCanonicalZeroBlindingFactor = [](AccountID const& account, MPTID const& mptID) { + Buffer scalar(kEcBlindingFactorLength); + std::array hashInput{}; + std::memcpy(hashInput.data(), "EncZero", 7); + std::memcpy(hashInput.data() + 7, account.data(), account.size()); + std::memcpy(hashInput.data() + 27, mptID.data(), mptID.size()); + + for (;;) + { + unsigned int mdLen = kEcBlindingFactorLength; + if (EVP_Digest( + hashInput.data(), + hashInput.size(), + scalar.data(), + &mdLen, + EVP_sha256(), + nullptr) != 1) + { + Throw("Failed to derive canonical zero blinding factor"); + } + + if (secp256k1_ec_seckey_verify(mpt_secp256k1_context(), scalar.data())) + return scalar; + + std::memcpy(hashInput.data(), scalar.data(), scalar.size()); + } + }; + + // Pick randomness that would cancel Bob's MergeInbox C1 to infinity + // without receiver-side re-randomization. + auto negateScalarSum = [](Buffer const& lhs, Buffer const& rhs) { + Buffer sum(kEcBlindingFactorLength); + Buffer negated(kEcBlindingFactorLength); + secp256k1_mpt_scalar_add(sum.data(), lhs.data(), rhs.data()); + secp256k1_mpt_scalar_negate(negated.data(), sum.data()); + return negated; + }; + + // Without an auditor, target Bob's holder inbox. The crafted send + // randomness would make MergeInbox hit the point at infinity unless + // ConfidentialMPTSend re-randomizes the recipient ciphertext. + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + Buffer const convertBlindingFactor = generateBlindingFactor(); + mptAlice.convert({ + .account = bob, + .amt = 20, + .holderPubKey = mptAlice.getPubKey(bob), + .blindingFactor = convertBlindingFactor, + }); + + Buffer const canonicalZeroBlindingFactor = + getCanonicalZeroBlindingFactor(bob.id(), mptAlice.issuanceID()); + + // Holder inbox cancellation happens later in MergeInbox, when + // Bob's spending Enc(0; r0) is added to inbox Enc(25; -r0). + Buffer const maliciousSendBlindingFactor = + negateScalarSum(canonicalZeroBlindingFactor, convertBlindingFactor); + + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 5, + .blindingFactor = maliciousSendBlindingFactor, + }); + + mptAlice.mergeInbox({.account = bob}); + + auto const bobSpending = + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending); + BEAST_EXPECT(bobSpending && *bobSpending == 25); + } + + // With an auditor, verify the destination auditor balance is also + // re-randomized. Auditor balance is updated during send, and this crafted + // randomness would otherwise make that homomorphic sum hit infinity without + // re-randomization. + { + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"), auditor("auditor"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(auditor); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + Buffer const convertBlindingFactor = generateBlindingFactor(); + mptAlice.convert({ + .account = bob, + .amt = 20, + .holderPubKey = mptAlice.getPubKey(bob), + .blindingFactor = convertBlindingFactor, + }); + + // This would make the homomorphic sum hit infinity. + Buffer const maliciousSendBlindingFactor = + negateScalarSum(gMakeZeroBuffer(kEcBlindingFactorLength), convertBlindingFactor); + + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 5, + .blindingFactor = maliciousSendBlindingFactor, + }); + + auto const bobAuditor = + mptAlice.getDecryptedBalance(bob, MPTTester::auditorEncryptedBalance); + BEAST_EXPECT(bobAuditor && *bobAuditor == 25); + } + } + + void + testWithFeats(FeatureBitset features) + { + // ConfidentialMPTConvert + testConvert(features); + testConvertPreflight(features); + testConvertInvalidProofContextBinding(features); + testConvertPreclaim(features); + testConvertWithAuditor(features); + + // ConfidentialMPTMergeInbox + testMergeInbox(features); + testMergeInboxPreflight(features); + testMergeInboxPreclaim(features); + + testSet(features); + testSetPreflight(features); + testSetPreclaim(features); + + // ConfidentialMPTSend + testSend(features); + testSendPreflight(features); + testSendPreclaim(features); + testSendRangeProof(features); + + testSendZeroAmount(features); + testSendWithAuditor(features); + + // ConfidentialMPTClawback + testClawback(features); + testClawbackPreflight(features); + testClawbackPreclaim(features); + testClawbackProof(features); + testClawbackWithAuditor(features); + testClawbackInvalidProofContextBinding(features); + + testDelete(features); + + // ConfidentialMPTConvertBack + testConvertBack(features); + testConvertBackPreflight(features); + testConvertBackPreclaim(features); + testConvertBackWithAuditor(features); + testConvertBackPedersenProof(features); + testConvertBackBulletproof(features); + + // Homomorphic operation tests + testSendHomomorphicOverflow(features); + testConvertBackHomomorphicCiphertextModification(features); + testConvertBackHomomorphicUnderflow(features); + + // Invalid curve points + testSendInvalidCurvePoints(features); + testSendWrongGroupPointInjection(features); + testConvertIdentityElementRejection(features); + testSendWrongIssuerPublicKey(features); + + // public and private txns + testPublicTransfersAfterClearingConfidentialFlag(features); + + // Replay tests + testMutatePrivacy(features); + testConvertBackInvalidProofContextBinding(features); + testConvertBackProofCiphertextBinding(features); + testConvertBackProofVersionMismatch(features); + + // Crafted-proof Tests + testSendSharedRandomnessViolation(features); + + // Transaction Fee Tests + testConfidentialMPTBaseFee(features); + + // TransferFee (transfer rate) Tests + testTransferFee(features); + + // Zero knowledge proof tests + testSendInvalidProofContextBinding(features); + testSendForgedEqualityProof(features); + testSendForgedRangeProof(features); + testSendNegativeValueMalleability(features); + testSendFiatShamirBinding(features); + testSendProofComponentReuse(features); + testSendSpecialWitnessValues(features); + testSendCrossStatementProofSubstitution(features); + + // Ciphertext malleability tests + testSendCiphertextMalleability(features); + testSendCiphertextNegation(features); + testSendCiphertextCombination(features); + testSendCiphertextRerandomization(features); + testSendZeroRandomnessCiphertext(features); + testSendRerandomizesRecipientInboxAgainstMergeCancellation(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testableAmendments()}; + + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, xrpl); + +} // namespace xrpl diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index f68f813853..02ee0750d5 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2747,7 +2747,9 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 51; + // Includes the five confidential MPT transaction types, which are + // explicitly marked Delegable in transactions.macro. + std::size_t const expectedDelegableCount = 56; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 6aa17c7840..9fde52ecb2 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -5176,6 +5176,254 @@ class Invariants_test : public beast::unit_test::Suite } } + void + testConfidentialMPTTransfer() + { + using namespace test::jtx; + testcase << "ValidConfidentialMPToken"; + + MPTID mptID; + + // Generate an MPT with privacy, issue 100 tokens to A2. + // Perform a confidential conversion to populate encrypted state. + auto const precloseConfidential = + [&mptID](Account const& a1, Account const& a2, Env& env) -> bool { + MPTTester mpt(env, a1, {.holders = {a2}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptID = mpt.issuanceID(); + + mpt.authorize({.account = a2}); + mpt.pay(a1, a2, 100); + + mpt.generateKeyPair(a1); + mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)}); + + mpt.generateKeyPair(a2); + mpt.convert({ + .account = a2, + .amt = 100, + .holderPubKey = mpt.getPubKey(a2), + }); + return true; + }; + + // badDelete + doInvariantCheck( + {"MPToken deleted with encrypted fields while COA > 0"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + // Force an erase of the object while the COA remains 100 + ac.view().erase(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + precloseConfidential); + + // badConsistency + doInvariantCheck( + {"MPToken encrypted field existence inconsistency"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + // Remove one of the required encrypted fields to create a mismatch + sleToken->makeFieldAbsent(sfIssuerEncryptedBalance); + ac.view().update(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + doInvariantCheck( + {"MPToken encrypted field existence inconsistency"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + sleToken->makeFieldAbsent(sfIssuerEncryptedBalance); + sleToken->makeFieldAbsent(sfConfidentialBalanceInbox); + sleToken->makeFieldAbsent(sfConfidentialBalanceSpending); + sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00}); + ac.view().update(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // requiresPrivacyFlag + auto const precloseNoPrivacy = [&mptID]( + Account const& a1, Account const& a2, Env& env) -> bool { + MPTTester mpt(env, a1, {.holders = {a2}, .fund = false}); + // completely omitted the tfMPTCanHoldConfidentialBalance flag here. + mpt.create({.flags = tfMPTCanTransfer}); + mptID = mpt.issuanceID(); + mpt.authorize({.account = a2}); + mpt.pay(a1, a2, 100); + return true; + }; + + doInvariantCheck( + {"MPToken has encrypted fields but Issuance does not have " + "lsfMPTCanHoldConfidentialBalance " + "set"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + // Inject all three encrypted fields consistently (inbox+spending+issuer must be + // in sync or badConsistency fires first and masks requiresPrivacyFlag). + sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00}); + sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00}); + sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00}); + ac.view().update(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseNoPrivacy); + + // badCOA + doInvariantCheck( + {"Confidential outstanding amount exceeds total outstanding amount"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID)); + if (!sleIssuance) + return false; + // Total outstanding is natively 100; bloat the COA over 100 + sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // Conservation Violation + doInvariantCheck( + {"Token conservation violation for MPT"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID)); + if (!sleIssuance) + return false; + + sleIssuance->setFieldU64( + sfConfidentialOutstandingAmount, + sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10); + ac.view().update(sleIssuance); + + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0) + doInvariantCheck( + {"Invariant failed: OutstandingAmount changed " + "by confidential transaction that should not " + "modify it for MPT"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID)); + if (!sleIssuance) + return false; + sleIssuance->setFieldU64( + sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1); + ac.view().update(sleIssuance); + return true; + }, + XRPAmount{}, + STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // Send/MergeInbox and zero-COA-delta confidential transactions must not + // change public holder MPTAmount. + doInvariantCheck( + {"Invariant failed: MPTAmount changed by confidential " + "transaction that should not modify this field."}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1); + ac.view().update(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // badVersion + doInvariantCheck( + {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending " + "changed"}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + Blob const kChangedConfidentialSpending = {0xBA, 0xDD}; + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending); + + // DO NOT update sfConfidentialBalanceVersion + ac.view().update(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseConfidential); + + // Skipping Deleted MPTs (Issuance deleted) + auto const precloseOrphan = [&mptID]( + Account const& a1, Account const& a2, Env& env) -> bool { + MPTTester mpt(env, a1, {.holders = {a2}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptID = mpt.issuanceID(); + mpt.authorize({.account = a2}); + + // Generate privacy keys and convert 0 amount so Bob has the encrypted fields + mpt.generateKeyPair(a1); + mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)}); + mpt.generateKeyPair(a2); + mpt.convert({ + .account = a2, + .amt = 0, + .holderPubKey = mpt.getPubKey(a2), + }); + + // Immediately destroy the issuance. A2's empty, encrypted token object lives on. + mpt.destroy(); + return true; + }; + + doInvariantCheck( + {}, + [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) { + auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id())); + if (!sleToken) + return false; + // Safely able to erase the deleted token. + ac.view().erase(sleToken); + return true; + }, + XRPAmount{}, + STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}}, + {tesSUCCESS, tesSUCCESS}, + precloseOrphan); + } + public: void run() override @@ -5204,6 +5452,7 @@ public: testValidPseudoAccounts(); testValidLoanBroker(); testVault(); + testConfidentialMPTTransfer(); testMPT(); testInvariantOverwrite(defaultAmendments()); testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 323184aa36..15e17b4536 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -617,7 +617,8 @@ class MPToken_test : public beast::unit_test::Suite // (2) mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG}); - if (!features[featureSingleAssetVault] && !features[featureDynamicMPT]) + if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] && + !features[featureConfidentialTransfer]) { // test invalid flags - nothing is being changed mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION}); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 6fefcfc404..dd28c7ec6e 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -5902,6 +5903,46 @@ class Vault_test : public beast::unit_test::Suite runTest(amendments); } + void + testRemoveEmptyHoldingConfidentialBalances() + { + testcase("removeEmptyHolding keeps MPToken with confidential balances"); + using namespace test::jtx; + + Env env{*this, testableAmendments()}; + + Account const issuer{"issuer"}; + Account const holder{"holder"}; + MPTTester mpt{env, issuer, {.holders = {holder}}}; + mpt.create({.authorize = MPTCreate::allHolders}); + + auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id()); + auto const encryptedBalanceFields = { + &sfConfidentialBalanceInbox, + &sfConfidentialBalanceSpending, + &sfIssuerEncryptedBalance, + &sfAuditorEncryptedBalance}; + + env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { + for (auto const field : encryptedBalanceFields) + { + Sandbox sb(&view, TapNone); + auto const token = sb.peek(tokenKeylet); + if (!BEAST_EXPECT(token)) + return false; + + token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength)); + sb.update(token); + + BEAST_EXPECT( + removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) == + tecHAS_OBLIGATIONS); + BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr); + } + return true; + }); + } + // ----------------------------------------------------------------------- // Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 + // fixCleanup3_2_0). The vault-level withdraw behavior is tested here; @@ -8073,6 +8114,7 @@ public: testAssetsMaximum(); testBug6LimitBypassWithShares(); testRemoveEmptyHoldingLockedAmount(); + testRemoveEmptyHoldingConfidentialBalances(); testWithdrawSoleShareholderFixedAssetExit(all_ - fixCleanup3_2_0); testWithdrawSoleShareholderFixedAssetExit(all_); diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h new file mode 100644 index 0000000000..b758683da6 --- /dev/null +++ b/src/test/jtx/ConfidentialTransfer.h @@ -0,0 +1,496 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +class ConfidentialTransferTestBase : public beast::unit_test::Suite +{ +protected: + template + static T + requireOptional(std::optional value, char const* message) + { + if (!value) + Throw(message); + return std::move(*value); + } + + template + static T const& + requireOptionalRef(std::optional const& value, char const* message) + { + if (!value) + Throw(message); + return *value; + } + + // Offset where the bulletproof begins in a send proof blob. + // Proof layout: [compact_sigma | bulletproof] + static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength; + + // Generate a forged aggregated bulletproof (double bulletproof) for + // the given values and blinding factors. Used to test that splicing + // a bulletproof claiming a different remaining balance is rejected. + // secp256k1 convention: returns 1 on success, 0 on failure. + static Buffer + getForgedBulletproof( + std::array const& values, + std::array const& blindingFactors, + uint256 const& contextHash) + { + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + Buffer proof(kEcDoubleBulletproofLength); + size_t proofLen = kEcDoubleBulletproofLength; + + unsigned char blindings[64]; + std::memcpy(blindings, blindingFactors[0].data(), 32); + std::memcpy(blindings + 32, blindingFactors[1].data(), 32); + + if (secp256k1_bulletproof_prove_agg( + ctx, + proof.data(), + &proofLen, + values.data(), + blindings, + 2, + &h, + contextHash.data()) == 0) + Throw("Failed to generate forged bulletproof"); + + return proof; + } + + // Get a bad ciphertext with valid structure but cryptographic invalid for + // testing purposes. For preflight test purposes. + static Buffer const& + getBadCiphertext() + { + static Buffer const kBadCiphertext = []() { + Buffer buf(kEcGamalEncryptedTotalLength); + std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + return buf; + }(); + + return kBadCiphertext; + } + + // Get a trivial buffer that is structurally and mathematically valid, but + // contains invalid data that does not match the ledger state. For preclaim + // test purposes. + static Buffer const& + getTrivialCiphertext() + { + static Buffer const kTrivialCiphertext = []() { + Buffer buf(kEcGamalEncryptedTotalLength); + std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + + buf.data()[kEcCiphertextComponentLength - 1] = 0x01; + buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01; + + return buf; + }(); + + return kTrivialCiphertext; + } + + // Returns a valid compressed EC point (33 bytes) that can pass preflight + // validation but contains invalid data for preclaim test purposes. + static Buffer const& + getTrivialCommitment() + { + static Buffer const kTrivialCommitment = []() { + Buffer buf(kEcPedersenCommitmentLength); + std::memset(buf.data(), 0, kEcPedersenCommitmentLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + // Set last byte to make it a valid x-coordinate on the curve + buf.data()[kEcPedersenCommitmentLength - 1] = 0x01; + + return buf; + }(); + + return kTrivialCommitment; + } + + static std::string + getTrivialSendProofHex() + { + Buffer buf(kEcSendProofLength); + std::memset(buf.data(), 0, kEcSendProofLength); + + for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength) + { + buf.data()[i] = kEcCompressedPrefixEvenY; + if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength) + buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01; + } + + return strHex(buf); + } + + // Helper struct to encapsulate common setup for integration tests. + struct ConfidentialSendSetup + { + // Constants + uint64_t sendAmount; + size_t nRecipients; + uint32_t version; + + // Blinding factors + Buffer blindingFactor; + Buffer amountBlindingFactor; + Buffer balanceBlindingFactor; + + // Encrypted amounts + Buffer senderAmt; + Buffer destAmt; + Buffer issuerAmt; + std::optional auditorAmt; + + // Commitments + Buffer amountCommitment; + + // Long-lived pub key buffers (to avoid dangling Slice) + Buffer senderPubKey; + Buffer destPubKey; + Buffer issuerPubKey; + std::optional auditorPubKey; + + // Balance data + uint64_t prevSpending; + Buffer prevEncryptedSpending; + + // Balance commitment (declared after prevSpending for init order) + Buffer balanceCommitment; + + // Recipients vector + std::vector recipients; + + // Constructor that performs all common setup + ConfidentialSendSetup( + test::jtx::MPTTester& mpt, + test::jtx::Account const& sender, + test::jtx::Account const& dest, + test::jtx::Account const& issuer, + uint64_t amount, + std::optional> auditor = std::nullopt) + : sendAmount(amount) + , nRecipients(auditor ? 4 : 3) + , version(mpt.getMPTokenVersion(sender)) + , blindingFactor(generateBlindingFactor()) + , amountBlindingFactor(blindingFactor) + , balanceBlindingFactor(generateBlindingFactor()) + , senderAmt(mpt.encryptAmount(sender, amount, blindingFactor)) + , destAmt(mpt.encryptAmount(dest, amount, blindingFactor)) + , issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor)) + , auditorAmt( + auditor ? std::optional( + mpt.encryptAmount(auditor->get(), amount, blindingFactor)) + : std::nullopt) + , amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor)) + , senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key")) + , destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key")) + , issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key")) + , auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt) + , prevSpending(requireOptional( + mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), + "Missing sender spending balance")) + , prevEncryptedSpending(requireOptional( + mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), + "Missing sender encrypted spending balance")) + , balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor)) + { + recipients.push_back({ + .publicKey = Slice(senderPubKey), + .encryptedAmount = senderAmt, + }); + recipients.push_back({ + .publicKey = Slice(destPubKey), + .encryptedAmount = destAmt, + }); + recipients.push_back({ + .publicKey = Slice(issuerPubKey), + .encryptedAmount = issuerAmt, + }); + if (auditor) + { + recipients.push_back({ + .publicKey = + Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")), + .encryptedAmount = + requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"), + }); + } + } + + // Generate proof with current account sequence + std::optional + generateProof( + test::jtx::MPTTester& mpt, + test::jtx::Env& env, + test::jtx::Account const& sender, + test::jtx::Account const& dest) const + { + auto const ctxHash = getSendContextHash( + sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version); + + return mpt.getConfidentialSendProof( + sender, + sendAmount, + recipients, + blindingFactor, + ctxHash, + { + .pedersenCommitment = amountCommitment, + .amt = sendAmount, + .encryptedAmt = senderAmt, + .blindingFactor = amountBlindingFactor, + }, + { + .pedersenCommitment = balanceCommitment, + .amt = prevSpending, + .encryptedAmt = prevEncryptedSpending, + .blindingFactor = balanceBlindingFactor, + }); + } + + [[nodiscard]] test::jtx::MPTConfidentialSend + sendArgs( + test::jtx::Account const& sender, + test::jtx::Account const& dest, + Buffer const& proof, + std::optional err = std::nullopt) const + { + return { + .account = sender, + .dest = dest, + .amt = sendAmount, + .proof = strHex(proof), + .senderEncryptedAmt = senderAmt, + .destEncryptedAmt = destAmt, + .issuerEncryptedAmt = issuerAmt, + .auditorEncryptedAmt = auditorAmt, + .amountCommitment = amountCommitment, + .balanceCommitment = balanceCommitment, + .err = err, + }; + } + }; + + // Helper that wraps the boilerplate setup: Env + MPT creation, funding, key + // generation, and seeding each holder with a confidential balance. + // The caller supplies the issuer and any number of holders. + struct ConfidentialEnv + { + // Per-holder configuration: the account, how much MPT to fund it + // with, and how much of that to convert to a confidential balance. + struct HolderInit + { + test::jtx::Account account; + std::uint64_t payAmount = 1000; + std::uint64_t convertAmount = 100; + }; + + test::jtx::MPTTester mpt; + + ConfidentialEnv( + test::jtx::Env& env, + test::jtx::Account const& issuer, + std::vector const& holders, + std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + std::optional auditor = std::nullopt) + : mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}} + { + mpt.create({.ownerCount = 1, .flags = flags}); + + for (auto const& h : holders) + { + mpt.authorize({.account = h.account}); + if ((flags & tfMPTRequireAuth) != 0) + mpt.authorize({.account = issuer, .holder = h.account}); + mpt.pay(issuer, h.account, h.payAmount); + } + + mpt.generateKeyPair(issuer); + for (auto const& h : holders) + mpt.generateKeyPair(h.account); + if (auditor) + mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor")); + + mpt.set({ + .account = issuer, + .issuerPubKey = mpt.getPubKey(issuer), + .auditorPubKey = auditor + ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor")) + : std::optional{}, + }); + + for (auto const& h : holders) + { + mpt.convert({ + .account = h.account, + .amt = h.convertAmount, + .holderPubKey = mpt.getPubKey(h.account), + }); + mpt.mergeInbox({.account = h.account}); + } + } + + private: + static std::vector + extractAccounts(std::vector const& holders) + { + std::vector accounts; + accounts.reserve(holders.size()); + for (auto const& h : holders) + accounts.push_back(h.account); + return accounts; + } + }; + + // Set up an MPT environment suitable for batch testing. + // alice is issuer; bob has 'bobAmt' in confidential spending; carol has + // 'carolAmt' in confidential spending; dave is initialised with pubkey but + // zero spending/inbox. + static void + setupBatchEnv( + test::jtx::MPTTester& mpt, + test::jtx::Account const& alice, + test::jtx::Account const& bob, + test::jtx::Account const& carol, + test::jtx::Account const& dave, + std::uint64_t bobAmt, + std::uint64_t carolAmt) + { + using namespace test::jtx; + mpt.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mpt.authorize({.account = bob}); + mpt.authorize({.account = carol}); + mpt.authorize({.account = dave}); + + if (bobAmt > 0) + mpt.pay(alice, bob, bobAmt); + if (carolAmt > 0) + mpt.pay(alice, carol, carolAmt); + + mpt.generateKeyPair(alice); + mpt.generateKeyPair(bob); + mpt.generateKeyPair(carol); + mpt.generateKeyPair(dave); + + mpt.set({ + .account = alice, + .issuerPubKey = mpt.getPubKey(alice), + }); + + if (bobAmt > 0) + { + mpt.convert({ + .account = bob, + .amt = bobAmt, + .holderPubKey = mpt.getPubKey(bob), + }); + mpt.mergeInbox({.account = bob}); + } + else + { + mpt.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mpt.getPubKey(bob), + }); + } + + if (carolAmt > 0) + { + mpt.convert({ + .account = carol, + .amt = carolAmt, + .holderPubKey = mpt.getPubKey(carol), + }); + mpt.mergeInbox({.account = carol}); + } + else + { + mpt.convert({ + .account = carol, + .amt = 0, + .holderPubKey = mpt.getPubKey(carol), + }); + } + + // dave: register pubkey only (0 spending/inbox) + mpt.convert({ + .account = dave, + .amt = 0, + .holderPubKey = mpt.getPubKey(dave), + }); + } +}; + +} // namespace xrpl diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index a80ce46b9c..0e78d409fa 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -14,18 +14,46 @@ #include #include -/** Batch operations */ +/** @brief Helpers for constructing Batch test transactions. */ namespace xrpl::test::jtx::batch { -/** Calculate Batch Fee. */ +/** + * @brief Calculate the expected outer Batch transaction fee. + * + * @param env The test environment providing ledger fee settings. + * @param numSigners Number of outer transaction signers. + * @param txns Number of inner transactions in the batch. + * @return The expected Batch fee. + */ XRPAmount calcBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0); -/** Batch. */ +/** + * @brief Calculate the expected Batch fee when inner transactions are + * confidential MPT transactions. + * + * @param env The test environment providing ledger fee settings. + * @param numSigners Number of outer transaction signers. + * @param txns Number of confidential MPT inner transactions in the batch. + * @return The expected Batch fee including confidential transaction fee + * multipliers. + */ +XRPAmount +calcConfidentialBatchFee(jtx::Env const& env, uint32_t const& numSigners, uint32_t const& txns = 0); + +/** + * @brief Build an outer Batch transaction JSON object. + * + * @param account The account submitting the outer Batch transaction. + * @param seq The sequence number for the outer Batch transaction. + * @param fee The fee to set on the outer Batch transaction. + * @param flags The transaction flags to set. + * @return The outer Batch transaction JSON object. + */ json::Value outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags); -/** Adds a new Batch Txn on a JTx and autofills. */ +/** @brief Adds an inner Batch transaction to a JTx and autofills it. */ class Inner { private: @@ -75,7 +103,7 @@ public: } }; -/** Set a batch signature on a JTx. */ +/** @brief Sets the Batch transaction signers on a JTx. */ class Sig { public: @@ -98,7 +126,7 @@ public: operator()(Env&, JTx& jt) const; }; -/** Set a batch nested multi-signature on a JTx. */ +/** @brief Sets a nested multi-signature for a Batch transaction on a JTx. */ class Msig { public: diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp index 2f7a67b45a..66ca0c7d54 100644 --- a/src/test/jtx/impl/batch.cpp +++ b/src/test/jtx/impl/batch.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,16 @@ calcBatchFee(test::jtx::Env const& env, uint32_t const& numSigners, uint32_t con return ((numSigners + 2) * feeDrops) + feeDrops * txns; } +XRPAmount +calcConfidentialBatchFee( + test::jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns) +{ + XRPAmount const feeDrops = env.current()->fees().base; + return ((numSigners + 2) * feeDrops) + feeDrops * (kConfidentialFeeMultiplier + 1) * txns; +} + // Batch. json::Value outer(jtx::Account const& account, uint32_t seq, STAmount const& fee, std::uint32_t flags) diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 84c5b5fbec..dddfc88c7f 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -18,8 +20,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -27,9 +31,17 @@ #include #include +#include + +#include +#include +#include + #include #include +#include #include +#include #include #include #include @@ -40,6 +52,47 @@ #include namespace xrpl::test::jtx { +namespace { + +constexpr std::uint64_t kElGamalDecryptRangeLow = 0; +constexpr std::uint64_t kElGamalDecryptRangeHigh = 3000; + +/** + * @brief Returns a reference to the value held by an optional, throwing if it + * is not an optional. + * + * @param opt The optional to unwrap. + * @param what Description used in the thrown exception if opt is empty. + * @return A const reference to the contained value. + */ +template +[[nodiscard]] T const& +requireValue(std::optional const& opt, char const* what) +{ + if (!opt) + Throw(what); + return *opt; +} + +/** + * @brief Helper function to convert a PedersenProofParams into the C library struct. + * + * @param params The Pedersen commitment proof parameters. + * @return The equivalent mpt_pedersen_proof_params for use with the C library. + */ +mpt_pedersen_proof_params +makePedersenParams(PedersenProofParams const& params) +{ + mpt_pedersen_proof_params res{}; + std::memcpy( + res.pedersen_commitment, params.pedersenCommitment.data(), kMPT_PEDERSEN_COMMIT_SIZE); + res.amount = params.amt; + std::memcpy(res.ciphertext, params.encryptedAmt.data(), kMPT_ELGAMAL_TOTAL_SIZE); + std::memcpy(res.blinding_factor, params.blindingFactor.data(), kMPT_BLINDING_FACTOR_SIZE); + return res; +} + +} // namespace struct MPTSetFlagMapping { @@ -88,13 +141,20 @@ MPTTester::makeHolders(std::vector const& holders) } MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg) - : env_(env), issuer_(std::move(issuer)), holders_(makeHolders(arg.holders)), close_(arg.close) + : env_(env) + , issuer_(std::move(issuer)) + , holders_(makeHolders(arg.holders)) + , auditor_(arg.auditor) + , close_(arg.close) { if (arg.fund) { env_.fund(arg.xrp, issuer_); for (auto const& it : holders_) env_.fund(arg.xrpHolders, it.second); + + if (arg.auditor) + env_.fund(arg.xrp, *arg.auditor); } if (close_) env.close(); @@ -107,6 +167,9 @@ MPTTester::MPTTester(Env& env, Account issuer, MPTInit const& arg) Throw("Issuer can't be holder"); env_.require(Owners(it.second, 0)); } + + if (arg.auditor) + env_.require(Owners(*arg.auditor, 0)); } if (arg.create) create(*arg.create); @@ -148,7 +211,12 @@ MPTTester::MPTTester(MPTInitDef const& arg) : MPTTester{ arg.env, arg.issuer, - MPTInit{.fund = arg.fund, .close = arg.close, .create = makeMPTCreate(arg)}} + MPTInit{ + .auditor = arg.auditor, + .fund = arg.fund, + .close = arg.close, + .create = makeMPTCreate(arg), + }} { } @@ -401,6 +469,10 @@ MPTTester::setJV(MPTSet const& arg) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) jv[sfMPTokenMetadata] = strHex(*arg.metadata); + if (arg.issuerPubKey) + jv[sfIssuerEncryptionKey] = strHex(*arg.issuerPubKey); + if (arg.auditorPubKey) + jv[sfAuditorEncryptionKey] = strHex(*arg.auditorPubKey); jv[sfTransactionType] = jss::MPTokenIssuanceSet; return jv; @@ -419,42 +491,92 @@ MPTTester::set(MPTSet const& arg) .transferFee = arg.transferFee, .metadata = arg.metadata, .delegate = arg.delegate, - .domainID = arg.domainID}); + .domainID = arg.domainID, + .issuerPubKey = arg.issuerPubKey, + .auditorPubKey = arg.auditorPubKey}); if (submit(arg, jv) == tesSUCCESS && ((arg.flags.value_or(0) != 0u) || arg.mutableFlags)) { - auto require = [&](std::optional const& holder, bool unchanged) { - auto flags = getFlags(holder); - if (!unchanged) - { - if (arg.flags) + if (((arg.flags.value_or(0) != 0u) || arg.mutableFlags)) + { + auto require = [&](std::optional const& holder, bool unchanged) { + auto flags = getFlags(holder); + if (!unchanged) { - if (*arg.flags & tfMPTLock) + if (arg.flags) { - flags |= lsfMPTLocked; - } - else if (*arg.flags & tfMPTUnlock) - { - flags &= ~lsfMPTLocked; - } - } - - if (arg.mutableFlags) - { - for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings) - { - if ((*arg.mutableFlags & setFlag) != 0u) + if (*arg.flags & tfMPTLock) { - flags |= ledgerFlag; + flags |= lsfMPTLocked; + } + else if (*arg.flags & tfMPTUnlock) + { + flags &= ~lsfMPTLocked; } } + + if (arg.mutableFlags) + { + for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings) + { + if ((*arg.mutableFlags & setFlag) != 0u) + { + flags |= ledgerFlag; + } + } + + if (*arg.mutableFlags & tmfMPTSetCanHoldConfidentialBalance) + flags |= tfMPTCanHoldConfidentialBalance; + } } + env_.require(MptFlags(*this, flags, holder)); + }; + if (arg.account) + require(std::nullopt, arg.holder.has_value()); + if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) + require(*account, false); + + if (arg.issuerPubKey) + { + env_.require(RequireAny([&]() -> bool { + return forObject([&](SLEP const& sle) -> bool { + if (sle) + { + auto const issuerPubKey = getPubKey(issuer_); + if (!issuerPubKey) + { + Throw( + "MPTTester::set: issuer's pubkey is not set"); + } + + return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey); + } + return false; + }); + })); } - env_.require(MptFlags(*this, flags, holder)); - }; - if (arg.account) - require(std::nullopt, arg.holder.has_value()); - if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) - require(*account, false); + if (arg.auditorPubKey) + { + env_.require(RequireAny([&]() -> bool { + return forObject([&](SLEP const& sle) -> bool { + if (sle) + { + if (!auditor_.has_value()) + Throw("MPTTester::set: auditor is not set"); + + auto const auditorPubKey = getPubKey(*auditor_); + if (!auditorPubKey) + { + Throw( + "MPTTester::set: auditor's pubkey is not set"); + } + + return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey); + } + return false; + }); + })); + } + } } } @@ -495,6 +617,14 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const [&](SLEP const& sle) { return expectedAmount == (*sle)[sfOutstandingAmount]; }); } +[[nodiscard]] bool +MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const +{ + return forObject([&](SLEP const& sle) { + return expectedAmount == (*sle)[~sfConfidentialOutstandingAmount].value_or(0); + }); +} + [[nodiscard]] bool MPTTester::checkFlags(uint32_t const expectedFlags, std::optional const& holder) const { @@ -641,6 +771,238 @@ MPTTester::getBalance(Account const& account) const return 0; } +std::int64_t +MPTTester::getIssuanceConfidentialBalance() const +{ + if (!id_) + Throw("MPT has not been created"); + + if (auto const sle = env_.le(keylet::mptokenIssuance(*id_))) + return (*sle)[~sfConfidentialOutstandingAmount].value_or(0); + + return 0; +} + +std::optional +MPTTester::getClawbackProof( + Account const& holder, + std::uint64_t amount, + Buffer const& privateKey, + uint256 const& contextHash) const +{ + if (!id_) + Throw("MPT has not been created"); + + auto const sleHolder = env_.le(keylet::mptoken(*id_, holder.id())); + auto const sleIssuance = env_.le(keylet::mptokenIssuance(*id_)); + + if (!sleHolder || !sleIssuance) + return std::nullopt; + + auto const ciphertextBlob = sleHolder->getFieldVL(sfIssuerEncryptedBalance); + if (ciphertextBlob.size() != kEcGamalEncryptedTotalLength) + return std::nullopt; + + auto const pubKeyBlob = sleIssuance->getFieldVL(sfIssuerEncryptionKey); + if (pubKeyBlob.size() != kEcPubKeyLength) + return std::nullopt; + + Buffer proof(kEcClawbackProofLength); + + if (mpt_get_clawback_proof( + privateKey.data(), + pubKeyBlob.data(), + contextHash.data(), + amount, + ciphertextBlob.data(), + proof.data()) != 0) + { + return std::nullopt; + } + + return proof; +} + +std::optional +MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const +{ + auto const pubKey = getPubKey(account); + if (!pubKey || pubKey->size() != kEcPubKeyLength) + return std::nullopt; + + auto const privKey = getPrivKey(account); + if (requireValue(privKey, "privKey").size() != kEcPrivKeyLength) + return std::nullopt; + + Buffer proof(kEcSchnorrProofLength); + + if (mpt_get_convert_proof( + requireValue(pubKey, "pubKey").data(), + requireValue(privKey, "privKey").data(), + ctxHash.data(), + proof.data()) != 0) + return std::nullopt; + + return proof; +} + +std::optional +MPTTester::getConfidentialSendProof( + Account const& sender, + std::uint64_t const amount, + std::vector const& recipients, + Slice const& blindingFactor, + uint256 const& contextHash, + PedersenProofParams const& amountParams, + PedersenProofParams const& balanceParams) const +{ + auto const pedersenBalanceParams = makePedersenParams(balanceParams); + + if (blindingFactor.size() != kEcBlindingFactorLength) + return std::nullopt; + + auto const senderPrivKey = getPrivKey(sender); + if (!senderPrivKey) + return std::nullopt; + + auto const senderPubKey = getPubKey(sender); + if (!senderPubKey || senderPubKey->size() != kEcPubKeyLength) + return std::nullopt; + + if (amountParams.pedersenCommitment.size() != kEcPedersenCommitmentLength) + return std::nullopt; + + // Build mpt_confidential_participant array + std::vector participants(recipients.size()); + for (size_t i = 0; i < recipients.size(); ++i) + { + auto const& r = recipients[i]; + if (r.encryptedAmount.size() != kEcGamalEncryptedTotalLength || + r.publicKey.size() != kEcPubKeyLength) + { + return std::nullopt; + } + std::memcpy(participants[i].pubkey, r.publicKey.data(), kEcPubKeyLength); + std::memcpy( + participants[i].ciphertext, r.encryptedAmount.data(), kEcGamalEncryptedTotalLength); + } + + size_t proofLen = kEcSendProofLength; + Buffer proof(proofLen); + + if (mpt_get_confidential_send_proof( + senderPrivKey->data(), + senderPubKey->data(), + amount, + participants.data(), + recipients.size(), + blindingFactor.data(), + contextHash.data(), + amountParams.pedersenCommitment.data(), + &pedersenBalanceParams, + proof.data(), + &proofLen) != 0) + return std::nullopt; + + return proof; +} + +Buffer +MPTTester::getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor) +{ + // Blinding factor (rho) must be a 32-byte scalar + if (pedersenBlindingFactor.size() != kEcBlindingFactorLength) + Throw("Invalid blinding factor size"); + + // secp256k1_mpt_pedersen_commit doesn't handle amount 0, return a trivial + // valid commitment for test purposes + if (amount == 0) + { + Buffer buf(kEcPedersenCommitmentLength); + std::memset(buf.data(), 0, kEcPedersenCommitmentLength); + buf.data()[0] = kEcCompressedPrefixEvenY; + buf.data()[kEcPedersenCommitmentLength - 1] = 0x01; + return buf; + } + + Buffer buf(kEcPedersenCommitmentLength); + + if (mpt_get_pedersen_commitment(amount, pedersenBlindingFactor.data(), buf.data()) != 0) + Throw("Pedersen commitment generation failed"); + + return buf; +} + +Buffer +MPTTester::getConvertBackProof( + Account const& holder, + std::uint64_t const amount, + uint256 const& contextHash, + PedersenProofParams const& pcParams) const +{ + // Expected total proof length: compact sigma proof (128 bytes) + single bulletproof (688 bytes) + std::size_t constexpr kExpectedProofLength = kEcConvertBackProofLength; + + auto const sleMptoken = env_.le(keylet::mptoken(issuanceID(), holder.id())); + if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) + return gMakeZeroBuffer(kExpectedProofLength); + + auto const holderPubKey = getPubKey(holder); + auto const holderPrivKey = getPrivKey(holder); + + if (!holderPubKey || !holderPrivKey) + return gMakeZeroBuffer(kExpectedProofLength); + + auto const pedersenParams = makePedersenParams(pcParams); + Buffer proof(kExpectedProofLength); + + if (mpt_get_convert_back_proof( + holderPrivKey->data(), + holderPubKey->data(), + contextHash.data(), + amount, + &pedersenParams, + proof.data()) != 0) + return gMakeZeroBuffer(kExpectedProofLength); + + return proof; +} + +std::optional +MPTTester::getEncryptedBalance(Account const& account, EncryptedBalanceType option) const +{ + if (!id_) + Throw("MPT has not been created"); + + if (auto const sle = env_.le(keylet::mptoken(*id_, account.id()))) + { + if (option == holderEncryptedInbox && sle->isFieldPresent(sfConfidentialBalanceInbox)) + { + return Buffer( + (*sle)[sfConfidentialBalanceInbox].data(), + (*sle)[sfConfidentialBalanceInbox].size()); + } + if (option == holderEncryptedSpending && sle->isFieldPresent(sfConfidentialBalanceSpending)) + { + return Buffer( + (*sle)[sfConfidentialBalanceSpending].data(), + (*sle)[sfConfidentialBalanceSpending].size()); + } + if (option == issuerEncryptedBalance && sle->isFieldPresent(sfIssuerEncryptedBalance)) + { + return Buffer( + (*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size()); + } + if (option == auditorEncryptedBalance && sle->isFieldPresent(sfAuditorEncryptedBalance)) + { + return Buffer( + (*sle)[sfAuditorEncryptedBalance].data(), (*sle)[sfAuditorEncryptedBalance].size()); + } + } + + return {}; +} + std::uint32_t MPTTester::getFlags(std::optional const& holder) const { @@ -667,4 +1029,1515 @@ MPTTester::operator()(std::int64_t amount) const return MPT("", issuanceID())(amount); } +template +void +MPTTester::fillConversionCiphertexts( + T const& arg, + json::Value& jv, + Buffer& holderCiphertext, + Buffer& issuerCiphertext, + std::optional& auditorCiphertext, + Buffer& blindingFactor) const +{ + blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); + + // Handle Holder + if (arg.holderEncryptedAmt) + { + holderCiphertext = *arg.holderEncryptedAmt; + } + else + { + holderCiphertext = encryptAmount( + requireValue(arg.account, "account"), requireValue(arg.amt, "amt"), blindingFactor); + } + + jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext); + + // Handle Issuer + if (arg.issuerEncryptedAmt) + { + issuerCiphertext = *arg.issuerEncryptedAmt; + } + else + { + issuerCiphertext = encryptAmount(issuer_, requireValue(arg.amt, "amt"), blindingFactor); + } + + jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext); + + // Handle Auditor + if (arg.auditorEncryptedAmt) + { + auditorCiphertext = *arg.auditorEncryptedAmt; + } + else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) + { + auditorCiphertext = encryptAmount( + requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); + } + + // Update auditor JSON only if ciphertext exists + if (auditorCiphertext) + jv[sfAuditorEncryptedAmount.jsonName] = strHex(*auditorCiphertext); +} + +void +MPTTester::convert(MPTConvert const& arg) +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + jv[jss::TransactionType] = jss::ConfidentialMPTConvert; + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + if (arg.amt) + jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + if (arg.holderPubKey) + jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey); + + Buffer holderCiphertext; + Buffer issuerCiphertext; + std::optional auditorCiphertext; + Buffer blindingFactor; + + fillConversionCiphertexts( + arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); + + jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); + if (arg.proof) + { + jv[sfZKProof.jsonName] = *arg.proof; + } + else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value())) + { + // whether to automatically generate and attach a Schnorr proof: + // if fillSchnorrProof is explicitly set, follow its value; + // otherwise, default to generating the proof only if holder pub key is + // present. + auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); + auto const contextHash = + getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq); + + auto const proof = getSchnorrProof(*arg.account, contextHash); + if (proof) + { + jv[sfZKProof.jsonName] = strHex(*proof); + } + else + { + jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength)); + } + } + + auto const holderAmt = getBalance(*arg.account); + auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); + + auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get Pre-convert balance"); + + std::optional prevAuditorBalance; + if (arg.auditorEncryptedAmt || auditor_) + { + prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + if (!prevAuditorBalance) + Throw("Failed to get Pre-convert balance"); + } + + auto const prevOutstanding = getIssuanceOutstandingBalance(); + + if (submit(arg, jv) == tesSUCCESS) + { + auto const postConfidentialOutstanding = getIssuanceConfidentialBalance(); + auto const postOutstanding = getIssuanceOutstandingBalance(); + env_.require(MptBalance( + *this, requireValue(arg.account, "account"), holderAmt - requireValue(arg.amt, "amt"))); + env_.require(RequireAny([&]() -> bool { + return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding; + })); + env_.require(RequireAny([&]() -> bool { + return prevConfidentialOutstanding + *arg.amt == postConfidentialOutstanding; + })); + + env_.require(RequireAny([&]() -> bool { + return getEncryptedBalance(*arg.account, holderEncryptedInbox).has_value(); + })); + env_.require(RequireAny([&]() -> bool { + return getEncryptedBalance(*arg.account, holderEncryptedSpending).has_value(); + })); + env_.require(RequireAny([&]() -> bool { + return getEncryptedBalance(*arg.account, issuerEncryptedBalance).has_value(); + })); + + auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + + if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) + Throw("Failed to get post-convert balance"); + + if (arg.auditorEncryptedAmt || auditor_) + { + auto const postAuditorBalance = + getDecryptedBalance(*arg.account, auditorEncryptedBalance); + + if (!postAuditorBalance) + Throw("Failed to get post-convert auditor balance"); + + env_.require(RequireAny([&]() -> bool { + return getEncryptedBalance(*arg.account, auditorEncryptedBalance).has_value(); + })); + + // auditor's encrypted balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevAuditorBalance + *arg.amt == *postAuditorBalance; })); + } + // spending balance should not change + env_.require( + RequireAny([&]() -> bool { return *postSpendingBalance == *prevSpendingBalance; })); + + // issuer's encrypted balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevIssuerBalance + *arg.amt == *postIssuerBalance; })); + + // holder's inbox balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevInboxBalance + *arg.amt == *postInboxBalance; })); + + // sum of holder's inbox and spending balance should equal to issuer's + // encrypted balance + env_.require(RequireAny([&]() -> bool { + return *postInboxBalance + *postSpendingBalance == *postIssuerBalance; + })); + + if (arg.holderPubKey) + { + env_.require(RequireAny([&]() -> bool { + return forObject( + [&](SLEP const& sle) -> bool { + if (sle) + { + auto const holderPubKey = getPubKey(*arg.account); + if (!holderPubKey) + { + Throw( + "MPTTester::convert: holder's pubkey is " + "not set"); + } + + return strHex((*sle)[sfHolderEncryptionKey]) == strHex(*holderPubKey); + } + return false; + }, + arg.account); + })); + } + } +} + +json::Value +MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq) +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + jv[jss::TransactionType] = jss::ConfidentialMPTConvert; + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + if (arg.amt) + jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + if (arg.holderPubKey) + jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey); + + Buffer holderCiphertext; + Buffer issuerCiphertext; + std::optional auditorCiphertext; + Buffer blindingFactor; + + fillConversionCiphertexts( + arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); + + jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); + + if (arg.proof) + { + jv[sfZKProof.jsonName] = *arg.proof; + } + else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value())) + { + auto const contextHash = + getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq); + auto const proof = getSchnorrProof(*arg.account, contextHash); + if (proof) + { + jv[sfZKProof.jsonName] = strHex(*proof); + } + else + { + jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength)); + } + } + + return jv; +} + +void +MPTTester::send(MPTConfidentialSend const& arg) +{ + json::Value jv; + jv[jss::TransactionType] = jss::ConfidentialMPTSend; + + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + if (arg.dest) + { + jv[sfDestination] = arg.dest->human(); + } + else + { + Throw("Destination not specified"); + } + + if (!arg.amt) + Throw("Amount not specified for testing purposes"); + + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + Buffer const blindingFactor = + arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); + + // fill in the encrypted amounts if not provided + auto const senderAmt = arg.senderEncryptedAmt + ? *arg.senderEncryptedAmt + : encryptAmount(*arg.account, *arg.amt, blindingFactor); + auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt + : encryptAmount(*arg.dest, *arg.amt, blindingFactor); + auto const issuerAmt = arg.issuerEncryptedAmt + ? *arg.issuerEncryptedAmt + : encryptAmount(issuer_, *arg.amt, blindingFactor); + + std::optional auditorAmt; + if (arg.auditorEncryptedAmt) + { + auditorAmt = arg.auditorEncryptedAmt; + } + else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) + { + auditorAmt = encryptAmount( + requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); + } + + jv[sfSenderEncryptedAmount] = strHex(senderAmt); + jv[sfDestinationEncryptedAmount] = strHex(destAmt); + jv[sfIssuerEncryptedAmount] = strHex(issuerAmt); + if (auditorAmt) + jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt); + + if (arg.credentials) + { + auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array); + for (auto const& hash : *arg.credentials) + arr.append(hash); + } + + // Version counters before send + auto const prevSenderVersion = getMPTokenVersion(*arg.account); + auto const prevDestVersion = getMPTokenVersion(*arg.dest); + + // Sender's previous confidential state + auto const prevSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const prevSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevSenderInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox); + auto const prevSenderSpendingEncrypted = + getEncryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevSenderIssuerEncrypted = + getEncryptedBalance(*arg.account, issuerEncryptedBalance); + if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer) + Throw("Failed to get Pre-send balance"); + + std::optional prevSenderAuditor; + auto const prevSenderAuditorEncrypted = + getEncryptedBalance(*arg.account, auditorEncryptedBalance); + if (arg.auditorEncryptedAmt || auditor_) + { + prevSenderAuditor = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + if (!prevSenderAuditor) + Throw("Failed to get Pre-send balance"); + } + + // Destination's previous confidential state + auto const prevDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox); + auto const prevDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending); + auto const prevDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance); + auto const prevDestInboxEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedInbox); + auto const prevDestSpendingEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedSpending); + auto const prevDestIssuerEncrypted = getEncryptedBalance(*arg.dest, issuerEncryptedBalance); + if (!prevDestInbox || !prevDestSpending || !prevDestIssuer) + Throw("Failed to get Pre-send balance"); + + std::optional prevDestAuditor; + auto const prevDestAuditorEncrypted = getEncryptedBalance(*arg.dest, auditorEncryptedBalance); + if (arg.auditorEncryptedAmt || auditor_) + { + prevDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance); + if (!prevDestAuditor) + Throw("Failed to get Pre-send balance"); + } + + // Fill in the commitment if not provided + // The amount commitment must use the same blinding factor as the ElGamal + // encryption. The sigma proof links the two, so using different randomness + // for each would cause proof verification to fail. + Buffer amountCommitment, balanceCommitment; + if (arg.amountCommitment) + { + amountCommitment = *arg.amountCommitment; + } + else + { + amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor); + } + + jv[sfAmountCommitment] = strHex(amountCommitment); + + auto const balanceBlindingFactor = generateBlindingFactor(); + if (arg.balanceCommitment) + { + balanceCommitment = *arg.balanceCommitment; + } + else + { + balanceCommitment = getPedersenCommitment(*prevSenderSpending, balanceBlindingFactor); + } + + jv[sfBalanceCommitment] = strHex(balanceCommitment); + + // Fill in the proof if not provided + if (arg.proof) + { + jv[sfZKProof] = *arg.proof; + } + else + { + auto const version = getMPTokenVersion(*arg.account); + auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); + auto const ctxHash = getSendContextHash( + requireValue(arg.account, "account").id(), + issuanceID(), + seq, + requireValue(arg.dest, "dest").id(), + version); + + std::vector recipients; + + auto const senderPubKey = getPubKey(*arg.account); + auto const destPubKey = getPubKey(*arg.dest); + auto const issuerPubKey = getPubKey(issuer_); + + // If a key is missing, we skip adding the recipient. This intentionally + // causes proof generation to fail, triggering the dummy proof fallback. + if (senderPubKey) + { + recipients.push_back({ + .publicKey = Slice(*senderPubKey), + .encryptedAmount = senderAmt, + }); + } + if (destPubKey) + { + recipients.push_back({ + .publicKey = Slice(*destPubKey), + .encryptedAmount = destAmt, + }); + } + if (issuerPubKey) + { + recipients.push_back({ + .publicKey = Slice(*issuerPubKey), + .encryptedAmount = issuerAmt, + }); + } + + std::optional auditorPubKey; + if (auditorAmt) + { + if (!auditor_) + Throw("Auditor not registered"); + + auditorPubKey = getPubKey(*auditor_); + if (auditorPubKey) + { + recipients.push_back({ + .publicKey = Slice(*auditorPubKey), + .encryptedAmount = *auditorAmt, + }); + } + } + + std::optional proof; + + // Skip proof generation if encrypted balance is missing (e.g., + // feature disabled), when the sender and destination are the same + // (malformed case causing pcm to be zero), or when spending balance + // is 0 + if (arg.account != arg.dest && prevSenderSpendingEncrypted && *prevSenderSpending > 0) + { + proof = getConfidentialSendProof( + *arg.account, + *arg.amt, + recipients, + blindingFactor, + ctxHash, + { + .pedersenCommitment = amountCommitment, + .amt = *arg.amt, + .encryptedAmt = senderAmt, + .blindingFactor = blindingFactor, + }, + { + .pedersenCommitment = balanceCommitment, + .amt = *prevSenderSpending, + .encryptedAmt = *prevSenderSpendingEncrypted, + .blindingFactor = balanceBlindingFactor, + }); + } + + if (proof) + { + jv[sfZKProof.jsonName] = strHex(*proof); + } + else + { + jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength)); + } + } + + auto const senderPubAmt = getBalance(*arg.account); + auto const destPubAmt = getBalance(*arg.dest); + auto const prevCOA = getIssuanceConfidentialBalance(); + auto const prevOA = getIssuanceOutstandingBalance(); + + if (submit(arg, jv) == tesSUCCESS) + { + auto const postCOA = getIssuanceConfidentialBalance(); + auto const postOA = getIssuanceOutstandingBalance(); + + // Sender's post confidential state + auto const postSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const postSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const postSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + + if (!postSenderInbox || !postSenderSpending || !postSenderIssuer) + Throw("Failed to get Post-send balance"); + + // Destination's post confidential state + auto const postDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox); + auto const postDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending); + auto const postDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance); + + if (!postDestInbox || !postDestSpending || !postDestIssuer) + Throw("Failed to get Post-send balance"); + + // Public balances unchanged + env_.require(MptBalance(*this, *arg.account, senderPubAmt)); + env_.require(MptBalance(*this, *arg.dest, destPubAmt)); + + // OA and COA unchanged + env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; })); + env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; })); + + // Verify sender changes + env_.require(RequireAny([&]() -> bool { + return *prevSenderSpending >= *arg.amt && + *postSenderSpending == *prevSenderSpending - *arg.amt; + })); + env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; })); + env_.require(RequireAny([&]() -> bool { + return *prevSenderIssuer >= *arg.amt && + *postSenderIssuer == *prevSenderIssuer - *arg.amt; + })); + + // Verify destination changes + env_.require( + RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + *arg.amt; })); + env_.require(RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; })); + env_.require( + RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + *arg.amt; })); + + // Cross checks + env_.require(RequireAny( + [&]() -> bool { return *postSenderInbox + *postSenderSpending == *postSenderIssuer; })); + env_.require(RequireAny( + [&]() -> bool { return *postDestInbox + *postDestSpending == *postDestIssuer; })); + + // Version: sender increments by 1; receiver version is unchanged by incoming sends + env_.require(RequireAny( + [&]() -> bool { return getMPTokenVersion(*arg.account) == prevSenderVersion + 1; })); + env_.require( + RequireAny([&]() -> bool { return getMPTokenVersion(*arg.dest) == prevDestVersion; })); + + if (arg.auditorEncryptedAmt || auditor_) + { + auto const postSenderAuditor = + getDecryptedBalance(*arg.account, auditorEncryptedBalance); + auto const postDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance); + if (!postSenderAuditor || !postDestAuditor) + Throw("Failed to get Post-send balance"); + + env_.require(RequireAny([&]() -> bool { + return *postSenderAuditor == *postSenderIssuer && + *postDestAuditor == *postDestIssuer; + })); + + // verify sender + env_.require(RequireAny([&]() -> bool { + return prevSenderAuditor >= *arg.amt && + *postSenderAuditor == *prevSenderAuditor - *arg.amt; + })); + + // verify dest + env_.require(RequireAny( + [&]() -> bool { return *postDestAuditor == *prevDestAuditor + *arg.amt; })); + } + } +} + +json::Value +MPTTester::sendJV( + MPTConfidentialSend const& arg, + std::uint32_t seq, + std::optional chain) +{ + json::Value jv; + jv[jss::TransactionType] = jss::ConfidentialMPTSend; + + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + if (arg.dest) + { + jv[sfDestination] = arg.dest->human(); + } + else + { + Throw("Destination not specified"); + } + + if (!arg.amt) + Throw("Amount not specified for testing purposes"); + + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + Buffer const blindingFactor = + arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); + + auto const senderAmt = arg.senderEncryptedAmt + ? *arg.senderEncryptedAmt + : encryptAmount(*arg.account, *arg.amt, blindingFactor); + auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt + : encryptAmount(*arg.dest, *arg.amt, blindingFactor); + auto const issuerAmt = arg.issuerEncryptedAmt + ? *arg.issuerEncryptedAmt + : encryptAmount(issuer_, *arg.amt, blindingFactor); + + std::optional auditorAmt; + if (arg.auditorEncryptedAmt) + { + auditorAmt = arg.auditorEncryptedAmt; + } + else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) + { + auditorAmt = encryptAmount( + requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); + } + + jv[sfSenderEncryptedAmount] = strHex(senderAmt); + jv[sfDestinationEncryptedAmount] = strHex(destAmt); + jv[sfIssuerEncryptedAmount] = strHex(issuerAmt); + if (auditorAmt) + jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt); + + if (arg.credentials) + { + auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array); + for (auto const& hash : *arg.credentials) + arr.append(hash); + } + + std::uint64_t prevSenderSpending = 0; + std::optional prevEncryptedSenderSpending; + std::uint32_t version = 0; + if (chain) + { + prevSenderSpending = chain->spending; + prevEncryptedSenderSpending = chain->encSpending; + version = chain->version; + } + else + { + auto const ledgerSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); + if (!ledgerSpending) + Throw("Failed to get sender spending balance"); + prevSenderSpending = *ledgerSpending; + prevEncryptedSenderSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending); + version = getMPTokenVersion(*arg.account); + } + + // The amount commitment must use the same blinding factor as the tx ElGamal + // encryption blinding factor. + Buffer amountCommitment, balanceCommitment; + if (arg.amountCommitment) + { + amountCommitment = *arg.amountCommitment; + } + else + { + amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor); + } + + jv[sfAmountCommitment] = strHex(amountCommitment); + + auto const balanceBlindingFactor = generateBlindingFactor(); + if (arg.balanceCommitment) + { + balanceCommitment = *arg.balanceCommitment; + } + else + { + balanceCommitment = getPedersenCommitment(prevSenderSpending, balanceBlindingFactor); + } + + jv[sfBalanceCommitment] = strHex(balanceCommitment); + + if (arg.proof) + { + jv[sfZKProof.jsonName] = *arg.proof; + } + else + { + auto const ctxHash = getSendContextHash( + requireValue(arg.account, "account").id(), + issuanceID(), + seq, + requireValue(arg.dest, "dest").id(), + version); + + std::vector recipients; + + auto const senderPubKey = getPubKey(*arg.account); + auto const destPubKey = getPubKey(*arg.dest); + auto const issuerPubKey = getPubKey(issuer_); + + if (senderPubKey) + { + recipients.push_back({ + .publicKey = Slice(*senderPubKey), + .encryptedAmount = senderAmt, + }); + } + if (destPubKey) + { + recipients.push_back({ + .publicKey = Slice(*destPubKey), + .encryptedAmount = destAmt, + }); + } + if (issuerPubKey) + { + recipients.push_back({ + .publicKey = Slice(*issuerPubKey), + .encryptedAmount = issuerAmt, + }); + } + + std::optional auditorPubKey; + if (auditorAmt) + { + if (!auditor_) + Throw("Auditor not registered"); + auditorPubKey = getPubKey(*auditor_); + if (auditorPubKey) + { + recipients.push_back({ + .publicKey = Slice(*auditorPubKey), + .encryptedAmount = *auditorAmt, + }); + } + } + + std::optional proof; + + // Skip proof generation when spending balance is 0 + if (arg.account != arg.dest && prevEncryptedSenderSpending && prevSenderSpending > 0) + { + proof = getConfidentialSendProof( + *arg.account, + *arg.amt, + recipients, + blindingFactor, + ctxHash, + { + .pedersenCommitment = amountCommitment, + .amt = *arg.amt, + .encryptedAmt = senderAmt, + .blindingFactor = blindingFactor, + }, + { + .pedersenCommitment = balanceCommitment, + .amt = prevSenderSpending, + .encryptedAmt = *prevEncryptedSenderSpending, + .blindingFactor = balanceBlindingFactor, + }); + } + + if (proof) + { + jv[sfZKProof.jsonName] = strHex(*proof); + } + else + { + jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength)); + } + } + + return jv; +} + +static Buffer +parseSenderEncAmt(json::Value const& jv) +{ + auto const hexStr = jv[sfSenderEncryptedAmount.jsonName].asString(); + auto const bytes = strUnHex(hexStr); + if (!bytes) + Throw("chainAfterSend: invalid hex in sfSenderEncryptedAmount"); + return Buffer(bytes->data(), bytes->size()); +} + +ConfidentialSendChainState +MPTTester::chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const +{ + auto const prevSpending = getDecryptedBalance(sender, holderEncryptedSpending); + auto const prevEncSpending = getEncryptedBalance(sender, holderEncryptedSpending); + auto const prevVersion = getMPTokenVersion(sender); + + if (!prevSpending || !prevEncSpending) + Throw("chainAfterSend: failed to read sender state from ledger"); + + Buffer const senderEncAmt = parseSenderEncAmt(jv); + auto chain = computeNextSendChainState( + *prevSpending, Slice(*prevEncSpending), prevVersion, sendAmt, Slice(senderEncAmt)); + if (!chain) + Throw("chainAfterSend: computeNextSendChainState failed"); + return std::move(*chain); +} + +std::optional +computeNextSendChainState( + std::uint64_t currentSpending, + Slice const& currentEncSpending, + std::uint32_t currentVersion, + std::uint64_t sendAmt, + Slice const& senderEncAmt) +{ + if (sendAmt > currentSpending) + return std::nullopt; // LCOV_EXCL_LINE + + auto newEncSpending = homomorphicSubtract(currentEncSpending, senderEncAmt); + if (!newEncSpending) + return std::nullopt; // LCOV_EXCL_LINE + + return ConfidentialSendChainState{ + .spending = currentSpending - sendAmt, + .encSpending = std::move(*newEncSpending), + .version = currentVersion + 1, + }; +} + +void +MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) +{ + json::Value jv; + auto const account = arg.account ? *arg.account : issuer_; + jv[sfAccount] = account.human(); + + if (arg.holder) + { + jv[sfHolder] = arg.holder->human(); + } + else + { + Throw("Holder not specified"); + } + + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else if (id_) + { + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + else + { + Throw("MPT has not been created"); + } + + if (arg.amt) + jv[sfMPTAmount] = std::to_string(*arg.amt); + + if (arg.proof) + { + jv[sfZKProof] = *arg.proof; + } + else + { + auto const seq = arg.ticketSeq ? *arg.ticketSeq : env_.seq(account); + auto const contextHash = getClawbackContextHash( + account.id(), issuanceID(), seq, requireValue(arg.holder, "holder").id()); + + auto const privKey = getPrivKey(account); + if (!privKey || privKey->size() != kEcPrivKeyLength) + Throw("Failed to get clawback private key"); + + auto const proof = getClawbackProof( + requireValue(arg.holder, "holder"), + requireValue(arg.amt, "amt"), + requireValue(privKey, "privKey"), + contextHash); + + if (proof) + { + jv[sfZKProof] = strHex(*proof); + } + else + { + jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcClawbackProofLength)); + } + } + + auto const holderPubAmt = getBalance(*arg.holder); + auto const prevCOA = getIssuanceConfidentialBalance(); + auto const prevOA = getIssuanceOutstandingBalance(); + auto const prevVersion = getMPTokenVersion(*arg.holder); + + if (submit(arg, jv) == tesSUCCESS) + { + auto const postCOA = getIssuanceConfidentialBalance(); + auto const postOA = getIssuanceOutstandingBalance(); + auto const postVersion = getMPTokenVersion(*arg.holder); + + // Verify holder's public balance is unchanged + env_.require(MptBalance(*this, *arg.holder, holderPubAmt)); + + // Verify COA and OA are reduced correctly + env_.require(RequireAny( + [&]() -> bool { return prevCOA >= *arg.amt && postCOA == prevCOA - *arg.amt; })); + env_.require(RequireAny([&]() -> bool { + return prevOA && postOA && *prevOA >= *arg.amt && *postOA == *prevOA - *arg.amt; + })); + + // Verify holder's confidential balances are zeroed out + env_.require(RequireAny( + [&]() -> bool { return getDecryptedBalance(*arg.holder, holderEncryptedInbox) == 0; })); + env_.require(RequireAny([&]() -> bool { + return getDecryptedBalance(*arg.holder, holderEncryptedSpending) == 0; + })); + env_.require(RequireAny([&]() -> bool { + return getDecryptedBalance(*arg.holder, issuerEncryptedBalance) == 0; + })); + env_.require(RequireAny([&]() -> bool { + return getDecryptedBalance(*arg.holder, auditorEncryptedBalance) == 0; + })); + + // Verify version is incremented + env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; })); + } +} + +void +MPTTester::generateKeyPair(Account const& account) +{ + unsigned char privKey[kEcPrivKeyLength]; + secp256k1_pubkey pubKey; + if (secp256k1_elgamal_generate_keypair(secp256k1Context(), privKey, &pubKey) == 0) + Throw("failed to generate key pair"); + + // Serialize public key to compressed format (33 bytes) + unsigned char compressedPubKey[kEcPubKeyLength]; + size_t outLen = kEcPubKeyLength; + if (secp256k1_ec_pubkey_serialize( + secp256k1Context(), compressedPubKey, &outLen, &pubKey, SECP256K1_EC_COMPRESSED) != 1 || + outLen != kEcPubKeyLength) + { + Throw("failed to serialize public key"); + } + + pubKeys_.insert({account.id(), Buffer{compressedPubKey, kEcPubKeyLength}}); + privKeys_.insert({account.id(), Buffer{privKey, kEcPrivKeyLength}}); +} + +std::optional +MPTTester::getPubKey(Account const& account) const +{ + if (auto const it = pubKeys_.find(account.id()); it != pubKeys_.end()) + return it->second; + + return std::nullopt; +} + +std::optional +MPTTester::getPrivKey(Account const& account) const +{ + if (auto const it = privKeys_.find(account.id()); it != privKeys_.end()) + return it->second; + + return std::nullopt; +} + +Buffer +MPTTester::encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) + const +{ + if (auto const pubKey = getPubKey(account)) + { + if (auto const result = xrpl::encryptAmount(amt, *pubKey, blindingFactor)) + return *result; + } + + // Return a dummy buffer on failure to allow testing of + // failures that occur prior to encryption. + return gMakeZeroBuffer(kEcGamalEncryptedTotalLength); +} + +std::optional +MPTTester::decryptAmount(Account const& account, Buffer const& amt) const +{ + if (amt.size() != kEcGamalEncryptedTotalLength) + return std::nullopt; + + auto const pair = makeEcPair(amt); + if (!pair) + return std::nullopt; + + auto const privKey = getPrivKey(account); + if (!privKey || privKey->size() != kEcPrivKeyLength) + return std::nullopt; + + uint64_t decryptedAmt = 0; + if (secp256k1_elgamal_decrypt( + secp256k1Context(), + &decryptedAmt, + &pair->c1, + &pair->c2, + privKey->data(), + kElGamalDecryptRangeLow, + kElGamalDecryptRangeHigh) == 0) + { + return std::nullopt; + } + + return decryptedAmt; +} + +std::optional +MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const +{ + auto const encryptedAmt = getEncryptedBalance(account, balanceType); + + // Return zero to test cases like Feature Disabled, where the ledger object + // does not exist. + if (!encryptedAmt) + return 0; + + Account decryptor = account; + + if (balanceType == issuerEncryptedBalance) + { + decryptor = issuer_; + } + else if (balanceType == auditorEncryptedBalance) + { + if (!auditor_) + return std::nullopt; + decryptor = *auditor_; + } + + return decryptAmount(decryptor, *encryptedAmt); +}; + +json::Value +MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox; + return jv; +} + +void +MPTTester::mergeInbox(MPTMergeInbox const& arg) +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox; + auto const holderPubAmt = getBalance(*arg.account); + auto const prevCOA = getIssuanceConfidentialBalance(); + auto const prevOA = getIssuanceOutstandingBalance(); + auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevAuditorEncrypted = getEncryptedBalance(*arg.account, auditorEncryptedBalance); + auto const prevVersion = getMPTokenVersion(*arg.account); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get pre-mergeInbox balances"); + + if (submit(arg, jv) == tesSUCCESS) + { + auto const postCOA = getIssuanceConfidentialBalance(); + auto const postOA = getIssuanceOutstandingBalance(); + auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const postInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox); + auto const postIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance); + auto const postAuditorEncrypted = + getEncryptedBalance(*arg.account, auditorEncryptedBalance); + auto const postVersion = getMPTokenVersion(*arg.account); + + if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance || + !prevIssuerEncrypted || !postInboxEncrypted || !postIssuerEncrypted) + Throw("Failed to get post-mergeInbox balances"); + + env_.require(MptBalance(*this, *arg.account, holderPubAmt)); + env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; })); + env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; })); + + env_.require(RequireAny([&]() -> bool { + return *postSpendingBalance == *prevInboxBalance + *prevSpendingBalance && + *postInboxBalance == 0; + })); + + env_.require( + RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; })); + + auto const holderPubKey = getPubKey(*arg.account); + if (!holderPubKey) + Throw("Failed to get holder public key"); + + auto const expectedInbox = encryptCanonicalZeroAmount( + requireValue(holderPubKey, "holderPubKey"), + requireValue(arg.account, "account").id(), + issuanceID()); + if (!expectedInbox) + Throw("Failed to get canonical zero encryption"); + + env_.require(RequireAny([&]() -> bool { return *postInboxEncrypted == *expectedInbox; })); + env_.require( + RequireAny([&]() -> bool { return *postIssuerEncrypted == *prevIssuerEncrypted; })); + env_.require(RequireAny([&]() -> bool { + return postAuditorEncrypted.has_value() == prevAuditorEncrypted.has_value() && + (!postAuditorEncrypted || *postAuditorEncrypted == *prevAuditorEncrypted); + })); + env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; })); + + env_.require(RequireAny([&]() -> bool { + return *postSpendingBalance + *postInboxBalance == *postIssuerBalance; + })); + } +} + +std::optional +MPTTester::getIssuanceOutstandingBalance() const +{ + if (!id_) + return std::nullopt; + + auto const sle = env_.current()->read(keylet::mptokenIssuance(*id_)); + + if (!sle) + return std::nullopt; + + return (*sle)[sfOutstandingAmount]; +} + +std::uint32_t +MPTTester::getMPTokenVersion(Account const account) const +{ + if (!id_) + Throw("Issuance ID does not exist"); + + auto const sle = env_.current()->read(keylet::mptoken(*id_, account)); + + // return 0 here instead of throwing an exception since tests for + // preclaim will check if the MPToken exists + if (!sle) + return 0; + + return (*sle)[~sfConfidentialBalanceVersion].value_or(0); +} + +void +MPTTester::convertBack(MPTConvertBack const& arg) +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack; + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + if (arg.amt) + jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + + Buffer holderCiphertext; + Buffer issuerCiphertext; + std::optional auditorCiphertext; + Buffer blindingFactor; + + fillConversionCiphertexts( + arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); + + jv[sfBlindingFactor] = strHex(blindingFactor); + + auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + + if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) + Throw("Failed to get Pre-convertBack balance"); + + Buffer pedersenCommitment; + Buffer const pcBlindingFactor = generateBlindingFactor(); + if (arg.pedersenCommitment) + { + pedersenCommitment = *arg.pedersenCommitment; + } + else + { + pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor); + } + + jv[sfBalanceCommitment] = strHex(pedersenCommitment); + + if (arg.proof) + { + jv[sfZKProof.jsonName] = strHex(*arg.proof); + } + else + { + auto const version = getMPTokenVersion(*arg.account); + + // if the caller generated ciphertexts themselves, they should also + // generate the proof themselves from the blinding factor + auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); + auto const contextHash = getConvertBackContextHash( + requireValue(arg.account, "account").id(), issuanceID(), seq, version); + auto const prevEncryptedSpendingBalance = + getEncryptedBalance(*arg.account, holderEncryptedSpending); + + Buffer proof; + // generate a dummy proof if no encrypted amount field, so that other + // preflight/preclaim are checked + if (!prevEncryptedSpendingBalance) + { + proof = gMakeZeroBuffer(kEcConvertBackProofLength); + } + else + { + proof = getConvertBackProof( + *arg.account, + requireValue(arg.amt, "amt"), + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = *prevSpendingBalance, + .encryptedAmt = *prevEncryptedSpendingBalance, + .blindingFactor = pcBlindingFactor, + }); + } + jv[sfZKProof] = strHex(proof); + } + + auto const holderAmt = getBalance(*arg.account); + auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); + + std::optional prevAuditorBalance; + if (arg.auditorEncryptedAmt || auditor_) + { + prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + if (!prevAuditorBalance) + Throw("Failed to get Pre-convertBack balance"); + } + + auto const prevOutstanding = getIssuanceOutstandingBalance(); + auto const prevVersion = getMPTokenVersion(*arg.account); + + if (submit(arg, jv) == tesSUCCESS) + { + auto const postConfidentialOutstanding = getIssuanceConfidentialBalance(); + auto const postOutstanding = getIssuanceOutstandingBalance(); + auto const postVersion = getMPTokenVersion(*arg.account); + env_.require(MptBalance( + *this, requireValue(arg.account, "account"), holderAmt + requireValue(arg.amt, "amt"))); + env_.require(RequireAny([&]() -> bool { + return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding; + })); + env_.require(RequireAny([&]() -> bool { + return prevConfidentialOutstanding - *arg.amt == postConfidentialOutstanding; + })); + + auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); + auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + + if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) + Throw("Failed to get post-convertBack balance"); + + if (arg.auditorEncryptedAmt || auditor_) + { + auto const postAuditorBalance = + getDecryptedBalance(*arg.account, auditorEncryptedBalance); + + if (!postAuditorBalance) + Throw("Failed to get post-convertBack balance"); + + // auditor's encrypted balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevAuditorBalance - *arg.amt == *postAuditorBalance; })); + } + + // inbox balance should not change + env_.require(RequireAny([&]() -> bool { return *postInboxBalance == *prevInboxBalance; })); + + // issuer's encrypted balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevIssuerBalance - *arg.amt == *postIssuerBalance; })); + + // holder's spending balance is updated correctly + env_.require(RequireAny( + [&]() -> bool { return *prevSpendingBalance - *arg.amt == *postSpendingBalance; })); + + // holder's confidential balance version is updated correctly + env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; })); + + // sum of holder's inbox and spending balance should equal to issuer's + // encrypted balance + env_.require(RequireAny([&]() -> bool { + return *postInboxBalance + *postSpendingBalance == *postIssuerBalance; + })); + } +} + +json::Value +MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq) +{ + json::Value jv; + if (arg.account) + { + jv[sfAccount] = arg.account->human(); + } + else + { + Throw("Account not specified"); + } + + jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack; + if (arg.id) + { + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + } + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + if (arg.amt) + jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + + Buffer holderCiphertext; + Buffer issuerCiphertext; + std::optional auditorCiphertext; + Buffer blindingFactor; + + fillConversionCiphertexts( + arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); + + jv[sfBlindingFactor] = strHex(blindingFactor); + + auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + if (!prevSpendingBalance) + Throw("convertBackJV: failed to read spending balance from ledger"); + + Buffer pedersenCommitment; + Buffer const pcBlindingFactor = generateBlindingFactor(); + if (arg.pedersenCommitment) + { + pedersenCommitment = *arg.pedersenCommitment; + } + else + { + pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor); + } + + jv[sfBalanceCommitment] = strHex(pedersenCommitment); + + if (arg.proof) + { + jv[sfZKProof.jsonName] = strHex(*arg.proof); + } + else + { + auto const version = getMPTokenVersion(*arg.account); + auto const prevEncSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending); + auto const contextHash = getConvertBackContextHash( + requireValue(arg.account, "account").id(), issuanceID(), seq, version); + + Buffer proof; + if (!prevEncSpending) + { + proof = gMakeZeroBuffer(kEcConvertBackProofLength); + } + else + { + proof = getConvertBackProof( + *arg.account, + requireValue(arg.amt, "amt"), + contextHash, + { + .pedersenCommitment = pedersenCommitment, + .amt = *prevSpendingBalance, + .encryptedAmt = *prevEncSpending, + .blindingFactor = pcBlindingFactor, + }); + } + + jv[sfZKProof] = strHex(proof); + } + + return jv; +} + } // namespace xrpl::test::jtx diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index 7da419c6e7..c298cee684 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +58,22 @@ fillFee(json::Value& jv, ReadView const& view) { if (jv.isMember(jss::Fee)) return; - jv[jss::Fee] = to_string(view.fees().base); + + auto const base = view.fees().base; + + // For confidential transactions, the fee is higher because confidential + // transaction processing is more expensive. + auto const txType = jv[jss::TransactionType].asString(); + if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack || + txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox || + txType == jss::ConfidentialMPTClawback) + { + jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1)); + } + else + { + jv[jss::Fee] = to_string(base); + } } void diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index c8a65d7541..d5fba82e08 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -2,12 +2,21 @@ #include #include +#include #include +#include #include +#include #include +#include #include #include +#include + +#include +#include +#include namespace xrpl::test::jtx { @@ -15,7 +24,28 @@ class MPTTester; auto const kMptDexFlags = tfMPTCanTrade | tfMPTCanTransfer; -// Check flags settings on MPT create +/** + * @brief Create a zero-initialized buffer for malformed cryptography test + * inputs. + * + * xrpl::Buffer(size) allocates uninitialized heap memory. Because CI runs unit + * tests sequentially in the same process, uninitialized memory can recycle + * valid secp256k1 keys or Pedersen commitments from earlier tests. Explicitly + * zeroing the buffer guarantees structural validation fails deterministically. + * + * @param size The number of zero bytes to allocate. + * @return A buffer containing size zero bytes. + */ +[[nodiscard]] inline Buffer +gMakeZeroBuffer(std::size_t size) +{ + Buffer b(size); + if (size > 0) + std::memset(b.data(), 0, size); + return b; +} + +/** @brief Test helper that checks MPT flag settings after creation. */ class MptFlags { private: @@ -36,7 +66,7 @@ public: operator()(Env& env) const; }; -// Check mptissuance or mptoken amount balances on payment +/** @brief Test helper that checks MPT issuance or holder balances. */ class MptBalance { private: @@ -54,6 +84,7 @@ public: operator()(Env& env) const; }; +/** @brief Test helper that accepts any condition supplied by a callback. */ class RequireAny { private: @@ -70,6 +101,7 @@ public: using Holders = std::vector; +/** @brief Arguments for building an MPTokenIssuanceCreate test transaction. */ struct MPTCreate { static inline std::vector allHolders = {}; @@ -93,9 +125,13 @@ struct MPTCreate std::optional err = std::nullopt; }; +/** @brief Arguments for initializing funded MPT test accounts and issuance. */ struct MPTInit { + // Default-initialized so designated-initializer call sites that omit + // `holders` don't trip GCC's -Werror=missing-field-initializers. Holders holders = {}; // NOLINT(readability-redundant-member-init) + std::optional auditor = std::nullopt; PrettyAmount const xrp = XRP(10'000); PrettyAmount const xrpHolders = XRP(10'000); bool fund = true; @@ -105,11 +141,13 @@ struct MPTInit }; static MPTInit const kMptInitNoFund{.fund = false}; +/** @brief Full constructor arguments for MPTTester initialization. */ struct MPTInitDef { Env& env; Account issuer; Holders holders = {}; // NOLINT(readability-redundant-member-init) + std::optional auditor = std::nullopt; std::uint16_t transferFee = 0; std::optional pay = std::nullopt; std::uint32_t flags = kMptDexFlags; @@ -121,6 +159,7 @@ struct MPTInitDef std::optional err = std::nullopt; }; +/** @brief Arguments for building an MPTokenIssuanceDestroy test transaction. */ struct MPTDestroy { std::optional issuer = std::nullopt; @@ -131,6 +170,7 @@ struct MPTDestroy std::optional err = std::nullopt; }; +/** @brief Arguments for building an MPTokenAuthorize test transaction. */ struct MPTAuthorize { std::optional account = std::nullopt; @@ -142,6 +182,7 @@ struct MPTAuthorize std::optional err = std::nullopt; }; +/** @brief Arguments for building an MPTokenIssuanceSet test transaction. */ struct MPTSet { std::optional account = std::nullopt; @@ -155,18 +196,215 @@ struct MPTSet std::optional metadata = std::nullopt; std::optional delegate = std::nullopt; std::optional domainID = std::nullopt; + std::optional issuerPubKey = std::nullopt; + std::optional auditorPubKey = std::nullopt; + std::optional ticketSeq = std::nullopt; std::optional err = std::nullopt; }; +/** @brief Arguments for building a ConfidentialMPTConvert test transaction. */ +struct MPTConvert +{ + std::optional account = std::nullopt; + std::optional id = std::nullopt; + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional fillAuditorEncryptedAmt = true; + // indicates whether to autofill schnorr proof. + // default : auto generate proof if holderPubKey is present. + // true: force proof generation. + // false: force proof omission. + std::optional fillSchnorrProof = std::nullopt; + std::optional holderPubKey = std::nullopt; + std::optional holderEncryptedAmt = std::nullopt; + std::optional issuerEncryptedAmt = std::nullopt; + std::optional auditorEncryptedAmt = std::nullopt; + + std::optional blindingFactor = std::nullopt; + std::optional delegate = std::nullopt; + std::optional ticketSeq = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional fee = std::nullopt; + std::optional err = std::nullopt; +}; + +/** @brief Arguments for building a ConfidentialMPTMergeInbox test transaction. */ +struct MPTMergeInbox +{ + std::optional account = std::nullopt; + std::optional id = std::nullopt; + std::optional delegate = std::nullopt; + std::optional ticketSeq = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional fee = std::nullopt; + std::optional err = std::nullopt; +}; + +/** @brief Arguments for building a ConfidentialMPTSend test transaction. */ +struct MPTConfidentialSend +{ + std::optional account = std::nullopt; + std::optional dest = std::nullopt; + std::optional id = std::nullopt; + // amt is to generate encrypted amounts for testing purposes + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional senderEncryptedAmt = std::nullopt; + std::optional destEncryptedAmt = std::nullopt; + std::optional issuerEncryptedAmt = std::nullopt; + std::optional auditorEncryptedAmt = std::nullopt; + std::optional fillAuditorEncryptedAmt = true; + std::optional> credentials = std::nullopt; + // not an txn param, only used for autofilling + std::optional blindingFactor = std::nullopt; + std::optional amountCommitment = std::nullopt; + std::optional balanceCommitment = std::nullopt; + std::optional delegate = std::nullopt; + std::optional destinationTag = std::nullopt; + std::optional ticketSeq = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional fee = std::nullopt; + std::optional err = std::nullopt; +}; + +/** @brief Arguments for building a ConfidentialMPTConvertBack test transaction. */ +struct MPTConvertBack +{ + std::optional account = std::nullopt; + std::optional id = std::nullopt; + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional holderEncryptedAmt = std::nullopt; + std::optional issuerEncryptedAmt = std::nullopt; + std::optional auditorEncryptedAmt = std::nullopt; + std::optional fillAuditorEncryptedAmt = true; + // not an txn param, only used for autofilling + std::optional blindingFactor = std::nullopt; + std::optional pedersenCommitment = std::nullopt; + std::optional delegate = std::nullopt; + std::optional ticketSeq = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional fee = std::nullopt; + std::optional err = std::nullopt; +}; + +/** @brief Arguments for building a ConfidentialMPTClawback test transaction. */ +struct MPTConfidentialClawback +{ + std::optional account = std::nullopt; + std::optional holder = std::nullopt; + std::optional id = std::nullopt; + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional delegate = std::nullopt; + std::optional ticketSeq = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional fee = std::nullopt; + std::optional err = std::nullopt; +}; + +/** + * @brief Stores the parameters that are exclusively used to generate a + * Pedersen linkage proof. + */ +struct PedersenProofParams +{ + /** @brief The Pedersen commitment used by the proof. */ + Buffer const pedersenCommitment; + + /** @brief Either the spending balance or the value being transferred. */ + uint64_t const amt; + + /** @brief The encrypted amount linked to the Pedersen commitment. */ + Buffer const encryptedAmt; + + /** @brief The blinding factor used to create the Pedersen commitment. */ + Buffer const blindingFactor; +}; + +/** + * @brief When building multiple confidential sends from the same account inside a + * single batch transaction, pass this state to the transaction builder for + * each subsequent send so that its proof references the post previous-send + * encrypted balance rather than the stale pre-send ledger state. + * + * The fields mirror what the ledger will contain after the preceding send's + * doApply() completes: + * encSpending = homomorphicSubtract(prevEncSpending, senderEncAmt) + * version = prevVersion + 1 + */ +struct ConfidentialSendChainState +{ + /** @brief Decrypted spending balance after the previous send. */ + std::uint64_t spending; + + /** @brief Encrypted spending balance after the previous send. */ + Buffer encSpending; + + /** @brief sfConfidentialBalanceVersion after the previous send. */ + std::uint32_t version; +}; + +/** + * @brief Use this when building a second (or later) confidential send from the same + * account in the same batch. Pass the state to the chain aware + * transaction builder so that the next proof is constructed against the + * correct post-send encrypted balance and version. + * + * @param currentSpending Decrypted spending balance before the send. + * @param currentEncSpending sfConfidentialBalanceSpending before the send. + * @param currentVersion sfConfidentialBalanceVersion before the send. + * @param sendAmt Plaintext amount being sent. + * @param senderEncAmt sfSenderEncryptedAmount from the send transaction. + * @return The predicted chain state, or std::nullopt if homomorphic + * subtraction fails. + */ +std::optional +computeNextSendChainState( + std::uint64_t currentSpending, + Slice const& currentEncSpending, + std::uint32_t currentVersion, + std::uint64_t sendAmt, + Slice const& senderEncAmt); + +/** + * @brief Test helper for creating, mutating, and asserting MPT and confidential + * MPT ledger state. + */ class MPTTester { Env& env_; Account const issuer_; std::unordered_map const holders_; + std::optional const auditor_; std::optional id_; bool close_; + std::unordered_map pubKeys_; + std::unordered_map privKeys_; public: + enum class EncryptedBalanceType { + IssuerEncryptedBalance, + HolderEncryptedInbox, + HolderEncryptedSpending, + AuditorEncryptedBalance, + }; + + static constexpr auto issuerEncryptedBalance = EncryptedBalanceType::IssuerEncryptedBalance; + static constexpr auto holderEncryptedInbox = EncryptedBalanceType::HolderEncryptedInbox; + static constexpr auto holderEncryptedSpending = EncryptedBalanceType::HolderEncryptedSpending; + static constexpr auto auditorEncryptedBalance = EncryptedBalanceType::AuditorEncryptedBalance; + MPTTester(Env& env, Account issuer, MPTInit const& constr = {}); MPTTester(MPTInitDef const& constr); MPTTester( @@ -204,6 +442,83 @@ public: static json::Value setJV(MPTSet const& set = {}); + void + convert(MPTConvert const& arg = MPTConvert{}); + + /** + * @brief Build a confidential convert JV without submitting it. + * + * @param arg Transaction builder arguments. + * @param seq Inner transaction sequence used in the Schnorr proof context + * hash. + * @return The transaction JSON object. + */ + json::Value + convertJV(MPTConvert const& arg, std::uint32_t seq); + + void + mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{}); + + [[nodiscard]] json::Value + mergeInboxJV(MPTMergeInbox const& arg = MPTMergeInbox{}) const; + + void + send(MPTConfidentialSend const& arg = MPTConfidentialSend{}); + + /** + * @brief Build a confidential send JV. + * + * When chain is provided, the sender's proof parameters are taken from it + * instead of the ledger, enabling proof generation for a second or later + * send from the same account inside a single batch. + * + * @param arg Transaction builder arguments. + * @param seq Inner transaction sequence used in the proof context hash. + * @param chain Optional predicted sender state from a previous batched + * send. + * @return The transaction JSON object. + */ + json::Value + sendJV( + MPTConfidentialSend const& arg, + std::uint32_t seq, + std::optional chain = std::nullopt); + + /** + * @brief Compute the projected sender state after a confidential send in a + * batch. + * + * Each confidential send requires a ZK proof that the sender's spending + * balance covers the transfer. In a batch, the second and later sends from + * the same sender need proofs built against the updated spending balance. + * + * @param sender The sender whose post-send state is being predicted. + * @param sendAmt The plaintext amount sent by the transaction. + * @param jv The confidential send transaction JSON object. + * @return The predicted sender state after applying the send. + */ + [[nodiscard]] ConfidentialSendChainState + chainAfterSend(Account const& sender, std::uint64_t sendAmt, json::Value const& jv) const; + + void + convertBack(MPTConvertBack const& arg = MPTConvertBack{}); + + /** + * @brief Build a confidential convertBack JV without submitting it. + * + * Reads the current encrypted spending balance and version from the ledger, + * so call this before the batch is submitted. + * + * @param arg Transaction builder arguments. + * @param seq Inner transaction sequence used in the proof context hash. + * @return The transaction JSON object. + */ + json::Value + convertBackJV(MPTConvertBack const& arg, std::uint32_t seq); + + void + confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{}); + [[nodiscard]] bool checkDomainID(std::optional expected) const; @@ -213,6 +528,9 @@ public: [[nodiscard]] bool checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const; + [[nodiscard]] bool + checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const; + [[nodiscard]] bool checkFlags(uint32_t const expectedFlags, std::optional const& holder = std::nullopt) const; @@ -265,6 +583,13 @@ public: [[nodiscard]] std::int64_t getBalance(Account const& account) const; + [[nodiscard]] std::int64_t + getIssuanceConfidentialBalance() const; + + [[nodiscard]] std::optional + getEncryptedBalance(Account const& account, EncryptedBalanceType option = holderEncryptedInbox) + const; + MPT operator[](std::string const& name) const; @@ -273,6 +598,60 @@ public: operator Asset() const; + void + generateKeyPair(Account const& account); + + [[nodiscard]] std::optional + getPubKey(Account const& account) const; + + [[nodiscard]] std::optional + getPrivKey(Account const& account) const; + + [[nodiscard]] Buffer + encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const; + + [[nodiscard]] std::optional + decryptAmount(Account const& account, Buffer const& amt) const; + + [[nodiscard]] std::optional + getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const; + + [[nodiscard]] std::optional + getIssuanceOutstandingBalance() const; + + [[nodiscard]] std::optional + getClawbackProof( + Account const& holder, + std::uint64_t amount, + Buffer const& privateKey, + uint256 const& txHash) const; + + [[nodiscard]] std::optional + getSchnorrProof(Account const& account, uint256 const& ctxHash) const; + + [[nodiscard]] std::optional + getConfidentialSendProof( + Account const& sender, + std::uint64_t const amount, + std::vector const& recipients, + Slice const& blindingFactor, + uint256 const& contextHash, + PedersenProofParams const& amountParams, + PedersenProofParams const& balanceParams) const; + + [[nodiscard]] Buffer + getConvertBackProof( + Account const& holder, + std::uint64_t const amount, + uint256 const& contextHash, + PedersenProofParams const& pcParams) const; + + [[nodiscard]] std::uint32_t + getMPTokenVersion(Account const account) const; + + static Buffer + getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor); + friend BookSpec operator~(MPTTester const& mpt) { @@ -288,9 +667,54 @@ private: template TER - submit(A const& arg, json::Value const& jv) + submit(A const& arg, json::Value jv) { - env_(jv, Txflags(arg.flags.value_or(0)), Ter(arg.err.value_or(tesSUCCESS))); + auto const expectedFlags = Txflags(arg.flags.value_or(0)); + auto const expectedTer = Ter(arg.err.value_or(tesSUCCESS)); + + if constexpr (requires { arg.fee; }) + { + if (arg.fee) + jv[jss::Fee] = to_string(*arg.fee); + } + + std::optional ticketSeq; + if constexpr (requires { arg.ticketSeq; }) + ticketSeq = arg.ticketSeq; + + std::optional delegateAcct; + if constexpr (requires { arg.delegate; }) + delegateAcct = arg.delegate; + + std::optional dstTag; + if constexpr (requires { arg.destinationTag; }) + dstTag = arg.destinationTag; + + if (ticketSeq && delegateAcct) + { + env_( + jv, + expectedFlags, + expectedTer, + ticket::Use(*ticketSeq), + delegate::As(*delegateAcct)); + } + else if (ticketSeq) + { + env_(jv, expectedFlags, expectedTer, ticket::Use(*ticketSeq)); + } + else if (delegateAcct) + { + env_(jv, expectedFlags, expectedTer, delegate::As(*delegateAcct)); + } + else if (dstTag) + { + env_(jv, expectedFlags, expectedTer, Dtag(*dstTag)); + } + else + { + env_(jv, expectedFlags, expectedTer); + } auto const err = env_.ter(); if (close_) env_.close(); @@ -309,6 +733,16 @@ private: [[nodiscard]] std::uint32_t getFlags(std::optional const& holder) const; + + template + void + fillConversionCiphertexts( + T const& arg, + json::Value& jv, + Buffer& holderCiphertext, + Buffer& issuerCiphertext, + std::optional& auditorCiphertext, + Buffer& blindingFactor) const; }; } // namespace xrpl::test::jtx diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp index 7479f0c63c..8dc5960ee0 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp @@ -34,6 +34,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) auto const domainIDValue = canonical_UINT256(); auto const mutableFlagsValue = canonical_UINT32(); auto const referenceHoldingValue = canonical_UINT256(); + auto const issuerEncryptionKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const confidentialOutstandingAmountValue = canonical_UINT64(); MPTokenIssuanceBuilder builder{ issuerValue, @@ -52,6 +55,9 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) builder.setDomainID(domainIDValue); builder.setMutableFlags(mutableFlagsValue); builder.setReferenceHolding(referenceHoldingValue); + builder.setIssuerEncryptionKey(issuerEncryptionKeyValue); + builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); + builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -162,6 +168,30 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasReferenceHolding()); } + { + auto const& expected = issuerEncryptionKeyValue; + auto const actualOpt = entry.getIssuerEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey"); + EXPECT_TRUE(entry.hasIssuerEncryptionKey()); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = entry.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + EXPECT_TRUE(entry.hasAuditorEncryptionKey()); + } + + { + auto const& expected = confidentialOutstandingAmountValue; + auto const actualOpt = entry.getConfidentialOutstandingAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfConfidentialOutstandingAmount"); + EXPECT_TRUE(entry.hasConfidentialOutstandingAmount()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -189,6 +219,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) auto const domainIDValue = canonical_UINT256(); auto const mutableFlagsValue = canonical_UINT32(); auto const referenceHoldingValue = canonical_UINT256(); + auto const issuerEncryptionKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const confidentialOutstandingAmountValue = canonical_UINT64(); auto sle = std::make_shared(MPTokenIssuance::entryType, index); @@ -206,6 +239,9 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) sle->at(sfDomainID) = domainIDValue; sle->at(sfMutableFlags) = mutableFlagsValue; sle->at(sfReferenceHolding) = referenceHoldingValue; + sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue; + sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue; + sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue; MPTokenIssuanceBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -380,6 +416,45 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfReferenceHolding"); } + { + auto const& expected = issuerEncryptionKeyValue; + + auto const fromSleOpt = entryFromSle.getIssuerEncryptionKey(); + auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptionKey(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptionKey"); + expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptionKey"); + } + + { + auto const& expected = auditorEncryptionKeyValue; + + auto const fromSleOpt = entryFromSle.getAuditorEncryptionKey(); + auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptionKey(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptionKey"); + expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey"); + } + + { + auto const& expected = confidentialOutstandingAmountValue; + + auto const fromSleOpt = entryFromSle.getConfidentialOutstandingAmount(); + auto const fromBuilderOpt = entryFromBuilder.getConfidentialOutstandingAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfConfidentialOutstandingAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfConfidentialOutstandingAmount"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -460,5 +535,11 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getMutableFlags().has_value()); EXPECT_FALSE(entry.hasReferenceHolding()); EXPECT_FALSE(entry.getReferenceHolding().has_value()); + EXPECT_FALSE(entry.hasIssuerEncryptionKey()); + EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value()); + EXPECT_FALSE(entry.hasAuditorEncryptionKey()); + EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value()); + EXPECT_FALSE(entry.hasConfidentialOutstandingAmount()); + EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp index c104e7b365..7db4b638a7 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp @@ -27,6 +27,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) auto const ownerNodeValue = canonical_UINT64(); auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const confidentialBalanceInboxValue = canonical_VL(); + auto const confidentialBalanceSpendingValue = canonical_VL(); + auto const confidentialBalanceVersionValue = canonical_UINT32(); + auto const issuerEncryptedBalanceValue = canonical_VL(); + auto const auditorEncryptedBalanceValue = canonical_VL(); + auto const holderEncryptionKeyValue = canonical_VL(); MPTokenBuilder builder{ accountValue, @@ -38,6 +44,12 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) builder.setMPTAmount(mPTAmountValue); builder.setLockedAmount(lockedAmountValue); + builder.setConfidentialBalanceInbox(confidentialBalanceInboxValue); + builder.setConfidentialBalanceSpending(confidentialBalanceSpendingValue); + builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue); + builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue); + builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue); + builder.setHolderEncryptionKey(holderEncryptionKeyValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -94,6 +106,54 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasLockedAmount()); } + { + auto const& expected = confidentialBalanceInboxValue; + auto const actualOpt = entry.getConfidentialBalanceInbox(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfConfidentialBalanceInbox"); + EXPECT_TRUE(entry.hasConfidentialBalanceInbox()); + } + + { + auto const& expected = confidentialBalanceSpendingValue; + auto const actualOpt = entry.getConfidentialBalanceSpending(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfConfidentialBalanceSpending"); + EXPECT_TRUE(entry.hasConfidentialBalanceSpending()); + } + + { + auto const& expected = confidentialBalanceVersionValue; + auto const actualOpt = entry.getConfidentialBalanceVersion(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfConfidentialBalanceVersion"); + EXPECT_TRUE(entry.hasConfidentialBalanceVersion()); + } + + { + auto const& expected = issuerEncryptedBalanceValue; + auto const actualOpt = entry.getIssuerEncryptedBalance(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfIssuerEncryptedBalance"); + EXPECT_TRUE(entry.hasIssuerEncryptedBalance()); + } + + { + auto const& expected = auditorEncryptedBalanceValue; + auto const actualOpt = entry.getAuditorEncryptedBalance(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedBalance"); + EXPECT_TRUE(entry.hasAuditorEncryptedBalance()); + } + + { + auto const& expected = holderEncryptionKeyValue; + auto const actualOpt = entry.getHolderEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey"); + EXPECT_TRUE(entry.hasHolderEncryptionKey()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -114,6 +174,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) auto const ownerNodeValue = canonical_UINT64(); auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); + auto const confidentialBalanceInboxValue = canonical_VL(); + auto const confidentialBalanceSpendingValue = canonical_VL(); + auto const confidentialBalanceVersionValue = canonical_UINT32(); + auto const issuerEncryptedBalanceValue = canonical_VL(); + auto const auditorEncryptedBalanceValue = canonical_VL(); + auto const holderEncryptionKeyValue = canonical_VL(); auto sle = std::make_shared(MPToken::entryType, index); @@ -124,6 +190,12 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) sle->at(sfOwnerNode) = ownerNodeValue; sle->at(sfPreviousTxnID) = previousTxnIDValue; sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + sle->at(sfConfidentialBalanceInbox) = confidentialBalanceInboxValue; + sle->at(sfConfidentialBalanceSpending) = confidentialBalanceSpendingValue; + sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue; + sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue; + sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue; + sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue; MPTokenBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -210,6 +282,84 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfLockedAmount"); } + { + auto const& expected = confidentialBalanceInboxValue; + + auto const fromSleOpt = entryFromSle.getConfidentialBalanceInbox(); + auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceInbox(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceInbox"); + expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceInbox"); + } + + { + auto const& expected = confidentialBalanceSpendingValue; + + auto const fromSleOpt = entryFromSle.getConfidentialBalanceSpending(); + auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceSpending(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceSpending"); + expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceSpending"); + } + + { + auto const& expected = confidentialBalanceVersionValue; + + auto const fromSleOpt = entryFromSle.getConfidentialBalanceVersion(); + auto const fromBuilderOpt = entryFromBuilder.getConfidentialBalanceVersion(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfConfidentialBalanceVersion"); + expectEqualField(expected, *fromBuilderOpt, "sfConfidentialBalanceVersion"); + } + + { + auto const& expected = issuerEncryptedBalanceValue; + + auto const fromSleOpt = entryFromSle.getIssuerEncryptedBalance(); + auto const fromBuilderOpt = entryFromBuilder.getIssuerEncryptedBalance(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfIssuerEncryptedBalance"); + expectEqualField(expected, *fromBuilderOpt, "sfIssuerEncryptedBalance"); + } + + { + auto const& expected = auditorEncryptedBalanceValue; + + auto const fromSleOpt = entryFromSle.getAuditorEncryptedBalance(); + auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptedBalance(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptedBalance"); + expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance"); + } + + { + auto const& expected = holderEncryptionKeyValue; + + auto const fromSleOpt = entryFromSle.getHolderEncryptionKey(); + auto const fromBuilderOpt = entryFromBuilder.getHolderEncryptionKey(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfHolderEncryptionKey"); + expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -276,5 +426,17 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getMPTAmount().has_value()); EXPECT_FALSE(entry.hasLockedAmount()); EXPECT_FALSE(entry.getLockedAmount().has_value()); + EXPECT_FALSE(entry.hasConfidentialBalanceInbox()); + EXPECT_FALSE(entry.getConfidentialBalanceInbox().has_value()); + EXPECT_FALSE(entry.hasConfidentialBalanceSpending()); + EXPECT_FALSE(entry.getConfidentialBalanceSpending().has_value()); + EXPECT_FALSE(entry.hasConfidentialBalanceVersion()); + EXPECT_FALSE(entry.getConfidentialBalanceVersion().has_value()); + EXPECT_FALSE(entry.hasIssuerEncryptedBalance()); + EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value()); + EXPECT_FALSE(entry.hasAuditorEncryptedBalance()); + EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value()); + EXPECT_FALSE(entry.hasHolderEncryptionKey()); + EXPECT_FALSE(entry.getHolderEncryptionKey().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp new file mode 100644 index 0000000000..611bc18465 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTClawbackTests.cpp @@ -0,0 +1,194 @@ +// Auto-generated unit tests for transaction ConfidentialMPTClawback + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTClawbackTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawback")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const holderValue = canonical_ACCOUNT(); + auto const mPTAmountValue = canonical_UINT64(); + auto const zKProofValue = canonical_VL(); + + ConfidentialMPTClawbackBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + holderValue, + mPTAmountValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = holderValue; + auto const actual = tx.getHolder(); + expectEqualField(expected, actual, "sfHolder"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = tx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTClawbackTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTClawbackFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const holderValue = canonical_ACCOUNT(); + auto const mPTAmountValue = canonical_UINT64(); + auto const zKProofValue = canonical_VL(); + + // Build an initial transaction + ConfidentialMPTClawbackBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + holderValue, + mPTAmountValue, + zKProofValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTClawbackBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = holderValue; + auto const actual = rebuiltTx.getHolder(); + expectEqualField(expected, actual, "sfHolder"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = rebuiltTx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTClawbackTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTClawback{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTClawbackTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTClawbackBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp new file mode 100644 index 0000000000..1a5fb3a046 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertBackTests.cpp @@ -0,0 +1,303 @@ +// Auto-generated unit tests for transaction ConfidentialMPTConvertBack + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTConvertBackTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBack")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const zKProofValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + + ConfidentialMPTConvertBackBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + zKProofValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = tx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = holderEncryptedAmountValue; + auto const actual = tx.getHolderEncryptedAmount(); + expectEqualField(expected, actual, "sfHolderEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = tx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = tx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = balanceCommitmentValue; + auto const actual = tx.getBalanceCommitment(); + expectEqualField(expected, actual, "sfBalanceCommitment"); + } + + // Verify optional fields + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = tx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + EXPECT_TRUE(tx.hasAuditorEncryptedAmount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTConvertBackTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const zKProofValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + + // Build an initial transaction + ConfidentialMPTConvertBackBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + zKProofValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTConvertBackBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = rebuiltTx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = holderEncryptedAmountValue; + auto const actual = rebuiltTx.getHolderEncryptedAmount(); + expectEqualField(expected, actual, "sfHolderEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = rebuiltTx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = rebuiltTx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = balanceCommitmentValue; + auto const actual = rebuiltTx.getBalanceCommitment(); + expectEqualField(expected, actual, "sfBalanceCommitment"); + } + + // Verify optional fields + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTConvertBackTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTConvertBack{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTConvertBackTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTConvertBackBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsConfidentialMPTConvertBackTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertBackNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const zKProofValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + + ConfidentialMPTConvertBackBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + zKProofValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasAuditorEncryptedAmount()); + EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp new file mode 100644 index 0000000000..6110196353 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTConvertTests.cpp @@ -0,0 +1,309 @@ +// Auto-generated unit tests for transaction ConfidentialMPTConvert + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTConvertTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvert")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptionKeyValue = canonical_VL(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const zKProofValue = canonical_VL(); + + ConfidentialMPTConvertBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setHolderEncryptionKey(holderEncryptionKeyValue); + builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + builder.setZKProof(zKProofValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = tx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = holderEncryptedAmountValue; + auto const actual = tx.getHolderEncryptedAmount(); + expectEqualField(expected, actual, "sfHolderEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = tx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = tx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + // Verify optional fields + { + auto const& expected = holderEncryptionKeyValue; + auto const actualOpt = tx.getHolderEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey"); + EXPECT_TRUE(tx.hasHolderEncryptionKey()); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = tx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + EXPECT_TRUE(tx.hasAuditorEncryptedAmount()); + } + + { + auto const& expected = zKProofValue; + auto const actualOpt = tx.getZKProof(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present"; + expectEqualField(expected, *actualOpt, "sfZKProof"); + EXPECT_TRUE(tx.hasZKProof()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTConvertTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptionKeyValue = canonical_VL(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const zKProofValue = canonical_VL(); + + // Build an initial transaction + ConfidentialMPTConvertBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + initialBuilder.setHolderEncryptionKey(holderEncryptionKeyValue); + initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + initialBuilder.setZKProof(zKProofValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTConvertBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = mPTAmountValue; + auto const actual = rebuiltTx.getMPTAmount(); + expectEqualField(expected, actual, "sfMPTAmount"); + } + + { + auto const& expected = holderEncryptedAmountValue; + auto const actual = rebuiltTx.getHolderEncryptedAmount(); + expectEqualField(expected, actual, "sfHolderEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = rebuiltTx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = rebuiltTx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + // Verify optional fields + { + auto const& expected = holderEncryptionKeyValue; + auto const actualOpt = rebuiltTx.getHolderEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolderEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfHolderEncryptionKey"); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + } + + { + auto const& expected = zKProofValue; + auto const actualOpt = rebuiltTx.getZKProof(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfZKProof should be present"; + expectEqualField(expected, *actualOpt, "sfZKProof"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTConvertTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTConvert{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTConvertTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTConvertBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsConfidentialMPTConvertTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTConvertNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const mPTAmountValue = canonical_UINT64(); + auto const holderEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + + ConfidentialMPTConvertBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + mPTAmountValue, + holderEncryptedAmountValue, + issuerEncryptedAmountValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasHolderEncryptionKey()); + EXPECT_FALSE(tx.getHolderEncryptionKey().has_value()); + EXPECT_FALSE(tx.hasAuditorEncryptedAmount()); + EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value()); + EXPECT_FALSE(tx.hasZKProof()); + EXPECT_FALSE(tx.getZKProof().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp new file mode 100644 index 0000000000..bfc3ec4f0d --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMergeInboxTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction ConfidentialMPTMergeInbox + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInbox")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + + ConfidentialMPTMergeInboxBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMergeInboxFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + + // Build an initial transaction + ConfidentialMPTMergeInboxBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTMergeInboxBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTMergeInboxTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTMergeInbox{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTMergeInboxTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTMergeInboxBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp new file mode 100644 index 0000000000..ca7793dece --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTSendTests.cpp @@ -0,0 +1,363 @@ +// Auto-generated unit tests for transaction ConfidentialMPTSend + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSend")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const destinationValue = canonical_ACCOUNT(); + auto const destinationTagValue = canonical_UINT32(); + auto const senderEncryptedAmountValue = canonical_VL(); + auto const destinationEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const zKProofValue = canonical_VL(); + auto const amountCommitmentValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + auto const credentialIDsValue = canonical_VECTOR256(); + + ConfidentialMPTSendBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + destinationValue, + senderEncryptedAmountValue, + destinationEncryptedAmountValue, + issuerEncryptedAmountValue, + zKProofValue, + amountCommitmentValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setDestinationTag(destinationTagValue); + builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + builder.setCredentialIDs(credentialIDsValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = destinationValue; + auto const actual = tx.getDestination(); + expectEqualField(expected, actual, "sfDestination"); + } + + { + auto const& expected = senderEncryptedAmountValue; + auto const actual = tx.getSenderEncryptedAmount(); + expectEqualField(expected, actual, "sfSenderEncryptedAmount"); + } + + { + auto const& expected = destinationEncryptedAmountValue; + auto const actual = tx.getDestinationEncryptedAmount(); + expectEqualField(expected, actual, "sfDestinationEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = tx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = amountCommitmentValue; + auto const actual = tx.getAmountCommitment(); + expectEqualField(expected, actual, "sfAmountCommitment"); + } + + { + auto const& expected = balanceCommitmentValue; + auto const actual = tx.getBalanceCommitment(); + expectEqualField(expected, actual, "sfBalanceCommitment"); + } + + // Verify optional fields + { + auto const& expected = destinationTagValue; + auto const actualOpt = tx.getDestinationTag(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; + expectEqualField(expected, *actualOpt, "sfDestinationTag"); + EXPECT_TRUE(tx.hasDestinationTag()); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = tx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + EXPECT_TRUE(tx.hasAuditorEncryptedAmount()); + } + + { + auto const& expected = credentialIDsValue; + auto const actualOpt = tx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + EXPECT_TRUE(tx.hasCredentialIDs()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const destinationValue = canonical_ACCOUNT(); + auto const destinationTagValue = canonical_UINT32(); + auto const senderEncryptedAmountValue = canonical_VL(); + auto const destinationEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const zKProofValue = canonical_VL(); + auto const amountCommitmentValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + auto const credentialIDsValue = canonical_VECTOR256(); + + // Build an initial transaction + ConfidentialMPTSendBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + destinationValue, + senderEncryptedAmountValue, + destinationEncryptedAmountValue, + issuerEncryptedAmountValue, + zKProofValue, + amountCommitmentValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + initialBuilder.setDestinationTag(destinationTagValue); + initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + initialBuilder.setCredentialIDs(credentialIDsValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTSendBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = destinationValue; + auto const actual = rebuiltTx.getDestination(); + expectEqualField(expected, actual, "sfDestination"); + } + + { + auto const& expected = senderEncryptedAmountValue; + auto const actual = rebuiltTx.getSenderEncryptedAmount(); + expectEqualField(expected, actual, "sfSenderEncryptedAmount"); + } + + { + auto const& expected = destinationEncryptedAmountValue; + auto const actual = rebuiltTx.getDestinationEncryptedAmount(); + expectEqualField(expected, actual, "sfDestinationEncryptedAmount"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actual = rebuiltTx.getIssuerEncryptedAmount(); + expectEqualField(expected, actual, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = amountCommitmentValue; + auto const actual = rebuiltTx.getAmountCommitment(); + expectEqualField(expected, actual, "sfAmountCommitment"); + } + + { + auto const& expected = balanceCommitmentValue; + auto const actual = rebuiltTx.getBalanceCommitment(); + expectEqualField(expected, actual, "sfBalanceCommitment"); + } + + // Verify optional fields + { + auto const& expected = destinationTagValue; + auto const actualOpt = rebuiltTx.getDestinationTag(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; + expectEqualField(expected, *actualOpt, "sfDestinationTag"); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + } + + { + auto const& expected = credentialIDsValue; + auto const actualOpt = rebuiltTx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTSendTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTSend{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTSendTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTSendBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsConfidentialMPTSendTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTSendNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const destinationValue = canonical_ACCOUNT(); + auto const senderEncryptedAmountValue = canonical_VL(); + auto const destinationEncryptedAmountValue = canonical_VL(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const zKProofValue = canonical_VL(); + auto const amountCommitmentValue = canonical_VL(); + auto const balanceCommitmentValue = canonical_VL(); + + ConfidentialMPTSendBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + destinationValue, + senderEncryptedAmountValue, + destinationEncryptedAmountValue, + issuerEncryptedAmountValue, + zKProofValue, + amountCommitmentValue, + balanceCommitmentValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasDestinationTag()); + EXPECT_FALSE(tx.getDestinationTag().has_value()); + EXPECT_FALSE(tx.hasAuditorEncryptedAmount()); + EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value()); + EXPECT_FALSE(tx.hasCredentialIDs()); + EXPECT_FALSE(tx.getCredentialIDs().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp index 5c97ae8871..e7b34590b2 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp @@ -35,6 +35,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) auto const mPTokenMetadataValue = canonical_VL(); auto const transferFeeValue = canonical_UINT16(); auto const mutableFlagsValue = canonical_UINT32(); + auto const issuerEncryptionKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); MPTokenIssuanceSetBuilder builder{ accountValue, @@ -49,6 +51,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) builder.setMPTokenMetadata(mPTokenMetadataValue); builder.setTransferFee(transferFeeValue); builder.setMutableFlags(mutableFlagsValue); + builder.setIssuerEncryptionKey(issuerEncryptionKeyValue); + builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); auto tx = builder.build(publicKey, secretKey); @@ -112,6 +116,22 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasMutableFlags()); } + { + auto const& expected = issuerEncryptionKeyValue; + auto const actualOpt = tx.getIssuerEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey"); + EXPECT_TRUE(tx.hasIssuerEncryptionKey()); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = tx.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + EXPECT_TRUE(tx.hasAuditorEncryptionKey()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -134,6 +154,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) auto const mPTokenMetadataValue = canonical_VL(); auto const transferFeeValue = canonical_UINT16(); auto const mutableFlagsValue = canonical_UINT32(); + auto const issuerEncryptionKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); // Build an initial transaction MPTokenIssuanceSetBuilder initialBuilder{ @@ -148,6 +170,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) initialBuilder.setMPTokenMetadata(mPTokenMetadataValue); initialBuilder.setTransferFee(transferFeeValue); initialBuilder.setMutableFlags(mutableFlagsValue); + initialBuilder.setIssuerEncryptionKey(issuerEncryptionKeyValue); + initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -207,6 +231,20 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfMutableFlags"); } + { + auto const& expected = issuerEncryptionKeyValue; + auto const actualOpt = rebuiltTx.getIssuerEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfIssuerEncryptionKey"); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -274,6 +312,10 @@ TEST(TransactionsMPTokenIssuanceSetTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getTransferFee().has_value()); EXPECT_FALSE(tx.hasMutableFlags()); EXPECT_FALSE(tx.getMutableFlags().has_value()); + EXPECT_FALSE(tx.hasIssuerEncryptionKey()); + EXPECT_FALSE(tx.getIssuerEncryptionKey().has_value()); + EXPECT_FALSE(tx.hasAuditorEncryptionKey()); + EXPECT_FALSE(tx.getAuditorEncryptionKey().has_value()); } } From 74b55a59b2c643929cba2352eef01c25be5ed479 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 29 Jun 2026 14:20:17 +0100 Subject: [PATCH 05/31] chore: Enable most bugprone checks (#7643) --- .clang-tidy | 18 ----- include/xrpl/config/BasicConfig.h | 3 +- include/xrpl/protocol/detail/token_errors.h | 1 - src/libxrpl/conditions/Condition.cpp | 9 --- src/libxrpl/conditions/Fulfillment.cpp | 12 --- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 7 +- .../nodestore/backend/RocksDBFactory.cpp | 8 +- .../tx/transactors/dex/AMMWithdraw.cpp | 7 +- src/test/app/AccountSet_test.cpp | 1 + src/test/app/LoanBroker_test.cpp | 9 ++- src/test/app/MPToken_test.cpp | 15 +--- src/test/app/NFTokenBurn_test.cpp | 1 + src/test/app/Vault_test.cpp | 3 + src/test/app/XChain_test.cpp | 10 +-- src/test/consensus/LedgerTrie_test.cpp | 1 + src/test/csf/Sim.h | 1 + src/test/nodestore/TestBase.h | 2 - src/test/rpc/LedgerEntry_test.cpp | 2 - src/test/unit_test/SuiteJournal.h | 3 +- src/tests/libxrpl/tx/AccountSet.cpp | 1 + src/xrpld/core/detail/Config.cpp | 10 +-- src/xrpld/rpc/detail/PathRequestManager.cpp | 3 +- src/xrpld/rpc/detail/Pathfinder.cpp | 77 +++++++------------ 23 files changed, 61 insertions(+), 143 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ef55e8517c..e12c73cc56 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,29 +1,11 @@ --- Checks: "-*, bugprone-*, - -bugprone-assignment-in-if-condition, - -bugprone-bitwise-pointer-cast, - -bugprone-branch-clone, - -bugprone-command-processor, - -bugprone-copy-constructor-mutates-argument, - -bugprone-default-operator-new-on-overaligned-type, -bugprone-easily-swappable-parameters, - -bugprone-exception-copy-constructor-throws, -bugprone-exception-escape, - -bugprone-float-loop-counter, - -bugprone-forwarding-reference-overload, -bugprone-implicit-widening-of-multiplication-result, - -bugprone-incorrect-enable-shared-from-this, -bugprone-narrowing-conversions, - -bugprone-nondeterministic-pointer-iteration-order, - -bugprone-not-null-terminated-result, - -bugprone-random-generator-seed, - -bugprone-raw-memory-call-on-non-trivial-type, - -bugprone-std-namespace-modification, - -bugprone-tagged-union-member-count, -bugprone-throwing-static-initialization, - -bugprone-unchecked-string-to-number-conversion, - -bugprone-unintended-char-ostream-output, cppcoreguidelines-*, -cppcoreguidelines-avoid-c-arrays, diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h index 5a82f7d081..858bf8bf2e 100644 --- a/include/xrpl/config/BasicConfig.h +++ b/include/xrpl/config/BasicConfig.h @@ -298,7 +298,8 @@ set(T& target, std::string const& name, Section const& section) try { auto const val = section.get(name); - if ((foundAndValid = val.has_value())) + foundAndValid = val.has_value(); + if (foundAndValid) target = *val; } catch (boost::bad_lexical_cast const&) // NOLINT(bugprone-empty-catch) diff --git a/include/xrpl/protocol/detail/token_errors.h b/include/xrpl/protocol/detail/token_errors.h index 9d5e98e646..97db9288f9 100644 --- a/include/xrpl/protocol/detail/token_errors.h +++ b/include/xrpl/protocol/detail/token_errors.h @@ -58,7 +58,6 @@ public: case TokenCodecErrc::InvalidEncodingChar: return "invalid encoding char"; case TokenCodecErrc::Unknown: - return "unknown"; default: return "unknown"; } diff --git a/src/libxrpl/conditions/Condition.cpp b/src/libxrpl/conditions/Condition.cpp index cb950c9e24..3be7aa7757 100644 --- a/src/libxrpl/conditions/Condition.cpp +++ b/src/libxrpl/conditions/Condition.cpp @@ -190,17 +190,8 @@ Condition::deserialize(Slice s, std::error_code& ec) break; case 1: // PrefixSha256 - ec = Error::UnsupportedType; - return {}; - case 2: // ThresholdSha256 - ec = Error::UnsupportedType; - return {}; - case 3: // RsaSha256 - ec = Error::UnsupportedType; - return {}; - case 4: // Ed25519Sha256 ec = Error::UnsupportedType; return {}; diff --git a/src/libxrpl/conditions/Fulfillment.cpp b/src/libxrpl/conditions/Fulfillment.cpp index eaad6b8cdb..23a2a5eb32 100644 --- a/src/libxrpl/conditions/Fulfillment.cpp +++ b/src/libxrpl/conditions/Fulfillment.cpp @@ -101,20 +101,8 @@ Fulfillment::deserialize(Slice s, std::error_code& ec) break; case safeCast(Type::PrefixSha256): - ec = Error::UnsupportedType; - return {}; - break; - case safeCast(Type::ThresholdSha256): - ec = Error::UnsupportedType; - return {}; - break; - case safeCast(Type::RsaSha256): - ec = Error::UnsupportedType; - return {}; - break; - case safeCast(Type::Ed25519Sha256): ec = Error::UnsupportedType; return {}; diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 2ecde25754..7988e24a56 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -411,11 +411,8 @@ accountHolds( auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account)); - if (!sleMpt) - { - amount.clear(mptIssue); - } - else if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue)) + if (!sleMpt || + (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue))) { amount.clear(mptIssue); } diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index 565fbded5d..69a648f2f4 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -36,7 +36,6 @@ #include #include -#include #include #include #include @@ -286,7 +285,7 @@ public: Status status = Status::Ok; rocksdb::ReadOptions const options; - rocksdb::Slice const slice(std::bit_cast(hash.data()), keyBytes); + rocksdb::Slice const slice(reinterpret_cast(hash.data()), keyBytes); std::string string; @@ -349,8 +348,9 @@ public: EncodedBlob const encoded(e); wb.Put( - rocksdb::Slice(std::bit_cast(encoded.getKey()), keyBytes), - rocksdb::Slice(std::bit_cast(encoded.getData()), encoded.getSize())); + rocksdb::Slice(reinterpret_cast(encoded.getKey()), keyBytes), + rocksdb::Slice( + reinterpret_cast(encoded.getData()), encoded.getSize())); } rocksdb::WriteOptions const options; diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index f26164c5fe..14ed5a4646 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -90,12 +90,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx) if (lpTokens || amount || amount2 || ePrice) return temMALFORMED; } - else if (ctx.tx.isFlag(tfOneAssetWithdrawAll)) - { - if (!amount || lpTokens || amount2 || ePrice) - return temMALFORMED; - } - else if (ctx.tx.isFlag(tfSingleAsset)) + else if (ctx.tx.isFlag(tfOneAssetWithdrawAll) || ctx.tx.isFlag(tfSingleAsset)) { if (!amount || lpTokens || amount2 || ePrice) return temMALFORMED; diff --git a/src/test/app/AccountSet_test.cpp b/src/test/app/AccountSet_test.cpp index 6688e3693f..362113aec2 100644 --- a/src/test/app/AccountSet_test.cpp +++ b/src/test/app/AccountSet_test.cpp @@ -383,6 +383,7 @@ public: auto const usd = gw["USD"]; // Test gateway with a variety of allowed transfer rates + // NOLINTNEXTLINE(bugprone-float-loop-counter) for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125) { Env env(*this); diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index bdc950eeff..2a4de18a9c 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -283,7 +283,8 @@ class LoanBroker_test : public beast::unit_test::Suite [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) { using namespace jtx; - if (BEAST_EXPECT(broker = env.le(keylet))) + broker = env.le(keylet); + if (BEAST_EXPECT(broker)) { auto const amount = vault.asset(n); BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number()); @@ -473,7 +474,8 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); // Check the results of modifications - if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker) + broker = env.le(keylet); + if (BEAST_EXPECT(broker) && checkChangedBroker) checkChangedBroker(broker); // Verify that fields get removed when set to default values @@ -486,7 +488,8 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); // Check the updated fields - if (BEAST_EXPECT(broker = env.le(keylet))) + broker = env.le(keylet); + if (BEAST_EXPECT(broker)) { BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); BEAST_EXPECT(!broker->isFieldPresent(sfData)); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 15e17b4536..bbf1c54ab1 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -111,20 +111,9 @@ class MPToken_test : public beast::unit_test::Suite .metadata = "test", .err = temMALFORMED}); - if (!features[featureSingleAssetVault]) + if (!features[featureSingleAssetVault] || !features[featurePermissionedDomains]) { - // tries to set DomainID when SAV is disabled - mptAlice.create( - {.maxAmt = 100, - .assetScale = 0, - .metadata = "test", - .flags = tfMPTRequireAuth, - .domainID = uint256(42), - .err = temDISABLED}); - } - else if (!features[featurePermissionedDomains]) - { - // tries to set DomainID when PD is disabled + // tries to set DomainID when SAV or PD is disabled mptAlice.create( {.maxAmt = 100, .assetScale = 0, diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 00667dc5ac..140fe2de15 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -198,6 +198,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite // Use a default initialized mersenne_twister because we want the // effect of random numbers, but we want the test to run the same // way each time. + // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test std::mt19937 engine; std::uniform_int_distribution feeDist( decltype(kMaxTransferFee){}, kMaxTransferFee); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index dd28c7ec6e..9b29442197 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -7896,7 +7896,10 @@ class Vault_test : public beast::unit_test::Suite // Depositor deep freeze → self-withdraw blocked env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); + // TODO: branches are identical - confirm the intended pre/post-fix330 + // expectations and replace with the correct values (one branch may be wrong). env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + // NOLINTNEXTLINE(bugprone-branch-clone) Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN))); env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index de80444f2e..36c1a08144 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -2164,14 +2164,7 @@ struct XChain_test : public beast::unit_test::Suite, public jtx::XChainBridgeObj scAttester, jvb, mcAlice, amt, payees[i], true, claimID, dst, signers[i]); TER const expectedTER = i < quorum ? tesSUCCESS : TER{tecXCHAIN_NO_CLAIM_ID}; - if (i + 1 == quorum) - { - scEnv.tx(att, Ter(expectedTER)).close(); - } - else - { - scEnv.tx(att, Ter(expectedTER)).close(); - } + scEnv.tx(att, Ter(expectedTER)).close(); if (i + 1 < quorum) { @@ -4375,6 +4368,7 @@ public: { using namespace jtx; uint64_t time = 0; + // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test std::mt19937 gen(27); // Standard mersenne_twister_engine std::uniform_int_distribution distrib(0, 9); diff --git a/src/test/consensus/LedgerTrie_test.cpp b/src/test/consensus/LedgerTrie_test.cpp index 2b638b1dfa..0ddf1bf82f 100644 --- a/src/test/consensus/LedgerTrie_test.cpp +++ b/src/test/consensus/LedgerTrie_test.cpp @@ -663,6 +663,7 @@ class LedgerTrie_test : public beast::unit_test::Suite std::uint32_t const iterations = 10000; // Use explicit seed to have same results for CI + // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test std::mt19937 gen{42}; std::uniform_int_distribution<> depthDist(0, depthConst - 1); std::uniform_int_distribution<> widthDist(0, width - 1); diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h index 89e6b95fe6..b5b7f5f9c2 100644 --- a/src/test/csf/Sim.h +++ b/src/test/csf/Sim.h @@ -65,6 +65,7 @@ public: and no network connections. */ + // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test Sim() : sink{scheduler.clock()}, j{sink}, net{scheduler} { } diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index c35042bd4f..885c3bbeac 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -73,9 +73,7 @@ public: case 2: return NodeObjectType::TransactionNode; case 3: - return NodeObjectType::Unknown; default: - // will never happen, but make static analysis tool happy. return NodeObjectType::Unknown; } }(); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 14908cf72a..59dbf618f0 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -133,7 +133,6 @@ getTypeName(FieldType typeID) case FieldType::TwoAccountArrayField: return "length-2 array of Accounts"; case FieldType::UInt32Field: - return "number"; case FieldType::UInt64Field: return "number"; default: @@ -308,7 +307,6 @@ class LedgerEntry_test : public beast::unit_test::Suite case FieldType::TwoAccountArrayField: return kTwoAccountArray; case FieldType::UInt32Field: - return 1; case FieldType::UInt64Field: return 1; default: diff --git a/src/test/unit_test/SuiteJournal.h b/src/test/unit_test/SuiteJournal.h index bd54683031..174e9ff2ff 100644 --- a/src/test/unit_test/SuiteJournal.h +++ b/src/test/unit_test/SuiteJournal.h @@ -60,9 +60,8 @@ SuiteJournalSink::writeAlways(beast::Severity level, std::string const& text) return "WRN:"; case Severity::Error: return "ERR:"; - default: - break; case Severity::Fatal: + default: break; } return "FTL:"; diff --git a/src/tests/libxrpl/tx/AccountSet.cpp b/src/tests/libxrpl/tx/AccountSet.cpp index 46b298dde8..87d00c58bf 100644 --- a/src/tests/libxrpl/tx/AccountSet.cpp +++ b/src/tests/libxrpl/tx/AccountSet.cpp @@ -685,6 +685,7 @@ TEST(AccountSet, Gateway) IOU const usd("USD", gw); // Test gateway with a variety of allowed transfer rates + // NOLINTNEXTLINE(bugprone-float-loop-counter) for (double transferRate = 1.0; transferRate <= 2.0; transferRate += 0.03125) { TxTest env; diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 1b2449823e..523cde743a 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -1006,13 +1006,9 @@ Config::loadFromString(std::string const& fileContents) if (!validatorsFile.empty()) { - if (!boost::filesystem::exists(validatorsFile)) - { - validatorsFile.clear(); - } - else if ( - !boost::filesystem::is_regular_file(validatorsFile) && - !boost::filesystem::is_symlink(validatorsFile)) + if (!boost::filesystem::exists(validatorsFile) || + (!boost::filesystem::is_regular_file(validatorsFile) && + !boost::filesystem::is_symlink(validatorsFile))) { validatorsFile.clear(); } diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp b/src/xrpld/rpc/detail/PathRequestManager.cpp index ba42a0122f..7013353cb1 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.cpp +++ b/src/xrpld/rpc/detail/PathRequestManager.cpp @@ -125,7 +125,8 @@ PathRequestManager::updateAll(std::shared_ptr const& inLedger) json::Value update = request->doUpdate(cache, false, continueCallback); request->updateComplete(); update[jss::type] = "path_find"; - if ((ipSub = getSubscriber(request))) + ipSub = getSubscriber(request); + if (ipSub) { ipSub->send(update, false); remove = false; diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index d7566622e8..fc788dea7d 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -646,21 +646,17 @@ Pathfinder::getBestPaths( { usePath = true; } - else if (extraPathsIterator->quality < pathsIterator->quality) + else if (extraPathsIterator->quality != pathsIterator->quality) { - useExtraPath = true; + // Prefer the lower (better) quality value + useExtraPath = extraPathsIterator->quality < pathsIterator->quality; + usePath = !useExtraPath; } - else if (extraPathsIterator->quality > pathsIterator->quality) + else if (extraPathsIterator->liquidity != pathsIterator->liquidity) { - usePath = true; - } - else if (extraPathsIterator->liquidity > pathsIterator->liquidity) - { - useExtraPath = true; - } - else if (extraPathsIterator->liquidity < pathsIterator->liquidity) - { - usePath = true; + // Equal quality: prefer the higher liquidity + useExtraPath = extraPathsIterator->liquidity > pathsIterator->liquidity; + usePath = !useExtraPath; } else { @@ -795,31 +791,22 @@ Pathfinder::getPathsOut( for (auto const& rspEntry : *lines) { if (pathAsset.get() != rspEntry.getLimit().get().currency) - { - } - else if ( - rspEntry.getBalance() <= beast::kZero && + continue; + if (rspEntry.getBalance() <= beast::kZero && (!rspEntry.getLimitPeer() || -rspEntry.getBalance() >= rspEntry.getLimitPeer() || (bAuthRequired && !rspEntry.getAuth()))) - { - } - else if (isDstAsset && dstAccount == rspEntry.getAccountIDPeer()) + continue; + if (isDstAsset && dstAccount == rspEntry.getAccountIDPeer()) { count += 10000; // count a path to the destination extra + continue; } - else if (rspEntry.getNoRipplePeer()) - { - // This probably isn't a useful path out - } - else if (rspEntry.getFreezePeer()) - { - // Not a useful path out - } - else - { - ++count; - } + if (rspEntry.getNoRipplePeer()) + continue; // This probably isn't a useful path out + if (rspEntry.getFreezePeer()) + continue; // Not a useful path out + ++count; } } }, @@ -828,26 +815,17 @@ Pathfinder::getPathsOut( { for (auto const& mpt : *mpts) { - if (pathAsset.get() != mpt.getMptID()) - { - } - else if (mpt.isZeroBalance() || mpt.isMaxedOut()) - { - } - else if (bAuthRequired) - { - } - else if (isDstAsset && dstAccount == getMPTIssuer(mpt)) + if (pathAsset.get() != mpt.getMptID() || mpt.isZeroBalance() || + mpt.isMaxedOut() || bAuthRequired) + continue; + if (isDstAsset && dstAccount == getMPTIssuer(mpt)) { count += 10000; + continue; } - else if (isIndividualFrozen(*ledger_, account, MPTIssue{mpt.getMptID()})) - { - } - else - { - ++count; - } + if (isIndividualFrozen(*ledger_, account, MPTIssue{mpt.getMptID()})) + continue; + ++count; } } }); @@ -1117,8 +1095,9 @@ Pathfinder::addLink( if (checkAsset()) { // Can't leave on this path + continue; } - else if (bToDestination) + if (bToDestination) { // destination is always worth trying if (uEndPathAsset == dstAmount_.asset()) From 809a6290752e4bfb484c602bf07beb9d19d898fb Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 29 Jun 2026 14:21:14 +0100 Subject: [PATCH 06/31] chore: Add a script to nicely format clang-tidy output (#7650) --- .github/workflows/reusable-clang-tidy.yml | 10 +-- bin/filter-clang-tidy.py | 102 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) create mode 100755 bin/filter-clang-tidy.py diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index e66909ffad..68ddea3ea2 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -96,10 +96,10 @@ jobs: set -o pipefail run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}" - - name: Print errors + - name: Print filtered clang-tidy errors if: ${{ steps.run_clang_tidy.outcome != 'success' }} run: | - sed '/error\||/!d' "${OUTPUT_FILE}" + bin/filter-clang-tidy.py "${OUTPUT_FILE}" - name: Upload clang-tidy output if: ${{ github.event.repository.visibility == 'public' && steps.run_clang_tidy.outcome != 'success' }} @@ -143,12 +143,12 @@ jobs: \`\`\` EOF - - name: Append clang-tidy output to issue body (filter for errors and warnings) + - name: Append filtered clang-tidy output to issue body if: ${{ steps.run_clang_tidy.outcome != 'success' }} run: | if [ -f "${OUTPUT_FILE}" ]; then - # Extract lines containing 'error:', 'warning:', or 'note:' - grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true + # Filter to the unique errors with their source context. + bin/filter-clang-tidy.py "${OUTPUT_FILE}" >"${FILTERED_OUTPUT_FILE}" || true # If filtered output is empty, use original (might be a different error format) if [ ! -s "${FILTERED_OUTPUT_FILE}" ]; then diff --git a/bin/filter-clang-tidy.py b/bin/filter-clang-tidy.py new file mode 100755 index 0000000000..204a4e36f3 --- /dev/null +++ b/bin/filter-clang-tidy.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +""" +Reduce run-clang-tidy output to its unique errors. + +It does two things: + + 1. Filters the raw output down to diagnostics and their source-context lines + (the indented " 103 | ..." / " | ^" lines clang-tidy prints), + matching the "path:line:col: error:" diagnostic shape. + + 2. Deduplicates. The same diagnostic in a header is reported once per + translation unit that includes it, so identical error blocks are collapsed + to their first occurrence. + +An "error block" is an "error:" line together with the indented context lines +and any "note:" lines that follow it (up to the next "error:" line). Blocks are +compared as a whole, so an error stays attached to its own context, and +first-occurrence order is preserved. + +The deduplicated output goes to stdout; a summary of unique error counts per +check is printed to stderr. + +Usage: + bin/filter-clang-tidy.py [INPUT_FILE] # read from file, or + run-clang-tidy ... | bin/filter-clang-tidy.py # read from stdin +""" + +import re +import sys +from collections import Counter + +# A clang-tidy diagnostic line looks like "path:line:col: error: msg [check]". +# Matching on that shape (rather than a loose "error" substring) avoids treating +# progress lines whose paths contain "error" as diagnostics, e.g. +# [284/850][0.7s] /nix/.../clang-tidy ... src/.../error.cpp +DIAG_RE = re.compile(r":\d+:\d+: (?:error|warning|note):") +ERROR_RE = re.compile(r":\d+:\d+: error:") +CHECK_RE = re.compile(r" error: .*\[([^\],]+)") + + +def filter_and_dedup(lines: list[str]) -> list[str]: + """Keep diagnostics with their context, then drop duplicate error blocks.""" + blocks: list[str] = [] + seen: set[str] = set() + current: list[str] = [] + + def flush() -> None: + if not current: + return + block = "".join(current) + if block not in seen: + seen.add(block) + blocks.append(block) + + for line in lines: + # Keep only diagnostics and their indented source-context lines; drop + # progress/status output and blank lines. + if not (DIAG_RE.search(line) or line[:1] in (" ", "\t")): + continue + # An "error:" line starts a new block; its context and any following + # "note:" lines (and their context) belong to it. + if ERROR_RE.search(line): + flush() + current = [] + current.append(line) + flush() + + return blocks + + +def summarize(blocks: list[str]) -> Counter[str]: + """Count unique errors per check name (e.g. "bugprone-branch-clone").""" + counts: Counter[str] = Counter() + for block in blocks: + # The error line is the first line of the block. + match = CHECK_RE.search(block.splitlines()[0]) + if match: + counts[match.group(1)] += 1 + return counts + + +def main() -> int: + if len(sys.argv) > 1 and sys.argv[1] != "-": + with open(sys.argv[1], encoding="utf-8") as f: + lines = f.readlines() + else: + lines = sys.stdin.readlines() + + blocks = filter_and_dedup(lines) + # Blank line between blocks so distinct errors are easy to tell apart. + sys.stdout.write("\n".join(blocks)) + + print("\nUnique errors per check:", file=sys.stderr) + for check, count in summarize(blocks).most_common(): + print(f"{count:>4} {check}", file=sys.stderr) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 62bfc4ca5b8ef0dcfc4d5e44199b254fc278ef01 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 30 Jun 2026 11:43:21 +0100 Subject: [PATCH 07/31] build: Mark sec256k1 and mpt-crypto as transitive headers (#7658) --- conanfile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conanfile.py b/conanfile.py index aec4f9eab0..db12dcb585 100644 --- a/conanfile.py +++ b/conanfile.py @@ -30,10 +30,8 @@ class Xrpl(ConanFile): "ed25519/2015.03", "grpc/1.81.1", "libarchive/3.8.7", - "mpt-crypto/0.4.0-rc2", "nudb/2.0.9", "openssl/3.6.3", - "secp256k1/0.7.1", "soci/4.0.3", "zlib/1.3.2", ] @@ -133,13 +131,15 @@ class Xrpl(ConanFile): def requirements(self): self.requires("boost/1.91.0", force=True, transitive_headers=True) self.requires("date/3.0.4", transitive_headers=True) - self.requires("lz4/1.10.0", force=True) - self.requires("protobuf/6.33.5", force=True) - self.requires("sqlite3/3.53.0", force=True) if self.options.jemalloc: self.requires("jemalloc/5.3.1") + self.requires("lz4/1.10.0", force=True) + self.requires("mpt-crypto/0.4.0-rc2", transitive_headers=True) + self.requires("protobuf/6.33.5", force=True) if self.options.rocksdb: self.requires("rocksdb/10.5.1") + self.requires("secp256k1/0.7.1", transitive_headers=True) + self.requires("sqlite3/3.53.0", force=True) self.requires("xxhash/0.8.3", transitive_headers=True) exports_sources = ( From 95d53b4d43236ef9143aadc69c98a5e3c9b3b309 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 30 Jun 2026 11:43:44 +0100 Subject: [PATCH 08/31] ci: Use macOS 26 Tahoe with apple-clang 21 (#7601) --- .github/scripts/strategy-matrix/macos.json | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 66d7a55a43..2d3cc75c7b 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -1,6 +1,6 @@ { "platform": "macos/arm64", - "runner": ["self-hosted", "macOS", "ARM64", "mac-runner-m1"], + "runner": ["self-hosted", "macOS", "ARM64", "macos-26-apple-clang-21"], "configs": [ { "build_type": "Release", diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index cb7d4c5382..bfa8d2e79c 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 + uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f with: enable_ccache: false diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index a81d9aec67..0cb0219d72 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -113,7 +113,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 + uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f with: enable_ccache: ${{ inputs.ccache_enabled }} diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 68ddea3ea2..b04847e137 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 + uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f with: enable_ccache: false diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 92b72cf6a9..88b364c2b1 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -68,7 +68,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8 + uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f with: enable_ccache: false From 8abbd1ba3a194a652a158fcb6f3a0e9a13e4c9c1 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 30 Jun 2026 12:03:19 +0100 Subject: [PATCH 09/31] chore: Use std::ranges where possible (#7634) --- src/libxrpl/tx/ApplyContext.cpp | 3 ++- src/libxrpl/tx/invariants/InvariantCheck.cpp | 7 +++--- .../tx/transactors/bridge/XChainBridge.cpp | 7 +++--- src/test/app/AmendmentTable_test.cpp | 2 +- src/test/app/Manifest_test.cpp | 7 +++--- src/test/app/OfferMPT_test.cpp | 14 +++++------ src/test/app/XChain_test.cpp | 5 ++-- .../beast/aged_associative_container_test.cpp | 20 ++++------------ .../beast/beast_io_latency_probe_test.cpp | 9 +++----- .../consensus/RCLCensorshipDetector_test.cpp | 4 ++-- src/test/csf/random.h | 2 +- src/test/peerfinder/Livecache_test.cpp | 14 +++++------ src/xrpld/app/ledger/detail/InboundLedger.cpp | 5 ++-- src/xrpld/consensus/LedgerTrie.h | 6 ++--- src/xrpld/consensus/Validations.h | 23 +++++++++---------- src/xrpld/peerfinder/detail/Handouts.h | 6 ++--- src/xrpld/peerfinder/detail/Logic.h | 18 +++++++-------- src/xrpld/rpc/detail/ServerHandler.cpp | 5 ++-- 18 files changed, 64 insertions(+), 93 deletions(-) diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp index 93b0d101af..5e5ab90441 100644 --- a/src/libxrpl/tx/ApplyContext.cpp +++ b/src/libxrpl/tx/ApplyContext.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -114,7 +115,7 @@ ApplyContext::checkInvariantsHelper( tx, result, fee, *view_, journal)...}}; // NOLINT(bugprone-unchecked-optional-access) // call each check's finalizer to see that it passes - if (!std::all_of(finalizers.cbegin(), finalizers.cend(), [](auto const& b) { return b; })) + if (!std::ranges::all_of(finalizers, [](auto const& b) { return b; })) { JLOG(journal.fatal()) << "Transaction has failed one or more global invariants: " << to_string(tx.getJson(JsonOptions::Values::None)); diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 231705efaf..308342da74 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -876,10 +876,9 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::const_ref before, SLE::const { std::vector const& fields = getPseudoAccountFields(); - auto const numFields = - std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool { - return after->isFieldPresent(*sf); - }); + auto const numFields = std::ranges::count_if( + fields, + [&after](SField const* sf) -> bool { return after->isFieldPresent(*sf); }); if (numFields != 1) { std::stringstream error; diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index 1e9e0bfc61..9092ae4140 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -301,10 +302,8 @@ onNewAttestations( } auto const& claimSigningAccount = att->attestationSignerAccount; - if (auto i = std::find_if( - attestations.begin(), - attestations.end(), - [&](auto const& a) { return a.keyAccount == claimSigningAccount; }); + if (auto i = std::ranges::find_if( + attestations, [&](auto const& a) { return a.keyAccount == claimSigningAccount; }); i != attestations.end()) { // existing attestation diff --git a/src/test/app/AmendmentTable_test.cpp b/src/test/app/AmendmentTable_test.cpp index 7c2087bd4a..d420990cbc 100644 --- a/src/test/app/AmendmentTable_test.cpp +++ b/src/test/app/AmendmentTable_test.cpp @@ -140,7 +140,7 @@ private: combineArg(std::vector& dest, std::vector const& src, Args const&... args) { assert(dest.capacity() >= dest.size() + src.size()); - std::copy(src.begin(), src.end(), std::back_inserter(dest)); + std::ranges::copy(src, std::back_inserter(dest)); if constexpr (sizeof...(args) > 0) combineArg(dest, args...); } diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index 0cf1155cf5..50e8ab4a8d 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -290,10 +290,9 @@ public: if (inManifests.size() == loadedManifests.size()) { BEAST_EXPECT( - std::equal( - inManifests.begin(), - inManifests.end(), - loadedManifests.begin(), + std::ranges::equal( + inManifests, + loadedManifests, [](Manifest const* lhs, Manifest const* rhs) { return *lhs == *rhs; })); } else diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index ac50924ac2..9958515c5f 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -3728,10 +3728,9 @@ public: auto actorOffers = offersOnAccount(env, actor.acct); auto const offerCount = std::distance( actorOffers.begin(), - std::remove_if( - actorOffers.begin(), actorOffers.end(), [](SLE::const_pointer& offer) { - return (*offer)[sfTakerGets].signum() == 0; - })); + std::ranges::remove_if(actorOffers, [](SLE::const_pointer& offer) { + return (*offer)[sfTakerGets].signum() == 0; + }).begin()); BEAST_EXPECT(offerCount == actor.offers); env.require(Balance(actor.acct, actor.xrp)); @@ -3898,10 +3897,9 @@ public: auto actorOffers = offersOnAccount(env, actor.acct); auto const offerCount = std::distance( actorOffers.begin(), - std::remove_if( - actorOffers.begin(), actorOffers.end(), [](SLE::const_pointer& offer) { - return (*offer)[sfTakerGets].signum() == 0; - })); + std::ranges::remove_if(actorOffers, [](SLE::const_pointer& offer) { + return (*offer)[sfTakerGets].signum() == 0; + }).begin()); BEAST_EXPECT(offerCount == actor.offers); env.require(Balance(actor.acct, actor.xrp)); diff --git a/src/test/app/XChain_test.cpp b/src/test/app/XChain_test.cpp index 36c1a08144..437c329e02 100644 --- a/src/test/app/XChain_test.cpp +++ b/src/test/app/XChain_test.cpp @@ -306,9 +306,8 @@ struct BalanceTransfer [[nodiscard]] bool payeesReceived(STAmount const& reward) const { - return std::all_of(rewardAccounts.begin(), rewardAccounts.end(), [&](balance const& b) { - return b.diff() == reward; - }); + return std::ranges::all_of( + rewardAccounts, [&](balance const& b) { return b.diff() == reward; }); } bool diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp index 2ac5fc5a33..413194491a 100644 --- a/src/test/beast/aged_associative_container_test.cpp +++ b/src/test/beast/aged_associative_container_test.cpp @@ -1296,13 +1296,7 @@ AgedAssociativeContainerTestBase::testChronological() typename Traits::template Cont<> c(v.begin(), v.end(), clock); - BEAST_EXPECT( - std::equal( - c.chronological.cbegin(), - c.chronological.cend(), - v.begin(), - v.end(), - EqualValue())); + BEAST_EXPECT(std::ranges::equal(c.chronological, v, EqualValue())); // Test touch() with a non-const iterator. for (auto iter(v.crbegin()); iter != v.crend(); ++iter) @@ -1336,13 +1330,7 @@ AgedAssociativeContainerTestBase::testChronological() c.touch(found); } - BEAST_EXPECT( - std::equal( - c.chronological.cbegin(), - c.chronological.cend(), - v.cbegin(), - v.cend(), - EqualValue())); + BEAST_EXPECT(std::ranges::equal(c.chronological, v, EqualValue())); { // Because touch (reverse_iterator pos) is not allowed, the following @@ -1407,8 +1395,8 @@ AgedAssociativeContainerTestBase::reverseFillAgedContainer(Container& c, Values clk.set(0); Values rev(values); - std::sort(rev.begin(), rev.end()); - std::reverse(rev.begin(), rev.end()); + std::ranges::sort(rev); + std::ranges::reverse(rev); for (auto& v : rev) { // Add values in reverse order so they are reversed chronologically. diff --git a/src/test/beast/beast_io_latency_probe_test.cpp b/src/test/beast/beast_io_latency_probe_test.cpp index b7e4980f05..9a93968dab 100644 --- a/src/test/beast/beast_io_latency_probe_test.cpp +++ b/src/test/beast/beast_io_latency_probe_test.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include // IWYU pragma: keep #include @@ -94,18 +95,14 @@ class io_latency_probe_test : public beast::unit_test::Suite, public beast::test auto getMax() { - return std::chrono::duration_cast( - *std::max_element(elapsedTimes.begin(), elapsedTimes.end())) - .count(); + return std::chrono::duration_cast(*std::ranges::max_element(elapsedTimes)).count(); } template auto getMin() { - return std::chrono::duration_cast( - *std::min_element(elapsedTimes.begin(), elapsedTimes.end())) - .count(); + return std::chrono::duration_cast(*std::ranges::min_element(elapsedTimes)).count(); } }; #endif diff --git a/src/test/consensus/RCLCensorshipDetector_test.cpp b/src/test/consensus/RCLCensorshipDetector_test.cpp index 32117e0089..722a34f937 100644 --- a/src/test/consensus/RCLCensorshipDetector_test.cpp +++ b/src/test/consensus/RCLCensorshipDetector_test.cpp @@ -31,12 +31,12 @@ class RCLCensorshipDetector_test : public beast::unit_test::Suite cdet.check(std::move(accepted), [&remove, &remain](auto id, auto seq) { // If the item is supposed to be removed from the censorship // detector internal tracker manually, do it now: - if (std::find(remove.begin(), remove.end(), id) != remove.end()) + if (std::ranges::find(remove, id) != remove.end()) return true; // If the item is supposed to still remain in the censorship // detector internal tracker; remove it from the vector. - auto it = std::find(remain.begin(), remain.end(), id); + auto it = std::ranges::find(remain, id); if (it != remain.end()) remain.erase(it); return false; diff --git a/src/test/csf/random.h b/src/test/csf/random.h index 30f98e37fe..c506397d88 100644 --- a/src/test/csf/random.h +++ b/src/test/csf/random.h @@ -44,7 +44,7 @@ std::vector sample(std::size_t size, RandomNumberDistribution dist, Generator& g) { std::vector res(size); - std::generate(res.begin(), res.end(), [&dist, &g]() { return dist(g); }); + std::ranges::generate(res, [&dist, &g]() { return dist(g); }); return res; } diff --git a/src/test/peerfinder/Livecache_test.cpp b/src/test/peerfinder/Livecache_test.cpp index 9dae410b5b..4f2d6e97e1 100644 --- a/src/test/peerfinder/Livecache_test.cpp +++ b/src/test/peerfinder/Livecache_test.cpp @@ -167,10 +167,9 @@ public: for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); ++i.first, ++i.second) { - std::copy((*i.second).begin(), (*i.second).end(), std::back_inserter(before[i.first])); - std::copy( - (*i.second).begin(), (*i.second).end(), std::back_inserter(beforeSorted[i.first])); - std::sort(beforeSorted[i.first].begin(), beforeSorted[i.first].end(), cmpEp); + std::ranges::copy(*i.second, std::back_inserter(before[i.first])); + std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); + std::ranges::sort(beforeSorted[i.first], cmpEp); } c.hops.shuffle(); @@ -180,10 +179,9 @@ public: for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); ++i.first, ++i.second) { - std::copy((*i.second).begin(), (*i.second).end(), std::back_inserter(after[i.first])); - std::copy( - (*i.second).begin(), (*i.second).end(), std::back_inserter(afterSorted[i.first])); - std::sort(afterSorted[i.first].begin(), afterSorted[i.first].end(), cmpEp); + std::ranges::copy(*i.second, std::back_inserter(after[i.first])); + std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); + std::ranges::sort(afterSorted[i.first], cmpEp); } // each hop bucket should contain the same items diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index e4df126ee8..4affffd1c1 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -127,9 +127,8 @@ std::size_t InboundLedger::getPeerCount() const { auto const& peerIds = peerSet_->getPeerIds(); - return std::count_if(peerIds.begin(), peerIds.end(), [this](auto id) { - return (app_.getOverlay().findPeerByShortID(id) != nullptr); - }); + return std::ranges::count_if( + peerIds, [this](auto id) { return (app_.getOverlay().findPeerByShortID(id) != nullptr); }); } void diff --git a/src/xrpld/consensus/LedgerTrie.h b/src/xrpld/consensus/LedgerTrie.h index cd9662ff02..b11a69a641 100644 --- a/src/xrpld/consensus/LedgerTrie.h +++ b/src/xrpld/consensus/LedgerTrie.h @@ -204,10 +204,8 @@ struct Node void erase(Node const* child) { - auto it = std::find_if( - children.begin(), children.end(), [child](std::unique_ptr const& curr) { - return curr.get() == child; - }); + auto it = std::ranges::find_if( + children, [child](std::unique_ptr const& curr) { return curr.get() == child; }); XRPL_ASSERT(it != children.end(), "xrpl::Node::erase : valid input"); std::swap(*it, children.back()); children.pop_back(); diff --git a/src/xrpld/consensus/Validations.h b/src/xrpld/consensus/Validations.h index f109ae620b..d4da8a2887 100644 --- a/src/xrpld/consensus/Validations.h +++ b/src/xrpld/consensus/Validations.h @@ -817,16 +817,15 @@ public: if (!preferred) { // fall back to majority over acquiring ledgers - auto it = std::max_element( - acquiring_.begin(), acquiring_.end(), [](auto const& a, auto const& b) { - std::pair const& aKey = a.first; - typename hash_set::size_type const& aSize = a.second.size(); - std::pair const& bKey = b.first; - typename hash_set::size_type const& bSize = b.second.size(); - // order by number of trusted peers validating that ledger - // break ties with ledger ID - return std::tie(aSize, aKey.second) < std::tie(bSize, bKey.second); - }); + auto it = std::ranges::max_element(acquiring_, [](auto const& a, auto const& b) { + std::pair const& aKey = a.first; + typename hash_set::size_type const& aSize = a.second.size(); + std::pair const& bKey = b.first; + typename hash_set::size_type const& bSize = b.second.size(); + // order by number of trusted peers validating that ledger + // break ties with ledger ID + return std::tie(aSize, aKey.second) < std::tie(bSize, bKey.second); + }); if (it != acquiring_.end()) return it->first; return std::nullopt; @@ -896,7 +895,7 @@ public: return (preferred->first >= minSeq) ? preferred->second : lcl.id(); // Otherwise, rely on peer ledgers - auto it = std::max_element(peerCounts.begin(), peerCounts.end(), [](auto& a, auto& b) { + auto it = std::ranges::max_element(peerCounts, [](auto const& a, auto const& b) { // Prefer larger counts, then larger ids on ties // (max_element expects this to return true if a < b) return std::tie(a.second, a.first) < std::tie(b.second, b.first); @@ -932,7 +931,7 @@ public: } // Count parent ledgers as fallback - return std::count_if(lastLedger_.begin(), lastLedger_.end(), [&ledgerID](auto const& it) { + return std::ranges::count_if(lastLedger_, [&ledgerID](auto const& it) { auto const& curr = it.second; return curr.seq() > Seq{0} && curr[curr.seq() - Seq{1}] == ledgerID; }); diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/src/xrpld/peerfinder/detail/Handouts.h index 19a367f8c1..7523197c40 100644 --- a/src/xrpld/peerfinder/detail/Handouts.h +++ b/src/xrpld/peerfinder/detail/Handouts.h @@ -143,7 +143,7 @@ RedirectHandouts::tryInsert(Endpoint const& ep) return false; // Make sure the address isn't already in our list - if (std::any_of(list_.begin(), list_.end(), [&ep](Endpoint const& other) { + if (std::ranges::any_of(list_, [&ep](Endpoint const& other) { // Ignore port for security reasons return other.address.address() == ep.address.address(); })) @@ -222,7 +222,7 @@ SlotHandouts::tryInsert(Endpoint const& ep) return false; // Make sure the address isn't already in our list - if (std::any_of(list_.begin(), list_.end(), [&ep](Endpoint const& other) { + if (std::ranges::any_of(list_, [&ep](Endpoint const& other) { // Ignore port for security reasons return other.address.address() == ep.address.address(); })) @@ -311,7 +311,7 @@ ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint) return false; // Make sure the address isn't already in our list - if (std::any_of(list_.begin(), list_.end(), [&endpoint](beast::IP::Endpoint const& other) { + if (std::ranges::any_of(list_, [&endpoint](beast::IP::Endpoint const& other) { // Ignore port for security reasons return other.address() == endpoint.address(); })) diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 55cce506ba..b74643f7a5 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -573,19 +573,17 @@ public: // build list of active slots std::vector activeSlots; activeSlots.reserve(slots.size()); - std::for_each( - slots.cbegin(), slots.cend(), [&activeSlots](Slots::value_type const& value) { - if (value.second->state() == Slot::State::Active) - activeSlots.emplace_back(value.second); - }); + std::ranges::for_each(slots, [&activeSlots](Slots::value_type const& value) { + if (value.second->state() == Slot::State::Active) + activeSlots.emplace_back(value.second); + }); std::shuffle(activeSlots.begin(), activeSlots.end(), defaultPrng()); // build target vector targets.reserve(activeSlots.size()); - std::for_each( - activeSlots.cbegin(), activeSlots.cend(), [&targets](SlotImp::ptr const& slot) { - targets.emplace_back(slot); - }); + std::ranges::for_each(activeSlots, [&targets](SlotImp::ptr const& slot) { + targets.emplace_back(slot); + }); } /* VFALCO NOTE @@ -987,7 +985,7 @@ public: { auto const& address(iter->first.address()); if (iter->second.when() <= now && squelches.find(address) == squelches.end() && - std::none_of(slots.cbegin(), slots.cend(), [address](Slots::value_type const& v) { + std::ranges::none_of(slots, [address](Slots::value_type const& v) { return address == v.first.address(); })) { diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 5177c85738..768cbf0dc0 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -1179,9 +1179,8 @@ parsePorts(Config const& config, std::ostream& log) } else { - auto const count = std::count_if(result.cbegin(), result.cend(), [](Port const& p) { - return p.protocol.contains("peer"); - }); + auto const count = std::ranges::count_if( + result, [](Port const& p) { return p.protocol.contains("peer"); }); if (count > 1) { From ecf7f805c90538e177470febb0a3a5b989c60620 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:51:41 +0200 Subject: [PATCH 10/31] feat: Introduce lending 1.1 amendment and add `MemoData` field to `VaultDelete` transaction (#6324) --- include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/transactions.macro | 1 + .../transactions/VaultDelete.h | 37 ++++++++++ .../tx/transactors/vault/VaultDelete.cpp | 8 +++ src/test/app/Vault_test.cpp | 70 +++++++++++++++++++ .../transactions/VaultDeleteTests.cpp | 49 +++++++++++++ 6 files changed, 166 insertions(+) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index a9b237be32..3c808d960b 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -14,6 +14,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 8a3b0ff2ce..b46fca92b6 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -889,6 +889,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, MustDeleteAcct | DestroyMptIssuance | MustModifyVault, ({ {sfVaultID, SoeRequired}, + {sfMemoData, SoeOptional}, })) /** This transaction trades assets for shares with a vault. */ diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h index 2b6e248c0a..b4c08ae229 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h @@ -57,6 +57,32 @@ public: { return this->tx_->at(sfVaultID); } + + /** + * @brief Get sfMemoData (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMemoData() const + { + if (hasMemoData()) + { + return this->tx_->at(sfMemoData); + } + return std::nullopt; + } + + /** + * @brief Check if sfMemoData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMemoData() const + { + return this->tx_->isFieldPresent(sfMemoData); + } }; /** @@ -112,6 +138,17 @@ public: return *this; } + /** + * @brief Set sfMemoData (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultDeleteBuilder& + setMemoData(std::decay_t const& value) + { + object_[sfMemoData] = value; + return *this; + } + /** * @brief Build and return the VaultDelete wrapper. * @param publicKey The public key for signing. diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp index 0550f08b17..fa38ae278b 100644 --- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include #include #include // IWYU pragma: keep @@ -28,6 +30,12 @@ VaultDelete::preflight(PreflightContext const& ctx) return temMALFORMED; } + if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1)) + return temDISABLED; + + if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength)) + return temMALFORMED; + return tesSUCCESS; } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 9b29442197..d9afd93b23 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -7511,6 +7511,74 @@ class Vault_test : public beast::unit_test::Suite } } + void + testVaultDeleteMemoData() + { + using namespace test::jtx; + + Env env{*this}; + + Account const owner{"owner"}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + + auto const keylet = keylet::vault(owner.id(), 1); + auto delTx = vault.del({.owner = owner, .id = keylet.key}); + + // Test VaultDelete with featureLendingProtocolV1_1 disabled + // Transaction fails if the data field is provided + { + testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled"); + env.disableFeature(featureLendingProtocolV1_1); + delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A')); + env(delTx, Ter(temDISABLED)); + env.enableFeature(featureLendingProtocolV1_1); + env.close(); + } + + // Transaction fails if the data field is too large + { + testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large"); + delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A')); + env(delTx, Ter(temMALFORMED)); + env.close(); + } + + // Transaction fails if the data field is set, but is empty + { + testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty"); + delTx[sfMemoData] = strHex(std::string(0, 'A')); + env(delTx, Ter(temMALFORMED)); + env.close(); + } + + { + testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault"); + auto const keylet = keylet::vault(owner.id(), env.seq(owner)); + + // Recreate the transaction as the vault keylet changed + auto delTx = vault.del({.owner = owner, .id = keylet.key}); + delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A')); + env(delTx, Ter(tecNO_ENTRY)); + env.close(); + } + + { + testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid"); + PrettyAsset const xrpAsset = xrpIssue(); + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, Ter(tesSUCCESS)); + env.close(); + // Recreate the transaction as the vault keylet changed + auto delTx = vault.del({.owner = owner, .id = keylet.key}); + delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A')); + env(delTx, Ter(tesSUCCESS)); + env.close(); + } + } + void testVaultDepositFreeze() { @@ -8082,6 +8150,7 @@ class Vault_test : public beast::unit_test::Suite runTests(); env.disableFeature(fixCleanup3_3_0); + runTests(); env.enableFeature(fixCleanup3_3_0); } @@ -8115,6 +8184,7 @@ public: testVaultClawbackAssets(); testVaultEscrowedMPT(); testAssetsMaximum(); + testVaultDeleteMemoData(); testBug6LimitBypassWithShares(); testRemoveEmptyHoldingLockedAmount(); testRemoveEmptyHoldingConfidentialBalances(); diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp index 70747af1ed..8f0eb2e4cd 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultDeleteTests.cpp @@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); VaultDeleteBuilder builder{ accountValue, @@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) }; // Set optional fields + builder.setMemoData(memoDataValue); auto tx = builder.build(publicKey, secretKey); @@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = tx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + EXPECT_TRUE(tx.hasMemoData()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) // Transaction-specific field values auto const vaultIDValue = canonical_UINT256(); + auto const memoDataValue = canonical_VL(); // Build an initial transaction VaultDeleteBuilder initialBuilder{ @@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) feeValue }; + initialBuilder.setMemoData(memoDataValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip) } // Verify optional fields + { + auto const& expected = memoDataValue; + auto const actualOpt = rebuiltTx.getMemoData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present"; + expectEqualField(expected, *actualOpt, "sfMemoData"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType) EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); } +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testVaultDeleteNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const vaultIDValue = canonical_UINT256(); + + VaultDeleteBuilder builder{ + accountValue, + vaultIDValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasMemoData()); + EXPECT_FALSE(tx.getMemoData().has_value()); +} } From 86d8b244d6e9b61952de736c010c789e12c6498e Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Wed, 1 Jul 2026 08:47:14 -0400 Subject: [PATCH 11/31] feat: Add Batch (XLS-56) V1_1 (#6446) Co-authored-by: Mayukha Vadari --- include/xrpl/core/HashRouter.h | 6 +- include/xrpl/ledger/OpenView.h | 2 +- include/xrpl/protocol/Batch.h | 10 +- include/xrpl/protocol/Protocol.h | 3 + include/xrpl/protocol/STObject.h | 5 + include/xrpl/protocol/STTx.h | 53 +- include/xrpl/protocol/TER.h | 2 + include/xrpl/protocol/detail/features.macro | 8 +- .../xrpl/protocol/detail/transactions.macro | 2 +- .../protocol_autogen/transactions/Batch.h | 2 +- include/xrpl/tx/ApplyContext.h | 6 +- include/xrpl/tx/Transactor.h | 49 +- include/xrpl/tx/transactors/system/Batch.h | 8 +- src/libxrpl/protocol/STObject.cpp | 14 + src/libxrpl/protocol/STTx.cpp | 133 +- src/libxrpl/protocol/TER.cpp | 2 + src/libxrpl/tx/Transactor.cpp | 105 +- src/libxrpl/tx/apply.cpp | 34 +- .../tx/transactors/lending/LoanSet.cpp | 2 +- .../tx/transactors/payment/Payment.cpp | 32 +- src/libxrpl/tx/transactors/system/Batch.cpp | 153 +- src/test/app/Batch_test.cpp | 1227 ++++++++++++++--- .../app/ConfidentialTransferExtended_test.cpp | 35 +- src/test/app/LedgerReplay_test.cpp | 45 +- src/test/app/TxQ_test.cpp | 3 +- src/test/jtx/TestHelpers.h | 4 +- src/test/jtx/batch.h | 5 +- src/test/jtx/impl/batch.cpp | 16 +- src/test/rpc/Feature_test.cpp | 2 +- src/test/rpc/Simulate_test.cpp | 16 +- src/xrpld/app/ledger/detail/BuildLedger.cpp | 9 + src/xrpld/app/misc/NetworkOPs.cpp | 7 +- src/xrpld/app/misc/detail/TxQ.cpp | 7 + src/xrpld/overlay/detail/PeerImp.cpp | 4 +- .../rpc/handlers/transaction/Simulate.cpp | 8 + 35 files changed, 1536 insertions(+), 483 deletions(-) diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h index d36b8aee6e..c8b34d8e93 100644 --- a/include/xrpl/core/HashRouter.h +++ b/include/xrpl/core/HashRouter.h @@ -19,12 +19,14 @@ enum class HashRouterFlags : std::uint16_t { HELD = 0x08, // Held by LedgerMaster after potential processing failure TRUSTED = 0x10, // Comes from a trusted source - // Private flags (used internally in apply.cpp) - // Do not attempt to read, set, or reuse. + // Private flags. Each group is owned by one file; do not read, set, or + // reuse a flag outside the file noted. + // Used in apply.cpp PRIVATE1 = 0x0100, PRIVATE2 = 0x0200, PRIVATE3 = 0x0400, PRIVATE4 = 0x0800, + // Used in EscrowFinish.cpp PRIVATE5 = 0x1000, PRIVATE6 = 0x2000 }; diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h index 4ba2a7759b..d145473516 100644 --- a/include/xrpl/ledger/OpenView.h +++ b/include/xrpl/ledger/OpenView.h @@ -28,7 +28,7 @@ inline constexpr struct OpenLedgerT /** Batch view construction tag. Views constructed with this tag are part of a stack of views - used during batch transaction applied. + used during batch transaction application. */ inline constexpr struct BatchViewT { diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h index 2f2412b3ff..4e021442d6 100644 --- a/include/xrpl/protocol/Batch.h +++ b/include/xrpl/protocol/Batch.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,9 +8,16 @@ namespace xrpl { inline void -serializeBatch(Serializer& msg, std::uint32_t const& flags, std::vector const& txids) +serializeBatch( + Serializer& msg, + AccountID const& outerAccount, + std::uint32_t outerSeqValue, + std::uint32_t const& flags, + std::vector const& txids) { msg.add32(HashPrefix::Batch); + msg.addBitString(outerAccount); + msg.add32(outerSeqValue); msg.add32(flags); msg.add32(std::uint32_t(txids.size())); for (auto const& txid : txids) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 7eac92e83c..ba6347dfa7 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -311,6 +311,9 @@ constexpr std::size_t kPermissionMaxSize = 10; /** The maximum number of transactions that can be in a batch. */ constexpr std::size_t kMaxBatchTxCount = 8; +/** The maximum number of batch signers. */ +constexpr std::size_t kMaxBatchSigners = kMaxBatchTxCount * 3; + /** Length of a secp256k1 scalar in bytes. */ constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index e65cc79c78..044e48ce62 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -217,6 +217,11 @@ public: [[nodiscard]] AccountID getAccountID(SField const& field) const; + /** The account responsible for the fee and authorization: the delegate when + sfDelegate is present, otherwise the account. */ + [[nodiscard]] AccountID + getFeePayer() const; + [[nodiscard]] Blob getFieldVL(SField const& field) const; [[nodiscard]] STAmount const& diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 659fede31d..e78fce27a1 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -12,6 +12,7 @@ #include #include +#include namespace xrpl { @@ -51,51 +52,48 @@ public: STTx(TxType type, std::function assembler); // STObject functions. - SerializedTypeID + [[nodiscard]] SerializedTypeID getSType() const override; - std::string + [[nodiscard]] std::string getFullText() const override; // Outer transaction functions / signature functions. static Blob getSignature(STObject const& sigObject); - Blob + [[nodiscard]] Blob getSignature() const { return getSignature(*this); } - uint256 + [[nodiscard]] uint256 getSigningHash() const; - TxType + [[nodiscard]] TxType getTxnType() const; - Blob + [[nodiscard]] Blob getSigningPubKey() const; - SeqProxy + [[nodiscard]] SeqProxy getSeqProxy() const; /** Returns the first non-zero value of (Sequence, TicketSequence). */ - std::uint32_t + [[nodiscard]] std::uint32_t getSeqValue() const; - AccountID - getFeePayer() const; - - boost::container::flat_set + [[nodiscard]] boost::container::flat_set getMentionedAccounts() const; - uint256 + [[nodiscard]] uint256 getTransactionID() const; - json::Value + [[nodiscard]] json::Value getJson(JsonOptions options) const override; - json::Value + [[nodiscard]] json::Value getJson(JsonOptions options, bool binary) const; void @@ -108,27 +106,27 @@ public: @param rules The current ledger rules. @return `true` if valid signature. If invalid, the error message string. */ - std::expected + [[nodiscard]] std::expected checkSign(Rules const& rules) const; - std::expected + [[nodiscard]] std::expected checkBatchSign(Rules const& rules) const; // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); - std::string + [[nodiscard]] std::string getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const; - std::string + [[nodiscard]] std::string getMetaSQL( Serializer rawTxn, std::uint32_t inLedger, TxnSql status, std::string const& escapedMetaData) const; - std::vector const& + [[nodiscard]] std::vector const& getBatchTransactionIDs() const; private: @@ -138,28 +136,31 @@ private: Will be *this more often than not. @return `true` if valid signature. If invalid, the error message string. */ - std::expected + [[nodiscard]] std::expected checkSign(Rules const& rules, STObject const& sigObject) const; - std::expected + [[nodiscard]] std::expected checkSingleSign(STObject const& sigObject) const; - std::expected + [[nodiscard]] std::expected checkMultiSign(Rules const& rules, STObject const& sigObject) const; - std::expected + [[nodiscard]] std::expected checkBatchSingleSign(STObject const& batchSigner) const; - std::expected + [[nodiscard]] std::expected checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const; + void + buildBatchTxnIds(); + STBase* copy(std::size_t n, void* buf) const override; STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; - mutable std::vector batchTxnIds_; + std::optional> batchTxnIds_; }; bool diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 84c344ea76..4ee084e714 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -175,6 +175,8 @@ enum TEFcodes : TERUnderlyingType { tefNO_TICKET, tefNFTOKEN_IS_NOT_TRANSFERABLE, tefINVALID_LEDGER_FIX_TYPE, + tefNO_DST_PARTIAL, + tefBAD_PATH_COUNT, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3c808d960b..a1911761bb 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -14,13 +14,14 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) -XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) + +XRPL_FEATURE(BatchV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) 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) -XRPL_FIX (BatchInnerSigs, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo) @@ -34,7 +35,6 @@ XRPL_FEATURE(TokenEscrow, Supported::Yes, VoteBehavior::DefaultN XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_3, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(Batch, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::Yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b46fca92b6..b0c4f66bae 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -940,7 +940,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, #endif TRANSACTION(ttBATCH, 71, Batch, Delegation::NotDelegable, - featureBatch, + featureBatchV1_1, NoPriv, ({ {sfRawTransactions, SoeRequired}, diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h b/include/xrpl/protocol_autogen/transactions/Batch.h index 55b99d0eef..00f553ada7 100644 --- a/include/xrpl/protocol_autogen/transactions/Batch.h +++ b/include/xrpl/protocol_autogen/transactions/Batch.h @@ -20,7 +20,7 @@ class BatchBuilder; * * Type: ttBATCH (71) * Delegable: Delegation::NotDelegable - * Amendment: featureBatch + * Amendment: featureBatchV1_1 * Privileges: NoPriv * * Immutable wrapper around STTx providing type-safe field access. diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index 8540037601..c98b3f82c5 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -24,6 +24,10 @@ public: ApplyFlags flags, beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}); + // Convenience constructor used only by tests that build an ApplyContext + // directly (e.g. invariant checks). Production always uses the parentBatchId + // constructor above; this one fixes parentBatchId to std::nullopt and so is + // never valid for a batch inner (hence the TapBatch assert). explicit ApplyContext( ServiceRegistry& registry, OpenView& base, @@ -124,7 +128,7 @@ private: ApplyFlags flags_; std::optional view_; - // The ID of the batch transaction we are executing under, if seated. + // The ID of the batch transaction we are executing under, if set. std::optional parentBatchId_; }; diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 02fedfe970..2b50cfa6e7 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -180,9 +180,6 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); - static NotTEC - checkBatchSign(PreclaimContext const& ctx); - // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -373,7 +370,12 @@ protected: std::optional const& parentBatchId, AccountID const& idAccount, STObject const& sigObject, - beast::Journal const j); + beast::Journal const j, + // A batch may carry an inner from an account that an earlier inner + // creates, so the signer account need not exist yet; when it does not, + // only its own master key may authorize it. Normal transactions require + // the account to already exist. + bool permitUncreatedAccount = false); // Base class always returns true static bool @@ -413,25 +415,8 @@ protected: std::optional value, unit::ValueUnit min = unit::ValueUnit{}); -private: - static NotTEC - checkPermission( - ReadView const& view, - STTx const& tx, - std::unordered_set& heldGranularPermissions); - - std::pair - reset(XRPAmount fee); - - TER - consumeSeqProxy(SLE::pointer const& sleAccount); - - TER - payFee(); - - std::tuple - processPersistentChanges(TER result, XRPAmount fee); - + // Signature-authorization helpers. protected so the Batch transactor can + // reuse them when validating each BatchSigner in Batch::checkBatchSign. static NotTEC checkSingleSign( ReadView const& view, @@ -448,6 +433,24 @@ private: STObject const& sigObject, beast::Journal const j); +private: + static NotTEC + checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions); + + std::pair + reset(XRPAmount fee); + + TER + consumeSeqProxy(SLE::pointer const& sleAccount); + TER + payFee(); + + std::tuple + processPersistentChanges(TER result, XRPAmount fee); + void trapTransaction(uint256) const; /** Performs early sanity checks on the account and fee fields. diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h index 43f0103319..927a5b27cd 100644 --- a/include/xrpl/tx/transactors/system/Batch.h +++ b/include/xrpl/tx/transactors/system/Batch.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include namespace xrpl { @@ -61,6 +59,12 @@ public: ttLOAN_MANAGE, ttLOAN_PAY, }); + +private: + // Skips signature verification for inner txns, so keep it private: it must + // only be reached through Batch::checkSign. + static NotTEC + checkBatchSign(PreclaimContext const& ctx); }; } // namespace xrpl diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index e16cbc871f..445da16828 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -635,6 +635,20 @@ STObject::getAccountID(SField const& field) const return getFieldByValue(field); } +AccountID +STObject::getFeePayer() const +{ + // If sfDelegate is present, the delegate account is the payer + // note: if a delegate is specified, its authorization to act on behalf of the account is + // enforced in `Transactor::invokeCheckPermission` + // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) + if (isFieldPresent(sfDelegate)) + return getAccountID(sfDelegate); + + // Default payer + return getAccountID(sfAccount); +} + Blob STObject::getFieldVL(SField const& field) const { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index be3b1a082f..cd2da12316 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include #include @@ -72,6 +71,7 @@ STTx::STTx(STObject&& object) : STObject(std::move(object)) txType_ = safeCast(getFieldU16(sfTransactionType)); applyTemplate(getTxFormat(txType_)->getSOTemplate()); // may throw tid_ = getHash(HashPrefix::TransactionId); + buildBatchTxnIds(); } STTx::STTx(SerialIter& sit) : STObject(sfTransaction) @@ -88,6 +88,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction) applyTemplate(getTxFormat(txType_)->getSOTemplate()); // May throw tid_ = getHash(HashPrefix::TransactionId); + buildBatchTxnIds(); } STTx::STTx(TxType type, std::function assembler) : STObject(sfTransaction) @@ -105,6 +106,7 @@ STTx::STTx(TxType type, std::function assembler) : STObject(sfT logicError("Transaction type was mutated during assembly"); tid_ = getHash(HashPrefix::TransactionId); + buildBatchTxnIds(); } STBase* @@ -212,20 +214,6 @@ STTx::getSeqValue() const return getSeqProxy().value(); } -AccountID -STTx::getFeePayer() const -{ - // If sfDelegate is present, the delegate account is the payer - // note: if a delegate is specified, its authorization to act on behalf of the account is - // enforced in `Transactor::invokeCheckPermission` - // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) - if (isFieldPresent(sfDelegate)) - return getAccountID(sfDelegate); - - // Default payer - return getAccountID(sfAccount); -} - void STTx::sign( PublicKey const& publicKey, @@ -279,6 +267,17 @@ STTx::checkSign(Rules const& rules) const if (auto const ret = checkSign(rules, counterSig); !ret) return std::unexpected("Counterparty: " + ret.error()); } + + // Verify the batch signer signatures here too, so they are cached with the + // rest of signature checking (checkValidity / SF_SIGGOOD) and stay out of + // the transaction engine. Gated on a batch (batchTxnIds_ seated) that + // actually carries signers; a batch whose inners are all from the outer + // account has no sfBatchSigners and needs no signer crypto. + if (batchTxnIds_ && isFieldPresent(sfBatchSigners)) + { + if (auto const ret = checkBatchSign(rules); !ret) + return ret; + } return {}; } @@ -287,12 +286,15 @@ STTx::checkBatchSign(Rules const& rules) const { try { - XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSign : not a batch transaction"); if (getTxnType() != ttBATCH) { - JLOG(debugLog().fatal()) << "not a batch transaction"; + // LCOV_EXCL_START + UNREACHABLE("STTx::checkBatchSign : not a batch transaction"); return std::unexpected("Not a batch transaction."); + // LCOV_EXCL_STOP } + if (!isFieldPresent(sfBatchSigners)) + return std::unexpected("Missing BatchSigners field."); // LCOV_EXCL_LINE STArray const& signers{getFieldArray(sfBatchSigners)}; for (auto const& signer : signers) { @@ -307,9 +309,10 @@ STTx::checkBatchSign(Rules const& rules) const } catch (std::exception const& e) { - JLOG(debugLog().error()) << "Batch signature check failed: " << e.what(); + // LCOV_EXCL_START + return std::unexpected(std::string("Internal batch signature check failure: ") + e.what()); + // LCOV_EXCL_STOP } - return std::unexpected("Internal batch signature check failure."); } json::Value @@ -429,8 +432,11 @@ STTx::checkSingleSign(STObject const& sigObject) const std::expected STTx::checkBatchSingleSign(STObject const& batchSigner) const { + XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction"); Serializer msg; - serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + serializeBatch( + msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs()); + finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg); return singleSignHelper(batchSigner, msg.slice()); } @@ -504,7 +510,7 @@ multiSignHelper( { return std::unexpected( std::string("Invalid signature on account ") + toBase58(accountID) + - errorWhat.value_or("") + "."); + (errorWhat ? ": " + *errorWhat : "") + "."); } } // All signatures verified. @@ -514,14 +520,18 @@ multiSignHelper( std::expected STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const { + XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction"); // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer // with the stuff that stays constant from signature to signature. + auto const batchSignerAccount = batchSigner.getAccountID(sfAccount); Serializer dataStart; - serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); + serializeBatch( + dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs()); + dataStart.addBitString(batchSignerAccount); return multiSignHelper( batchSigner, - std::nullopt, + batchSignerAccount, [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); @@ -555,42 +565,38 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const rules); } -/** - * @brief Retrieves a batch of transaction IDs from the STTx. - * - * This function returns a vector of transaction IDs by extracting them from - * the field array `sfRawTransactions` within the STTx. If the batch - * transaction IDs have already been computed and cached in `batchTxnIds_`, - * it returns the cached vector. Otherwise, it computes the transaction IDs, - * caches them, and then returns the vector. - * - * @return A vector of `uint256` containing the batch transaction IDs. - * - * @note The function asserts that the `sfRawTransactions` field array is not - * empty and that the size of the computed batch transaction IDs matches the - * size of the `sfRawTransactions` field array. - */ +void +STTx::buildBatchTxnIds() +{ + // Precondition: the template must have been applied first, so the fields + // (including sfRawTransactions) are canonical before the inner txns are + // hashed. The constructors call this immediately after applying the + // template; isFree() being false confirms a template is set. + XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template applied"); + if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions)) + return; + + auto const& raw = getFieldArray(sfRawTransactions); + + // Seated for any batch with raw transactions. The count is validated in + // preflight and at the relay boundary, so build every id here; this keeps + // the invariant batchTxnIds_->size() == rawTransactions.size(). + auto& ids = batchTxnIds_.emplace(); + ids.reserve(raw.size()); + for (STObject const& rb : raw) + ids.push_back(rb.getHash(HashPrefix::TransactionId)); +} + std::vector const& STTx::getBatchTransactionIDs() const { - XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : not a batch transaction"); + XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : batch transaction"); XRPL_ASSERT( - !getFieldArray(sfRawTransactions).empty(), - "STTx::getBatchTransactionIDs : empty raw transactions"); - - // The list of inner ids is built once, then reused on subsequent calls. - // After the list is built, it must always have the same size as the array - // `sfRawTransactions`. The assert below verifies that. - if (batchTxnIds_.empty()) - { - for (STObject const& rb : getFieldArray(sfRawTransactions)) - batchTxnIds_.push_back(rb.getHash(HashPrefix::TransactionId)); - } - + batchTxnIds_.has_value(), "STTx::getBatchTransactionIDs : batch transaction IDs built"); XRPL_ASSERT( - batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(), + batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(), "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); - return batchTxnIds_; + return *batchTxnIds_; } //------------------------------------------------------------------------------ @@ -727,13 +733,22 @@ invalidMPTAmountInTx(STObject const& tx) } static bool -isRawTransactionOkay(STObject const& st, std::string& reason) +isBatchRawTransactionOkay(STObject const& st, std::string& reason) { if (!st.isFieldPresent(sfRawTransactions)) return true; + // sfRawTransactions only appears on a Batch. passesLocalChecks runs on + // unverified user and peer input, so reject (rather than assert) a non-batch + // transaction that carries it. + if (st.getFieldU16(sfTransactionType) != ttBATCH) + { + reason = "Only Batch transactions may contain raw transactions."; + return false; + } + if (st.isFieldPresent(sfBatchSigners) && - st.getFieldArray(sfBatchSigners).size() > kMaxBatchTxCount) + st.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners) { reason = "Batch Signers array exceeds max entries."; return false; @@ -757,6 +772,12 @@ isRawTransactionOkay(STObject const& st, std::string& reason) } raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); + + // passesLocalChecks recurses back into isBatchRawTransactionOkay, + // but an inner can never be a batch (rejected above), so the + // recursion terminates at depth 1. + if (!passesLocalChecks(raw, reason)) + return false; } catch (std::exception const& e) { @@ -791,7 +812,7 @@ passesLocalChecks(STObject const& st, std::string& reason) return false; } - if (!isRawTransactionOkay(st, reason)) + if (!isBatchRawTransactionOkay(st, reason)) return false; return true; diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index e5c1d17b1a..a6f8192a2f 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -130,6 +130,8 @@ transResults() MAKE_ERROR(tefNO_TICKET, "Ticket is not in ledger."), MAKE_ERROR(tefNFTOKEN_IS_NOT_TRANSFERABLE, "The specified NFToken is not transferable."), MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."), + MAKE_ERROR(tefNO_DST_PARTIAL, "Partial payment to create account not allowed."), + MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."), MAKE_ERROR(telLOCAL_ERROR, "Local failure."), MAKE_ERROR(telBAD_DOMAIN, "Domain too long."), diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 9ab8779f03..0f6988543e 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1,6 +1,5 @@ #include -#include #include #include #include @@ -221,13 +220,15 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID)) return temINVALID; - if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1)) return temINVALID_FLAG; - XRPL_ASSERT( - ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || - !ctx.rules.enabled(featureBatch), - "Inner batch transaction must have a parent batch ID."); + // Reject if the inner batch flag and parentBatchId are inconsistent. + // A standalone tx with tfInnerBatchTxn but no parentBatchId is an + // attack attempt. A tx with parentBatchId but without tfInnerBatchTxn + // is a programming error. + if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value()) + return temINVALID_INNER_BATCH; return tesSUCCESS; } @@ -243,15 +244,19 @@ Transactor::preflight2(PreflightContext const& ctx) return *ret; } - // It should be impossible for the InnerBatchTxn flag to be set without - // featureBatch being enabled - XRPL_ASSERT_PARTS( - !ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch), - "xrpl::Transactor::preflight2", - "InnerBatch flag only set if feature enabled"); - // Skip signature check on batch inner transactions - if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch)) + // Skip the signature check on batch inner transactions. preflight1 already + // enforces both conditions; re-checking them as defense in depth guarantees + // we never return success (and so skip signature validation) for an inner + // transaction unless the amendment is enabled and it really sits inside a + // batch. + if (ctx.tx.isFlag(tfInnerBatchTxn)) + { + if (!ctx.rules.enabled(featureBatchV1_1)) + return temINVALID_FLAG; + if (!ctx.parentBatchId.has_value()) + return temINVALID_INNER_BATCH; return tesSUCCESS; + } // Do not add any checks after this point that are relevant for // batch inner transactions. They will be skipped. @@ -708,23 +713,26 @@ Transactor::checkSign( std::optional const& parentBatchId, AccountID const& idAccount, STObject const& sigObject, - beast::Journal const j) + beast::Journal const j, + bool permitUncreatedAccount) { { auto const sle = view.read(keylet::account(idAccount)); - if (view.rules().enabled(featureLendingProtocol) && isPseudoAccount(sle)) + if ((view.rules().enabled(featureLendingProtocol) || + view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) && + isPseudoAccount(sle)) { - // Pseudo-accounts can't sign transactions. This check is gated on - // the Lending Protocol amendment because that's the project it was - // added under, and it doesn't justify another amendment + // Pseudo-accounts can't sign transactions. This check is gated on a + // few different amendments so that it takes effect as soon as any of + // them is activated. return tefBAD_AUTH; } } auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); // Ignore signature check on batch inner transactions - if (parentBatchId && view.rules().enabled(featureBatch)) + if (parentBatchId && view.rules().enabled(featureBatchV1_1)) { // Defensive Check: These values are also checked in Batch::preflight if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || @@ -762,7 +770,16 @@ Transactor::checkSign( auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); auto const sleAccount = view.read(keylet::account(idAccount)); if (!sleAccount) - return terNO_ACCOUNT; + { + // An account that does not exist yet can only be authorized by its own + // master key, and only where an un-created signer is permitted (a batch + // whose earlier inner creates the account). Otherwise it cannot sign. + if (!permitUncreatedAccount) + return terNO_ACCOUNT; + if (idAccount != idSigner) + return tefBAD_AUTH; + return tesSUCCESS; + } return checkSingleSign(view, idSigner, idAccount, sleAccount, j); } @@ -775,50 +792,6 @@ Transactor::checkSign(PreclaimContext const& ctx) return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j); } -NotTEC -Transactor::checkBatchSign(PreclaimContext const& ctx) -{ - NotTEC ret = tesSUCCESS; - STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; - for (auto const& signer : signers) - { - auto const idAccount = signer.getAccountID(sfAccount); - - Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); - if (pkSigner.empty()) - { - if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j); - !isTesSuccess(ret)) - return ret; - } - else - { - // LCOV_EXCL_START - if (!publicKeyType(makeSlice(pkSigner))) - return tefBAD_AUTH; - // LCOV_EXCL_STOP - - auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); - auto const sleAccount = ctx.view.read(keylet::account(idAccount)); - - // A batch can include transactions from an un-created account ONLY - // when the account master key is the signer - if (!sleAccount) - { - if (idAccount != idSigner) - return tefBAD_AUTH; - - return tesSUCCESS; - } - - if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j); - !isTesSuccess(ret)) - return ret; - } - } - return ret; -} - NotTEC Transactor::checkSingleSign( ReadView const& view, @@ -1114,7 +1087,7 @@ Transactor::reset(XRPAmount fee) // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( - balance != beast::kZero && (!view().open() || balance >= fee), + (fee == beast::kZero || balance != beast::kZero) && (!view().open() || balance >= fee), "xrpl::Transactor::reset : valid balance"); // We retry/reject the transaction if the account balance is zero or diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index b70cb0d345..d85c4cfc40 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -39,30 +38,14 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules) auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); - // Ignore signature check on batch inner transactions - if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + // Batch inner transactions are never independently valid: they are applied + // within their batch, not through checkValidity. Reaching here means one was + // relayed or submitted on its own, so mark it bad regardless of the + // amendment (like PeerImp and NetworkOPs). + if (tx.isFlag(tfInnerBatchTxn)) { - // Defensive Check: These values are also checked in Batch::preflight - if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() || - tx.isFieldPresent(sfSigners)) - return {Validity::SigBad, "Malformed: Invalid inner batch transaction."}; - - // This block should probably have never been included in the - // original `Batch` implementation. An inner transaction never - // has a valid signature. - bool const neverValid = rules.enabled(fixBatchInnerSigs); - if (!neverValid) - { - std::string reason; - if (!passesLocalChecks(tx, reason)) - { - router.setFlags(id, kSfLocalbad); - return {Validity::SigGoodOnly, reason}; - } - - router.setFlags(id, kSfSiggood); - return {Validity::Valid, ""}; - } + router.setFlags(id, kSfSigbad); + return {Validity::SigBad, "Batch inner transactions are never considered validly signed."}; } if (any(flags & kSfSigbad)) @@ -183,6 +166,9 @@ applyBatchTransactions( // If the transaction should be applied push its changes to the // whole-batch view. + // NOTE: each inner tx is individually capped at kOversizeMetaDataCap; + // there is no aggregate cap here. Bounded by kMaxBatchTxCount * cap, + // which standalone txns can already produce in one ledger. if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) perTxBatchView.apply(batchView); diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 903707320b..1ac387d1b1 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -57,7 +57,7 @@ LoanSet::preflight(PreflightContext const& ctx) auto const& tx = ctx.tx; // Special case for Batch inner transactions - if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) && + if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) && !tx.isFieldPresent(sfCounterparty)) { auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0}); diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 7f1e4d8079..6e3883572a 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -363,16 +363,21 @@ Payment::preclaim(PreclaimContext const& ctx) // transaction would succeed. return tecNO_DST; } - if (ctx.view.open() && partialPaymentAllowed) + // A partial payment may not fund a new account. + if (partialPaymentAllowed) { - // You cannot fund an account with a partial payment. - // Make retry work smaller, by rejecting this. - JLOG(ctx.j.trace()) << "Delay transaction: Partial payment not " - "allowed to create account."; - - // Another transaction could create the account and then this - // transaction would succeed. - return telNO_DST_PARTIAL; + // Open view: the soft tel (unchanged). + if (ctx.view.open()) + { + // Make retry work smaller, by rejecting this. + JLOG(ctx.j.trace()) << "Delay transaction: Partial payment not " + "allowed to create account."; + return telNO_DST_PARTIAL; + } + // Inner batch txns are claimed on a closed view, where a tel is + // invalid, so use the tef. + if (ctx.parentBatchId && ctx.view.rules().enabled(featureBatchV1_1)) + return tefNO_DST_PARTIAL; } if (dstAmount < STAmount(ctx.view.fees().reserve)) { @@ -400,7 +405,7 @@ Payment::preclaim(PreclaimContext const& ctx) } // Payment with at least one intermediate step and uses transitive balances. - if ((hasPaths || sendMax || !dstAmount.native()) && ctx.view.open()) + if (hasPaths || sendMax || !dstAmount.native()) { STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths); @@ -408,7 +413,12 @@ Payment::preclaim(PreclaimContext const& ctx) return path.size() > kMaxPathLength; })) { - return telBAD_PATH_COUNT; + // Open view: the soft tel (unchanged). Inner batch txns are claimed + // on a closed view, where a tel is invalid, so use the tef. + if (ctx.view.open()) + return telBAD_PATH_COUNT; + if (ctx.parentBatchId && ctx.view.rules().enabled(featureBatchV1_1)) + return tefBAD_PATH_COUNT; } } diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index 64a62ac273..16d27f98d9 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,11 +23,13 @@ #include #include +#include #include #include #include #include #include +#include namespace xrpl { @@ -107,13 +111,13 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) } // Calculate the Signers/BatchSigners Fees - std::int32_t signerCount = 0; + std::uint32_t signerCount = 0; if (tx.isFieldPresent(sfBatchSigners)) { auto const& signers = tx.getFieldArray(sfBatchSigners); // LCOV_EXCL_START - if (signers.size() > kMaxBatchTxCount) + if (signers.size() > kMaxBatchSigners) { JLOG(debugLog().error()) << "BatchTrace: Batch Signers array exceeds max entries."; return XRPAmount{kInitialXrp}; @@ -128,7 +132,16 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) } else if (signer.isFieldPresent(sfSigners)) { - signerCount += signer.getFieldArray(sfSigners).size(); + auto const& nestedSigners = signer.getFieldArray(sfSigners); + // LCOV_EXCL_START + if (nestedSigners.size() > STTx::kMaxMultiSigners) + { + JLOG(debugLog().error()) + << "BatchTrace: Nested Signers array exceeds max entries."; + return kInitialXrp; + } + // LCOV_EXCL_STOP + signerCount += nestedSigners.size(); } } } @@ -149,7 +162,8 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation."; return XRPAmount{kInitialXrp}; } - if (txnFees + signerFees > maxAmount - batchBase) + XRPAmount const innerFees = txnFees + signerFees; + if (innerFees > maxAmount - batchBase) { JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in total fee calculation."; return XRPAmount{kInitialXrp}; @@ -157,7 +171,7 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) // LCOV_EXCL_STOP // 10 drops per batch signature + sum of inner tx fees + batchBase - return signerFees + txnFees + batchBase; + return innerFees + batchBase; } std::uint32_t @@ -227,6 +241,14 @@ Batch::preflight(PreflightContext const& ctx) return temARRAY_TOO_LARGE; } + if (ctx.tx.isFieldPresent(sfBatchSigners) && + ctx.tx.getFieldArray(sfBatchSigners).size() > kMaxBatchSigners) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "signers array exceeds " << kMaxBatchSigners << " entries."; + return temARRAY_TOO_LARGE; + } + // Validation Inner Batch Txns std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; @@ -384,46 +406,48 @@ Batch::preflight(PreflightContext const& ctx) NotTEC Batch::preflightSigValidated(PreflightContext const& ctx) { + XRPL_ASSERT( + ctx.tx.getTxnType() == ttBATCH, "xrpl::Batch::preflightSigValidated : batch transaction"); auto const parentBatchId = ctx.tx.getTransactionID(); auto const outerAccount = ctx.tx.getAccountID(sfAccount); auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); - // Build the signers list - std::unordered_set requiredSigners; + // Accounts that must sign the batch: each inner authorizer and counterparty + // (excluding the outer account), sorted and de-duplicated to match against + // the ascending, unique batch signers. + std::vector requiredSigners; + requiredSigners.reserve(kMaxBatchSigners); for (STObject const& rb : rawTxns) { - auto const innerAccount = rb.getAccountID(sfAccount); + // A delegated inner is signed by the delegate, not the account holder, + // so the delegate is the required signer when present. + AccountID const authorizer = rb.getFeePayer(); - // If the inner account is the same as the outer account, do not add the - // inner account to the required signers set. - if (innerAccount != outerAccount) - requiredSigners.insert(innerAccount); + // The outer account signs the batch itself, so it is never added to the + // required signers. + if (authorizer != outerAccount) + requiredSigners.push_back(authorizer); // Some transactions have a Counterparty, who must also sign the // transaction if they are not the outer account - if (auto const counterparty = rb.at(~sfCounterparty); + if (auto const counterparty = rb[~sfCounterparty]; counterparty && counterparty != outerAccount) - requiredSigners.insert(*counterparty); + requiredSigners.push_back(*counterparty); } + std::ranges::sort(requiredSigners); + auto const dupes = std::ranges::unique(requiredSigners); + requiredSigners.erase(dupes.begin(), dupes.end()); + + std::size_t numReqSignersMatched = 0; // Validation Batch Signers - std::unordered_set batchSigners; if (ctx.tx.isFieldPresent(sfBatchSigners)) { STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners); - // Check that the batch signers array is not too large. - if (signers.size() > kMaxBatchTxCount) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "signers array exceeds 8 entries."; - return temARRAY_TOO_LARGE; - } - - // Add batch signers to the set to ensure all signer accounts are - // unique. Meanwhile, remove signer accounts from the set of inner - // transaction accounts (`requiredSigners`). By the end of the loop, - // `requiredSigners` should be empty, indicating that all inner - // accounts are matched with signers. + // Batch signers must be strictly ascending and match the required + // signers exactly; since both are sorted, each must be the next + // required signer. + AccountID lastBatchSigner{beast::kZero}; for (auto const& signer : signers) { AccountID const signerAccount = signer.getAccountID(sfAccount); @@ -434,40 +458,66 @@ Batch::preflightSigValidated(PreflightContext const& ctx) return temBAD_SIGNER; } - if (!batchSigners.insert(signerAccount).second) + if (lastBatchSigner == signerAccount) { JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " << "duplicate signer found: " << signerAccount; - return temREDUNDANT; - } - - // Check that the batch signer is in the required signers set. - // Remove it if it does, as it can be crossed off the list. - if (requiredSigners.erase(signerAccount) == 0) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "no account signature for inner txn."; return temBAD_SIGNER; } - } - // Check the batch signers signatures. - auto const sigResult = ctx.tx.checkBatchSign(ctx.rules); + if (lastBatchSigner > signerAccount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "unsorted signers array: " << signerAccount; + return temBAD_SIGNER; + } + lastBatchSigner = signerAccount; - if (!sigResult) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "invalid batch txn signature: " << sigResult.error(); - return temBAD_SIGNATURE; + if (numReqSignersMatched >= requiredSigners.size() || + requiredSigners[numReqSignersMatched] != signerAccount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "missing signer or extra signer provided: " << signerAccount; + return temBAD_SIGNER; + } + ++numReqSignersMatched; } } - if (!requiredSigners.empty()) + // Every required signer must be matched. Also covers sfBatchSigners being + // absent while inner txns require signers (numReqSignersMatched stays 0). + if (numReqSignersMatched != requiredSigners.size()) { JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " << "invalid batch signers."; return temBAD_SIGNER; } + + return tesSUCCESS; +} + +NotTEC +Batch::checkBatchSign(PreclaimContext const& ctx) +{ + STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + // Reuse the standard signer-authorization rules so the batch and + // non-batch paths cannot drift. permitUncreatedAccount allows an inner + // from an account that an earlier inner creates, which must be + // authorized by its own master key. + auto const idAccount = signer.getAccountID(sfAccount); + if (auto const ret = Transactor::checkSign( + ctx.view, + ctx.flags, + ctx.parentBatchId, + idAccount, + signer, + ctx.j, + /*permitUncreatedAccount=*/true); + !isTesSuccess(ret)) + return ret; + } return tesSUCCESS; } @@ -479,7 +529,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx) * corresponding error code. * * Next, it verifies the batch-specific signature requirements by calling - * Transactor::checkBatchSign. If this check fails, it also returns the + * Batch::checkBatchSign. If this check fails, it also returns the * corresponding error code. * * If both checks succeed, the function returns tesSUCCESS. @@ -494,8 +544,11 @@ Batch::checkSign(PreclaimContext const& ctx) if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret)) return ret; - if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret)) - return ret; + if (ctx.tx.isFieldPresent(sfBatchSigners)) + { + if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret)) + return ret; + } return tesSUCCESS; } diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 6aaae62155..f9c156fe06 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -47,12 +48,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -61,15 +64,17 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include #include -#include #include #include #include @@ -200,16 +205,13 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - bool const withInnerSigFix = features[fixBatchInnerSigs]; - for (bool const withBatch : {true, false}) { - testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled") - << ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled"); + testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled"); - auto const amend = withBatch ? features : features - featureBatch; + auto const amend = withBatch ? features : features - featureBatchV1_1; - test::jtx::Env env{*this, amend}; + Env env{*this, amend}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -230,12 +232,10 @@ class Batch_test : public beast::unit_test::Suite } // tfInnerBatchTxn - // If the feature is disabled, the transaction fails with - // temINVALID_FLAG. If the feature is enabled, the transaction fails - // early in checkValidity() + // A standalone transaction carrying this flag is never valid, so it + // is rejected early in checkValidity() regardless of the amendment. { - auto const txResult = withBatch ? Ter(telENV_RPC_FAILED) : Ter(temINVALID_FLAG); - env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), txResult); + env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED)); env.close(); } @@ -254,7 +254,7 @@ class Batch_test : public beast::unit_test::Suite //---------------------------------------------------------------------- // preflight - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -431,6 +431,41 @@ class Batch_test : public beast::unit_test::Suite env.close(); } + // temINVALID_INNER_BATCH: tfInnerBatchTxn set but no parentBatchId. + { + auto jtx = env.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn)); + PreflightContext const pfCtx( + env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); + auto const pf = Transactor::invokePreflight(pfCtx); + BEAST_EXPECT(pf == temINVALID_INNER_BATCH); + } + + // temINVALID_FLAG: tfInnerBatchTxn set but featureBatchV1_1 disabled. + { + Env disabledEnv{*this, features - featureBatchV1_1}; + disabledEnv.fund(XRP(10000), alice, bob); + disabledEnv.close(); + auto jtx = disabledEnv.jt(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn)); + PreflightContext const pfCtx( + disabledEnv.app(), + *jtx.stx, + disabledEnv.current()->rules(), + TapNone, + disabledEnv.journal); + auto const pf = Transactor::invokePreflight(pfCtx); + BEAST_EXPECT(pf == temINVALID_FLAG); + } + + // temINVALID_INNER_BATCH: parentBatchId set but tfInnerBatchTxn not + // set. + { + auto jtx = env.jt(pay(alice, bob, XRP(1))); + PreflightContext const pfCtx( + env.app(), *jtx.stx, uint256{1}, env.current()->rules(), TapBatch, env.journal); + auto const pf = Transactor::invokePreflight(pfCtx); + BEAST_EXPECT(pf == temINVALID_INNER_BATCH); + } + // temBAD_FEE: Batch: inner txn must have a fee of 0. { auto const seq = env.seq(alice); @@ -537,16 +572,16 @@ class Batch_test : public beast::unit_test::Suite env.close(); } - // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8 - // entries. + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds + // kMaxBatchSigners entries. // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() { auto const seq = env.seq(alice); - auto const batchFee = batch::calcBatchFee(env, 9, 2); + auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2); env(batch::outer(alice, seq, batchFee, tfAllOrNothing), batch::Inner(pay(alice, bob, XRP(10)), seq + 1), batch::Inner(pay(alice, bob, XRP(5)), seq + 2), - batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice), + batch::Sig(std::vector(kMaxBatchSigners + 1, bob)), Ter(telENV_RPC_FAILED)); env.close(); } @@ -563,7 +598,7 @@ class Batch_test : public beast::unit_test::Suite env.close(); } - // temREDUNDANT: Batch: duplicate signer found + // temBAD_SIGNER: Batch: duplicate signer (caught by ascending order check) { auto const seq = env.seq(alice); auto const batchFee = batch::calcBatchFee(env, 2, 2); @@ -571,7 +606,7 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(alice, bob, XRP(10)), seq + 1), batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)), batch::Sig(bob, bob), - Ter(temREDUNDANT)); + Ter(temBAD_SIGNER)); env.close(); } @@ -611,7 +646,13 @@ class Batch_test : public beast::unit_test::Suite batch::Inner(pay(bob, alice, XRP(5)), bobSeq)); Serializer msg; - serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs()); + serializeBatch( + msg, + jt.stx->getAccountID(sfAccount), + jt.stx->getSeqValue(), + tfAllOrNothing, + jt.stx->getBatchTransactionIDs()); + finishMultiSigningData(bob.id(), msg); auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice()); jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] = bob.human(); @@ -620,7 +661,7 @@ class Batch_test : public beast::unit_test::Suite jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] = strHex(Slice{sig.data(), sig.size()}); - env(jt.jv, Ter(temBAD_SIGNATURE)); + env(jt.jv, Ter(telENV_RPC_FAILED)); env.close(); } @@ -649,7 +690,7 @@ class Batch_test : public beast::unit_test::Suite //---------------------------------------------------------------------- // preclaim - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -890,7 +931,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -971,6 +1012,46 @@ class Batch_test : public beast::unit_test::Suite env(jt.jv, batch::Sig(bob), Ter(telENV_RPC_FAILED)); env.close(); } + + // Inner OfferCreate with MPT TakerPays. Valid under featureMPTokensV2. + { + MPTIssue const issue(makeMptID(1, alice)); + STAmount const mptAmt{issue, UINT64_C(100)}; + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + json::Value tx1; + tx1[jss::TransactionType] = jss::OfferCreate; + tx1[jss::Account] = alice.human(); + tx1[jss::TakerPays] = mptAmt.getJson(JsonOptions::Values::None); + tx1[jss::TakerGets] = XRP(10).value().getJson(JsonOptions::Values::None); + + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(tx1, seq + 1), + batch::Inner(pay(alice, bob, XRP(1)), seq + 2), + Ter(tesSUCCESS)); + env.close(); + } + + // Invalid: inner txn with invalid memo (non-URL-safe MemoType) + { + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx1 = pay(alice, bob, XRP(10)); + auto& ma = tx1["Memos"]; + auto& mi = ma[ma.size()]; + auto& m = mi["Memo"]; + m["MemoType"] = strHex(std::string("\x01\x02\x03", 3)); + m["MemoData"] = strHex(std::string("test")); + + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(tx1, seq + 1), + batch::Inner(pay(alice, bob, XRP(1)), seq + 2), + Ter(telENV_RPC_FAILED)); + env.close(); + } } void @@ -981,7 +1062,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1239,7 +1320,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee Without Signer { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1261,7 +1342,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee With MultiSign { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1288,7 +1369,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee With MultiSign + BatchSigners { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1317,7 +1398,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee With MultiSign + BatchSigners.Signers { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1349,7 +1430,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee With BatchSigners { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1373,7 +1454,7 @@ class Batch_test : public beast::unit_test::Suite // Bad Fee Dynamic Fee Calculation { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1412,7 +1493,7 @@ class Batch_test : public beast::unit_test::Suite // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries. { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1437,7 +1518,7 @@ class Batch_test : public beast::unit_test::Suite // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1465,41 +1546,52 @@ class Batch_test : public beast::unit_test::Suite }); } - // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries. + // Regression: the relay-boundary local check (isBatchRawTransactionOkay) + // caps the signers array at kMaxBatchSigners, matching Batch::preflight - + // not kMaxBatchTxCount. A batch with more than kMaxBatchTxCount but at + // most kMaxBatchSigners signers must get past local checks and reach + // signer validation. Before the cap was aligned it was wrongly rejected + // at the boundary (telENV_RPC_FAILED) before preflight ran; now it + // reaches preflightSigValidated and fails there as extra signers + // (temBAD_SIGNER) rather than being dropped pre-engine. { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); env.close(); + // Over kMaxBatchTxCount (8) but within kMaxBatchSigners (24). + std::size_t const signerCount = kMaxBatchTxCount + 1; + auto const aliceSeq = env.seq(alice); - auto const batchFee = batch::calcBatchFee(env, 9, 2); + auto const batchFee = batch::calcBatchFee(env, signerCount, 2); env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2), - batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob), - Ter(telENV_RPC_FAILED)); + batch::Sig(std::vector(signerCount, bob)), + Ter(temBAD_SIGNER)); env.close(); } - // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. + // temARRAY_TOO_LARGE: Batch preflight: signers array exceeds + // kMaxBatchSigners entries. { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); env.fund(XRP(10000), alice, bob); env.close(); - auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const batchFee = batch::calcBatchFee(env, kMaxBatchSigners + 1, 2); auto const aliceSeq = env.seq(alice); auto jt = env.jtnofill( batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), batch::Inner(pay(alice, bob, XRP(5)), aliceSeq + 2), - batch::Sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob)); + batch::Sig(std::vector(kMaxBatchSigners + 1, bob))); env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, j); @@ -1517,7 +1609,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1677,7 +1769,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1966,7 +2058,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2267,7 +2359,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2538,22 +2630,16 @@ class Batch_test : public beast::unit_test::Suite void doTestInnerSubmitRPC(FeatureBitset features, bool withBatch) { - bool const withInnerSigFix = features[fixBatchInnerSigs]; + std::string const testName = + std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": "; - std::string const testName = [&]() { - std::stringstream ss; - ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled") - << ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": "; - return ss.str(); - }(); - - auto const amend = withBatch ? features : features - featureBatch; + auto const amend = withBatch ? features : features - featureBatchV1_1; using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, amend}; - if (!BEAST_EXPECT(amend[featureBatch] == withBatch)) + Env env{*this, amend}; + if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch)) return; auto const alice = Account("alice"); @@ -2562,37 +2648,20 @@ class Batch_test : public beast::unit_test::Suite env.fund(XRP(10000), alice, bob); env.close(); - auto submitAndValidate = [&](std::string caseName, - Slice const& slice, - int line, - std::optional expectedEnabled = std::nullopt, - std::optional expectedDisabled = std::nullopt, - bool expectInvalidFlag = false) { - testcase << testName << caseName - << (expectInvalidFlag ? " - Expected to reach tx engine!" : ""); + // Any transaction carrying tfInnerBatchTxn is rejected in checkValidity() + // before it reaches the tx engine, regardless of its signing fields or + // whether the amendment is enabled. + auto submitAndValidate = [&](std::string caseName, Slice const& slice, int line) { + testcase << testName << caseName; auto const jrr = env.rpc("submit", strHex(slice))[jss::result]; - auto const expected = withBatch - ? expectedEnabled.value_or( - "fails local checks: Malformed: Invalid inner batch " - "transaction.") - : expectedDisabled.value_or("fails local checks: Empty SigningPubKey."); - if (expectInvalidFlag) - { - expect( - jrr[jss::status] == "success" && jrr[jss::engine_result] == "temINVALID_FLAG", - pretty(jrr), - __FILE__, - line); - } - else - { - expect( - jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" && - jrr[jss::error_exception] == expected, - pretty(jrr), - __FILE__, - line); - } + expect( + jrr[jss::status] == "error" && jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Batch inner transactions are never " + "considered validly signed.", + pretty(jrr), + __FILE__, + line); env.close(); }; @@ -2621,12 +2690,7 @@ class Batch_test : public beast::unit_test::Suite STParsedJSONObject parsed("test", txn.getTxn()); Serializer s; parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access) - submitAndValidate( - "SigningPubKey set", - s.slice(), - __LINE__, - std::nullopt, - "fails local checks: Invalid signature."); + submitAndValidate("SigningPubKey set", s.slice(), __LINE__); } // Invalid RPC Submission: Signers @@ -2640,12 +2704,7 @@ class Batch_test : public beast::unit_test::Suite STParsedJSONObject parsed("test", txn.getTxn()); Serializer s; parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access) - submitAndValidate( - "Signers set", - s.slice(), - __LINE__, - std::nullopt, - "fails local checks: Invalid Signers array size."); + submitAndValidate("Signers set", s.slice(), __LINE__); } { @@ -2656,8 +2715,7 @@ class Batch_test : public beast::unit_test::Suite STParsedJSONObject parsed("test", jt.jv); Serializer s; parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access) - submitAndValidate( - "Fully signed", s.slice(), __LINE__, std::nullopt, std::nullopt, !withBatch); + submitAndValidate("Fully signed", s.slice(), __LINE__); } // Invalid RPC Submission: tfInnerBatchTxn @@ -2670,13 +2728,7 @@ class Batch_test : public beast::unit_test::Suite STParsedJSONObject parsed("test", txn.getTxn()); Serializer s; parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access) - submitAndValidate( - "No signing fields set", - s.slice(), - __LINE__, - "fails local checks: Empty SigningPubKey.", - "fails local checks: Empty SigningPubKey.", - withBatch && !withInnerSigFix); + submitAndValidate("No signing fields set", s.slice(), __LINE__); } // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction @@ -2687,7 +2739,7 @@ class Batch_test : public beast::unit_test::Suite { STTx const amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) { obj.setAccountID(sfAccount, AccountID()); - obj.setFieldH256(sfAmendment, fixBatchInnerSigs); + obj.setFieldH256(sfAmendment, featureBatchV1_1); obj.setFieldU32(sfLedgerSequence, seq); obj.setFieldU32(sfFlags, tfInnerBatchTxn); }); @@ -2695,13 +2747,7 @@ class Batch_test : public beast::unit_test::Suite STParsedJSONObject parsed("test", txn.getTxn()); Serializer s; parsed.object->add(s); // NOLINT(bugprone-unchecked-optional-access) - submitAndValidate( - "Pseudo-transaction", - s.slice(), - __LINE__, - withInnerSigFix ? "fails local checks: Empty SigningPubKey." - : "fails local checks: Cannot submit pseudo transactions.", - "fails local checks: Empty SigningPubKey."); + submitAndValidate("Pseudo-transaction", s.slice(), __LINE__); } } @@ -2722,7 +2768,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2771,6 +2817,93 @@ class Batch_test : public beast::unit_test::Suite // Alice pays XRP & Fee; Bob receives XRP BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee); BEAST_EXPECT(env.balance(bob) == XRP(1000)); + + // An inner partial payment must never fund a new account. With + // featureBatchV1_1 the partial-payment-to-create check runs on the + // batch's closed view and rejects it (tefNO_DST_PARTIAL). A USD->XRP path is + // set up so that, absent the check, the partial payment would deliver + // XRP and create carol; the check must block exactly that. A + // cross-currency shape (IOU SendMax, XRP Amount) also lets the inner + // pass preflight and reach the preclaim check. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const gw = Account("gw"); + auto const carol = Account("carol"); // never funded + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, gw); + env.close(); + env.memoize(carol); + + env(trust(alice, usd(100000))); + env(pay(gw, alice, usd(10000))); + env(offer(gw, usd(2000), XRP(2000))); // USD -> XRP liquidity + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto pp = pay(alice, carol, XRP(1000)); + pp[jss::SendMax] = usd(2000).value().getJson(JsonOptions::Values::None); + pp[jss::Flags] = tfPartialPayment; + + env(batch::outer(alice, seq, batchFee, tfIndependent), + batch::Inner(pp, seq + 1), + batch::Inner(noop(alice), seq + 2), + Ter(tesSUCCESS)); + env.close(); + + // carol was not created despite the available path. + BEAST_EXPECT(!env.le(carol)); + } + } + + void + testCheckAllSignatures(FeatureBitset features) + { + testcase("check all signatures"); + + using namespace test::jtx; + using namespace std::literals; + + // Verifies that checkBatchSign validates all signers even when an + // unfunded account (signed with its master key) appears first in the + // sorted signer list. A funded account with an invalid signature must + // still be rejected with tefBAD_AUTH. + + Env env{*this, features}; + + auto const alice = Account("alice"); + // "aaa" sorts before other accounts alphabetically, ensuring the + // unfunded account is checked first in the sorted signer list + auto const unfunded = Account("aaa"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, carol); + env.close(); + + // Verify sort order: unfunded.id() < carol.id() + BEAST_EXPECT(unfunded.id() < carol.id()); + + auto const seq = env.seq(alice); + auto const ledSeq = env.current()->seq(); + auto const batchFee = batch::calcBatchFee(env, 2, 3); + + // The batch includes: + // 1. alice pays unfunded (to create unfunded's account) + // 2. unfunded does a noop (signed by unfunded's master key - valid) + // 3. carol pays alice (signed by alice's key - INVALID since alice is + // not carol's regular key) + // + // checkBatchSign must validate all signers regardless of order. + // This must fail with tefBAD_AUTH. + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, unfunded, XRP(100)), seq + 1), + batch::Inner(noop(unfunded), ledSeq), + batch::Inner(pay(carol, alice, XRP(1000)), env.seq(carol)), + batch::Sig(unfunded, Reg{carol, alice}), + Ter(tefBAD_AUTH)); + env.close(); } void @@ -2781,7 +2914,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2845,7 +2978,7 @@ class Batch_test : public beast::unit_test::Suite // tfIndependent: account delete success { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2897,7 +3030,7 @@ class Batch_test : public beast::unit_test::Suite // tfIndependent: account delete fails { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -2957,7 +3090,7 @@ class Batch_test : public beast::unit_test::Suite // tfAllOrNothing: account delete fails { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3009,7 +3142,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; - test::jtx::Env env{*this, features}; + Env env{*this, features}; Account const issuer{"issuer"}; // For simplicity, lender will be the sole actor for the vault & @@ -3186,7 +3319,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3322,7 +3455,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3396,7 +3529,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3470,7 +3603,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3531,7 +3664,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3591,7 +3724,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -3666,7 +3799,7 @@ class Batch_test : public beast::unit_test::Suite // overwritten by the payment in the batch transaction. Because the // terPRE_SEQ is outside of the batch this noop transaction will ge // reapplied in the following ledger - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); env.close(); @@ -3729,7 +3862,7 @@ class Batch_test : public beast::unit_test::Suite // IMPORTANT: The batch txn is applied first, then the noop txn. // Because of this ordering, the noop txn is not applied and is // overwritten by the payment in the batch transaction. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -3783,7 +3916,7 @@ class Batch_test : public beast::unit_test::Suite // IMPORTANT: The batch txn is applied first, then the noop txn. // Because of this ordering, the noop txn is not applied and is // overwritten by the payment in the batch transaction. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -3831,7 +3964,7 @@ class Batch_test : public beast::unit_test::Suite // Outer Batch terPRE_SEQ { - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); env.close(); @@ -3905,7 +4038,7 @@ class Batch_test : public beast::unit_test::Suite // IMPORTANT: The batch txn is applied first, then the noop txn. // Because of this ordering, the noop txn is not applied and is // overwritten by the payment in the batch transaction. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -3964,7 +4097,7 @@ class Batch_test : public beast::unit_test::Suite // IMPORTANT: The batch txn is applied first, then the noop txn. // Because of this ordering, the noop txn is not applied and is // overwritten by the payment in the batch transaction. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -4037,7 +4170,7 @@ class Batch_test : public beast::unit_test::Suite // batch will run in the close ledger process. The batch will be // allied and then retry this transaction in the current ledger. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -4100,7 +4233,7 @@ class Batch_test : public beast::unit_test::Suite // Create Object Before Batch Txn { - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -4163,7 +4296,7 @@ class Batch_test : public beast::unit_test::Suite // batch will run in the close ledger process. The batch will be // applied and then retry this transaction in the current ledger. - test::jtx::Env env{*this, features}; + Env env{*this, features}; env.fund(XRP(10000), alice, bob); env.close(); @@ -4226,7 +4359,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -4265,7 +4398,7 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - test::jtx::Env env{*this, features}; + Env env{*this, features}; XRPAmount const baseFee = env.current()->fees().base; auto const alice = Account("alice"); @@ -4359,9 +4492,12 @@ class Batch_test : public beast::unit_test::Suite using namespace test::jtx; using namespace std::literals; - // only outer batch transactions are counter towards the queue size + // A Batch is never queued. Under open-ledger congestion a Batch that + // pays only its base fee cannot apply directly and, unlike an ordinary + // transaction, is rejected outright rather than held in the queue. A + // Batch that pays the escalated open-ledger fee applies directly. { - test::jtx::Env env{ + Env env{ *this, makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}), features, @@ -4378,7 +4514,7 @@ class Batch_test : public beast::unit_test::Suite env.fund(XRP(10000), noripple(carol)); env.close(env.now() + 5s, 10000ms); - // Fill the ledger + // Fill the open ledger so escalation is active. env(noop(alice)); env(noop(alice)); env(noop(alice)); @@ -4391,33 +4527,28 @@ class Batch_test : public beast::unit_test::Suite auto const bobSeq = env.seq(bob); auto const batchFee = batch::calcBatchFee(env, 1, 2); - // Queue Batch - { - env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), - batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), - batch::Inner(pay(bob, alice, XRP(5)), bobSeq), - batch::Sig(bob), - Ter(terQUEUED)); - } + // Paying only the base fee, the Batch cannot enter the congested + // open ledger and is rejected rather than queued. + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::Inner(pay(bob, alice, XRP(5)), bobSeq), + batch::Sig(bob), + Ter(telCAN_NOT_QUEUE)); - checkMetrics(*this, env, 2, std::nullopt, 3, 2); + // The Batch was not queued; only carol's transaction is queued. + checkMetrics(*this, env, 1, std::nullopt, 3, 2); - // Replace Queued Batch - { - env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing), - batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), - batch::Inner(pay(bob, alice, XRP(5)), bobSeq), - batch::Sig(bob), - Ter(tesSUCCESS)); - env.close(); - } - - checkMetrics(*this, env, 0, 12, 1, 6); + // Paying the escalated open-ledger fee, the Batch applies directly. + env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::Inner(pay(bob, alice, XRP(5)), bobSeq), + batch::Sig(bob), + Ter(tesSUCCESS)); } // inner batch transactions are counter towards the ledger tx count { - test::jtx::Env env{ + Env env{ *this, makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}), features, @@ -4457,6 +4588,50 @@ class Batch_test : public beast::unit_test::Suite env(noop(carol), Ter(terQUEUED)); checkMetrics(*this, env, 1, std::nullopt, 3, 2); } + + // A Batch is never queued, so it also cannot sit behind the account's + // already-queued transactions: with a sequence gap it cannot apply + // directly and is rejected rather than queued. + { + Env env{ + *this, + makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}), + features, + nullptr, + beast::Severity::Error}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + + // Fill the open ledger so subsequent transactions queue. + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + auto const aliceSeq = env.seq(alice); + + // Queue two normal transactions for alice. + env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED)); + env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED)); + checkMetrics(*this, env, 2, std::nullopt, 3, 2); + + // The Batch's sequence sits behind the queued transactions, so it + // cannot apply directly and is rejected rather than queued. + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3), + batch::Inner(pay(bob, alice, XRP(5)), bobSeq), + batch::Sig(bob), + Ter(telCAN_NOT_QUEUE)); + + // The two queued transactions are untouched. + checkMetrics(*this, env, 2, std::nullopt, 3, 2); + } } void @@ -4508,6 +4683,376 @@ class Batch_test : public beast::unit_test::Suite } } + void + testBatchDelegateConsent(FeatureBitset features) + { + testcase("batch delegate consent"); + + using namespace test::jtx; + using namespace std::literals; + + // Delegate consent bypass. + // + // Alice delegates Payment to Bob. A Batch carries an inner Payment with + // Delegate=Bob, but Bob never signs the batch (and, because the inner + // Account == the outer Account, no BatchSigners are required at all). + // Batch waives the per-inner signature check, and preflightSigValidated + // derives required signers from sfAccount only -- it ignores sfDelegate + // -- so Bob's delegated authority is exercised without Bob's consent. + // + // SECURE EXPECTATION: the delegate (Bob) must authorize the inner txn, + // so the batch must be rejected (temBAD_SIGNER) when Bob does not sign. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, mallory); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1); + inner[jss::Delegate] = bob.human(); + + // Bob is NOT among the batch signers; he never consents. + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + inner, + batch::Inner(pay(alice, mallory, XRP(1)), seq + 2), + Ter(temBAD_SIGNER)); + env.close(); + + // No funds should have moved to Mallory. + BEAST_EXPECT(env.balance(mallory) == preMallory); + } + + // Self-grant attribution forgery. + // + // With no pre-existing delegation, Alice places DelegateSet(authorize + // Bob) and a Delegate=Bob action in the SAME batch. The second inner + // reads the delegation created by the first from the batch's running + // view, and Bob never signs -- manufacturing on-chain attribution of + // the action to Bob without his consent. + // + // SECURE EXPECTATION: rejected (temBAD_SIGNER) -- Bob must consent, and + // a grant cannot be created and exercised within the same atomic batch. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, mallory); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2); + inner[jss::Delegate] = bob.human(); + + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1), + inner, + Ter(temBAD_SIGNER)); + env.close(); + + BEAST_EXPECT(env.balance(mallory) == preMallory); + } + + // Legitimate counterpart: the same atomic grant-and-use is allowed when + // the delegate co-signs the batch -- consent is present, so it succeeds. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, mallory); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + + auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 2); + inner[jss::Delegate] = bob.human(); + + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(delegate::set(alice, bob, {"Payment"}), seq + 1), + inner, + batch::Sig(bob), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000)); + } + + // Multi-account: a delegated inner from a non-outer account also + // requires the delegate's signature (not the account holder's). + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(delegate::set(bob, carol, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq); + inner[jss::Delegate] = carol.human(); + + // Carol (the delegate) does not sign. + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + inner, + batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + Ter(temBAD_SIGNER)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == preAlice); + } + + // Wrong signer: a batch signature from someone other than the named + // delegate does not satisfy the requirement. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, carol, mallory); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + + auto inner = batch::Inner(pay(alice, mallory, XRP(1000)), seq + 1); + inner[jss::Delegate] = bob.human(); + + // Carol signs instead of the named delegate Bob. + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + inner, + batch::Inner(pay(alice, mallory, XRP(1)), seq + 2), + batch::Sig(carol), + Ter(temBAD_SIGNER)); + env.close(); + + BEAST_EXPECT(env.balance(mallory) == preMallory); + } + + // Delegate is the outer account: the outer signature already provides + // the delegate's consent, so no BatchSigners are required. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + // Bob delegates Payment to Alice, who is also the batch submitter. + env(delegate::set(bob, alice, {"Payment"})); + env.close(); + + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq); + inner[jss::Delegate] = alice.human(); + + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + inner, + batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + Ter(tesSUCCESS)); + env.close(); + + // Net: Bob sends 1 to Alice, receives 2 from Alice. + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // Principal signs instead of the delegate. Bob delegates Payment to + // Carol and an inner pays from Bob with Delegate=Carol, so Carol -- not + // Bob -- is the required batch signer. Bob's own signature (the account + // holder) does not satisfy the delegate-consent requirement. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(delegate::set(bob, carol, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto inner = batch::Inner(pay(bob, alice, XRP(1)), bobSeq); + inner[jss::Delegate] = carol.human(); + + // Bob (the principal) signs in place of Carol (the delegate). + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + inner, + batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::Sig(bob), + Ter(temBAD_SIGNER)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == preAlice); + } + + // Control for the revocation case below: identical setup, but inner 1 + // is a benign payment instead of a revocation. With Alice's delegation + // intact, the delegated inner 2 succeeds -- proving the failure in the + // revocation case is caused by the revocation, not the setup. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, mallory); + env.close(); + + env(delegate::set(bob, alice, {"Payment"})); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(bob); + + auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2); + inner2[jss::Delegate] = alice.human(); + + // Inner 1 is Bob's own payment (no revocation); Alice co-signs for + // the delegated inner 2. + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(bob, seq, batchFee, tfIndependent), + batch::Inner(pay(bob, alice, XRP(1)), seq + 1), + inner2, + batch::Sig(alice)); + env.close(); + + std::vector const testCases = { + {.index = 0, + .txType = "Batch", + .result = "tesSUCCESS", + .txHash = batchID, + .batchID = std::nullopt}, + {.index = 1, + .txType = "Payment", + .result = "tesSUCCESS", + .txHash = txIDs[0], + .batchID = batchID}, + {.index = 2, + .txType = "Payment", + .result = "tesSUCCESS", + .txHash = txIDs[1], + .batchID = batchID}, + }; + validateClosedLedger(env, testCases); + + // The delegated payment applied: Mallory received the funds. + BEAST_EXPECT(env.balance(mallory) == preMallory + XRP(1000)); + } + + // Revocation within the same batch. Bob grants Alice Payment permission + // beforehand; an earlier inner revokes it before a later delegated + // inner tries to use it. Alice must still co-sign (delegate consent is + // derived from the static tx), but the revoked permission makes the + // delegated inner fail at apply time -- a grant cannot be used after it + // is removed earlier in the same batch. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const mallory = Account("mallory"); + env.fund(XRP(10000), alice, bob, mallory); + env.close(); + + env(delegate::set(bob, alice, {"Payment"})); + env.close(); + + auto const preMallory = env.balance(mallory); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(bob); + + // Inner 2: Alice acts as Bob's delegate, so Alice is the required + // batch signer. + auto inner2 = batch::Inner(pay(bob, mallory, XRP(1000)), seq + 2); + inner2[jss::Delegate] = alice.human(); + + // tfIndependent: inner 1 (the revocation) applies; inner 2 then + // fails for lack of permission without reverting inner 1. An empty + // permission list deletes the Delegate object. + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(bob, seq, batchFee, tfIndependent), + batch::Inner(delegate::set(bob, alice, {}), seq + 1), + inner2, + batch::Sig(alice)); + env.close(); + + std::vector const testCases = { + {.index = 0, + .txType = "Batch", + .result = "tesSUCCESS", + .txHash = batchID, + .batchID = std::nullopt}, + {.index = 1, + .txType = "DelegateSet", + .result = "tesSUCCESS", + .txHash = txIDs[0], + .batchID = batchID}, + // inner 2 fails: Alice's permission was revoked in inner 1. + }; + validateClosedLedger(env, testCases); + + // The delegated payment never applied. + BEAST_EXPECT(env.balance(mallory) == preMallory); + BEAST_EXPECT(env.rpc("tx", txIDs[1])[jss::result][jss::error] == "txnNotFound"); + } + } + void testBatchDelegate(FeatureBitset features) { @@ -4518,7 +5063,7 @@ class Batch_test : public beast::unit_test::Suite // delegated non atomic inner { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -4533,17 +5078,20 @@ class Batch_test : public beast::unit_test::Suite auto const preAlice = env.balance(alice); auto const preBob = env.balance(bob); - auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const batchFee = batch::calcBatchFee(env, 1, 2); auto const seq = env.seq(alice); auto tx = batch::Inner(pay(alice, bob, XRP(1)), seq + 1); tx[jss::Delegate] = bob.human(); + // The delegate (Bob) authorizes the delegated inner, so Bob must + // provide the batch signature. auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(alice, seq, batchFee, tfAllOrNothing), tx, - batch::Inner(pay(alice, bob, XRP(2)), seq + 2)); + batch::Inner(pay(alice, bob, XRP(2)), seq + 2), + batch::Sig(bob)); env.close(); std::vector const testCases = { @@ -4575,7 +5123,7 @@ class Batch_test : public beast::unit_test::Suite // delegated atomic inner { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -4598,13 +5146,15 @@ class Batch_test : public beast::unit_test::Suite auto tx = batch::Inner(pay(bob, alice, XRP(1)), bobSeq); tx[jss::Delegate] = carol.human(); + // Carol is the delegate authorizing the inner txn on Bob's behalf, + // so Carol -- not Bob -- must provide the batch signature. auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), tx, batch::Inner(pay(alice, bob, XRP(2)), aliceSeq + 1), - batch::Sig(bob)); + batch::Sig(carol)); env.close(); std::vector const testCases = { @@ -4639,7 +5189,7 @@ class Batch_test : public beast::unit_test::Suite // this also makes sure tfInnerBatchTxn won't block delegated AccountSet // with granular permission { - test::jtx::Env env{*this, features}; + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -4654,19 +5204,22 @@ class Batch_test : public beast::unit_test::Suite auto const preAlice = env.balance(alice); auto const preBob = env.balance(bob); - auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const batchFee = batch::calcBatchFee(env, 1, 2); auto const seq = env.seq(alice); auto tx = batch::Inner(noop(alice), seq + 1); std::string const domain = "example.com"; tx[sfDomain.jsonName] = strHex(domain); tx[jss::Delegate] = bob.human(); + // Bob is the delegate authorizing the inner AccountSet, so Bob must + // provide the batch signature. auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(alice, seq, batchFee, tfAllOrNothing), tx, - batch::Inner(pay(alice, bob, XRP(2)), seq + 2)); + batch::Inner(pay(alice, bob, XRP(2)), seq + 2), + batch::Sig(bob)); env.close(); std::vector const testCases = { @@ -4700,7 +5253,7 @@ class Batch_test : public beast::unit_test::Suite // this also makes sure tfInnerBatchTxn won't block delegated // MPTokenIssuanceSet with granular permission { - test::jtx::Env env{*this, features}; + Env env{*this, features}; Account const alice{"alice"}; Account const bob{"bob"}; env.fund(XRP(100000), alice, bob); @@ -4717,7 +5270,7 @@ class Batch_test : public beast::unit_test::Suite env.close(); auto const seq = env.seq(alice); - auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const batchFee = batch::calcBatchFee(env, 1, 2); json::Value jv1; jv1[sfTransactionType] = jss::MPTokenIssuanceSet; @@ -4735,12 +5288,15 @@ class Batch_test : public beast::unit_test::Suite jv2[sfMPTokenIssuanceID] = to_string(mptID); jv2[sfFlags] = tfMPTUnlock; + // Both inners are delegated to Bob, so Bob must provide the batch + // signature (one signer covers both). auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(alice, seq, batchFee, tfAllOrNothing), batch::Inner(jv1, seq + 1), - batch::Inner(jv2, seq + 2)); + batch::Inner(jv2, seq + 2), + batch::Sig(bob)); env.close(); std::vector const testCases = { @@ -4767,7 +5323,7 @@ class Batch_test : public beast::unit_test::Suite // this also makes sure tfInnerBatchTxn won't block delegated TrustSet // with granular permission { - test::jtx::Env env{*this, features}; + Env env{*this, features}; Account const gw{"gw"}; Account const alice{"alice"}; Account const bob{"bob"}; @@ -4781,19 +5337,22 @@ class Batch_test : public beast::unit_test::Suite env.close(); auto const seq = env.seq(gw); - auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const batchFee = batch::calcBatchFee(env, 1, 2); auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth); jv1[sfDelegate] = bob.human(); auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze); jv2[sfDelegate] = bob.human(); + // Both inners are delegated to Bob, so Bob must provide the batch + // signature. auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(gw, seq, batchFee, tfAllOrNothing), batch::Inner(jv1, seq + 1), - batch::Inner(jv2, seq + 2)); + batch::Inner(jv2, seq + 2), + batch::Sig(bob)); env.close(); std::vector const testCases = { @@ -4818,7 +5377,7 @@ class Batch_test : public beast::unit_test::Suite // inner transaction not authorized by the delegating account. { - test::jtx::Env env{*this, features}; + Env env{*this, features}; Account const gw{"gw"}; Account const alice{"alice"}; Account const bob{"bob"}; @@ -4832,20 +5391,23 @@ class Batch_test : public beast::unit_test::Suite env.close(); auto const seq = env.seq(gw); - auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const batchFee = batch::calcBatchFee(env, 1, 2); auto jv1 = trust(gw, gw["USD"](0), alice, tfSetFreeze); jv1[sfDelegate] = bob.human(); auto jv2 = trust(gw, gw["USD"](0), alice, tfClearFreeze); jv2[sfDelegate] = bob.human(); + // Both inners are delegated to Bob, so Bob must provide the batch + // signature; jv2 still fails preclaim for lack of permission. auto const [txIDs, batchID] = submitBatch( env, tesSUCCESS, batch::outer(gw, seq, batchFee, tfIndependent), batch::Inner(jv1, seq + 1), // terNO_DELEGATE_PERMISSION: not authorized to clear freeze - batch::Inner(jv2, seq + 2)); + batch::Inner(jv2, seq + 2), + batch::Sig(bob)); env.close(); std::vector const testCases = { @@ -5004,7 +5566,7 @@ class Batch_test : public beast::unit_test::Suite batch::outer(alice, seq, batchFee, tfAllOrNothing), batch::Inner(pay(alice, bob, XRP(10)), seq + 1), batch::Inner(pay(alice, bob, XRP(5)), seq + 2), - batch::Sig(bob, carol, alice, bob, carol, alice, bob, carol, alice, alice)); + batch::Sig(std::vector(kMaxBatchSigners + 1, bob))); XRPAmount const txBaseFee = getBaseFee(jtx); BEAST_EXPECT(txBaseFee == XRPAmount(kInitialXrp)); } @@ -5022,6 +5584,304 @@ class Batch_test : public beast::unit_test::Suite } } + void + testStandaloneInnerBatchFlag(FeatureBitset features) + { + testcase("standalone tx with tfInnerBatchTxn rejected"); + + using namespace test::jtx; + using namespace std::literals; + + // A standalone Payment with tfInnerBatchTxn must be rejected. + // Without proper guards this would bypass signature verification + // in preflight2. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + // Submit a normal Payment with tfInnerBatchTxn flag. + // preflight1 must reject with temINVALID_INNER_BATCH because + // the flag is set but no parentBatchId exists. + env(pay(alice, bob, XRP(1)), Txflags(tfInnerBatchTxn), Ter(telENV_RPC_FAILED)); + env.close(); + + // Verify via direct apply path (bypassing RPC layer) + env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { + // Construct a Payment STTx with tfInnerBatchTxn, + // empty signing pub key, and no signature — mimicking + // what an attacker would send to skip sig verification. + STTx const stx = STTx(ttPAYMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, alice.id()); + obj.setAccountID(sfDestination, bob.id()); + obj.setFieldAmount(sfAmount, XRP(1)); + obj.setFieldAmount(sfFee, XRP(0)); + obj.setFieldU32(sfSequence, env.seq(alice)); + obj.setFieldU32(sfFlags, tfInnerBatchTxn); + }); + + auto const result = xrpl::apply(env.app(), view, stx, TapNone, j); + // Must NOT be applied — signature was never checked + BEAST_EXPECT(!result.applied); + return false; + }); + } + } + + void + testOuterBinding(FeatureBitset features) + { + testcase("outer binding"); + + using namespace test::jtx; + + // Signatures captured from one outer account cannot be replayed + // under a different outer account. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const eve = Account("eve"); + env.fund(XRP(10000), alice, bob, carol, eve); + env.close(); + + auto const preCarol = env.balance(carol); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + + auto const aliceSeq = env.seq(alice); + auto const batchFee1 = batch::calcBatchFee(env, 2, 2); + auto jt1 = env.jt( + batch::outer(alice, aliceSeq, batchFee1, tfOnlyOne), + batch::Inner(pay(bob, alice, XRP(100)), bobSeq), + batch::Inner(pay(carol, alice, XRP(50)), carolSeq), + batch::Sig(bob, carol)); + + auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName]; + + env(jt1, Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.seq(carol) == carolSeq); + + auto const batchFee2 = batch::calcBatchFee(env, 2, 2); + auto jt2 = env.jtnofill( + batch::outer(eve, env.seq(eve), batchFee2, tfOnlyOne), + batch::Inner(pay(bob, alice, XRP(100)), bobSeq), + batch::Inner(pay(carol, alice, XRP(50)), carolSeq)); + + jt2.jv[sfBatchSigners.jsonName] = capturedSigners; + env(jt2.jv, Ter(telENV_RPC_FAILED)); + env.close(); + + BEAST_EXPECT(env.seq(carol) == carolSeq); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + + // Signatures are bound to the outer sequence; replaying them + // at a higher sequence must fail. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const preCarol = env.balance(carol); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + + auto const aliceSeq1 = env.seq(alice); + auto const batchFee1 = batch::calcBatchFee(env, 2, 2); + auto jt1 = env.jt( + batch::outer(alice, aliceSeq1, batchFee1, tfOnlyOne), + batch::Inner(pay(bob, alice, XRP(500)), bobSeq), + batch::Inner(pay(carol, alice, XRP(500)), carolSeq), + batch::Sig(bob, carol)); + + auto const capturedSigners = jt1.jv[sfBatchSigners.jsonName]; + + env(jt1, Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.seq(carol) == carolSeq); + + auto const batchFee2 = batch::calcBatchFee(env, 2, 2); + auto jt2 = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee2, tfOnlyOne), + batch::Inner(pay(bob, alice, XRP(500)), bobSeq), + batch::Inner(pay(carol, alice, XRP(500)), carolSeq)); + + jt2.jv[sfBatchSigners.jsonName] = capturedSigners; + env(jt2.jv, Ter(telENV_RPC_FAILED)); + env.close(); + + BEAST_EXPECT(env.balance(carol) == preCarol); + BEAST_EXPECT(env.seq(carol) == carolSeq); + } + + // Multi-signed batch signer entries are bound to their account; + // reusing inner signatures under a different batch signer must fail. + { + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + env.fund(XRP(10000), alice, bob, carol, dave, elsa); + env.close(); + + env(signers(bob, 2, {{dave, 1}, {elsa, 1}})); + env.close(); + env(signers(carol, 2, {{dave, 1}, {elsa, 1}})); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + auto jt1 = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(10)), seq + 1), + batch::Inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::Msig(bob, {dave, elsa}), + Ter(tesSUCCESS)); + + auto const bobSignerEntry = jt1.jv[sfBatchSigners.jsonName][0u]; + + env(jt1, Ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + auto const batchFee2 = batch::calcBatchFee(env, 3, 2); + auto jt2 = env.jtnofill( + batch::outer(alice, seq2, batchFee2, tfAllOrNothing), + batch::Inner(pay(alice, carol, XRP(10)), seq2 + 1), + batch::Inner(pay(carol, alice, XRP(5)), env.seq(carol))); + + json::Value carolSigner; + carolSigner[sfBatchSigner.jsonName][jss::Account] = carol.human(); + carolSigner[sfBatchSigner.jsonName][jss::SigningPubKey] = ""; + carolSigner[sfBatchSigner.jsonName][sfSigners.jsonName] = + bobSignerEntry[sfBatchSigner.jsonName][sfSigners.jsonName]; + + jt2.jv[sfBatchSigners.jsonName][0u] = carolSigner; + env(jt2.jv, Ter(telENV_RPC_FAILED)); + env.close(); + } + } + + void + testUnsortedBatchSigners(FeatureBitset features) + { + testcase("unsorted batch signers"); + + using namespace test::jtx; + + Env env{*this, features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const seq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + + auto jt = env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(bob, alice, XRP(10)), bobSeq), + batch::Inner(pay(carol, alice, XRP(5)), carolSeq), + batch::Sig(bob, carol)); + + auto const s0 = jt.jv[sfBatchSigners.jsonName][0u]; + auto const s1 = jt.jv[sfBatchSigners.jsonName][1u]; + jt.jv[sfBatchSigners.jsonName][0u] = s1; + jt.jv[sfBatchSigners.jsonName][1u] = s0; + + env(jt.jv, Ter(temBAD_SIGNER)); + env.close(); + } + + void + testBatchSigCache(FeatureBitset features) + { + testcase("batch signature caching"); + + using namespace test::jtx; + + // Mirrors apply.cpp's file-local kSfSiggood (the standard signature-good + // cache); batch signer sigs are now verified and cached alongside the + // outer signature in checkSign/checkValidity. + constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2; + + // Valid batch: alice (outer) + an inner from bob, who co-signs. + auto buildValidBatch = [](Env& env) { + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + return env.jt( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), seq + 1), + batch::Inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::Sig(bob)); + }; + + // WRITE: a valid batch records "good" on its tx id. + { + Env env{*this, features}; + env.fund(XRP(10000), Account("alice"), Account("bob")); + env.close(); + + auto jt = buildValidBatch(env); + auto const txid = jt.stx->getTransactionID(); + + BEAST_EXPECT(!any(env.app().getHashRouter().getFlags(txid) & kSfSiggood)); + env(jt, Ter(tesSUCCESS)); + BEAST_EXPECT(any(env.app().getHashRouter().getFlags(txid) & kSfSiggood)); + env.close(); + } + + // READ: corrupt only a signer's signature (outer sig + signer key + // untouched), so just the crypto would fail. Caught when uncached... + { + Env env{*this, features}; + env.fund(XRP(10000), Account("alice"), Account("bob")); + env.close(); + + auto jt = buildValidBatch(env); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] = + "00"; + env(jt.jv, Ter(telENV_RPC_FAILED)); + env.close(); + } + { + // ...but a planted "good" skips the crypto, so it applies. + Env env{*this, features}; + env.fund(XRP(10000), Account("alice"), Account("bob")); + env.close(); + + auto jt = buildValidBatch(env); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfTxnSignature.jsonName] = + "00"; + auto const txid = STTx{parse(jt.jv)}.getTransactionID(); + env.app().getHashRouter().setFlags(txid, kSfSiggood); + env(jt.jv, Ter(tesSUCCESS)); + env.close(); + } + } + void testWithFeats(FeatureBitset features) { @@ -5038,6 +5898,7 @@ class Batch_test : public beast::unit_test::Suite testIndependent(features); testInnerSubmitRPC(features); testAccountActivation(features); + testCheckAllSignatures(features); testAccountSet(features); testAccountDelete(features); testLoan(features); @@ -5053,8 +5914,13 @@ class Batch_test : public beast::unit_test::Suite testBatchTxQueue(features); testBatchNetworkOps(features); testBatchDelegate(features); + testBatchDelegateConsent(features); testValidateRPCResponse(features); testBatchCalculateBaseFee(features); + testStandaloneInnerBatchFlag(features); + testOuterBinding(features); + testUnsortedBatchSigners(features); + testBatchSigCache(features); } public: @@ -5064,7 +5930,6 @@ public: using namespace test::jtx; auto const sa = testableAmendments(); - testWithFeats(sa - fixBatchInnerSigs); testWithFeats(sa); } }; diff --git a/src/test/app/ConfidentialTransferExtended_test.cpp b/src/test/app/ConfidentialTransferExtended_test.cpp index 0aee7516c9..953325a6e9 100644 --- a/src/test/app/ConfidentialTransferExtended_test.cpp +++ b/src/test/app/ConfidentialTransferExtended_test.cpp @@ -1945,7 +1945,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env.close(); auto const bobSeq = env.seq(bob); - auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); // jv1: proof against spending balance 100 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1); @@ -1957,6 +1957,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), batch::Inner(jv1, bobSeq + 1), batch::Inner(jv2, bobSeq + 2), + batch::Sig(dave), Ter(tesSUCCESS)); env.close(); @@ -1981,7 +1982,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env.close(); auto const bobSeq = env.seq(bob); - auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2); + auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); // jv1: proof against spending balance 100. auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1); @@ -1994,6 +1995,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), batch::Inner(jv1, bobSeq + 1), batch::Inner(jv2, bobSeq + 2), + batch::Sig(dave), Ter(tesSUCCESS)); env.close(); @@ -2029,7 +2031,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase auto const bobSeq = env.seq(bob); auto const carolSeq = env.seq(carol); - auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2); // jv1: direct send from carol (valid proof). auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq); @@ -2040,7 +2042,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing), batch::Inner(jv1, carolSeq), batch::Inner(jv2, bobSeq + 1), - batch::Sig(carol), + batch::Sig(carol, dave), Ter(tesSUCCESS)); env.close(); @@ -2066,7 +2068,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase // Bob does not grant dave any permissions. auto const bobSeq = env.seq(bob); auto const carolSeq = env.seq(carol); - auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2); + auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2); auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1); jv1[jss::Delegate] = dave.human(); @@ -2075,7 +2077,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(batch::outer(bob, bobSeq, batchFee, tfIndependent), batch::Inner(jv1, bobSeq + 1), batch::Inner(jv2, carolSeq), - batch::Sig(carol), + batch::Sig(carol, dave), Ter(tesSUCCESS)); env.close(); @@ -2093,8 +2095,9 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase testcase("Test batch delegated send with delegate as outer account"); using namespace test::jtx; - // Dave has delegation permission, but the inner Account is bob. - // Without bob's BatchSigner, the batch is rejected. + // Dave holds bob's ConfidentialMPTSend delegation and is the outer batch + // signer, so dave's outer signature consents to the delegated inner. + // The batch applies. { Env env{*this, features}; Account const alice("alice"); @@ -2119,11 +2122,11 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing), batch::Inner(jv1, bobSeq), batch::Inner(jv2, daveSeq + 1), - Ter(temBAD_SIGNER)); + Ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100); - BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0); + BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 60); + BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40); } // Dave submits a mixed batch: bob signs inner tx1, and @@ -2163,8 +2166,10 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70); } - // Verify the delegator Bob's BatchSigner does not bypass the missing delegation permission. - // The delegated inner send fails. + // The delegated inner's required signer is the delegate (dave), not bob. + // Bob signs but is not a required signer, so the batch is rejected as an + // extra signer. The delegator's signature cannot stand in for the + // delegate's. { Env env{*this, features}; Account const alice("alice"); @@ -2189,7 +2194,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase batch::Inner(jv1, bobSeq), batch::Inner(jv2, carolSeq), batch::Sig(bob, carol), - Ter(tesSUCCESS)); + Ter(temBAD_SIGNER)); env.close(); // jv1 fails before jv2 is attempted. @@ -2257,7 +2262,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase batch::Inner(jv4, frankSeq), batch::Inner(jv5, frankSeq + 1), batch::Inner(jv6, bobSeq + 2), - batch::Sig(bob, carol, frank), + batch::Sig(erin, frank), Ter(tesSUCCESS)); env.close(); diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 2422db05de..ed99817f34 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -1,12 +1,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -36,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -68,7 +72,7 @@ namespace xrpl::test { struct LedgerReplay_test : public beast::unit_test::Suite { void - run() override + testReplayLedger() { testcase("Replay ledger"); @@ -91,6 +95,45 @@ struct LedgerReplay_test : public beast::unit_test::Suite BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash); } + + void + testReplayBatchLedger() + { + testcase("Replay ledger with batch transactions"); + + using namespace jtx; + + Env env(*this, testableAmendments()); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(100000), alice, bob); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), seq + 1), + batch::Inner(pay(alice, bob, XRP(2)), seq + 2), + Ter(tesSUCCESS)); + env.close(); + + LedgerMaster& ledgerMaster = env.app().getLedgerMaster(); + auto const lastClosed = ledgerMaster.getClosedLedger(); + auto const lastClosedParent = ledgerMaster.getLedgerByHash(lastClosed->header().parentHash); + + auto const replayed = buildLedger( + LedgerReplay(lastClosedParent, lastClosed), TapNone, env.app(), env.journal); + + BEAST_EXPECT(replayed->header().hash == lastClosed->header().hash); + } + + void + run() override + { + testReplayLedger(); + testReplayBatchLedger(); + } }; enum class InboundLedgersBehavior { diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 2d10eb4beb..91eb26da7e 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -61,8 +61,7 @@ namespace xrpl::test { class TxQPosNegFlows_test : public beast::unit_test::Suite { - // Same as corresponding values from TxQ.h - static constexpr FeeLevel64 kBaseFeeLevel{256}; + static constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel}; static constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500; static void diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 0e35e6b9ec..34532b17f7 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -731,8 +731,8 @@ create(jtx::Account const& account, jtx::Account const& dest, STAmount const& se } // namespace check -static constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel}; -static constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500; +inline constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel}; +inline constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500; inline uint256 getCheckIndex(AccountID const& account, std::uint32_t uSequence) diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index 0e78d409fa..4d564f150a 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -58,7 +58,6 @@ class Inner { private: json::Value txn_; - std::uint32_t seq_; std::optional ticket_; public: @@ -66,10 +65,10 @@ public: json::Value txn, std::uint32_t const& sequence, std::optional const& ticket = std::nullopt) - : txn_(std::move(txn)), seq_(sequence), ticket_(ticket) + : txn_(std::move(txn)), ticket_(ticket) { txn_[jss::SigningPubKey] = ""; - txn_[jss::Sequence] = seq_; + txn_[jss::Sequence] = sequence; txn_[jss::Fee] = "0"; txn_[jss::Flags] = txn_[jss::Flags].asUInt() | tfInnerBatchTxn; diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp index 66ca0c7d54..b1061f65a3 100644 --- a/src/test/jtx/impl/batch.cpp +++ b/src/test/jtx/impl/batch.cpp @@ -99,7 +99,13 @@ Sig::operator()(Env& env, JTx& jt) const jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); Serializer msg; - serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + serializeBatch( + msg, + stx.getAccountID(sfAccount), + stx.getSeqValue(), + stx.getFlags(), + stx.getBatchTransactionIDs()); + finishMultiSigningData(e.acct.id(), msg); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()}); @@ -137,7 +143,13 @@ Msig::operator()(Env& env, JTx& jt) const iso[jss::SigningPubKey] = strHex(e.sig.pk().slice()); Serializer msg; - serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + serializeBatch( + msg, + stx.getAccountID(sfAccount), + stx.getSeqValue(), + stx.getFlags(), + stx.getBatchTransactionIDs()); + msg.addBitString(master.id()); finishMultiSigningData(e.acct.id(), msg); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp index 8cda5965cb..a36e51cb6f 100644 --- a/src/test/rpc/Feature_test.cpp +++ b/src/test/rpc/Feature_test.cpp @@ -134,7 +134,7 @@ class Feature_test : public beast::unit_test::Suite // or removed, swap out for any other feature. BEAST_EXPECT( featureToName(fixRemoveNFTokenAutoTrustLine) == "fixRemoveNFTokenAutoTrustLine"); - BEAST_EXPECT(featureToName(featureBatch) == "Batch"); + BEAST_EXPECT(featureToName(featureBatchV1_1) == "BatchV1_1"); BEAST_EXPECT(featureToName(featureDID) == "DID"); BEAST_EXPECT(featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields"); BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow"); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 5ea79c3996..0d00360a58 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -420,6 +420,20 @@ class Simulate_test : public beast::unit_test::Suite BEAST_EXPECT( resp[jss::result][jss::error_message] == "Transaction should not be signed."); } + { + // tfInnerBatchTxn flag on top-level transaction + json::Value params; + json::Value txJson{json::ValueType::Object}; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + txJson[jss::Flags] = tfInnerBatchTxn; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "tfInnerBatchTxn flag is not allowed on top-level transactions."); + } } void @@ -476,7 +490,7 @@ class Simulate_test : public beast::unit_test::Suite auto jt = env.jtnofill( batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), batch::Inner(pay(alice, bob, XRP(10)), seq + 1), - batch::Inner(pay(alice, bob, XRP(10)), seq + 1)); + batch::Inner(pay(alice, bob, XRP(10)), seq + 2)); jt.jv.removeMember(jss::TxnSignature); json::Value params; diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index 2eb64ecaf9..038a7be4b7 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -233,7 +234,15 @@ buildLedger( j, [&](OpenView& accum, std::shared_ptr const& built) { for (auto& tx : replayData.orderedTxns()) + { + // Inner batch transactions are applied as part of their outer + // Batch transaction, never on their own. Skip them here so they + // are not re-applied a second time outside of the batch during + // replay. + if (tx.second->isFlag(tfInnerBatchTxn)) + continue; applyTransaction(app, accum, *tx.second, false, applyFlags, j); + } }); } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 1802441a66..82445f3625 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1253,8 +1253,9 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } - // Enforce Network bar for batch txn - if (iTrans->isFlag(tfInnerBatchTxn) && ledgerMaster_.getValidatedRules().enabled(featureBatch)) + // Reject any transaction with the tfInnerBatchTxn flag at the network + // boundary, regardless of amendment state. + if (iTrans->isFlag(tfInnerBatchTxn)) { JLOG(journal_.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present."; return; @@ -1320,7 +1321,7 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction) // under no circumstances will we ever accept an inner txn within a batch // txn from the network. auto const sttx = *transaction->getSTransaction(); - if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch)) + if (sttx.isFlag(tfInnerBatchTxn)) { transaction->setStatus(TransStatus::INVALID); transaction->setResult(temINVALID_FLAG); diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index ec5ac569d6..7e57302d23 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -390,6 +391,12 @@ TxQ::canBeHeld( std::optional const& replacementIter, std::scoped_lock const& lock) { + // A Batch is never queued: its inner transactions can change the sequence + // numbers of multiple accounts, which the TxQ's per-account model cannot + // forecast. It must apply straight to the open ledger or not at all. + if (tx.getTxnType() == ttBATCH) + return telCAN_NOT_QUEUE; + // PreviousTxnID is deprecated and should never be used. // AccountTxnID is not supported by the transaction // queue yet, but should be added in the future. diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index e1d7d23215..5ac61cfd91 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1309,7 +1309,7 @@ PeerImp::handleTransaction( // Charge strongly for attempting to relay a txn with tfInnerBatchTxn // LCOV_EXCL_START /* - There is no need to check whether the featureBatch amendment is + There is no need to check whether the featureBatchV1_1 amendment is enabled. * If the `tfInnerBatchTxn` flag is set, and the amendment is @@ -2873,7 +2873,7 @@ PeerImp::checkTransaction( // charge strongly for relaying batch txns // LCOV_EXCL_START /* - There is no need to check whether the featureBatch amendment is + There is no need to check whether the featureBatchV1_1 amendment is enabled. * If the `tfInnerBatchTxn` flag is set, and the amendment is diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 7ee28c4886..2a6c2280e3 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -355,6 +356,13 @@ doSimulate(RPC::JsonContext& context) return RPC::makeError(RpcNotImpl); } + // Reject transactions with the tfInnerBatchTxn flag. + if (stTx->isFlag(tfInnerBatchTxn)) + { + return RPC::makeError( + RpcInvalidParams, "tfInnerBatchTxn flag is not allowed on top-level transactions."); + } + std::string reason; auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor From ea13be81b7037ce0a03c706e5b916b0beb581496 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:21:23 +0200 Subject: [PATCH 12/31] feat: Add an invariant to ensure object deletion also deletes its pseudo-account (#7445) Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> --- include/xrpl/tx/invariants/InvariantCheck.h | 26 +++- src/libxrpl/tx/invariants/InvariantCheck.cpp | 95 +++++++++++-- src/test/app/Invariants_test.cpp | 142 +++++++++++++++++-- 3 files changed, 242 insertions(+), 21 deletions(-) diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index 8931d189fd..ba91cba064 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -375,16 +375,35 @@ public: */ class ValidAmounts { - std::vector> afterEntries_; + std::vector afterEntries_; public: void - visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + visitEntry(bool, SLE::const_ref, SLE::const_ref); [[nodiscard]] bool finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; }; +/* + * Verify that when an object with an associated pseudo-account is deleted, + * its pseudo-account is also deleted. + * + * The reverse (pseudo-account deleted → object deleted) is enforced by + * AccountRootsDeletedClean via getPseudoAccountFields(). + */ +class ObjectHasPseudoAccount +{ +public: + void + visitEntry(bool, SLE::const_ref, SLE::const_ref); + + [[nodiscard]] bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const; + +private: + std::vector deletedObjSles_; +}; // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -416,7 +435,8 @@ using InvariantChecks = std::tuple< ValidConfidentialMPToken, ValidMPTPayment, ValidAmounts, - ValidMPTTransfer>; + ValidMPTTransfer, + ObjectHasPseudoAccount>; /** * @brief get a tuple of all invariant checks diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 308342da74..32df44a96b 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include namespace xrpl { @@ -63,6 +64,23 @@ hasPrivilege(STTx const& tx, Privilege priv) #undef TRANSACTION #pragma pop_macro("TRANSACTION") +// Returns the human-readable name of a ledger entry's type, falling back to +// the numeric type if the format is somehow unknown. +static std::string +ledgerEntryTypeName(SLE const& sle) +{ + auto const item = LedgerFormats::getInstance().findByType(sle.getType()); + + if (item == nullptr) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format"); + return std::to_string(sle.getType()); + // LCOV_EXCL_STOP + } + return item->getName(); +} + void TransactionFeeCheck::visitEntry(bool, SLE::const_ref, SLE::const_ref) { @@ -457,16 +475,8 @@ AccountRootsDeletedClean::finalize( if (auto const sle = view.read(keylet)) { // Finding the object is bad - auto const typeName = [&sle]() { - auto item = LedgerFormats::getInstance().findByType(sle->getType()); - - if (item != nullptr) - return item->getName(); - return std::to_string(sle->getType()); - }(); - - JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName - << " object"; + JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " + << ledgerEntryTypeName(*sle) << " object"; // The comment above starting with "assert(enforce)" explains this // assert. XRPL_ASSERT( @@ -1070,4 +1080,69 @@ ValidAmounts::finalize( return true; } +void +ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + if (!isDelete) + return; + + // Before should never be null when isDelete = true + if (!before) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state"); + return; + // LCOV_EXCL_STOP + } + + switch (before->getType()) + { + case ltAMM: + case ltVAULT: + case ltLOAN_BROKER: + deletedObjSles_.push_back(before); + break; + default: + return; + } +} + +[[nodiscard]] bool +ObjectHasPseudoAccount::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) const +{ + if (!view.rules().enabled(fixCleanup3_3_0)) + return true; + + if (deletedObjSles_.empty()) + return true; + + bool failed = false; + for (auto const& sle : deletedObjSles_) + { + if (!sle->isFieldPresent(sfAccount)) + { + JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle) + << " is missing pseudo-account field"; + failed = true; + continue; + } + + // The pseudo-account must NOT exist on the ledger after the object is deleted. + if (view.exists(keylet::account(sle->getAccountID(sfAccount)))) + { + JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle) + << " without deleting its pseudo-account"; + failed = true; + } + } + + return !failed; +} + } // namespace xrpl diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 9fde52ecb2..079ff9df88 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -2829,7 +2829,8 @@ class Invariants_test : public beast::unit_test::Suite }); doInvariantCheck( - {"vault updated by a wrong transaction type"}, + {"vault updated by a wrong transaction type", + "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); auto sleVault = ac.view().peek(keylet); @@ -2840,7 +2841,7 @@ class Invariants_test : public beast::unit_test::Suite }, XRPAmount{}, STTx{ttPAYMENT, [](STObject&) {}}, - {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, [&](Account const& a1, Account const& a2, Env& env) { Vault const vault{env}; auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()}); @@ -2877,6 +2878,7 @@ class Invariants_test : public beast::unit_test::Suite auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id())); sleVault->setFieldU64(sfOwnerNode, *vaultPage); + sleVault->setAccountID(sfAccount, a1.id()); ac.view().insert(sleVault); return true; }, @@ -2885,7 +2887,8 @@ class Invariants_test : public beast::unit_test::Suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); doInvariantCheck( - {"vault deleted by a wrong transaction type"}, + {"vault deleted by a wrong transaction type", + "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); auto sleVault = ac.view().peek(keylet); @@ -2896,7 +2899,7 @@ class Invariants_test : public beast::unit_test::Suite }, XRPAmount{}, STTx{ttVAULT_SET, [](STObject&) {}}, - {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, [&](Account const& a1, Account const& a2, Env& env) { Vault const vault{env}; auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()}); @@ -2905,7 +2908,8 @@ class Invariants_test : public beast::unit_test::Suite }); doInvariantCheck( - {"vault operation updated more than single vault"}, + {"vault operation updated more than single vault", + "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); @@ -2925,7 +2929,7 @@ class Invariants_test : public beast::unit_test::Suite }, XRPAmount{}, STTx{ttVAULT_DELETE, [](STObject&) {}}, - {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, [&](Account const& a1, Account const& a2, Env& env) { Vault const vault{env}; { @@ -2949,6 +2953,7 @@ class Invariants_test : public beast::unit_test::Suite auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id())); sleVault->setFieldU64(sfOwnerNode, *vaultPage); + sleVault->setAccountID(sfAccount, a.id()); ac.view().insert(sleVault); }; insertVault(a1); @@ -2960,7 +2965,8 @@ class Invariants_test : public beast::unit_test::Suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); doInvariantCheck( - {"deleted vault must also delete shares"}, + {"deleted vault must also delete shares", + "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); auto sleVault = ac.view().peek(keylet); @@ -2971,7 +2977,7 @@ class Invariants_test : public beast::unit_test::Suite }, XRPAmount{}, STTx{ttVAULT_DELETE, [](STObject&) {}}, - {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, [&](Account const& a1, Account const& a2, Env& env) { Vault const vault{env}; auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()}); @@ -5176,6 +5182,125 @@ class Invariants_test : public beast::unit_test::Suite } } + void + testObjectHasPseudoAccount() + { + testcase << "object has pseudo-account"; + using namespace jtx; + + auto const amendments = defaultAmendments() | fixCleanup3_3_0; + + // Vault: object deleted without its pseudo-account + { + Keylet vaultKeylet = keylet::amendments(); + doInvariantCheck( + Env{*this, amendments}, + {{"deleted Vault without deleting its pseudo-account"}}, + [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(vaultKeylet); + if (!sle) + return false; + ac.view().erase(sle); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&vaultKeylet](Account const& a1, Account const&, Env& env) { + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()}); + env(tx); + vaultKeylet = keylet; + return true; + }); + } + + // AMM: object deleted without its pseudo-account + { + uint256 ammID{}; + Account const gw{"gw"}; + doInvariantCheck( + Env{*this, amendments}, + {{"deleted AMM without deleting its pseudo-account"}}, + [&ammID](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::amm(ammID)); + if (!sle) + return false; + ac.view().erase(sle); + return true; + }, + XRPAmount{}, + STTx{ttAMM_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&ammID, &gw](Account const&, Account const&, Env& env) { + env.fund(XRP(1'000), gw); + AMM const amm(env, gw, XRP(100), gw["USD"](100)); + ammID = amm.ammID(); + return true; + }); + } + + // LoanBroker: object deleted without its pseudo-account + { + Keylet loanBrokerKeylet = keylet::amendments(); + doInvariantCheck( + Env{*this, amendments}, + {{"deleted LoanBroker without deleting its pseudo-account"}}, + [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(loanBrokerKeylet); + if (!sle) + return false; + ac.view().erase(sle); + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) { + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset); + return BEAST_EXPECT(env.le(loanBrokerKeylet)); + }); + } + + // Deleted object missing sfAccount field (defensive check). + // Manually construct the view to place a vault SLE without + // sfAccount into the base ledger, then erase it. + { + Env env{*this, amendments}; + Account const a1{"A1"}; + Account const a2{"A2"}; + env.fund(XRP(1000), a1, a2); + env.close(); + + OpenView ov{*env.current()}; + + auto const vaultKeylet = keylet::vault(a1.id(), ov.seq()); + auto sleVault = std::make_shared(vaultKeylet); + sleVault->makeFieldAbsent(sfAccount); + ov.rawInsert(sleVault); + + STTx const tx{ttVAULT_DELETE, [](STObject&) {}}; + test::StreamSink sink{beast::Severity::Warning}; + beast::Journal const jlog{sink}; + ApplyContext ac{ + env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog}; + CurrentTransactionRulesGuard const rulesGuard(ov.rules()); + + auto sle = ac.view().peek(vaultKeylet); + if (!BEAST_EXPECT(sle)) + return; + ac.view().erase(sle); + + auto transactor = makeTransactor(ac); + if (!BEAST_EXPECT(transactor)) + return; + TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{}); + BEAST_EXPECT(result == tecINVARIANT_FAILED); + BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field")); + } + } + void testConfidentialMPTTransfer() { @@ -5458,6 +5583,7 @@ public: testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3); testVaultComputeCoarsestScale(); testAMM(); + testObjectHasPseudoAccount(); } }; From 6aed3bb71d2820f081ce2a22666e456420a814b2 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 1 Jul 2026 14:28:14 +0100 Subject: [PATCH 13/31] chore: Update pre-commit hooks && actions (#7686) --- .github/workflows/pre-commit.yml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- .pre-commit-config.yaml | 6 +++--- docs/0001-negative-unl/README.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 0363534af5..1acd28208e 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,7 +14,7 @@ on: jobs: # Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks. run-hooks: - uses: XRPLF/actions/.github/workflows/pre-commit.yml@e06d4138c9ec8dceeb7c818645faa38087ea9e3d + uses: XRPLF/actions/.github/workflows/pre-commit.yml@1bde119a1ab71305ba5d3716e7a82cea1c7bdede with: runs_on: ubuntu-latest container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index bfa8d2e79c..1ac8d61655 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f + uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c with: enable_ccache: false diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 0cb0219d72..e7a88a0e66 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -113,7 +113,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f + uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c with: enable_ccache: ${{ inputs.ccache_enabled }} diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index b04847e137..5528f3452e 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f + uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c with: enable_ccache: false diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 88b364c2b1..22ad36d98f 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -68,7 +68,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@9355d190fd7d4de80fadfd161e6edddc9702cd9f + uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c with: enable_ccache: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cbf4c1dd0..7bac4d1140 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,12 +51,12 @@ repos: exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/ - repo: https://github.com/BlankSpruce/gersemi-pre-commit - rev: faadd6a9d852369ca94f4d15b2404c967ba8cb01 # frozen: 0.27.6 + rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7 hooks: - id: gersemi - repo: https://github.com/rbubley/mirrors-prettier - rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3 + rev: 39e2973981e6d2f9b6c543b0086a2d2393abdc89 # frozen: v3.9.4 hooks: - id: prettier args: [--end-of-line=auto] @@ -86,7 +86,7 @@ repos: files: \.md$ - repo: https://github.com/streetsidesoftware/cspell-cli - rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0 + rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1 hooks: - id: cspell # Spell check changed files exclude: | diff --git a/docs/0001-negative-unl/README.md b/docs/0001-negative-unl/README.md index dd5f9af2ae..0bc65cd860 100644 --- a/docs/0001-negative-unl/README.md +++ b/docs/0001-negative-unl/README.md @@ -288,7 +288,7 @@ components with non-trivial changes are colored green. validated. ![Sequence diagram](./negativeUNL_highLevel_sequence.png?raw=true "Negative UNL - Changes") +Changes") ## Roads Not Taken From 6d0b758a12eeb24b3c980d4673be3673040fdc63 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 1 Jul 2026 14:28:41 +0100 Subject: [PATCH 14/31] build: Don't reuse binaries between different C++ versions (#7681) --- conan/profiles/default | 12 +++++++----- conan/profiles/sanitizers | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/conan/profiles/default b/conan/profiles/default index e0a88ebca1..ae6e23c3c3 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -10,16 +10,18 @@ os={{ os }} arch={{ arch }} build_type=Debug -compiler={{compiler}} +compiler={{ compiler }} compiler.version={{ compiler_version }} compiler.cppstd=23 {% if os == "Windows" %} compiler.runtime=static {% else %} -compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}} +compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }} {% endif %} [conf] -{% if compiler == "gcc" and compiler_version < 13 %} -tools.build:cxxflags+=['-Wno-restrict'] -{% endif %} +{# By default, Conan tries to reuse binaries built with different cppstd versions. #} +{# We want to avoid that to improve reproduceability, so we add the cppstd version to the package ID. #} +{# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #} +user.package:cppstd_version=23 +tools.info.package_id:confs+=["user.package:cppstd_version"] diff --git a/conan/profiles/sanitizers b/conan/profiles/sanitizers index 083807ea9e..09e6aef02b 100644 --- a/conan/profiles/sanitizers +++ b/conan/profiles/sanitizers @@ -87,15 +87,15 @@ include(default) {% 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:defines+={{ defines }} +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 | join(' ')}}", "SANITIZERS_LINKER_FLAGS": "{{sanitizer_linker_flags | join(' ')}}"} +&: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 ba739c94ce184933e728295527131242317acb9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:29:06 -0400 Subject: [PATCH 15/31] ci: [DEPENDABOT] bump actions/setup-python from 6.2.0 to 6.3.0 (#7657) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/reusable-package.yml | 2 +- .github/workflows/reusable-strategy-matrix.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 249e807592..6feecbfb75 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.13" diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index c1a1c1a78b..690aa3d423 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.13" From d1ff948244cb8b3027a74851466e8816f7940a90 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 1 Jul 2026 14:30:03 +0100 Subject: [PATCH 16/31] test: Add tests for TMProofPathResponse and TMReplayDeltaResponse invalid hash/key sizes (#7593) --- src/test/app/LedgerReplay_test.cpp | 62 ++++++++++++++++++++++++++++++ src/test/basics/base_uint_test.cpp | 18 +++++++++ 2 files changed, 80 insertions(+) diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index ed99817f34..da98c03380 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -982,6 +982,46 @@ struct LedgerReplayer_test : public beast::unit_test::Suite BEAST_EXPECT(!reply->has_error()); BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply)); + { + // bad reply: invalid hash/key sizes + { + // reply with undersized ledgerhash (31 bytes) + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string(31, '\x01')); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + { + // reply with oversized ledgerhash (33 bytes) + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string(33, '\x01')); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + { + // reply with empty ledgerhash + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string()); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + { + // reply with undersized key (31 bytes) + auto bad = std::make_shared(*reply); + bad->set_key(std::string(31, '\x01')); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + { + // reply with oversized key (33 bytes) + auto bad = std::make_shared(*reply); + bad->set_key(std::string(33, '\x01')); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + { + // reply with empty key + auto bad = std::make_shared(*reply); + bad->set_key(std::string()); + BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + } + } + { // bad reply // bad header @@ -1031,6 +1071,28 @@ struct LedgerReplayer_test : public beast::unit_test::Suite BEAST_EXPECT(!reply->has_error()); BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply)); + { + // bad reply: invalid hash sizes + { + // reply with undersized ledgerhash (31 bytes) + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string(31, '\x01')); + BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + } + { + // reply with oversized ledgerhash (33 bytes) + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string(33, '\x01')); + BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + } + { + // reply with empty ledgerhash + auto bad = std::make_shared(*reply); + bad->set_ledgerhash(std::string()); + BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + } + } + { // bad reply // bad header diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp index 5816b4eb59..c6572a0028 100644 --- a/src/test/basics/base_uint_test.cpp +++ b/src/test/basics/base_uint_test.cpp @@ -117,11 +117,29 @@ struct base_uint_test : beast::unit_test::Suite } } + void + testFromRawSizeMismatch() + { + testcase("base_uint: fromRaw size mismatch"); + + // Container larger than the base_uint (16 bytes vs 12 bytes for test96). + // Only the first 12 bytes are copied; the extra bytes are ignored. + { + Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + test96 const result = test96::fromRaw(tooBig); + BEAST_EXPECT(to_string(result) == "0102030405060708090A0B0C"); + } + } + void run() override { testcase("base_uint: general purpose tests"); +#ifdef NDEBUG + testFromRawSizeMismatch(); +#endif + static_assert(!std::is_constructible_v>); static_assert(!std::is_assignable_v>); From 0d149ba5b621d7ae6210c8ed005c3109a4782413 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:04:38 +0200 Subject: [PATCH 17/31] fix: Disable AMM creation with Vault shares (#7666) --- include/xrpl/ledger/helpers/MPTokenHelpers.h | 9 + include/xrpl/ledger/helpers/TokenHelpers.h | 32 +- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 61 +- src/libxrpl/tx/invariants/MPTInvariant.cpp | 25 +- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 17 + src/test/app/AMMMPT_test.cpp | 176 +----- src/test/app/Invariants_test.cpp | 217 ++----- src/test/app/Vault_test.cpp | 619 ++++++++++--------- 8 files changed, 495 insertions(+), 661 deletions(-) diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index c709badab8..b7f87337cf 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -23,6 +23,15 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); +/** Returns true if @p account's MPToken for @p mptIssue carries the + * individual-lock flag (lsfMPTLocked). + * + * @warning This checks only the raw per-holder lock bit. It does **not** + * perform the transitive vault pseudo-account check: if @p mptIssue is a + * vault share whose underlying asset is frozen, this function returns false. + * Call @ref isFrozen instead when determining whether an account may send or + * receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and + * isVaultPseudoAccountFrozen into a single complete check. */ [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 0c9871cd76..c8a67f776a 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -144,19 +144,18 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass * * Otherwise checks, in order: * 1. If the asset is globally frozen the remaining checks are redundant. - * 2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its - * underlying asset. - * 3. The pseudo-account's trustline / MPToken must not be frozen for sending. - * 4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent - * recovering one's own funds. - * 5. The destination must not be deep-frozen (cannot receive under any circumstance). + * 2. The pseudo-account's trustline / MPToken must not be individually frozen for sending. + * 3. The submitter's trustline / MPToken must not be individually frozen. Skipped when + * submitter == dst (self-withdrawal) so a regular freeze does not prevent recovering one's own + * funds. (Enforced as defensive code; no current caller exercises a frozen submitter ≠ dst.) + * 4. The destination must not be deep-frozen. * - * For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep - * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always + * For IOUs a regular individual freeze on the submitter does NOT block self-withdrawal; only deep + * freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always * blocked. * * @param view Ledger view to read freeze state from. - * @param srcAcct Pseudo-account the funds are withdrawn from (sender). + * @param pseudoAcct Pseudo-account the funds are withdrawn from (sender). * @param submitterAcct Account that submitted the withdrawal transaction. * @param dstAcct Account receiving the withdrawn funds. * @param asset Asset being withdrawn. @@ -166,7 +165,7 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass [[nodiscard]] TER checkWithdrawFreeze( ReadView const& view, - AccountID const& srcAcct, + AccountID const& pseudoAcct, AccountID const& submitterAcct, AccountID const& dstAcct, Asset const& asset); @@ -175,20 +174,17 @@ checkWithdrawFreeze( * Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM, * LoanBroker). * - * * Checks, in order: * 1. If the asset is globally frozen the remaining checks are redundant. - * 2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its - * underlying asset (returns tecLOCKED). - * 3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer, - * since the issuer can always send its own asset. - * 4. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts, + * 2. The depositor must not be individually frozen for the asset. Skipped when srcAcct is the + * asset issuer, since the issuer can always send its own asset. + * 3. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts, * pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds * could not later be withdrawn. * * @param view Ledger view to read freeze state from. * @param srcAcct Depositor sending the funds. - * @param dstAcct Pseudo-account receiving the deposit. + * @param pseudoAcct Pseudo-account receiving the deposit. * @param asset Asset being deposited. * @return tesSUCCESS if the deposit is permitted, otherwise a freeze result * (tecFROZEN for IOUs, tecLOCKED for MPTs). @@ -197,7 +193,7 @@ checkWithdrawFreeze( checkDepositFreeze( ReadView const& view, AccountID const& srcAcct, - AccountID const& dstAcct, + AccountID const& pseudoAcct, Asset const& asset); //------------------------------------------------------------------------------ diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 7988e24a56..0ab97a4b60 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -160,19 +160,24 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass [[nodiscard]] TER checkWithdrawFreeze( ReadView const& view, - AccountID const& srcAcct, + AccountID const& pseudoAcct, AccountID const& submitterAcct, AccountID const& dstAcct, Asset const& asset) { XRPL_ASSERT( - isPseudoAccount(view, srcAcct), "xrpl::checkWithdrawFreeze : source is a pseudo-account"); + isPseudoAccount(view, pseudoAcct), + "xrpl::checkWithdrawFreeze : source is a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, submitterAcct), "xrpl::checkWithdrawFreeze : submitter is not a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, dstAcct), "xrpl::checkWithdrawFreeze : destination is not a pseudo-account"); + // The asset being withdrawn must not be issued by a pseudo-account + XRPL_ASSERT( + !isPseudoAccount(view, asset.getIssuer()), + "xrpl::checkWithdrawFreeze : asset issuer cannot be a pseudo-account"); // Funds can always be sent to the issuer if (dstAcct == asset.getIssuer()) @@ -182,16 +187,9 @@ checkWithdrawFreeze( if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) return ret; - // Special case for shares - check if the shares (and the transitive asset) is not frozen - if (asset.holds() && - isVaultPseudoAccountFrozen(view, srcAcct, asset.get(), 0)) - { - return tecLOCKED; - } - // The transfer is from Submitter to Destination via Source (pseudo-account) // Both Source and Submitter must not be frozen to allow sending funds - if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) + if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret)) return ret; // Check submitter's individual freeze only when Submitter != Destination (a regular freeze @@ -203,33 +201,42 @@ checkWithdrawFreeze( } // The destination account must not be deep frozen to receive the funds - return checkDeepFrozen(view, dstAcct, asset); + if (auto const ret = checkDeepFrozen(view, dstAcct, asset); !isTesSuccess(ret)) + return ret; + + if (asset.holds() && + isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::checkWithdrawFreeze : pseudo-account backed object holds shares"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + return tesSUCCESS; } [[nodiscard]] TER checkDepositFreeze( ReadView const& view, AccountID const& srcAcct, - AccountID const& dstAcct, + AccountID const& pseudoAcct, Asset const& asset) { XRPL_ASSERT( - isPseudoAccount(view, dstAcct), + isPseudoAccount(view, pseudoAcct), "xrpl::checkDepositFreeze : destination is a pseudo-account"); XRPL_ASSERT( !isPseudoAccount(view, srcAcct), "xrpl::checkDepositFreeze : source is not a pseudo-account"); + // The asset being deposited must not be issued by a pseudo-account + XRPL_ASSERT( + !isPseudoAccount(view, asset.getIssuer()), + "xrpl::checkDepositFreeze : asset issuer cannot be a pseudo-account"); if (auto const ret = checkGlobalFrozen(view, asset); !isTesSuccess(ret)) return ret; - // Special case for shares - check if the shares and the transitive asset is not frozen - if (asset.holds() && - isVaultPseudoAccountFrozen(view, dstAcct, asset.get(), 0)) - { - return tecLOCKED; - } - if (srcAcct != asset.getIssuer()) { if (auto const ret = checkIndividualFrozen(view, srcAcct, asset); !isTesSuccess(ret)) @@ -238,7 +245,19 @@ checkDepositFreeze( // Unlike regular accounts, pseudo-accounts cannot receive deposits under a regular freeze // because those funds cannot be later withdrawn - return checkIndividualFrozen(view, dstAcct, asset); + if (auto const ret = checkIndividualFrozen(view, pseudoAcct, asset); !isTesSuccess(ret)) + return ret; + + if (asset.holds() && + isVaultPseudoAccountFrozen(view, pseudoAcct, asset.get(), 0)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::checkDepositFreeze : pseudo-account backed object holds shares"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + return tesSUCCESS; } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 26bee4effb..d8f7fcc27d 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -837,8 +836,6 @@ ValidMPTTransfer::finalize( ReadView const& view, beast::Journal const& j) { - auto const fix330Enabled = view.rules().enabled(fixCleanup3_3_0); - if (hasPrivilege(tx, OverrideFreeze)) return true; @@ -901,27 +898,9 @@ ValidMPTTransfer::finalize( // Check once: if any involved account is frozen, the whole issuance transfer is // considered frozen. Only need to check for frozen if there is a transfer of funds. - // - // Post-fix330: full isFrozen() applies — vault-share transitive freeze is part of - // the freeze semantics for all changed holders. - // - // Pre-fix330: legacy AMM withdraw only checked individual freeze on the - // destination, not the transitive vault freeze. All other paths (and the AMM - // account itself as sender) did apply the full check. - MPTIssue const issue{mptID}; - auto const legacyAccountFrozen = [&] { - if (isGlobalFrozen(view, issue) || isIndividualFrozen(view, account, issue)) - return true; - bool const isReceiver = - !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore; - if (txnType == ttAMM_WITHDRAW && isReceiver) - return false; - return isVaultPseudoAccountFrozen(view, account, issue, 0); - }; - bool const accountFrozen = - fix330Enabled ? isFrozen(view, account, issue) : legacyAccountFrozen(); if (!invalidTransfer && - (accountFrozen || !isAuthorized(view, mptID, account, reqAuth))) + (isFrozen(view, account, MPTIssue{mptID}) || + !isAuthorized(view, mptID, account, reqAuth))) { invalidTransfer = true; } diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index ef271ba362..66d059c54d 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -193,6 +193,23 @@ AMMCreate::preclaim(PreclaimContext const& ctx) pseudoAccountAddress(ctx.view, keylet::amm(amount.asset(), amount2.asset()).key); accountId == beast::kZero) return terADDRESS_COLLISION; + + auto const isMPTIssuerPseudo = [&](Asset const& asset) { + if (asset.native()) + return false; + + if (asset.holds()) + return false; + + return isPseudoAccount(ctx.view, asset.getIssuer()); + }; + + if (isMPTIssuerPseudo(amount.asset()) || isMPTIssuerPseudo(amount2.asset())) + { + JLOG(ctx.j.debug()) << "AMM Instance: can't create with vault shares " << amount << " " + << amount2; + return tecWRONG_ASSET; + } } if (auto const ter = canMPTTradeAndTransfer(ctx.view, amount.asset(), accountID, accountID); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 9ff6c65c17..c68270591a 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -6892,168 +6892,38 @@ private: void testAMMWithVaultShares() { - testcase("AMM with vault shares — underlying freeze blocks share withdrawal"); + testcase("AMM with vault shares not allowed"); using namespace jtx; + // AMMTestBase::testableAmendments() strips featureSingleAssetVault, // but vault shares require it. Use the global jtx set directly. - FeatureBitset const all{jtx::testableAmendments()}; + Env env{*this, envconfig(), jtx::testableAmendments(), nullptr, beast::Severity::Disabled}; - // When alice's underlying asset is individually frozen: - // - // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM - // pseudo-account's underlying, not alice's — deposit is allowed. - // Pre-fix: featureAMMClawback calls isFrozen(alice, share) which - // descends via isVaultPseudoAccountFrozen(alice,...) and finds the - // frozen underlying — deposit is blocked. - // - // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with - // checkDeepFrozen(alice, share) which calls isFrozen(alice, share) - // and finds the frozen underlying — withdrawal is blocked. - // Pre-fix: the old path only checks the AMM account's MPToken lock, - // which is unset — withdrawal succeeds. + env.fund(XRP(100'000), gw_, alice_); + env(fset(gw_, asfDefaultRipple)); + env.close(); - auto runIOU = [&](FeatureBitset const& features) { - bool const fix330 = features[fixCleanup3_3_0]; - Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; + PrettyAsset const iou = gw_["IOU"]; + env.trust(iou(1'000'000), alice_); + env(pay(gw_, alice_, iou(10'000))); + env.close(); - env.fund(XRP(100'000), gw_, alice_); - env(fset(gw_, asfDefaultRipple)); - env.close(); + Vault const vault{env}; + auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); + env(createTx); + env.close(); - PrettyAsset const iou = gw_["IOU"]; - env.trust(iou(1'000'000), alice_); - env(pay(gw_, alice_, iou(10'000))); - env.close(); + env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)})); + env.close(); - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou}); - env(createTx); - env.close(); + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; - // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6) - env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)})); - env.close(); - - auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); - // Use half the shares for the AMM; alice keeps the other half. - STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; - // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8 - AMM amm{env, alice_, XRP(100), shareAmt}; - env.close(); - - // Freeze alice's IOU trustline (individual freeze on underlying). - env(trust(gw_, iou(0), alice_, tfSetFreeze)); - env.close(); - - // post-fix330: checkDepositFreeze checks AMM pseudo's underlying - // (not alice's) → deposit is allowed - // pre-fix330: featureAMMClawback path calls isFrozen(alice, share) - // which descends to alice's frozen IOU → tecLOCKED - amm.deposit( - {.account = alice_, - .asset1In = XRP(1), - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); - - // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share) - // descends to alice's frozen IOU → tecLOCKED - // pre-fix330: the AMM pseudo-account is not authorized for the - // share's underlying (requireAuth recurses share→IOU - // and the AMM holds no IOU trustline), so - // accountHolds(ZeroIfUnauthorized) reports the pool's - // share balance as 0 and the withdrawal math fails → - // tecAMM_FAILED. Vault shares deposited into an AMM are - // only withdrawable once fixCleanup3_3_0 exempts the - // pseudo-account from the recursive auth check. - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); - - env(trust(gw_, iou(0), alice_, tfClearFreeze)); - env.close(); - - // Lifting the freeze lets the deposit through in both cases. The - // withdrawal only succeeds post-fix330; pre-fix330 the share balance - // remains inaccessible to the unauthorized pseudo-account, so the - // shares stay stuck → tecAMM_FAILED. - amm.deposit({.account = alice_, .asset1In = XRP(1)}); - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); - }; - - runIOU(all); - runIOU(all - fixCleanup3_3_0); - - auto runMPT = [&](FeatureBitset const& features) { - bool const fix330 = features[fixCleanup3_3_0]; - // Expected freeze failures fire invariant checks that log at Error; - // silence them so the test output stays clean. - Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled}; - - env.fund(XRP(100'000), gw_, alice_); - env.close(); - - MPTTester mptt{env, gw_, kMptInitNoFund}; - mptt.create({.flags = kMptDexFlags | tfMPTCanLock}); - PrettyAsset const mpt = mptt.issuanceID(); - mptt.authorize({.account = alice_}); - env(pay(gw_, alice_, mpt(30'000))); - env.close(); - - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt}); - env(createTx); - env.close(); - - // 20000 MPT → 20000 vault shares (MPT vault scale = 0) - env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)})); - env.close(); - - auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID); - // Use half the shares for the AMM; alice keeps the other half. - // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6 - STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000}; - AMM amm{env, alice_, XRP(100), shareAmt}; - env.close(); - - // Lock alice's underlying MPT (individual lock). - mptt.set({.holder = alice_, .flags = tfMPTLock}); - - // Same pre/post-fix330 semantics as the IOU case above. - amm.deposit( - {.account = alice_, - .asset1In = XRP(1), - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))}); - - // {.tokens = 1'000} → frac = 1000/1e6 = 0.001 - // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10 - // post-fix330: checkWithdrawFreeze sees alice's locked underlying - // MPT via the share → tecLOCKED. - // pre-fix330: the AMM pseudo-account is unauthorized for the - // share's underlying MPT (it holds no underlying - // MPToken), so accountHolds(ZeroIfUnauthorized) zeros - // the pool's share balance and the math fails → - // tecAMM_FAILED. - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))}); - - mptt.set({.holder = alice_, .flags = tfMPTUnlock}); - - // Unlocking lets the deposit through; the withdrawal only succeeds - // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED). - amm.deposit({.account = alice_, .asset1In = XRP(1)}); - amm.withdraw( - {.account = alice_, - .tokens = 1'000, - .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))}); - }; - - runMPT(all); - runMPT(all - fixCleanup3_3_0); + auto const shareMPTID = vaultSle->at(sfShareMPTID); + STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000}; + AMM const amm{env, alice_, XRP(100), shareAmt, Ter(tecWRONG_ASSET)}; + env.close(); } void diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 079ff9df88..79fdbb54ed 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -65,6 +64,7 @@ #include #include #include +#include #include #include @@ -4606,15 +4606,17 @@ class Invariants_test : public beast::unit_test::Suite } } - // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen - // on fixCleanup3_3_0. Pre-amendment, vault-share transfers are allowed - // even when the underlying asset is individually frozen for the sender; - // post-amendment they are blocked. + // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends + // through sfReferenceHolding to test the vault's underlying asset for + // each changed holder. { Account const gw{"gw"}; MPTID shareID{}; - auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + // Vault setup: a1 and a2 both deposit IOU and hold vault shares. + auto const setupVault = [&](Account const& a1, + Account const& a2, + Env& env) -> std::tuple { env.fund(XRP(1'000), gw); env.trust(gw["IOU"](10'000), a1); env.trust(gw["IOU"](10'000), a2); @@ -4623,25 +4625,17 @@ class Invariants_test : public beast::unit_test::Suite env(pay(gw, a2, gw["IOU"](500))); env.close(); - PrettyAsset const iou = gw["IOU"]; Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou}); + auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]}); env(createTx); env.close(); - // Both a1 and a2 deposit IOU, each receiving vault shares. - env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)})); - env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)})); + env(vault.deposit( + {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)})); + env(vault.deposit( + {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)})); env.close(); - shareID = env.le(vaultKeylet)->at(sfShareMPTID); - - // Freeze a2's IOU trustline from the issuer side. - // a2 is the receiver in the simulated AMM withdraw; the - // distinction under test is that pre-fix330 the invariant - // does not apply the transitive vault freeze to receivers. - env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); - env.close(); - return true; + return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)}; }; // Simulate a vault-share transfer: a1 sends 10 shares to a2. @@ -4658,156 +4652,45 @@ class Invariants_test : public beast::unit_test::Suite return true; }; - // post-fixCleanup3_3_0: full isFrozen() applies to all holders; - // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen → - // invalidTransfer → invariant fires. - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose); + // Case: vault pseudo-account's IOU trustline is frozen. + { + auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + auto [sid, vid] = setupVault(a1, a2, env); + shareID = sid; + env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze)); + env.close(); + return true; + }; - // pre-fixCleanup3_3_0: legacy AMM withdraw only checked - // checkIndividualFrozen on the destination, not the transitive - // vault freeze; a2 as receiver is exempt → invariant passes. - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {}, - precheck, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tesSUCCESS, tesSUCCESS}, - preclose); - } + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose); + } - // Side-specific vault-share AMM_WITHDRAW invariant tests. - // Both cases use a real vault (IOU underlying) and a real AMM whose - // pool includes vault shares. precheck simulates an AMM_WITHDRAW by - // transferring 10 vault shares from the AMM pseudo-account to a2. - { - MPTID shareID{}; - AccountID ammAcctID{}; - AccountID vaultPseudoID{}; - Account const gw{"gw"}; + // Case: receiver's (a2's) IOU trustline is frozen. + { + auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool { + auto [sid, vid] = setupVault(a1, a2, env); + shareID = sid; + env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); + env.close(); + return true; + }; - // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares - // to a2. The AMM pseudo is the sender (decreasing balance); - // a2 is the receiver (increasing balance). - auto const precheck2 = - [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool { - auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID)); - auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id())); - if (!sleAMM || !sle2) - return false; - (*sleAMM)[sfMPTAmount] -= 10; - (*sle2)[sfMPTAmount] += 10; - ac.view().update(sleAMM); - ac.view().update(sle2); - return true; - }; - - // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and - // creates an AMM with XRP + 100 vault shares, giving the AMM - // pseudo-account a vault-share MPToken balance. - auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool { - env.fund(XRP(1'000), gw); - env(fset(gw, asfDefaultRipple)); - env.close(); - - env.trust(gw["IOU"](10'000), a1); - env.trust(gw["IOU"](10'000), a2); - env.close(); - env(pay(gw, a1, gw["IOU"](1'000))); - env(pay(gw, a2, gw["IOU"](500))); - env.close(); - - Vault const vault{env}; - auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]}); - env(createTx); - env.close(); - - env(vault.deposit( - {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)})); - env(vault.deposit( - {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)})); - env.close(); - - shareID = env.le(vaultKeylet)->at(sfShareMPTID); - vaultPseudoID = env.le(vaultKeylet)->at(sfAccount); - - // a1 creates AMM with XRP + 100 vault shares; the AMM - // pseudo-account receives an MPToken record for shareID. - AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100}); - ammAcctID = amm.ammAccount(); - return true; - }; - - // Case 1: freeze the vault pseudo-account's IOU trustline. - // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo, - // ammAcct}, IOU); since vaultPseudo is frozen it returns true. The - // AMM sender has a decreasing balance (not a receiver) so it is - // never exempt from the check — invariant fires both pre- and - // post-fixCleanup3_3_0. - auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVaultAMM(a1, a2, env)) - return false; - env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze)); - env.close(); - return true; - }; - - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose3); - - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose3); - - // Case 2: freeze a2's (receiver's) IOU trustline. - // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2}, - // IOU) → true. The AMM sender's check passes (vaultPseudo and - // ammAcct are not frozen). Pre-fix330: receiver is exempt from - // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes. - // Post-fix330: full isFrozen() applied to a2 → fires. - auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool { - if (!setupVaultAMM(a1, a2, env)) - return false; - env(trust(gw, gw["IOU"](0), a2, tfSetFreeze)); - env.close(); - return true; - }; - - doInvariantCheck( - Env{*this, defaultAmendments()}, - {{"invalid MPToken transfer between holders"}}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - preclose4); - - doInvariantCheck( - Env{*this, defaultAmendments() - fixCleanup3_3_0}, - {}, - precheck2, - XRPAmount{}, - STTx{ttAMM_WITHDRAW, [](STObject&) {}}, - {tesSUCCESS, tesSUCCESS}, - preclose4); + doInvariantCheck( + Env{*this, defaultAmendments()}, + {{"invalid MPToken transfer between holders"}}, + precheck, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + preclose); + } } } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index d9afd93b23..f6574872f9 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1634,8 +1634,6 @@ class Vault_test : public beast::unit_test::Suite env(tx, Ter(tecNO_ENTRY)); }); - // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze - testCase([this]( Env& env, Account const& issuer, @@ -2238,11 +2236,6 @@ class Vault_test : public beast::unit_test::Suite env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION}); env.close(); - // The inherited CanTrade restriction also blocks AMM creation. - AMM const ammUnderlyingFail( - env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION}); - AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION}); - // Deposit still works before enabling CanTrade. env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)})); env.close(); @@ -7580,185 +7573,212 @@ class Vault_test : public beast::unit_test::Suite } void - testVaultDepositFreeze() + testVaultDepositFreezeIOU() { using namespace test::jtx; + testcase("VaultDeposit IOU freeze checks"); Account const issuer{"issuer"}; Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - // === IOU === - { - testcase("VaultDeposit IOU freeze checks"); - Env env{*this}; - Vault vault{env}; + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); - env.fund(XRP(100'000), issuer, owner); - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - PrettyAsset const asset = issuer["IOU"]; - env.trust(asset(1'000'000), owner); - env(pay(issuer, owner, asset(100'000))); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + // Initial deposit so the vault pseudo-account has a trustline + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); - // Initial deposit so the vault pseudo-account has a trustline - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - - // Global freeze + // Global freeze + { + testcase("VaultDeposit IOU global freeze"); env(fset(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(fclear(issuer, asfGlobalFreeze)); + } - // Depositor regular freeze + // Depositor freeze + { + testcase("VaultDeposit IOU depositor freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(trust(issuer, asset(0), owner, tfClearFreeze)); + } - // Depositor deep freeze + // Depositor deep freeze + { + testcase("VaultDeposit IOU depositor deep freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + } - // Vault-account regular freeze - // Post-fix: checkDepositFreeze catches it → tecFROZEN - // Pre-fix: not checked directly, but the transitive share - // check triggers → tecLOCKED - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); + // Vault-account freeze + // Post-fix: checkDepositFreeze catches it → tecFROZEN + // Pre-fix: not checked directly, but the transitive share + // check triggers → tecLOCKED + { + testcase("VaultDeposit IOU pseudo-account freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfSetFreeze; - env(trustSet); - env.close(); + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); - TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(expected)); + TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(expected)); - trustSet[jss::Flags] = tfClearFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } - // Vault-account deep freeze - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); + // Vault-account deep freeze + { + testcase("VaultDeposit IOU pseudo-account deep freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); + { + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); + } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze; - env(trustSet); - env.close(); + trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze; + env(trustSet); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); - trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze; + env(trustSet); + env.close(); + } - // Clawback works while frozen + // Clawback works while frozen + { + testcase("VaultDeposit IOU freeze clawback unaffected"); env(fset(issuer, asfGlobalFreeze)); env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); env(fclear(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } - // === MPT === - { - testcase("VaultDeposit MPT lock checks"); - Env env{*this}; - Vault vault{env}; + void + testVaultDepositFreezeMPT() + { + using namespace test::jtx; + testcase("VaultDeposit MPT lock checks"); - env.fund(XRP(100'000), issuer, owner); - env.close(); + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); - PrettyAsset const mpt{mptt.issuanceID()}; + env.fund(XRP(100'000), issuer, owner); + env.close(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = issuer, .holder = owner}); - env.close(); - env(pay(issuer, owner, mpt(100'000))); - env.close(); + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; - auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); - env(tx); - env.close(); - auto const vaultAcctID = env.le(keylet)->at(sfAccount); - Account const vaultAcct("vault", vaultAcctID); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + auto const vaultAcctID = env.le(keylet)->at(sfAccount); + Account const vaultAcct("vault", vaultAcctID); - // For MPT isDeepFrozen == isFrozen, so all locks block in - // both pre- and post-fix. - auto runTests = [&]() { - // Global lock + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); + + // For MPT isDeepFrozen == isFrozen, so all locks block in + // both pre- and post-fix. + auto runTests = [&]() { + // Global lock + { + testcase("VaultDeposit MPT global lock"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.flags = tfMPTUnlock}); env.close(); + } - // Depositor individual lock + // Depositor individual lock + { + testcase("VaultDeposit MPT depositor lock"); mptt.set({.holder = owner, .flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = owner, .flags = tfMPTUnlock}); env.close(); + } - // Vault pseudo-account individual lock + // Vault pseudo-account individual lock + { + testcase("VaultDeposit MPT pseudo-account lock"); mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); env.close(); + } - // Clawback works while locked + // Clawback works while locked + { + testcase("VaultDeposit MPT lock clawback unaffected"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.clawback( @@ -7767,16 +7787,16 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); } - // Focused demonstration: a depositor under a regular individual IOU freeze + // Focused demonstration: a depositor under an individual IOU freeze // can still withdraw to themselves (self-withdrawal), but is blocked from // withdrawing to a third party. // @@ -7818,7 +7838,7 @@ class Vault_test : public beast::unit_test::Suite auto runTests = [&]() { auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Set a regular individual freeze on the owner's IOU trustline. + // Set an individual freeze on the owner's IOU trustline. env(trust(issuer, asset(0), owner, tfSetFreeze)); env.close(); @@ -7850,110 +7870,111 @@ class Vault_test : public beast::unit_test::Suite } void - testVaultWithdrawFreeze() + testVaultWithdrawFreezeIOU() { using namespace test::jtx; + testcase("VaultWithdraw IOU freeze checks"); Account const issuer{"issuer"}; Account const owner{"owner"}; + Env env{*this}; + Vault const vault{env}; - // === IOU === - { - testcase("VaultWithdraw IOU freeze checks"); - Env env{*this}; - Vault vault{env}; + env.fund(XRP(100'000), issuer, owner); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), owner); + env(pay(issuer, owner, asset(100'000))); + env.close(); - env.fund(XRP(100'000), issuer, owner); - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - PrettyAsset const asset = issuer["IOU"]; - env.trust(asset(1'000'000), owner); - env(pay(issuer, owner, asset(100'000))); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount)); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.trust(asset(1'000'000), charlie); + env.close(); - Account const charlie{"charlie"}; - env.fund(XRP(10'000), charlie); - env.trust(asset(1'000'000), charlie); - env.close(); - - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); - // Post-fix: submitter freeze blocks withdraw to 3rd party - // Pre-fix: submitter's IOU freeze not checked, but - // checkFrozen(depositor, share) may trigger tecLOCKED - TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); - - // Global freeze → self-withdraw + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + // Global freeze → self-withdraw + { + testcase("VaultWithdraw IOU global freeze"); env(fset(issuer, asfGlobalFreeze)); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(tecFROZEN)); // Global freeze → withdraw to 3rd party - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(tecFROZEN)); - } + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); + env(fclear(issuer, asfGlobalFreeze)); + } - // Vault-account regular freeze - { - auto trustSet = [&]() { - json::Value jv; - jv[jss::Account] = issuer.human(); - { - auto& ja = jv[jss::LimitAmount] = - asset(0).value().getJson(JsonOptions::Values::None); - ja[jss::issuer] = toBase58(vaultAcct.id()); - } - jv[jss::TransactionType] = jss::TrustSet; - return jv; - }(); - - trustSet[jss::Flags] = tfSetFreeze; - env(trustSet); - env.close(); - - TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); - - // Self-withdraw - env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - Ter(vaultAcctFreeze)); - // Withdraw to 3rd party + // Vault-account freeze + { + testcase("VaultWithdraw IOU pseudo-account freeze"); + auto trustSet = [&]() { + json::Value jv; + jv[jss::Account] = issuer.human(); { - auto withdrawToCharlie = vault.withdraw( - {.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(vaultAcctFreeze)); + auto& ja = jv[jss::LimitAmount] = + asset(0).value().getJson(JsonOptions::Values::None); + ja[jss::issuer] = toBase58(vaultAcct.id()); } + jv[jss::TransactionType] = jss::TrustSet; + return jv; + }(); - trustSet[jss::Flags] = tfClearFreeze; - env(trustSet); - env.close(); - } + trustSet[jss::Flags] = tfSetFreeze; + env(trustSet); + env.close(); - // Depositor regular freeze → self-withdraw + TER const terExpected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED); + + // Self-withdraw + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(terExpected)); + // Withdraw to 3rd party + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(terExpected)); + + trustSet[jss::Flags] = tfClearFreeze; + env(trustSet); + env.close(); + } + + // Depositor freeze, self-withdraw + { + testcase("VaultWithdraw IOU self-withdraw freeze check"); env(trust(issuer, asset(0), owner, tfSetFreeze)); + // Post-fix: self-withdraw allowed (submitter==dst skip) // Pre-fix: isFrozen(depositor, iou) catches it env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); - // Depositor regular freeze → withdraw to 3rd party - { - auto withdrawTo3rd = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawTo3rd[sfDestination] = charlie.human(); - env(withdrawTo3rd, Ter(submitterTo3rd)); - } + // Depositor freeze withdraw to 3rd party + auto withdrawTo3rd = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawTo3rd[sfDestination] = charlie.human(); + + // Post-fix: submitter freeze blocks withdraw to 3rd party + // Pre-fix: submitter's IOU freeze not checked, but checkFrozen(depositor, + // share) triggers tecLOCKED + env(withdrawTo3rd, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED))); + env(trust(issuer, asset(0), owner, tfClearFreeze)); // Replenish what was withdrawn if (fix330Enabled) @@ -7961,104 +7982,131 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); } env.close(); + } - // Depositor deep freeze → self-withdraw blocked + // Depositor deep freeze → self-withdraw blocked + { + testcase("VaultWithdraw IOU depositor deep freeze"); env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze)); - // TODO: branches are identical - confirm the intended pre/post-fix330 - // expectations and replace with the correct values (one branch may be wrong). - env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), - // NOLINTNEXTLINE(bugprone-branch-clone) - Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN))); - env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); - // Destination regular freeze → withdraw to 3rd party + env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}), + Ter(tecFROZEN)); + + env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze)); + } + + // Destination freeze → withdraw to 3rd party + { + testcase("VaultWithdraw IOU freeze withdraw to 3rd party"); + env(trust(issuer, asset(0), charlie, tfSetFreeze)); + // Self-withdraw unaffected by charlie's freeze env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - // Post-fix: regular freeze on dst allowed - // Pre-fix: checkFrozen(dst, iou) catches it - env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); - } + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + + // Post-fix: freeze on dst allowed + // Pre-fix: checkFrozen(dst, iou) catches it + env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN))); + env(trust(issuer, asset(0), charlie, tfClearFreeze)); + // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(fix330Enabled ? 2 : 1)})); env.close(); + } + + // Destination deep freeze → withdraw to 3rd party blocked + { + testcase("VaultWithdraw IOU deep freeze withdraw to 3rd party"); - // Destination deep freeze → withdraw to 3rd party blocked env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze)); - { - auto withdrawToCharlie = - vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); - withdrawToCharlie[sfDestination] = charlie.human(); - env(withdrawToCharlie, Ter(tecFROZEN)); - } + + auto withdrawToCharlie = + vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}); + withdrawToCharlie[sfDestination] = charlie.human(); + env(withdrawToCharlie, Ter(tecFROZEN)); + // Destination deep freeze → self-withdraw unaffected env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)})); + env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); + } - // Clawback works while frozen + // Clawback works while frozen + { + testcase("VaultWithdraw IOU freeze clawback unaffected"); env(fset(issuer, asfGlobalFreeze)); + env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)})); + env(fclear(issuer, asfGlobalFreeze)); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); + } - // === MPT === - { - testcase("VaultWithdraw MPT lock checks"); - Env env{*this}; - Vault vault{env}; + void + testVaultWithdrawFreezeMPT() + { + using namespace test::jtx; + testcase("VaultWithdraw MPT lock checks"); - env.fund(XRP(100'000), issuer, owner); - env.close(); + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Env env{*this}; + Vault vault{env}; - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); - PrettyAsset const mpt{mptt.issuanceID()}; + env.fund(XRP(100'000), issuer, owner); + env.close(); - mptt.authorize({.account = owner}); - mptt.authorize({.account = issuer, .holder = owner}); - env.close(); - env(pay(issuer, owner, mpt(100'000))); - env.close(); + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth}); + PrettyAsset const mpt{mptt.issuanceID()}; - auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); - env(tx); - env.close(); - Account const vaultAcct("vault", env.le(keylet)->at(sfAccount)); + mptt.authorize({.account = owner}); + mptt.authorize({.account = issuer, .holder = owner}); + env.close(); + env(pay(issuer, owner, mpt(100'000))); + env.close(); - env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); - env.close(); + auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt}); + env(tx); + env.close(); + Account const vaultAcct("vault", env.le(keylet)->at(sfAccount)); - Account const charlie{"charlie"}; - env.fund(XRP(10'000), charlie); - env.close(); - mptt.authorize({.account = charlie}); - mptt.authorize({.account = issuer, .holder = charlie}); - env.close(); + env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)})); + env.close(); - auto runTests = [&]() { - auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + Account const charlie{"charlie"}; + env.fund(XRP(10'000), charlie); + env.close(); + mptt.authorize({.account = charlie}); + mptt.authorize({.account = issuer, .holder = charlie}); + env.close(); - // Global lock + auto runTests = [&]() { + auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0); + + // Global lock + { + testcase("VaultWithdraw MPT global lock"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), @@ -8081,17 +8129,23 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); } env.close(); + } - // Vault pseudo-account individual lock + // Vault pseudo-account individual lock + { + testcase("VaultWithdraw MPT pseudo-account lock"); mptt.set({.holder = vaultAcct, .flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), Ter(tecLOCKED)); mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock}); env.close(); + } - // Depositor individual lock → self-withdraw blocked - // (isDeepFrozen == isFrozen for MPT) + // Depositor individual lock → self-withdraw blocked + // (isDeepFrozen == isFrozen for MPT) + { + testcase("VaultWithdraw MPT depositor lock"); mptt.set({.holder = owner, .flags = tfMPTLock}); env.close(); env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}), @@ -8120,8 +8174,11 @@ class Vault_test : public beast::unit_test::Suite env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); } env.close(); + } - // 3rd party destination lock → withdraw to 3rd party blocked + // 3rd party destination lock → withdraw to 3rd party blocked + { + testcase("VaultWithdraw MPT 3rd party destination lock"); mptt.set({.holder = charlie, .flags = tfMPTLock}); env.close(); { @@ -8136,8 +8193,11 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); + } - // Clawback works while locked + // Clawback works while locked + { + testcase("VaultWithdraw MPT lock clawback unaffected"); mptt.set({.flags = tfMPTLock}); env.close(); env(vault.clawback( @@ -8146,14 +8206,13 @@ class Vault_test : public beast::unit_test::Suite env.close(); env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)})); env.close(); - }; + } + }; - runTests(); - env.disableFeature(fixCleanup3_3_0); - - runTests(); - env.enableFeature(fixCleanup3_3_0); - } + runTests(); + env.disableFeature(fixCleanup3_3_0); + runTests(); + env.enableFeature(fixCleanup3_3_0); } public: @@ -8198,8 +8257,10 @@ public: testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice(); testWithdrawSoleShareholderLoanRepaymentExit(); - testVaultDepositFreeze(); - testVaultWithdrawFreeze(); + testVaultDepositFreezeIOU(); + testVaultDepositFreezeMPT(); + testVaultWithdrawFreezeIOU(); + testVaultWithdrawFreezeMPT(); testVaultSelfWithdrawWhileFrozen(); testReferenceHolding(); From c53aafa6bfeb8591532c770cfcc59abf5f156a2f Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Wed, 1 Jul 2026 11:36:28 -0400 Subject: [PATCH 18/31] refactor: Retire InnerObjTemplate fix (#7368) --- include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/ledger/helpers/AMMHelpers.cpp | 6 ++--- src/libxrpl/protocol/STObject.cpp | 6 ++--- src/libxrpl/tx/transactors/dex/AMMBid.cpp | 17 ++++---------- src/libxrpl/tx/transactors/dex/AMMVote.cpp | 4 +--- src/test/app/AMM_test.cpp | 24 +------------------- src/test/jtx/impl/AMM.cpp | 10 ++------ src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp | 4 +--- 8 files changed, 15 insertions(+), 58 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index a1911761bb..7e64a49b0c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -58,7 +58,6 @@ XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PriceOracle, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMOverflowOffer, Supported::Yes, VoteBehavior::DefaultYes) -XRPL_FIX (InnerObjTemplate, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (FillOrKill, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DID, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo) @@ -101,6 +100,7 @@ XRPL_RETIRE_FIX(1623) XRPL_RETIRE_FIX(1781) XRPL_RETIRE_FIX(AmendmentMajorityCalc) XRPL_RETIRE_FIX(CheckThreading) +XRPL_RETIRE_FIX(InnerObjTemplate) XRPL_RETIRE_FIX(MasterKeyAsRegularKey) XRPL_RETIRE_FIX(NonFungibleTokensV1_2) XRPL_RETIRE_FIX(NFTokenRemint) diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp index 7cd27a1f34..df6d335085 100644 --- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -602,9 +602,7 @@ std::uint16_t getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account) { using namespace std::chrono; - XRPL_ASSERT( - !view.rules().enabled(fixInnerObjTemplate) || ammSle.isFieldPresent(sfAuctionSlot), - "xrpl::getTradingFee : auction present"); + XRPL_ASSERT(ammSle.isFieldPresent(sfAuctionSlot), "xrpl::getTradingFee : auction present"); if (ammSle.isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safeDowncast(ammSle.peekAtField(sfAuctionSlot)); @@ -822,7 +820,7 @@ initializeFeeAuctionVote( // AMM creator gets the auction slot for free. // AuctionSlot is created on AMMCreate and updated on AMMDeposit // when AMM is in an empty state - if (rules.enabled(fixInnerObjTemplate) && !ammSle->isFieldPresent(sfAuctionSlot)) + if (!ammSle->isFieldPresent(sfAuctionSlot)) { STObject auctionSlot = STObject::makeInnerObject(sfAuctionSlot); ammSle->set(std::move(auctionSlot)); diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 445da16828..c493ba65af 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -78,12 +78,10 @@ STObject::makeInnerObject(SField const& name) // The if is complicated because inner object templates were added in // two phases: // 1. If there are no available Rules, then always apply the template. - // 2. fixInnerObjTemplate added templates to two AMM inner objects. - // 3. fixInnerObjTemplate2 added templates to all remaining inner objects. + // 2. fixInnerObjTemplate2 added templates to all remaining inner objects. std::optional const& rules = getCurrentTransactionRules(); bool const isAMMObj = name == sfAuctionSlot || name == sfVoteEntry; - if (!rules || (rules->enabled(fixInnerObjTemplate) && isAMMObj) || - (rules->enabled(fixInnerObjTemplate2) && !isAMMObj)) + if (!rules || isAMMObj || rules->enabled(fixInnerObjTemplate2)) { if (SOTemplate const* elements = InnerObjectFormats::getInstance().findSOTemplateBySField(name)) diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp b/src/libxrpl/tx/transactors/dex/AMMBid.cpp index 83432fa0d0..3454559e82 100644 --- a/src/libxrpl/tx/transactors/dex/AMMBid.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp @@ -184,18 +184,11 @@ applyBid(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Journa return {tecINTERNAL, false}; STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance]; auto const lpTokens = ammLPHolds(sb, *ammSle, account, ctx.journal); - auto const& rules = ctx.view().rules(); - if (!rules.enabled(fixInnerObjTemplate)) - { - if (!ammSle->isFieldPresent(sfAuctionSlot)) - ammSle->makeFieldPresent(sfAuctionSlot); - } - else - { - XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyBid : has auction slot"); - if (!ammSle->isFieldPresent(sfAuctionSlot)) - return {tecINTERNAL, false}; - } + + XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyBid : has auction slot"); + if (!ammSle->isFieldPresent(sfAuctionSlot)) + return {tecINTERNAL, false}; + auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot); auto const current = duration_cast(ctx.view().header().parentCloseTime.time_since_epoch()).count(); diff --git a/src/libxrpl/tx/transactors/dex/AMMVote.cpp b/src/libxrpl/tx/transactors/dex/AMMVote.cpp index 23604d46cc..0f2b721ac4 100644 --- a/src/libxrpl/tx/transactors/dex/AMMVote.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMVote.cpp @@ -195,9 +195,7 @@ applyVote(ApplyContext& ctx, Sandbox& sb, AccountID const& accountID, beast::Jou } } - XRPL_ASSERT( - !ctx.view().rules().enabled(fixInnerObjTemplate) || ammSle->isFieldPresent(sfAuctionSlot), - "xrpl::applyVote : has auction slot"); + XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyVote : has auction slot"); // Update the vote entries and the trading/discounted fee. ammSle->setFieldArray(sfVoteSlots, updatedVoteSlots); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 3672c4a68a..d4b6ba5429 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -5747,38 +5747,16 @@ private: }; // ledger is closed after each transaction, vote/withdraw don't fail - // regardless whether the amendment is enabled or not test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true); - test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true); // ledger is not closed after each transaction - // vote/withdraw don't fail if the amendment is enabled test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false); - // vote/withdraw fail if the amendment is not enabled - // second vote/withdraw still fail: second vote fails because - // the initial trading fee is 0, consequently second withdraw fails - // because the second vote fails - test( - all - fixInnerObjTemplate, - tefEXCEPTION, - tefEXCEPTION, - tefEXCEPTION, - tefEXCEPTION, - 0, - false); // if non-zero trading/discounted fee then vote/withdraw - // don't fail whether the ledger is closed or not and - // the amendment is enabled or not + // don't fail whether the ledger is closed or not test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true); - test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true); test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false); - test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false); // non-zero trading fee but discounted fee is 0, vote doesn't fail // but withdraw fails test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false); - // second vote sets the trading fee to non-zero, consequently - // second withdraw doesn't fail even if the amendment is not - // enabled and the ledger is not closed - test(all - fixInnerObjTemplate, tesSUCCESS, tefEXCEPTION, tesSUCCESS, tesSUCCESS, 9, false); } void diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 2184f7e1b0..5c815d4226 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -738,8 +738,7 @@ AMM::bid(BidArg const& arg) { if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - if (env_.current()->rules().enabled(fixInnerObjTemplate) && - !amm->isFieldPresent(sfAuctionSlot)) + if (!amm->isFieldPresent(sfAuctionSlot)) Throw("AMM::Bid"); if (amm->isFieldPresent(sfAuctionSlot)) { @@ -867,8 +866,7 @@ AMM::expectAuctionSlot(auto&& cb) const { if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { - if (env_.current()->rules().enabled(fixInnerObjTemplate) && - !amm->isFieldPresent(sfAuctionSlot)) + if (!amm->isFieldPresent(sfAuctionSlot)) Throw("AMM::expectAuctionSlot"); if (amm->isFieldPresent(sfAuctionSlot)) { @@ -876,10 +874,6 @@ AMM::expectAuctionSlot(auto&& cb) const safeDowncast(amm->peekAtField(sfAuctionSlot)); if (auctionSlot.isFieldPresent(sfAccount)) { - // This could fail in pre-fixInnerObjTemplate tests - // if the submitted transactions recreate one of - // the failure scenarios. Access as optional - // to avoid the failure. auto const slotFee = auctionSlot[~sfDiscountedFee].value_or(0); auto const slotInterval = ammAuctionTimeSlot( env_.app().getTimeKeeper().now().time_since_epoch().count(), auctionSlot); diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp index 043fdd2d7b..7f54f81423 100644 --- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp @@ -214,9 +214,7 @@ doAMMInfo(RPC::JsonContext& context) } if (voteSlots.size() > 0) ammResult[jss::vote_slots] = std::move(voteSlots); - XRPL_ASSERT( - !ledger->rules().enabled(fixInnerObjTemplate) || amm->isFieldPresent(sfAuctionSlot), - "xrpl::doAMMInfo : auction slot is set"); + XRPL_ASSERT(amm->isFieldPresent(sfAuctionSlot), "xrpl::doAMMInfo : auction slot is present"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safeDowncast(amm->peekAtField(sfAuctionSlot)); From 8e378c4f4727a188ca0300b76142f2f867ba1e30 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 2 Jul 2026 00:14:17 +0100 Subject: [PATCH 19/31] build: Add verify-headers target to cleanup headers (#7670) --- .clang-tidy | 2 +- .gersemi/definitions.cmake | 6 + .../scripts/levelization/results/loops.txt | 6 - .../scripts/levelization/results/ordering.txt | 11 +- .github/workflows/reusable-clang-tidy.yml | 3 +- .pre-commit-config.yaml | 6 +- BUILD.md | 38 +++- bin/pre-commit/clang_tidy_check.py | 171 ++---------------- cmake/XrplCore.cmake | 9 + cmake/XrplSettings.cmake | 17 ++ cmake/add_module.cmake | 17 ++ cmake/verify_headers.cmake | 84 +++++++++ include/xrpl/basics/Buffer.h | 1 + include/xrpl/basics/CompressionAlgorithms.h | 1 + include/xrpl/basics/DecayingSample.h | 1 + include/xrpl/basics/FileUtilities.h | 2 + include/xrpl/basics/IntrusivePointer.h | 1 + include/xrpl/basics/IntrusiveRefCounts.h | 1 + include/xrpl/basics/Log.h | 3 +- include/xrpl/basics/Number.h | 4 + include/xrpl/basics/RangeSet.h | 1 + include/xrpl/basics/Resolver.h | 1 + include/xrpl/basics/ResolverAsio.h | 2 + include/xrpl/basics/SHAMapHash.h | 2 + include/xrpl/basics/SharedWeakCachePointer.h | 2 + include/xrpl/basics/SlabAllocator.h | 1 + include/xrpl/basics/StringUtilities.h | 3 +- include/xrpl/basics/TaggedCache.h | 15 +- include/xrpl/basics/TaggedCache.ipp | 1 + include/xrpl/basics/UnorderedContainers.h | 4 +- include/xrpl/basics/base64.h | 1 + include/xrpl/basics/base_uint.h | 10 + include/xrpl/basics/chrono.h | 1 + include/xrpl/basics/hardened_hash.h | 1 - include/xrpl/basics/join.h | 2 + include/xrpl/basics/make_SSLContext.h | 1 + .../xrpl/basics/partitioned_unordered_map.h | 3 + include/xrpl/basics/random.h | 1 - include/xrpl/basics/safe_cast.h | 2 - include/xrpl/basics/strHex.h | 3 + include/xrpl/basics/tagged_integer.h | 1 + include/xrpl/beast/asio/io_latency_probe.h | 1 + .../beast/container/aged_container_utility.h | 1 + include/xrpl/beast/container/aged_map.h | 1 + include/xrpl/beast/container/aged_multimap.h | 1 + .../xrpl/beast/container/aged_unordered_map.h | 1 + .../beast/container/aged_unordered_multimap.h | 1 + .../container/detail/aged_ordered_container.h | 27 +-- .../detail/aged_unordered_container.h | 6 + include/xrpl/beast/core/CurrentThreadName.h | 1 + include/xrpl/beast/core/LexicalCast.h | 5 +- include/xrpl/beast/core/List.h | 2 + include/xrpl/beast/core/LockFreeStack.h | 3 +- include/xrpl/beast/hash/hash_append.h | 2 +- include/xrpl/beast/insight/Collector.h | 2 + include/xrpl/beast/insight/Insight.h | 15 -- include/xrpl/beast/insight/NullCollector.h | 2 + include/xrpl/beast/insight/StatsDCollector.h | 3 + include/xrpl/beast/net/IPAddress.h | 1 + include/xrpl/beast/net/IPAddressV4.h | 2 - include/xrpl/beast/net/IPAddressV6.h | 2 - include/xrpl/beast/net/IPEndpoint.h | 5 + include/xrpl/beast/rfc2616.h | 1 + include/xrpl/beast/test/yield_to.h | 2 + include/xrpl/beast/unit_test.h | 9 - include/xrpl/beast/unit_test/match.h | 1 + include/xrpl/beast/unit_test/recorder.h | 4 + include/xrpl/beast/unit_test/reporter.h | 5 +- include/xrpl/beast/unit_test/results.h | 1 + include/xrpl/beast/unit_test/suite.h | 4 +- include/xrpl/beast/unit_test/suite_info.h | 2 +- include/xrpl/beast/unit_test/suite_list.h | 1 + include/xrpl/beast/unit_test/thread.h | 2 + include/xrpl/beast/utility/Journal.h | 3 + include/xrpl/beast/utility/PropertyStream.h | 2 + include/xrpl/beast/utility/WrappedSink.h | 1 + include/xrpl/beast/utility/rngfill.h | 2 - include/xrpl/conditions/Condition.h | 4 + include/xrpl/conditions/Fulfillment.h | 5 + .../xrpl/conditions/detail/PreimageSha256.h | 4 + include/xrpl/conditions/detail/utils.h | 3 + include/xrpl/config/BasicConfig.h | 4 + include/xrpl/core/ClosureCounter.h | 3 + include/xrpl/core/HashRouter.h | 6 + include/xrpl/core/Job.h | 5 + include/xrpl/core/JobQueue.h | 22 ++- include/xrpl/core/JobTypeData.h | 3 + include/xrpl/core/JobTypeInfo.h | 4 + include/xrpl/core/JobTypes.h | 4 + include/xrpl/core/LoadMonitor.h | 1 + include/xrpl/core/PeerReservationTable.h | 1 + include/xrpl/core/PerfLog.h | 3 +- include/xrpl/core/ServiceRegistry.h | 6 + include/xrpl/core/StartUpType.h | 2 +- include/xrpl/core/detail/semaphore.h | 1 + include/xrpl/crypto/RFC1751.h | 1 + include/xrpl/crypto/csprng.h | 3 + include/xrpl/json/JsonPropertyStream.h | 3 + include/xrpl/json/Writer.h | 4 +- include/xrpl/json/detail/json_assert.h | 3 - include/xrpl/json/json_reader.h | 3 + include/xrpl/json/json_writer.h | 3 + include/xrpl/ledger/AcceptedLedgerTx.h | 10 + include/xrpl/ledger/AmendmentTable.h | 23 +++ include/xrpl/ledger/ApplyView.h | 15 +- include/xrpl/ledger/ApplyViewImpl.h | 11 ++ include/xrpl/ledger/BookDirs.h | 7 + include/xrpl/ledger/CachedView.h | 9 + include/xrpl/ledger/CanonicalTXSet.h | 6 + include/xrpl/ledger/Dir.h | 10 +- include/xrpl/ledger/Ledger.h | 22 ++- include/xrpl/ledger/LedgerTiming.h | 5 +- include/xrpl/ledger/OpenView.h | 13 +- include/xrpl/ledger/PaymentSandbox.h | 10 +- include/xrpl/ledger/RawView.h | 3 + include/xrpl/ledger/ReadView.h | 11 +- include/xrpl/ledger/Sandbox.h | 2 + include/xrpl/ledger/View.h | 9 + include/xrpl/ledger/detail/ApplyStateTable.h | 13 ++ include/xrpl/ledger/detail/ApplyViewBase.h | 10 + include/xrpl/ledger/detail/RawStateTable.h | 8 + include/xrpl/ledger/detail/ReadViewFwdRange.h | 2 + include/xrpl/ledger/helpers/AMMHelpers.h | 16 +- .../xrpl/ledger/helpers/AccountRootHelpers.h | 5 + .../xrpl/ledger/helpers/CredentialHelpers.h | 9 +- include/xrpl/ledger/helpers/DelegateHelpers.h | 3 + .../xrpl/ledger/helpers/DirectoryHelpers.h | 6 +- include/xrpl/ledger/helpers/EscrowHelpers.h | 17 +- include/xrpl/ledger/helpers/LendingHelpers.h | 21 ++- include/xrpl/ledger/helpers/MPTokenHelpers.h | 6 + include/xrpl/ledger/helpers/NFTokenHelpers.h | 16 +- .../ledger/helpers/PaymentChannelHelpers.h | 5 +- .../ledger/helpers/PermissionedDEXHelpers.h | 6 +- .../xrpl/ledger/helpers/RippleStateHelpers.h | 7 + include/xrpl/ledger/helpers/TokenHelpers.h | 7 + include/xrpl/net/AutoSocket.h | 8 + include/xrpl/net/HTTPClient.h | 1 + include/xrpl/net/HTTPClientSSLContext.h | 8 + include/xrpl/net/RegisterSSLCerts.h | 2 +- include/xrpl/nodestore/Backend.h | 9 + include/xrpl/nodestore/Database.h | 18 +- include/xrpl/nodestore/DatabaseRotating.h | 7 + include/xrpl/nodestore/DummyScheduler.h | 1 + include/xrpl/nodestore/Factory.h | 6 +- include/xrpl/nodestore/Manager.h | 9 +- include/xrpl/nodestore/NodeObject.h | 4 + include/xrpl/nodestore/Types.h | 1 + include/xrpl/nodestore/detail/BatchWriter.h | 2 + .../xrpl/nodestore/detail/DatabaseNodeImp.h | 16 ++ .../nodestore/detail/DatabaseRotatingImp.h | 11 ++ include/xrpl/nodestore/detail/DecodedBlob.h | 2 + include/xrpl/nodestore/detail/EncodedBlob.h | 3 + include/xrpl/nodestore/detail/ManagerImp.h | 11 ++ include/xrpl/nodestore/detail/codec.h | 4 + include/xrpl/nodestore/detail/varint.h | 1 + include/xrpl/proto/org/xrpl/rpc/v1/README.md | 6 +- include/xrpl/protocol/AMMCore.h | 5 + include/xrpl/protocol/AccountID.h | 6 +- include/xrpl/protocol/AmountConversions.h | 10 + include/xrpl/protocol/ApiVersion.h | 1 + include/xrpl/protocol/Asset.h | 11 +- include/xrpl/protocol/Batch.h | 5 +- include/xrpl/protocol/Book.h | 11 ++ include/xrpl/protocol/BuildInfo.h | 1 + include/xrpl/protocol/ConfidentialTransfer.h | 15 +- include/xrpl/protocol/ErrorCodes.h | 3 +- include/xrpl/protocol/Feature.h | 2 + include/xrpl/protocol/Fees.h | 3 + include/xrpl/protocol/IOUAmount.h | 1 + include/xrpl/protocol/Indexes.h | 11 +- include/xrpl/protocol/InnerObjectFormats.h | 2 + include/xrpl/protocol/Issue.h | 6 +- include/xrpl/protocol/KnownFormats.h | 4 + include/xrpl/protocol/LedgerFormats.h | 3 + include/xrpl/protocol/LedgerHeader.h | 3 + include/xrpl/protocol/MPTAmount.h | 4 +- include/xrpl/protocol/MPTIssue.h | 10 + include/xrpl/protocol/PathAsset.h | 7 + include/xrpl/protocol/Permissions.h | 5 +- include/xrpl/protocol/Protocol.h | 2 + include/xrpl/protocol/PublicKey.h | 10 + include/xrpl/protocol/Quality.h | 6 +- include/xrpl/protocol/QualityFunction.h | 6 + include/xrpl/protocol/Rate.h | 2 +- include/xrpl/protocol/Rules.h | 3 + include/xrpl/protocol/SField.h | 3 +- include/xrpl/protocol/SOTemplate.h | 2 + include/xrpl/protocol/STAccount.h | 4 + include/xrpl/protocol/STAmount.h | 16 +- include/xrpl/protocol/STArray.h | 11 ++ include/xrpl/protocol/STBase.h | 3 + include/xrpl/protocol/STBitString.h | 7 + include/xrpl/protocol/STBlob.h | 6 +- include/xrpl/protocol/STCurrency.h | 6 +- include/xrpl/protocol/STExchange.h | 3 +- include/xrpl/protocol/STInteger.h | 8 + include/xrpl/protocol/STIssue.h | 9 + include/xrpl/protocol/STLedgerEntry.h | 14 +- include/xrpl/protocol/STNumber.h | 7 + include/xrpl/protocol/STObject.h | 14 +- include/xrpl/protocol/STParsedJSON.h | 5 +- include/xrpl/protocol/STPathSet.h | 5 +- include/xrpl/protocol/STTakesAsset.h | 2 + include/xrpl/protocol/STTx.h | 14 +- include/xrpl/protocol/STValidation.h | 43 +++-- include/xrpl/protocol/STVector256.h | 10 +- include/xrpl/protocol/STXChainBridge.h | 10 + include/xrpl/protocol/SecretKey.h | 4 + include/xrpl/protocol/Seed.h | 3 + include/xrpl/protocol/Serializer.h | 3 +- include/xrpl/protocol/Sign.h | 4 + include/xrpl/protocol/SystemParameters.h | 3 + include/xrpl/protocol/TER.h | 2 + include/xrpl/protocol/TxFormats.h | 2 + include/xrpl/protocol/TxMeta.h | 11 +- include/xrpl/protocol/UintTypes.h | 6 +- include/xrpl/protocol/Units.h | 4 + include/xrpl/protocol/XChainAttestations.h | 8 +- include/xrpl/protocol/XRPAmount.h | 5 + include/xrpl/protocol/detail/STVar.h | 1 + include/xrpl/protocol/detail/b58_utils.h | 4 +- include/xrpl/protocol/detail/token_errors.h | 2 + include/xrpl/protocol/digest.h | 3 + include/xrpl/protocol/json_get_or_throw.h | 4 + include/xrpl/protocol/messages.h | 2 - include/xrpl/protocol/serialize.h | 3 + include/xrpl/protocol/st.h | 18 -- include/xrpl/protocol/tokens.h | 3 +- include/xrpl/rdb/DatabaseCon.h | 12 +- include/xrpl/rdb/RelationalDatabase.h | 18 +- include/xrpl/rdb/SociDB.h | 5 +- include/xrpl/resource/Charge.h | 2 + include/xrpl/resource/Consumer.h | 5 +- include/xrpl/resource/ResourceManager.h | 4 + include/xrpl/resource/Types.h | 10 - include/xrpl/resource/detail/Entry.h | 7 +- include/xrpl/resource/detail/Import.h | 2 + include/xrpl/resource/detail/Key.h | 3 +- include/xrpl/resource/detail/Logic.h | 11 +- include/xrpl/server/InfoSub.h | 9 + include/xrpl/server/LoadFeeTrack.h | 1 + include/xrpl/server/Manifest.h | 8 + include/xrpl/server/NetworkOPs.h | 15 +- include/xrpl/server/Port.h | 3 +- include/xrpl/server/Server.h | 4 +- include/xrpl/server/Session.h | 6 +- include/xrpl/server/SimpleWriter.h | 5 +- include/xrpl/server/State.h | 4 +- include/xrpl/server/Vacuum.h | 1 + include/xrpl/server/WSSession.h | 3 +- include/xrpl/server/Wallet.h | 12 ++ include/xrpl/server/Writer.h | 1 + include/xrpl/server/detail/BaseHTTPPeer.h | 11 +- include/xrpl/server/detail/BasePeer.h | 3 +- include/xrpl/server/detail/BaseWSPeer.h | 8 + include/xrpl/server/detail/Door.h | 5 + include/xrpl/server/detail/JSONRPCUtil.h | 3 +- include/xrpl/server/detail/LowestLayer.h | 9 - include/xrpl/server/detail/PlainHTTPPeer.h | 5 + include/xrpl/server/detail/PlainWSPeer.h | 4 + include/xrpl/server/detail/SSLHTTPPeer.h | 6 + include/xrpl/server/detail/SSLWSPeer.h | 7 +- include/xrpl/server/detail/ServerImpl.h | 11 +- include/xrpl/server/detail/Spawn.h | 1 + include/xrpl/server/detail/io_list.h | 1 + include/xrpl/shamap/Family.h | 2 + include/xrpl/shamap/FullBelowCache.h | 4 + include/xrpl/shamap/SHAMap.h | 19 +- .../xrpl/shamap/SHAMapAccountStateLeafNode.h | 7 + include/xrpl/shamap/SHAMapInnerNode.h | 6 +- include/xrpl/shamap/SHAMapItem.h | 8 + include/xrpl/shamap/SHAMapLeafNode.h | 3 + include/xrpl/shamap/SHAMapMissingNode.h | 4 +- include/xrpl/shamap/SHAMapNodeID.h | 2 + include/xrpl/shamap/SHAMapSyncFilter.h | 3 + include/xrpl/shamap/SHAMapTreeNode.h | 2 +- include/xrpl/shamap/SHAMapTxLeafNode.h | 7 + .../xrpl/shamap/SHAMapTxPlusMetaLeafNode.h | 7 + include/xrpl/shamap/TreeNodeCache.h | 1 + include/xrpl/shamap/detail/TaggedPointer.h | 7 +- include/xrpl/tx/ApplyContext.h | 11 ++ include/xrpl/tx/SignerEntries.h | 8 +- include/xrpl/tx/Transactor.h | 16 ++ include/xrpl/tx/apply.h | 6 +- include/xrpl/tx/applySteps.h | 14 +- include/xrpl/tx/invariants/AMMInvariant.h | 3 + .../xrpl/tx/invariants/DirectoryInvariant.h | 2 + include/xrpl/tx/invariants/FreezeInvariant.h | 3 + include/xrpl/tx/invariants/InvariantCheck.h | 7 +- .../tx/invariants/InvariantCheckPrivilege.h | 1 + .../xrpl/tx/invariants/LoanBrokerInvariant.h | 2 + include/xrpl/tx/invariants/LoanInvariant.h | 3 + include/xrpl/tx/invariants/MPTInvariant.h | 8 + include/xrpl/tx/invariants/NFTInvariant.h | 3 +- .../tx/invariants/PermissionedDEXInvariant.h | 3 + .../invariants/PermissionedDomainInvariant.h | 3 + include/xrpl/tx/invariants/VaultInvariant.h | 5 + include/xrpl/tx/paths/AMMLiquidity.h | 10 +- include/xrpl/tx/paths/AMMOffer.h | 8 +- include/xrpl/tx/paths/BookTip.h | 7 +- include/xrpl/tx/paths/Flow.h | 7 + include/xrpl/tx/paths/Offer.h | 12 +- include/xrpl/tx/paths/OfferStream.h | 7 +- include/xrpl/tx/paths/RippleCalc.h | 5 + include/xrpl/tx/paths/detail/EitherAmount.h | 7 +- include/xrpl/tx/paths/detail/FlowDebugInfo.h | 12 +- include/xrpl/tx/paths/detail/StepChecks.h | 6 + include/xrpl/tx/paths/detail/Steps.h | 16 +- include/xrpl/tx/paths/detail/StrandFlow.h | 21 ++- .../tx/transactors/account/AccountDelete.h | 7 + .../xrpl/tx/transactors/account/AccountSet.h | 10 +- .../tx/transactors/account/SetRegularKey.h | 7 + .../tx/transactors/account/SignerListSet.h | 9 + .../xrpl/tx/transactors/bridge/XChainBridge.h | 11 +- .../xrpl/tx/transactors/check/CheckCancel.h | 7 + include/xrpl/tx/transactors/check/CheckCash.h | 7 + .../xrpl/tx/transactors/check/CheckCreate.h | 7 + .../credentials/CredentialAccept.h | 9 + .../credentials/CredentialCreate.h | 9 + .../credentials/CredentialDelete.h | 9 + .../tx/transactors/delegate/DelegateSet.h | 8 + include/xrpl/tx/transactors/dex/AMMBid.h | 7 + include/xrpl/tx/transactors/dex/AMMClawback.h | 13 ++ include/xrpl/tx/transactors/dex/AMMCreate.h | 7 + include/xrpl/tx/transactors/dex/AMMDelete.h | 7 + include/xrpl/tx/transactors/dex/AMMDeposit.h | 14 ++ include/xrpl/tx/transactors/dex/AMMVote.h | 7 + include/xrpl/tx/transactors/dex/AMMWithdraw.h | 18 +- include/xrpl/tx/transactors/dex/OfferCancel.h | 8 +- include/xrpl/tx/transactors/dex/OfferCreate.h | 20 ++ include/xrpl/tx/transactors/did/DIDDelete.h | 10 + include/xrpl/tx/transactors/did/DIDSet.h | 7 + .../xrpl/tx/transactors/escrow/EscrowCancel.h | 7 + .../xrpl/tx/transactors/escrow/EscrowCreate.h | 7 + .../xrpl/tx/transactors/escrow/EscrowFinish.h | 7 + .../lending/LoanBrokerCoverClawback.h | 7 + .../lending/LoanBrokerCoverDeposit.h | 7 + .../lending/LoanBrokerCoverWithdraw.h | 7 + .../tx/transactors/lending/LoanBrokerDelete.h | 7 + .../tx/transactors/lending/LoanBrokerSet.h | 10 + .../xrpl/tx/transactors/lending/LoanDelete.h | 7 + .../xrpl/tx/transactors/lending/LoanManage.h | 11 ++ include/xrpl/tx/transactors/lending/LoanPay.h | 9 + include/xrpl/tx/transactors/lending/LoanSet.h | 13 +- .../tx/transactors/nft/NFTokenAcceptOffer.h | 10 + include/xrpl/tx/transactors/nft/NFTokenBurn.h | 7 + .../tx/transactors/nft/NFTokenCancelOffer.h | 7 + .../tx/transactors/nft/NFTokenCreateOffer.h | 9 + include/xrpl/tx/transactors/nft/NFTokenMint.h | 12 +- .../xrpl/tx/transactors/nft/NFTokenModify.h | 7 + .../xrpl/tx/transactors/oracle/OracleDelete.h | 9 + .../xrpl/tx/transactors/oracle/OracleSet.h | 7 + .../tx/transactors/payment/DepositPreauth.h | 9 + include/xrpl/tx/transactors/payment/Payment.h | 12 ++ .../payment_channel/PaymentChannelClaim.h | 9 + .../payment_channel/PaymentChannelCreate.h | 7 + .../payment_channel/PaymentChannelFund.h | 7 + .../PermissionedDomainDelete.h | 7 + .../PermissionedDomainSet.h | 7 + include/xrpl/tx/transactors/system/Batch.h | 11 ++ include/xrpl/tx/transactors/system/Change.h | 7 + .../tx/transactors/system/LedgerStateFix.h | 9 + .../xrpl/tx/transactors/system/TicketCreate.h | 9 + include/xrpl/tx/transactors/token/Clawback.h | 7 + .../token/ConfidentialMPTClawback.h | 9 + .../token/ConfidentialMPTConvert.h | 9 + .../token/ConfidentialMPTConvertBack.h | 9 + .../token/ConfidentialMPTMergeInbox.h | 9 + .../transactors/token/ConfidentialMPTSend.h | 9 + .../tx/transactors/token/MPTokenAuthorize.h | 12 ++ .../transactors/token/MPTokenIssuanceCreate.h | 13 ++ .../token/MPTokenIssuanceDestroy.h | 7 + .../tx/transactors/token/MPTokenIssuanceSet.h | 9 + include/xrpl/tx/transactors/token/TrustSet.h | 12 +- .../xrpl/tx/transactors/vault/VaultClawback.h | 10 + .../xrpl/tx/transactors/vault/VaultCreate.h | 9 + .../xrpl/tx/transactors/vault/VaultDelete.h | 7 + .../xrpl/tx/transactors/vault/VaultDeposit.h | 7 + include/xrpl/tx/transactors/vault/VaultSet.h | 7 + .../xrpl/tx/transactors/vault/VaultWithdraw.h | 7 + src/libxrpl/core/detail/JobQueue.cpp | 1 + src/libxrpl/json/json_value.cpp | 2 + src/libxrpl/net/RegisterSSLCerts.cpp | 2 + src/libxrpl/protocol/AMMCore.cpp | 1 + src/libxrpl/protocol/ConfidentialTransfer.cpp | 1 + src/libxrpl/protocol/Permissions.cpp | 1 + src/libxrpl/protocol/STNumber.cpp | 2 +- src/libxrpl/server/State.cpp | 1 + src/libxrpl/server/Wallet.cpp | 2 + .../PermissionedDomainInvariant.cpp | 1 + .../tx/transactors/dex/OfferCreate.cpp | 1 + .../tx/transactors/nft/NFTokenCreateOffer.cpp | 1 + src/libxrpl/tx/transactors/system/Batch.cpp | 1 + src/test/app/FlowMPT_test.cpp | 1 + src/test/app/LedgerReplay_test.cpp | 1 + src/test/app/MultiSign_test.cpp | 1 + src/test/app/OfferMPT_test.cpp | 2 + src/test/app/PayStrand_test.cpp | 1 + src/test/app/SHAMapStore_test.cpp | 1 + src/test/basics/hardened_hash_test.cpp | 1 + src/test/beast/IPEndpointCommon.h | 4 + .../beast/aged_associative_container_test.cpp | 20 +- src/test/core/SociDB_test.cpp | 1 + src/test/csf.h | 19 -- src/test/csf/BasicNetwork.h | 2 + src/test/csf/CollectorRef.h | 7 + src/test/csf/Digraph.h | 5 +- src/test/csf/Histogram.h | 4 +- src/test/csf/Peer.h | 20 +- src/test/csf/PeerGroup.h | 9 + src/test/csf/Scheduler.h | 1 + src/test/csf/Sim.h | 8 +- src/test/csf/TrustGraph.h | 6 +- src/test/csf/Tx.h | 1 + src/test/csf/Validation.h | 4 +- src/test/csf/collectors.h | 10 + src/test/csf/events.h | 5 +- src/test/csf/ledgers.h | 8 +- src/test/csf/random.h | 3 + src/test/csf/submitters.h | 3 +- src/test/csf/timers.h | 1 + src/test/json/TestOutputSuite.h | 9 +- src/test/jtx.h | 60 ------ src/test/jtx/AMM.h | 23 ++- src/test/jtx/AMMTest.h | 12 ++ src/test/jtx/AbstractClient.h | 2 + src/test/jtx/Account.h | 4 +- src/test/jtx/CaptureLogs.h | 6 + src/test/jtx/CheckMessageLogs.h | 5 + src/test/jtx/ConfidentialTransfer.h | 22 --- src/test/jtx/Env.h | 27 ++- src/test/jtx/Env_ss.h | 6 + src/test/jtx/JTx.h | 3 + src/test/jtx/Oracle.h | 21 ++- src/test/jtx/PathSet.h | 16 +- src/test/jtx/TestHelpers.h | 33 +++- src/test/jtx/TestSuite.h | 4 +- src/test/jtx/TrustedPublisherServer.h | 19 ++ src/test/jtx/WSClient.h | 5 + src/test/jtx/account_txn_id.h | 3 + src/test/jtx/acctdelete.h | 4 +- src/test/jtx/amount.h | 14 +- src/test/jtx/balance.h | 4 + src/test/jtx/batch.h | 10 +- src/test/jtx/check.h | 6 +- src/test/jtx/credentials.h | 13 +- src/test/jtx/delegate.h | 6 + src/test/jtx/delivermin.h | 1 + src/test/jtx/deposit.h | 9 +- src/test/jtx/did.h | 8 +- src/test/jtx/directory.h | 6 +- src/test/jtx/domain.h | 3 + src/test/jtx/envconfig.h | 5 + src/test/jtx/escrow.h | 11 +- src/test/jtx/fee.h | 3 + src/test/jtx/flags.h | 4 + src/test/jtx/impl/Oracle.cpp | 1 + src/test/jtx/invoice_id.h | 3 + src/test/jtx/jtx_json.h | 3 + src/test/jtx/last_ledger_sequence.h | 3 + src/test/jtx/ledgerStateFix.h | 4 +- src/test/jtx/memo.h | 2 + src/test/jtx/mpt.h | 20 ++ src/test/jtx/multisign.h | 10 +- src/test/jtx/noop.h | 3 + src/test/jtx/offer.h | 2 + src/test/jtx/owners.h | 3 +- src/test/jtx/paths.h | 7 +- src/test/jtx/pay.h | 1 + src/test/jtx/permissioned_dex.h | 5 + src/test/jtx/permissioned_domains.h | 12 ++ src/test/jtx/prop.h | 2 + src/test/jtx/quality.h | 3 + src/test/jtx/require.h | 1 + src/test/jtx/rpc.h | 7 +- src/test/jtx/sendmax.h | 1 + src/test/jtx/seq.h | 2 + src/test/jtx/sig.h | 5 + src/test/jtx/tag.h | 3 + src/test/jtx/ter.h | 4 + src/test/jtx/ticket.h | 4 + src/test/jtx/token.h | 8 +- src/test/jtx/trust.h | 3 + src/test/jtx/txflags.h | 3 + src/test/jtx/utility.h | 4 +- src/test/jtx/vault.h | 2 +- src/test/jtx/xchain_bridge.h | 9 +- src/test/nodestore/TestBase.h | 11 +- src/test/protocol/STObject_test.cpp | 1 + src/test/protocol/STParsedJSON_test.cpp | 1 + src/test/rpc/GRPCTestClientBase.h | 32 ---- src/test/shamap/common.h | 13 ++ src/test/unit_test/FileDirGuard.h | 8 +- src/test/unit_test/SuiteJournal.h | 7 +- src/test/unit_test/multi_runner.h | 9 +- src/tests/libxrpl/CMakeLists.txt | 7 + src/tests/libxrpl/helpers/IOU.h | 2 +- src/tests/libxrpl/helpers/TestFamily.h | 9 + .../libxrpl/helpers/TestServiceRegistry.h | 5 + src/tests/libxrpl/helpers/TestSink.h | 2 + src/tests/libxrpl/helpers/TxTest.h | 10 +- .../libxrpl/protocol_autogen/TestHelpers.h | 1 + .../app/consensus/RCLCensorshipDetector.h | 2 +- src/xrpld/app/consensus/RCLConsensus.h | 20 +- src/xrpld/app/consensus/RCLCxLedger.h | 8 +- src/xrpld/app/consensus/RCLCxPeerPos.h | 3 + src/xrpld/app/consensus/RCLCxTx.h | 8 + src/xrpld/app/consensus/RCLValidations.h | 11 ++ src/xrpld/app/ledger/AcceptedLedger.h | 5 + src/xrpld/app/ledger/AccountStateSF.h | 6 + src/xrpld/app/ledger/BuildLedger.h | 4 + src/xrpld/app/ledger/ConsensusTransSetSF.cpp | 1 + src/xrpld/app/ledger/ConsensusTransSetSF.h | 7 + src/xrpld/app/ledger/InboundLedger.h | 17 ++ src/xrpld/app/ledger/InboundLedgers.h | 14 ++ src/xrpld/app/ledger/InboundTransactions.h | 7 + src/xrpld/app/ledger/LedgerCleaner.h | 2 + src/xrpld/app/ledger/LedgerHistory.h | 7 + src/xrpld/app/ledger/LedgerHolder.h | 3 + src/xrpld/app/ledger/LedgerMaster.h | 21 ++- src/xrpld/app/ledger/LedgerPersistence.h | 4 + src/xrpld/app/ledger/LedgerReplayTask.h | 5 + src/xrpld/app/ledger/LedgerReplayer.h | 11 +- src/xrpld/app/ledger/LedgerToJson.h | 8 +- src/xrpld/app/ledger/LocalTxs.h | 3 + src/xrpld/app/ledger/OpenLedger.h | 10 +- src/xrpld/app/ledger/OrderBookDBImpl.h | 11 ++ src/xrpld/app/ledger/TransactionMaster.h | 9 + src/xrpld/app/ledger/TransactionStateSF.h | 6 + .../app/ledger/detail/LedgerDeltaAcquire.cpp | 2 + .../app/ledger/detail/LedgerDeltaAcquire.h | 10 + src/xrpld/app/ledger/detail/LedgerMaster.cpp | 1 + .../ledger/detail/LedgerReplayMsgHandler.h | 5 +- .../app/ledger/detail/LedgerReplayTask.cpp | 1 + .../app/ledger/detail/SkipListAcquire.cpp | 2 + src/xrpld/app/ledger/detail/SkipListAcquire.h | 12 +- src/xrpld/app/ledger/detail/TimeoutCounter.h | 6 + .../app/ledger/detail/TransactionAcquire.h | 11 ++ src/xrpld/app/main/Application.cpp | 1 + src/xrpld/app/main/Application.h | 12 +- src/xrpld/app/main/BasicApp.h | 1 + src/xrpld/app/main/CollectorManager.h | 7 +- src/xrpld/app/main/GRPCServer.cpp | 1 + src/xrpld/app/main/GRPCServer.h | 15 +- src/xrpld/app/main/LoadManager.h | 2 +- src/xrpld/app/main/NodeIdentity.h | 2 + src/xrpld/app/main/NodeStoreScheduler.h | 1 + src/xrpld/app/main/Tuning.h | 1 + src/xrpld/app/misc/AmendmentTableImpl.h | 6 +- src/xrpld/app/misc/FeeVote.h | 6 + src/xrpld/app/misc/FeeVoteImpl.cpp | 1 + src/xrpld/app/misc/NegativeUNLVote.h | 7 + src/xrpld/app/misc/SHAMapStore.h | 7 +- src/xrpld/app/misc/SHAMapStoreImp.cpp | 1 + src/xrpld/app/misc/SHAMapStoreImp.h | 16 ++ src/xrpld/app/misc/Transaction.h | 9 + src/xrpld/app/misc/TxQ.h | 18 ++ src/xrpld/app/misc/ValidatorKeys.h | 2 + src/xrpld/app/misc/ValidatorList.h | 17 +- src/xrpld/app/misc/ValidatorSite.h | 9 +- src/xrpld/app/misc/detail/AccountTxPaging.h | 3 + src/xrpld/app/misc/detail/ValidatorSite.cpp | 1 - src/xrpld/app/misc/detail/WorkBase.h | 4 +- src/xrpld/app/misc/detail/WorkFile.h | 3 + src/xrpld/app/misc/detail/WorkPlain.h | 3 + src/xrpld/app/misc/detail/WorkSSL.h | 5 +- src/xrpld/app/misc/make_NetworkOPs.h | 3 +- src/xrpld/app/rdb/PeerFinder.h | 7 +- src/xrpld/app/rdb/backend/SQLiteDatabase.h | 12 ++ src/xrpld/app/rdb/backend/detail/Node.cpp | 2 + src/xrpld/app/rdb/backend/detail/Node.h | 24 +++ src/xrpld/app/rdb/detail/PeerFinder.cpp | 1 + src/xrpld/consensus/Consensus.h | 11 +- src/xrpld/consensus/ConsensusParms.h | 2 +- src/xrpld/consensus/ConsensusProposal.h | 1 + src/xrpld/consensus/ConsensusTypes.h | 4 + src/xrpld/consensus/DisputedTx.h | 5 + src/xrpld/consensus/LedgerTrie.h | 4 + src/xrpld/consensus/Validations.h | 11 +- src/xrpld/core/Config.h | 4 + src/xrpld/core/TimeKeeper.h | 1 + src/xrpld/overlay/Cluster.h | 5 + src/xrpld/overlay/Compression.h | 4 + src/xrpld/overlay/Message.h | 11 +- src/xrpld/overlay/Overlay.h | 11 ++ src/xrpld/overlay/Peer.h | 6 + src/xrpld/overlay/PeerSet.h | 9 + src/xrpld/overlay/ReduceRelayCommon.h | 2 + src/xrpld/overlay/Slot.h | 18 +- src/xrpld/overlay/Squelch.h | 1 + src/xrpld/overlay/detail/ConnectAttempt.h | 12 ++ src/xrpld/overlay/detail/Handshake.h | 6 +- src/xrpld/overlay/detail/OverlayImpl.h | 22 ++- src/xrpld/overlay/detail/PeerImp.h | 31 ++++ src/xrpld/overlay/detail/ProtocolMessage.h | 9 +- src/xrpld/overlay/detail/TrafficCount.h | 8 +- src/xrpld/overlay/detail/TxMetrics.h | 4 +- src/xrpld/overlay/detail/ZeroCopyStream.h | 11 +- src/xrpld/overlay/make_Overlay.h | 7 + src/xrpld/overlay/predicates.h | 1 + src/xrpld/peerfinder/PeerfinderManager.h | 9 + src/xrpld/peerfinder/Slot.h | 2 + src/xrpld/peerfinder/detail/Bootcache.h | 1 + src/xrpld/peerfinder/detail/Checker.h | 2 + src/xrpld/peerfinder/detail/Counts.h | 7 +- src/xrpld/peerfinder/detail/Fixed.h | 5 + src/xrpld/peerfinder/detail/Handouts.h | 5 + src/xrpld/peerfinder/detail/Livecache.h | 14 ++ src/xrpld/peerfinder/detail/Logic.h | 16 ++ src/xrpld/peerfinder/detail/SlotImp.h | 5 + src/xrpld/peerfinder/detail/Source.h | 4 + src/xrpld/peerfinder/detail/SourceStrings.h | 2 + src/xrpld/peerfinder/detail/Store.h | 6 + src/xrpld/peerfinder/detail/StoreSqdb.h | 9 + src/xrpld/peerfinder/detail/Tuning.h | 3 + src/xrpld/peerfinder/detail/iosformat.h | 3 + src/xrpld/peerfinder/make_Manager.h | 4 + src/xrpld/perflog/detail/PerfLogImp.cpp | 3 + src/xrpld/perflog/detail/PerfLogImp.h | 8 +- src/xrpld/rpc/BookChanges.h | 12 ++ src/xrpld/rpc/CTID.h | 7 +- src/xrpld/rpc/Context.h | 6 + src/xrpld/rpc/DeliveredAmount.h | 2 +- src/xrpld/rpc/GRPCHandlers.h | 8 +- src/xrpld/rpc/MPTokenIssuanceID.h | 2 +- src/xrpld/rpc/Output.h | 9 +- src/xrpld/rpc/RPCCall.h | 5 +- src/xrpld/rpc/RPCHandler.h | 5 + src/xrpld/rpc/RPCSub.h | 3 + src/xrpld/rpc/Role.h | 4 +- src/xrpld/rpc/ServerHandler.h | 17 +- src/xrpld/rpc/Status.h | 7 + src/xrpld/rpc/detail/AccountAssets.h | 6 +- src/xrpld/rpc/detail/AssetCache.h | 6 +- src/xrpld/rpc/detail/Handler.h | 10 +- src/xrpld/rpc/detail/MPT.h | 2 +- src/xrpld/rpc/detail/PathRequest.h | 16 +- src/xrpld/rpc/detail/PathRequestManager.h | 10 + src/xrpld/rpc/detail/Pathfinder.h | 16 +- src/xrpld/rpc/detail/PathfinderUtils.h | 4 + src/xrpld/rpc/detail/RPCHandler.cpp | 1 + src/xrpld/rpc/detail/RPCHelpers.h | 15 +- src/xrpld/rpc/detail/RPCLedgerHelpers.cpp | 4 + src/xrpld/rpc/detail/RPCLedgerHelpers.h | 9 +- src/xrpld/rpc/detail/TransactionSign.h | 5 + src/xrpld/rpc/detail/TrustLine.h | 7 +- src/xrpld/rpc/detail/Tuning.h | 2 + src/xrpld/rpc/detail/WSInfoSub.h | 3 + src/xrpld/rpc/handlers/Handlers.h | 2 + .../rpc/handlers/account/AccountNFTs.cpp | 1 + .../admin/peer/PeerReservationsDel.cpp | 1 + .../admin/peer/PeerReservationsList.cpp | 1 + .../rpc/handlers/admin/status/GetCounts.h | 2 + src/xrpld/rpc/handlers/ledger/Ledger.h | 8 +- .../rpc/handlers/ledger/LedgerEntryHelpers.h | 17 +- .../rpc/handlers/orderbook/BookChanges.cpp | 1 + .../handlers/orderbook/GetAggregatePrice.cpp | 1 + .../rpc/handlers/orderbook/NFTOffersHelpers.h | 12 +- src/xrpld/rpc/handlers/server_info/Version.h | 7 + src/xrpld/rpc/json_body.h | 5 + src/xrpld/shamap/NodeFamily.cpp | 1 + src/xrpld/shamap/NodeFamily.h | 9 + 662 files changed, 3870 insertions(+), 743 deletions(-) create mode 100644 cmake/verify_headers.cmake delete mode 100644 include/xrpl/beast/insight/Insight.h delete mode 100644 include/xrpl/protocol/st.h delete mode 100644 include/xrpl/resource/Types.h delete mode 100644 src/test/csf.h delete mode 100644 src/test/jtx.h delete mode 100644 src/test/rpc/GRPCTestClientBase.h diff --git a/.clang-tidy b/.clang-tidy index e12c73cc56..84849db7a0 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -147,7 +147,7 @@ CheckOptions: bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc - misc-include-cleaner.IgnoreHeaders: ".*/(detail|impl)/.*;.*fwd\\.h(pp)?;time.h;stdlib.h;sqlite3.h;netinet/in\\.h;sys/resource\\.h;sys/sysinfo\\.h;linux/sysinfo\\.h;__chrono/.*;bits/.*;_abort\\.h;boost/uuid/uuid_hash.hpp;boost/beast/core/flat_buffer\\.hpp;boost/beast/http/field\\.hpp;boost/beast/http/dynamic_body\\.hpp;boost/beast/http/message\\.hpp;boost/beast/http/read\\.hpp;boost/beast/http/write\\.hpp;openssl/obj_mac\\.h" + misc-include-cleaner.IgnoreHeaders: ".*/(detail|impl)/.*;.*fwd\\.h(pp)?;time.h;stdlib.h;sqlite3.h;netinet/in\\.h;sys/resource\\.h;sys/sysinfo\\.h;linux/sysinfo\\.h;__chrono/.*;bits/.*;_abort\\.h;boost/.*;openssl/obj_mac\\.h" readability-braces-around-statements.ShortStatementLines: 2 readability-identifier-naming.MacroDefinitionCase: UPPER_CASE diff --git a/.gersemi/definitions.cmake b/.gersemi/definitions.cmake index 58bc74c70a..aa63076c8b 100644 --- a/.gersemi/definitions.cmake +++ b/.gersemi/definitions.cmake @@ -48,6 +48,12 @@ endfunction() function(add_module parent name) endfunction() +function(verify_target_headers target headers_dir) +endfunction() + +function(_verify_add_headers target dir) +endfunction() + function(setup_protocol_autogen) endfunction() diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index fb449441e3..cf70468e32 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -1,9 +1,3 @@ -Loop: test.jtx test.toplevel - test.toplevel > test.jtx - -Loop: test.jtx test.unit_test - test.unit_test ~= test.jtx - Loop: xrpld.app xrpld.overlay xrpld.app > xrpld.overlay diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 7b31042158..5cc49dd2f5 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -105,9 +105,9 @@ test.csf > xrpl.basics test.csf > xrpld.consensus test.csf > xrpl.json test.csf > xrpl.ledger -test.csf > xrpl.protocol test.json > test.jtx test.json > xrpl.json +test.jtx > test.unit_test test.jtx > xrpl.basics test.jtx > xrpl.config test.jtx > xrpl.core @@ -194,8 +194,6 @@ test.shamap > xrpl.config test.shamap > xrpl.nodestore test.shamap > xrpl.protocol test.shamap > xrpl.shamap -test.toplevel > test.csf -test.toplevel > xrpl.json test.unit_test > xrpl.basics test.unit_test > xrpl.protocol tests.libxrpl > xrpl.basics @@ -218,11 +216,14 @@ xrpl.core > xrpl.json xrpl.core > xrpl.protocol xrpl.json > xrpl.basics xrpl.ledger > xrpl.basics +xrpl.ledger > xrpl.json +xrpl.ledger > xrpl.nodestore xrpl.ledger > xrpl.protocol xrpl.ledger > xrpl.shamap xrpl.net > xrpl.basics xrpl.nodestore > xrpl.basics xrpl.nodestore > xrpl.config +xrpl.nodestore > xrpl.json xrpl.nodestore > xrpl.protocol xrpl.protocol > xrpl.basics xrpl.protocol > xrpl.json @@ -240,7 +241,6 @@ xrpl.server > xrpl.json xrpl.server > xrpl.protocol xrpl.server > xrpl.rdb xrpl.server > xrpl.resource -xrpl.server > xrpl.shamap xrpl.shamap > xrpl.basics xrpl.shamap > xrpl.nodestore xrpl.shamap > xrpl.protocol @@ -295,8 +295,10 @@ xrpld.peerfinder > xrpl.rdb xrpld.perflog > xrpl.basics xrpld.perflog > xrpl.config xrpld.perflog > xrpl.core +xrpld.perflog > xrpld.app xrpld.perflog > xrpld.rpc xrpld.perflog > xrpl.json +xrpld.perflog > xrpl.nodestore xrpld.perflog > xrpl.protocol xrpld.rpc > xrpl.basics xrpld.rpc > xrpl.config @@ -314,5 +316,6 @@ xrpld.rpc > xrpl.shamap xrpld.rpc > xrpl.tx xrpld.shamap > xrpl.basics xrpld.shamap > xrpld.core +xrpld.shamap > xrpl.nodestore xrpld.shamap > xrpl.protocol xrpld.shamap > xrpl.shamap diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 5528f3452e..a6bbe669ce 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -79,6 +79,7 @@ jobs: -Dtests=ON \ -Dwerr=ON \ -Dxrpld=ON \ + -Dverify_headers=ON \ .. # clang-tidy needs headers generated from proto files @@ -91,7 +92,7 @@ jobs: id: run_clang_tidy continue-on-error: true env: - TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'src tests' }} + TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'include src tests' }} run: | set -o pipefail run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bac4d1140..2e4521870d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,8 +28,10 @@ repos: entry: ./bin/pre-commit/clang_tidy_check.py language: python types_or: [c++, c] - exclude: ^include/xrpl/protocol_autogen - pass_filenames: false # script determines the staged files itself + # .ipp fragments are included by their owning header rather than compiled + # as standalone translation units, so they have no compile_commands.json + # entry to lint (verify_headers checks them transitively). + exclude: '^include/xrpl/protocol_autogen|\.ipp$' - id: fix-include-style name: fix include style entry: ./bin/pre-commit/fix_include_style.py diff --git a/BUILD.md b/BUILD.md index 847cd7bc1a..6ccdde12d5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -317,21 +317,41 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. ## Options -| Option | Default Value | Description | -| ---------- | ------------- | -------------------------------------------------------------- | -| `assert` | OFF | Force enabling assertions. | -| `coverage` | OFF | Prepare the coverage report. | -| `tests` | OFF | Build tests. | -| `unity` | OFF | Configure a unity build. | -| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. | -| `werr` | OFF | Treat compilation warnings as errors | -| `wextra` | OFF | Enable additional compilation warnings | +| Option | Default Value | Description | +| ---------------- | ------------- | ----------------------------------------------------------------------------- | +| `assert` | OFF | Force enabling assertions. | +| `coverage` | OFF | Prepare the coverage report. | +| `tests` | OFF | Build tests. | +| `unity` | OFF | Configure a unity build. | +| `verify_headers` | ON | Make the `verify-headers` target available to compile each header on its own. | +| `xrpld` | OFF | Build the xrpld application, and not just the libxrpl library. | +| `werr` | OFF | Treat compilation warnings as errors | +| `wextra` | OFF | Enable additional compilation warnings | [Unity builds][unity-build] may be faster for the first build (at the cost of much more memory) since they concatenate sources into fewer translation units. Non-unity builds may be faster for incremental builds, and can be helpful for detecting `#include` omissions. +### Verifying headers + +The regular build only compiles `.cpp` files, so a header is only ever checked +through whatever translation unit happens to include it. A header that forgets +an `#include` is not caught as long as every `.cpp` that uses it includes its +missing dependency first. The `verify_headers` option (ON by default) adds a +`verify-headers` target that compiles every header on its own, which fails if a +header is not self-contained: + +```bash +cmake --build . --target verify-headers +``` + +The per-header objects are excluded from the `all` target, so a normal build +never compiles them; they are built only through `verify-headers`. The generated +translation units do appear in `compile_commands.json`, so clang-tidy (and +clangd and IDEs) can lint each header on its own. Pass `-Dverify_headers=OFF` to +omit them entirely. + ## Troubleshooting ### Conan diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py index f134660671..d074c56acf 100755 --- a/bin/pre-commit/clang_tidy_check.py +++ b/bin/pre-commit/clang_tidy_check.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 -"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy.""" +"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy. + +The set of files is chosen by pre-commit (see .pre-commit-config.yaml), which +filters to C/C++ sources and excludes `.ipp` fragments. Headers are linted +directly: the `verify_headers` build option (ON by default) compiles every +`.h`/`.hpp` on its own, so each header is the main file of its own +compile_commands.json entry and run-clang-tidy can analyse it just like a +`.cpp`. +""" from __future__ import annotations -import json import os -import re import shutil import subprocess import sys -from collections import defaultdict from pathlib import Path -HEADER_EXTENSIONS = {".h", ".hpp", ".ipp"} -SOURCE_EXTENSIONS = {".cpp"} -INCLUDE_RE = re.compile(r"^\s*#\s*include\s*[<\"]([^>\"]+)[>\"]") - def find_run_clang_tidy() -> str | None: for candidate in ("run-clang-tidy-21", "run-clang-tidy"): @@ -32,150 +33,11 @@ def find_build_dir(repo_root: Path) -> Path | None: return None -def build_include_graph(build_dir: Path, repo_root: Path) -> tuple[dict, set]: - """ - Scan all files reachable from compile_commands.json and build an inverted include graph. - - Returns: - inverted: header_path -> set of files that include it - source_files: set of all TU paths from compile_commands.json - """ - with open(build_dir / "compile_commands.json") as f: - db = json.load(f) - - source_files = {Path(e["file"]).resolve() for e in db} - include_roots = [repo_root / "include", repo_root / "src"] - inverted: dict[Path, set[Path]] = defaultdict(set) - - to_scan: set[Path] = set(source_files) - scanned: set[Path] = set() - - while to_scan: - file = to_scan.pop() - if file in scanned or not file.exists(): - continue - scanned.add(file) - - content = file.read_text() - - for line in content.splitlines(): - m = INCLUDE_RE.match(line) - if not m: - continue - for root in include_roots: - candidate = (root / m.group(1)).resolve() - if candidate.exists(): - inverted[candidate].add(file) - if candidate not in scanned: - to_scan.add(candidate) - break - - return inverted, source_files - - -def find_tus_for_headers( - headers: list[Path], - inverted: dict[Path, set[Path]], - source_files: set[Path], -) -> set[Path]: - """ - For each header, pick one TU that transitively includes it. - Prefers a TU whose stem matches the header's stem, otherwise picks the first found. - """ - result: set[Path] = set() - - for header in headers: - preferred: Path | None = None - visited: set[Path] = {header} - stack: list[Path] = [header] - - while stack: - h = stack.pop() - for inc in inverted.get(h, ()): - if inc in source_files: - if inc.stem == header.stem: - preferred = inc - break - if preferred is None: - preferred = inc - if inc not in visited: - visited.add(inc) - stack.append(inc) - if preferred is not None and preferred.stem == header.stem: - break - - if preferred is not None: - result.add(preferred) - - return result - - -def resolve_files( - input_files: list[str], build_dir: Path, repo_root: Path -) -> list[str]: - """ - Split input into source files and headers. Source files are passed through; - headers are resolved to the TUs that transitively include them. - """ - sources: list[Path] = [] - headers: list[Path] = [] - - for f in input_files: - p = Path(f).resolve() - if p.suffix in SOURCE_EXTENSIONS: - sources.append(p) - elif p.suffix in HEADER_EXTENSIONS: - headers.append(p) - - if not headers: - return [str(p) for p in sources] - - print( - f"Resolving {len(headers)} header(s) to compilation units...", file=sys.stderr - ) - inverted, source_files = build_include_graph(build_dir, repo_root) - tus = find_tus_for_headers(headers, inverted, source_files) - - if not tus: - print( - "Warning: no compilation units found that include the modified headers; " - "skipping clang-tidy for headers.", - file=sys.stderr, - ) - - return sorted({str(p) for p in (*sources, *tus)}) - - -def staged_files(repo_root: Path) -> list[str]: - result = subprocess.run( - ["git", "diff", "--staged", "--name-only", "--diff-filter=d"], - capture_output=True, - text=True, - cwd=repo_root, - ) - if result.returncode != 0: - print( - "clang-tidy check failed: 'git diff --staged' command failed.", - file=sys.stderr, - ) - if result.stderr: - print(result.stderr, file=sys.stderr) - sys.exit(result.returncode or 1) - return [str(repo_root / p) for p in result.stdout.splitlines() if p] - - def main(): if not os.environ.get("TIDY"): return 0 - repo_root = Path( - subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], - cwd=Path(__file__).parent, - text=True, - ).strip() - ) - files = staged_files(repo_root) + files = sys.argv[1:] if not files: return 0 @@ -188,6 +50,13 @@ def main(): ) return 1 + repo_root = Path( + subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + cwd=Path(__file__).parent, + text=True, + ).strip() + ) build_dir = find_build_dir(repo_root) if not build_dir: print( @@ -197,13 +66,9 @@ def main(): ) return 1 - tidy_files = resolve_files(files, build_dir, repo_root) - if not tidy_files: - return 0 - result = subprocess.run( [run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"] - + tidy_files + + files ) return result.returncode diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index 4d4a800d9a..3e49267715 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -293,4 +293,13 @@ if(xrpld) PRIVATE ${CMAKE_SOURCE_DIR}/external/antithesis-sdk ) endif() + + # The xrpld headers are not built with add_module, so verify them against + # the executable's own compile environment. + if(verify_headers) + verify_target_headers(xrpld "${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld") + if(tests) + verify_target_headers(xrpld "${CMAKE_CURRENT_SOURCE_DIR}/src/test") + endif() + endif() endif() diff --git a/cmake/XrplSettings.cmake b/cmake/XrplSettings.cmake index 44a727a994..757a596096 100644 --- a/cmake/XrplSettings.cmake +++ b/cmake/XrplSettings.cmake @@ -30,6 +30,23 @@ if(tests) endif() endif() +# Enabled by default so every header is compiled on its own as the main file of +# its own compile_commands.json entry - this is what lets clang-tidy (and clangd +# and IDEs) analyse a header's own includes directly. The per-header objects are +# EXCLUDE_FROM_ALL (see cmake/verify_headers.cmake) and the aggregate target +# below is not part of `all`, so a normal `cmake --build` never compiles them. +option( + verify_headers + "Compile every header on its own to verify it is self-contained." + ON +) +if(verify_headers) + # Aggregate target that builds every per-module header-verification library + # created by add_module (see cmake/verify_headers.cmake). Build it with: + # cmake --build . --target verify-headers + add_custom_target(verify-headers) +endif() + option(unity "Creates a build using UNITY support in cmake." OFF) if(unity) if(NOT is_ci) diff --git a/cmake/add_module.cmake b/cmake/add_module.cmake index 316d6c627b..b72d1077bb 100644 --- a/cmake/add_module.cmake +++ b/cmake/add_module.cmake @@ -1,4 +1,5 @@ include(isolate_headers) +include(verify_headers) # Create an OBJECT library target named # @@ -37,4 +38,20 @@ function(add_module parent name) "${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}" PRIVATE ) + # protocol_autogen contains generated headers that are deliberately exempt + # from clang-tidy (see ExcludeHeaderFilterRegex in .clang-tidy), so we do not + # verify them either. + if( + verify_headers + AND NOT "${parent}/${name}" STREQUAL "xrpl/protocol_autogen" + ) + verify_target_headers( + ${target} + "${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}" + ) + verify_target_headers( + ${target} + "${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}" + ) + endif() endfunction() diff --git a/cmake/verify_headers.cmake b/cmake/verify_headers.cmake new file mode 100644 index 0000000000..2c36869441 --- /dev/null +++ b/cmake/verify_headers.cmake @@ -0,0 +1,84 @@ +# Our normal build only ever compiles `.cpp` files, so a header is only ever +# checked through whatever translation unit happens to include it. A header that +# is missing an `#include` is never caught as long as every `.cpp` that uses it +# includes its missing dependency first. To check a header on its own we compile +# it directly as a translation unit. +# +# Compiling the header itself - rather than a `.cpp` wrapper that includes it - +# gives two checks at once: +# * the compiler fails if the header is not self-contained, i.e. it uses a +# declaration that is not available (directly or transitively); and +# * the header is the *main file* of its `compile_commands.json` entry, so +# clang-tidy's misc-include-cleaner analyses (and can --fix) the header's own +# includes - flagging a dependency that is only available transitively, which +# a plain compile cannot catch. A wrapper would be the main file instead, and +# include-cleaner never looks inside the headers a main file includes. +# +# The objects are never linked anywhere; we build them only for these checks. + +# Verify that the headers under headers_dir compile on their own, using the +# compile environment of an existing target so each header is compiled exactly as +# that target compiles it. This works for both add_module libraries and the xrpld +# and test binaries: a library's isolated public and private include directories +# and a binary's `-I src` both live in its INCLUDE_DIRECTORIES, and the modules or +# libraries it links live in its LINK_LIBRARIES. We copy those usage requirements +# through generator expressions (rather than linking ${target}, which is +# impossible for an executable), evaluated at generation time so they capture +# requirements the caller adds after this runs. The verify library is created +# once; call this repeatedly to add more header directories. +# +# verify_target_headers(target headers_dir) +function(verify_target_headers target headers_dir) + set(verify ${target}.verify) + if(NOT TARGET ${verify}) + add_library(${verify} OBJECT EXCLUDE_FROM_ALL) + # A unity build would concatenate the headers into a single translation + # unit, where a header missing an include could be satisfied by one that + # precedes it in the blob - exactly the bug we want to catch. + set_target_properties(${verify} PROPERTIES UNITY_BUILD OFF) + target_include_directories( + ${verify} + PRIVATE $ + ) + target_compile_definitions( + ${verify} + PRIVATE $ + ) + target_compile_options( + ${verify} + PRIVATE $ + ) + target_link_libraries( + ${verify} + PRIVATE $ + ) + add_dependencies(verify-headers ${verify}) + endif() + _verify_add_headers(${verify} "${headers_dir}") +endfunction() + +# Add every .h/.hpp under dir to target as a directly-compiled C++ translation +# unit. .ipp files are inline-implementation fragments included by their owning +# header (often after a class declaration), so they are not self-contained on +# their own and are verified transitively when that header is verified. +function(_verify_add_headers target dir) + file(GLOB_RECURSE headers CONFIGURE_DEPENDS "${dir}/*.h" "${dir}/*.hpp") + if(NOT headers) + return() + endif() + # `-xc++` forces the header to be compiled as a C++ translation unit; a lone + # `.h` is otherwise treated as a header to precompile. `#pragma once` is + # harmless (and warns) when the header is the main file, so silence it. + # Compiled on its own, a header legitimately defines constants and static or + # template functions that nothing in this single translation unit uses (they + # exist for the files that include it), so the resulting unused-entity + # warnings are expected and must not fail the build under -Werror. + set_source_files_properties( + ${headers} + PROPERTIES + LANGUAGE CXX + COMPILE_OPTIONS + "-xc++;-Wno-pragma-once-outside-header;-Wno-unused-const-variable;-Wno-unused-function" + ) + target_sources(${target} PRIVATE ${headers}) +endfunction() diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 59968a4fa4..c0ae8ef56e 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/CompressionAlgorithms.h b/include/xrpl/basics/CompressionAlgorithms.h index e24c490337..a5ec8645b6 100644 --- a/include/xrpl/basics/CompressionAlgorithms.h +++ b/include/xrpl/basics/CompressionAlgorithms.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/basics/DecayingSample.h b/include/xrpl/basics/DecayingSample.h index 910c8f9e14..86a8baa62e 100644 --- a/include/xrpl/basics/DecayingSample.h +++ b/include/xrpl/basics/DecayingSample.h @@ -2,6 +2,7 @@ #include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/FileUtilities.h b/include/xrpl/basics/FileUtilities.h index 8cf7e4893f..c7a427b8a9 100644 --- a/include/xrpl/basics/FileUtilities.h +++ b/include/xrpl/basics/FileUtilities.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h index d66c340d3f..c23d6afb85 100644 --- a/include/xrpl/basics/IntrusivePointer.h +++ b/include/xrpl/basics/IntrusivePointer.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/include/xrpl/basics/IntrusiveRefCounts.h b/include/xrpl/basics/IntrusiveRefCounts.h index 0b00f1d5b1..5eb1422541 100644 --- a/include/xrpl/basics/IntrusiveRefCounts.h +++ b/include/xrpl/basics/IntrusiveRefCounts.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h index 0699cdd3d9..4e3437fe71 100644 --- a/include/xrpl/basics/Log.h +++ b/include/xrpl/basics/Log.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -11,7 +10,9 @@ #include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index cee0c45355..073da12f89 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -2,7 +2,9 @@ #include +#include #include +#include #include #include #include @@ -11,7 +13,9 @@ #include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/RangeSet.h b/include/xrpl/basics/RangeSet.h index e1cee8b6c4..2ed543b376 100644 --- a/include/xrpl/basics/RangeSet.h +++ b/include/xrpl/basics/RangeSet.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/basics/Resolver.h b/include/xrpl/basics/Resolver.h index 3b6a950247..d48958b76d 100644 --- a/include/xrpl/basics/Resolver.h +++ b/include/xrpl/basics/Resolver.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/basics/ResolverAsio.h b/include/xrpl/basics/ResolverAsio.h index 2764777327..0b78b9747c 100644 --- a/include/xrpl/basics/ResolverAsio.h +++ b/include/xrpl/basics/ResolverAsio.h @@ -5,6 +5,8 @@ #include +#include + namespace xrpl { class ResolverAsio : public Resolver diff --git a/include/xrpl/basics/SHAMapHash.h b/include/xrpl/basics/SHAMapHash.h index 76d9d4fa3d..3c3d525022 100644 --- a/include/xrpl/basics/SHAMapHash.h +++ b/include/xrpl/basics/SHAMapHash.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/SharedWeakCachePointer.h b/include/xrpl/basics/SharedWeakCachePointer.h index c2c3239eea..a143647a1e 100644 --- a/include/xrpl/basics/SharedWeakCachePointer.h +++ b/include/xrpl/basics/SharedWeakCachePointer.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include diff --git a/include/xrpl/basics/SlabAllocator.h b/include/xrpl/basics/SlabAllocator.h index 0172b1ade2..8e741991f6 100644 --- a/include/xrpl/basics/SlabAllocator.h +++ b/include/xrpl/basics/SlabAllocator.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #if BOOST_OS_LINUX diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 1d3434b7ed..97df43d68f 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -1,18 +1,19 @@ #pragma once #include -#include #include #include #include #include +#include #include #include #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index 973fcd828a..71ebfc57a4 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -1,17 +1,24 @@ #pragma once -#include -#include -#include +#include +#include // IWYU pragma: keep #include #include #include -#include +#include +#include +#include +#include +#include #include +#include #include +#include #include +#include #include +#include #include #include #include diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index 7e812ce4c7..ffcd533216 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -1,6 +1,7 @@ #pragma once #include +#include // IWYU pragma: keep #include namespace xrpl { diff --git a/include/xrpl/basics/UnorderedContainers.h b/include/xrpl/basics/UnorderedContainers.h index 5a417d5045..e0700c4055 100644 --- a/include/xrpl/basics/UnorderedContainers.h +++ b/include/xrpl/basics/UnorderedContainers.h @@ -2,12 +2,14 @@ #include #include -#include #include #include +#include +#include #include #include +#include /** * Use hash_* containers for keys that do not need a cryptographically secure diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index 660958ce14..24fd660e65 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -34,6 +34,7 @@ #pragma once +#include #include #include #include diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 93520ff699..e6ca1993f9 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -18,8 +19,17 @@ #include #include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/basics/chrono.h b/include/xrpl/basics/chrono.h index 5d6de06248..61246fc699 100644 --- a/include/xrpl/basics/chrono.h +++ b/include/xrpl/basics/chrono.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/hardened_hash.h b/include/xrpl/basics/hardened_hash.h index b8ea1e0f3f..5a855736b3 100644 --- a/include/xrpl/basics/hardened_hash.h +++ b/include/xrpl/basics/hardened_hash.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/include/xrpl/basics/join.h b/include/xrpl/basics/join.h index c214212473..492e4f3122 100644 --- a/include/xrpl/basics/join.h +++ b/include/xrpl/basics/join.h @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/basics/make_SSLContext.h b/include/xrpl/basics/make_SSLContext.h index 46f6a15e84..45ac637c36 100644 --- a/include/xrpl/basics/make_SSLContext.h +++ b/include/xrpl/basics/make_SSLContext.h @@ -2,6 +2,7 @@ #include +#include #include namespace xrpl { diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index c51cedf2dd..c2750a5769 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -3,7 +3,10 @@ #include #include +#include #include +#include +#include #include #include #include diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index 0b298e12d9..cceaa6f029 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -3,7 +3,6 @@ #include #include -#include #include #include #include diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h index f71edc47ad..714146e089 100644 --- a/include/xrpl/basics/safe_cast.h +++ b/include/xrpl/basics/safe_cast.h @@ -1,7 +1,5 @@ #pragma once -#include - #include namespace xrpl { diff --git a/include/xrpl/basics/strHex.h b/include/xrpl/basics/strHex.h index 9cae234f06..1366515bd3 100644 --- a/include/xrpl/basics/strHex.h +++ b/include/xrpl/basics/strHex.h @@ -3,6 +3,9 @@ #include #include +#include +#include + namespace xrpl { template diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h index ddcde479f3..8fbd2a274b 100644 --- a/include/xrpl/basics/tagged_integer.h +++ b/include/xrpl/basics/tagged_integer.h @@ -7,6 +7,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/beast/asio/io_latency_probe.h b/include/xrpl/beast/asio/io_latency_probe.h index 5e1b098dcb..f67ff4a692 100644 --- a/include/xrpl/beast/asio/io_latency_probe.h +++ b/include/xrpl/beast/asio/io_latency_probe.h @@ -8,6 +8,7 @@ #include #include +#include #include #include diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h index 879672e9cf..7cda863fab 100644 --- a/include/xrpl/beast/container/aged_container_utility.h +++ b/include/xrpl/beast/container/aged_container_utility.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace beast { diff --git a/include/xrpl/beast/container/aged_map.h b/include/xrpl/beast/container/aged_map.h index c1f6943451..20daab70a4 100644 --- a/include/xrpl/beast/container/aged_map.h +++ b/include/xrpl/beast/container/aged_map.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace beast { diff --git a/include/xrpl/beast/container/aged_multimap.h b/include/xrpl/beast/container/aged_multimap.h index 65efd1bbf9..f6133ced1c 100644 --- a/include/xrpl/beast/container/aged_multimap.h +++ b/include/xrpl/beast/container/aged_multimap.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace beast { diff --git a/include/xrpl/beast/container/aged_unordered_map.h b/include/xrpl/beast/container/aged_unordered_map.h index a2189e2409..d6ea6e97bd 100644 --- a/include/xrpl/beast/container/aged_unordered_map.h +++ b/include/xrpl/beast/container/aged_unordered_map.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace beast { diff --git a/include/xrpl/beast/container/aged_unordered_multimap.h b/include/xrpl/beast/container/aged_unordered_multimap.h index f1348ed39f..3b72be98b7 100644 --- a/include/xrpl/beast/container/aged_unordered_multimap.h +++ b/include/xrpl/beast/container/aged_unordered_multimap.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace beast { diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 4cb2246a22..0533a51f00 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -11,9 +11,14 @@ #include #include +#include +#include +#include #include #include #include +#include +#include #include #include @@ -1247,12 +1252,7 @@ AgedOrderedContainer::AgedOrd template AgedOrderedContainer::AgedOrderedContainer( AgedOrderedContainer const& other) - : config_(other.config_) -#if BOOST_VERSION >= 108000 - , cont_(other.cont_.get_comp()) -#else - , cont_(other.cont_.comp()) -#endif + : config_(other.config_), cont_(other.cont_.get_comp()) { insert(other.cbegin(), other.cend()); } @@ -1261,12 +1261,7 @@ template ::AgedOrderedContainer( AgedOrderedContainer const& other, Allocator const& alloc) - : config_(other.config_, alloc) -#if BOOST_VERSION >= 108000 - , cont_(other.cont_.get_comp()) -#else - , cont_(other.cont_.comp()) -#endif + : config_(other.config_, alloc), cont_(other.cont_.get_comp()) { insert(other.cbegin(), other.cend()); } @@ -1283,13 +1278,7 @@ template ::AgedOrderedContainer( AgedOrderedContainer&& other, // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) Allocator const& alloc) - : config_(std::move(other.config_), alloc) -#if BOOST_VERSION >= 108000 - , cont_(std::move(other.cont_.get_comp())) -#else - , cont_(std::move(other.cont_.comp())) -#endif - + : config_(std::move(other.config_), alloc), cont_(std::move(other.cont_.get_comp())) { insert(other.cbegin(), other.cend()); other.clear(); diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index 3bad12d9e5..782f36cd52 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -10,13 +10,19 @@ #include #include +#include #include +#include +#include #include #include #include #include +#include +#include #include #include +#include /* diff --git a/include/xrpl/beast/core/CurrentThreadName.h b/include/xrpl/beast/core/CurrentThreadName.h index 6175d99b16..3cdfe4c678 100644 --- a/include/xrpl/beast/core/CurrentThreadName.h +++ b/include/xrpl/beast/core/CurrentThreadName.h @@ -6,6 +6,7 @@ #include +#include #include #include diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 18e63c9c10..8faf90f53d 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -5,11 +5,12 @@ #include #include -#include +#include #include -#include #include #include +#include +#include #include #include diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index 1c3827ae1c..1eeeaa87d1 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include namespace beast { diff --git a/include/xrpl/beast/core/LockFreeStack.h b/include/xrpl/beast/core/LockFreeStack.h index d4ad45cf5c..19225a4343 100644 --- a/include/xrpl/beast/core/LockFreeStack.h +++ b/include/xrpl/beast/core/LockFreeStack.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -58,7 +59,7 @@ public: return result; } - NodePtr + [[nodiscard]] NodePtr node() const { return node_; diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 83cff4bdea..0b36d4c983 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -5,8 +5,8 @@ #include #include +#include #include -#include #include #include #include diff --git a/include/xrpl/beast/insight/Collector.h b/include/xrpl/beast/insight/Collector.h index 2e73d60400..3f83e329d4 100644 --- a/include/xrpl/beast/insight/Collector.h +++ b/include/xrpl/beast/insight/Collector.h @@ -4,8 +4,10 @@ #include #include #include +#include #include +#include #include namespace beast::insight { diff --git a/include/xrpl/beast/insight/Insight.h b/include/xrpl/beast/insight/Insight.h deleted file mode 100644 index bf3743cfd8..0000000000 --- a/include/xrpl/beast/insight/Insight.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/include/xrpl/beast/insight/NullCollector.h b/include/xrpl/beast/insight/NullCollector.h index b865526ade..67903420fa 100644 --- a/include/xrpl/beast/insight/NullCollector.h +++ b/include/xrpl/beast/insight/NullCollector.h @@ -2,6 +2,8 @@ #include +#include + namespace beast::insight { /** A Collector which does not collect metrics. */ diff --git a/include/xrpl/beast/insight/StatsDCollector.h b/include/xrpl/beast/insight/StatsDCollector.h index ad436dc626..9a438c48f1 100644 --- a/include/xrpl/beast/insight/StatsDCollector.h +++ b/include/xrpl/beast/insight/StatsDCollector.h @@ -4,6 +4,9 @@ #include #include +#include +#include + namespace beast::insight { /** A Collector that reports metrics to a StatsD server. diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index 67deaaa787..f4327b7b8a 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -9,6 +9,7 @@ #include #include +#include #include //------------------------------------------------------------------------------ diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h index dbe5a6095f..9367fbe1eb 100644 --- a/include/xrpl/beast/net/IPAddressV4.h +++ b/include/xrpl/beast/net/IPAddressV4.h @@ -1,7 +1,5 @@ #pragma once -#include - #include namespace beast::IP { diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h index 10f806417d..1bfa079990 100644 --- a/include/xrpl/beast/net/IPAddressV6.h +++ b/include/xrpl/beast/net/IPAddressV6.h @@ -1,7 +1,5 @@ #pragma once -#include - #include namespace beast::IP { diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index fec6e1556f..0b661108f2 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -3,8 +3,13 @@ #include #include #include +#include +#include +#include #include +#include +#include #include #include diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index e810733210..7c681ab140 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/beast/test/yield_to.h b/include/xrpl/beast/test/yield_to.h index 84d7d8846d..1a34ec436e 100644 --- a/include/xrpl/beast/test/yield_to.h +++ b/include/xrpl/beast/test/yield_to.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include diff --git a/include/xrpl/beast/unit_test.h b/include/xrpl/beast/unit_test.h index 51ac96cacb..b4d53b2b1c 100644 --- a/include/xrpl/beast/unit_test.h +++ b/include/xrpl/beast/unit_test.h @@ -1,15 +1,6 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #ifndef BEAST_EXPECT #define BEAST_EXPECT_S1(x) #x diff --git a/include/xrpl/beast/unit_test/match.h b/include/xrpl/beast/unit_test/match.h index da466ab228..5faeaa1100 100644 --- a/include/xrpl/beast/unit_test/match.h +++ b/include/xrpl/beast/unit_test/match.h @@ -7,6 +7,7 @@ #include #include +#include namespace beast::unit_test { diff --git a/include/xrpl/beast/unit_test/recorder.h b/include/xrpl/beast/unit_test/recorder.h index 2ed88d4a46..1b7347dc2e 100644 --- a/include/xrpl/beast/unit_test/recorder.h +++ b/include/xrpl/beast/unit_test/recorder.h @@ -6,6 +6,10 @@ #include #include +#include + +#include +#include namespace beast::unit_test { diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index ff990dece5..a903d9f8c2 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -5,18 +5,21 @@ #pragma once #include -#include +#include +#include #include #include #include #include +#include #include #include #include #include #include +#include namespace beast::unit_test { diff --git a/include/xrpl/beast/unit_test/results.h b/include/xrpl/beast/unit_test/results.h index 02aa9730d1..718d764c9f 100644 --- a/include/xrpl/beast/unit_test/results.h +++ b/include/xrpl/beast/unit_test/results.h @@ -6,6 +6,7 @@ #include +#include #include #include #include diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index fded866da0..487663fcc5 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -634,7 +636,7 @@ Suite::run(Runner& r) #define BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Class, Module, Library, Priority) #else -#include +#include // IWYU pragma: keep #define BEAST_DEFINE_TESTSUITE(Class, Module, Library) \ BEAST_DEFINE_TESTSUITE_INSERT(Class, Module, Library, false, 0) #define BEAST_DEFINE_TESTSUITE_MANUAL(Class, Module, Library) \ diff --git a/include/xrpl/beast/unit_test/suite_info.h b/include/xrpl/beast/unit_test/suite_info.h index c09a0c2257..bda10ae7e3 100644 --- a/include/xrpl/beast/unit_test/suite_info.h +++ b/include/xrpl/beast/unit_test/suite_info.h @@ -4,9 +4,9 @@ #pragma once -#include #include #include +#include #include namespace beast::unit_test { diff --git a/include/xrpl/beast/unit_test/suite_list.h b/include/xrpl/beast/unit_test/suite_list.h index 748f994602..cf9fb9c5b1 100644 --- a/include/xrpl/beast/unit_test/suite_list.h +++ b/include/xrpl/beast/unit_test/suite_list.h @@ -10,6 +10,7 @@ #include #include +#include #include #include diff --git a/include/xrpl/beast/unit_test/thread.h b/include/xrpl/beast/unit_test/thread.h index 7ae093eb85..0de039cb89 100644 --- a/include/xrpl/beast/unit_test/thread.h +++ b/include/xrpl/beast/unit_test/thread.h @@ -6,7 +6,9 @@ #include +#include #include +#include #include #include diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h index 1262a64179..ac08b1384b 100644 --- a/include/xrpl/beast/utility/Journal.h +++ b/include/xrpl/beast/utility/Journal.h @@ -3,7 +3,10 @@ #include #include +#include #include +#include +#include namespace beast { diff --git a/include/xrpl/beast/utility/PropertyStream.h b/include/xrpl/beast/utility/PropertyStream.h index 62de019edd..3fb6df53d9 100644 --- a/include/xrpl/beast/utility/PropertyStream.h +++ b/include/xrpl/beast/utility/PropertyStream.h @@ -3,8 +3,10 @@ #include #include +#include #include #include +#include namespace beast { diff --git a/include/xrpl/beast/utility/WrappedSink.h b/include/xrpl/beast/utility/WrappedSink.h index 22d75927fe..a24ad595db 100644 --- a/include/xrpl/beast/utility/WrappedSink.h +++ b/include/xrpl/beast/utility/WrappedSink.h @@ -2,6 +2,7 @@ #include +#include #include namespace beast { diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index 2ea84a7a3d..0fc3ffe0d0 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h index 66d1d24736..3c798663d7 100644 --- a/include/xrpl/conditions/Condition.h +++ b/include/xrpl/conditions/Condition.h @@ -4,8 +4,12 @@ #include #include +#include #include +#include #include +#include +#include namespace xrpl::cryptoconditions { diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h index fd8cd7d31e..a3001b2620 100644 --- a/include/xrpl/conditions/Fulfillment.h +++ b/include/xrpl/conditions/Fulfillment.h @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include + namespace xrpl::cryptoconditions { struct Fulfillment diff --git a/include/xrpl/conditions/detail/PreimageSha256.h b/include/xrpl/conditions/detail/PreimageSha256.h index c592ea37ee..0973a52e4a 100644 --- a/include/xrpl/conditions/detail/PreimageSha256.h +++ b/include/xrpl/conditions/detail/PreimageSha256.h @@ -7,7 +7,11 @@ #include #include +#include +#include #include +#include +#include namespace xrpl::cryptoconditions { diff --git a/include/xrpl/conditions/detail/utils.h b/include/xrpl/conditions/detail/utils.h index 87f2265034..bf16bfb42b 100644 --- a/include/xrpl/conditions/detail/utils.h +++ b/include/xrpl/conditions/detail/utils.h @@ -6,7 +6,10 @@ #include +#include +#include #include +#include // A collection of functions to decode binary blobs // encoded with X.690 Distinguished Encoding Rules. diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h index 858bf8bf2e..5680b51fe7 100644 --- a/include/xrpl/config/BasicConfig.h +++ b/include/xrpl/config/BasicConfig.h @@ -6,9 +6,13 @@ #include #include +#include #include +#include +#include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/core/ClosureCounter.h b/include/xrpl/core/ClosureCounter.h index fb13047f40..ed15db032e 100644 --- a/include/xrpl/core/ClosureCounter.h +++ b/include/xrpl/core/ClosureCounter.h @@ -1,11 +1,14 @@ #pragma once #include +#include #include +#include #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h index c8b34d8e93..ef8f24d43c 100644 --- a/include/xrpl/core/HashRouter.h +++ b/include/xrpl/core/HashRouter.h @@ -4,10 +4,16 @@ #include #include #include +#include #include +#include +#include +#include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/core/Job.h b/include/xrpl/core/Job.h index 6af32eb2d8..e16d7412bf 100644 --- a/include/xrpl/core/Job.h +++ b/include/xrpl/core/Job.h @@ -2,9 +2,14 @@ #include #include +#include #include +#include +#include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/core/JobQueue.h b/include/xrpl/core/JobQueue.h index fc15e9a064..14170a39be 100644 --- a/include/xrpl/core/JobQueue.h +++ b/include/xrpl/core/JobQueue.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -12,10 +11,27 @@ // `boost/context/pooled_fixedsize_stack.hpp`, whose `.malloc()` / `.free()` // member calls on `boost::pool` collide with MSVC's `_CRTDBG_MAP_ALLOC` macros // in Debug builds (see cmake/XrplCompiler.cmake). +#include +#include +#include +#include +#include +#include + #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include namespace xrpl { @@ -112,7 +128,7 @@ public: resume(); /** Returns true if the Coro is still runnable (has not returned). */ - bool + [[nodiscard]] bool runnable() const; /** Once called, the Coro allows early exit without an assert. */ @@ -384,7 +400,7 @@ private: } // namespace xrpl -#include +#include // IWYU pragma: keep namespace xrpl { diff --git a/include/xrpl/core/JobTypeData.h b/include/xrpl/core/JobTypeData.h index 4e9f95dc04..d53440e1ca 100644 --- a/include/xrpl/core/JobTypeData.h +++ b/include/xrpl/core/JobTypeData.h @@ -2,7 +2,10 @@ #include #include +#include +#include #include +#include #include diff --git a/include/xrpl/core/JobTypeInfo.h b/include/xrpl/core/JobTypeInfo.h index 430e80b388..b5db0dbaab 100644 --- a/include/xrpl/core/JobTypeInfo.h +++ b/include/xrpl/core/JobTypeInfo.h @@ -2,6 +2,10 @@ #include +#include +#include +#include + namespace xrpl { /** Holds all the 'static' information about a job, which does not change */ diff --git a/include/xrpl/core/JobTypes.h b/include/xrpl/core/JobTypes.h index fb5c7988cb..f338b19f6c 100644 --- a/include/xrpl/core/JobTypes.h +++ b/include/xrpl/core/JobTypes.h @@ -1,10 +1,14 @@ #pragma once +#include #include #include +#include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/core/LoadMonitor.h b/include/xrpl/core/LoadMonitor.h index 32a813baa7..f1a8eb6c56 100644 --- a/include/xrpl/core/LoadMonitor.h +++ b/include/xrpl/core/LoadMonitor.h @@ -5,6 +5,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/core/PeerReservationTable.h b/include/xrpl/core/PeerReservationTable.h index a9ab894124..e6f6dd622e 100644 --- a/include/xrpl/core/PeerReservationTable.h +++ b/include/xrpl/core/PeerReservationTable.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include diff --git a/include/xrpl/core/PerfLog.h b/include/xrpl/core/PerfLog.h index ca0d9333a4..f09665e291 100644 --- a/include/xrpl/core/PerfLog.h +++ b/include/xrpl/core/PerfLog.h @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 1d0c9e38f4..50bf2d7c10 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -1,11 +1,17 @@ #pragma once #include +#include #include #include +#include +#include #include +#include +#include + namespace xrpl { // Forward declarations diff --git a/include/xrpl/core/StartUpType.h b/include/xrpl/core/StartUpType.h index 46359ad7b6..6d05149618 100644 --- a/include/xrpl/core/StartUpType.h +++ b/include/xrpl/core/StartUpType.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include namespace xrpl { diff --git a/include/xrpl/core/detail/semaphore.h b/include/xrpl/core/detail/semaphore.h index 7bc83f86f5..e40463e322 100644 --- a/include/xrpl/core/detail/semaphore.h +++ b/include/xrpl/core/detail/semaphore.h @@ -29,6 +29,7 @@ #pragma once #include +#include #include namespace xrpl { diff --git a/include/xrpl/crypto/RFC1751.h b/include/xrpl/crypto/RFC1751.h index 19b636b9dc..278f3c207b 100644 --- a/include/xrpl/crypto/RFC1751.h +++ b/include/xrpl/crypto/RFC1751.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/include/xrpl/crypto/csprng.h b/include/xrpl/crypto/csprng.h index e386d9d11e..cdc6a723c8 100644 --- a/include/xrpl/crypto/csprng.h +++ b/include/xrpl/crypto/csprng.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/json/JsonPropertyStream.h b/include/xrpl/json/JsonPropertyStream.h index 47317b9ddb..405a61cd34 100644 --- a/include/xrpl/json/JsonPropertyStream.h +++ b/include/xrpl/json/JsonPropertyStream.h @@ -3,6 +3,9 @@ #include #include +#include +#include + namespace xrpl { /** A PropertyStream::Sink which produces a json::Value of type ValueType::Object. */ diff --git a/include/xrpl/json/Writer.h b/include/xrpl/json/Writer.h index 87e3e99c7e..024876a43c 100644 --- a/include/xrpl/json/Writer.h +++ b/include/xrpl/json/Writer.h @@ -1,11 +1,13 @@ #pragma once -#include #include #include #include +#include #include +#include +#include namespace json { diff --git a/include/xrpl/json/detail/json_assert.h b/include/xrpl/json/detail/json_assert.h index 8e33f45b65..f501e42aa4 100644 --- a/include/xrpl/json/detail/json_assert.h +++ b/include/xrpl/json/detail/json_assert.h @@ -1,8 +1,5 @@ #pragma once -#include -#include - #define JSON_ASSERT_MESSAGE(condition, message) \ if (!(condition)) \ xrpl::Throw(message); diff --git a/include/xrpl/json/json_reader.h b/include/xrpl/json/json_reader.h index 9251183281..c1535bfb55 100644 --- a/include/xrpl/json/json_reader.h +++ b/include/xrpl/json/json_reader.h @@ -5,7 +5,10 @@ #include +#include +#include #include +#include namespace json { diff --git a/include/xrpl/json/json_writer.h b/include/xrpl/json/json_writer.h index afc99fe8c9..4bc15b71da 100644 --- a/include/xrpl/json/json_writer.h +++ b/include/xrpl/json/json_writer.h @@ -3,7 +3,10 @@ #include #include +#include #include +#include +#include #include namespace json { diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h b/include/xrpl/ledger/AcceptedLedgerTx.h index 0a1592f6e1..f59b8a074d 100644 --- a/include/xrpl/ledger/AcceptedLedgerTx.h +++ b/include/xrpl/ledger/AcceptedLedgerTx.h @@ -1,13 +1,23 @@ #pragma once +#include #include +#include #include #include +#include +#include #include +#include +#include #include #include +#include +#include +#include + namespace xrpl { /** diff --git a/include/xrpl/ledger/AmendmentTable.h b/include/xrpl/ledger/AmendmentTable.h index 8ed3cb81ff..6598be5a5c 100644 --- a/include/xrpl/ledger/AmendmentTable.h +++ b/include/xrpl/ledger/AmendmentTable.h @@ -1,14 +1,37 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include +#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index 362eae0f79..3bf5d479d1 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -1,9 +1,22 @@ #pragma once +#include #include #include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/ApplyViewImpl.h b/include/xrpl/ledger/ApplyViewImpl.h index 1245568630..9a3734a8ca 100644 --- a/include/xrpl/ledger/ApplyViewImpl.h +++ b/include/xrpl/ledger/ApplyViewImpl.h @@ -1,9 +1,20 @@ #pragma once +#include +#include +#include #include +#include #include #include +#include +#include #include +#include + +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h index 36798934da..dc4361136d 100644 --- a/include/xrpl/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -1,6 +1,13 @@ #pragma once +#include #include +#include +#include + +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h index 462db48ee3..f83c3e1297 100644 --- a/include/xrpl/ledger/CachedView.h +++ b/include/xrpl/ledger/CachedView.h @@ -1,11 +1,20 @@ #pragma once +#include #include #include #include +#include +#include +#include +#include +#include +#include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index 4dffadd52f..f8349dfab6 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -1,10 +1,16 @@ #pragma once #include +#include +#include #include #include #include +#include +#include +#include + namespace xrpl { /** Holds transactions which were deferred to the next pass of consensus. diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index d305e21938..05df887d8b 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -1,7 +1,15 @@ #pragma once +#include #include -#include +#include +#include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/Ledger.h b/include/xrpl/ledger/Ledger.h index 5f7d79c61d..3453389a5e 100644 --- a/include/xrpl/ledger/Ledger.h +++ b/include/xrpl/ledger/Ledger.h @@ -1,16 +1,32 @@ #pragma once #include +#include +#include +#include #include #include -#include +#include #include -#include +#include +#include +#include #include #include +#include +#include #include -#include +#include +#include #include +#include + +#include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/LedgerTiming.h b/include/xrpl/ledger/LedgerTiming.h index 508403d760..a97e229046 100644 --- a/include/xrpl/ledger/LedgerTiming.h +++ b/include/xrpl/ledger/LedgerTiming.h @@ -1,9 +1,10 @@ #pragma once -#include -#include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/OpenView.h b/include/xrpl/ledger/OpenView.h index d145473516..de81787906 100644 --- a/include/xrpl/ledger/OpenView.h +++ b/include/xrpl/ledger/OpenView.h @@ -1,15 +1,26 @@ #pragma once +#include +#include #include #include #include -#include +#include +#include +#include +#include +#include +#include #include #include #include +#include #include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/ledger/PaymentSandbox.h b/include/xrpl/ledger/PaymentSandbox.h index 1cd89d9388..0117a962ff 100644 --- a/include/xrpl/ledger/PaymentSandbox.h +++ b/include/xrpl/ledger/PaymentSandbox.h @@ -1,11 +1,19 @@ #pragma once +#include #include -#include +#include #include #include +#include +#include +#include +#include +#include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/ledger/RawView.h b/include/xrpl/ledger/RawView.h index cf61c3e814..ac2674226f 100644 --- a/include/xrpl/ledger/RawView.h +++ b/include/xrpl/ledger/RawView.h @@ -3,6 +3,9 @@ #include #include #include +#include + +#include namespace xrpl { diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h index f4ee7e6fd2..8bbd3e06cb 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -1,21 +1,28 @@ #pragma once +#include #include #include +#include #include +#include #include -#include -#include +#include +#include #include +#include #include #include #include #include +#include #include #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/Sandbox.h b/include/xrpl/ledger/Sandbox.h index dc80df5ba2..ca8838631f 100644 --- a/include/xrpl/ledger/Sandbox.h +++ b/include/xrpl/ledger/Sandbox.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 255413e459..c89764df1d 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -1,13 +1,22 @@ #pragma once +#include +#include #include #include #include +#include +#include +#include +#include #include #include +#include +#include #include #include #include +#include #include #include diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h b/include/xrpl/ledger/detail/ApplyStateTable.h index f40e3d0d1c..752c87d588 100644 --- a/include/xrpl/ledger/detail/ApplyStateTable.h +++ b/include/xrpl/ledger/detail/ApplyStateTable.h @@ -1,13 +1,26 @@ #pragma once +#include +#include #include #include #include #include +#include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include +#include + namespace xrpl::detail { // Helper class that buffers modifications diff --git a/include/xrpl/ledger/detail/ApplyViewBase.h b/include/xrpl/ledger/detail/ApplyViewBase.h index d6493c46a8..b5b01de277 100644 --- a/include/xrpl/ledger/detail/ApplyViewBase.h +++ b/include/xrpl/ledger/detail/ApplyViewBase.h @@ -1,10 +1,20 @@ #pragma once +#include #include +#include #include #include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl::detail { class ApplyViewBase : public ApplyView, public RawView diff --git a/include/xrpl/ledger/detail/RawStateTable.h b/include/xrpl/ledger/detail/RawStateTable.h index d2567e34f1..cfa528d355 100644 --- a/include/xrpl/ledger/detail/RawStateTable.h +++ b/include/xrpl/ledger/detail/RawStateTable.h @@ -1,12 +1,20 @@ #pragma once +#include +#include #include #include +#include +#include #include #include +#include +#include #include +#include +#include #include namespace xrpl::detail { diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index c548ccb101..82d8b59c6a 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index de8bb9d3f7..c6a2053010 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -2,22 +2,36 @@ #include #include +#include #include +#include +#include #include #include -#include #include #include +#include #include +#include #include #include #include +#include #include #include #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index cf6082d533..d0cfc175a3 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -1,13 +1,18 @@ #pragma once +#include #include #include #include +#include #include #include +#include #include #include +#include +#include #include #include #include diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index d6b797ce34..8e78a00923 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -1,15 +1,22 @@ #pragma once -#include +#include #include +#include #include #include #include #include #include +#include #include +#include #include +#include +#include +#include + namespace xrpl { namespace credentials { diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index a517eefdaa..3c277cb4f7 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -4,6 +4,9 @@ #include #include #include +#include + +#include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h index a0be52df99..76a5f3bdad 100644 --- a/include/xrpl/ledger/helpers/DirectoryHelpers.h +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h @@ -1,12 +1,16 @@ #pragma once +#include #include #include #include +#include #include +#include +#include #include -#include +#include #include #include #include diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index dc7c479c42..001dc9cb25 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -1,15 +1,28 @@ #pragma once #include +#include #include -#include #include #include #include +#include +#include +#include #include #include -#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index 8de945233b..c21e5bf0ce 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -1,11 +1,28 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index b7f87337cf..a725871231 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -4,11 +4,17 @@ #include #include #include +#include +#include #include #include #include +#include #include +#include +#include +#include #include #include diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 362cfe5a8c..1c4d395fbe 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -1,13 +1,25 @@ #pragma once -#include +#include #include +#include #include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include -#include +#include +#include +#include +#include #include namespace xrpl::nft { diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h index 3c08ee9f32..6e8cd17f7f 100644 --- a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h @@ -1,12 +1,13 @@ #pragma once +#include #include #include +#include +#include #include -#include #include -#include #include namespace xrpl { diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h index 695a4950f0..12681257aa 100644 --- a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h +++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h @@ -1,6 +1,10 @@ #pragma once -#include +#include +#include +#include +#include +#include namespace xrpl::permissioned_dex { diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 3aaaa541fd..ab09b931cc 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -1,14 +1,21 @@ #pragma once +#include #include #include #include #include +#include #include #include #include #include #include +#include +#include + +#include +#include //------------------------------------------------------------------------------ // diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index c8a67f776a..32f785a0d6 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -1,15 +1,22 @@ #pragma once +#include #include #include #include +#include #include +#include #include #include #include #include +#include +#include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h index 16ed0d6ca9..72eaed7439 100644 --- a/include/xrpl/net/AutoSocket.h +++ b/include/xrpl/net/AutoSocket.h @@ -2,12 +2,20 @@ #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include + // Socket wrapper that supports both SSL and non-SSL connections. // Generally, handle it as you would an SSL connection. // To force a non-SSL connection, just don't call async_handshake. diff --git a/include/xrpl/net/HTTPClient.h b/include/xrpl/net/HTTPClient.h index 456f769922..7ed9b35b9b 100644 --- a/include/xrpl/net/HTTPClient.h +++ b/include/xrpl/net/HTTPClient.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h index ca1983f141..4192455ec9 100644 --- a/include/xrpl/net/HTTPClientSSLContext.h +++ b/include/xrpl/net/HTTPClientSSLContext.h @@ -10,6 +10,14 @@ #include #include +#include +#include + +#include +#include +#include +#include + namespace xrpl { class HTTPClientSSLContext diff --git a/include/xrpl/net/RegisterSSLCerts.h b/include/xrpl/net/RegisterSSLCerts.h index e313b1cb06..5cc9934638 100644 --- a/include/xrpl/net/RegisterSSLCerts.h +++ b/include/xrpl/net/RegisterSSLCerts.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 0061890237..29c4a8b526 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -1,8 +1,17 @@ #pragma once +#include +#include +#include #include +#include #include +#include +#include +#include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/Database.h b/include/xrpl/nodestore/Database.h index 68c5dcefb6..49002ee301 100644 --- a/include/xrpl/nodestore/Database.h +++ b/include/xrpl/nodestore/Database.h @@ -1,13 +1,25 @@ #pragma once -#include -#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include #include #include #include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl { class Section; diff --git a/include/xrpl/nodestore/DatabaseRotating.h b/include/xrpl/nodestore/DatabaseRotating.h index a7deed294a..69eb31261d 100644 --- a/include/xrpl/nodestore/DatabaseRotating.h +++ b/include/xrpl/nodestore/DatabaseRotating.h @@ -1,6 +1,13 @@ #pragma once +#include +#include #include +#include + +#include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/DummyScheduler.h b/include/xrpl/nodestore/DummyScheduler.h index 472684ff13..f626115786 100644 --- a/include/xrpl/nodestore/DummyScheduler.h +++ b/include/xrpl/nodestore/DummyScheduler.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/Factory.h b/include/xrpl/nodestore/Factory.h index 3e6ba76a08..e79ae3e05d 100644 --- a/include/xrpl/nodestore/Factory.h +++ b/include/xrpl/nodestore/Factory.h @@ -4,7 +4,11 @@ #include #include -#include +#include + +#include +#include +#include namespace xrpl { class Section; diff --git a/include/xrpl/nodestore/Manager.h b/include/xrpl/nodestore/Manager.h index 1c4e5b63cf..f813412846 100644 --- a/include/xrpl/nodestore/Manager.h +++ b/include/xrpl/nodestore/Manager.h @@ -1,7 +1,14 @@ #pragma once -#include +#include +#include +#include #include +#include + +#include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/NodeObject.h b/include/xrpl/nodestore/NodeObject.h index 04ba391b2b..3f3b75d5f8 100644 --- a/include/xrpl/nodestore/NodeObject.h +++ b/include/xrpl/nodestore/NodeObject.h @@ -4,6 +4,10 @@ #include #include +#include +#include +#include + // VFALCO NOTE Intentionally not in the NodeStore namespace namespace xrpl { diff --git a/include/xrpl/nodestore/Types.h b/include/xrpl/nodestore/Types.h index 21c01e9111..eaee82c99e 100644 --- a/include/xrpl/nodestore/Types.h +++ b/include/xrpl/nodestore/Types.h @@ -2,6 +2,7 @@ #include +#include #include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/BatchWriter.h b/include/xrpl/nodestore/detail/BatchWriter.h index b0383838dc..7fa23bcb3e 100644 --- a/include/xrpl/nodestore/detail/BatchWriter.h +++ b/include/xrpl/nodestore/detail/BatchWriter.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include #include +#include #include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h b/include/xrpl/nodestore/detail/DatabaseNodeImp.h index 38b8763f31..6f2fca682f 100644 --- a/include/xrpl/nodestore/detail/DatabaseNodeImp.h +++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h @@ -1,10 +1,26 @@ #pragma once +#include #include +#include #include +#include +#include +#include #include #include +#include #include +#include +#include + +#include +#include +#include +#include +#include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h index 1ba9435a5f..6343275c76 100644 --- a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h @@ -1,8 +1,19 @@ #pragma once +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h b/include/xrpl/nodestore/detail/DecodedBlob.h index 90a7b6c9cb..b2d5fc9c26 100644 --- a/include/xrpl/nodestore/detail/DecodedBlob.h +++ b/include/xrpl/nodestore/detail/DecodedBlob.h @@ -2,6 +2,8 @@ #include +#include + namespace xrpl::NodeStore { /** Parsed key/value blob into NodeObject components. diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h b/include/xrpl/nodestore/detail/EncodedBlob.h index 343e1720a0..3982ab1b95 100644 --- a/include/xrpl/nodestore/detail/EncodedBlob.h +++ b/include/xrpl/nodestore/detail/EncodedBlob.h @@ -7,7 +7,10 @@ #include #include +#include #include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/ManagerImp.h b/include/xrpl/nodestore/detail/ManagerImp.h index 98aec6459b..fc84b0aa57 100644 --- a/include/xrpl/nodestore/detail/ManagerImp.h +++ b/include/xrpl/nodestore/detail/ManagerImp.h @@ -1,6 +1,17 @@ #pragma once +#include +#include +#include +#include #include +#include + +#include +#include +#include +#include +#include namespace xrpl::NodeStore { diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h index 49238fa34a..56e1fbcf73 100644 --- a/include/xrpl/nodestore/detail/codec.h +++ b/include/xrpl/nodestore/detail/codec.h @@ -1,6 +1,10 @@ #pragma once // Disable lz4 deprecation warning due to incompatibility with clang attributes +#include +#include +#include +#include #define LZ4_DISABLE_DEPRECATE_WARNINGS #include diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index 0c49274d70..21a13bd6de 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -2,6 +2,7 @@ #include +#include #include #include diff --git a/include/xrpl/proto/org/xrpl/rpc/v1/README.md b/include/xrpl/proto/org/xrpl/rpc/v1/README.md index e8566ec179..d0ff14cd13 100644 --- a/include/xrpl/proto/org/xrpl/rpc/v1/README.md +++ b/include/xrpl/proto/org/xrpl/rpc/v1/README.md @@ -70,9 +70,9 @@ into helper functions (see Tx.cpp or AccountTx.cpp for an example). #### Testing When modifying an existing gRPC method, be sure to test that modification in the -corresponding, existing unit test. When creating a new gRPC method, implement a class that -derives from GRPCTestClientBase, and use the newly created class to call the new -method. See the class `GrpcTxClient` in the file Tx_test.cpp for an example. +corresponding, existing unit test. When creating a new gRPC method, create a +client stub with `XRPLedgerAPIService::NewStub` and `grpc::CreateChannel`, and +use it to call the new method. See `GRPCServerTLS_test.cpp` for an example. The gRPC tests are paired with their JSON counterpart, and the tests should mirror the JSON test as much as possible. diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index a83c8bfa84..c4fccd029a 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -3,9 +3,14 @@ #include #include #include +#include #include #include +#include +#include +#include + namespace xrpl { constexpr std::uint16_t kTradingFeeThreshold = 1000; // 1% diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h index 4938812ffa..a7d49246ca 100644 --- a/include/xrpl/protocol/AccountID.h +++ b/include/xrpl/protocol/AccountID.h @@ -3,13 +3,17 @@ #include // VFALCO Uncomment when the header issues are resolved // #include -#include #include +#include +#include #include +#include #include #include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index a5f7ec310f..66ada68d6f 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -1,10 +1,20 @@ #pragma once +#include +#include +#include +#include #include +#include +#include +#include #include #include #include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index 10b7571641..c3292e6074 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -5,6 +5,7 @@ #include #include +#include #include #include diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index ec9d8db02f..2bf24b19fe 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -2,10 +2,19 @@ #include #include +#include +#include +#include #include #include #include -#include +#include + +#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h index 4e021442d6..1e4f811fa9 100644 --- a/include/xrpl/protocol/Batch.h +++ b/include/xrpl/protocol/Batch.h @@ -1,10 +1,13 @@ #pragma once +#include #include #include -#include #include +#include +#include + namespace xrpl { inline void diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 01dc40075b..476bdba35a 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -2,10 +2,21 @@ #include #include +#include #include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include + namespace xrpl { /** Specifies an order book. diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h index 47a27339a8..a60c37e714 100644 --- a/include/xrpl/protocol/BuildInfo.h +++ b/include/xrpl/protocol/BuildInfo.h @@ -2,6 +2,7 @@ #include #include +#include /** Versioning information for this build. */ // VFALCO The namespace is deprecated diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 5b1bcbf606..325117eed4 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -1,22 +1,21 @@ #pragma once +#include #include #include -#include -#include -#include -#include -#include +#include +#include +#include // IWYU pragma: keep #include -#include #include -#include +#include #include -#include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index f5e67fd572..38b8bc6d76 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -1,7 +1,8 @@ #pragma once #include -#include + +#include namespace xrpl { diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 5de8ca64a9..927fde542a 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include +#include #include #include #include diff --git a/include/xrpl/protocol/Fees.h b/include/xrpl/protocol/Fees.h index 14bcc068bf..4e79204f4c 100644 --- a/include/xrpl/protocol/Fees.h +++ b/include/xrpl/protocol/Fees.h @@ -2,6 +2,9 @@ #include +#include +#include + namespace xrpl { // Deprecated constant for backwards compatibility with pre-XRPFees amendment. diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index b057f1c245..186ce054f1 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 75a2335f6f..053a66787f 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -1,18 +1,25 @@ #pragma once +#include #include +#include +#include +#include +#include #include +#include #include #include #include -#include #include -#include #include #include +#include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/InnerObjectFormats.h b/include/xrpl/protocol/InnerObjectFormats.h index 9d07a21d1c..c8312c3701 100644 --- a/include/xrpl/protocol/InnerObjectFormats.h +++ b/include/xrpl/protocol/InnerObjectFormats.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Issue.h b/include/xrpl/protocol/Issue.h index c8022698d3..3d556e83eb 100644 --- a/include/xrpl/protocol/Issue.h +++ b/include/xrpl/protocol/Issue.h @@ -1,9 +1,13 @@ #pragma once -#include #include +#include #include +#include +#include +#include + namespace xrpl { /** A currency issued by an account. diff --git a/include/xrpl/protocol/KnownFormats.h b/include/xrpl/protocol/KnownFormats.h index 6e21d4bc3a..c31e28c37d 100644 --- a/include/xrpl/protocol/KnownFormats.h +++ b/include/xrpl/protocol/KnownFormats.h @@ -7,7 +7,11 @@ #include #include +#include #include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 70afd12f34..5b8a8cc2c5 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -3,9 +3,12 @@ // NOLINTBEGIN(readability-identifier-naming) #include +#include +#include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/LedgerHeader.h b/include/xrpl/protocol/LedgerHeader.h index f05b11d1eb..df8f314c5f 100644 --- a/include/xrpl/protocol/LedgerHeader.h +++ b/include/xrpl/protocol/LedgerHeader.h @@ -3,10 +3,13 @@ #include #include #include +#include #include #include #include +#include + namespace xrpl { /** Information about the notional ledger backing the view. */ diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 6ea36fc294..329d83610e 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -2,13 +2,15 @@ #include #include -#include #include #include #include #include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index f55029f50d..0c495aa57f 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -1,8 +1,18 @@ #pragma once +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include + namespace xrpl { /* Adapt MPTID to provide the same interface as Issue. Enables using static diff --git a/include/xrpl/protocol/PathAsset.h b/include/xrpl/protocol/PathAsset.h index b51dc52b47..ebf6fb68a4 100644 --- a/include/xrpl/protocol/PathAsset.h +++ b/include/xrpl/protocol/PathAsset.h @@ -1,7 +1,14 @@ #pragma once +#include #include #include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index c6f464082d..703a0939c9 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -1,9 +1,12 @@ #pragma once +#include #include -#include +#include #include +#include +#include #include #include #include diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index ba6347dfa7..f802cfe058 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 20693160d3..13db17fc6e 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -1,8 +1,15 @@ #pragma once #include +#include +#include +#include #include +#include +#include #include +#include +#include #include #include #include @@ -11,8 +18,11 @@ #include #include #include +#include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index e261025cb8..6bb2033dc0 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -1,11 +1,13 @@ #pragma once +#include +#include #include -#include #include -#include #include +#include +#include #include #include #include diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index 96d30735b8..7830519deb 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -1,9 +1,15 @@ #pragma once #include +#include +#include #include #include +#include +#include +#include + namespace xrpl { /** Average quality of a path as a function of `out`: q(out) = m * out + b, diff --git a/include/xrpl/protocol/Rate.h b/include/xrpl/protocol/Rate.h index 504b17ed80..b8b04c8fb9 100644 --- a/include/xrpl/protocol/Rate.h +++ b/include/xrpl/protocol/Rate.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 47b20756db..da2031650f 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -4,7 +4,10 @@ #include #include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 34fb66ce00..d97bcb0a1d 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -2,10 +2,11 @@ #include #include -#include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/SOTemplate.h b/include/xrpl/protocol/SOTemplate.h index 72e0573d29..682a7c655e 100644 --- a/include/xrpl/protocol/SOTemplate.h +++ b/include/xrpl/protocol/SOTemplate.h @@ -3,9 +3,11 @@ #include #include +#include #include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/STAccount.h b/include/xrpl/protocol/STAccount.h index 65f404d58d..17d3affc57 100644 --- a/include/xrpl/protocol/STAccount.h +++ b/include/xrpl/protocol/STAccount.h @@ -1,9 +1,13 @@ #pragma once +#include #include #include +#include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 1a5b442d8b..5e53a85129 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -1,14 +1,20 @@ #pragma once #include -#include #include +#include +#include #include +#include #include +#include +#include #include +#include #include #include #include +#include #include #include #include @@ -16,6 +22,14 @@ #include #include +#include +#include +#include +#include +#include +#include +#include + namespace xrpl { // Internal form: diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index 61753c52dc..ed39e65026 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -1,7 +1,18 @@ #pragma once #include +#include +#include +#include #include +#include + +#include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index 6633253d3b..341c80edd7 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -1,9 +1,12 @@ #pragma once #include +#include #include #include +#include +#include #include #include #include diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 0267eac22d..4f25eccca6 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -1,8 +1,15 @@ #pragma once #include +#include #include +#include +#include #include +#include + +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STBlob.h b/include/xrpl/protocol/STBlob.h index 0667c54e30..ab6175f5e3 100644 --- a/include/xrpl/protocol/STBlob.h +++ b/include/xrpl/protocol/STBlob.h @@ -3,10 +3,14 @@ #include #include #include -#include +#include #include +#include +#include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h index 55d1ab1e74..18642b20cf 100644 --- a/include/xrpl/protocol/STCurrency.h +++ b/include/xrpl/protocol/STCurrency.h @@ -1,11 +1,15 @@ #pragma once -#include +#include #include #include #include #include +#include +#include +#include + namespace xrpl { class STCurrency final : public STBase diff --git a/include/xrpl/protocol/STExchange.h b/include/xrpl/protocol/STExchange.h index c733df37cf..a9c1f57bd8 100644 --- a/include/xrpl/protocol/STExchange.h +++ b/include/xrpl/protocol/STExchange.h @@ -1,14 +1,15 @@ #pragma once -#include #include #include #include #include +#include #include #include #include +#include #include #include #include diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index 52e0f7a365..1c7ac98f3e 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -1,7 +1,15 @@ #pragma once #include +#include +#include +#include #include +#include + +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STIssue.h b/include/xrpl/protocol/STIssue.h index f5e1f61168..8ff579553f 100644 --- a/include/xrpl/protocol/STIssue.h +++ b/include/xrpl/protocol/STIssue.h @@ -1,11 +1,20 @@ #pragma once #include +#include +#include #include +#include +#include #include #include #include +#include +#include +#include +#include + namespace xrpl { class STIssue final : public STBase, CountedObject diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index aa87411ae6..a5f449f99c 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -1,7 +1,19 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include #include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index 8594a292f4..7efb63ac5e 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -2,10 +2,17 @@ #include #include +#include +#include +#include #include #include +#include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 044e48ce62..c254a37aaf 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -1,27 +1,39 @@ #pragma once +#include #include +#include #include -#include +#include #include #include +#include +#include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include +#include +#include +#include #include #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STParsedJSON.h b/include/xrpl/protocol/STParsedJSON.h index 2557ab055b..1eeecc8b9e 100644 --- a/include/xrpl/protocol/STParsedJSON.h +++ b/include/xrpl/protocol/STParsedJSON.h @@ -1,8 +1,11 @@ #pragma once -#include +#include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 1508dcb727..f6b0fde7da 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -3,14 +3,17 @@ #include #include #include -#include +#include #include #include #include +#include #include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STTakesAsset.h b/include/xrpl/protocol/STTakesAsset.h index bf75ffccf7..70bafd0e91 100644 --- a/include/xrpl/protocol/STTakesAsset.h +++ b/include/xrpl/protocol/STTakesAsset.h @@ -3,6 +3,8 @@ #include #include +#include + namespace xrpl { /** Intermediate class for any STBase-derived class to store an Asset. diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index e78fce27a1..b36207bf61 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -1,18 +1,30 @@ #pragma once -#include +#include +#include +#include +#include +#include #include #include +#include +#include #include #include #include +#include #include #include +#include +#include #include #include +#include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 91ce88b441..67a6594419 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -1,15 +1,30 @@ #pragma once +#include +#include #include +#include +#include +#include +#include #include +#include #include +#include +#include +#include #include #include -#include +#include +#include +#include +#include #include #include #include +#include +#include namespace xrpl { @@ -72,35 +87,35 @@ public: F&& f); // Hash of the validated ledger - uint256 + [[nodiscard]] uint256 getLedgerHash() const; // Hash of consensus transaction set used to generate ledger - uint256 + [[nodiscard]] uint256 getConsensusHash() const; - NetClock::time_point + [[nodiscard]] NetClock::time_point getSignTime() const; - NetClock::time_point + [[nodiscard]] NetClock::time_point getSeenTime() const noexcept; - PublicKey const& + [[nodiscard]] PublicKey const& getSignerPublic() const noexcept; - NodeID const& + [[nodiscard]] NodeID const& getNodeID() const noexcept; - bool + [[nodiscard]] bool isValid() const noexcept; - bool + [[nodiscard]] bool isFull() const noexcept; - bool + [[nodiscard]] bool isTrusted() const noexcept; - uint256 + [[nodiscard]] uint256 getSigningHash() const; void @@ -112,13 +127,13 @@ public: void setSeen(NetClock::time_point s); - Blob + [[nodiscard]] Blob getSerialized() const; - Blob + [[nodiscard]] Blob getSignature() const; - std::string + [[nodiscard]] std::string render() const { std::stringstream ss; diff --git a/include/xrpl/protocol/STVector256.h b/include/xrpl/protocol/STVector256.h index 5c454b6be0..46a1abc713 100644 --- a/include/xrpl/protocol/STVector256.h +++ b/include/xrpl/protocol/STVector256.h @@ -1,9 +1,15 @@ #pragma once #include +#include +#include +#include #include -#include -#include +#include + +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/STXChainBridge.h b/include/xrpl/protocol/STXChainBridge.h index 292ffe2767..24d64ef02b 100644 --- a/include/xrpl/protocol/STXChainBridge.h +++ b/include/xrpl/protocol/STXChainBridge.h @@ -1,9 +1,19 @@ #pragma once #include +#include +#include +#include +#include #include #include #include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/SecretKey.h b/include/xrpl/protocol/SecretKey.h index 712b095f81..8a0d917ab4 100644 --- a/include/xrpl/protocol/SecretKey.h +++ b/include/xrpl/protocol/SecretKey.h @@ -2,14 +2,18 @@ #include #include +#include #include #include #include #include #include +#include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Seed.h b/include/xrpl/protocol/Seed.h index 0b93b84516..a669f52079 100644 --- a/include/xrpl/protocol/Seed.h +++ b/include/xrpl/protocol/Seed.h @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index ffe9afabe8..1d0453d6aa 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -6,13 +6,14 @@ #include #include #include -#include #include #include #include #include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index 0b5b5d7239..18f085352d 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -1,9 +1,13 @@ #pragma once +#include #include +#include #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index 1cc35a0f31..b31dd0cd42 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -1,9 +1,12 @@ #pragma once +#include #include #include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 4ee084e714..b29cb11e15 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index 77ab069feb..36eb6d0889 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -1,7 +1,9 @@ #pragma once #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 6895350e9f..813f1b1615 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -1,12 +1,21 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index b38c544096..1a3cb96691 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -1,9 +1,11 @@ #pragma once -#include #include #include -#include + +#include +#include +#include namespace xrpl { namespace detail { diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 39f745e84e..8b418704d6 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -3,14 +3,18 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 1aec5fe549..8f1c7a4ce3 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -1,20 +1,20 @@ #pragma once #include +#include #include -#include #include #include -#include +#include #include #include -#include #include #include #include -#include +#include +#include #include #include diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index f09ddc337a..3190980ecb 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,7 +11,11 @@ #include #include +#include +#include #include +#include +#include #include #include diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 71077d4b33..6853166174 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -5,6 +5,7 @@ #include #include +#include #include namespace xrpl::detail { diff --git a/include/xrpl/protocol/detail/b58_utils.h b/include/xrpl/protocol/detail/b58_utils.h index e800dbda06..f7aa4e74b4 100644 --- a/include/xrpl/protocol/detail/b58_utils.h +++ b/include/xrpl/protocol/detail/b58_utils.h @@ -1,12 +1,14 @@ #pragma once -#include #include #include #include #include +#include +#include +#include #include #include #include diff --git a/include/xrpl/protocol/detail/token_errors.h b/include/xrpl/protocol/detail/token_errors.h index 97db9288f9..99a04596bb 100644 --- a/include/xrpl/protocol/detail/token_errors.h +++ b/include/xrpl/protocol/detail/token_errors.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include namespace xrpl { enum class TokenCodecErrc { diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h index 721ce60767..c1e70cada2 100644 --- a/include/xrpl/protocol/digest.h +++ b/include/xrpl/protocol/digest.h @@ -6,6 +6,9 @@ #include #include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/protocol/json_get_or_throw.h b/include/xrpl/protocol/json_get_or_throw.h index 9399f25827..7c7d9afe6c 100644 --- a/include/xrpl/protocol/json_get_or_throw.h +++ b/include/xrpl/protocol/json_get_or_throw.h @@ -7,8 +7,12 @@ #include #include +#include #include #include +#include +#include +#include namespace json { struct JsonMissingKeyError : std::exception diff --git a/include/xrpl/protocol/messages.h b/include/xrpl/protocol/messages.h index e6654cc183..dcbe8649c0 100644 --- a/include/xrpl/protocol/messages.h +++ b/include/xrpl/protocol/messages.h @@ -10,5 +10,3 @@ #ifdef TYPE_BOOL #undef TYPE_BOOL #endif - -#include diff --git a/include/xrpl/protocol/serialize.h b/include/xrpl/protocol/serialize.h index 5b84a28b5d..e758b57d49 100644 --- a/include/xrpl/protocol/serialize.h +++ b/include/xrpl/protocol/serialize.h @@ -1,9 +1,12 @@ #pragma once +#include #include #include #include +#include + namespace xrpl { /** Serialize an object to a blob. */ diff --git a/include/xrpl/protocol/st.h b/include/xrpl/protocol/st.h deleted file mode 100644 index 61571196f2..0000000000 --- a/include/xrpl/protocol/st.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/include/xrpl/protocol/tokens.h b/include/xrpl/protocol/tokens.h index 67cb25c7fb..bce7e7b5d6 100644 --- a/include/xrpl/protocol/tokens.h +++ b/include/xrpl/protocol/tokens.h @@ -1,14 +1,15 @@ #pragma once -#include #include +#include #include #include #include #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/rdb/DatabaseCon.h b/include/xrpl/rdb/DatabaseCon.h index 08376f0e71..90aed04337 100644 --- a/include/xrpl/rdb/DatabaseCon.h +++ b/include/xrpl/rdb/DatabaseCon.h @@ -1,16 +1,24 @@ #pragma once +#include #include #include #include -#include #include #include +#include + +#include +#include +#include +#include +#include #include -#include #include +#include +#include namespace soci { class session; diff --git a/include/xrpl/rdb/RelationalDatabase.h b/include/xrpl/rdb/RelationalDatabase.h index a38d6a997f..91c282ff16 100644 --- a/include/xrpl/rdb/RelationalDatabase.h +++ b/include/xrpl/rdb/RelationalDatabase.h @@ -1,18 +1,34 @@ #pragma once +#include +#include #include +#include #include #include +#include #include #include #include +#include #include #include -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace xrpl { class Transaction; diff --git a/include/xrpl/rdb/SociDB.h b/include/xrpl/rdb/SociDB.h index e87aa2a182..0f427bd18b 100644 --- a/include/xrpl/rdb/SociDB.h +++ b/include/xrpl/rdb/SociDB.h @@ -8,6 +8,10 @@ This module requires the @ref beast_sqlite external module. */ +#include +#include + +#include #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" @@ -17,7 +21,6 @@ #include #define SOCI_USE_BOOST -#include #include #include diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h index 3fd8b02e83..394d641e6c 100644 --- a/include/xrpl/resource/Charge.h +++ b/include/xrpl/resource/Charge.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include namespace xrpl::Resource { diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h index 14935bb3f1..18207832ac 100644 --- a/include/xrpl/resource/Consumer.h +++ b/include/xrpl/resource/Consumer.h @@ -1,10 +1,13 @@ #pragma once -#include +#include #include #include #include +#include +#include + namespace xrpl::Resource { struct Entry; diff --git a/include/xrpl/resource/ResourceManager.h b/include/xrpl/resource/ResourceManager.h index 42b2a47126..13e0d09343 100644 --- a/include/xrpl/resource/ResourceManager.h +++ b/include/xrpl/resource/ResourceManager.h @@ -10,6 +10,10 @@ #include +#include +#include +#include + namespace xrpl::Resource { /** Tracks load and resource consumption. */ diff --git a/include/xrpl/resource/Types.h b/include/xrpl/resource/Types.h deleted file mode 100644 index fb71cffe37..0000000000 --- a/include/xrpl/resource/Types.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace xrpl { -namespace Resource { - -struct Key; -struct Entry; - -} // namespace Resource -} // namespace xrpl diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h index 361c8409c8..6f44ac2c29 100644 --- a/include/xrpl/resource/detail/Entry.h +++ b/include/xrpl/resource/detail/Entry.h @@ -3,10 +3,15 @@ #include #include #include -#include +#include #include #include +#include +#include +#include +#include + namespace xrpl::Resource { using clock_type = beast::AbstractClock; diff --git a/include/xrpl/resource/detail/Import.h b/include/xrpl/resource/detail/Import.h index 6e2a7b9e7c..a3df6fd73b 100644 --- a/include/xrpl/resource/detail/Import.h +++ b/include/xrpl/resource/detail/Import.h @@ -3,6 +3,8 @@ #include #include +#include + namespace xrpl::Resource { /** A set of imported consumer data from a gossip origin. */ diff --git a/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h index 7d24b33955..a0f11422a7 100644 --- a/include/xrpl/resource/detail/Key.h +++ b/include/xrpl/resource/detail/Key.h @@ -1,9 +1,10 @@ #pragma once +#include #include -#include #include +#include #include namespace xrpl::Resource { diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index b11ac100f5..6b1c5e7397 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -4,16 +4,25 @@ #include #include #include -#include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include #include #include #include #include +#include +#include +#include namespace xrpl::Resource { diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index f316885fd6..d708a456c4 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -1,13 +1,22 @@ #pragma once #include +#include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include + namespace xrpl { // Operations that clients may wish to perform against the network diff --git a/include/xrpl/server/LoadFeeTrack.h b/include/xrpl/server/LoadFeeTrack.h index aa32e70ac8..a19ca063d5 100644 --- a/include/xrpl/server/LoadFeeTrack.h +++ b/include/xrpl/server/LoadFeeTrack.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index eed1c14dae..eb625d220a 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -1,14 +1,22 @@ #pragma once +#include +#include #include +#include #include #include #include +#include +#include +#include #include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/server/NetworkOPs.h b/include/xrpl/server/NetworkOPs.h index 785d808935..ed2dbbb220 100644 --- a/include/xrpl/server/NetworkOPs.h +++ b/include/xrpl/server/NetworkOPs.h @@ -1,16 +1,24 @@ #pragma once -#include +#include +#include #include +#include +#include +#include #include #include -#include #include -#include #include +#include +#include +#include #include +#include +#include +#include namespace xrpl { @@ -25,6 +33,7 @@ class Transaction; class ValidatorKeys; class CanonicalTXSet; class RCLCxPeerPos; +class SHAMap; // This is the primary interface into the "client" portion of the program. // Code that wants to do normal operations on the network such as diff --git a/include/xrpl/server/Port.h b/include/xrpl/server/Port.h index fd773c78fc..c48a2546c1 100644 --- a/include/xrpl/server/Port.h +++ b/include/xrpl/server/Port.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -11,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/include/xrpl/server/Server.h b/include/xrpl/server/Server.h index 4d97fe9877..956a414be8 100644 --- a/include/xrpl/server/Server.h +++ b/include/xrpl/server/Server.h @@ -1,12 +1,12 @@ #pragma once #include -#include -#include #include #include +#include + namespace xrpl { /** Create the HTTP server using the specified handler. */ diff --git a/include/xrpl/server/Session.h b/include/xrpl/server/Session.h index 151e57e7f2..266570862a 100644 --- a/include/xrpl/server/Session.h +++ b/include/xrpl/server/Session.h @@ -2,16 +2,16 @@ #include #include +#include +#include #include #include #include -#include +#include #include -#include #include -#include namespace xrpl { diff --git a/include/xrpl/server/SimpleWriter.h b/include/xrpl/server/SimpleWriter.h index dfb0b165e8..996403eafb 100644 --- a/include/xrpl/server/SimpleWriter.h +++ b/include/xrpl/server/SimpleWriter.h @@ -7,7 +7,10 @@ #include #include -#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/server/State.h b/include/xrpl/server/State.h index c3cc4f609c..8590f6e18f 100644 --- a/include/xrpl/server/State.h +++ b/include/xrpl/server/State.h @@ -2,10 +2,12 @@ #include #include -#include +#include #include +#include + namespace xrpl { struct SavedState diff --git a/include/xrpl/server/Vacuum.h b/include/xrpl/server/Vacuum.h index 5f80eced87..7930265e76 100644 --- a/include/xrpl/server/Vacuum.h +++ b/include/xrpl/server/Vacuum.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace xrpl { diff --git a/include/xrpl/server/WSSession.h b/include/xrpl/server/WSSession.h index b2fa52c859..0087f9f50f 100644 --- a/include/xrpl/server/WSSession.h +++ b/include/xrpl/server/WSSession.h @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -11,7 +10,9 @@ #include #include +#include #include +#include #include #include #include diff --git a/include/xrpl/server/Wallet.h b/include/xrpl/server/Wallet.h index c15014b1ef..eea44db200 100644 --- a/include/xrpl/server/Wallet.h +++ b/include/xrpl/server/Wallet.h @@ -1,9 +1,21 @@ #pragma once +#include +#include +#include +#include #include +#include +#include #include #include +#include +#include +#include +#include +#include + namespace xrpl { /** diff --git a/include/xrpl/server/Writer.h b/include/xrpl/server/Writer.h index 05fea863be..fe9d9519e1 100644 --- a/include/xrpl/server/Writer.h +++ b/include/xrpl/server/Writer.h @@ -2,6 +2,7 @@ #include +#include #include #include diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index d26817bc45..f096d3c5a9 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -2,8 +2,12 @@ #include #include -#include +#include +#include +#include +#include #include +#include #include #include @@ -20,9 +24,12 @@ #include #include +#include +#include #include #include #include +#include #include #include @@ -392,7 +399,7 @@ BaseHTTPPeer::write(void const* buf, std::size_t bytes) if ([&] { std::scoped_lock const lock(mutex_); wq_.emplace_back(buf, bytes); - return wq_.size() == 1 && wq2_.size() == 0; + return wq_.size() == 1 && wq2_.empty(); }()) { if (!strand_.running_in_this_thread()) diff --git a/include/xrpl/server/detail/BasePeer.h b/include/xrpl/server/detail/BasePeer.h index edde28981c..5301a2f018 100644 --- a/include/xrpl/server/detail/BasePeer.h +++ b/include/xrpl/server/detail/BasePeer.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include #include #include @@ -9,6 +9,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index 13225dcba1..1b6a9faca3 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -1,10 +1,13 @@ #pragma once #include +#include #include #include #include #include +#include +#include #include #include #include @@ -16,8 +19,13 @@ #include #include +#include #include +#include #include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index 79d36cae0c..09b0adc5f2 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -19,6 +21,9 @@ #include #include +#include +#include + #if !BOOST_OS_WINDOWS #include diff --git a/include/xrpl/server/detail/JSONRPCUtil.h b/include/xrpl/server/detail/JSONRPCUtil.h index b2e87f5332..1c157708c2 100644 --- a/include/xrpl/server/detail/JSONRPCUtil.h +++ b/include/xrpl/server/detail/JSONRPCUtil.h @@ -2,7 +2,8 @@ #include #include -#include + +#include namespace xrpl { diff --git a/include/xrpl/server/detail/LowestLayer.h b/include/xrpl/server/detail/LowestLayer.h index d28510d1a5..4daf91cd4c 100644 --- a/include/xrpl/server/detail/LowestLayer.h +++ b/include/xrpl/server/detail/LowestLayer.h @@ -1,23 +1,14 @@ #pragma once -#if BOOST_VERSION >= 107000 #include -#else -#include -#endif namespace xrpl { -// Before boost 1.70, get_lowest_layer required an explicit template parameter template decltype(auto) getLowestLayer(T& t) noexcept { -#if BOOST_VERSION >= 107000 return boost::beast::get_lowest_layer(t); -#else - return t.lowest_layer(); -#endif } } // namespace xrpl diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h b/include/xrpl/server/detail/PlainHTTPPeer.h index 791ab91acf..b89272fe9e 100644 --- a/include/xrpl/server/detail/PlainHTTPPeer.h +++ b/include/xrpl/server/detail/PlainHTTPPeer.h @@ -1,12 +1,17 @@ #pragma once #include +#include +#include +#include #include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/PlainWSPeer.h b/include/xrpl/server/detail/PlainWSPeer.h index 593973a3ed..279e256f51 100644 --- a/include/xrpl/server/detail/PlainWSPeer.h +++ b/include/xrpl/server/detail/PlainWSPeer.h @@ -1,10 +1,14 @@ #pragma once +#include +#include #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h b/include/xrpl/server/detail/SSLHTTPPeer.h index f3b047150f..ea2108b917 100644 --- a/include/xrpl/server/detail/SSLHTTPPeer.h +++ b/include/xrpl/server/detail/SSLHTTPPeer.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include +#include #include #include @@ -9,7 +13,9 @@ #include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/SSLWSPeer.h b/include/xrpl/server/detail/SSLWSPeer.h index cb03d5a796..fe522e3c9a 100644 --- a/include/xrpl/server/detail/SSLWSPeer.h +++ b/include/xrpl/server/detail/SSLWSPeer.h @@ -1,7 +1,9 @@ #pragma once -#include +#include +#include #include +#include #include #include @@ -9,8 +11,11 @@ #include #include #include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/ServerImpl.h b/include/xrpl/server/detail/ServerImpl.h index 48ffa26cfe..df2bba0284 100644 --- a/include/xrpl/server/detail/ServerImpl.h +++ b/include/xrpl/server/detail/ServerImpl.h @@ -1,7 +1,8 @@ #pragma once -#include -#include +#include +#include +#include #include #include @@ -11,9 +12,15 @@ #include #include +#include +#include #include #include +#include +#include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/server/detail/Spawn.h b/include/xrpl/server/detail/Spawn.h index 2560a2718b..1ba32c2f0b 100644 --- a/include/xrpl/server/detail/Spawn.h +++ b/include/xrpl/server/detail/Spawn.h @@ -6,6 +6,7 @@ #include #include +#include #include namespace xrpl::util { diff --git a/include/xrpl/server/detail/io_list.h b/include/xrpl/server/detail/io_list.h index 4daa23fb7e..0153bd3457 100644 --- a/include/xrpl/server/detail/io_list.h +++ b/include/xrpl/server/detail/io_list.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include diff --git a/include/xrpl/shamap/Family.h b/include/xrpl/shamap/Family.h index b9dd85443a..c5bf953bfd 100644 --- a/include/xrpl/shamap/Family.h +++ b/include/xrpl/shamap/Family.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/shamap/FullBelowCache.h b/include/xrpl/shamap/FullBelowCache.h index e9fd04ac58..dc597278df 100644 --- a/include/xrpl/shamap/FullBelowCache.h +++ b/include/xrpl/shamap/FullBelowCache.h @@ -4,9 +4,13 @@ #include #include #include +#include #include #include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index 3d08318cf6..d49e323b3f 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -1,11 +1,14 @@ #pragma once +#include #include -#include +#include +#include +#include #include #include -#include #include +#include #include #include #include @@ -14,8 +17,20 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h index e388d205d1..ee81107f61 100644 --- a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h +++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h @@ -1,10 +1,17 @@ #pragma once #include +#include +#include #include +#include #include #include #include +#include + +#include +#include namespace xrpl { diff --git a/include/xrpl/shamap/SHAMapInnerNode.h b/include/xrpl/shamap/SHAMapInnerNode.h index cafb498218..0fb4e24077 100644 --- a/include/xrpl/shamap/SHAMapInnerNode.h +++ b/include/xrpl/shamap/SHAMapInnerNode.h @@ -1,7 +1,11 @@ #pragma once -#include +#include +#include +#include +#include #include +#include #include #include diff --git a/include/xrpl/shamap/SHAMapItem.h b/include/xrpl/shamap/SHAMapItem.h index 41558197bf..846f0bab4e 100644 --- a/include/xrpl/shamap/SHAMapItem.h +++ b/include/xrpl/shamap/SHAMapItem.h @@ -5,10 +5,18 @@ #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include + namespace xrpl { // an item stored in a SHAMap diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h index af5ed29702..112c243131 100644 --- a/include/xrpl/shamap/SHAMapLeafNode.h +++ b/include/xrpl/shamap/SHAMapLeafNode.h @@ -1,9 +1,12 @@ #pragma once +#include #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/shamap/SHAMapMissingNode.h b/include/xrpl/shamap/SHAMapMissingNode.h index 0bc072463e..3168d0eff1 100644 --- a/include/xrpl/shamap/SHAMapMissingNode.h +++ b/include/xrpl/shamap/SHAMapMissingNode.h @@ -1,9 +1,9 @@ #pragma once +#include #include -#include +#include -#include #include #include #include diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h index 248c9cb80b..b812c10ca9 100644 --- a/include/xrpl/shamap/SHAMapNodeID.h +++ b/include/xrpl/shamap/SHAMapNodeID.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include #include diff --git a/include/xrpl/shamap/SHAMapSyncFilter.h b/include/xrpl/shamap/SHAMapSyncFilter.h index 4104220a3f..b6ce175915 100644 --- a/include/xrpl/shamap/SHAMapSyncFilter.h +++ b/include/xrpl/shamap/SHAMapSyncFilter.h @@ -1,7 +1,10 @@ #pragma once +#include +#include #include +#include #include /** Callback for filtering SHAMap during sync. */ diff --git a/include/xrpl/shamap/SHAMapTreeNode.h b/include/xrpl/shamap/SHAMapTreeNode.h index 5cca2ea41a..1eebbaa17f 100644 --- a/include/xrpl/shamap/SHAMapTreeNode.h +++ b/include/xrpl/shamap/SHAMapTreeNode.h @@ -3,8 +3,8 @@ #include #include #include +#include #include -#include #include #include diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h b/include/xrpl/shamap/SHAMapTxLeafNode.h index 49f4f90906..86186434f8 100644 --- a/include/xrpl/shamap/SHAMapTxLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxLeafNode.h @@ -1,10 +1,17 @@ #pragma once #include +#include +#include #include +#include #include #include #include +#include + +#include +#include namespace xrpl { diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h index 3f4163ac41..9e4573d45b 100644 --- a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h +++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h @@ -1,10 +1,17 @@ #pragma once #include +#include +#include #include +#include #include #include #include +#include + +#include +#include namespace xrpl { diff --git a/include/xrpl/shamap/TreeNodeCache.h b/include/xrpl/shamap/TreeNodeCache.h index 2d5782c7e9..bff03a76e2 100644 --- a/include/xrpl/shamap/TreeNodeCache.h +++ b/include/xrpl/shamap/TreeNodeCache.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/shamap/detail/TaggedPointer.h b/include/xrpl/shamap/detail/TaggedPointer.h index 5eb3863de0..79f3464d5b 100644 --- a/include/xrpl/shamap/detail/TaggedPointer.h +++ b/include/xrpl/shamap/detail/TaggedPointer.h @@ -1,11 +1,14 @@ #pragma once -#include +#include #include -#include +#include #include #include +#include +#include +#include // IWYU pragma: keep namespace xrpl { diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index c98b3f82c5..ca778387cd 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -1,12 +1,23 @@ #pragma once +#include #include +#include #include +#include #include +#include +#include +#include #include +#include +#include #include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/SignerEntries.h b/include/xrpl/tx/SignerEntries.h index af0c9b9d28..c8481f6062 100644 --- a/include/xrpl/tx/SignerEntries.h +++ b/include/xrpl/tx/SignerEntries.h @@ -1,13 +1,15 @@ #pragma once +#include #include // beast::Journal -#include // temMALFORMED -#include // AccountID -#include // NotTEC +#include +#include // temMALFORMED +#include #include #include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 2b50cfa6e7..bc5e8c80e7 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -1,14 +1,30 @@ #pragma once +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include #include #include +#include #include +#include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/apply.h b/include/xrpl/tx/apply.h index 49b30fea02..55d365c31b 100644 --- a/include/xrpl/tx/apply.h +++ b/include/xrpl/tx/apply.h @@ -1,10 +1,14 @@ #pragma once +#include #include -#include +#include +#include +#include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/applySteps.h b/include/xrpl/tx/applySteps.h index 897cf500d6..3298e49192 100644 --- a/include/xrpl/tx/applySteps.h +++ b/include/xrpl/tx/applySteps.h @@ -1,7 +1,19 @@ #pragma once +#include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/tx/invariants/AMMInvariant.h b/include/xrpl/tx/invariants/AMMInvariant.h index 4b56370774..29d962fb79 100644 --- a/include/xrpl/tx/invariants/AMMInvariant.h +++ b/include/xrpl/tx/invariants/AMMInvariant.h @@ -2,9 +2,12 @@ #include #include +#include #include +#include #include #include +#include #include diff --git a/include/xrpl/tx/invariants/DirectoryInvariant.h b/include/xrpl/tx/invariants/DirectoryInvariant.h index 96643ea465..d18ac539cc 100644 --- a/include/xrpl/tx/invariants/DirectoryInvariant.h +++ b/include/xrpl/tx/invariants/DirectoryInvariant.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include #include #include diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h index a76eb66497..4b3e9beec4 100644 --- a/include/xrpl/tx/invariants/FreezeInvariant.h +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -2,10 +2,13 @@ #include #include +#include #include #include +#include #include #include +#include #include #include diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index ba91cba064..d19f34635d 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -1,10 +1,11 @@ #pragma once -#include #include #include +#include #include #include +#include #include #include #include @@ -17,7 +18,11 @@ #include #include +#include +#include #include +#include +#include namespace xrpl { diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h index e55419dfc1..b2f1c62a54 100644 --- a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/include/xrpl/tx/invariants/LoanBrokerInvariant.h b/include/xrpl/tx/invariants/LoanBrokerInvariant.h index 684bbff423..979f57de35 100644 --- a/include/xrpl/tx/invariants/LoanBrokerInvariant.h +++ b/include/xrpl/tx/invariants/LoanBrokerInvariant.h @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h index 3f408d169a..0648881423 100644 --- a/include/xrpl/tx/invariants/LoanInvariant.h +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h index aa90f1b8ef..1d39fd13d8 100644 --- a/include/xrpl/tx/invariants/MPTInvariant.h +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -1,13 +1,21 @@ #pragma once +#include +#include #include #include +#include #include #include #include +#include +#include +#include #include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/invariants/NFTInvariant.h b/include/xrpl/tx/invariants/NFTInvariant.h index 698df05247..11bdf5a569 100644 --- a/include/xrpl/tx/invariants/NFTInvariant.h +++ b/include/xrpl/tx/invariants/NFTInvariant.h @@ -1,10 +1,11 @@ #pragma once -#include #include #include +#include #include #include +#include #include diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h index 2ec22ded88..2763b80a94 100644 --- a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h @@ -1,10 +1,13 @@ #pragma once +#include #include #include #include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h index 19edcc0b39..fae7c24d25 100644 --- a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h +++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h index 2a9ffc8282..bc8246b234 100644 --- a/include/xrpl/tx/invariants/VaultInvariant.h +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -3,13 +3,18 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include #include #include +#include #include #include #include diff --git a/include/xrpl/tx/paths/AMMLiquidity.h b/include/xrpl/tx/paths/AMMLiquidity.h index 9abe37f868..bf62155b61 100644 --- a/include/xrpl/tx/paths/AMMLiquidity.h +++ b/include/xrpl/tx/paths/AMMLiquidity.h @@ -1,13 +1,17 @@ #pragma once -#include +#include +#include #include -#include -#include +#include #include #include +#include #include +#include +#include + namespace xrpl { template diff --git a/include/xrpl/tx/paths/AMMOffer.h b/include/xrpl/tx/paths/AMMOffer.h index 21c45c36a3..40a9cf40b3 100644 --- a/include/xrpl/tx/paths/AMMOffer.h +++ b/include/xrpl/tx/paths/AMMOffer.h @@ -1,12 +1,18 @@ #pragma once +#include +#include #include -#include #include +#include #include #include #include +#include +#include +#include + namespace xrpl { template diff --git a/include/xrpl/tx/paths/BookTip.h b/include/xrpl/tx/paths/BookTip.h index c4bdb0415c..bad007ca5b 100644 --- a/include/xrpl/tx/paths/BookTip.h +++ b/include/xrpl/tx/paths/BookTip.h @@ -1,8 +1,11 @@ #pragma once -#include -#include +#include +#include +#include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/paths/Flow.h b/include/xrpl/tx/paths/Flow.h index c746249866..f73b9a3440 100644 --- a/include/xrpl/tx/paths/Flow.h +++ b/include/xrpl/tx/paths/Flow.h @@ -1,9 +1,16 @@ #pragma once +#include +#include +#include #include +#include +#include #include #include +#include + namespace xrpl { namespace path::detail { diff --git a/include/xrpl/tx/paths/Offer.h b/include/xrpl/tx/paths/Offer.h index 2dab5bcebf..164a933ba1 100644 --- a/include/xrpl/tx/paths/Offer.h +++ b/include/xrpl/tx/paths/Offer.h @@ -1,16 +1,26 @@ #pragma once #include +#include #include -#include +#include +#include +#include #include +#include #include +#include #include #include #include #include +#include +#include +#include +#include #include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/paths/OfferStream.h b/include/xrpl/tx/paths/OfferStream.h index 69409b9ef7..98c58876c9 100644 --- a/include/xrpl/tx/paths/OfferStream.h +++ b/include/xrpl/tx/paths/OfferStream.h @@ -1,15 +1,20 @@ #pragma once #include +#include #include #include -#include +#include +#include #include #include #include #include +#include +#include + namespace xrpl { template diff --git a/include/xrpl/tx/paths/RippleCalc.h b/include/xrpl/tx/paths/RippleCalc.h index c747787820..62c966d384 100644 --- a/include/xrpl/tx/paths/RippleCalc.h +++ b/include/xrpl/tx/paths/RippleCalc.h @@ -1,12 +1,17 @@ #pragma once +#include #include #include +#include #include +#include #include #include +#include + namespace xrpl { class Config; namespace path { diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h b/include/xrpl/tx/paths/detail/EitherAmount.h index ffd90751b8..68ad90d2d4 100644 --- a/include/xrpl/tx/paths/detail/EitherAmount.h +++ b/include/xrpl/tx/paths/detail/EitherAmount.h @@ -1,10 +1,15 @@ #pragma once +#include #include #include -#include +#include // IWYU pragma: keep #include +#include +#include +#include + namespace xrpl { struct EitherAmount diff --git a/include/xrpl/tx/paths/detail/FlowDebugInfo.h b/include/xrpl/tx/paths/detail/FlowDebugInfo.h index 1ccfba34ce..48066840f5 100644 --- a/include/xrpl/tx/paths/detail/FlowDebugInfo.h +++ b/include/xrpl/tx/paths/detail/FlowDebugInfo.h @@ -1,14 +1,22 @@ #pragma once -#include +#include +#include #include +#include +#include #include +#include #include #include -#include +#include #include +#include +#include +#include +#include namespace xrpl::path::detail { // Track performance information of a single payment diff --git a/include/xrpl/tx/paths/detail/StepChecks.h b/include/xrpl/tx/paths/detail/StepChecks.h index 4955c4f8e6..f62613e95c 100644 --- a/include/xrpl/tx/paths/detail/StepChecks.h +++ b/include/xrpl/tx/paths/detail/StepChecks.h @@ -2,9 +2,15 @@ #include #include +#include #include #include #include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h index 9909c4bdcd..7eb6b938a5 100644 --- a/include/xrpl/tx/paths/detail/Steps.h +++ b/include/xrpl/tx/paths/detail/Steps.h @@ -1,16 +1,30 @@ #pragma once -#include #include +#include +#include +#include +#include #include #include #include +#include #include +#include #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include namespace xrpl { class PaymentSandbox; diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index 4d988a4c7e..fe657b2100 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -1,12 +1,21 @@ #pragma once #include -#include +#include +#include +#include +#include #include #include -#include +#include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -17,8 +26,16 @@ #include #include +#include +#include #include +#include #include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/account/AccountDelete.h b/include/xrpl/tx/transactors/account/AccountDelete.h index 16661a4b7c..e69d948bb8 100644 --- a/include/xrpl/tx/transactors/account/AccountDelete.h +++ b/include/xrpl/tx/transactors/account/AccountDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/account/AccountSet.h b/include/xrpl/tx/transactors/account/AccountSet.h index 91b38e7968..4326621f3d 100644 --- a/include/xrpl/tx/transactors/account/AccountSet.h +++ b/include/xrpl/tx/transactors/account/AccountSet.h @@ -1,8 +1,16 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class AccountSet : public Transactor diff --git a/include/xrpl/tx/transactors/account/SetRegularKey.h b/include/xrpl/tx/transactors/account/SetRegularKey.h index 6ff9c5aa52..90e4cd96cd 100644 --- a/include/xrpl/tx/transactors/account/SetRegularKey.h +++ b/include/xrpl/tx/transactors/account/SetRegularKey.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/account/SignerListSet.h b/include/xrpl/tx/transactors/account/SignerListSet.h index 46e3191323..da2274a1de 100644 --- a/include/xrpl/tx/transactors/account/SignerListSet.h +++ b/include/xrpl/tx/transactors/account/SignerListSet.h @@ -1,11 +1,20 @@ #pragma once +#include +#include +#include +#include +#include #include #include +#include +#include +#include #include #include #include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/bridge/XChainBridge.h b/include/xrpl/tx/transactors/bridge/XChainBridge.h index 58a546de2f..24f49eb340 100644 --- a/include/xrpl/tx/transactors/bridge/XChainBridge.h +++ b/include/xrpl/tx/transactors/bridge/XChainBridge.h @@ -1,8 +1,17 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl { constexpr size_t kXbridgeMaxAccountCreateClaims = 128; diff --git a/include/xrpl/tx/transactors/check/CheckCancel.h b/include/xrpl/tx/transactors/check/CheckCancel.h index b8e8b6c52d..b54d1e6c84 100644 --- a/include/xrpl/tx/transactors/check/CheckCancel.h +++ b/include/xrpl/tx/transactors/check/CheckCancel.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/check/CheckCash.h b/include/xrpl/tx/transactors/check/CheckCash.h index 7d4e615cfd..4656f941f4 100644 --- a/include/xrpl/tx/transactors/check/CheckCash.h +++ b/include/xrpl/tx/transactors/check/CheckCash.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/check/CheckCreate.h b/include/xrpl/tx/transactors/check/CheckCreate.h index 178fe4707c..50a1f076e0 100644 --- a/include/xrpl/tx/transactors/check/CheckCreate.h +++ b/include/xrpl/tx/transactors/check/CheckCreate.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/credentials/CredentialAccept.h b/include/xrpl/tx/transactors/credentials/CredentialAccept.h index 8630ac3f7f..b9dfa684e7 100644 --- a/include/xrpl/tx/transactors/credentials/CredentialAccept.h +++ b/include/xrpl/tx/transactors/credentials/CredentialAccept.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class CredentialAccept : public Transactor diff --git a/include/xrpl/tx/transactors/credentials/CredentialCreate.h b/include/xrpl/tx/transactors/credentials/CredentialCreate.h index 91b5e829d3..ce9f9305be 100644 --- a/include/xrpl/tx/transactors/credentials/CredentialCreate.h +++ b/include/xrpl/tx/transactors/credentials/CredentialCreate.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class CredentialCreate : public Transactor diff --git a/include/xrpl/tx/transactors/credentials/CredentialDelete.h b/include/xrpl/tx/transactors/credentials/CredentialDelete.h index 70fe5cb3d0..2f151a9fa0 100644 --- a/include/xrpl/tx/transactors/credentials/CredentialDelete.h +++ b/include/xrpl/tx/transactors/credentials/CredentialDelete.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class CredentialDelete : public Transactor diff --git a/include/xrpl/tx/transactors/delegate/DelegateSet.h b/include/xrpl/tx/transactors/delegate/DelegateSet.h index a55524ecff..b7141276de 100644 --- a/include/xrpl/tx/transactors/delegate/DelegateSet.h +++ b/include/xrpl/tx/transactors/delegate/DelegateSet.h @@ -1,5 +1,13 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/AMMBid.h b/include/xrpl/tx/transactors/dex/AMMBid.h index dfa50d06ba..fa257696e8 100644 --- a/include/xrpl/tx/transactors/dex/AMMBid.h +++ b/include/xrpl/tx/transactors/dex/AMMBid.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/AMMClawback.h b/include/xrpl/tx/transactors/dex/AMMClawback.h index 6f31480490..54eb3c9f27 100644 --- a/include/xrpl/tx/transactors/dex/AMMClawback.h +++ b/include/xrpl/tx/transactors/dex/AMMClawback.h @@ -1,7 +1,20 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include + namespace xrpl { class Sandbox; class AMMClawback : public Transactor diff --git a/include/xrpl/tx/transactors/dex/AMMCreate.h b/include/xrpl/tx/transactors/dex/AMMCreate.h index 64d2a1e3b1..188af8d4ef 100644 --- a/include/xrpl/tx/transactors/dex/AMMCreate.h +++ b/include/xrpl/tx/transactors/dex/AMMCreate.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/AMMDelete.h b/include/xrpl/tx/transactors/dex/AMMDelete.h index d3e8cfeeb4..4a0905fe10 100644 --- a/include/xrpl/tx/transactors/dex/AMMDelete.h +++ b/include/xrpl/tx/transactors/dex/AMMDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/AMMDeposit.h b/include/xrpl/tx/transactors/dex/AMMDeposit.h index 453046ddad..b87db19b2d 100644 --- a/include/xrpl/tx/transactors/dex/AMMDeposit.h +++ b/include/xrpl/tx/transactors/dex/AMMDeposit.h @@ -1,7 +1,21 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include + namespace xrpl { class Sandbox; diff --git a/include/xrpl/tx/transactors/dex/AMMVote.h b/include/xrpl/tx/transactors/dex/AMMVote.h index 8defc1369e..10ad284bb3 100644 --- a/include/xrpl/tx/transactors/dex/AMMVote.h +++ b/include/xrpl/tx/transactors/dex/AMMVote.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 8b6d39349b..8f6700037d 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -1,9 +1,23 @@ #pragma once -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include + namespace xrpl { class Sandbox; diff --git a/include/xrpl/tx/transactors/dex/OfferCancel.h b/include/xrpl/tx/transactors/dex/OfferCancel.h index 2806b6942f..974478475c 100644 --- a/include/xrpl/tx/transactors/dex/OfferCancel.h +++ b/include/xrpl/tx/transactors/dex/OfferCancel.h @@ -1,6 +1,12 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/dex/OfferCreate.h b/include/xrpl/tx/transactors/dex/OfferCreate.h index 7faf613d3a..a3bf7626f9 100644 --- a/include/xrpl/tx/transactors/dex/OfferCreate.h +++ b/include/xrpl/tx/transactors/dex/OfferCreate.h @@ -1,8 +1,28 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + namespace xrpl { class PaymentSandbox; diff --git a/include/xrpl/tx/transactors/did/DIDDelete.h b/include/xrpl/tx/transactors/did/DIDDelete.h index 94615f51b6..c119b0348e 100644 --- a/include/xrpl/tx/transactors/did/DIDDelete.h +++ b/include/xrpl/tx/transactors/did/DIDDelete.h @@ -1,5 +1,15 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/did/DIDSet.h b/include/xrpl/tx/transactors/did/DIDSet.h index 8c84ea6c9b..481222243b 100644 --- a/include/xrpl/tx/transactors/did/DIDSet.h +++ b/include/xrpl/tx/transactors/did/DIDSet.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/escrow/EscrowCancel.h b/include/xrpl/tx/transactors/escrow/EscrowCancel.h index af09f70202..b52fb2a9fa 100644 --- a/include/xrpl/tx/transactors/escrow/EscrowCancel.h +++ b/include/xrpl/tx/transactors/escrow/EscrowCancel.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h b/include/xrpl/tx/transactors/escrow/EscrowCreate.h index 8800e97b80..b8e22aa24e 100644 --- a/include/xrpl/tx/transactors/escrow/EscrowCreate.h +++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/escrow/EscrowFinish.h b/include/xrpl/tx/transactors/escrow/EscrowFinish.h index 061fa0527c..07459bf959 100644 --- a/include/xrpl/tx/transactors/escrow/EscrowFinish.h +++ b/include/xrpl/tx/transactors/escrow/EscrowFinish.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h index 81ea97ce7e..cd72a01bc3 100644 --- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h index 43b932726b..03ed295766 100644 --- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h index a757ac51bf..1132211e58 100644 --- a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h index 0ce5f29387..7a9a529510 100644 --- a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h +++ b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h index 75175e2dd6..e7137c9311 100644 --- a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h +++ b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h @@ -1,7 +1,17 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class LoanBrokerSet : public Transactor diff --git a/include/xrpl/tx/transactors/lending/LoanDelete.h b/include/xrpl/tx/transactors/lending/LoanDelete.h index 9e8c3c172a..604388a2bd 100644 --- a/include/xrpl/tx/transactors/lending/LoanDelete.h +++ b/include/xrpl/tx/transactors/lending/LoanDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/lending/LoanManage.h b/include/xrpl/tx/transactors/lending/LoanManage.h index d2344c0ec0..c8a5584131 100644 --- a/include/xrpl/tx/transactors/lending/LoanManage.h +++ b/include/xrpl/tx/transactors/lending/LoanManage.h @@ -1,7 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class LoanManage : public Transactor diff --git a/include/xrpl/tx/transactors/lending/LoanPay.h b/include/xrpl/tx/transactors/lending/LoanPay.h index 9be9695c64..599bea6ae6 100644 --- a/include/xrpl/tx/transactors/lending/LoanPay.h +++ b/include/xrpl/tx/transactors/lending/LoanPay.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class LoanPay : public Transactor diff --git a/include/xrpl/tx/transactors/lending/LoanSet.h b/include/xrpl/tx/transactors/lending/LoanSet.h index d277629e44..fab489e3db 100644 --- a/include/xrpl/tx/transactors/lending/LoanSet.h +++ b/include/xrpl/tx/transactors/lending/LoanSet.h @@ -1,8 +1,19 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl { class LoanSet : public Transactor diff --git a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h index c5cd10fa6a..28037ae3b9 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h +++ b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h @@ -1,5 +1,15 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/nft/NFTokenBurn.h b/include/xrpl/tx/transactors/nft/NFTokenBurn.h index 849d09cb7e..a9640e3a9e 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenBurn.h +++ b/include/xrpl/tx/transactors/nft/NFTokenBurn.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h index a74a1c3e59..35c9067f63 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h +++ b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h index c874381dd0..f11ac5c373 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h +++ b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class NFTokenCreateOffer : public Transactor diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h b/include/xrpl/tx/transactors/nft/NFTokenMint.h index 9267e8e801..9d5874fa60 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenMint.h +++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h @@ -1,9 +1,19 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include + namespace xrpl { class NFTokenMint : public Transactor diff --git a/include/xrpl/tx/transactors/nft/NFTokenModify.h b/include/xrpl/tx/transactors/nft/NFTokenModify.h index 0d18e4a6d4..970914bbfe 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenModify.h +++ b/include/xrpl/tx/transactors/nft/NFTokenModify.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/oracle/OracleDelete.h b/include/xrpl/tx/transactors/oracle/OracleDelete.h index c16d5fb2a9..f9e9230527 100644 --- a/include/xrpl/tx/transactors/oracle/OracleDelete.h +++ b/include/xrpl/tx/transactors/oracle/OracleDelete.h @@ -1,5 +1,14 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h b/include/xrpl/tx/transactors/oracle/OracleSet.h index 831c11b8c4..e95970923b 100644 --- a/include/xrpl/tx/transactors/oracle/OracleSet.h +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/payment/DepositPreauth.h b/include/xrpl/tx/transactors/payment/DepositPreauth.h index 742b1ef3f7..b9b42ddac6 100644 --- a/include/xrpl/tx/transactors/payment/DepositPreauth.h +++ b/include/xrpl/tx/transactors/payment/DepositPreauth.h @@ -1,5 +1,14 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/payment/Payment.h b/include/xrpl/tx/transactors/payment/Payment.h index dd792aa1c2..1356a368e9 100644 --- a/include/xrpl/tx/transactors/payment/Payment.h +++ b/include/xrpl/tx/transactors/payment/Payment.h @@ -1,7 +1,19 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include + namespace xrpl { class Payment : public Transactor diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h index e13fea6d6c..85df2a02e7 100644 --- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class PaymentChannelClaim : public Transactor diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h index 56e984cd57..f0c9da6349 100644 --- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h index 272076ff20..377fbe9cf2 100644 --- a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h index 88883fb86f..5a07262a3b 100644 --- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h index 4afa8cef5a..38de800284 100644 --- a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h index 927a5b27cd..6540005877 100644 --- a/include/xrpl/tx/transactors/system/Batch.h +++ b/include/xrpl/tx/transactors/system/Batch.h @@ -1,7 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl { class Batch : public Transactor diff --git a/include/xrpl/tx/transactors/system/Change.h b/include/xrpl/tx/transactors/system/Change.h index 339723ae8e..b25208f075 100644 --- a/include/xrpl/tx/transactors/system/Change.h +++ b/include/xrpl/tx/transactors/system/Change.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/system/LedgerStateFix.h b/include/xrpl/tx/transactors/system/LedgerStateFix.h index 973f89faa9..dd91f6f367 100644 --- a/include/xrpl/tx/transactors/system/LedgerStateFix.h +++ b/include/xrpl/tx/transactors/system/LedgerStateFix.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class LedgerStateFix : public Transactor diff --git a/include/xrpl/tx/transactors/system/TicketCreate.h b/include/xrpl/tx/transactors/system/TicketCreate.h index 5783faa6d1..2a1036c732 100644 --- a/include/xrpl/tx/transactors/system/TicketCreate.h +++ b/include/xrpl/tx/transactors/system/TicketCreate.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class TicketCreate : public Transactor diff --git a/include/xrpl/tx/transactors/token/Clawback.h b/include/xrpl/tx/transactors/token/Clawback.h index ed90776e59..5eaa32fe48 100644 --- a/include/xrpl/tx/transactors/token/Clawback.h +++ b/include/xrpl/tx/transactors/token/Clawback.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h index ff6f76e8ea..21d95326c9 100644 --- a/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTClawback.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { /** diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h index 2e2591844f..6de2ce141f 100644 --- a/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvert.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { /** diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h index cb4b6295a0..e2ee844560 100644 --- a/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTConvertBack.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { /** diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h index 585c273e12..7c348d36a5 100644 --- a/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTMergeInbox.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { /** diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h index 72599ab987..aecf1ea5e2 100644 --- a/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTSend.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { /** diff --git a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h index e30d123e52..b9fd48abe3 100644 --- a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h +++ b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h @@ -1,7 +1,19 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl { struct MPTAuthorizeArgs diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index d946587e32..045911c7ae 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -1,9 +1,22 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h index 21032e7337..743e5a9502 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h index 428c573e2f..52f155e8fe 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class MPTokenIssuanceSet : public Transactor diff --git a/include/xrpl/tx/transactors/token/TrustSet.h b/include/xrpl/tx/transactors/token/TrustSet.h index d719f06326..fbf50f56ee 100644 --- a/include/xrpl/tx/transactors/token/TrustSet.h +++ b/include/xrpl/tx/transactors/token/TrustSet.h @@ -1,8 +1,18 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + namespace xrpl { class TrustSet : public Transactor diff --git a/include/xrpl/tx/transactors/vault/VaultClawback.h b/include/xrpl/tx/transactors/vault/VaultClawback.h index 2ff97abca2..ba0eb9f320 100644 --- a/include/xrpl/tx/transactors/vault/VaultClawback.h +++ b/include/xrpl/tx/transactors/vault/VaultClawback.h @@ -1,8 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/vault/VaultCreate.h b/include/xrpl/tx/transactors/vault/VaultCreate.h index 9b11f97957..f946a22c3a 100644 --- a/include/xrpl/tx/transactors/vault/VaultCreate.h +++ b/include/xrpl/tx/transactors/vault/VaultCreate.h @@ -1,7 +1,16 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include +#include + namespace xrpl { class VaultCreate : public Transactor diff --git a/include/xrpl/tx/transactors/vault/VaultDelete.h b/include/xrpl/tx/transactors/vault/VaultDelete.h index b8bb3c4096..ca73b1a685 100644 --- a/include/xrpl/tx/transactors/vault/VaultDelete.h +++ b/include/xrpl/tx/transactors/vault/VaultDelete.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/vault/VaultDeposit.h b/include/xrpl/tx/transactors/vault/VaultDeposit.h index 523b3f2e53..d49f504243 100644 --- a/include/xrpl/tx/transactors/vault/VaultDeposit.h +++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h b/include/xrpl/tx/transactors/vault/VaultSet.h index 5c362d5db4..835ed69610 100644 --- a/include/xrpl/tx/transactors/vault/VaultSet.h +++ b/include/xrpl/tx/transactors/vault/VaultSet.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h b/include/xrpl/tx/transactors/vault/VaultWithdraw.h index 7bbe06187d..22ad39d26d 100644 --- a/include/xrpl/tx/transactors/vault/VaultWithdraw.h +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp index 5c07bf68cc..ffb91db72d 100644 --- a/src/libxrpl/core/detail/JobQueue.cpp +++ b/src/libxrpl/core/detail/JobQueue.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 93875f497a..074a88428c 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -1,9 +1,11 @@ #include #include +#include // IWYU pragma: keep #include #include #include +#include // IWYU pragma: keep #include #include diff --git a/src/libxrpl/net/RegisterSSLCerts.cpp b/src/libxrpl/net/RegisterSSLCerts.cpp index ff59b5971e..2f005554a0 100644 --- a/src/libxrpl/net/RegisterSSLCerts.cpp +++ b/src/libxrpl/net/RegisterSSLCerts.cpp @@ -6,6 +6,8 @@ #include #if BOOST_OS_WINDOWS +#include + #include #include diff --git a/src/libxrpl/protocol/AMMCore.cpp b/src/libxrpl/protocol/AMMCore.cpp index e58ab29257..52b7fe82c2 100644 --- a/src/libxrpl/protocol/AMMCore.cpp +++ b/src/libxrpl/protocol/AMMCore.cpp @@ -12,6 +12,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 0baff1e33a..fe8a08c2ef 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index 80aa1b1f4e..2f3e25f823 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -3,6 +3,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 8ef7b9760f..fcc0077f6b 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/server/State.cpp b/src/libxrpl/server/State.cpp index d9793f53d0..ce17505ee6 100644 --- a/src/libxrpl/server/State.cpp +++ b/src/libxrpl/server/State.cpp @@ -7,6 +7,7 @@ #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp index f3ae9dc925..f3a7ff76ba 100644 --- a/src/libxrpl/server/Wallet.cpp +++ b/src/libxrpl/server/Wallet.cpp @@ -19,7 +19,9 @@ #include #include // IWYU pragma: keep +#include // IWYU pragma: keep #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp index 544a3af2dc..fcb53e7c8f 100644 --- a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp @@ -8,6 +8,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index ed5a22e3db..a915bb60d1 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index 1948f3803d..259b9d7864 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp b/src/libxrpl/tx/transactors/system/Batch.cpp index 16d27f98d9..ba54c14b1e 100644 --- a/src/libxrpl/tx/transactors/system/Batch.cpp +++ b/src/libxrpl/tx/transactors/system/Batch.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index 2e224a7eb1..39d722da65 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index da98c03380..bac1d8b61f 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index 8fb1eb31ab..5f02f1b079 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index 9958515c5f..800b14877a 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -12,11 +12,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 1f9290de63..11a0e5bab0 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -8,6 +8,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 7a149b2f64..c219bd8737 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/basics/hardened_hash_test.cpp b/src/test/basics/hardened_hash_test.cpp index 885ba9d60b..8b10967932 100644 --- a/src/test/basics/hardened_hash_test.cpp +++ b/src/test/basics/hardened_hash_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/src/test/beast/IPEndpointCommon.h b/src/test/beast/IPEndpointCommon.h index 0ff0da35be..45d036476c 100644 --- a/src/test/beast/IPEndpointCommon.h +++ b/src/test/beast/IPEndpointCommon.h @@ -1,8 +1,12 @@ #pragma once #include +#include +#include #include +#include + namespace beast::IP { inline Endpoint diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp index 413194491a..3dbaf74040 100644 --- a/src/test/beast/aged_associative_container_test.cpp +++ b/src/test/beast/aged_associative_container_test.cpp @@ -1,12 +1,12 @@ #include -#include -#include -#include -#include -#include -#include -#include -#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include #include @@ -18,9 +18,9 @@ #include #include #include -#include +#include // IWYU pragma: keep #include -#include +#include // IWYU pragma: keep #ifndef BEAST_AGED_UNORDERED_NO_ALLOC_DEFAULTCTOR #ifdef _MSC_VER diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index 57ff19fec5..d37569d6cd 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -11,6 +11,7 @@ #include #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include #include diff --git a/src/test/csf.h b/src/test/csf.h deleted file mode 100644 index d2ddbb460d..0000000000 --- a/src/test/csf.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include diff --git a/src/test/csf/BasicNetwork.h b/src/test/csf/BasicNetwork.h index 418cdcf289..4b88592128 100644 --- a/src/test/csf/BasicNetwork.h +++ b/src/test/csf/BasicNetwork.h @@ -3,6 +3,8 @@ #include #include +#include + namespace xrpl::test::csf { /** Peer to peer network simulator. diff --git a/src/test/csf/CollectorRef.h b/src/test/csf/CollectorRef.h index 23baa020b8..5bc55c7515 100644 --- a/src/test/csf/CollectorRef.h +++ b/src/test/csf/CollectorRef.h @@ -1,7 +1,14 @@ #pragma once +#include #include +#include +#include #include +#include + +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h index 20e45faa5b..82ed713561 100644 --- a/src/test/csf/Digraph.h +++ b/src/test/csf/Digraph.h @@ -4,10 +4,11 @@ #include #include +#include #include #include -#include -#include +#include +#include namespace xrpl { namespace detail { diff --git a/src/test/csf/Histogram.h b/src/test/csf/Histogram.h index 84f1cc7b72..60cb9a132e 100644 --- a/src/test/csf/Histogram.h +++ b/src/test/csf/Histogram.h @@ -1,9 +1,9 @@ #pragma once -#include #include -#include #include +#include +#include #include namespace xrpl::test::csf { diff --git a/src/test/csf/Peer.h b/src/test/csf/Peer.h index 3e9eed1c52..9d29704172 100644 --- a/src/test/csf/Peer.h +++ b/src/test/csf/Peer.h @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include #include @@ -10,15 +12,31 @@ #include #include +#include +#include #include +#include +#include +#include +#include +#include #include -#include +#include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/PeerGroup.h b/src/test/csf/PeerGroup.h index 01bba82c98..e5efecac34 100644 --- a/src/test/csf/PeerGroup.h +++ b/src/test/csf/PeerGroup.h @@ -1,9 +1,18 @@ #pragma once #include +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl::test::csf { diff --git a/src/test/csf/Scheduler.h b/src/test/csf/Scheduler.h index e7cbe27036..3fc187e1e7 100644 --- a/src/test/csf/Scheduler.h +++ b/src/test/csf/Scheduler.h @@ -6,6 +6,7 @@ #include #include +#include #include #include diff --git a/src/test/csf/Sim.h b/src/test/csf/Sim.h index b5b7f5f9c2..d2a63f6507 100644 --- a/src/test/csf/Sim.h +++ b/src/test/csf/Sim.h @@ -2,16 +2,22 @@ #include #include -#include #include #include #include #include #include +#include +#include + +#include +#include #include #include #include +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/TrustGraph.h b/src/test/csf/TrustGraph.h index 202d17a6d5..a10e318706 100644 --- a/src/test/csf/TrustGraph.h +++ b/src/test/csf/TrustGraph.h @@ -1,13 +1,11 @@ #pragma once #include -#include #include -#include -#include -#include +#include +#include #include namespace xrpl::test::csf { diff --git a/src/test/csf/Tx.h b/src/test/csf/Tx.h index 5a0f6a9bae..f871909f48 100644 --- a/src/test/csf/Tx.h +++ b/src/test/csf/Tx.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include diff --git a/src/test/csf/Validation.h b/src/test/csf/Validation.h index 81f14efa18..73b6784534 100644 --- a/src/test/csf/Validation.h +++ b/src/test/csf/Validation.h @@ -2,10 +2,12 @@ #include +#include #include -#include +#include #include +#include #include namespace xrpl::test::csf { diff --git a/src/test/csf/collectors.h b/src/test/csf/collectors.h index 6dcbd65a1e..1100f6c690 100644 --- a/src/test/csf/collectors.h +++ b/src/test/csf/collectors.h @@ -2,14 +2,24 @@ #include #include +#include +#include #include #include +#include +#include #include +#include +#include +#include +#include #include #include #include +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/events.h b/src/test/csf/events.h index ea163d3a80..dfedd2627a 100644 --- a/src/test/csf/events.h +++ b/src/test/csf/events.h @@ -1,12 +1,9 @@ #pragma once -#include #include #include #include -#include - namespace xrpl::test::csf { // Events are emitted by peers at a variety of points during the simulation. @@ -77,7 +74,7 @@ struct SubmitTx struct StartRound { //! The preferred ledger for the start of consensus - Ledger::ID bestLedger; + Ledger::ID bestLedger{}; //! The prior ledger on hand Ledger prevLedger; diff --git a/src/test/csf/ledgers.h b/src/test/csf/ledgers.h index 25672de133..dc5ec5277c 100644 --- a/src/test/csf/ledgers.h +++ b/src/test/csf/ledgers.h @@ -2,7 +2,6 @@ #include -#include #include #include #include @@ -11,8 +10,15 @@ #include +#include +#include +#include #include #include +#include +#include +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/random.h b/src/test/csf/random.h index c506397d88..9fc51f1217 100644 --- a/src/test/csf/random.h +++ b/src/test/csf/random.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include diff --git a/src/test/csf/submitters.h b/src/test/csf/submitters.h index a5c494beb0..546681abe0 100644 --- a/src/test/csf/submitters.h +++ b/src/test/csf/submitters.h @@ -1,10 +1,11 @@ #pragma once -#include #include #include #include +#include +#include #include namespace xrpl::test::csf { diff --git a/src/test/csf/timers.h b/src/test/csf/timers.h index 2f2ec4dc93..2f86fe7729 100644 --- a/src/test/csf/timers.h +++ b/src/test/csf/timers.h @@ -4,6 +4,7 @@ #include #include +#include #include namespace xrpl::test::csf { diff --git a/src/test/json/TestOutputSuite.h b/src/test/json/TestOutputSuite.h index e954a043b8..1be2a72c6d 100644 --- a/src/test/json/TestOutputSuite.h +++ b/src/test/json/TestOutputSuite.h @@ -5,8 +5,10 @@ #include #include -namespace xrpl { -namespace test { +#include +#include + +namespace xrpl::test { class TestOutputSuite : public TestSuite { @@ -32,5 +34,4 @@ protected: } }; -} // namespace test -} // namespace xrpl +} // namespace xrpl::test diff --git a/src/test/jtx.h b/src/test/jtx.h deleted file mode 100644 index d4b88b0b9e..0000000000 --- a/src/test/jtx.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -// Convenience header that includes everything - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 99131637bb..605f15f812 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -6,14 +6,33 @@ #include #include -#include - +#include +#include +#include #include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include #include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace xrpl::test::jtx { class LPToken diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index a2e6c38db3..971cc5db84 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -1,11 +1,23 @@ #pragma once #include +#include #include #include #include #include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/AbstractClient.h b/src/test/jtx/AbstractClient.h index 1eb51f2123..7c7107ca79 100644 --- a/src/test/jtx/AbstractClient.h +++ b/src/test/jtx/AbstractClient.h @@ -2,6 +2,8 @@ #include +#include + namespace xrpl::test { /* Abstract XRPL client interface. diff --git a/src/test/jtx/Account.h b/src/test/jtx/Account.h index b20b4a359a..264a39f08b 100644 --- a/src/test/jtx/Account.h +++ b/src/test/jtx/Account.h @@ -1,12 +1,14 @@ #pragma once #include +#include #include +#include #include -#include #include #include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/CaptureLogs.h b/src/test/jtx/CaptureLogs.h index b5fe302629..38db19219a 100644 --- a/src/test/jtx/CaptureLogs.h +++ b/src/test/jtx/CaptureLogs.h @@ -1,6 +1,12 @@ #pragma once #include +#include + +#include +#include +#include +#include namespace xrpl::test { diff --git a/src/test/jtx/CheckMessageLogs.h b/src/test/jtx/CheckMessageLogs.h index 4bbdec6f06..fc46f41671 100644 --- a/src/test/jtx/CheckMessageLogs.h +++ b/src/test/jtx/CheckMessageLogs.h @@ -1,6 +1,11 @@ #pragma once #include +#include + +#include +#include +#include namespace xrpl::test { diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index b758683da6..404ddbe31d 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -1,19 +1,8 @@ #pragma once -#include #include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include #include #include @@ -22,21 +11,10 @@ #include #include #include -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#include #include #include -#include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index aca6074c4a..ba28d19738 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -17,25 +16,39 @@ #include #include +#include #include +#include +#include #include +#include #include -#include -#include +#include +#include +#include #include +#include +#include #include -#include #include +#include #include #include #include +#include -#include +#include +#include +#include +#include #include +#include +#include +#include #include +#include #include -#include -#include +#include #include #include #include diff --git a/src/test/jtx/Env_ss.h b/src/test/jtx/Env_ss.h index ca0825eac7..16e1cdbc82 100644 --- a/src/test/jtx/Env_ss.h +++ b/src/test/jtx/Env_ss.h @@ -1,6 +1,12 @@ #pragma once #include +#include + +#include + +#include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 121e6cc825..da105e01aa 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -10,6 +10,9 @@ #include #include +#include +#include +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/Oracle.h b/src/test/jtx/Oracle.h index cf091c6d13..2c296ba705 100644 --- a/src/test/jtx/Oracle.h +++ b/src/test/jtx/Oracle.h @@ -1,8 +1,25 @@ #pragma once -#include +#include +#include +#include +#include +#include -#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl::test::jtx::oracle { diff --git a/src/test/jtx/PathSet.h b/src/test/jtx/PathSet.h index a391adcb1b..fa3f0d40f9 100644 --- a/src/test/jtx/PathSet.h +++ b/src/test/jtx/PathSet.h @@ -1,10 +1,20 @@ #pragma once -#include +#include +#include -#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include + +#include namespace xrpl::test { diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 34532b17f7..b1643de362 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -1,20 +1,51 @@ #pragma once +#include #include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include #include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include diff --git a/src/test/jtx/TestSuite.h b/src/test/jtx/TestSuite.h index 705adbb620..d40cc6aba6 100644 --- a/src/test/jtx/TestSuite.h +++ b/src/test/jtx/TestSuite.h @@ -1,7 +1,9 @@ #pragma once -#include +#include +#include +#include #include namespace xrpl { diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h index 9a53a32481..008bbe70c3 100644 --- a/src/test/jtx/TrustedPublisherServer.h +++ b/src/test/jtx/TrustedPublisherServer.h @@ -2,11 +2,18 @@ #include +#include #include +#include #include #include +#include +#include #include +#include +#include #include +#include #include #include @@ -19,9 +26,21 @@ #include #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include #include +#include namespace xrpl::test { diff --git a/src/test/jtx/WSClient.h b/src/test/jtx/WSClient.h index 5a22fafe2e..4aa41e072c 100644 --- a/src/test/jtx/WSClient.h +++ b/src/test/jtx/WSClient.h @@ -4,9 +4,14 @@ #include +#include + #include +#include #include #include +#include +#include namespace xrpl::test { diff --git a/src/test/jtx/account_txn_id.h b/src/test/jtx/account_txn_id.h index 89f7368ee6..ffee4123aa 100644 --- a/src/test/jtx/account_txn_id.h +++ b/src/test/jtx/account_txn_id.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 5c8a136617..3f8b9f1ed2 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -3,7 +3,9 @@ #include #include -#include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index db281419ef..ce0234b41a 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -3,14 +3,26 @@ #include #include +#include #include +#include +#include +#include +#include #include #include +#include #include -#include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include #include diff --git a/src/test/jtx/balance.h b/src/test/jtx/balance.h index 5ea80c77f8..3f39e9f0fb 100644 --- a/src/test/jtx/balance.h +++ b/src/test/jtx/balance.h @@ -1,8 +1,12 @@ #pragma once +#include #include +#include #include +#include + #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index 4d564f150a..bb1deec1a2 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -2,17 +2,21 @@ #include #include +#include #include -#include -#include -#include +#include +#include +#include #include +#include +#include #include #include #include #include +#include /** @brief Helpers for constructing Batch test transactions. */ namespace xrpl::test::jtx::batch { diff --git a/src/test/jtx/check.h b/src/test/jtx/check.h index 7c3dc6aa80..f66d802247 100644 --- a/src/test/jtx/check.h +++ b/src/test/jtx/check.h @@ -1,9 +1,13 @@ #pragma once #include -#include #include +#include +#include +#include +#include + #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/credentials.h b/src/test/jtx/credentials.h index 4bdd716918..8da2f289a1 100644 --- a/src/test/jtx/credentials.h +++ b/src/test/jtx/credentials.h @@ -2,7 +2,18 @@ #include #include -#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include namespace xrpl::test::jtx::credentials { diff --git a/src/test/jtx/delegate.h b/src/test/jtx/delegate.h index ae848603a7..9b64c105ba 100644 --- a/src/test/jtx/delegate.h +++ b/src/test/jtx/delegate.h @@ -2,8 +2,14 @@ #include #include +#include +#include +#include + +#include #include +#include namespace xrpl::test::jtx::delegate { diff --git a/src/test/jtx/delivermin.h b/src/test/jtx/delivermin.h index 0a8fa9f823..29256e37bd 100644 --- a/src/test/jtx/delivermin.h +++ b/src/test/jtx/delivermin.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 0d8e363138..5ba12648e2 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -1,7 +1,14 @@ #pragma once #include -#include + +#include +#include +#include +#include + +#include +#include /** Deposit preauthorize operations */ namespace xrpl::test::jtx::deposit { diff --git a/src/test/jtx/did.h b/src/test/jtx/did.h index 7662c6556a..30c89fd879 100644 --- a/src/test/jtx/did.h +++ b/src/test/jtx/did.h @@ -2,7 +2,13 @@ #include #include -#include +#include + +#include +#include +#include + +#include /** DID operations. */ namespace xrpl::test::jtx::did { diff --git a/src/test/jtx/directory.h b/src/test/jtx/directory.h index c63640ae1f..13473f949e 100644 --- a/src/test/jtx/directory.h +++ b/src/test/jtx/directory.h @@ -2,11 +2,15 @@ #include +#include +#include #include -#include +#include +#include #include #include +#include #include /** Directory operations. */ diff --git a/src/test/jtx/domain.h b/src/test/jtx/domain.h index b3a0744768..ebcfdb662a 100644 --- a/src/test/jtx/domain.h +++ b/src/test/jtx/domain.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h index 4c56ec8217..0079605637 100644 --- a/src/test/jtx/envconfig.h +++ b/src/test/jtx/envconfig.h @@ -2,6 +2,11 @@ #include +#include +#include +#include +#include + namespace xrpl::test { extern std::atomic gEnvUseIPv4; diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h index 58be9b701a..728b2e3643 100644 --- a/src/test/jtx/escrow.h +++ b/src/test/jtx/escrow.h @@ -3,10 +3,15 @@ #include #include #include -#include -#include -#include +#include +#include +#include +#include +#include + +#include +#include /** Escrow operations. */ namespace xrpl::test::jtx::escrow { diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h index 048b262e88..ad3002dc75 100644 --- a/src/test/jtx/fee.h +++ b/src/test/jtx/fee.h @@ -1,12 +1,15 @@ #pragma once #include +#include #include #include #include +#include #include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 6e0c49b752..83470f9a1b 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -1,11 +1,15 @@ #pragma once +#include #include #include +#include #include #include +#include +#include #include namespace xrpl { diff --git a/src/test/jtx/impl/Oracle.cpp b/src/test/jtx/impl/Oracle.cpp index 991c84fc18..cd96ef5c0d 100644 --- a/src/test/jtx/impl/Oracle.cpp +++ b/src/test/jtx/impl/Oracle.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include diff --git a/src/test/jtx/invoice_id.h b/src/test/jtx/invoice_id.h index 2246d54abf..b2f13a1d96 100644 --- a/src/test/jtx/invoice_id.h +++ b/src/test/jtx/invoice_id.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/jtx_json.h b/src/test/jtx/jtx_json.h index d58f0b47ac..b22d4c30e1 100644 --- a/src/test/jtx/jtx_json.h +++ b/src/test/jtx/jtx_json.h @@ -1,9 +1,12 @@ #pragma once #include +#include #include +#include + namespace xrpl::test::jtx { /** Inject raw JSON. */ diff --git a/src/test/jtx/last_ledger_sequence.h b/src/test/jtx/last_ledger_sequence.h index 6170c2e4e1..ff6390298e 100644 --- a/src/test/jtx/last_ledger_sequence.h +++ b/src/test/jtx/last_ledger_sequence.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/ledgerStateFix.h b/src/test/jtx/ledgerStateFix.h index 71f3f76101..dd1ac19f04 100644 --- a/src/test/jtx/ledgerStateFix.h +++ b/src/test/jtx/ledgerStateFix.h @@ -1,7 +1,9 @@ #pragma once #include -#include + +#include +#include /** LedgerStateFix operations. */ namespace xrpl::test::jtx::ledgerStateFix { diff --git a/src/test/jtx/memo.h b/src/test/jtx/memo.h index 9b5c1118ac..cea4923d38 100644 --- a/src/test/jtx/memo.h +++ b/src/test/jtx/memo.h @@ -1,7 +1,9 @@ #pragma once #include +#include +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index d5fba82e08..1a7fe94785 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,14 +10,33 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 3fef2ab446..c90e28537a 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -1,14 +1,22 @@ #pragma once #include +#include +#include #include -#include #include #include +#include +#include +#include +#include + #include #include #include +#include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/noop.h b/src/test/jtx/noop.h index 095e20f387..c38dfdde25 100644 --- a/src/test/jtx/noop.h +++ b/src/test/jtx/noop.h @@ -1,7 +1,10 @@ #pragma once +#include #include +#include + namespace xrpl::test::jtx { /** The null transaction. */ diff --git a/src/test/jtx/offer.h b/src/test/jtx/offer.h index 9a31904058..f3e7277933 100644 --- a/src/test/jtx/offer.h +++ b/src/test/jtx/offer.h @@ -5,6 +5,8 @@ #include #include +#include + namespace xrpl::test::jtx { /** Create an offer. */ diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index 44fd3762c2..0fb38b407c 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -1,10 +1,11 @@ #pragma once +#include #include #include +#include #include -#include #include #include diff --git a/src/test/jtx/paths.h b/src/test/jtx/paths.h index a8a8f7900c..1450985687 100644 --- a/src/test/jtx/paths.h +++ b/src/test/jtx/paths.h @@ -1,8 +1,13 @@ #pragma once +#include #include +#include +#include -#include +#include +#include +#include #include diff --git a/src/test/jtx/pay.h b/src/test/jtx/pay.h index 8f4f3287ae..fa193e7b02 100644 --- a/src/test/jtx/pay.h +++ b/src/test/jtx/pay.h @@ -4,6 +4,7 @@ #include #include +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/permissioned_dex.h b/src/test/jtx/permissioned_dex.h index b0a4d4e2d5..a8afcac2eb 100644 --- a/src/test/jtx/permissioned_dex.h +++ b/src/test/jtx/permissioned_dex.h @@ -3,6 +3,11 @@ #include #include +#include + +#include +#include + namespace xrpl::test::jtx { uint256 diff --git a/src/test/jtx/permissioned_domains.h b/src/test/jtx/permissioned_domains.h index fdd2b0da1d..f4dc080cbf 100644 --- a/src/test/jtx/permissioned_domains.h +++ b/src/test/jtx/permissioned_domains.h @@ -4,6 +4,18 @@ #include #include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + namespace xrpl::test::jtx::pdomain { // Helpers for PermissionedDomains testing diff --git a/src/test/jtx/prop.h b/src/test/jtx/prop.h index c3ec6bc0fe..e85751e4c9 100644 --- a/src/test/jtx/prop.h +++ b/src/test/jtx/prop.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include diff --git a/src/test/jtx/quality.h b/src/test/jtx/quality.h index 75bf930434..c7896fadf9 100644 --- a/src/test/jtx/quality.h +++ b/src/test/jtx/quality.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/require.h b/src/test/jtx/require.h index 91cc75b3f5..19e161b938 100644 --- a/src/test/jtx/require.h +++ b/src/test/jtx/require.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/src/test/jtx/rpc.h b/src/test/jtx/rpc.h index 1f538f9ca5..bc05450909 100644 --- a/src/test/jtx/rpc.h +++ b/src/test/jtx/rpc.h @@ -1,8 +1,13 @@ #pragma once #include +#include -#include +#include +#include + +#include +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/sendmax.h b/src/test/jtx/sendmax.h index 125b10b7d4..1241d76b91 100644 --- a/src/test/jtx/sendmax.h +++ b/src/test/jtx/sendmax.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include diff --git a/src/test/jtx/seq.h b/src/test/jtx/seq.h index de06ac3fbc..956c0a77e8 100644 --- a/src/test/jtx/seq.h +++ b/src/test/jtx/seq.h @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index e88785ef15..76f0a34dff 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -1,6 +1,11 @@ #pragma once +#include #include +#include +#include + +#include #include diff --git a/src/test/jtx/tag.h b/src/test/jtx/tag.h index 9279e2a13b..77870367a9 100644 --- a/src/test/jtx/tag.h +++ b/src/test/jtx/tag.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/ter.h b/src/test/jtx/ter.h index c410b46a1e..880711dca0 100644 --- a/src/test/jtx/ter.h +++ b/src/test/jtx/ter.h @@ -1,7 +1,11 @@ #pragma once #include +#include +#include + +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/ticket.h b/src/test/jtx/ticket.h index 02f742ee93..1035be7674 100644 --- a/src/test/jtx/ticket.h +++ b/src/test/jtx/ticket.h @@ -2,8 +2,12 @@ #include #include +#include #include +#include +#include + #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/token.h b/src/test/jtx/token.h index 206d170534..97f968fdfc 100644 --- a/src/test/jtx/token.h +++ b/src/test/jtx/token.h @@ -2,11 +2,17 @@ #include #include -#include +#include +#include #include +#include +#include +#include #include +#include +#include namespace xrpl::test::jtx::token { diff --git a/src/test/jtx/trust.h b/src/test/jtx/trust.h index 07fd0f7c4e..034f80fcec 100644 --- a/src/test/jtx/trust.h +++ b/src/test/jtx/trust.h @@ -5,6 +5,9 @@ #include #include +#include +#include + namespace xrpl::test::jtx { /** Modify a trust line. */ diff --git a/src/test/jtx/txflags.h b/src/test/jtx/txflags.h index 975038d26a..7f5b31b2ac 100644 --- a/src/test/jtx/txflags.h +++ b/src/test/jtx/txflags.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include namespace xrpl::test::jtx { diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index c3ed91f672..f1cf3f7ae8 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -2,11 +2,13 @@ #include +#include #include -#include +#include #include #include +#include #include namespace xrpl::test::jtx { diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h index c1e0831a66..4e6b90fe1f 100644 --- a/src/test/jtx/vault.h +++ b/src/test/jtx/vault.h @@ -1,13 +1,13 @@ #pragma once #include -#include #include #include #include #include +#include #include #include diff --git a/src/test/jtx/xchain_bridge.h b/src/test/jtx/xchain_bridge.h index e4c012510d..aacef80bbe 100644 --- a/src/test/jtx/xchain_bridge.h +++ b/src/test/jtx/xchain_bridge.h @@ -1,12 +1,19 @@ #pragma once #include +#include #include #include #include +#include +#include #include -#include + +#include +#include +#include +#include namespace xrpl::test::jtx { diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index 885c3bbeac..1254fc51aa 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -1,17 +1,22 @@ #pragma once -#include +#include +#include #include -#include +#include #include #include #include #include +#include #include #include -#include +#include +#include +#include +#include namespace xrpl::NodeStore { diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index 801eebbe63..b823b24962 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index bc5c72b4fc..24981053f5 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/rpc/GRPCTestClientBase.h b/src/test/rpc/GRPCTestClientBase.h deleted file mode 100644 index 15d335781d..0000000000 --- a/src/test/rpc/GRPCTestClientBase.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace xrpl { -namespace test { - -struct GRPCTestClientBase -{ - explicit GRPCTestClientBase(std::string const& port) - : stub( - org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub( - grpc::CreateChannel( - beast::IP::Endpoint( - boost::asio::ip::make_address(getEnvLocalhostAddr()), - std::stoi(port)) - .to_string(), - grpc::InsecureChannelCredentials()))) - { - } - - grpc::Status status; - grpc::ClientContext context; - std::unique_ptr stub; -}; - -} // namespace test -} // namespace xrpl diff --git a/src/test/shamap/common.h b/src/test/shamap/common.h index 0475acdf6d..9bef10d851 100644 --- a/src/test/shamap/common.h +++ b/src/test/shamap/common.h @@ -1,11 +1,24 @@ #pragma once +#include +#include #include +#include +#include +#include #include #include +#include #include #include #include +#include +#include + +#include +#include +#include +#include namespace xrpl::tests { diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index 19110469ee..3fc5d015b0 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -1,12 +1,16 @@ #pragma once -#include - #include +#include #include +#include #include +#include +#include +#include +#include namespace xrpl::detail { diff --git a/src/test/unit_test/SuiteJournal.h b/src/test/unit_test/SuiteJournal.h index 174e9ff2ff..66287e328c 100644 --- a/src/test/unit_test/SuiteJournal.h +++ b/src/test/unit_test/SuiteJournal.h @@ -1,8 +1,13 @@ #pragma once -#include +#include #include +#include +#include +#include +#include + namespace xrpl::test { // A Journal::Sink intended for use with the beast unit test framework. diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h index 55cf6c25fa..3390014cb8 100644 --- a/src/test/unit_test/multi_runner.h +++ b/src/test/unit_test/multi_runner.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,11 +13,15 @@ #include #include -#include +#include +#include +#include +#include +#include +#include #include #include #include -#include #include namespace xrpl { diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index bd56028728..df483a36d8 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -1,5 +1,6 @@ include(GoogleTest) include(isolate_headers) +include(verify_headers) # Test requirements. find_package(GTest REQUIRED) @@ -54,4 +55,10 @@ foreach(module IN LISTS test_modules) ) endforeach() +# The test helpers and per-module test headers are not built with add_module, +# so verify them against the test binary's own compile environment. +if(verify_headers) + verify_target_headers(xrpl_tests "${CMAKE_CURRENT_SOURCE_DIR}") +endif() + gtest_discover_tests(xrpl_tests DISCOVERY_TIMEOUT 60) diff --git a/src/tests/libxrpl/helpers/IOU.h b/src/tests/libxrpl/helpers/IOU.h index d80f962edf..10deaee99b 100644 --- a/src/tests/libxrpl/helpers/IOU.h +++ b/src/tests/libxrpl/helpers/IOU.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,7 +9,6 @@ #include -#include #include #include #include diff --git a/src/tests/libxrpl/helpers/TestFamily.h b/src/tests/libxrpl/helpers/TestFamily.h index 50f514480a..a5ff34a111 100644 --- a/src/tests/libxrpl/helpers/TestFamily.h +++ b/src/tests/libxrpl/helpers/TestFamily.h @@ -1,13 +1,22 @@ #pragma once +#include +#include #include +#include +#include #include #include +#include #include #include #include +#include +#include +#include #include +#include namespace xrpl::test { diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index 0536176344..0108f886d3 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -13,8 +15,11 @@ #include #include +#include +#include #include #include +#include namespace xrpl::test { diff --git a/src/tests/libxrpl/helpers/TestSink.h b/src/tests/libxrpl/helpers/TestSink.h index 28c85f00e4..b8637b7038 100644 --- a/src/tests/libxrpl/helpers/TestSink.h +++ b/src/tests/libxrpl/helpers/TestSink.h @@ -2,6 +2,8 @@ #include +#include + namespace xrpl { class TestSink : public beast::Journal::Sink { diff --git a/src/tests/libxrpl/helpers/TxTest.h b/src/tests/libxrpl/helpers/TxTest.h index 1b3ce460a2..cb75cd5ee0 100644 --- a/src/tests/libxrpl/helpers/TxTest.h +++ b/src/tests/libxrpl/helpers/TxTest.h @@ -1,18 +1,23 @@ #pragma once #include +#include #include -#include +#include #include -#include #include #include +#include +#include #include #include #include +#include +#include #include #include #include +#include #include #include #include @@ -24,6 +29,7 @@ #include #include +#include #include #include #include diff --git a/src/tests/libxrpl/protocol_autogen/TestHelpers.h b/src/tests/libxrpl/protocol_autogen/TestHelpers.h index a32ab5e20c..0fc26032f0 100644 --- a/src/tests/libxrpl/protocol_autogen/TestHelpers.h +++ b/src/tests/libxrpl/protocol_autogen/TestHelpers.h @@ -9,6 +9,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/consensus/RCLCensorshipDetector.h b/src/xrpld/app/consensus/RCLCensorshipDetector.h index 48df7368d9..4318a7b4ff 100644 --- a/src/xrpld/app/consensus/RCLCensorshipDetector.h +++ b/src/xrpld/app/consensus/RCLCensorshipDetector.h @@ -1,9 +1,9 @@ #pragma once #include -#include #include +#include #include #include diff --git a/src/xrpld/app/consensus/RCLConsensus.h b/src/xrpld/app/consensus/RCLConsensus.h index 32759300fa..359f6b8009 100644 --- a/src/xrpld/app/consensus/RCLConsensus.h +++ b/src/xrpld/app/consensus/RCLConsensus.h @@ -4,21 +4,37 @@ #include #include #include +#include #include #include #include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include #include -#include +#include + +#include #include +#include +#include +#include #include #include +#include #include #include #include +#include namespace xrpl { diff --git a/src/xrpld/app/consensus/RCLCxLedger.h b/src/xrpld/app/consensus/RCLCxLedger.h index 9f7984aaa6..89f70f9add 100644 --- a/src/xrpld/app/consensus/RCLCxLedger.h +++ b/src/xrpld/app/consensus/RCLCxLedger.h @@ -2,10 +2,16 @@ #include +#include +#include #include -#include +#include +#include #include +#include +#include + namespace xrpl { /** Represents a ledger in RCLConsensus. diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h b/src/xrpld/app/consensus/RCLCxPeerPos.h index 4618bdb746..e73ac3b532 100644 --- a/src/xrpld/app/consensus/RCLCxPeerPos.h +++ b/src/xrpld/app/consensus/RCLCxPeerPos.h @@ -2,11 +2,14 @@ #include +#include #include +#include #include #include #include #include +#include #include diff --git a/src/xrpld/app/consensus/RCLCxTx.h b/src/xrpld/app/consensus/RCLCxTx.h index 52637d32b3..f174a2fd54 100644 --- a/src/xrpld/app/consensus/RCLCxTx.h +++ b/src/xrpld/app/consensus/RCLCxTx.h @@ -1,6 +1,14 @@ #pragma once +#include +#include #include +#include +#include + +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/consensus/RCLValidations.h b/src/xrpld/app/consensus/RCLValidations.h index 37a6f6c743..e8a1996204 100644 --- a/src/xrpld/app/consensus/RCLValidations.h +++ b/src/xrpld/app/consensus/RCLValidations.h @@ -2,12 +2,23 @@ #include +#include +#include +#include #include #include +#include #include +#include #include +#include +#include +#include +#include #include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/AcceptedLedger.h b/src/xrpld/app/ledger/AcceptedLedger.h index b05af1f18a..ec83839d7a 100644 --- a/src/xrpld/app/ledger/AcceptedLedger.h +++ b/src/xrpld/app/ledger/AcceptedLedger.h @@ -3,6 +3,11 @@ #include #include #include +#include + +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/AccountStateSF.h b/src/xrpld/app/ledger/AccountStateSF.h index 5d00e65120..f5117db4d4 100644 --- a/src/xrpld/app/ledger/AccountStateSF.h +++ b/src/xrpld/app/ledger/AccountStateSF.h @@ -2,8 +2,14 @@ #include +#include +#include #include #include +#include + +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/BuildLedger.h b/src/xrpld/app/ledger/BuildLedger.h index 32d45daea3..faa800daa7 100644 --- a/src/xrpld/app/ledger/BuildLedger.h +++ b/src/xrpld/app/ledger/BuildLedger.h @@ -3,6 +3,10 @@ #include #include #include +#include + +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp index 7c74c10b1b..a8ce70d978 100644 --- a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp @@ -6,6 +6,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.h b/src/xrpld/app/ledger/ConsensusTransSetSF.h index f1d0e26d26..59894add7f 100644 --- a/src/xrpld/app/ledger/ConsensusTransSetSF.h +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.h @@ -2,8 +2,15 @@ #include +#include +#include #include +#include #include +#include + +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/InboundLedger.h b/src/xrpld/app/ledger/InboundLedger.h index b82e2f69cd..5f9f0e1baf 100644 --- a/src/xrpld/app/ledger/InboundLedger.h +++ b/src/xrpld/app/ledger/InboundLedger.h @@ -2,14 +2,31 @@ #include #include +#include #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#include +#include +#include +#include #include #include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h index f6c864f777..65b2db7d8e 100644 --- a/src/xrpld/app/ledger/InboundLedgers.h +++ b/src/xrpld/app/ledger/InboundLedgers.h @@ -1,9 +1,23 @@ #pragma once #include +#include +#include +#include +#include +#include +#include +#include #include +#include + +#include +#include +#include +#include + namespace xrpl { /** Manages the lifetime of inbound ledgers. diff --git a/src/xrpld/app/ledger/InboundTransactions.h b/src/xrpld/app/ledger/InboundTransactions.h index bd176dbd95..d9799d9ad0 100644 --- a/src/xrpld/app/ledger/InboundTransactions.h +++ b/src/xrpld/app/ledger/InboundTransactions.h @@ -2,9 +2,16 @@ #include +#include #include +#include #include +#include + +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/LedgerCleaner.h b/src/xrpld/app/ledger/LedgerCleaner.h index 3f76f5593c..fd693d6bec 100644 --- a/src/xrpld/app/ledger/LedgerCleaner.h +++ b/src/xrpld/app/ledger/LedgerCleaner.h @@ -6,6 +6,8 @@ #include #include +#include + namespace xrpl { /** Check the ledger/transaction databases to make sure they have continuity */ diff --git a/src/xrpld/app/ledger/LedgerHistory.h b/src/xrpld/app/ledger/LedgerHistory.h index 057de7b1bc..3fb6e345cf 100644 --- a/src/xrpld/app/ledger/LedgerHistory.h +++ b/src/xrpld/app/ledger/LedgerHistory.h @@ -2,10 +2,17 @@ #include +#include #include +#include +#include +#include #include +#include #include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/LedgerHolder.h b/src/xrpld/app/ledger/LedgerHolder.h index 69fe00b439..3e70544bfc 100644 --- a/src/xrpld/app/ledger/LedgerHolder.h +++ b/src/xrpld/app/ledger/LedgerHolder.h @@ -2,8 +2,11 @@ #include #include +#include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/LedgerMaster.h b/src/xrpld/app/ledger/LedgerMaster.h index 885ab6db25..efd8c15e20 100644 --- a/src/xrpld/app/ledger/LedgerMaster.h +++ b/src/xrpld/app/ledger/LedgerMaster.h @@ -1,25 +1,42 @@ #pragma once #include -#include +#include #include #include #include #include #include +#include #include #include +#include #include #include +#include +#include +#include +#include #include #include +#include #include #include -#include +#include +#include + +#include +#include +#include +#include +#include #include #include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/LedgerPersistence.h b/src/xrpld/app/ledger/LedgerPersistence.h index e131932af4..f466c32296 100644 --- a/src/xrpld/app/ledger/LedgerPersistence.h +++ b/src/xrpld/app/ledger/LedgerPersistence.h @@ -1,7 +1,11 @@ #pragma once +#include #include +#include +#include +#include #include #include diff --git a/src/xrpld/app/ledger/LedgerReplayTask.h b/src/xrpld/app/ledger/LedgerReplayTask.h index 7bb75253f2..09329761c1 100644 --- a/src/xrpld/app/ledger/LedgerReplayTask.h +++ b/src/xrpld/app/ledger/LedgerReplayTask.h @@ -4,6 +4,11 @@ #include #include +#include +#include + +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/LedgerReplayer.h b/src/xrpld/app/ledger/LedgerReplayer.h index e2d256f597..d44289121c 100644 --- a/src/xrpld/app/ledger/LedgerReplayer.h +++ b/src/xrpld/app/ledger/LedgerReplayer.h @@ -1,11 +1,20 @@ #pragma once -#include +#include #include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/xrpld/app/ledger/LedgerToJson.h b/src/xrpld/app/ledger/LedgerToJson.h index 853d4468cd..c9939fd2f4 100644 --- a/src/xrpld/app/ledger/LedgerToJson.h +++ b/src/xrpld/app/ledger/LedgerToJson.h @@ -5,8 +5,12 @@ #include #include -#include -#include +#include +#include + +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/LocalTxs.h b/src/xrpld/app/ledger/LocalTxs.h index 1575a18075..fe955eba24 100644 --- a/src/xrpld/app/ledger/LocalTxs.h +++ b/src/xrpld/app/ledger/LocalTxs.h @@ -2,7 +2,10 @@ #include #include +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/OpenLedger.h b/src/xrpld/app/ledger/OpenLedger.h index 554002d6af..3e0577a9be 100644 --- a/src/xrpld/app/ledger/OpenLedger.h +++ b/src/xrpld/app/ledger/OpenLedger.h @@ -3,15 +3,23 @@ #include #include -#include #include #include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include #include +#include #include namespace xrpl { diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h b/src/xrpld/app/ledger/OrderBookDBImpl.h index a68f63c043..d57d051cce 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.h +++ b/src/xrpld/app/ledger/OrderBookDBImpl.h @@ -1,11 +1,22 @@ #pragma once +#include +#include #include #include +#include +#include +#include #include +#include +#include +#include +#include #include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/TransactionMaster.h b/src/xrpld/app/ledger/TransactionMaster.h index 84cbf56e8b..5dcad09f51 100644 --- a/src/xrpld/app/ledger/TransactionMaster.h +++ b/src/xrpld/app/ledger/TransactionMaster.h @@ -4,10 +4,19 @@ #include #include +#include #include +#include +#include #include #include +#include +#include +#include +#include +#include + namespace xrpl { class Application; diff --git a/src/xrpld/app/ledger/TransactionStateSF.h b/src/xrpld/app/ledger/TransactionStateSF.h index c5c113be7f..a3f7e7f55a 100644 --- a/src/xrpld/app/ledger/TransactionStateSF.h +++ b/src/xrpld/app/ledger/TransactionStateSF.h @@ -2,8 +2,14 @@ #include +#include +#include #include #include +#include + +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp index 4c79068bca..7ac85b892e 100644 --- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h index 57e8fb5652..10c8a21b3a 100644 --- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h @@ -2,12 +2,22 @@ #include #include +#include #include #include #include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include namespace xrpl { class InboundLedgers; diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 31510b44d2..3fcac03ed5 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h index 260f1ff753..5a8951fb25 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h @@ -1,7 +1,10 @@ #pragma once #include -#include + +#include + +#include namespace xrpl { class Application; diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp index f9f1aa3188..e7cd031247 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp index 6c6679ed87..8ebd14083a 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include #include #include #include diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.h b/src/xrpld/app/ledger/detail/SkipListAcquire.h index 6600b495c9..6f8ceb8b74 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.h +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.h @@ -1,11 +1,19 @@ #pragma once -#include #include #include +#include +#include #include -#include +#include + +#include +#include +#include +#include +#include +#include namespace xrpl { class InboundLedgers; diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.h b/src/xrpld/app/ledger/detail/TimeoutCounter.h index 18b443c67d..ab4dd28e47 100644 --- a/src/xrpld/app/ledger/detail/TimeoutCounter.h +++ b/src/xrpld/app/ledger/detail/TimeoutCounter.h @@ -2,12 +2,18 @@ #include +#include #include #include #include +#include +#include +#include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h b/src/xrpld/app/ledger/detail/TransactionAcquire.h index b94a9f4bfc..5b33066390 100644 --- a/src/xrpld/app/ledger/detail/TransactionAcquire.h +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h @@ -1,9 +1,20 @@ #pragma once #include +#include +#include #include +#include +#include +#include #include +#include + +#include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 08d84636f6..b99c98afa1 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -73,6 +73,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/main/Application.h b/src/xrpld/app/main/Application.h index 08e41e2c4c..33876b97b9 100644 --- a/src/xrpld/app/main/Application.h +++ b/src/xrpld/app/main/Application.h @@ -2,17 +2,25 @@ #include +#include +#include #include +#include #include -#include #include #include -#include #include #include +#include +#include +#include +#include #include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/main/BasicApp.h b/src/xrpld/app/main/BasicApp.h index 5ba7c719e3..aa367ca674 100644 --- a/src/xrpld/app/main/BasicApp.h +++ b/src/xrpld/app/main/BasicApp.h @@ -2,6 +2,7 @@ #include +#include #include #include #include diff --git a/src/xrpld/app/main/CollectorManager.h b/src/xrpld/app/main/CollectorManager.h index d07da15353..e695ddc956 100644 --- a/src/xrpld/app/main/CollectorManager.h +++ b/src/xrpld/app/main/CollectorManager.h @@ -1,8 +1,13 @@ #pragma once -#include +#include +#include +#include #include +#include +#include + namespace xrpl { /** Provides the beast::insight::Collector service. */ diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1af1343fc5..1146cbdc08 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -1,5 +1,6 @@ #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/main/GRPCServer.h b/src/xrpld/app/main/GRPCServer.h index 7d299474d9..db948cab99 100644 --- a/src/xrpld/app/main/GRPCServer.h +++ b/src/xrpld/app/main/GRPCServer.h @@ -2,16 +2,27 @@ #include #include -#include #include #include +#include #include #include #include -#include +#include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/main/LoadManager.h b/src/xrpld/app/main/LoadManager.h index 5baa01550e..794048567a 100644 --- a/src/xrpld/app/main/LoadManager.h +++ b/src/xrpld/app/main/LoadManager.h @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include diff --git a/src/xrpld/app/main/NodeIdentity.h b/src/xrpld/app/main/NodeIdentity.h index bebd5c261d..789d061021 100644 --- a/src/xrpld/app/main/NodeIdentity.h +++ b/src/xrpld/app/main/NodeIdentity.h @@ -7,6 +7,8 @@ #include +#include + namespace xrpl { /** The cryptographic credentials identifying this server instance. diff --git a/src/xrpld/app/main/NodeStoreScheduler.h b/src/xrpld/app/main/NodeStoreScheduler.h index 19e8d9d212..48e606bb45 100644 --- a/src/xrpld/app/main/NodeStoreScheduler.h +++ b/src/xrpld/app/main/NodeStoreScheduler.h @@ -2,6 +2,7 @@ #include #include +#include namespace xrpl { diff --git a/src/xrpld/app/main/Tuning.h b/src/xrpld/app/main/Tuning.h index d61f27ec03..0f7a69f5b1 100644 --- a/src/xrpld/app/main/Tuning.h +++ b/src/xrpld/app/main/Tuning.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl { diff --git a/src/xrpld/app/misc/AmendmentTableImpl.h b/src/xrpld/app/misc/AmendmentTableImpl.h index fe7c067d5a..db71189170 100644 --- a/src/xrpld/app/misc/AmendmentTableImpl.h +++ b/src/xrpld/app/misc/AmendmentTableImpl.h @@ -1,8 +1,12 @@ #pragma once +#include #include +#include -#include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/misc/FeeVote.h b/src/xrpld/app/misc/FeeVote.h index 5c6206558a..d6b4b0fc6a 100644 --- a/src/xrpld/app/misc/FeeVote.h +++ b/src/xrpld/app/misc/FeeVote.h @@ -1,9 +1,15 @@ #pragma once +#include #include +#include +#include #include #include +#include +#include + namespace xrpl { /** Manager to process fee votes. */ diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp index c4e6c3cdc2..363c17f4fa 100644 --- a/src/xrpld/app/misc/FeeVoteImpl.cpp +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/misc/NegativeUNLVote.h b/src/xrpld/app/misc/NegativeUNLVote.h index 5c2d3f8630..2896962a84 100644 --- a/src/xrpld/app/misc/NegativeUNLVote.h +++ b/src/xrpld/app/misc/NegativeUNLVote.h @@ -1,12 +1,19 @@ #pragma once +#include +#include #include #include #include #include #include +#include +#include +#include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/app/misc/SHAMapStore.h b/src/xrpld/app/misc/SHAMapStore.h index 3f43bc8fa4..9f12546463 100644 --- a/src/xrpld/app/misc/SHAMapStore.h +++ b/src/xrpld/app/misc/SHAMapStore.h @@ -2,9 +2,14 @@ #include +#include #include -#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 33bbf1c613..0389130f7a 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h index 1803c15e71..4025236868 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.h +++ b/src/xrpld/app/misc/SHAMapStoreImp.h @@ -1,17 +1,33 @@ #pragma once #include +#include #include +#include +#include +#include +#include +#include #include #include +#include #include #include #include +#include #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/misc/Transaction.h b/src/xrpld/app/misc/Transaction.h index 80e041f5b7..ab0fa1f4d8 100644 --- a/src/xrpld/app/misc/Transaction.h +++ b/src/xrpld/app/misc/Transaction.h @@ -1,7 +1,11 @@ #pragma once +#include +#include #include +#include #include +#include #include #include #include @@ -9,8 +13,13 @@ #include #include #include +#include +#include +#include #include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/misc/TxQ.h b/src/xrpld/app/misc/TxQ.h index d3caec55cf..135cd592f0 100644 --- a/src/xrpld/app/misc/TxQ.h +++ b/src/xrpld/app/misc/TxQ.h @@ -1,17 +1,35 @@ #pragma once +#include +#include +#include #include #include +#include +#include +#include #include +#include +#include #include #include #include +#include +#include #include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/misc/ValidatorKeys.h b/src/xrpld/app/misc/ValidatorKeys.h index 1f96815681..df88ec33a3 100644 --- a/src/xrpld/app/misc/ValidatorKeys.h +++ b/src/xrpld/app/misc/ValidatorKeys.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h index 3f1823f930..0db3eee284 100644 --- a/src/xrpld/app/misc/ValidatorList.h +++ b/src/xrpld/app/misc/ValidatorList.h @@ -3,17 +3,30 @@ #include #include -#include #include -#include +#include +#include +#include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include +#include +#include namespace protocol { class TMValidatorList; diff --git a/src/xrpld/app/misc/ValidatorSite.h b/src/xrpld/app/misc/ValidatorSite.h index 55f060baf1..7302ebbb52 100644 --- a/src/xrpld/app/misc/ValidatorSite.h +++ b/src/xrpld/app/misc/ValidatorSite.h @@ -4,14 +4,21 @@ #include #include -#include #include +#include #include #include +#include +#include +#include +#include +#include #include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.h b/src/xrpld/app/misc/detail/AccountTxPaging.h index a895549322..628ef82c3d 100644 --- a/src/xrpld/app/misc/detail/AccountTxPaging.h +++ b/src/xrpld/app/misc/detail/AccountTxPaging.h @@ -1,8 +1,11 @@ #pragma once +#include +#include #include #include +#include //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp b/src/xrpld/app/misc/detail/ValidatorSite.cpp index 6ce3711652..7b51fbd597 100644 --- a/src/xrpld/app/misc/detail/ValidatorSite.cpp +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include #include diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h index 56b227613f..5f5fdd28b7 100644 --- a/src/xrpld/app/misc/detail/WorkBase.h +++ b/src/xrpld/app/misc/detail/WorkBase.h @@ -2,7 +2,7 @@ #include -#include +#include #include #include @@ -12,6 +12,8 @@ #include #include +#include +#include #include namespace xrpl::detail { diff --git a/src/xrpld/app/misc/detail/WorkFile.h b/src/xrpld/app/misc/detail/WorkFile.h index 067dc4c38b..2fc7ab952d 100644 --- a/src/xrpld/app/misc/detail/WorkFile.h +++ b/src/xrpld/app/misc/detail/WorkFile.h @@ -10,6 +10,9 @@ #include #include +#include +#include +#include #include namespace xrpl::detail { diff --git a/src/xrpld/app/misc/detail/WorkPlain.h b/src/xrpld/app/misc/detail/WorkPlain.h index d3c0309e77..4c45fb4801 100644 --- a/src/xrpld/app/misc/detail/WorkPlain.h +++ b/src/xrpld/app/misc/detail/WorkPlain.h @@ -2,6 +2,9 @@ #include +#include +#include + namespace xrpl::detail { // Work over TCP/IP diff --git a/src/xrpld/app/misc/detail/WorkSSL.h b/src/xrpld/app/misc/detail/WorkSSL.h index 74676bb7c1..d4b3b9ff25 100644 --- a/src/xrpld/app/misc/detail/WorkSSL.h +++ b/src/xrpld/app/misc/detail/WorkSSL.h @@ -3,13 +3,14 @@ #include #include -#include +#include #include #include #include -#include +#include +#include namespace xrpl::detail { diff --git a/src/xrpld/app/misc/make_NetworkOPs.h b/src/xrpld/app/misc/make_NetworkOPs.h index a848067e36..b4dba2b065 100644 --- a/src/xrpld/app/misc/make_NetworkOPs.h +++ b/src/xrpld/app/misc/make_NetworkOPs.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -8,6 +8,7 @@ #include +#include #include namespace xrpl { diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h index e5ac6dda8c..4f186ff7e2 100644 --- a/src/xrpld/app/rdb/PeerFinder.h +++ b/src/xrpld/app/rdb/PeerFinder.h @@ -1,10 +1,15 @@ #pragma once -#include #include +#include +#include #include +#include +#include +#include + namespace xrpl { /** diff --git a/src/xrpld/app/rdb/backend/SQLiteDatabase.h b/src/xrpld/app/rdb/backend/SQLiteDatabase.h index e5c69c2703..f5b930d23e 100644 --- a/src/xrpld/app/rdb/backend/SQLiteDatabase.h +++ b/src/xrpld/app/rdb/backend/SQLiteDatabase.h @@ -1,10 +1,22 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include #include +#include #include #include diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index dcc146397b..6e92a0de60 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -43,7 +43,9 @@ #include // IWYU pragma: keep #include +#include // IWYU pragma: keep #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/rdb/backend/detail/Node.h b/src/xrpld/app/rdb/backend/detail/Node.h index 8267bb1a82..ba3eb91ca3 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.h +++ b/src/xrpld/app/rdb/backend/detail/Node.h @@ -2,9 +2,33 @@ #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace xrpl::detail { /* Need to change TableTypeCount if TableType is modified. */ diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp index 9452c3af99..c40e3e42c6 100644 --- a/src/xrpld/app/rdb/detail/PeerFinder.cpp +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp @@ -11,6 +11,7 @@ #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h index b8d04e18b5..fa41f25be1 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/src/xrpld/consensus/Consensus.h @@ -3,19 +3,28 @@ #include #include #include -#include #include +#include #include +#include #include +#include +#include #include #include #include #include +#include +#include #include +#include +#include #include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/consensus/ConsensusParms.h b/src/xrpld/consensus/ConsensusParms.h index e6dd7f046e..d00a5dc3c7 100644 --- a/src/xrpld/consensus/ConsensusParms.h +++ b/src/xrpld/consensus/ConsensusParms.h @@ -4,9 +4,9 @@ #include #include -#include #include #include +#include namespace xrpl { diff --git a/src/xrpld/consensus/ConsensusProposal.h b/src/xrpld/consensus/ConsensusProposal.h index d16679bfb5..c27d5c2819 100644 --- a/src/xrpld/consensus/ConsensusProposal.h +++ b/src/xrpld/consensus/ConsensusProposal.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace xrpl { /** Represents a proposed position taken during a round of consensus. diff --git a/src/xrpld/consensus/ConsensusTypes.h b/src/xrpld/consensus/ConsensusTypes.h index f043fc0663..6b33f50662 100644 --- a/src/xrpld/consensus/ConsensusTypes.h +++ b/src/xrpld/consensus/ConsensusTypes.h @@ -3,10 +3,14 @@ #include #include +#include #include +#include #include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/consensus/DisputedTx.h b/src/xrpld/consensus/DisputedTx.h index ba8329714b..96df1536f5 100644 --- a/src/xrpld/consensus/DisputedTx.h +++ b/src/xrpld/consensus/DisputedTx.h @@ -4,10 +4,15 @@ #include #include +#include #include #include +#include +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/consensus/LedgerTrie.h b/src/xrpld/consensus/LedgerTrie.h index b11a69a641..a21eea2a8a 100644 --- a/src/xrpld/consensus/LedgerTrie.h +++ b/src/xrpld/consensus/LedgerTrie.h @@ -5,9 +5,13 @@ #include #include +#include +#include #include +#include #include #include +#include #include #include #include diff --git a/src/xrpld/consensus/Validations.h b/src/xrpld/consensus/Validations.h index d4da8a2887..a204cd0c78 100644 --- a/src/xrpld/consensus/Validations.h +++ b/src/xrpld/consensus/Validations.h @@ -5,12 +5,21 @@ #include #include #include +#include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include #include diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 45b36808b2..285ea7b9ac 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -1,16 +1,20 @@ #pragma once #include +#include #include #include #include #include #include #include // VFALCO Breaks levelization +#include #include #include // VFALCO FIX: This include should not be here +#include +#include #include #include #include diff --git a/src/xrpld/core/TimeKeeper.h b/src/xrpld/core/TimeKeeper.h index 8eb13e75c0..9e067759ec 100644 --- a/src/xrpld/core/TimeKeeper.h +++ b/src/xrpld/core/TimeKeeper.h @@ -4,6 +4,7 @@ #include #include +#include namespace xrpl { diff --git a/src/xrpld/overlay/Cluster.h b/src/xrpld/overlay/Cluster.h index a8c2083fbc..b3864e0fc6 100644 --- a/src/xrpld/overlay/Cluster.h +++ b/src/xrpld/overlay/Cluster.h @@ -7,9 +7,14 @@ #include #include +#include +#include #include #include +#include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/overlay/Compression.h b/src/xrpld/overlay/Compression.h index fb58bd128a..4b7493e7e6 100644 --- a/src/xrpld/overlay/Compression.h +++ b/src/xrpld/overlay/Compression.h @@ -2,6 +2,10 @@ #include #include +#include + +#include +#include namespace xrpl::compression { diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index cd21ca40c6..63aa360f92 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -4,10 +4,17 @@ #include #include -#include -#include +#include + +#include + +#include #include +#include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/overlay/Overlay.h b/src/xrpld/overlay/Overlay.h index 87c6ff132a..cc5be791a7 100644 --- a/src/xrpld/overlay/Overlay.h +++ b/src/xrpld/overlay/Overlay.h @@ -2,8 +2,12 @@ #include +#include +#include +#include #include #include +#include #include #include @@ -11,8 +15,15 @@ #include #include +#include + +#include +#include #include +#include #include +#include +#include namespace boost::asio::ssl { class context; diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 0c86776030..29778b42a6 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -7,6 +7,12 @@ #include #include +#include +#include +#include +#include +#include + namespace xrpl { namespace Resource { diff --git a/src/xrpld/overlay/PeerSet.h b/src/xrpld/overlay/PeerSet.h index f50b7130f4..4670ec9783 100644 --- a/src/xrpld/overlay/PeerSet.h +++ b/src/xrpld/overlay/PeerSet.h @@ -4,6 +4,15 @@ #include #include +#include + +#include + +#include +#include +#include +#include + namespace xrpl { /** Supports data retrieval by managing a set of peers. diff --git a/src/xrpld/overlay/ReduceRelayCommon.h b/src/xrpld/overlay/ReduceRelayCommon.h index 2389c21f0e..243ce167f9 100644 --- a/src/xrpld/overlay/ReduceRelayCommon.h +++ b/src/xrpld/overlay/ReduceRelayCommon.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include // Blog post explaining the rationale behind reduction of flooding gossip // protocol: diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h index 7490798787..a29020e03a 100644 --- a/src/xrpld/overlay/Slot.h +++ b/src/xrpld/overlay/Slot.h @@ -5,19 +5,33 @@ #include #include -#include +#include +#include +#include +#include #include #include #include +#include +#include #include -#include + +#include #include +#include +#include +#include +#include +#include +#include #include #include +#include #include #include #include +#include namespace xrpl::reduce_relay { diff --git a/src/xrpld/overlay/Squelch.h b/src/xrpld/overlay/Squelch.h index 96d8c26f1d..0485c91c81 100644 --- a/src/xrpld/overlay/Squelch.h +++ b/src/xrpld/overlay/Squelch.h @@ -3,6 +3,7 @@ #include #include +#include #include #include diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index aba224d5c7..bed3f672be 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -1,9 +1,21 @@ #pragma once +#include #include #include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h index 3cbaa118da..6dcc06bbf1 100644 --- a/src/xrpld/overlay/detail/Handshake.h +++ b/src/xrpld/overlay/detail/Handshake.h @@ -3,8 +3,9 @@ #include #include +#include +#include #include -#include #include #include @@ -13,8 +14,9 @@ #include #include +#include #include -#include +#include namespace xrpl { diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 545d9eb75c..7db32c0d23 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -3,20 +3,30 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#include #include #include +#include #include #include @@ -25,14 +35,22 @@ #include #include +#include + #include #include #include +#include #include +#include #include #include #include +#include +#include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 28fb6b33a4..4a96f9a6e6 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -2,28 +2,59 @@ #include #include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include #include +#include +#include +#include +#include #include +#include #include +#include +#include #include +#include #include #include +#include +#include #include +#include #include #include #include +#include + +#include + #include +#include +#include #include +#include +#include +#include #include #include +#include +#include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index b1a30bad10..da418f8e82 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -5,14 +5,21 @@ #include #include -#include #include #include +#include + +#include + +#include #include +#include #include +#include #include +#include #include namespace xrpl { diff --git a/src/xrpld/overlay/detail/TrafficCount.h b/src/xrpld/overlay/detail/TrafficCount.h index cb77c2359f..b96ee022d6 100644 --- a/src/xrpld/overlay/detail/TrafficCount.h +++ b/src/xrpld/overlay/detail/TrafficCount.h @@ -1,10 +1,16 @@ #pragma once #include -#include + +#include + +#include #include +#include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/overlay/detail/TxMetrics.h b/src/xrpld/overlay/detail/TxMetrics.h index 50df7a65cc..44cd0272ee 100644 --- a/src/xrpld/overlay/detail/TxMetrics.h +++ b/src/xrpld/overlay/detail/TxMetrics.h @@ -1,11 +1,13 @@ #pragma once #include -#include #include +#include + #include +#include #include namespace xrpl::metrics { diff --git a/src/xrpld/overlay/detail/ZeroCopyStream.h b/src/xrpld/overlay/detail/ZeroCopyStream.h index f77f266321..deccb627b9 100644 --- a/src/xrpld/overlay/detail/ZeroCopyStream.h +++ b/src/xrpld/overlay/detail/ZeroCopyStream.h @@ -6,6 +6,9 @@ #include +#include +#include + namespace xrpl { /** Implements ZeroCopyInputStream around a buffer sequence. @@ -20,7 +23,7 @@ private: using iterator = Buffers::const_iterator; using const_buffer = boost::asio::const_buffer; - google::protobuf::int64 count_ = 0; + std::int64_t count_ = 0; iterator last_; iterator first_; // Where pos_ comes from const_buffer pos_; // What Next() will return @@ -37,7 +40,7 @@ public: bool Skip(int count) override; - [[nodiscard]] google::protobuf::int64 + [[nodiscard]] std::int64_t ByteCount() const override { return count_; @@ -116,7 +119,7 @@ private: Streambuf& streambuf_; std::size_t blockSize_; - google::protobuf::int64 count_ = 0; + std::int64_t count_ = 0; std::size_t commit_ = 0; buffers_type buffers_; iterator pos_; @@ -132,7 +135,7 @@ public: void BackUp(int count) override; - [[nodiscard]] google::protobuf::int64 + [[nodiscard]] std::int64_t ByteCount() const override { return count_; diff --git a/src/xrpld/overlay/make_Overlay.h b/src/xrpld/overlay/make_Overlay.h index acd477deec..f0dfa429c0 100644 --- a/src/xrpld/overlay/make_Overlay.h +++ b/src/xrpld/overlay/make_Overlay.h @@ -1,12 +1,19 @@ #pragma once +#include #include #include #include +#include +#include +#include +#include #include +#include + namespace xrpl { Overlay::Setup diff --git a/src/xrpld/overlay/predicates.h b/src/xrpld/overlay/predicates.h index d7c2add937..2527b8c728 100644 --- a/src/xrpld/overlay/predicates.h +++ b/src/xrpld/overlay/predicates.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace xrpl { diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index ec4beb2db4..d482ae7241 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -5,11 +5,20 @@ #include #include +#include #include +#include #include +#include +#include +#include +#include +#include #include +#include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/Slot.h b/src/xrpld/peerfinder/Slot.h index 289252e3fa..f43b7d1009 100644 --- a/src/xrpld/peerfinder/Slot.h +++ b/src/xrpld/peerfinder/Slot.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Bootcache.h b/src/xrpld/peerfinder/detail/Bootcache.h index 01ee2cad33..5384bb356a 100644 --- a/src/xrpld/peerfinder/detail/Bootcache.h +++ b/src/xrpld/peerfinder/detail/Bootcache.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Checker.h b/src/xrpld/peerfinder/detail/Checker.h index 2f324bf8b6..20687448df 100644 --- a/src/xrpld/peerfinder/detail/Checker.h +++ b/src/xrpld/peerfinder/detail/Checker.h @@ -1,12 +1,14 @@ #pragma once #include +#include #include #include #include #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h index 67b8370996..0d8bf1c56e 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/src/xrpld/peerfinder/detail/Counts.h @@ -4,7 +4,12 @@ #include #include -#include +#include +#include + +#include +#include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Fixed.h b/src/xrpld/peerfinder/detail/Fixed.h index 61df9caddb..3319994251 100644 --- a/src/xrpld/peerfinder/detail/Fixed.h +++ b/src/xrpld/peerfinder/detail/Fixed.h @@ -1,7 +1,12 @@ #pragma once +#include #include +#include +#include +#include + namespace xrpl::PeerFinder { /** Metadata for a Fixed slot. */ diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/src/xrpld/peerfinder/detail/Handouts.h index 7523197c40..faadb51fd2 100644 --- a/src/xrpld/peerfinder/detail/Handouts.h +++ b/src/xrpld/peerfinder/detail/Handouts.h @@ -1,12 +1,17 @@ #pragma once +#include #include #include #include +#include #include +#include +#include #include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Livecache.h b/src/xrpld/peerfinder/detail/Livecache.h index c5f04be90a..1dcc8f6daf 100644 --- a/src/xrpld/peerfinder/detail/Livecache.h +++ b/src/xrpld/peerfinder/detail/Livecache.h @@ -7,13 +7,27 @@ #include #include #include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index b74643f7a5..7d10f7f516 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -14,14 +15,29 @@ #include #include #include +#include #include +#include +#include +#include #include +#include +#include #include +#include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/SlotImp.h b/src/xrpld/peerfinder/detail/SlotImp.h index 0cf4d7a3c8..cf1915268f 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.h +++ b/src/xrpld/peerfinder/detail/SlotImp.h @@ -4,9 +4,14 @@ #include #include +#include +#include #include +#include +#include #include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Source.h b/src/xrpld/peerfinder/detail/Source.h index c86176911e..cf8920e056 100644 --- a/src/xrpld/peerfinder/detail/Source.h +++ b/src/xrpld/peerfinder/detail/Source.h @@ -2,8 +2,12 @@ #include +#include + #include +#include + namespace xrpl::PeerFinder { /** A static or dynamic source of peer addresses. diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h b/src/xrpld/peerfinder/detail/SourceStrings.h index 156db0ce85..618970fa03 100644 --- a/src/xrpld/peerfinder/detail/SourceStrings.h +++ b/src/xrpld/peerfinder/detail/SourceStrings.h @@ -3,6 +3,8 @@ #include #include +#include +#include namespace xrpl::PeerFinder { diff --git a/src/xrpld/peerfinder/detail/Store.h b/src/xrpld/peerfinder/detail/Store.h index 347fc09b15..570dba0523 100644 --- a/src/xrpld/peerfinder/detail/Store.h +++ b/src/xrpld/peerfinder/detail/Store.h @@ -1,5 +1,11 @@ #pragma once +#include + +#include +#include +#include + namespace xrpl::PeerFinder { /** Abstract persistence for PeerFinder data. */ diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h index 19cdd774b7..f868e89ff7 100644 --- a/src/xrpld/peerfinder/detail/StoreSqdb.h +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h @@ -3,8 +3,17 @@ #include #include +#include +#include +#include #include +#include + +#include +#include +#include + namespace xrpl::PeerFinder { /** Database persistence for PeerFinder using SQLite */ diff --git a/src/xrpld/peerfinder/detail/Tuning.h b/src/xrpld/peerfinder/detail/Tuning.h index b4781495b8..1bf9df382e 100644 --- a/src/xrpld/peerfinder/detail/Tuning.h +++ b/src/xrpld/peerfinder/detail/Tuning.h @@ -1,6 +1,9 @@ #pragma once +#include #include +#include +#include /** Heuristically tuned constants. */ /** @{ */ diff --git a/src/xrpld/peerfinder/detail/iosformat.h b/src/xrpld/peerfinder/detail/iosformat.h index 632ac10c16..a0b9ff537a 100644 --- a/src/xrpld/peerfinder/detail/iosformat.h +++ b/src/xrpld/peerfinder/detail/iosformat.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include #include diff --git a/src/xrpld/peerfinder/make_Manager.h b/src/xrpld/peerfinder/make_Manager.h index 846330988b..1f3f226397 100644 --- a/src/xrpld/peerfinder/make_Manager.h +++ b/src/xrpld/peerfinder/make_Manager.h @@ -2,6 +2,10 @@ #include +#include +#include +#include + #include #include diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp index 5ace4d8c8b..3aa7e38ea2 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/src/xrpld/perflog/detail/PerfLogImp.h b/src/xrpld/perflog/detail/PerfLogImp.h index bd60912c18..88bf473554 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.h +++ b/src/xrpld/perflog/detail/PerfLogImp.h @@ -3,17 +3,23 @@ #include #include +#include +#include #include +#include #include #include #include #include -#include +#include +#include +#include #include #include #include +#include #include namespace xrpl::perf { diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h index 45c3cf2e4b..3c10ece78f 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -1,13 +1,25 @@ #pragma once +#include +#include #include +#include +#include #include +#include +#include #include #include #include #include +#include +#include #include +#include +#include +#include +#include namespace json { class Value; diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index 781cd85e29..c2133a19b5 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -2,9 +2,14 @@ #include +#include +#include +#include #include -#include #include +#include +#include +#include namespace xrpl::RPC { diff --git a/src/xrpld/rpc/Context.h b/src/xrpld/rpc/Context.h index 3724ce1783..fe6bb81ce3 100644 --- a/src/xrpld/rpc/Context.h +++ b/src/xrpld/rpc/Context.h @@ -4,8 +4,14 @@ #include #include +#include +#include +#include #include +#include +#include + namespace xrpl { class Application; diff --git a/src/xrpld/rpc/DeliveredAmount.h b/src/xrpld/rpc/DeliveredAmount.h index b603fa4acd..bba045d494 100644 --- a/src/xrpld/rpc/DeliveredAmount.h +++ b/src/xrpld/rpc/DeliveredAmount.h @@ -3,8 +3,8 @@ #include #include -#include #include +#include namespace json { class Value; diff --git a/src/xrpld/rpc/GRPCHandlers.h b/src/xrpld/rpc/GRPCHandlers.h index ac419c18ee..9dc7e0b13a 100644 --- a/src/xrpld/rpc/GRPCHandlers.h +++ b/src/xrpld/rpc/GRPCHandlers.h @@ -2,9 +2,13 @@ #include -#include +#include +#include +#include +#include +#include -#include +#include namespace xrpl { diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index 8a8f6f3d51..cb2bfd1bdc 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -1,9 +1,9 @@ #pragma once -#include #include #include #include +#include #include #include diff --git a/src/xrpld/rpc/Output.h b/src/xrpld/rpc/Output.h index 1c74562842..30b5c090d7 100644 --- a/src/xrpld/rpc/Output.h +++ b/src/xrpld/rpc/Output.h @@ -2,8 +2,10 @@ #include -namespace xrpl { -namespace RPC { +#include +#include + +namespace xrpl::RPC { using Output = std::function; @@ -13,5 +15,4 @@ stringOutput(std::string& s) return [&](boost::string_ref const& b) { s.append(b.data(), b.size()); }; } -} // namespace RPC -} // namespace xrpl +} // namespace xrpl::RPC diff --git a/src/xrpld/rpc/RPCCall.h b/src/xrpld/rpc/RPCCall.h index 7a09115e42..a06eca4413 100644 --- a/src/xrpld/rpc/RPCCall.h +++ b/src/xrpld/rpc/RPCCall.h @@ -2,14 +2,17 @@ #include -#include +#include +#include #include #include +#include #include #include #include +#include #include namespace xrpl { diff --git a/src/xrpld/rpc/RPCHandler.h b/src/xrpld/rpc/RPCHandler.h index 7e57b456fc..d1cd54145d 100644 --- a/src/xrpld/rpc/RPCHandler.h +++ b/src/xrpld/rpc/RPCHandler.h @@ -1,8 +1,13 @@ #pragma once #include +#include #include +#include + +#include + namespace xrpl::RPC { struct JsonContext; diff --git a/src/xrpld/rpc/RPCSub.h b/src/xrpld/rpc/RPCSub.h index 4c0903ed3e..95206e5cf6 100644 --- a/src/xrpld/rpc/RPCSub.h +++ b/src/xrpld/rpc/RPCSub.h @@ -6,6 +6,9 @@ #include +#include +#include + namespace xrpl { /** Subscription object for JSON RPC. */ diff --git a/src/xrpld/rpc/Role.h b/src/xrpld/rpc/Role.h index d1b641f067..660fb92c7c 100644 --- a/src/xrpld/rpc/Role.h +++ b/src/xrpld/rpc/Role.h @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include #include @@ -10,7 +12,7 @@ #include #include -#include +#include #include namespace xrpl { diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h index 0aac05ea44..054bec9b5b 100644 --- a/src/xrpld/rpc/ServerHandler.h +++ b/src/xrpld/rpc/ServerHandler.h @@ -2,11 +2,19 @@ #include #include +#include #include +#include +#include +#include +#include #include #include -#include +#include +#include +#include +#include // IWYU pragma: keep #include #include @@ -15,8 +23,15 @@ #include #include +#include +#include #include +#include #include +#include +#include +#include +#include #include namespace xrpl { diff --git a/src/xrpld/rpc/Status.h b/src/xrpld/rpc/Status.h index 8f7c620baa..4f15ab0242 100644 --- a/src/xrpld/rpc/Status.h +++ b/src/xrpld/rpc/Status.h @@ -1,9 +1,16 @@ #pragma once #include +#include #include #include +#include +#include +#include +#include +#include + namespace xrpl::RPC { /** Status represents the results of an operation that might fail. diff --git a/src/xrpld/rpc/detail/AccountAssets.h b/src/xrpld/rpc/detail/AccountAssets.h index 55fce62fa6..16c6087e3f 100644 --- a/src/xrpld/rpc/detail/AccountAssets.h +++ b/src/xrpld/rpc/detail/AccountAssets.h @@ -2,7 +2,11 @@ #include -#include +#include +#include +#include + +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/AssetCache.h b/src/xrpld/rpc/detail/AssetCache.h index e53bc3ff94..4b89487526 100644 --- a/src/xrpld/rpc/detail/AssetCache.h +++ b/src/xrpld/rpc/detail/AssetCache.h @@ -4,10 +4,14 @@ #include #include +#include #include -#include +#include +#include +#include #include +#include #include #include diff --git a/src/xrpld/rpc/detail/Handler.h b/src/xrpld/rpc/detail/Handler.h index 140f421ed3..5e583aa5bb 100644 --- a/src/xrpld/rpc/detail/Handler.h +++ b/src/xrpld/rpc/detail/Handler.h @@ -1,13 +1,21 @@ #pragma once -#include #include +#include #include #include +#include +#include #include +#include +#include #include +#include +#include +#include + namespace json { class Object; } // namespace json diff --git a/src/xrpld/rpc/detail/MPT.h b/src/xrpld/rpc/detail/MPT.h index 5cf2d5490f..93c8517539 100644 --- a/src/xrpld/rpc/detail/MPT.h +++ b/src/xrpld/rpc/detail/MPT.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h index 372223e99f..64bc6ef181 100644 --- a/src/xrpld/rpc/detail/PathRequest.h +++ b/src/xrpld/rpc/detail/PathRequest.h @@ -1,19 +1,31 @@ #pragma once +#include #include #include +#include +#include #include +#include #include -#include +#include +#include #include -#include +#include +#include +#include +#include #include +#include +#include #include +#include #include #include #include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/PathRequestManager.h b/src/xrpld/rpc/detail/PathRequestManager.h index c8e272a97d..94d126ed23 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.h +++ b/src/xrpld/rpc/detail/PathRequestManager.h @@ -4,7 +4,17 @@ #include #include +#include +#include +#include +#include +#include +#include + #include +#include +#include +#include #include #include diff --git a/src/xrpld/rpc/detail/Pathfinder.h b/src/xrpld/rpc/detail/Pathfinder.h index 36caab308e..5ef6c31b25 100644 --- a/src/xrpld/rpc/detail/Pathfinder.h +++ b/src/xrpld/rpc/detail/Pathfinder.h @@ -4,11 +4,25 @@ #include #include +#include +#include +#include #include -#include +#include +#include +#include #include #include #include +#include +#include + +#include +#include +#include +#include +#include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h b/src/xrpld/rpc/detail/PathfinderUtils.h index a703127148..ad48b3e9d7 100644 --- a/src/xrpld/rpc/detail/PathfinderUtils.h +++ b/src/xrpld/rpc/detail/PathfinderUtils.h @@ -1,6 +1,10 @@ #pragma once +#include +#include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 7e30b81fa6..e839d9d78d 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -1,5 +1,6 @@ #include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index bbc101a072..4a4dca42e5 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -1,16 +1,27 @@ #pragma once -#include #include #include #include -#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include // IWYU pragma: keep #include +#include #include +#include #include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 38827dc93c..6843c34b19 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,9 @@ #include #include +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h b/src/xrpld/rpc/detail/RPCLedgerHelpers.h index 9ec2a6673c..cbd47d38e6 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.h +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h @@ -1,17 +1,20 @@ #pragma once -#include #include #include #include +#include +#include #include -#include #include #include +#include + +#include #include -#include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/TransactionSign.h b/src/xrpld/rpc/detail/TransactionSign.h index 1fd9e87b54..cb3fb176dc 100644 --- a/src/xrpld/rpc/detail/TransactionSign.h +++ b/src/xrpld/rpc/detail/TransactionSign.h @@ -4,9 +4,14 @@ #include #include +#include #include #include +#include +#include +#include + namespace xrpl { // Forward declarations diff --git a/src/xrpld/rpc/detail/TrustLine.h b/src/xrpld/rpc/detail/TrustLine.h index 7a0a01d744..a80d81e4ae 100644 --- a/src/xrpld/rpc/detail/TrustLine.h +++ b/src/xrpld/rpc/detail/TrustLine.h @@ -1,13 +1,18 @@ #pragma once #include -#include +#include +#include +#include +#include +#include #include #include #include #include #include +#include namespace xrpl { diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index 12c09bcb15..5a9d546472 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -1,5 +1,7 @@ #pragma once +#include + /** Tuned constants. */ /** @{ */ namespace xrpl::RPC::Tuning { diff --git a/src/xrpld/rpc/detail/WSInfoSub.h b/src/xrpld/rpc/detail/WSInfoSub.h index c224b93e0e..cefb7ad2e4 100644 --- a/src/xrpld/rpc/detail/WSInfoSub.h +++ b/src/xrpld/rpc/detail/WSInfoSub.h @@ -7,8 +7,11 @@ #include #include +#include #include #include +#include +#include namespace xrpl { diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 2d39e42e02..7b347b2ecc 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -2,6 +2,8 @@ #include +#include + namespace xrpl { json::Value diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp index 0249963908..5ce10f6121 100644 --- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp index 40042a0390..c2a8319876 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp index 95da4c8567..e0204159fd 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.h b/src/xrpld/rpc/handlers/admin/status/GetCounts.h index 05751637ee..15cfa32a29 100644 --- a/src/xrpld/rpc/handlers/admin/status/GetCounts.h +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.h @@ -2,6 +2,8 @@ #include +#include + namespace xrpl { json::Value diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h b/src/xrpld/rpc/handlers/ledger/Ledger.h index 719e635170..59b64832f7 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.h +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h @@ -1,16 +1,18 @@ #pragma once -#include -#include #include +#include // IWYU pragma: keep #include #include #include #include +#include #include #include -#include + +#include +#include namespace json { class Object; diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h index 11f6553dfa..57c4e58242 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h @@ -2,18 +2,25 @@ #include +#include #include -#include +#include #include -#include +#include +#include +#include #include -#include -#include +#include #include #include +#include +#include #include -#include +#include +#include +#include +#include namespace xrpl::LedgerEntryHelpers { diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp index 947a257916..abad196246 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp @@ -4,6 +4,7 @@ #include #include +#include // IWYU pragma: keep #include diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp index 6a75277b1b..c1c8712655 100644 --- a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h index 42b99a0009..70bc258d77 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h @@ -5,15 +5,25 @@ #include #include +#include +#include #include #include -#include #include +#include #include +#include +#include +#include #include +#include #include #include +#include +#include +#include + namespace xrpl { inline void diff --git a/src/xrpld/rpc/handlers/server_info/Version.h b/src/xrpld/rpc/handlers/server_info/Version.h index 1868df5d40..f25d8679ba 100644 --- a/src/xrpld/rpc/handlers/server_info/Version.h +++ b/src/xrpld/rpc/handlers/server_info/Version.h @@ -1,5 +1,12 @@ #pragma once +#include // IWYU pragma: keep +#include +#include +#include +#include + +#include #include namespace xrpl::RPC { diff --git a/src/xrpld/rpc/json_body.h b/src/xrpld/rpc/json_body.h index 5c4c56cef8..49c2b0e6e0 100644 --- a/src/xrpld/rpc/json_body.h +++ b/src/xrpld/rpc/json_body.h @@ -6,6 +6,11 @@ #include #include +#include +#include +#include +#include + namespace xrpl { /// Body that holds JSON diff --git a/src/xrpld/shamap/NodeFamily.cpp b/src/xrpld/shamap/NodeFamily.cpp index a1668e80b2..2e48117d6a 100644 --- a/src/xrpld/shamap/NodeFamily.cpp +++ b/src/xrpld/shamap/NodeFamily.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/shamap/NodeFamily.h b/src/xrpld/shamap/NodeFamily.h index e0655292a0..d532f13ecc 100644 --- a/src/xrpld/shamap/NodeFamily.h +++ b/src/xrpld/shamap/NodeFamily.h @@ -2,8 +2,17 @@ #include +#include +#include +#include #include #include +#include +#include + +#include +#include +#include namespace xrpl { From 4c619e8a85cc1ca162a9d12522548d51c29c8170 Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Thu, 2 Jul 2026 10:17:32 -0400 Subject: [PATCH 20/31] refactor: Retire DisallowIncomingV1 fix (#7364) --- include/xrpl/protocol/detail/features.macro | 2 +- src/libxrpl/tx/transactors/token/TrustSet.cpp | 18 +---- src/test/app/TrustSet_test.cpp | 69 +++++++++---------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 7e64a49b0c..a7a62f0322 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -60,7 +60,6 @@ XRPL_FEATURE(PriceOracle, Supported::Yes, VoteBehavior::DefaultNo XRPL_FIX (AMMOverflowOffer, Supported::Yes, VoteBehavior::DefaultYes) XRPL_FIX (FillOrKill, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DID, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMM, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XRPFees, Supported::Yes, VoteBehavior::DefaultNo) @@ -100,6 +99,7 @@ XRPL_RETIRE_FIX(1623) XRPL_RETIRE_FIX(1781) XRPL_RETIRE_FIX(AmendmentMajorityCalc) XRPL_RETIRE_FIX(CheckThreading) +XRPL_RETIRE_FIX(DisallowIncomingV1) XRPL_RETIRE_FIX(InnerObjTemplate) XRPL_RETIRE_FIX(MasterKeyAsRegularKey) XRPL_RETIRE_FIX(NonFungibleTokensV1_2) diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 151e82bab9..e79e3cad5a 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -184,22 +184,10 @@ TrustSet::preclaim(PreclaimContext const& ctx) // If the destination has opted to disallow incoming trustlines // then honour that flag - if (sleDst->isFlag(lsfDisallowIncomingTrustline)) + if (sleDst && sleDst->isFlag(lsfDisallowIncomingTrustline) && + !ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency))) { - // The original implementation of featureDisallowIncoming was - // too restrictive. If - // o fixDisallowIncomingV1 is enabled and - // o The trust line already exists - // Then allow the TrustSet. - if (ctx.view.rules().enabled(fixDisallowIncomingV1) && - ctx.view.exists(keylet::trustLine(id, uDstAccountID, currency))) - { - // pass - } - else - { - return tecNO_PERMISSION; - } + return tecNO_PERMISSION; } // In general, trust lines to pseudo accounts are not permitted, unless diff --git a/src/test/app/TrustSet_test.cpp b/src/test/app/TrustSet_test.cpp index e9f0553840..15abc3bd06 100644 --- a/src/test/app/TrustSet_test.cpp +++ b/src/test/app/TrustSet_test.cpp @@ -474,6 +474,38 @@ public: checkQuality(!createQuality); } + void + testDisallowIncomingWithRequireAuth() + { + testcase("Create trustline with disallow incoming requiring auth"); + + using namespace test::jtx; + + Env env{*this}; + auto const dist = Account("dist"); + auto const gw = Account("gw"); + auto const usd = gw["USD"]; + auto const distUSD = dist["USD"]; + + env.fund(XRP(1000), gw, dist); + env.close(); + + env(fset(gw, asfRequireAuth)); + env.close(); + + env(fset(dist, asfDisallowIncomingTrustline)); + env.close(); + + env(trust(dist, usd(10000))); + env.close(); + + env(trust(gw, distUSD(10000)), Txflags(tfSetfAuth), Ter(tesSUCCESS)); + env.close(); + + env(pay(gw, dist, usd(1000)), Ter(tesSUCCESS)); + env.close(); + } + void testDisallowIncoming(FeatureBitset features) { @@ -481,42 +513,6 @@ public: using namespace test::jtx; - // fixDisallowIncomingV1 - { - for (bool const withFix : {true, false}) - { - auto const amend = withFix ? features : features - fixDisallowIncomingV1; - - Env env{*this, amend}; - auto const dist = Account("dist"); - auto const gw = Account("gw"); - auto const usd = gw["USD"]; - auto const distUSD = dist["USD"]; - - env.fund(XRP(1000), gw, dist); - env.close(); - - env(fset(gw, asfRequireAuth)); - env.close(); - - env(fset(dist, asfDisallowIncomingTrustline)); - env.close(); - - env(trust(dist, usd(10000))); - env.close(); - - // withFix: can set trustline - // withOutFix: cannot set trustline - auto const trustResult = withFix ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION); - env(trust(gw, distUSD(10000)), Txflags(tfSetfAuth), trustResult); - env.close(); - - auto const txResult = withFix ? Ter(tesSUCCESS) : Ter(tecPATH_DRY); - env(pay(gw, dist, usd(1000)), txResult); - env.close(); - } - } - Env env{*this, features}; auto const gw = Account{"gateway"}; @@ -601,6 +597,7 @@ public: testModifyQualityOfTrustline(features, false, true); testModifyQualityOfTrustline(features, true, false); testModifyQualityOfTrustline(features, true, true); + testDisallowIncomingWithRequireAuth(); testDisallowIncoming(features); testTrustLineResetWithAuthFlag(); testTrustLineDelete(); From 3b9e24e0e0bf3a5941808446ac1fca28bd8e8e69 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 2 Jul 2026 11:01:30 -0400 Subject: [PATCH 21/31] chore: Improve pre-commit hooks (#7702) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- cspell.config.yaml => .cspell.config.yaml | 4 +--- .pre-commit-config.yaml | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) rename cspell.config.yaml => .cspell.config.yaml (96%) diff --git a/cspell.config.yaml b/.cspell.config.yaml similarity index 96% rename from cspell.config.yaml rename to .cspell.config.yaml index c120c31855..c558fc0984 100644 --- a/cspell.config.yaml +++ b/.cspell.config.yaml @@ -36,9 +36,7 @@ overrides: - /'[^']*'/g # single-quoted strings - /`[^`]*`/g # backtick strings suggestWords: - - xprl->xrpl - - xprld->xrpld # cspell: disable-line not sure what this problem is.... - - unsynched->unsynced # cspell: disable-line not sure what this problem is.... + - unsynched->unsynced - synched->synced - synch->sync words: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e4521870d..910bda8d4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,20 +90,19 @@ repos: - repo: https://github.com/streetsidesoftware/cspell-cli rev: ea11f9efc0bec520073405bc30552da887ba71bc # frozen: v10.0.1 hooks: - - id: cspell # Spell check changed files + - id: cspell + name: check changed files spelling exclude: | (?x)^( - .config/cspell.config.yaml| + \.cspell\.config\.yaml| include/xrpl/protocol_autogen/(transactions|ledger_entries)/.* )$ - - id: cspell # Spell check the commit message + - id: cspell name: check commit message spelling args: - --no-must-find-files - --no-progress - --no-summary - - --files - - .git/COMMIT_EDITMSG stages: [commit-msg] - repo: local From 6f0f5b8bb36c30c2578a62d1b2a5befa736da4c4 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 2 Jul 2026 17:26:09 +0100 Subject: [PATCH 22/31] chore: Make clang-tidy happy on macOS (#7701) --- BUILD.md | 2 +- bin/pre-commit/clang_tidy_check.py | 8 +++++--- include/xrpl/beast/unit_test/suite_list.h | 6 +++--- include/xrpl/ledger/ApplyView.h | 2 +- include/xrpl/ledger/ReadView.h | 2 +- include/xrpl/ledger/helpers/LendingHelpers.h | 2 +- include/xrpl/protocol/AmountConversions.h | 2 +- include/xrpl/tx/paths/detail/EitherAmount.h | 2 +- src/libxrpl/ledger/helpers/NFTokenHelpers.cpp | 2 +- src/libxrpl/ledger/helpers/OfferHelpers.cpp | 2 +- src/libxrpl/ledger/helpers/VaultHelpers.cpp | 2 +- src/libxrpl/protocol/Quality.cpp | 4 ++-- src/libxrpl/protocol/STInteger.cpp | 2 +- src/libxrpl/protocol/STTx.cpp | 1 + src/libxrpl/protocol/STValidation.cpp | 2 +- src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp | 2 +- src/test/app/PayChan_test.cpp | 2 +- src/test/app/Vault_test.cpp | 2 +- src/test/beast/beast_io_latency_probe_test.cpp | 2 +- src/test/jtx/impl/amount.cpp | 2 +- src/test/rpc/DepositAuthorized_test.cpp | 2 +- src/xrpld/app/ledger/detail/BuildLedger.cpp | 4 ++-- src/xrpld/app/ledger/detail/InboundLedger.cpp | 4 ++-- src/xrpld/app/ledger/detail/LedgerPersistence.cpp | 4 ++-- src/xrpld/app/ledger/detail/OpenLedger.cpp | 2 +- src/xrpld/app/main/Application.cpp | 4 ++-- src/xrpld/app/misc/FeeVoteImpl.cpp | 2 +- src/xrpld/overlay/detail/PeerImp.cpp | 1 + 28 files changed, 39 insertions(+), 35 deletions(-) diff --git a/BUILD.md b/BUILD.md index 6ccdde12d5..a15c94edc9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -25,7 +25,7 @@ You can verify that the required tools are installed and runnable with: | ----------- | --------------- | | GCC | 15.2 | | Clang | 22 | -| Apple Clang | 17 | +| Apple Clang | 21 | | MSVC | 19.44[^windows] | ## Operating Systems diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py index d074c56acf..5b5792b405 100755 --- a/bin/pre-commit/clang_tidy_check.py +++ b/bin/pre-commit/clang_tidy_check.py @@ -17,9 +17,11 @@ import subprocess import sys from pathlib import Path +CLANG_TIDY_VERSION = 22 + def find_run_clang_tidy() -> str | None: - for candidate in ("run-clang-tidy-21", "run-clang-tidy"): + for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"): if path := shutil.which(candidate): return path return None @@ -44,8 +46,8 @@ def main(): run_clang_tidy = find_run_clang_tidy() if not run_clang_tidy: print( - "clang-tidy check failed: TIDY is enabled but neither " - "'run-clang-tidy-21' nor 'run-clang-tidy' was found in PATH.", + f"clang-tidy check failed: TIDY is enabled but neither " + f"'run-clang-tidy-{CLANG_TIDY_VERSION}' nor 'run-clang-tidy' was found in PATH.", file=sys.stderr, ) return 1 diff --git a/include/xrpl/beast/unit_test/suite_list.h b/include/xrpl/beast/unit_test/suite_list.h index cf9fb9c5b1..7dd0dd80f0 100644 --- a/include/xrpl/beast/unit_test/suite_list.h +++ b/include/xrpl/beast/unit_test/suite_list.h @@ -10,9 +10,9 @@ #include #include -#include -#include -#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep namespace beast::unit_test { diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index 3bf5d479d1..e519013d9a 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/include/xrpl/ledger/ReadView.h b/include/xrpl/ledger/ReadView.h index 8bbd3e06cb..724533039b 100644 --- a/include/xrpl/ledger/ReadView.h +++ b/include/xrpl/ledger/ReadView.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index c21e5bf0ce..873abae272 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -8,7 +8,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 66ada68d6f..3bcd80e827 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -13,7 +13,7 @@ #include #include -#include +#include // IWYU pragma: keep #include #include diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h b/include/xrpl/tx/paths/detail/EitherAmount.h index 68ad90d2d4..bc9488d5db 100644 --- a/include/xrpl/tx/paths/detail/EitherAmount.h +++ b/include/xrpl/tx/paths/detail/EitherAmount.h @@ -6,7 +6,7 @@ #include // IWYU pragma: keep #include -#include +#include // IWYU pragma: keep #include #include diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index af429358bb..6dca715a8c 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -24,7 +24,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp b/src/libxrpl/ledger/helpers/OfferHelpers.cpp index 5249870143..6e72b71564 100644 --- a/src/libxrpl/ledger/helpers/OfferHelpers.cpp +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index 3a3a756499..b5b076d1cb 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/protocol/Quality.cpp b/src/libxrpl/protocol/Quality.cpp index 7ad426bef7..dde7921a76 100644 --- a/src/libxrpl/protocol/Quality.cpp +++ b/src/libxrpl/protocol/Quality.cpp @@ -1,12 +1,12 @@ #include -#include +#include // IWYU pragma: keep #include #include #include #include -#include +#include // IWYU pragma: keep namespace xrpl { diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index 5f3fb6ffa4..b65504dd5f 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include // IWYU pragma: keep namespace xrpl { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index cd2da12316..b3de717da8 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -596,6 +596,7 @@ STTx::getBatchTransactionIDs() const XRPL_ASSERT( batchTxnIds_->size() == getFieldArray(sfRawTransactions).size(), "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access): guarded by assert above return *batchTxnIds_; } diff --git a/src/libxrpl/protocol/STValidation.cpp b/src/libxrpl/protocol/STValidation.cpp index 5eafb407ec..1656aad3a2 100644 --- a/src/libxrpl/protocol/STValidation.cpp +++ b/src/libxrpl/protocol/STValidation.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp index 3d30005876..ad91c55723 100644 --- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index fd2c7790d5..f1a9506e0c 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index f6574872f9..3877e08f7e 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -7542,7 +7542,7 @@ class Vault_test : public beast::unit_test::Suite // Transaction fails if the data field is set, but is empty { testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty"); - delTx[sfMemoData] = strHex(std::string(0, 'A')); + delTx[sfMemoData] = strHex(std::string()); env(delTx, Ter(temMALFORMED)); env.close(); } diff --git a/src/test/beast/beast_io_latency_probe_test.cpp b/src/test/beast/beast_io_latency_probe_test.cpp index 9a93968dab..807ad81c49 100644 --- a/src/test/beast/beast_io_latency_probe_test.cpp +++ b/src/test/beast/beast_io_latency_probe_test.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index b07703dace..f90102245a 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/test/rpc/DepositAuthorized_test.cpp b/src/test/rpc/DepositAuthorized_test.cpp index e6720602c9..6d6b54d70a 100644 --- a/src/test/rpc/DepositAuthorized_test.cpp +++ b/src/test/rpc/DepositAuthorized_test.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index 038a7be4b7..d11e0610ba 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -13,10 +13,10 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include -#include +#include // IWYU pragma: keep #include #include diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 4affffd1c1..4d34f60374 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -21,11 +21,11 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp index 3561b66951..6baf64e923 100644 --- a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp +++ b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp @@ -8,9 +8,9 @@ #include #include #include -#include +#include // IWYU pragma: keep #include -#include +#include // IWYU pragma: keep #include #include diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 60599c80d3..4d8a37cdf1 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index b99c98afa1..594195b93e 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -79,11 +79,11 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include #include -#include +#include // IWYU pragma: keep #include #include #include diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp index 363c17f4fa..76a4d8f186 100644 --- a/src/xrpld/app/misc/FeeVoteImpl.cpp +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 5ac61cfd91..83e4d9c851 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2134,6 +2134,7 @@ PeerImp::onValidatorListMessage( publisherListSequences_[pubKey] = applyResult.sequence; } break; + // NOLINTNEXTLINE(bugprone-branch-clone): identical to the next branch only in Release case ListDisposition::SameSequence: case ListDisposition::KnownSequence: #ifndef NDEBUG From 41622b87aeeb6d5e9f03f12d0a6847e0a04e6174 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 2 Jul 2026 19:30:59 +0100 Subject: [PATCH 23/31] chore: Enable modernize-unary-static-assert (#7705) --- .clang-tidy | 1 - include/xrpl/basics/base_uint.h | 2 +- include/xrpl/beast/utility/Journal.h | 48 ++-- include/xrpl/ledger/CachedView.h | 2 +- include/xrpl/ledger/detail/ReadViewFwdRange.h | 4 +- include/xrpl/protocol/STObject.h | 2 +- include/xrpl/protocol/Serializer.h | 2 +- src/libxrpl/protocol/Serializer.cpp | 2 +- src/libxrpl/protocol/digest.cpp | 6 +- src/test/app/PayStrand_test.cpp | 2 +- src/test/basics/Buffer_test.cpp | 4 +- src/test/jtx/Env_test.cpp | 8 +- src/test/protocol/Quality_test.cpp | 4 +- src/test/protocol/STObject_test.cpp | 6 +- src/test/protocol/SeqProxy_test.cpp | 220 +++++++++--------- src/test/shamap/SHAMap_test.cpp | 88 +++---- src/xrpld/app/misc/detail/TxQ.cpp | 8 +- 17 files changed, 203 insertions(+), 206 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 84849db7a0..2b0b7e4418 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -80,7 +80,6 @@ Checks: "-*, -modernize-replace-random-shuffle, -modernize-return-braced-init-list, -modernize-shrink-to-fit, - -modernize-unary-static-assert, -modernize-use-auto, -modernize-use-bool-literals, -modernize-use-constraints, diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index e6ca1993f9..c60fbf35b4 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -97,7 +97,7 @@ public: // static constexpr std::size_t kBytes = Bits / 8; - static_assert(sizeof(data_) == kBytes, ""); + static_assert(sizeof(data_) == kBytes); using size_type = std::size_t; using difference_type = std::ptrdiff_t; diff --git a/include/xrpl/beast/utility/Journal.h b/include/xrpl/beast/utility/Journal.h index ac08b1384b..3de3cfb0e0 100644 --- a/include/xrpl/beast/utility/Journal.h +++ b/include/xrpl/beast/utility/Journal.h @@ -108,12 +108,12 @@ public: }; #ifndef __INTELLISENSE__ - static_assert(!std::is_default_constructible_v, ""); - static_assert(!std::is_copy_constructible_v, ""); - static_assert(!std::is_move_constructible_v, ""); - static_assert(!std::is_copy_assignable_v, ""); - static_assert(!std::is_move_assignable_v, ""); - static_assert(std::is_nothrow_destructible_v, ""); + static_assert(!std::is_default_constructible_v); + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_assignable_v); + static_assert(std::is_nothrow_destructible_v); #endif /** Returns a Sink which does nothing. */ @@ -164,12 +164,12 @@ public: }; #ifndef __INTELLISENSE__ - static_assert(!std::is_default_constructible_v, ""); - static_assert(std::is_copy_constructible_v, ""); - static_assert(std::is_move_constructible_v, ""); - static_assert(!std::is_copy_assignable_v, ""); - static_assert(!std::is_move_assignable_v, ""); - static_assert(std::is_nothrow_destructible_v, ""); + static_assert(!std::is_default_constructible_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_assignable_v); + static_assert(std::is_nothrow_destructible_v); #endif //-------------------------------------------------------------------------- @@ -246,12 +246,12 @@ public: }; #ifndef __INTELLISENSE__ - static_assert(std::is_default_constructible_v, ""); - static_assert(std::is_copy_constructible_v, ""); - static_assert(std::is_move_constructible_v, ""); - static_assert(!std::is_copy_assignable_v, ""); - static_assert(!std::is_move_assignable_v, ""); - static_assert(std::is_nothrow_destructible_v, ""); + static_assert(std::is_default_constructible_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_assignable_v); + static_assert(std::is_nothrow_destructible_v); #endif //-------------------------------------------------------------------------- @@ -329,12 +329,12 @@ public: }; #ifndef __INTELLISENSE__ -static_assert(!std::is_default_constructible_v, ""); -static_assert(std::is_copy_constructible_v, ""); -static_assert(std::is_move_constructible_v, ""); -static_assert(std::is_copy_assignable_v, ""); -static_assert(std::is_move_assignable_v, ""); -static_assert(std::is_nothrow_destructible_v, ""); +static_assert(!std::is_default_constructible_v); +static_assert(std::is_copy_constructible_v); +static_assert(std::is_move_constructible_v); +static_assert(std::is_copy_assignable_v); +static_assert(std::is_move_assignable_v); +static_assert(std::is_nothrow_destructible_v); #endif //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/CachedView.h b/include/xrpl/ledger/CachedView.h index f83c3e1297..1da3a67563 100644 --- a/include/xrpl/ledger/CachedView.h +++ b/include/xrpl/ledger/CachedView.h @@ -141,7 +141,7 @@ template class CachedView : public detail::CachedViewImpl { private: - static_assert(std::is_base_of_v, ""); + static_assert(std::is_base_of_v); std::shared_ptr sp_; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index 82d8b59c6a..19ac0698c2 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -108,8 +108,8 @@ public: std::optional mutable cache_; }; - static_assert(std::is_nothrow_move_constructible{}, ""); - static_assert(std::is_nothrow_move_assignable{}, ""); + static_assert(std::is_nothrow_move_constructible{}); + static_assert(std::is_nothrow_move_assignable{}); using const_iterator = Iterator; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c254a37aaf..a60e8f7fe8 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -1241,7 +1241,7 @@ template void STObject::setFieldUsingSetValue(SField const& field, V value) { - static_assert(!std::is_lvalue_reference_v, ""); + static_assert(!std::is_lvalue_reference_v); STBase* rf = getPField(field, true); diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 1d0453d6aa..73bd9c8289 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -334,7 +334,7 @@ public: template explicit SerialIter(std::uint8_t const (&data)[N]) : SerialIter(&data[0], N) { - static_assert(N > 0, ""); + static_assert(N > 0); } [[nodiscard]] bool diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp index d500919df9..1eda04705f 100644 --- a/src/libxrpl/protocol/Serializer.cpp +++ b/src/libxrpl/protocol/Serializer.cpp @@ -444,7 +444,7 @@ template T SerialIter::getRawHelper(int size) { - static_assert(std::is_same_v || std::is_same_v, ""); + static_assert(std::is_same_v || std::is_same_v); if (remain_ < size) Throw("invalid SerialIter getRaw"); T result(size); diff --git a/src/libxrpl/protocol/digest.cpp b/src/libxrpl/protocol/digest.cpp index 2e1b2b25cf..1d84f8779e 100644 --- a/src/libxrpl/protocol/digest.cpp +++ b/src/libxrpl/protocol/digest.cpp @@ -9,7 +9,7 @@ namespace xrpl { OpensslRipemd160Hasher::OpensslRipemd160Hasher() { - static_assert(sizeof(decltype(OpensslRipemd160Hasher::ctx_)) == sizeof(RIPEMD160_CTX), ""); + static_assert(sizeof(decltype(OpensslRipemd160Hasher::ctx_)) == sizeof(RIPEMD160_CTX)); auto const ctx = reinterpret_cast(ctx_); RIPEMD160_Init(ctx); } @@ -34,7 +34,7 @@ operator result_type() noexcept OpensslSha512Hasher::OpensslSha512Hasher() { - static_assert(sizeof(decltype(OpensslSha512Hasher::ctx_)) == sizeof(SHA512_CTX), ""); + static_assert(sizeof(decltype(OpensslSha512Hasher::ctx_)) == sizeof(SHA512_CTX)); auto const ctx = reinterpret_cast(ctx_); SHA512_Init(ctx); } @@ -59,7 +59,7 @@ operator result_type() noexcept OpensslSha256Hasher::OpensslSha256Hasher() { - static_assert(sizeof(decltype(OpensslSha256Hasher::ctx_)) == sizeof(SHA256_CTX), ""); + static_assert(sizeof(decltype(OpensslSha256Hasher::ctx_)) == sizeof(SHA256_CTX)); auto const ctx = reinterpret_cast(ctx_); SHA256_Init(ctx); } diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 11a0e5bab0..ddbcfe8b70 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -112,7 +112,7 @@ class ElementComboIter }; std::uint16_t state_ = 0; - static_assert(safeCast(SB::Last) <= sizeof(decltype(state_)) * 8, ""); + static_assert(safeCast(SB::Last) <= sizeof(decltype(state_)) * 8); STPathElement const* prev_ = nullptr; // disallow iss and cur to be specified with acc is specified (simplifies // some tests) diff --git a/src/test/basics/Buffer_test.cpp b/src/test/basics/Buffer_test.cpp index 816a697ca4..c748a3f9dd 100644 --- a/src/test/basics/Buffer_test.cpp +++ b/src/test/basics/Buffer_test.cpp @@ -103,8 +103,8 @@ struct Buffer_test : beast::unit_test::Suite { testcase("Move Construction / Assignment"); - static_assert(std::is_nothrow_move_constructible_v, ""); - static_assert(std::is_nothrow_move_assignable_v, ""); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); { // Move-construct from empty buf Buffer x; diff --git a/src/test/jtx/Env_test.cpp b/src/test/jtx/Env_test.cpp index d82cb86b36..47cfb604f4 100644 --- a/src/test/jtx/Env_test.cpp +++ b/src/test/jtx/Env_test.cpp @@ -110,10 +110,10 @@ public: PrettyAmount(0u); // NOLINT(bugprone-unused-raii) PrettyAmount(1u); // NOLINT(bugprone-unused-raii) PrettyAmount(-1); // NOLINT(bugprone-unused-raii) - static_assert(!std::is_trivially_constructible_v, ""); - static_assert(!std::is_trivially_constructible_v, ""); - static_assert(!std::is_trivially_constructible_v, ""); - static_assert(!std::is_trivially_constructible_v, ""); + static_assert(!std::is_trivially_constructible_v); + static_assert(!std::is_trivially_constructible_v); + static_assert(!std::is_trivially_constructible_v); + static_assert(!std::is_trivially_constructible_v); try { diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp index df95cea8ef..db81886e94 100644 --- a/src/test/protocol/Quality_test.cpp +++ b/src/test/protocol/Quality_test.cpp @@ -24,7 +24,7 @@ public: static STAmount amount(Integer integer, std::enable_if_t>* = 0) { - static_assert(std::is_integral_v, ""); + static_assert(std::is_integral_v); return STAmount(integer, false); } @@ -32,7 +32,7 @@ public: static STAmount amount(Integer integer, std::enable_if_t>* = 0) { - static_assert(std::is_integral_v, ""); + static_assert(std::is_integral_v); if (integer < 0) return STAmount(-integer, true); return STAmount(integer, false); diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index b823b24962..8b7db632c5 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -356,8 +356,7 @@ public: { STObject st(sfGeneric); auto const v = ~st[~sf1Outer]; - static_assert( - std::is_same_v, std::optional>, ""); + static_assert(std::is_same_v, std::optional>); } // UDT scalar fields @@ -431,8 +430,7 @@ public: BEAST_EXPECT(cst[~sf]->size() == 2); // NOLINT(bugprone-unchecked-optional-access) BEAST_EXPECT(cst[sf][0] == 1); BEAST_EXPECT(cst[sf][1] == 2); - static_assert( - std::is_same_v const&>, ""); + static_assert(std::is_same_v const&>); } // Default by reference field diff --git a/src/test/protocol/SeqProxy_test.cpp b/src/test/protocol/SeqProxy_test.cpp index 90345ddc22..44aab41992 100644 --- a/src/test/protocol/SeqProxy_test.cpp +++ b/src/test/protocol/SeqProxy_test.cpp @@ -81,128 +81,128 @@ struct SeqProxy_test : public beast::unit_test::Suite static constexpr SeqProxy kTicBig{kTicket, kUintMax}; // Verify operation of value(), isSeq() and isTicket(). - static_assert(expectValues(kSeqZero, 0, kSeq), ""); - static_assert(expectValues(kSeqSmall, 1, kSeq), ""); - static_assert(expectValues(kSeqMiD0, 2, kSeq), ""); - static_assert(expectValues(kSeqMiD1, 2, kSeq), ""); - static_assert(expectValues(kSeqBig, kUintMax, kSeq), ""); + static_assert(expectValues(kSeqZero, 0, kSeq)); + static_assert(expectValues(kSeqSmall, 1, kSeq)); + static_assert(expectValues(kSeqMiD0, 2, kSeq)); + static_assert(expectValues(kSeqMiD1, 2, kSeq)); + static_assert(expectValues(kSeqBig, kUintMax, kSeq)); - static_assert(expectValues(kTicZero, 0, kTicket), ""); - static_assert(expectValues(kTicSmall, 1, kTicket), ""); - static_assert(expectValues(kTicMid0, 2, kTicket), ""); - static_assert(expectValues(kTicMid1, 2, kTicket), ""); - static_assert(expectValues(kTicBig, kUintMax, kTicket), ""); + static_assert(expectValues(kTicZero, 0, kTicket)); + static_assert(expectValues(kTicSmall, 1, kTicket)); + static_assert(expectValues(kTicMid0, 2, kTicket)); + static_assert(expectValues(kTicMid1, 2, kTicket)); + static_assert(expectValues(kTicBig, kUintMax, kTicket)); // Verify expected behavior of comparison operators. - static_assert(expectEq(kSeqZero, kSeqZero), ""); - static_assert(expectLt(kSeqZero, kSeqSmall), ""); - static_assert(expectLt(kSeqZero, kSeqMiD0), ""); - static_assert(expectLt(kSeqZero, kSeqMiD1), ""); - static_assert(expectLt(kSeqZero, kSeqBig), ""); - static_assert(expectLt(kSeqZero, kTicZero), ""); - static_assert(expectLt(kSeqZero, kTicSmall), ""); - static_assert(expectLt(kSeqZero, kTicMid0), ""); - static_assert(expectLt(kSeqZero, kTicMid1), ""); - static_assert(expectLt(kSeqZero, kTicBig), ""); + static_assert(expectEq(kSeqZero, kSeqZero)); + static_assert(expectLt(kSeqZero, kSeqSmall)); + static_assert(expectLt(kSeqZero, kSeqMiD0)); + static_assert(expectLt(kSeqZero, kSeqMiD1)); + static_assert(expectLt(kSeqZero, kSeqBig)); + static_assert(expectLt(kSeqZero, kTicZero)); + static_assert(expectLt(kSeqZero, kTicSmall)); + static_assert(expectLt(kSeqZero, kTicMid0)); + static_assert(expectLt(kSeqZero, kTicMid1)); + static_assert(expectLt(kSeqZero, kTicBig)); - static_assert(expectGt(kSeqSmall, kSeqZero), ""); - static_assert(expectEq(kSeqSmall, kSeqSmall), ""); - static_assert(expectLt(kSeqSmall, kSeqMiD0), ""); - static_assert(expectLt(kSeqSmall, kSeqMiD1), ""); - static_assert(expectLt(kSeqSmall, kSeqBig), ""); - static_assert(expectLt(kSeqSmall, kTicZero), ""); - static_assert(expectLt(kSeqSmall, kTicSmall), ""); - static_assert(expectLt(kSeqSmall, kTicMid0), ""); - static_assert(expectLt(kSeqSmall, kTicMid1), ""); - static_assert(expectLt(kSeqSmall, kTicBig), ""); + static_assert(expectGt(kSeqSmall, kSeqZero)); + static_assert(expectEq(kSeqSmall, kSeqSmall)); + static_assert(expectLt(kSeqSmall, kSeqMiD0)); + static_assert(expectLt(kSeqSmall, kSeqMiD1)); + static_assert(expectLt(kSeqSmall, kSeqBig)); + static_assert(expectLt(kSeqSmall, kTicZero)); + static_assert(expectLt(kSeqSmall, kTicSmall)); + static_assert(expectLt(kSeqSmall, kTicMid0)); + static_assert(expectLt(kSeqSmall, kTicMid1)); + static_assert(expectLt(kSeqSmall, kTicBig)); - static_assert(expectGt(kSeqMiD0, kSeqZero), ""); - static_assert(expectGt(kSeqMiD0, kSeqSmall), ""); - static_assert(expectEq(kSeqMiD0, kSeqMiD0), ""); - static_assert(expectEq(kSeqMiD0, kSeqMiD1), ""); - static_assert(expectLt(kSeqMiD0, kSeqBig), ""); - static_assert(expectLt(kSeqMiD0, kTicZero), ""); - static_assert(expectLt(kSeqMiD0, kTicSmall), ""); - static_assert(expectLt(kSeqMiD0, kTicMid0), ""); - static_assert(expectLt(kSeqMiD0, kTicMid1), ""); - static_assert(expectLt(kSeqMiD0, kTicBig), ""); + static_assert(expectGt(kSeqMiD0, kSeqZero)); + static_assert(expectGt(kSeqMiD0, kSeqSmall)); + static_assert(expectEq(kSeqMiD0, kSeqMiD0)); + static_assert(expectEq(kSeqMiD0, kSeqMiD1)); + static_assert(expectLt(kSeqMiD0, kSeqBig)); + static_assert(expectLt(kSeqMiD0, kTicZero)); + static_assert(expectLt(kSeqMiD0, kTicSmall)); + static_assert(expectLt(kSeqMiD0, kTicMid0)); + static_assert(expectLt(kSeqMiD0, kTicMid1)); + static_assert(expectLt(kSeqMiD0, kTicBig)); - static_assert(expectGt(kSeqMiD1, kSeqZero), ""); - static_assert(expectGt(kSeqMiD1, kSeqSmall), ""); - static_assert(expectEq(kSeqMiD1, kSeqMiD0), ""); - static_assert(expectEq(kSeqMiD1, kSeqMiD1), ""); - static_assert(expectLt(kSeqMiD1, kSeqBig), ""); - static_assert(expectLt(kSeqMiD1, kTicZero), ""); - static_assert(expectLt(kSeqMiD1, kTicSmall), ""); - static_assert(expectLt(kSeqMiD1, kTicMid0), ""); - static_assert(expectLt(kSeqMiD1, kTicMid1), ""); - static_assert(expectLt(kSeqMiD1, kTicBig), ""); + static_assert(expectGt(kSeqMiD1, kSeqZero)); + static_assert(expectGt(kSeqMiD1, kSeqSmall)); + static_assert(expectEq(kSeqMiD1, kSeqMiD0)); + static_assert(expectEq(kSeqMiD1, kSeqMiD1)); + static_assert(expectLt(kSeqMiD1, kSeqBig)); + static_assert(expectLt(kSeqMiD1, kTicZero)); + static_assert(expectLt(kSeqMiD1, kTicSmall)); + static_assert(expectLt(kSeqMiD1, kTicMid0)); + static_assert(expectLt(kSeqMiD1, kTicMid1)); + static_assert(expectLt(kSeqMiD1, kTicBig)); - static_assert(expectGt(kSeqBig, kSeqZero), ""); - static_assert(expectGt(kSeqBig, kSeqSmall), ""); - static_assert(expectGt(kSeqBig, kSeqMiD0), ""); - static_assert(expectGt(kSeqBig, kSeqMiD1), ""); - static_assert(expectEq(kSeqBig, kSeqBig), ""); - static_assert(expectLt(kSeqBig, kTicZero), ""); - static_assert(expectLt(kSeqBig, kTicSmall), ""); - static_assert(expectLt(kSeqBig, kTicMid0), ""); - static_assert(expectLt(kSeqBig, kTicMid1), ""); - static_assert(expectLt(kSeqBig, kTicBig), ""); + static_assert(expectGt(kSeqBig, kSeqZero)); + static_assert(expectGt(kSeqBig, kSeqSmall)); + static_assert(expectGt(kSeqBig, kSeqMiD0)); + static_assert(expectGt(kSeqBig, kSeqMiD1)); + static_assert(expectEq(kSeqBig, kSeqBig)); + static_assert(expectLt(kSeqBig, kTicZero)); + static_assert(expectLt(kSeqBig, kTicSmall)); + static_assert(expectLt(kSeqBig, kTicMid0)); + static_assert(expectLt(kSeqBig, kTicMid1)); + static_assert(expectLt(kSeqBig, kTicBig)); - static_assert(expectGt(kTicZero, kSeqZero), ""); - static_assert(expectGt(kTicZero, kSeqSmall), ""); - static_assert(expectGt(kTicZero, kSeqMiD0), ""); - static_assert(expectGt(kTicZero, kSeqMiD1), ""); - static_assert(expectGt(kTicZero, kSeqBig), ""); - static_assert(expectEq(kTicZero, kTicZero), ""); - static_assert(expectLt(kTicZero, kTicSmall), ""); - static_assert(expectLt(kTicZero, kTicMid0), ""); - static_assert(expectLt(kTicZero, kTicMid1), ""); - static_assert(expectLt(kTicZero, kTicBig), ""); + static_assert(expectGt(kTicZero, kSeqZero)); + static_assert(expectGt(kTicZero, kSeqSmall)); + static_assert(expectGt(kTicZero, kSeqMiD0)); + static_assert(expectGt(kTicZero, kSeqMiD1)); + static_assert(expectGt(kTicZero, kSeqBig)); + static_assert(expectEq(kTicZero, kTicZero)); + static_assert(expectLt(kTicZero, kTicSmall)); + static_assert(expectLt(kTicZero, kTicMid0)); + static_assert(expectLt(kTicZero, kTicMid1)); + static_assert(expectLt(kTicZero, kTicBig)); - static_assert(expectGt(kTicSmall, kSeqZero), ""); - static_assert(expectGt(kTicSmall, kSeqSmall), ""); - static_assert(expectGt(kTicSmall, kSeqMiD0), ""); - static_assert(expectGt(kTicSmall, kSeqMiD1), ""); - static_assert(expectGt(kTicSmall, kSeqBig), ""); - static_assert(expectGt(kTicSmall, kTicZero), ""); - static_assert(expectEq(kTicSmall, kTicSmall), ""); - static_assert(expectLt(kTicSmall, kTicMid0), ""); - static_assert(expectLt(kTicSmall, kTicMid1), ""); - static_assert(expectLt(kTicSmall, kTicBig), ""); + static_assert(expectGt(kTicSmall, kSeqZero)); + static_assert(expectGt(kTicSmall, kSeqSmall)); + static_assert(expectGt(kTicSmall, kSeqMiD0)); + static_assert(expectGt(kTicSmall, kSeqMiD1)); + static_assert(expectGt(kTicSmall, kSeqBig)); + static_assert(expectGt(kTicSmall, kTicZero)); + static_assert(expectEq(kTicSmall, kTicSmall)); + static_assert(expectLt(kTicSmall, kTicMid0)); + static_assert(expectLt(kTicSmall, kTicMid1)); + static_assert(expectLt(kTicSmall, kTicBig)); - static_assert(expectGt(kTicMid0, kSeqZero), ""); - static_assert(expectGt(kTicMid0, kSeqSmall), ""); - static_assert(expectGt(kTicMid0, kSeqMiD0), ""); - static_assert(expectGt(kTicMid0, kSeqMiD1), ""); - static_assert(expectGt(kTicMid0, kSeqBig), ""); - static_assert(expectGt(kTicMid0, kTicZero), ""); - static_assert(expectGt(kTicMid0, kTicSmall), ""); - static_assert(expectEq(kTicMid0, kTicMid0), ""); - static_assert(expectEq(kTicMid0, kTicMid1), ""); - static_assert(expectLt(kTicMid0, kTicBig), ""); + static_assert(expectGt(kTicMid0, kSeqZero)); + static_assert(expectGt(kTicMid0, kSeqSmall)); + static_assert(expectGt(kTicMid0, kSeqMiD0)); + static_assert(expectGt(kTicMid0, kSeqMiD1)); + static_assert(expectGt(kTicMid0, kSeqBig)); + static_assert(expectGt(kTicMid0, kTicZero)); + static_assert(expectGt(kTicMid0, kTicSmall)); + static_assert(expectEq(kTicMid0, kTicMid0)); + static_assert(expectEq(kTicMid0, kTicMid1)); + static_assert(expectLt(kTicMid0, kTicBig)); - static_assert(expectGt(kTicMid1, kSeqZero), ""); - static_assert(expectGt(kTicMid1, kSeqSmall), ""); - static_assert(expectGt(kTicMid1, kSeqMiD0), ""); - static_assert(expectGt(kTicMid1, kSeqMiD1), ""); - static_assert(expectGt(kTicMid1, kSeqBig), ""); - static_assert(expectGt(kTicMid1, kTicZero), ""); - static_assert(expectGt(kTicMid1, kTicSmall), ""); - static_assert(expectEq(kTicMid1, kTicMid0), ""); - static_assert(expectEq(kTicMid1, kTicMid1), ""); - static_assert(expectLt(kTicMid1, kTicBig), ""); + static_assert(expectGt(kTicMid1, kSeqZero)); + static_assert(expectGt(kTicMid1, kSeqSmall)); + static_assert(expectGt(kTicMid1, kSeqMiD0)); + static_assert(expectGt(kTicMid1, kSeqMiD1)); + static_assert(expectGt(kTicMid1, kSeqBig)); + static_assert(expectGt(kTicMid1, kTicZero)); + static_assert(expectGt(kTicMid1, kTicSmall)); + static_assert(expectEq(kTicMid1, kTicMid0)); + static_assert(expectEq(kTicMid1, kTicMid1)); + static_assert(expectLt(kTicMid1, kTicBig)); - static_assert(expectGt(kTicBig, kSeqZero), ""); - static_assert(expectGt(kTicBig, kSeqSmall), ""); - static_assert(expectGt(kTicBig, kSeqMiD0), ""); - static_assert(expectGt(kTicBig, kSeqMiD1), ""); - static_assert(expectGt(kTicBig, kSeqBig), ""); - static_assert(expectGt(kTicBig, kTicZero), ""); - static_assert(expectGt(kTicBig, kTicSmall), ""); - static_assert(expectGt(kTicBig, kTicMid0), ""); - static_assert(expectGt(kTicBig, kTicMid1), ""); - static_assert(expectEq(kTicBig, kTicBig), ""); + static_assert(expectGt(kTicBig, kSeqZero)); + static_assert(expectGt(kTicBig, kSeqSmall)); + static_assert(expectGt(kTicBig, kSeqMiD0)); + static_assert(expectGt(kTicBig, kSeqMiD1)); + static_assert(expectGt(kTicBig, kSeqBig)); + static_assert(expectGt(kTicBig, kTicZero)); + static_assert(expectGt(kTicBig, kTicSmall)); + static_assert(expectGt(kTicBig, kTicMid0)); + static_assert(expectGt(kTicBig, kTicMid1)); + static_assert(expectEq(kTicBig, kTicBig)); // Verify streaming. BEAST_EXPECT(streamTest(kSeqZero)); diff --git a/src/test/shamap/SHAMap_test.cpp b/src/test/shamap/SHAMap_test.cpp index ab7e01e3af..5ff5ef5e05 100644 --- a/src/test/shamap/SHAMap_test.cpp +++ b/src/test/shamap/SHAMap_test.cpp @@ -26,57 +26,57 @@ namespace xrpl::tests { #ifndef __INTELLISENSE__ -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(!std::is_default_constructible{}, ""); -static_assert(!std::is_copy_constructible{}, ""); -static_assert(!std::is_copy_assignable{}, ""); -static_assert(!std::is_move_constructible{}, ""); -static_assert(!std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(!std::is_default_constructible{}); +static_assert(!std::is_copy_constructible{}); +static_assert(!std::is_copy_assignable{}); +static_assert(!std::is_move_constructible{}); +static_assert(!std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(std::is_copy_constructible{}, ""); -static_assert(std::is_copy_assignable{}, ""); -static_assert(std::is_move_constructible{}, ""); -static_assert(std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(std::is_copy_constructible{}); +static_assert(std::is_copy_assignable{}); +static_assert(std::is_move_constructible{}); +static_assert(std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(!std::is_default_constructible{}, ""); -static_assert(!std::is_copy_constructible{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(!std::is_default_constructible{}); +static_assert(!std::is_copy_constructible{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(std::is_default_constructible{}, ""); -static_assert(std::is_copy_constructible{}, ""); -static_assert(std::is_copy_assignable{}, ""); -static_assert(std::is_move_constructible{}, ""); -static_assert(std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(std::is_default_constructible{}); +static_assert(std::is_copy_constructible{}); +static_assert(std::is_copy_assignable{}); +static_assert(std::is_move_constructible{}); +static_assert(std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(std::is_default_constructible{}, ""); -static_assert(std::is_copy_constructible{}, ""); -static_assert(std::is_copy_assignable{}, ""); -static_assert(std::is_move_constructible{}, ""); -static_assert(std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(std::is_default_constructible{}); +static_assert(std::is_copy_constructible{}); +static_assert(std::is_copy_assignable{}); +static_assert(std::is_move_constructible{}); +static_assert(std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(!std::is_default_constructible{}, ""); -static_assert(!std::is_copy_constructible{}, ""); -static_assert(!std::is_copy_assignable{}, ""); -static_assert(!std::is_move_constructible{}, ""); -static_assert(!std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(!std::is_default_constructible{}); +static_assert(!std::is_copy_constructible{}); +static_assert(!std::is_copy_assignable{}); +static_assert(!std::is_move_constructible{}); +static_assert(!std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(!std::is_default_constructible{}, ""); -static_assert(!std::is_copy_constructible{}, ""); -static_assert(!std::is_copy_assignable{}, ""); -static_assert(!std::is_move_constructible{}, ""); -static_assert(!std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(!std::is_default_constructible{}); +static_assert(!std::is_copy_constructible{}); +static_assert(!std::is_copy_assignable{}); +static_assert(!std::is_move_constructible{}); +static_assert(!std::is_move_assignable{}); -static_assert(std::is_nothrow_destructible{}, ""); -static_assert(!std::is_default_constructible{}, ""); -static_assert(!std::is_copy_constructible{}, ""); -static_assert(!std::is_copy_assignable{}, ""); -static_assert(!std::is_move_constructible{}, ""); -static_assert(!std::is_move_assignable{}, ""); +static_assert(std::is_nothrow_destructible{}); +static_assert(!std::is_default_constructible{}); +static_assert(!std::is_copy_constructible{}); +static_assert(!std::is_copy_assignable{}); +static_assert(!std::is_move_constructible{}); +static_assert(!std::is_move_assignable{}); #endif inline bool diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 7e57302d23..5d1020d296 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -228,11 +228,11 @@ static_assert(sumOfFirstSquares(1).second == 1); static_assert(sumOfFirstSquares(2).first); static_assert(sumOfFirstSquares(2).second == 5); -static_assert(sumOfFirstSquares(0x1FFFFF).first, ""); -static_assert(sumOfFirstSquares(0x1FFFFF).second == 0x2AAAA8AAAAB00000ul, ""); +static_assert(sumOfFirstSquares(0x1FFFFF).first); +static_assert(sumOfFirstSquares(0x1FFFFF).second == 0x2AAAA8AAAAB00000ul); -static_assert(!sumOfFirstSquares(0x200000).first, ""); -static_assert(sumOfFirstSquares(0x200000).second == std::numeric_limits::max(), ""); +static_assert(!sumOfFirstSquares(0x200000).first); +static_assert(sumOfFirstSquares(0x200000).second == std::numeric_limits::max()); } // namespace detail From 6003fd03fc11c7675a9f03ba791e63aaa0e8265a Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Thu, 2 Jul 2026 14:37:37 -0400 Subject: [PATCH 24/31] feat: Enable ConfidentialTransfer and BatchV1_1 (#7698) --- include/xrpl/protocol/detail/features.macro | 4 ++-- src/test/app/Delegate_test.cpp | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index a7a62f0322..3f078561a3 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,9 +15,9 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -XRPL_FEATURE(BatchV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) -XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo) 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) diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 02ee0750d5..6f1d2ce7de 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2747,8 +2747,6 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - // Includes the five confidential MPT transaction types, which are - // explicitly marked Delegable in transactions.macro. std::size_t const expectedDelegableCount = 56; BEAST_EXPECTS( From 3d847f2a6039be6837d7ed8329a9a8496283dc4a Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 2 Jul 2026 14:46:27 -0400 Subject: [PATCH 25/31] build: Add protobuf dependencies to Nix (#7706) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- nix/packages.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nix/packages.nix b/nix/packages.nix index 6202168733..bcadfe7456 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -32,6 +32,15 @@ in perl # needed for openssl pkg-config pre-commit + # protoc generates the Go gRPC bindings and embeds its own version string into every committed + # .pb.go file. To allow CI to verify those files with a plain `git diff`, we pin the version to + # `protobuf_34` rather than the rolling `protobuf` to keep regeneration reproducible across the + # Nix frequently changing unstable channel. The protoc-gen-go* plugins have no versioned + # attributes in nixpkgs; protoc-gen-go's version is in turn constrained by the go.mod require + # on google.golang.org/protobuf. + protobuf_34 # provides protoc + protoc-gen-go # protoc plugin for the Go message bindings + protoc-gen-go-grpc # protoc plugin for the Go gRPC service stubs python3 runClangTidy vim From 7ba1d76d0543c94e74df3c634b11448b293cdaf8 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 2 Jul 2026 21:02:55 +0100 Subject: [PATCH 26/31] chore: Enable modernize-use-auto (#7707) --- .clang-tidy | 1 - include/xrpl/beast/hash/hash_append.h | 2 +- include/xrpl/beast/utility/rngfill.h | 4 ++-- include/xrpl/core/JobTypes.h | 2 +- include/xrpl/ledger/helpers/EscrowHelpers.h | 2 +- include/xrpl/nodestore/detail/codec.h | 8 ++++---- include/xrpl/nodestore/detail/varint.h | 4 ++-- include/xrpl/protocol/Quality.h | 2 +- include/xrpl/protocol/STBitString.h | 2 +- include/xrpl/protocol/STInteger.h | 2 +- src/libxrpl/basics/Number.cpp | 2 +- src/libxrpl/beast/insight/StatsDCollector.cpp | 4 ++-- src/libxrpl/core/detail/JobQueue.cpp | 10 +++++----- src/libxrpl/crypto/RFC1751.cpp | 2 +- src/libxrpl/json/json_reader.cpp | 2 +- src/libxrpl/json/json_value.cpp | 16 ++++++++-------- src/libxrpl/json/json_writer.cpp | 6 +++--- src/libxrpl/nodestore/DecodedBlob.cpp | 2 +- src/libxrpl/nodestore/backend/MemoryFactory.cpp | 2 +- src/libxrpl/nodestore/backend/NuDBFactory.cpp | 2 +- src/libxrpl/nodestore/backend/RocksDBFactory.cpp | 2 +- src/libxrpl/protocol/IOUAmount.cpp | 2 +- src/libxrpl/protocol/MPTIssue.cpp | 3 +-- src/libxrpl/protocol/NFTokenID.cpp | 3 +-- src/libxrpl/protocol/STAmount.cpp | 2 +- src/libxrpl/protocol/STBlob.cpp | 2 +- src/libxrpl/protocol/STCurrency.cpp | 2 +- src/libxrpl/protocol/STIssue.cpp | 2 +- src/libxrpl/protocol/STNumber.cpp | 2 +- src/libxrpl/protocol/STObject.cpp | 10 +++++----- src/libxrpl/protocol/STPathSet.cpp | 4 ++-- src/libxrpl/protocol/STVector256.cpp | 2 +- src/libxrpl/protocol/STXChainBridge.cpp | 2 +- .../tx/transactors/escrow/EscrowCreate.cpp | 2 +- src/test/app/ConfidentialTransfer_test.cpp | 2 +- src/test/app/Loan_test.cpp | 2 +- src/test/app/Offer_test.cpp | 2 +- src/test/beast/LexicalCast_test.cpp | 6 +++--- .../beast/aged_associative_container_test.cpp | 2 +- src/test/shamap/FetchPack_test.cpp | 2 +- src/xrpld/app/consensus/RCLConsensus.cpp | 2 +- src/xrpld/app/ledger/detail/InboundLedgers.cpp | 2 +- src/xrpld/app/main/Application.cpp | 2 +- src/xrpld/app/misc/NetworkOPs.cpp | 4 ++-- src/xrpld/app/misc/detail/Transaction.cpp | 2 +- src/xrpld/app/misc/detail/TxQ.cpp | 10 +++++----- src/xrpld/app/rdb/backend/detail/Node.cpp | 2 +- src/xrpld/core/detail/Config.cpp | 2 +- src/xrpld/rpc/CTID.h | 6 +++--- src/xrpld/rpc/handlers/orderbook/BookOffers.cpp | 2 +- 50 files changed, 82 insertions(+), 85 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 2b0b7e4418..6a24cbc8d3 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -80,7 +80,6 @@ Checks: "-*, -modernize-replace-random-shuffle, -modernize-return-braced-init-list, -modernize-shrink-to-fit, - -modernize-use-auto, -modernize-use-bool-literals, -modernize-use-constraints, -modernize-use-default-member-init, diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 0b36d4c983..5f77f41e5d 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -26,7 +26,7 @@ template inline void reverseBytes(T& t) { - unsigned char* bytes = + auto* bytes = static_cast(std::memmove(std::addressof(t), std::addressof(t), sizeof(T))); for (unsigned i = 0; i < sizeof(T) / 2; ++i) std::swap(bytes[i], bytes[sizeof(T) - 1 - i]); diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index 0fc3ffe0d0..ee7a5e2434 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -14,7 +14,7 @@ rngfill(void* const buffer, std::size_t const bytes, Generator& g) using result_type = Generator::result_type; constexpr std::size_t kResultSize = sizeof(result_type); - std::uint8_t* const bufferStart = static_cast(buffer); + auto* const bufferStart = static_cast(buffer); std::size_t const completeIterations = bytes / kResultSize; std::size_t const bytesRemaining = bytes % kResultSize; @@ -42,7 +42,7 @@ rngfill(std::array& a, Generator& g) { using result_type = Generator::result_type; auto i = N / sizeof(result_type); - result_type* p = reinterpret_cast(a.data()); + auto* p = reinterpret_cast(a.data()); while (i--) *p++ = g(); } diff --git a/include/xrpl/core/JobTypes.h b/include/xrpl/core/JobTypes.h index f338b19f6c..cc2f3ecbf5 100644 --- a/include/xrpl/core/JobTypes.h +++ b/include/xrpl/core/JobTypes.h @@ -118,7 +118,7 @@ public: [[nodiscard]] JobTypeInfo const& get(JobType jt) const { - Map::const_iterator const iter(map.find(jt)); + auto const iter = map.find(jt); XRPL_ASSERT(iter != map.end(), "xrpl::JobTypes::get : valid input"); if (iter != map.end()) diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 001dc9cb25..d173c33c56 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -54,7 +54,7 @@ escrowUnlockApplyHelper( bool createAsset, beast::Journal journal) { - Issue const& issue = amount.get(); + auto const& issue = amount.get(); Keylet const trustLineKey = keylet::trustLine(receiver, issue); bool const recvLow = issuer > receiver; bool const senderIssuer = issuer == sender; diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h index 56e1fbcf73..2f69d532be 100644 --- a/include/xrpl/nodestore/detail/codec.h +++ b/include/xrpl/nodestore/detail/codec.h @@ -62,7 +62,7 @@ lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf) std::array::kMax> vi{}; auto const n = writeVarint(vi.data(), inSize); auto const outMax = LZ4_compressBound(inSize); - std::uint8_t* out = reinterpret_cast(bf(n + outMax)); + auto* out = reinterpret_cast(bf(n + outMax)); result.first = out; std::memcpy(out, vi.data(), n); auto const outSize = LZ4_compress_default( @@ -90,7 +90,7 @@ nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf) { using namespace nudb::detail; - std::uint8_t const* p = reinterpret_cast(in); + auto const* p = reinterpret_cast(in); std::size_t type = 0; auto const vn = readVarint(p, inSize, type); if (vn == 0) @@ -237,7 +237,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) auto const vs = sizeVarint(type); result.second = vs + field::size + // mask (n * 32); // hashes - std::uint8_t* out = reinterpret_cast(bf(result.second)); + auto* out = reinterpret_cast(bf(result.second)); result.first = out; ostream os(out, result.second); write(os, type); @@ -249,7 +249,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) auto const type = 3U; auto const vs = sizeVarint(type); result.second = vs + (n * 32); // hashes - std::uint8_t* out = reinterpret_cast(bf(result.second)); + auto* out = reinterpret_cast(bf(result.second)); result.first = out; ostream os(out, result.second); write(os, type); diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index 21a13bd6de..6102c0cf2d 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -39,7 +39,7 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t) if (buflen == 0) return 0; t = 0; - std::uint8_t const* p = reinterpret_cast(buf); + auto const* p = reinterpret_cast(buf); std::size_t n = 0; while (p[n] & 0x80) { @@ -86,7 +86,7 @@ std::size_t writeVarint(void* p0, std::size_t v) { // NOLINTNEXTLINE(misc-const-correctness) - std::uint8_t* p = reinterpret_cast(p0); + auto* p = reinterpret_cast(p0); do { std::uint8_t d = v % 127; diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index 6bb2033dc0..de61d79ca5 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -282,7 +282,7 @@ public: auto const maxVMantissa = mantissa(maxV); auto const expDiff = exponent(maxV) - exponent(minV); - double const minVD = static_cast(minVMantissa); + auto const minVD = static_cast(minVMantissa); double const maxVD = (expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast(maxVMantissa); diff --git a/include/xrpl/protocol/STBitString.h b/include/xrpl/protocol/STBitString.h index 4f25eccca6..6f71f48f25 100644 --- a/include/xrpl/protocol/STBitString.h +++ b/include/xrpl/protocol/STBitString.h @@ -148,7 +148,7 @@ template bool STBitString::isEquivalent(STBase const& t) const { - STBitString const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return v && (value_ == v->value_); } diff --git a/include/xrpl/protocol/STInteger.h b/include/xrpl/protocol/STInteger.h index 1c7ac98f3e..951c4fc52f 100644 --- a/include/xrpl/protocol/STInteger.h +++ b/include/xrpl/protocol/STInteger.h @@ -115,7 +115,7 @@ template inline bool STInteger::isEquivalent(STBase const& t) const { - STInteger const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return v && (value_ == v->value_); } diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 23e913bbdc..4d7a821040 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -876,7 +876,7 @@ Number::operator/=(Number const& y) int const ds = (dp ? -1 : 1); // Create the denominator as 128-bit unsigned, since that's what we // need to work with. - uint128_t const dm = static_cast(y.mantissa_); + auto const dm = static_cast(y.mantissa_); auto const de = y.exponent_; auto const& range = kRange.get(); diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 0d0e013274..1da5315de1 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -640,14 +640,14 @@ StatsDGaugeImpl::doIncrement(GaugeImpl::difference_type amount) if (amount > 0) { - GaugeImpl::value_type const d(static_cast(amount)); + auto const d = static_cast(amount); value += (d >= std::numeric_limits::max() - value_) ? std::numeric_limits::max() - value_ : d; } else if (amount < 0) { - GaugeImpl::value_type const d(static_cast(-amount)); + auto const d = static_cast(-amount); value = (d >= value) ? 0 : value - d; } diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp index ffb91db72d..f773270af3 100644 --- a/src/libxrpl/core/detail/JobQueue.cpp +++ b/src/libxrpl/core/detail/JobQueue.cpp @@ -122,7 +122,7 @@ JobQueue::getJobCount(JobType t) const { std::scoped_lock const lock(mutex_); - JobDataMap::const_iterator const c = jobData_.find(t); + auto const c = jobData_.find(t); return (c == jobData_.end()) ? 0 : c->second.waiting; } @@ -132,7 +132,7 @@ JobQueue::getJobCountTotal(JobType t) const { std::scoped_lock const lock(mutex_); - JobDataMap::const_iterator const c = jobData_.find(t); + auto const c = jobData_.find(t); return (c == jobData_.end()) ? 0 : (c->second.waiting + c->second.running); } @@ -157,7 +157,7 @@ JobQueue::getJobCountGE(JobType t) const std::unique_ptr JobQueue::makeLoadEvent(JobType t, std::string const& name) { - JobDataMap::iterator const iter(jobData_.find(t)); + auto const iter = jobData_.find(t); XRPL_ASSERT(iter != jobData_.end(), "xrpl::JobQueue::makeLoadEvent : valid job type input"); if (iter == jobData_.end()) @@ -172,7 +172,7 @@ JobQueue::addLoadEvents(JobType t, int count, std::chrono::milliseconds elapsed) if (isStopped()) logicError("JobQueue::addLoadEvents() called after JobQueue stopped"); - JobDataMap::iterator const iter(jobData_.find(t)); + auto const iter = jobData_.find(t); XRPL_ASSERT(iter != jobData_.end(), "xrpl::JobQueue::addLoadEvents : valid job type input"); iter->second.load().addSamples(count, elapsed); } @@ -250,7 +250,7 @@ JobQueue::rendezvous() JobTypeData& JobQueue::getJobTypeData(JobType type) { - JobDataMap::iterator const c(jobData_.find(type)); + auto const c = jobData_.find(type); XRPL_ASSERT(c != jobData_.end(), "xrpl::JobQueue::getJobTypeData : valid job type input"); // NIKB: This is ugly and I hate it. We must remove JtInvalid completely diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp index 16482945d2..41e29ee00c 100644 --- a/src/libxrpl/crypto/RFC1751.cpp +++ b/src/libxrpl/crypto/RFC1751.cpp @@ -434,7 +434,7 @@ RFC1751::getWordFromBlob(void const* blob, size_t bytes) // This is a simple implementation of the Jenkins one-at-a-time hash // algorithm: // http://en.wikipedia.org/wiki/Jenkins_hash_function#one-at-a-time - unsigned char const* data = static_cast(blob); + auto const* data = static_cast(blob); std::uint32_t hash = 0; for (size_t i = 0; i < bytes; ++i) diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp index 3786f51fdd..45e448c480 100644 --- a/src/libxrpl/json/json_reader.cpp +++ b/src/libxrpl/json/json_reader.cpp @@ -908,7 +908,7 @@ Reader::getFormattedErrorMessages() const { std::string formattedMessage; - for (Errors::const_iterator itError = errors_.begin(); itError != errors_.end(); ++itError) + for (auto itError = errors_.begin(); itError != errors_.end(); ++itError) { ErrorInfo const& error = *itError; formattedMessage += "* " + getLocationLineAndColumn(error.token.start) + "\n"; diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 074a88428c..7218b8c6cf 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -802,7 +802,7 @@ Value::size() const case ValueType::Array: // size of the array is highest index + 1 if (!value_.mapVal->empty()) { - ObjectValues::const_iterator itLast = value_.mapVal->end(); + auto itLast = value_.mapVal->end(); --itLast; return (*itLast).first.index() + 1; } @@ -866,7 +866,7 @@ Value::operator[](UInt index) *this = Value(ValueType::Array); CZString const key(index); - ObjectValues::iterator it = value_.mapVal->lower_bound(key); + auto it = value_.mapVal->lower_bound(key); if (it != value_.mapVal->end() && (*it).first == key) return (*it).second; @@ -887,7 +887,7 @@ Value::operator[](UInt index) const return kNull; CZString const key(index); - ObjectValues::const_iterator const it = value_.mapVal->find(key); + auto const it = value_.mapVal->find(key); if (it == value_.mapVal->end()) return kNull; @@ -915,7 +915,7 @@ Value::resolveReference(char const* key, bool isStatic) key, isStatic ? CZString::DuplicationPolicy::NoDuplication : CZString::DuplicationPolicy::DuplicateOnCopy); - ObjectValues::iterator it = value_.mapVal->lower_bound(actualKey); + auto it = value_.mapVal->lower_bound(actualKey); if (it != value_.mapVal->end() && (*it).first == actualKey) return (*it).second; @@ -950,7 +950,7 @@ Value::operator[](char const* key) const return kNull; CZString const actualKey(key, CZString::DuplicationPolicy::NoDuplication); - ObjectValues::const_iterator const it = value_.mapVal->find(actualKey); + auto const it = value_.mapVal->find(actualKey); if (it == value_.mapVal->end()) return kNull; @@ -1018,7 +1018,7 @@ Value::removeMember(char const* key) return kNull; CZString const actualKey(key, CZString::DuplicationPolicy::NoDuplication); - ObjectValues::iterator const it = value_.mapVal->find(actualKey); + auto const it = value_.mapVal->find(actualKey); if (it == value_.mapVal->end()) return kNull; @@ -1068,8 +1068,8 @@ Value::getMemberNames() const Members members; members.reserve(value_.mapVal->size()); - ObjectValues::const_iterator it = value_.mapVal->begin(); - ObjectValues::const_iterator const itEnd = value_.mapVal->end(); + auto it = value_.mapVal->begin(); + auto const itEnd = value_.mapVal->end(); for (; it != itEnd; ++it) members.emplace_back((*it).first.cStr()); diff --git a/src/libxrpl/json/json_writer.cpp b/src/libxrpl/json/json_writer.cpp index 4c38bdcf92..c9f8cb688b 100644 --- a/src/libxrpl/json/json_writer.cpp +++ b/src/libxrpl/json/json_writer.cpp @@ -232,7 +232,7 @@ FastWriter::writeValue(Value const& value) Value::Members members(value.getMemberNames()); document_ += "{"; - for (Value::Members::iterator it = members.begin(); it != members.end(); ++it) + for (auto it = members.begin(); it != members.end(); ++it) { std::string const& name = *it; @@ -310,7 +310,7 @@ StyledWriter::writeValue(Value const& value) { writeWithIndent("{"); indent(); - Value::Members::iterator it = members.begin(); + auto it = members.begin(); while (true) { @@ -545,7 +545,7 @@ StyledStreamWriter::writeValue(Value const& value) { writeWithIndent("{"); indent(); - Value::Members::iterator it = members.begin(); + auto it = members.begin(); while (true) { diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp b/src/libxrpl/nodestore/DecodedBlob.cpp index fb7569bd8c..fe07252f23 100644 --- a/src/libxrpl/nodestore/DecodedBlob.cpp +++ b/src/libxrpl/nodestore/DecodedBlob.cpp @@ -33,7 +33,7 @@ DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) if (valueBytes > 8) { - unsigned char const* byte = static_cast(value); + auto const* byte = static_cast(value); objectType_ = safeCast(byte[8]); } diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp b/src/libxrpl/nodestore/backend/MemoryFactory.cpp index 70578c8613..22557d652e 100644 --- a/src/libxrpl/nodestore/backend/MemoryFactory.cpp +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp @@ -136,7 +136,7 @@ public: std::scoped_lock const _(db_->mutex); - Map::iterator const iter = db_->table.find(hash); + auto const iter = db_->table.find(hash); if (iter == db_->table.end()) { pObject->reset(); diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index 749d4020b5..38ea34258f 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -367,7 +367,7 @@ private: try { - std::size_t const parsedBlockSize = beast::lexicalCastThrow(blockSizeStr); + auto const parsedBlockSize = beast::lexicalCastThrow(blockSizeStr); // Validate: must be power of 2 between 4K and 32K if (parsedBlockSize < 4096 || parsedBlockSize > 32768 || diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index 69a648f2f4..bcf4ba4a49 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -80,7 +80,7 @@ public: void StartThread(void (*f)(void*), void* a) override { - ThreadParams* const p(new ThreadParams(f, a)); + auto* const p = new ThreadParams(f, a); EnvWrapper::StartThread(&RocksDBEnv::threadEntry, p); } }; diff --git a/src/libxrpl/protocol/IOUAmount.cpp b/src/libxrpl/protocol/IOUAmount.cpp index acbf6724e1..111edbf8b5 100644 --- a/src/libxrpl/protocol/IOUAmount.cpp +++ b/src/libxrpl/protocol/IOUAmount.cpp @@ -181,7 +181,7 @@ mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundU hasRem = bool(sav - low * kPowerTable[mustShrink]); } - std::int64_t mantissa = low.convert_to(); + auto mantissa = low.convert_to(); // normalize before rounding if (neg) diff --git a/src/libxrpl/protocol/MPTIssue.cpp b/src/libxrpl/protocol/MPTIssue.cpp index e9ec852d1d..35ee743b2a 100644 --- a/src/libxrpl/protocol/MPTIssue.cpp +++ b/src/libxrpl/protocol/MPTIssue.cpp @@ -31,8 +31,7 @@ MPTIssue::getIssuer() const // MPTID is concatenation of sequence + account static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID))); // copy from id skipping the sequence - AccountID const* account = - reinterpret_cast(mptID_.data() + sizeof(std::uint32_t)); + auto const* account = reinterpret_cast(mptID_.data() + sizeof(std::uint32_t)); return *account; } diff --git a/src/libxrpl/protocol/NFTokenID.cpp b/src/libxrpl/protocol/NFTokenID.cpp index cd2c46b66b..729f052101 100644 --- a/src/libxrpl/protocol/NFTokenID.cpp +++ b/src/libxrpl/protocol/NFTokenID.cpp @@ -72,8 +72,7 @@ getNFTokenIDFromPage(TxMeta const& transactionMeta) // However, there will always be NFTs listed in the final fields, // as xrpld outputs all fields in final fields even if they were // not changed. - STObject const& previousFields = - node.peekAtField(sfPreviousFields).downcast(); + auto const& previousFields = node.peekAtField(sfPreviousFields).downcast(); if (!previousFields.isFieldPresent(sfNFTokens)) continue; diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 748d00f25a..212c34322b 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -789,7 +789,7 @@ STAmount::add(Serializer& s) const bool STAmount::isEquivalent(STBase const& t) const { - STAmount const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (*v == *this); } diff --git a/src/libxrpl/protocol/STBlob.cpp b/src/libxrpl/protocol/STBlob.cpp index 3f44c9b529..14d09b633c 100644 --- a/src/libxrpl/protocol/STBlob.cpp +++ b/src/libxrpl/protocol/STBlob.cpp @@ -53,7 +53,7 @@ STBlob::add(Serializer& s) const bool STBlob::isEquivalent(STBase const& t) const { - STBlob const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (value_ == v->value_); } diff --git a/src/libxrpl/protocol/STCurrency.cpp b/src/libxrpl/protocol/STCurrency.cpp index 9b761864d9..fc8bd756ed 100644 --- a/src/libxrpl/protocol/STCurrency.cpp +++ b/src/libxrpl/protocol/STCurrency.cpp @@ -56,7 +56,7 @@ STCurrency::add(Serializer& s) const bool STCurrency::isEquivalent(STBase const& t) const { - STCurrency const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (*v == *this); } diff --git a/src/libxrpl/protocol/STIssue.cpp b/src/libxrpl/protocol/STIssue.cpp index c9b8109e32..10403d2c50 100644 --- a/src/libxrpl/protocol/STIssue.cpp +++ b/src/libxrpl/protocol/STIssue.cpp @@ -107,7 +107,7 @@ STIssue::add(Serializer& s) const bool STIssue::isEquivalent(STBase const& t) const { - STIssue const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (*v == *this); } diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index fcc0077f6b..79f7655869 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -142,7 +142,7 @@ STNumber::isEquivalent(STBase const& t) const { XRPL_ASSERT( t.getSType() == this->getSType(), "xrpl::STNumber::isEquivalent : field type match"); - STNumber const& v = dynamic_cast(t); + auto const& v = dynamic_cast(t); return value_ == v; } diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index c493ba65af..ea8553dc4f 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -338,7 +338,7 @@ STObject::getText() const bool STObject::isEquivalent(STBase const& t) const { - STObject const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); if (v == nullptr) return false; @@ -474,7 +474,7 @@ STObject::peekFieldArray(SField const& field) bool STObject::setFlag(std::uint32_t f) { - STUInt32* t = dynamic_cast(getPField(sfFlags, true)); + auto* t = dynamic_cast(getPField(sfFlags, true)); if (t == nullptr) return false; @@ -486,7 +486,7 @@ STObject::setFlag(std::uint32_t f) bool STObject::clearFlag(std::uint32_t f) { - STUInt32* t = dynamic_cast(getPField(sfFlags)); + auto* t = dynamic_cast(getPField(sfFlags)); if (t == nullptr) return false; @@ -504,7 +504,7 @@ STObject::isFlag(std::uint32_t f) const std::uint32_t STObject::getFlags(void) const { - STUInt32 const* t = dynamic_cast(peekAtPField(sfFlags)); + auto const* t = dynamic_cast(peekAtPField(sfFlags)); if (t == nullptr) return 0; @@ -651,7 +651,7 @@ Blob STObject::getFieldVL(SField const& field) const { STBlob const empty; - STBlob const& b = getFieldByConstRef(field, empty); + auto const& b = getFieldByConstRef(field, empty); return Blob(b.data(), b.data() + b.size()); } diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index cb70ac36fe..d61f17ecc6 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -127,7 +127,7 @@ STPathSet::assembleAdd(STPath const& base, STPathElement const& tail) { // assemble base+tail and add it to the set if it's not a duplicate value_.push_back(base); - std::vector::reverse_iterator it = value_.rbegin(); + auto it = value_.rbegin(); STPath& newPath = *it; newPath.pushBack(tail); @@ -146,7 +146,7 @@ STPathSet::assembleAdd(STPath const& base, STPathElement const& tail) bool STPathSet::isEquivalent(STBase const& t) const { - STPathSet const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (value_ == v->value_); } diff --git a/src/libxrpl/protocol/STVector256.cpp b/src/libxrpl/protocol/STVector256.cpp index 7aca309667..877e34c73c 100644 --- a/src/libxrpl/protocol/STVector256.cpp +++ b/src/libxrpl/protocol/STVector256.cpp @@ -68,7 +68,7 @@ STVector256::add(Serializer& s) const bool STVector256::isEquivalent(STBase const& t) const { - STVector256 const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (value_ == v->value_); } diff --git a/src/libxrpl/protocol/STXChainBridge.cpp b/src/libxrpl/protocol/STXChainBridge.cpp index ce6ad2368a..005c9ccbce 100644 --- a/src/libxrpl/protocol/STXChainBridge.cpp +++ b/src/libxrpl/protocol/STXChainBridge.cpp @@ -168,7 +168,7 @@ STXChainBridge::getSType() const bool STXChainBridge::isEquivalent(STBase const& t) const { - STXChainBridge const* v = dynamic_cast(&t); + auto const* v = dynamic_cast(&t); return (v != nullptr) && (*v == *this); } diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 0d83885349..531098bd1c 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -194,7 +194,7 @@ escrowCreatePreclaimHelper( AccountID const& dest, STAmount const& amount) { - Issue const& issue = amount.get(); + auto const& issue = amount.get(); AccountID const& issuer = amount.getIssuer(); // If the issuer is the same as the account, return tecNO_PERMISSION if (issuer == account) diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index e6faf0a2ae..d3e0182db5 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -6920,7 +6920,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase auto& mptAlice = confEnv.mpt; uint64_t const sendAmount = 10; - uint64_t const negativeRemaining = static_cast(-10); // 0xFFFFFFFFFFFFFFF6 + auto const negativeRemaining = static_cast(-10); // 0xFFFFFFFFFFFFFFF6 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 56a7cab47a..3e62af48ff 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -8651,7 +8651,7 @@ protected: Account const borrower("borrower"); // Determine all the random parameters at once - AssetType const assetType = static_cast(assetDist_(engine_)); + auto const assetType = static_cast(assetDist_(engine_)); auto const principalRequest = principalDist_(engine_); TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; auto const serviceFee = serviceFeeDist_(engine_); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 804bd9b86c..2724a6474b 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -2195,7 +2195,7 @@ public: jtx::Account const& account, jtx::PrettyAmount const& expectBalance) { - Issue const& issue = expectBalance.value().get(); + auto const& issue = expectBalance.value().get(); auto const sleTrust = env.le(keylet::trustLine(account.id(), issue)); BEAST_EXPECT(sleTrust); if (sleTrust) diff --git a/src/test/beast/LexicalCast_test.cpp b/src/test/beast/LexicalCast_test.cpp index f988aa03cf..b1d37daab8 100644 --- a/src/test/beast/LexicalCast_test.cpp +++ b/src/test/beast/LexicalCast_test.cpp @@ -24,7 +24,7 @@ public: testInteger(IntType in) { std::string s; - IntType out = static_cast(~in); // Ensure out != in + auto out = static_cast(~in); // Ensure out != in expect(lexicalCastChecked(s, in)); expect(lexicalCastChecked(out, s)); @@ -42,7 +42,7 @@ public: for (int i = 0; i < 1000; ++i) { - IntType const value(nextRandomInt(r)); + auto const value = nextRandomInt(r); testInteger(value); } } @@ -229,7 +229,7 @@ public: while (i <= std::numeric_limits::max()) { - std::int16_t const j = static_cast(i); + auto const j = static_cast(i); auto actual = std::to_string(j); diff --git a/src/test/beast/aged_associative_container_test.cpp b/src/test/beast/aged_associative_container_test.cpp index 3dbaf74040..df61824c99 100644 --- a/src/test/beast/aged_associative_container_test.cpp +++ b/src/test/beast/aged_associative_container_test.cpp @@ -1391,7 +1391,7 @@ AgedAssociativeContainerTestBase::reverseFillAgedContainer(Container& c, Values // c.clock() returns an abstract_clock, so dynamic_cast to ManualClock. // VFALCO NOTE This is sketchy using ManualClock = TestTraitsBase::ManualClock; - ManualClock& clk(dynamic_cast(c.clock())); + auto& clk = dynamic_cast(c.clock()); clk.set(0); Values rev(values); diff --git a/src/test/shamap/FetchPack_test.cpp b/src/test/shamap/FetchPack_test.cpp index b2fc185b31..bcc5129f01 100644 --- a/src/test/shamap/FetchPack_test.cpp +++ b/src/test/shamap/FetchPack_test.cpp @@ -68,7 +68,7 @@ public: [[nodiscard]] std::optional getNode(SHAMapHash const& nodeHash) const override { - Map::iterator const it = map.find(nodeHash); + auto const it = map.find(nodeHash); if (it == map.end()) { JLOG(journal.fatal()) << "Test filter missing node"; diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index a7c2b26c04..2abc881adc 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -690,7 +690,7 @@ RCLConsensus::Adaptor::doAccept( JLOG(j_.info()) << "We closed at " << closeTime.time_since_epoch().count(); using usec64_t = std::chrono::duration; - usec64_t closeTotal = std::chrono::duration_cast(closeTime.time_since_epoch()); + auto closeTotal = std::chrono::duration_cast(closeTime.time_since_epoch()); int closeCount = 1; for (auto const& [t, v] : rawCloseTimes.peers) diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index 95e3e46f73..07daa7560e 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -378,7 +378,7 @@ public: { ScopedLockType const sl(lock_); - MapType::iterator it(ledgers_.begin()); + auto it = ledgers_.begin(); total = ledgers_.size(); stuffToSweep.reserve(total); diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 594195b93e..d329475874 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -2178,7 +2178,7 @@ fixConfigPorts(Config& config, Endpoints const& endpoints) auto const optPort = section.get(Keys::kPort); if (optPort) { - std::uint16_t const port = beast::lexicalCast(*optPort); + auto const port = beast::lexicalCast(*optPort); if (port == 0u) section.set(Keys::kPort, std::to_string(ep.port())); } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 82445f3625..d1f794b74d 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -4392,7 +4392,7 @@ NetworkOPsImp::findRpcSub(std::string const& strUrl) { std::scoped_lock const sl(subLock_); - subRpcMapType::iterator const it = rpcSubMap_.find(strUrl); + auto const it = rpcSubMap_.find(strUrl); if (it != rpcSubMap_.end()) return it->second; @@ -4827,7 +4827,7 @@ NetworkOPsImp::StateAccounting::json(json::Value& obj) const counters[static_cast(mode)].dur += current; obj[jss::state_accounting] = json::ValueType::Object; - for (std::size_t i = static_cast(OperatingMode::DISCONNECTED); + for (auto i = static_cast(OperatingMode::DISCONNECTED); i <= static_cast(OperatingMode::FULL); ++i) { diff --git a/src/xrpld/app/misc/detail/Transaction.cpp b/src/xrpld/app/misc/detail/Transaction.cpp index 2c55c474eb..e29181bfe9 100644 --- a/src/xrpld/app/misc/detail/Transaction.cpp +++ b/src/xrpld/app/misc/detail/Transaction.cpp @@ -103,7 +103,7 @@ Transaction::transactionFromSQL( Blob const& rawTxn, Application& app) { - std::uint32_t const inLedger = rangeCheckedCast(ledgerSeq.value_or(0)); + auto const inLedger = rangeCheckedCast(ledgerSeq.value_or(0)); SerialIter it(makeSlice(rawTxn)); auto txn = std::make_shared(it); diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 5d1020d296..f6d00974b4 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -788,7 +788,7 @@ TxQ::apply( std::scoped_lock const lock(mutex_); // accountIter is not const because it may be updated further down. - AccountMap::iterator accountIter = byAccount_.find(account); + auto accountIter = byAccount_.find(account); bool const accountIsInQueue = accountIter != byAccount_.end(); // _If_ the account is in the queue, then ignore any sequence-based @@ -817,7 +817,7 @@ TxQ::apply( // Find the first transaction in the queue that we might apply. TxQAccount::TxMap& acctTxs = accountIter->second.transactions; - TxQAccount::TxMap::iterator const firstIter = acctTxs.lower_bound(acctSeqProx); + auto const firstIter = acctTxs.lower_bound(acctSeqProx); if (firstIter == acctTxs.end()) { @@ -996,7 +996,7 @@ TxQ::apply( // Find the entry in the queue that precedes the new // transaction, if one does. - TxQAccount::TxMap::const_iterator const prevIter = txQAcct.getPrevTx(txSeqProx); + auto const prevIter = txQAcct.getPrevTx(txSeqProx); // Does the new transaction go to the front of the queue? // This can happen if: @@ -1615,7 +1615,7 @@ TxQ::nextQueuableSeqImpl(SLE::const_ref sleAccount, std::scoped_lock // Ignore any sequence-based queued transactions that slipped into the // ledger while we were not watching. This does actually happen in the // wild, but it's uncommon. - TxQAccount::TxMap::const_iterator txIter = acctTxs.lower_bound(acctSeqProx); + auto txIter = acctTxs.lower_bound(acctSeqProx); if (txIter == acctTxs.end() || !txIter->first.isSeq() || txIter->first != acctSeqProx) { @@ -1700,7 +1700,7 @@ TxQ::tryDirectApply( // queue then remove the replaced transaction. std::scoped_lock const lock(mutex_); - AccountMap::iterator const accountIter = byAccount_.find(account); + auto const accountIter = byAccount_.find(account); if (accountIter != byAccount_.end()) { TxQAccount& txQAcct = accountIter->second; diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index 6e92a0de60..f8dc4a6981 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -1273,7 +1273,7 @@ getTransaction( if (!ledgerSeq) return std::pair{std::move(txn), nullptr}; - std::uint32_t const inLedger = rangeCheckedCast(ledgerSeq.value()); + auto const inLedger = rangeCheckedCast(ledgerSeq.value()); auto txMeta = std::make_shared(id, inLedger, rawMeta); diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 523cde743a..07c780118a 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -926,7 +926,7 @@ Config::loadFromString(std::string const& fileContents) ", must be: [0-9]+ [minutes|hours|days|weeks]"); } - std::uint32_t const duration = beast::lexicalCastThrow(match[1].str()); + auto const duration = beast::lexicalCastThrow(match[1].str()); if (boost::iequals(match[2], "minutes")) { diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index c2133a19b5..7566e0e143 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -104,9 +104,9 @@ decodeCTID(T const ctid) noexcept if ((ctidValue & kCtidPrefixMask) != kCtidPrefix) return std::nullopt; - uint32_t const ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF); - uint16_t const txnIndex = static_cast((ctidValue >> 16) & 0xFFFF); - uint16_t const networkID = static_cast(ctidValue & 0xFFFF); + auto const ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF); + auto const txnIndex = static_cast((ctidValue >> 16) & 0xFFFF); + auto const networkID = static_cast(ctidValue & 0xFFFF); return std::make_tuple(ledgerSeq, txnIndex, networkID); } diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp index 5d031c2c74..63dee76f1b 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp @@ -107,7 +107,7 @@ parseTakerIssuerJSON( if (taker.isMember(jss::currency)) { - Issue& issue = asset.get(); + auto& issue = asset.get(); if (taker.isMember(jss::issuer)) { From f151293e8a87d192ed1e4f3c95d9782c602a2efb Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 3 Jul 2026 12:17:03 +0100 Subject: [PATCH 27/31] chore: Enable modernize-avoid-bind (#7711) --- .clang-tidy | 1 - include/xrpl/basics/TaggedCache.ipp | 5 +- include/xrpl/beast/unit_test/thread.h | 5 +- include/xrpl/net/AutoSocket.h | 9 +- include/xrpl/net/HTTPClientSSLContext.h | 6 +- include/xrpl/server/detail/BaseHTTPPeer.h | 74 +++++--------- include/xrpl/server/detail/BasePeer.h | 3 +- include/xrpl/server/detail/BaseWSPeer.h | 66 ++++++------- include/xrpl/server/detail/Door.h | 9 +- include/xrpl/server/detail/PlainHTTPPeer.h | 10 +- include/xrpl/server/detail/SSLHTTPPeer.h | 18 ++-- src/libxrpl/basics/ResolverAsio.cpp | 34 +++---- src/libxrpl/beast/insight/StatsDCollector.cpp | 63 +++++------- src/libxrpl/core/detail/JobQueue.cpp | 3 +- src/libxrpl/net/HTTPClient.cpp | 69 +++++++------ src/test/jtx/impl/WSClient.cpp | 13 +-- src/test/overlay/short_read_test.cpp | 99 +++++++++---------- src/test/resource/Logic_test.cpp | 8 +- src/test/rpc/AccountTx_test.cpp | 3 +- src/test/rpc/LedgerRequest_test.cpp | 3 +- src/test/rpc/RPCCall_test.cpp | 3 +- src/test/rpc/TransactionEntry_test.cpp | 3 +- src/test/rpc/Transaction_test.cpp | 6 +- src/test/shamap/FetchPack_test.cpp | 96 ++++++++---------- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 +- src/xrpld/app/misc/NetworkOPs.cpp | 2 +- src/xrpld/app/misc/SHAMapStoreImp.cpp | 8 +- src/xrpld/app/misc/detail/WorkBase.h | 38 +++---- src/xrpld/app/misc/detail/WorkFile.h | 6 +- src/xrpld/app/misc/detail/WorkSSL.cpp | 3 +- .../app/rdb/backend/detail/SQLiteDatabase.cpp | 20 ++-- src/xrpld/overlay/detail/ConnectAttempt.cpp | 20 ++-- src/xrpld/overlay/detail/OverlayImpl.cpp | 10 +- src/xrpld/overlay/detail/OverlayImpl.h | 2 +- src/xrpld/overlay/detail/PeerImp.cpp | 28 +++--- src/xrpld/peerfinder/detail/Checker.h | 3 +- src/xrpld/peerfinder/detail/Logic.h | 10 +- .../peerfinder/detail/PeerfinderManager.cpp | 3 +- src/xrpld/rpc/detail/Pathfinder.cpp | 9 +- src/xrpld/rpc/detail/RPCCall.cpp | 28 +++--- 40 files changed, 356 insertions(+), 445 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 6a24cbc8d3..c5f638b395 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -64,7 +64,6 @@ Checks: "-*, -misc-use-internal-linkage, modernize-*, - -modernize-avoid-bind, -modernize-avoid-c-arrays, -modernize-avoid-c-style-cast, -modernize-avoid-setjmp-longjmp, diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index ffcd533216..ca50bb64b7 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -57,7 +57,10 @@ inline TaggedCache< beast::insight::Collector::ptr const& collector) : journal_(journal) , clock_(clock) - , stats_(name, std::bind(&TaggedCache::collectMetrics, this), collector) + , stats_( + name, + [this] { collectMetrics(); }, + collector) , name_(name) , targetSize_(size) , targetAge_(expiration) diff --git a/include/xrpl/beast/unit_test/thread.h b/include/xrpl/beast/unit_test/thread.h index 0de039cb89..91d8cf3cab 100644 --- a/include/xrpl/beast/unit_test/thread.h +++ b/include/xrpl/beast/unit_test/thread.h @@ -45,7 +45,10 @@ public: template explicit Thread(Suite& s, F&& f, Args&&... args) : s_(&s) { - std::function b = std::bind(std::forward(f), std::forward(args)...); + std::function b = [f = std::forward(f), + ... args = std::forward(args)]() mutable { + std::invoke(f, args...); + }; t_ = std::thread(&Thread::run, this, std::move(b)); } diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h index 72eaed7439..b98885959d 100644 --- a/include/xrpl/net/AutoSocket.h +++ b/include/xrpl/net/AutoSocket.h @@ -120,12 +120,9 @@ public: socket_->next_layer().async_receive( boost::asio::buffer(buffer_), boost::asio::socket_base::message_peek, - std::bind( - &AutoSocket::handleAutodetect, - this, - cbFunc, - std::placeholders::_1, - std::placeholders::_2)); + [this, cbFunc](error_code const& ec, size_t bytesTransferred) { + handleAutodetect(cbFunc, ec, bytesTransferred); + }); } } diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h index 4192455ec9..2b26d7e404 100644 --- a/include/xrpl/net/HTTPClientSSLContext.h +++ b/include/xrpl/net/HTTPClientSSLContext.h @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -127,8 +126,9 @@ public: if (!ec) { strm.set_verify_callback( - std::bind( - &rfc6125Verify, host, std::placeholders::_1, std::placeholders::_2, j_), + [host, j = j_](bool preverified, boost::asio::ssl::verify_context& ctx) { + return rfc6125Verify(host, preverified, ctx, j); + }, ec); } } diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index f096d3c5a9..7b35dbd4be 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -223,10 +223,7 @@ BaseHTTPPeer::close() { if (!strand_.running_in_this_thread()) { - return post( - strand_, - std::bind( - (void (BaseHTTPPeer::*)(void))&BaseHTTPPeer::close, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->close(); }); } boost::beast::get_lowest_layer(impl().stream_).close(); } @@ -322,22 +319,18 @@ BaseHTTPPeer::onWrite(error_code const& ec, std::size_t bytesTran v, bind_executor( strand_, - std::bind( - &BaseHTTPPeer::onWrite, - impl().shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = impl().shared_from_this()]( + error_code const& ec, std::size_t bytesTransferred) { + self->onWrite(ec, bytesTransferred); + })); } if (!complete_) return; if (graceful_) return doClose(); - util::spawn( - strand_, - std::bind( - &BaseHTTPPeer::doRead, - impl().shared_from_this(), - std::placeholders::_1)); + util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) { + self->doRead(doYield); + }); } template @@ -351,14 +344,9 @@ BaseHTTPPeer::doWriter( { auto const p = impl().shared_from_this(); resume = std::function([this, p, writer, keepAlive]() { - util::spawn( - strand_, - std::bind( - &BaseHTTPPeer::doWriter, - p, - writer, - keepAlive, - std::placeholders::_1)); + util::spawn(strand_, [p, writer, keepAlive](yield_context doYield) { + p->doWriter(writer, keepAlive, doYield); + }); }); } @@ -379,12 +367,9 @@ BaseHTTPPeer::doWriter( if (!keepAlive) return doClose(); - util::spawn( - strand_, - std::bind( - &BaseHTTPPeer::doRead, - impl().shared_from_this(), - std::placeholders::_1)); + util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) { + self->doRead(doYield); + }); } //------------------------------------------------------------------------------ @@ -405,8 +390,7 @@ BaseHTTPPeer::write(void const* buf, std::size_t bytes) if (!strand_.running_in_this_thread()) { return post( - strand_, - std::bind(&BaseHTTPPeer::onWrite, impl().shared_from_this(), error_code{}, 0)); + strand_, [self = impl().shared_from_this()] { self->onWrite(error_code{}, 0); }); } return onWrite(error_code{}, 0); } @@ -417,13 +401,9 @@ void BaseHTTPPeer::write(std::shared_ptr const& writer, bool keepAlive) { util::spawn( - strand_, - std::bind( - &BaseHTTPPeer::doWriter, - impl().shared_from_this(), - writer, - keepAlive, - std::placeholders::_1)); + strand_, [self = impl().shared_from_this(), writer, keepAlive](yield_context doYield) { + self->doWriter(writer, keepAlive, doYield); + }); } // DEPRECATED @@ -443,8 +423,7 @@ BaseHTTPPeer::complete() { if (!strand_.running_in_this_thread()) { - return post( - strand_, std::bind(&BaseHTTPPeer::complete, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->complete(); }); } message_ = {}; @@ -457,12 +436,9 @@ BaseHTTPPeer::complete() } // keep-alive - util::spawn( - strand_, - std::bind( - &BaseHTTPPeer::doRead, - impl().shared_from_this(), - std::placeholders::_1)); + util::spawn(strand_, [self = impl().shared_from_this()](yield_context doYield) { + self->doRead(doYield); + }); } // DEPRECATED @@ -474,11 +450,7 @@ BaseHTTPPeer::close(bool graceful) if (!strand_.running_in_this_thread()) { return post( - strand_, - std::bind( - (void (BaseHTTPPeer::*)(bool))&BaseHTTPPeer::close, - impl().shared_from_this(), - graceful)); + strand_, [self = impl().shared_from_this(), graceful] { self->close(graceful); }); } complete_ = true; diff --git a/include/xrpl/server/detail/BasePeer.h b/include/xrpl/server/detail/BasePeer.h index 5301a2f018..84aa1e0da9 100644 --- a/include/xrpl/server/detail/BasePeer.h +++ b/include/xrpl/server/detail/BasePeer.h @@ -10,7 +10,6 @@ #include #include -#include #include #include @@ -84,7 +83,7 @@ void BasePeer::close() { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BasePeer::close, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->close(); }); error_code ec; xrpl::getLowestLayer(impl().ws_).socket().close(ec); } diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index 1b6a9faca3..d557140bd4 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -180,12 +181,13 @@ void BaseWSPeer::run() { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BaseWSPeer::run, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->run(); }); impl().ws_.set_option(port().pmdOptions); // Must manage the control callback memory outside of the `control_callback` // function - controlCallback_ = - std::bind(&BaseWSPeer::onPingPong, this, std::placeholders::_1, std::placeholders::_2); + controlCallback_ = [this]( + boost::beast::websocket::frame_type kind, + boost::beast::string_view payload) { onPingPong(kind, payload); }; impl().ws_.control_callback(controlCallback_); startTimer(); closeOnTimer_ = true; @@ -193,11 +195,9 @@ BaseWSPeer::run() res.set(boost::beast::http::field::server, BuildInfo::getFullVersionString()); })); impl().ws_.async_accept( - request_, - bind_executor( - strand_, - std::bind( - &BaseWSPeer::onWsHandshake, impl().shared_from_this(), std::placeholders::_1))); + request_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) { + self->onWsHandshake(ec); + })); } template @@ -205,7 +205,10 @@ void BaseWSPeer::send(std::shared_ptr w) { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BaseWSPeer::send, impl().shared_from_this(), std::move(w))); + { + return post( + strand_, [self = impl().shared_from_this(), w = std::move(w)] { self->send(w); }); + } if (doClose_) return; if (wq_.size() > port().wsQueueLimit) @@ -258,7 +261,7 @@ void BaseWSPeer::complete() { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BaseWSPeer::complete, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->complete(); }); doRead(); } @@ -277,7 +280,7 @@ void BaseWSPeer::doWrite() { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BaseWSPeer::doWrite, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->doWrite(); }); onWrite({}); } @@ -288,8 +291,7 @@ BaseWSPeer::onWrite(error_code const& ec) if (ec) return fail(ec, "write"); auto& w = *wq_.front(); - auto const result = - w.prepare(65536, std::bind(&BaseWSPeer::doWrite, impl().shared_from_this())); + auto const result = w.prepare(65536, [self = impl().shared_from_this()] { self->doWrite(); }); if (boost::indeterminate(result.first)) return; startTimer(); @@ -299,8 +301,9 @@ BaseWSPeer::onWrite(error_code const& ec) static_cast(result.first), result.second, bind_executor( - strand_, - std::bind(&BaseWSPeer::onWrite, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onWrite(ec); + })); } else { @@ -308,9 +311,9 @@ BaseWSPeer::onWrite(error_code const& ec) static_cast(result.first), result.second, bind_executor( - strand_, - std::bind( - &BaseWSPeer::onWriteFin, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onWriteFin(ec); + })); } } @@ -324,10 +327,9 @@ BaseWSPeer::onWriteFin(error_code const& ec) if (doClose_) { impl().ws_.async_close( - cr_, - bind_executor( - strand_, - std::bind(&BaseWSPeer::onClose, impl().shared_from_this(), std::placeholders::_1))); + cr_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) { + self->onClose(ec); + })); } else if (!wq_.empty()) { @@ -340,12 +342,13 @@ void BaseWSPeer::doRead() { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&BaseWSPeer::doRead, impl().shared_from_this())); + return post(strand_, [self = impl().shared_from_this()] { self->doRead(); }); impl().ws_.async_read( rb_, bind_executor( - strand_, - std::bind(&BaseWSPeer::onRead, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onRead(ec); + })); } template @@ -389,11 +392,7 @@ BaseWSPeer::startTimer() } timer_.async_wait(bind_executor( - strand_, - std::bind( - &BaseWSPeer::onTimer, - impl().shared_from_this(), - std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } // Convenience for discarding the error code @@ -461,10 +460,9 @@ BaseWSPeer::onTimer(error_code ec) beast::rngfill(payload_.begin(), payload_.size(), cryptoPrng()); impl().ws_.async_ping( payload_, - bind_executor( - strand_, - std::bind( - &BaseWSPeer::onPing, impl().shared_from_this(), std::placeholders::_1))); + bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) { + self->onPing(ec); + })); JLOG(this->j_.trace()) << "sent ping"; return; } diff --git a/include/xrpl/server/detail/Door.h b/include/xrpl/server/detail/Door.h index 09b0adc5f2..d2d7a7baf4 100644 --- a/include/xrpl/server/detail/Door.h +++ b/include/xrpl/server/detail/Door.h @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -182,7 +181,7 @@ void Door::Detector::run() { util::spawn( - strand_, std::bind(&Detector::doDetect, this->shared_from_this(), std::placeholders::_1)); + strand_, [self = this->shared_from_this()](yield_context yield) { self->doDetect(yield); }); } template @@ -297,8 +296,7 @@ void Door::run() { util::spawn( - strand_, - std::bind(&Door::doAccept, this->shared_from_this(), std::placeholders::_1)); + strand_, [self = this->shared_from_this()](yield_context yield) { self->doAccept(yield); }); } template @@ -307,8 +305,7 @@ Door::close() { if (!strand_.running_in_this_thread()) { - return boost::asio::post( - strand_, std::bind(&Door::close, this->shared_from_this())); + return boost::asio::post(strand_, [self = this->shared_from_this()] { self->close(); }); } backoffTimer_.cancel(); error_code ec; diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h b/include/xrpl/server/detail/PlainHTTPPeer.h index b89272fe9e..688fbbd425 100644 --- a/include/xrpl/server/detail/PlainHTTPPeer.h +++ b/include/xrpl/server/detail/PlainHTTPPeer.h @@ -9,7 +9,6 @@ #include -#include #include #include @@ -89,7 +88,9 @@ PlainHTTPPeer::run() { if (!this->handler_.onAccept(this->session(), this->remoteAddress_)) { - util::spawn(this->strand_, std::bind(&PlainHTTPPeer::doClose, this->shared_from_this())); + util::spawn(this->strand_, [self = this->shared_from_this()](boost::asio::yield_context) { + self->doClose(); + }); return; } @@ -97,8 +98,9 @@ PlainHTTPPeer::run() return; util::spawn( - this->strand_, - std::bind(&PlainHTTPPeer::doRead, this->shared_from_this(), std::placeholders::_1)); + this->strand_, [self = this->shared_from_this()](boost::asio::yield_context doYield) { + self->doRead(doYield); + }); } template diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h b/include/xrpl/server/detail/SSLHTTPPeer.h index ea2108b917..7fe21ed6b5 100644 --- a/include/xrpl/server/detail/SSLHTTPPeer.h +++ b/include/xrpl/server/detail/SSLHTTPPeer.h @@ -13,7 +13,6 @@ #include #include -#include #include #include @@ -99,14 +98,15 @@ SSLHTTPPeer::run() { if (!this->handler_.onAccept(this->session(), this->remoteAddress_)) { - util::spawn(this->strand_, std::bind(&SSLHTTPPeer::doClose, this->shared_from_this())); + util::spawn( + this->strand_, [self = this->shared_from_this()](yield_context) { self->doClose(); }); return; } if (!socket_.is_open()) return; - util::spawn( - this->strand_, - std::bind(&SSLHTTPPeer::doHandshake, this->shared_from_this(), std::placeholders::_1)); + util::spawn(this->strand_, [self = this->shared_from_this()](yield_context doYield) { + self->doHandshake(doYield); + }); } template @@ -142,9 +142,9 @@ SSLHTTPPeer::doHandshake(yield_context doYield) this->port().protocol.count("https") > 0; if (http) { - util::spawn( - this->strand_, - std::bind(&SSLHTTPPeer::doRead, this->shared_from_this(), std::placeholders::_1)); + util::spawn(this->strand_, [self = this->shared_from_this()](yield_context doYield) { + self->doRead(doYield); + }); return; } // `this` will be destroyed @@ -172,7 +172,7 @@ SSLHTTPPeer::doClose() this->startTimer(); stream_.async_shutdown(bind_executor( this->strand_, - std::bind(&SSLHTTPPeer::onShutdown, this->shared_from_this(), std::placeholders::_1))); + [self = this->shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); } template diff --git a/src/libxrpl/basics/ResolverAsio.cpp b/src/libxrpl/basics/ResolverAsio.cpp index ddf006b681..7e1b56f87a 100644 --- a/src/libxrpl/basics/ResolverAsio.cpp +++ b/src/libxrpl/basics/ResolverAsio.cpp @@ -192,7 +192,7 @@ public: boost::asio::dispatch( ioContext, boost::asio::bind_executor( - strand, std::bind(&ResolverAsioImpl::doStop, this, CompletionCounter(this)))); + strand, [this, counter = CompletionCounter(this)] { doStop(counter); })); JLOG(journal.debug()) << "Queued a stop request"; } @@ -221,9 +221,9 @@ public: boost::asio::dispatch( ioContext, boost::asio::bind_executor( - strand, - std::bind( - &ResolverAsioImpl::doResolve, this, names, handler, CompletionCounter(this)))); + strand, [this, names, handler, counter = CompletionCounter(this)] { + doResolve(names, handler, counter); + })); } //------------------------------------------------------------------------- @@ -272,7 +272,7 @@ public: boost::asio::post( ioContext, boost::asio::bind_executor( - strand, std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this)))); + strand, [this, counter = CompletionCounter(this)] { doWork(counter); })); } static HostAndPort @@ -291,8 +291,10 @@ public: // a port separator // Attempt to find the first and last non-whitespace - auto const findWhitespace = - std::bind(&std::isspace, std::placeholders::_1, std::locale()); + std::locale const loc; + auto const findWhitespace = [&loc](std::string::value_type c) { + return std::isspace(c, loc); + }; auto hostFirst = std::ranges::find_if_not(str, findWhitespace); @@ -348,7 +350,7 @@ public: boost::asio::post( ioContext, boost::asio::bind_executor( - strand, std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this)))); + strand, [this, counter = CompletionCounter(this)] { doWork(counter); })); return; } @@ -356,14 +358,11 @@ public: resolver.async_resolve( host, port, - std::bind( - &ResolverAsioImpl::doFinish, - this, - name, - std::placeholders::_1, - handler, - std::placeholders::_2, - CompletionCounter(this))); + [this, name, handler, counter = CompletionCounter(this)]( + boost::system::error_code const& ec, + boost::asio::ip::tcp::resolver::results_type results) { + doFinish(name, ec, handler, results, counter); + }); } void @@ -383,8 +382,7 @@ public: boost::asio::post( ioContext, boost::asio::bind_executor( - strand, - std::bind(&ResolverAsioImpl::doWork, this, CompletionCounter(this)))); + strand, [this, counter = CompletionCounter(this)] { doWork(counter); })); } } } diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 1da5315de1..3cff5d93b5 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -325,9 +325,9 @@ public: postBuffer(std::string&& buffer) { boost::asio::dispatch( - ioContext_, - boost::asio::bind_executor( - strand_, std::bind(&StatsDCollectorImp::doPostBuffer, this, std::move(buffer)))); + ioContext_, boost::asio::bind_executor(strand_, [this, buffer = std::move(buffer)] { + doPostBuffer(buffer); + })); } // The keepAlive parameter makes sure the buffers sent to @@ -392,12 +392,10 @@ public: log(buffers); socket_.async_send( buffers, - std::bind( - &StatsDCollectorImp::onSend, - this, - keepAlive, - std::placeholders::_1, - std::placeholders::_2)); + [this, keepAlive]( + boost::system::error_code const& ec, std::size_t bytesTransferred) { + onSend(keepAlive, ec, bytesTransferred); + }); buffers.clear(); size = 0; } @@ -411,12 +409,10 @@ public: log(buffers); socket_.async_send( buffers, - std::bind( - &StatsDCollectorImp::onSend, - this, - keepAlive, - std::placeholders::_1, - std::placeholders::_2)); + [this, keepAlive]( + boost::system::error_code const& ec, std::size_t bytesTransferred) { + onSend(keepAlive, ec, bytesTransferred); + }); } } @@ -425,7 +421,7 @@ public: { using namespace std::chrono_literals; timer_.expires_after(1s); - timer_.async_wait(std::bind(&StatsDCollectorImp::onTimer, this, std::placeholders::_1)); + timer_.async_wait([this](boost::system::error_code const& ec) { onTimer(ec); }); } void @@ -513,10 +509,9 @@ StatsDCounterImpl::increment(CounterImpl::value_type amount) { boost::asio::dispatch( impl_->getIoContext(), - std::bind( - &StatsDCounterImpl::doIncrement, - std::static_pointer_cast(shared_from_this()), - amount)); + [self = std::static_pointer_cast(shared_from_this()), amount] { + self->doIncrement(amount); + }); } void @@ -558,10 +553,9 @@ StatsDEventImpl::notify(EventImpl::value_type const& value) { boost::asio::dispatch( impl_->getIoContext(), - std::bind( - &StatsDEventImpl::doNotify, - std::static_pointer_cast(shared_from_this()), - value)); + [self = std::static_pointer_cast(shared_from_this()), value] { + self->doNotify(value); + }); } void @@ -591,10 +585,9 @@ StatsDGaugeImpl::set(GaugeImpl::value_type value) { boost::asio::dispatch( impl_->getIoContext(), - std::bind( - &StatsDGaugeImpl::doSet, - std::static_pointer_cast(shared_from_this()), - value)); + [self = std::static_pointer_cast(shared_from_this()), value] { + self->doSet(value); + }); } void @@ -602,10 +595,9 @@ StatsDGaugeImpl::increment(GaugeImpl::difference_type amount) { boost::asio::dispatch( impl_->getIoContext(), - std::bind( - &StatsDGaugeImpl::doIncrement, - std::static_pointer_cast(shared_from_this()), - amount)); + [self = std::static_pointer_cast(shared_from_this()), amount] { + self->doIncrement(amount); + }); } void @@ -678,10 +670,9 @@ StatsDMeterImpl::increment(MeterImpl::value_type amount) { boost::asio::dispatch( impl_->getIoContext(), - std::bind( - &StatsDMeterImpl::doIncrement, - std::static_pointer_cast(shared_from_this()), - amount)); + [self = std::static_pointer_cast(shared_from_this()), amount] { + self->doIncrement(amount); + }); } void diff --git a/src/libxrpl/core/detail/JobQueue.cpp b/src/libxrpl/core/detail/JobQueue.cpp index f773270af3..8f95a8a5fa 100644 --- a/src/libxrpl/core/detail/JobQueue.cpp +++ b/src/libxrpl/core/detail/JobQueue.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -36,7 +35,7 @@ JobQueue::JobQueue( { JLOG(journal_.info()) << "Using " << threadCount << " threads"; - hook_ = collector_->makeHook(std::bind(&JobQueue::collect, this)); + hook_ = collector_->makeHook([this] { collect(); }); jobCount_ = collector_->makeGauge("job_count"); { diff --git a/src/libxrpl/net/HTTPClient.cpp b/src/libxrpl/net/HTTPClient.cpp index 4b9cc9d6e6..7294633964 100644 --- a/src/libxrpl/net/HTTPClient.cpp +++ b/src/libxrpl/net/HTTPClient.cpp @@ -134,12 +134,10 @@ public: request( bSSL, deqSites, - std::bind( - &HTTPClientImp::makeGet, - shared_from_this(), - strPath, - std::placeholders::_1, - std::placeholders::_2), + [self = shared_from_this(), strPath]( + boost::asio::streambuf& sb, std::string const& strHost) { + self->makeGet(strPath, sb, strHost); + }, timeout, complete); } @@ -166,9 +164,9 @@ public: shutdown_ = e.code(); JLOG(j_.trace()) << "expires_after: " << shutdown_.message(); - deadline_.async_wait( - std::bind( - &HTTPClientImp::handleDeadline, shared_from_this(), std::placeholders::_1)); + deadline_.async_wait([self = shared_from_this()](boost::system::error_code const& ec) { + self->handleDeadline(ec); + }); } if (!shutdown_) @@ -179,11 +177,11 @@ public: query_->host, query_->port, query_->flags, - std::bind( - &HTTPClientImp::handleResolve, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2)); + [self = shared_from_this()]( + boost::system::error_code const& ecResult, + boost::asio::ip::tcp::resolver::results_type results) { + self->handleResolve(ecResult, results); + }); } if (shutdown_) @@ -220,9 +218,9 @@ public: resolver_.cancel(); // Stop the transaction. - socket_.asyncShutdown( - std::bind( - &HTTPClientImp::handleShutdown, shared_from_this(), std::placeholders::_1)); + socket_.asyncShutdown([self = shared_from_this()](boost::system::error_code const& ec) { + self->handleShutdown(ec); + }); } } @@ -262,8 +260,9 @@ public: boost::asio::async_connect( socket_.lowestLayer(), result, - std::bind( - &HTTPClientImp::handleConnect, shared_from_this(), std::placeholders::_1)); + [self = shared_from_this()]( + boost::system::error_code const& ecResult, + boost::asio::ip::tcp::endpoint const&) { self->handleConnect(ecResult); }); } } @@ -301,8 +300,9 @@ public: { socket_.asyncHandshake( AutoSocket::ssl_socket::client, - std::bind( - &HTTPClientImp::handleRequest, shared_from_this(), std::placeholders::_1)); + [self = shared_from_this()](boost::system::error_code const& ec) { + self->handleRequest(ec); + }); } else { @@ -330,11 +330,10 @@ public: socket_.asyncWrite( request_, - std::bind( - &HTTPClientImp::handleWrite, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2)); + [self = shared_from_this()]( + boost::system::error_code const& ecResult, std::size_t bytesTransferred) { + self->handleWrite(ecResult, bytesTransferred); + }); } } @@ -357,11 +356,10 @@ public: socket_.asyncReadUntil( header_, "\r\n\r\n", - std::bind( - &HTTPClientImp::handleHeader, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2)); + [self = shared_from_this()]( + boost::system::error_code const& ecResult, std::size_t bytesTransferred) { + self->handleHeader(ecResult, bytesTransferred); + }); } } @@ -424,11 +422,10 @@ public: socket_.asyncRead( response_.prepare(responseSize - body_.size()), boost::asio::transfer_all(), - std::bind( - &HTTPClientImp::handleData, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2)); + [self = shared_from_this()]( + boost::system::error_code const& ecResult, std::size_t bytesTransferred) { + self->handleData(ecResult, bytesTransferred); + }); } } diff --git a/src/test/jtx/impl/WSClient.cpp b/src/test/jtx/impl/WSClient.cpp index c00a88f270..ca322415fb 100644 --- a/src/test/jtx/impl/WSClient.cpp +++ b/src/test/jtx/impl/WSClient.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -172,9 +173,9 @@ public: })); ws_.handshake(ep.address().to_string() + ":" + std::to_string(ep.port()), "/"); ws_.async_read( - rb_, - boost::asio::bind_executor( - strand_, std::bind(&WSClientImpl::onReadMsg, this, std::placeholders::_1))); + rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) { + onReadMsg(ec); + })); } catch (std::exception&) { @@ -310,9 +311,9 @@ private: cv_.notify_all(); } ws_.async_read( - rb_, - boost::asio::bind_executor( - strand_, std::bind(&WSClientImpl::onReadMsg, this, std::placeholders::_1))); + rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) { + onReadMsg(ec); + })); } // Called when the read op terminates diff --git a/src/test/overlay/short_read_test.cpp b/src/test/overlay/short_read_test.cpp index 9af632d65c..f235008d89 100644 --- a/src/test/overlay/short_read_test.cpp +++ b/src/test/overlay/short_read_test.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -199,7 +198,7 @@ private: { if (!strand.running_in_this_thread()) { - post(strand, std::bind(&Acceptor::close, shared_from_this())); + post(strand, [self = shared_from_this()] { self->close(); }); return; } acceptor.close(); @@ -210,9 +209,9 @@ private: { acceptor.async_accept( socket, - bind_executor( - strand, - std::bind(&Acceptor::onAccept, shared_from_this(), std::placeholders::_1))); + bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onAccept(ec); + })); } void @@ -239,9 +238,9 @@ private: p->run(); acceptor.async_accept( socket, - bind_executor( - strand, - std::bind(&Acceptor::onAccept, shared_from_this(), std::placeholders::_1))); + bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onAccept(ec); + })); } }; @@ -271,7 +270,7 @@ private: { if (!strand.running_in_this_thread()) { - post(strand, std::bind(&Connection::close, shared_from_this())); + post(strand, [self = shared_from_this()] { self->close(); }); return; } if (socket.is_open()) @@ -287,13 +286,12 @@ private: timer.expires_after(std::chrono::seconds(3)); timer.async_wait(bind_executor( strand, - std::bind(&Connection::onTimer, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); stream.async_handshake( stream_type::server, - bind_executor( - strand, - std::bind( - &Connection::onHandshake, shared_from_this(), std::placeholders::_1))); + bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onHandshake(ec); + })); } void @@ -337,11 +335,10 @@ private: "\n", bind_executor( strand, - std::bind( - &Connection::onRead, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()]( + error_code const& ec, std::size_t bytesTransferred) { + self->onRead(ec, bytesTransferred); + })); #else close(); #endif @@ -353,10 +350,10 @@ private: if (ec == boost::asio::error::eof) { server.test_.log << "[server] read: EOF" << std::endl; - stream.async_shutdown(bind_executor( - strand, - std::bind( - &Connection::onShutdown, shared_from_this(), std::placeholders::_1))); + stream.async_shutdown( + bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onShutdown(ec); + })); return; } if (ec) @@ -373,11 +370,10 @@ private: buf.data(), bind_executor( strand, - std::bind( - &Connection::onWrite, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()]( + error_code const& ec, std::size_t bytesTransferred) { + self->onWrite(ec, bytesTransferred); + })); } void @@ -391,7 +387,7 @@ private: } stream.async_shutdown(bind_executor( strand, - std::bind(&Connection::onShutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); } void @@ -463,7 +459,7 @@ private: { if (!strand.running_in_this_thread()) { - post(strand, std::bind(&Connection::close, shared_from_this())); + post(strand, [self = shared_from_this()] { self->close(); }); return; } if (socket.is_open()) @@ -479,13 +475,11 @@ private: timer.expires_after(std::chrono::seconds(3)); timer.async_wait(bind_executor( strand, - std::bind(&Connection::onTimer, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); socket.async_connect( - ep, - bind_executor( - strand, - std::bind( - &Connection::onConnect, shared_from_this(), std::placeholders::_1))); + ep, bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onConnect(ec); + })); } void @@ -524,10 +518,9 @@ private: } stream.async_handshake( stream_type::client, - bind_executor( - strand, - std::bind( - &Connection::onHandshake, shared_from_this(), std::placeholders::_1))); + bind_executor(strand, [self = shared_from_this()](error_code const& ec) { + self->onHandshake(ec); + })); } void @@ -546,16 +539,14 @@ private: buf.data(), bind_executor( strand, - std::bind( - &Connection::onWrite, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()]( + error_code const& ec, std::size_t bytesTransferred) { + self->onWrite(ec, bytesTransferred); + })); #else stream_.async_shutdown(bind_executor( strand_, - std::bind( - &Connection::on_shutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->on_shutdown(ec); })); #endif } @@ -575,16 +566,14 @@ private: "\n", bind_executor( strand, - std::bind( - &Connection::onRead, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()]( + error_code const& ec, std::size_t bytesTransferred) { + self->onRead(ec, bytesTransferred); + })); #else stream_.async_shutdown(bind_executor( strand_, - std::bind( - &Connection::on_shutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->on_shutdown(ec); })); #endif } @@ -599,7 +588,7 @@ private: buf.commit(bytesTransferred); stream.async_shutdown(bind_executor( strand, - std::bind(&Connection::onShutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); } void diff --git a/src/test/resource/Logic_test.cpp b/src/test/resource/Logic_test.cpp index de4575cb16..f6b4875356 100644 --- a/src/test/resource/Logic_test.cpp +++ b/src/test/resource/Logic_test.cpp @@ -89,9 +89,11 @@ public: Charge const fee(kDropThreshold + 1); beast::IP::Endpoint const addr(beast::IP::Endpoint::fromString("192.0.2.2")); - std::function const ep = limited - ? std::bind(&TestLogic::newInboundEndpoint, &logic, std::placeholders::_1) - : std::bind(&TestLogic::newUnlimitedEndpoint, &logic, std::placeholders::_1); + std::function const ep = + [&logic, limited](beast::IP::Endpoint const& address) { + return limited ? logic.newInboundEndpoint(address) + : logic.newUnlimitedEndpoint(address); + }; { Consumer c(ep(addr)); diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index bd7b07e936..ace8743e1e 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -41,7 +41,6 @@ #include #include #include -#include #include #include #include @@ -893,7 +892,7 @@ public: void run() override { - forAllApiVersions(std::bind_front(&AccountTx_test::testParameters, this)); + forAllApiVersions([this](unsigned apiVersion) { testParameters(apiVersion); }); testContents(); testAccountDelete(); testMPT(); diff --git a/src/test/rpc/LedgerRequest_test.cpp b/src/test/rpc/LedgerRequest_test.cpp index 4759256b96..93feee9497 100644 --- a/src/test/rpc/LedgerRequest_test.cpp +++ b/src/test/rpc/LedgerRequest_test.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -350,7 +349,7 @@ public: { testLedgerRequest(); testEvolution(); - forAllApiVersions(std::bind_front(&LedgerRequest_test::testBadInput, this)); + forAllApiVersions([this](unsigned apiVersion) { testBadInput(apiVersion); }); testMoreThan256Closed(); testNonAdmin(); } diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index cd3cd3cb41..4b5ab1f230 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -5927,7 +5926,7 @@ public: void run() override { - forAllApiVersions(std::bind_front(&RPCCall_test::testRPCCall, this)); + forAllApiVersions([this](unsigned apiVersion) { testRPCCall(apiVersion); }); } }; diff --git a/src/test/rpc/TransactionEntry_test.cpp b/src/test/rpc/TransactionEntry_test.cpp index 8724e2974d..38a95e84f8 100644 --- a/src/test/rpc/TransactionEntry_test.cpp +++ b/src/test/rpc/TransactionEntry_test.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -353,7 +352,7 @@ public: run() override { testBadInput(); - forAllApiVersions(std::bind_front(&TransactionEntry_test::testRequest, this)); + forAllApiVersions([this](unsigned apiVersion) { testRequest(apiVersion); }); } }; diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 8c50736b85..c3bf707ec4 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -886,7 +885,7 @@ public: run() override { using namespace test::jtx; - forAllApiVersions(std::bind_front(&Transaction_test::testBinaryRequest, this)); + forAllApiVersions([this](unsigned apiVersion) { testBinaryRequest(apiVersion); }); FeatureBitset const all{testableAmendments()}; testWithFeats(all); @@ -899,7 +898,8 @@ public: testRangeCTIDRequest(features); testCTIDValidation(features); testRPCsForCTID(features); - forAllApiVersions(std::bind_front(&Transaction_test::testRequest, this, features)); + forAllApiVersions( + [this, features](unsigned apiVersion) { testRequest(features, apiVersion); }); } }; diff --git a/src/test/shamap/FetchPack_test.cpp b/src/test/shamap/FetchPack_test.cpp index bcc5129f01..f2e7578ee2 100644 --- a/src/test/shamap/FetchPack_test.cpp +++ b/src/test/shamap/FetchPack_test.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -26,7 +25,6 @@ #include #include #include -#include namespace xrpl::tests { @@ -40,15 +38,6 @@ public: using Table = SHAMap; using Item = SHAMapItem; - struct Handler - { - void - operator()(std::uint32_t refNum) const - { - Throw("missing node"); - } - }; - struct TestFilter : SHAMapSyncFilter { TestFilter(Map& map, beast::Journal journal) : map(map), journal(journal) @@ -93,7 +82,7 @@ public: static void addRandomItems(std::size_t n, Table& t, beast::xor_shift_engine& r) { - while ((n--) != 0u) + for (std::size_t i = 0; i < n; ++i) { auto const result(t.addItem(SHAMapNodeType::TnAccountState, makeRandomItemMember(r))); assert(result); @@ -111,56 +100,51 @@ public: void run() override { - using beast::Severity; + testFetchPack(); + } + + // Exercises a fetch-pack round trip: build a SHAMap, serialize every node + // into a pack keyed by node hash, then rebuild the map in a fresh SHAMap by + // sourcing every node from the pack through a SHAMapSyncFilter and comparing + // the result. This covers the filter-based reconstruction path (fetchRoot + + // getMissingNodes with a SHAMapSyncFilter), complementing SHAMapSync_test, + // which drives the getNodeFat/addKnownNode path. + void + testFetchPack() + { test::SuiteJournal journal("FetchPack_test", *this); + TestNodeFamily f(journal), f2(journal); + beast::xor_shift_engine r; - TestNodeFamily f(journal); - std::shared_ptr const t1(std::make_shared
(SHAMapType::FREE, f)); + // Build a source map. getHash() unshares the tree and computes every + // node hash; this must happen before serializing nodes below, otherwise + // inner nodes still carry stale cached hashes. + auto const source = std::make_shared
(SHAMapType::FREE, f); + addRandomItems(kTableItems + kTableItemsExtra, *source, r); + source->setImmutable(); + auto const rootHash = source->getHash(); - pass(); + // Turn the source into a fetch pack: node hash -> serialized node. + Map map; + source->visitNodes([this, &map](SHAMapTreeNode& node) { + Serializer s; + node.serializeWithPrefix(s); + onFetch(map, node.getHash(), s.getData()); + return true; + }); - // beast::Random r; - // add_random_items_ (tableItems, *t1, r); - // std::shared_ptr
t2 (t1->snapShot (true)); - // - // add_random_items_ (tableItemsExtra, *t1, r); - // add_random_items_ (tableItemsExtra, *t2, r); + // Rebuild the map in a fresh family, sourcing every node from the pack + // through the SHAMapSyncFilter. + auto const rebuilt = std::make_shared
(SHAMapType::FREE, rootHash.asUInt256(), f2); + TestFilter filter(map, journal); + rebuilt->setSynching(); + BEAST_EXPECT(rebuilt->fetchRoot(rootHash, &filter)); - // turn t1 into t2 - // Map map; - // t2->getFetchPack (t1.get(), true, 1000000, std::bind ( - // &FetchPack_test::on_fetch, this, std::ref (map), - // std::placeholders::_1, std::placeholders::_2)); - // t1->getFetchPack (nullptr, true, 1000000, std::bind ( - // &FetchPack_test::on_fetch, this, std::ref (map), - // std::placeholders::_1, std::placeholders::_2)); + // Everything should be in the pack, so no nodes should be missing. + BEAST_EXPECT(rebuilt->getMissingNodes(2048, &filter).empty()); + rebuilt->clearSynching(); - // try to rebuild t2 from the fetch pack - // std::shared_ptr
t3; - // try - // { - // TestFilter filter (map, beast::Journal()); - // - // t3 = std::make_shared
(SHAMapType::FREE, - // t2->getHash (), - // fullBelowCache); - // - // BEAST_EXPECT(t3->fetchRoot (t2->getHash (), &filter), - // "unable to get root"); - // - // // everything should be in the pack, no hashes should be - // needed std::vector hashes = - // t3->getNeededHashes(1, &filter); - // BEAST_EXPECT(hashes.empty(), "missing hashes"); - // - // BEAST_EXPECT(t3->getHash () == t2->getHash (), "root - // hashes do not match"); BEAST_EXPECT(t3->deepCompare - // (*t2), "failed compare"); - // } - // catch (std::exception const&) - // { - // fail ("unhandled exception"); - // } + BEAST_EXPECT(rebuilt->deepCompare(*source)); } }; diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 3fcac03ed5..bab0dca827 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -137,7 +137,7 @@ LedgerMaster::LedgerMaster( std::chrono::seconds{45}, stopwatch, app_.getJournal("TaggedCache")) - , stats_(std::bind(&LedgerMaster::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d1f794b74d..4d40247a29 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -330,7 +330,7 @@ public: , jobQueue_(jobQueue) , standalone_(standalone) , minPeerCount_(startValid ? 0 : minPeerCount) - , stats_(std::bind(&NetworkOPsImp::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 0389130f7a..0e0099cdbf 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -331,11 +331,9 @@ SHAMapStoreImp::run() try { validatedLedger->stateMap().snapShot(false)->visitNodes( - std::bind( - &SHAMapStoreImp::copyNode, - this, - std::ref(nodeCount), - std::placeholders::_1)); + [this, &nodeCount](SHAMapTreeNode const& node) { + return copyNode(nodeCount, node); + }); } catch (SHAMapMissingNode const& e) { diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h index 5f5fdd28b7..73e5081036 100644 --- a/src/xrpld/app/misc/detail/WorkBase.h +++ b/src/xrpld/app/misc/detail/WorkBase.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -138,9 +139,9 @@ WorkBase::run() if (!strand_.running_in_this_thread()) { return boost::asio::post( - ios_, - boost::asio::bind_executor( - strand_, std::bind(&WorkBase::run, impl().shared_from_this()))); + ios_, boost::asio::bind_executor(strand_, [self = impl().shared_from_this()] { + self->run(); + })); } resolver_.async_resolve( @@ -148,11 +149,9 @@ WorkBase::run() port_, boost::asio::bind_executor( strand_, - std::bind( - &WorkBase::onResolve, - impl().shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = impl().shared_from_this()](error_code const& ec, results_type results) { + self->onResolve(ec, results); + })); } template @@ -165,7 +164,7 @@ WorkBase::cancel() ios_, boost::asio::bind_executor( - strand_, std::bind(&WorkBase::cancel, impl().shared_from_this()))); + strand_, [self = impl().shared_from_this()] { self->cancel(); })); } error_code ec; @@ -196,11 +195,12 @@ WorkBase::onResolve(error_code const& ec, results_type results) results, boost::asio::bind_executor( strand_, - std::bind( - &WorkBase::onConnect, - impl().shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = impl().shared_from_this()]( + error_code const& ec, endpoint_type const& endpoint) { + // Call the base-class overload explicitly: the derived Impl + // hides it with its own single-argument onConnect(ec). + self->WorkBase::onConnect(ec, endpoint); + })); } template @@ -229,8 +229,9 @@ WorkBase::onStart() impl().stream(), req_, boost::asio::bind_executor( - strand_, - std::bind(&WorkBase::onRequest, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onRequest(ec); + })); } template @@ -245,8 +246,9 @@ WorkBase::onRequest(error_code const& ec) readBuf_, res_, boost::asio::bind_executor( - strand_, - std::bind(&WorkBase::onResponse, impl().shared_from_this(), std::placeholders::_1))); + strand_, [self = impl().shared_from_this()](error_code const& ec, std::size_t) { + self->onResponse(ec); + })); } template diff --git a/src/xrpld/app/misc/detail/WorkFile.h b/src/xrpld/app/misc/detail/WorkFile.h index 2fc7ab952d..eb42d14db4 100644 --- a/src/xrpld/app/misc/detail/WorkFile.h +++ b/src/xrpld/app/misc/detail/WorkFile.h @@ -63,9 +63,9 @@ WorkFile::run() { if (!strand_.running_in_this_thread()) { - boost::asio::post( - ios_, - boost::asio::bind_executor(strand_, std::bind(&WorkFile::run, shared_from_this()))); + boost::asio::post(ios_, boost::asio::bind_executor(strand_, [self = shared_from_this()] { + self->run(); + })); return; } diff --git a/src/xrpld/app/misc/detail/WorkSSL.cpp b/src/xrpld/app/misc/detail/WorkSSL.cpp index e1b864e81f..e8d24b55d6 100644 --- a/src/xrpld/app/misc/detail/WorkSSL.cpp +++ b/src/xrpld/app/misc/detail/WorkSSL.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include @@ -55,7 +54,7 @@ WorkSSL::onConnect(error_code const& ec) stream_.async_handshake( boost::asio::ssl::stream_base::client, boost::asio::bind_executor( - strand_, std::bind(&WorkSSL::onHandshake, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onHandshake(ec); })); } void diff --git a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp index fd1298516f..338a3a33df 100644 --- a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp +++ b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp @@ -421,8 +421,9 @@ SQLiteDatabase::oldestAccountTxPage(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(200); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; AccountTxs ret; auto onTransaction = [&ret, &app = registry_.get().getApp()]( std::uint32_t ledgerIndex, @@ -451,8 +452,9 @@ SQLiteDatabase::newestAccountTxPage(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(200); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; AccountTxs ret; auto onTransaction = [&ret, &app = registry_.get().getApp()]( std::uint32_t ledgerIndex, @@ -481,8 +483,9 @@ SQLiteDatabase::oldestAccountTxPageB(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(500); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; MetaTxsList ret; auto onTransaction = [&ret]( @@ -509,8 +512,9 @@ SQLiteDatabase::newestAccountTxPageB(AccountTxPageOptions const& options) return {}; static std::uint32_t const kPageLength(500); - auto onUnsavedLedger = - std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), std::placeholders::_1); + auto onUnsavedLedger = [&app = registry_.get().getApp()](std::uint32_t seq) { + saveLedgerAsync(app, seq); + }; MetaTxsList ret; auto onTransaction = [&ret]( diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 295f4f5497..064b4ecd3e 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -35,8 +35,8 @@ #include #include +#include #include -#include #include #include #include @@ -86,7 +86,7 @@ ConnectAttempt::stop() { if (!strand_.running_in_this_thread()) { - boost::asio::post(strand_, std::bind(&ConnectAttempt::stop, shared_from_this())); + boost::asio::post(strand_, [self = shared_from_this()] { self->stop(); }); return; } if (socket_.is_open()) @@ -104,8 +104,7 @@ ConnectAttempt::run() stream_.next_layer().async_connect( remoteEndpoint_, boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onConnect, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onConnect(ec); })); } //------------------------------------------------------------------------------ @@ -160,8 +159,7 @@ ConnectAttempt::setTimer() timer_.async_wait( boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onTimer, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } void @@ -228,8 +226,7 @@ ConnectAttempt::onConnect(error_code ec) stream_.async_handshake( boost::asio::ssl::stream_base::client, boost::asio::bind_executor( - strand_, - std::bind(&ConnectAttempt::onHandshake, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onHandshake(ec); })); } void @@ -290,7 +287,7 @@ ConnectAttempt::onHandshake(error_code ec) req_, boost::asio::bind_executor( strand_, - std::bind(&ConnectAttempt::onWrite, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec, std::size_t) { self->onWrite(ec); })); } void @@ -316,7 +313,7 @@ ConnectAttempt::onWrite(error_code ec) response_, boost::asio::bind_executor( strand_, - std::bind(&ConnectAttempt::onRead, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec, std::size_t) { self->onRead(ec); })); } void @@ -339,8 +336,7 @@ ConnectAttempt::onRead(error_code ec) stream_.async_shutdown( boost::asio::bind_executor( strand_, - std::bind( - &ConnectAttempt::onShutdown, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); return; } diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 89c7dfe5eb..b7af3b6ace 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -133,7 +133,7 @@ OverlayImpl::Timer::asyncWait() timer.async_wait( boost::asio::bind_executor( overlay_.strand_, - std::bind(&Timer::onTimer, shared_from_this(), std::placeholders::_1))); + [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } void @@ -190,7 +190,7 @@ OverlayImpl::OverlayImpl( , nextId_(1) , slots_(app, *this, app.config()) , stats_( - std::bind(&OverlayImpl::collectMetrics, this), + [this] { collectMetrics(); }, collector, [counts = traffic_.getCounts(), collector]() { std::unordered_map ret; @@ -593,7 +593,7 @@ OverlayImpl::start() void OverlayImpl::stop() { - boost::asio::dispatch(strand_, std::bind(&OverlayImpl::stopChildren, this)); + boost::asio::dispatch(strand_, [this] { stopChildren(); }); { std::unique_lock lock(mutex_); cond_.wait(lock, [this] { return list_.empty(); }); @@ -1490,7 +1490,7 @@ OverlayImpl::deletePeer(Peer::id_t id) { if (!strand_.running_in_this_thread()) { - post(strand_, std::bind(&OverlayImpl::deletePeer, this, id)); + post(strand_, [this, id] { deletePeer(id); }); return; } @@ -1502,7 +1502,7 @@ OverlayImpl::deleteIdlePeers() { if (!strand_.running_in_this_thread()) { - post(strand_, std::bind(&OverlayImpl::deleteIdlePeers, this)); + post(strand_, [this] { deleteIdlePeers(); }); return; } diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 7db32c0d23..83d5a81a89 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -427,7 +427,7 @@ public: addTxMetrics(Args... args) { if (!strand_.running_in_this_thread()) - return post(strand_, std::bind(&OverlayImpl::addTxMetrics, this, args...)); + return post(strand_, [this, args...] { addTxMetrics(args...); }); txMetrics_.addMetrics(args...); } diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 83e4d9c851..4e42d46f46 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -321,9 +321,9 @@ PeerImp::send(std::shared_ptr const& m) self->stream_, boost::asio::buffer(self->sendQueue_.front()->getBuffer(self->compressionEnabled_)), bind_executor( - self->strand_, - std::bind( - &PeerImp::onWriteMessage, self, std::placeholders::_1, std::placeholders::_2))); + self->strand_, [self](error_code const& ec, std::size_t bytesTransferred) { + self->onWriteMessage(ec, bytesTransferred); + })); }); } @@ -650,7 +650,7 @@ PeerImp::gracefulClose() return; setTimer(); stream_.async_shutdown(bind_executor( - strand_, std::bind(&PeerImp::onShutdown, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); } void @@ -666,7 +666,7 @@ PeerImp::setTimer() return; } timer_.async_wait(bind_executor( - strand_, std::bind(&PeerImp::onTimer, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onTimer(ec); })); } // convenience for ignoring the error code @@ -975,11 +975,9 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) readBuffer_.prepare(std::max(Tuning::kReadBufferBytes, hint)), bind_executor( strand_, - std::bind( - &PeerImp::onReadMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { + self->onReadMessage(ec, bytesTransferred); + })); } void @@ -1014,18 +1012,16 @@ PeerImp::onWriteMessage(error_code ec, std::size_t bytesTransferred) boost::asio::buffer(sendQueue_.front()->getBuffer(compressionEnabled_)), bind_executor( strand_, - std::bind( - &PeerImp::onWriteMessage, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2))); + [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { + self->onWriteMessage(ec, bytesTransferred); + })); return; } if (gracefulClose_) { stream_.async_shutdown(bind_executor( - strand_, std::bind(&PeerImp::onShutdown, shared_from_this(), std::placeholders::_1))); + strand_, [self = shared_from_this()](error_code const& ec) { self->onShutdown(ec); })); return; } } diff --git a/src/xrpld/peerfinder/detail/Checker.h b/src/xrpld/peerfinder/detail/Checker.h index 20687448df..208bad390c 100644 --- a/src/xrpld/peerfinder/detail/Checker.h +++ b/src/xrpld/peerfinder/detail/Checker.h @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -183,7 +182,7 @@ Checker::asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& h } op->socket.async_connect( beast::IPAddressConversion::toAsioEndpoint(endpoint), - std::bind(&BasicAsyncOp::operator(), op, std::placeholders::_1)); + [op](error_code const& ec) { (*op)(ec); }); } template diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 7d10f7f516..8ea348f560 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -802,12 +802,10 @@ public: // checker.asyncConnect( ep.address, - std::bind( - &Logic::checkComplete, - this, - slot->remoteEndpoint(), - ep.address, - std::placeholders::_1)); + [this, remoteAddress = slot->remoteEndpoint(), checkedAddress = ep.address]( + boost::system::error_code const& ec) { + checkComplete(remoteAddress, checkedAddress, ec); + }); // Note that we simply discard the first Endpoint // that the neighbor sends when we perform the diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp index 9dbedfe4f1..2727a03013 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include @@ -61,7 +60,7 @@ public: , checker_(io_context_) , logic_(clock, store_, checker_, journal) , config_(config) - , stats_(std::bind(&ManagerImp::collectMetrics, this), collector) + , stats_([this] { collectMetrics(); }, collector) { } diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index fc788dea7d..5b7f1415a2 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -1158,11 +1158,10 @@ Pathfinder::addLink( { std::ranges::sort( candidates, - std::bind( - compareAccountCandidate, - ledger_->seq(), - std::placeholders::_1, - std::placeholders::_2)); + [seq = ledger_->seq()]( + AccountCandidate const& first, AccountCandidate const& second) { + return compareAccountCandidate(seq, first, second); + }); int count = candidates.size(); // allow more paths from source diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 123b9fc7a5..af5677008d 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -1745,7 +1745,9 @@ rpcClient( static_cast(setup.client.secure) != 0, // Use SSL config.quiet(), logs, - std::bind(RPCCallImp::callRPCHandler, &jvOutput, std::placeholders::_1), + [&jvOutput](json::Value const& jvInput) { + RPCCallImp::callRPCHandler(&jvOutput, jvInput); + }, headers); isService.run(); // This blocks until there are no more // outstanding async calls. @@ -1870,24 +1872,16 @@ fromNetwork( ioContext, strIp, iPort, - std::bind( - &RPCCallImp::onRequest, - strMethod, - jvParams, - headers, - strPath, - std::placeholders::_1, - std::placeholders::_2, - j), + [strMethod, jvParams, headers, strPath, j]( + boost::asio::streambuf& sb, std::string const& strHost) { + RPCCallImp::onRequest(strMethod, jvParams, headers, strPath, sb, strHost, j); + }, kRpcReplyMaxBytes, kRpcWebhookTimeout, - std::bind( - &RPCCallImp::onResponse, - callbackFuncP, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - j), + [callbackFuncP, j]( + boost::system::error_code const& ecResult, int iStatus, std::string const& strData) { + return RPCCallImp::onResponse(callbackFuncP, ecResult, iStatus, strData, j); + }, j); } From 53649cc298c5a7c23aa75cb09b66b62431a28c69 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 3 Jul 2026 15:28:15 +0100 Subject: [PATCH 28/31] chore: Enable modernize-use-constraints (#7715) --- .clang-tidy | 1 - include/xrpl/basics/Slice.h | 6 +- include/xrpl/basics/TaggedCache.h | 6 +- include/xrpl/basics/TaggedCache.ipp | 6 +- include/xrpl/basics/ToString.h | 3 +- include/xrpl/basics/base_uint.h | 26 ++- include/xrpl/basics/random.h | 29 ++- include/xrpl/basics/safe_cast.h | 18 +- include/xrpl/basics/scope.h | 25 +-- include/xrpl/basics/tagged_integer.h | 8 +- .../beast/container/aged_container_utility.h | 4 +- .../detail/aged_container_iterator.h | 16 +- .../container/detail/aged_ordered_container.h | 189 +++++++++--------- .../detail/aged_unordered_container.h | 158 ++++++++------- include/xrpl/beast/core/LexicalCast.h | 9 +- include/xrpl/beast/hash/hash_append.h | 93 +++++---- include/xrpl/beast/hash/xxhasher.h | 12 +- include/xrpl/beast/utility/rngfill.h | 7 +- include/xrpl/core/JobQueue.h | 8 +- .../xrpl/ledger/helpers/DirectoryHelpers.h | 14 +- include/xrpl/net/HTTPClientSSLContext.h | 18 +- include/xrpl/nodestore/detail/varint.h | 9 +- include/xrpl/protocol/STArray.h | 28 +-- include/xrpl/protocol/STObject.h | 16 +- include/xrpl/protocol/TER.h | 59 +++--- include/xrpl/server/Manifest.h | 5 +- src/libxrpl/protocol/STParsedJSON.cpp | 6 +- src/libxrpl/protocol/STTx.cpp | 2 +- src/libxrpl/protocol/tokens.cpp | 3 +- src/test/app/NFToken_test.cpp | 2 +- .../beast/aged_associative_container_test.cpp | 119 +++++++---- src/test/csf/Tx.h | 6 +- src/test/csf/submitters.h | 3 +- src/test/jtx/TestHelpers.h | 3 +- src/test/jtx/amount.h | 22 +- src/test/jtx/paths.h | 3 +- src/test/protocol/Quality_test.cpp | 6 +- src/test/protocol/TER_test.cpp | 9 +- src/xrpld/overlay/detail/PeerImp.h | 12 +- src/xrpld/overlay/detail/ProtocolMessage.h | 13 +- src/xrpld/rpc/Status.h | 8 +- 41 files changed, 549 insertions(+), 441 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index c5f638b395..3bad686a11 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -80,7 +80,6 @@ Checks: "-*, -modernize-return-braced-init-list, -modernize-shrink-to-fit, -modernize-use-bool-literals, - -modernize-use-constraints, -modernize-use-default-member-init, -modernize-use-integer-sign-comparison, -modernize-use-noexcept, diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 948d012958..f87ca063b8 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -211,15 +211,17 @@ operator<<(Stream& s, Slice const& v) } template -std::enable_if_t || std::is_same_v, Slice> +Slice makeSlice(std::array const& a) + requires(std::is_same_v || std::is_same_v) { return Slice(a.data(), a.size()); } template -std::enable_if_t || std::is_same_v, Slice> +Slice makeSlice(std::vector const& v) + requires(std::is_same_v || std::is_same_v) { return Slice(v.data(), v.size()); } diff --git a/include/xrpl/basics/TaggedCache.h b/include/xrpl/basics/TaggedCache.h index 71ebfc57a4..16c87cf833 100644 --- a/include/xrpl/basics/TaggedCache.h +++ b/include/xrpl/basics/TaggedCache.h @@ -212,11 +212,13 @@ public: */ template auto - insert(key_type const& key, T const& value) -> std::enable_if_t; + insert(key_type const& key, T const& value) -> ReturnType + requires(!IsKeyCache); template auto - insert(key_type const& key) -> std::enable_if_t; + insert(key_type const& key) -> ReturnType + requires IsKeyCache; // VFALCO NOTE It looks like this returns a copy of the data in // the output parameter 'data'. This could be expensive. diff --git a/include/xrpl/basics/TaggedCache.ipp b/include/xrpl/basics/TaggedCache.ipp index ca50bb64b7..b79561c71a 100644 --- a/include/xrpl/basics/TaggedCache.ipp +++ b/include/xrpl/basics/TaggedCache.ipp @@ -503,7 +503,8 @@ template < template inline auto TaggedCache:: - insert(key_type const& key, T const& value) -> std::enable_if_t + insert(key_type const& key, T const& value) -> ReturnType + requires(!IsKeyCache) { static_assert( std::is_same_v, SharedPointerType> || @@ -533,7 +534,8 @@ template < template inline auto TaggedCache:: - insert(key_type const& key) -> std::enable_if_t + insert(key_type const& key) -> ReturnType + requires IsKeyCache { std::scoped_lock const lock(mutex_); clock_type::time_point const now(clock_.now()); diff --git a/include/xrpl/basics/ToString.h b/include/xrpl/basics/ToString.h index 7764c1e3e3..e9f8f43633 100644 --- a/include/xrpl/basics/ToString.h +++ b/include/xrpl/basics/ToString.h @@ -12,8 +12,9 @@ namespace xrpl { */ template -std::enable_if_t, std::string> +std::string to_string(T t) // NOLINT(readability-identifier-naming) + requires(std::is_arithmetic_v) { return std::to_string(t); } diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index c60fbf35b4..481a7dbd77 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -280,12 +280,11 @@ public: { } - template < - class Container, - class = std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v>> + template explicit BaseUInt(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { // Use AlwaysFalseT so the static_assert condition is dependent // and only triggers when this constructor template is instantiated. @@ -295,13 +294,12 @@ public: "Use base_uint::fromRaw instead."); } - template < - class Container, - class = std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v>> + template static BaseUInt fromRaw(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { BaseUInt result; XRPL_ASSERT( @@ -312,11 +310,11 @@ public: } template - std::enable_if_t< - detail::IsContiguousContainer::value && - std::is_trivially_copyable_v, - BaseUInt&> + BaseUInt& operator=(Container const& c) + requires( + detail::IsContiguousContainer::value && + std::is_trivially_copyable_v) { XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), diff --git a/include/xrpl/basics/random.h b/include/xrpl/basics/random.h index cceaa6f029..c544e7d0c8 100644 --- a/include/xrpl/basics/random.h +++ b/include/xrpl/basics/random.h @@ -91,8 +91,9 @@ defaultPrng() */ /** @{ */ template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine, Integral min, Integral max) + requires(std::is_integral_v && detail::is_engine::value) { XRPL_ASSERT(max > min, "xrpl::randInt : max over min inputs"); @@ -103,36 +104,41 @@ randInt(Engine& engine, Integral min, Integral max) } template -std::enable_if_t, Integral> +Integral randInt(Integral min, Integral max) + requires(std::is_integral_v) { return randInt(defaultPrng(), min, max); } template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine, Integral max) + requires(std::is_integral_v && detail::is_engine::value) { return randInt(engine, Integral(0), max); } template -std::enable_if_t, Integral> +Integral randInt(Integral max) + requires(std::is_integral_v) { return randInt(defaultPrng(), max); } template -std::enable_if_t && detail::is_engine::value, Integral> +Integral randInt(Engine& engine) + requires(std::is_integral_v && detail::is_engine::value) { return randInt(engine, std::numeric_limits::max()); } template -std::enable_if_t, Integral> +Integral randInt() + requires(std::is_integral_v) { return randInt(defaultPrng(), std::numeric_limits::max()); } @@ -141,19 +147,20 @@ randInt() /** Return a random byte */ /** @{ */ template -std::enable_if_t< - (std::is_same_v || std::is_same_v) && - detail::is_engine::value, - Byte> +Byte randByte(Engine& engine) + requires( + (std::is_same_v || std::is_same_v) && + detail::is_engine::value) { return static_cast(randInt( engine, std::numeric_limits::min(), std::numeric_limits::max())); } template -std::enable_if_t<(std::is_same_v || std::is_same_v), Byte> +Byte randByte() + requires(std::is_same_v || std::is_same_v) { return randByte(defaultPrng()); } diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h index 714146e089..483a783f5d 100644 --- a/include/xrpl/basics/safe_cast.h +++ b/include/xrpl/basics/safe_cast.h @@ -15,8 +15,9 @@ concept SafeToCast = (std::is_integral_v && std::is_integral_v) && : sizeof(Dest) >= sizeof(Src)); template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_integral_v && std::is_integral_v) { static_assert( std::is_signed_v || std::is_unsigned_v, "Cannot cast signed to unsigned"); @@ -28,15 +29,17 @@ safeCast(Src s) noexcept } template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_enum_v && std::is_integral_v) { return static_cast(safeCast>(s)); } template -constexpr std::enable_if_t && std::is_enum_v, Dest> +constexpr Dest safeCast(Src s) noexcept + requires(std::is_integral_v && std::is_enum_v) { return safeCast(static_cast>(s)); } @@ -46,8 +49,9 @@ safeCast(Src s) noexcept // underlying types become safe, it can be converted to a safe_cast. template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_integral_v && std::is_integral_v) { static_assert( !SafeToCast, @@ -57,15 +61,17 @@ unsafeCast(Src s) noexcept } template -constexpr std::enable_if_t && std::is_integral_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_enum_v && std::is_integral_v) { return static_cast(unsafeCast>(s)); } template -constexpr std::enable_if_t && std::is_enum_v, Dest> +constexpr Dest unsafeCast(Src s) noexcept + requires(std::is_integral_v && std::is_enum_v) { return unsafeCast(static_cast>(s)); } diff --git a/include/xrpl/basics/scope.h b/include/xrpl/basics/scope.h index cfd21e6e30..e63bb69eb5 100644 --- a/include/xrpl/basics/scope.h +++ b/include/xrpl/basics/scope.h @@ -46,11 +46,9 @@ public: operator=(ScopeExit&&) = delete; template - explicit ScopeExit( - EFP&& f, - std::enable_if_t< - !std::is_same_v, ScopeExit> && - std::is_constructible_v>* = 0) noexcept + explicit ScopeExit(EFP&& f) noexcept + requires( + !std::is_same_v, ScopeExit> && std::is_constructible_v) : exitFunction_{std::forward(f)} { static_assert(std::is_nothrow_constructible_v(f))>); @@ -93,11 +91,9 @@ public: operator=(ScopeFail&&) = delete; template - explicit ScopeFail( - EFP&& f, - std::enable_if_t< - !std::is_same_v, ScopeFail> && - std::is_constructible_v>* = 0) noexcept + explicit ScopeFail(EFP&& f) noexcept + requires( + !std::is_same_v, ScopeFail> && std::is_constructible_v) : exitFunction_{std::forward(f)} { static_assert(std::is_nothrow_constructible_v(f))>); @@ -140,12 +136,11 @@ public: operator=(ScopeSuccess&&) = delete; template - explicit ScopeSuccess( - EFP&& f, - std::enable_if_t< + explicit ScopeSuccess(EFP&& f) noexcept( + std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + requires( !std::is_same_v, ScopeSuccess> && - std::is_constructible_v>* = - 0) noexcept(std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + std::is_constructible_v) : exitFunction_{std::forward(f)} { } diff --git a/include/xrpl/basics/tagged_integer.h b/include/xrpl/basics/tagged_integer.h index 8fbd2a274b..5a088db863 100644 --- a/include/xrpl/basics/tagged_integer.h +++ b/include/xrpl/basics/tagged_integer.h @@ -43,10 +43,10 @@ public: TaggedInteger() = default; - template < - class OtherInt, - class = std::enable_if_t && sizeof(OtherInt) <= sizeof(Int)>> - explicit constexpr TaggedInteger(OtherInt value) noexcept : value_(value) + template + explicit constexpr TaggedInteger(OtherInt value) noexcept + requires(std::is_integral_v && sizeof(OtherInt) <= sizeof(Int)) + : value_(value) { static_assert(sizeof(TaggedInteger) == sizeof(Int), "tagged_integer is adding padding"); } diff --git a/include/xrpl/beast/container/aged_container_utility.h b/include/xrpl/beast/container/aged_container_utility.h index 7cda863fab..f43e59b0f7 100644 --- a/include/xrpl/beast/container/aged_container_utility.h +++ b/include/xrpl/beast/container/aged_container_utility.h @@ -4,14 +4,14 @@ #include #include -#include namespace beast { /** Expire aged container items past the specified age. */ template -std::enable_if_t::value, std::size_t> +std::size_t expire(AgedContainer& c, std::chrono::duration const& age) + requires(IsAgedContainer::value) { std::size_t n(0); auto const expired(c.clock().now() - age); diff --git a/include/xrpl/beast/container/detail/aged_container_iterator.h b/include/xrpl/beast/container/detail/aged_container_iterator.h index 02fb3927dd..d6c061bb86 100644 --- a/include/xrpl/beast/container/detail/aged_container_iterator.h +++ b/include/xrpl/beast/container/detail/aged_container_iterator.h @@ -30,20 +30,19 @@ public: // Disable constructing a const_iterator from a non-const_iterator. // Converting between reverse and non-reverse iterators should be explicit. - template < - bool OtherIsConst, - class OtherIterator, - class = std::enable_if_t< - (!OtherIsConst || IsConst) && - !static_cast(std::is_same_v)>> + template explicit AgedContainerIterator(AgedContainerIterator const& other) + requires( + (!OtherIsConst || IsConst) && + !static_cast(std::is_same_v)) : iter_(other.iter_) { } // Disable constructing a const_iterator from a non-const_iterator. - template > + template AgedContainerIterator(AgedContainerIterator const& other) + requires(!OtherIsConst || IsConst) : iter_(other.iter_) { } @@ -52,7 +51,8 @@ public: template auto operator=(AgedContainerIterator const& other) - -> std::enable_if_t + -> AgedContainerIterator& + requires(!OtherIsConst || IsConst) { iter_ = other.iter_; return *this; diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 0533a51f00..f739f05d2c 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -111,10 +111,9 @@ private: { } - template < - class... Args, - class = std::enable_if_t>> + template Element(time_point const& when, Args&&... args) + requires(std::is_constructible_v) : value(std::forward(args)...), when(when) { } @@ -608,35 +607,25 @@ public: // //-------------------------------------------------------------------------- - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - at(K const& k); + at(K const& k) + requires(MaybeMap && !MaybeMulti); - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional::type const& - at(K const& k) const; + at(K const& k) const + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key const& key); + operator[](Key const& key) + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key&& key); + operator[](Key&& key) + requires(MaybeMap && !MaybeMulti); //-------------------------------------------------------------------------- // @@ -770,35 +759,40 @@ public: // map, set template auto - insert(value_type const& value) -> std::enable_if_t>; + insert(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insert(value_type const& value) -> std::enable_if_t; + insert(value_type const& value) -> iterator + requires MaybeMulti; // set template auto - insert(value_type&& value) - -> std::enable_if_t>; + insert(value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap); // multiset template auto - insert(value_type&& value) -> std::enable_if_t; + insert(value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap); //--- // map, set template auto - insert(const_iterator hint, value_type const& value) -> std::enable_if_t; + insert(const_iterator hint, value_type const& value) -> iterator + requires(!MaybeMulti); // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return insert(value); @@ -807,12 +801,14 @@ public: // map, set template auto - insert(const_iterator hint, value_type&& value) -> std::enable_if_t; + insert(const_iterator hint, value_type&& value) -> iterator + requires(!MaybeMulti); // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return insert(std::move(value)); @@ -820,20 +816,18 @@ public: // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplace(std::forward

(value)); } // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(const_iterator hint, P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplaceHint(hint, std::forward

(value)); } @@ -855,46 +849,45 @@ public: // map, set template auto - emplace(Args&&... args) -> std::enable_if_t>; + emplace(Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template auto - emplace(Args&&... args) -> std::enable_if_t; + emplace(Args&&... args) -> iterator + requires MaybeMulti; // map, set template auto - emplaceHint(const_iterator hint, Args&&... args) - -> std::enable_if_t>; + emplaceHint(const_iterator hint, Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template - std::enable_if_t + iterator emplaceHint(const_iterator /*hint*/, Args&&... args) + requires MaybeMulti { // VFALCO TODO Figure out how to utilize 'hint' return emplace(std::forward(args)...); } - // enable_if prevents erase (reverse_iterator pos) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents erase (reverse_iterator pos) from compiling + template beast::detail::AgedContainerIterator - erase(beast::detail::AgedContainerIterator pos); + erase(beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value); - // enable_if prevents erase (reverse_iterator first, reverse_iterator last) + // The constraint prevents erase (reverse_iterator first, reverse_iterator last) // from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + template beast::detail::AgedContainerIterator erase( beast::detail::AgedContainerIterator first, - beast::detail::AgedContainerIterator last); + beast::detail::AgedContainerIterator last) + requires(!IsBoostReverseIterator::value); template auto @@ -905,13 +898,11 @@ public: //-------------------------------------------------------------------------- - // enable_if prevents touch (reverse_iterator pos) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents touch (reverse_iterator pos) from compiling + template void touch(beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value) { touch(pos, clock().now()); } @@ -1142,25 +1133,25 @@ public: } private: - // enable_if prevents erase (reverse_iterator pos, now) from compiling - template < - bool IsConst, - class Iterator, - class = std::enable_if_t::value>> + // The constraint prevents erase (reverse_iterator pos, now) from compiling + template void touch( beast::detail::AgedContainerIterator pos, - clock_type::time_point const& now); + clock_type::time_point const& now) + requires(!IsBoostReverseIterator::value); template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t - swapData(AgedOrderedContainer& other) noexcept; + void + swapData(AgedOrderedContainer& other) noexcept + requires MaybePropagate; template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t - swapData(AgedOrderedContainer& other) noexcept; + void + swapData(AgedOrderedContainer& other) noexcept + requires(!MaybePropagate); private: ConfigT config_; @@ -1369,9 +1360,10 @@ AgedOrderedContainer::operato //------------------------------------------------------------------------------ template -template +template std::conditional_t& AgedOrderedContainer::at(K const& k) + requires(MaybeMap && !MaybeMulti) { auto const iter(cont_.find(k, std::cref(config_.keyCompare()))); if (iter == cont_.end()) @@ -1380,9 +1372,10 @@ AgedOrderedContainer::at(K co } template -template +template std::conditional::type const& AgedOrderedContainer::at(K const& k) const + requires(MaybeMap && !MaybeMulti) { auto const iter(cont_.find(k, std::cref(config_.keyCompare()))); if (iter == cont_.end()) @@ -1391,9 +1384,10 @@ AgedOrderedContainer::at(K co } template -template +template std::conditional_t& AgedOrderedContainer::operator[](Key const& key) + requires(MaybeMap && !MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(key, std::cref(config_.keyCompare()), d)); @@ -1409,9 +1403,10 @@ AgedOrderedContainer::operato } template -template +template std::conditional_t& AgedOrderedContainer::operator[](Key&& key) + requires(MaybeMap && !MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(key, std::cref(config_.keyCompare()), d)); @@ -1445,7 +1440,8 @@ template auto AgedOrderedContainer::insert( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(extract(value), std::cref(config_.keyCompare()), d)); @@ -1464,7 +1460,8 @@ template auto AgedOrderedContainer::insert( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { auto const before(cont_.upper_bound(extract(value), std::cref(config_.keyCompare()))); Element* const p(newElement(value)); @@ -1478,7 +1475,8 @@ template auto AgedOrderedContainer::insert(value_type&& value) - -> std::enable_if_t> + -> std::pair + requires(!MaybeMulti && !MaybeMap) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check(extract(value), std::cref(config_.keyCompare()), d)); @@ -1497,7 +1495,8 @@ template auto AgedOrderedContainer::insert(value_type&& value) - -> std::enable_if_t + -> iterator + requires(MaybeMulti && !MaybeMap) { auto const before(cont_.upper_bound(extract(value), std::cref(config_.keyCompare()))); Element* const p(newElement(std::move(value))); @@ -1514,7 +1513,8 @@ template auto AgedOrderedContainer::insert( const_iterator hint, - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result( @@ -1535,7 +1535,8 @@ template auto AgedOrderedContainer::insert( const_iterator hint, - value_type&& value) -> std::enable_if_t + value_type&& value) -> iterator + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result( @@ -1555,7 +1556,8 @@ template auto AgedOrderedContainer::emplace(Args&&... args) - -> std::enable_if_t> + -> std::pair + requires(!MaybeMulti) { // VFALCO NOTE Its unfortunate that we need to // construct element here @@ -1577,7 +1579,8 @@ template auto AgedOrderedContainer::emplace(Args&&... args) - -> std::enable_if_t + -> iterator + requires MaybeMulti { Element* const p(newElement(std::forward(args)...)); auto const before(cont_.upper_bound(extract(p->value), std::cref(config_.keyCompare()))); @@ -1592,7 +1595,8 @@ template auto AgedOrderedContainer::emplaceHint( const_iterator hint, - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { // VFALCO NOTE Its unfortunate that we need to // construct element here @@ -1611,21 +1615,23 @@ AgedOrderedContainer::emplace } template -template +template beast::detail::AgedContainerIterator AgedOrderedContainer::erase( beast::detail::AgedContainerIterator pos) + requires(!IsBoostReverseIterator::value) { unlinkAndDeleteElement(&*((pos++).iterator())); return beast::detail::AgedContainerIterator(pos.iterator()); } template -template +template beast::detail::AgedContainerIterator AgedOrderedContainer::erase( beast::detail::AgedContainerIterator first, beast::detail::AgedContainerIterator last) + requires(!IsBoostReverseIterator::value) { for (; first != last;) unlinkAndDeleteElement(&*((first++).iterator())); @@ -1728,11 +1734,12 @@ AgedOrderedContainer::operato //------------------------------------------------------------------------------ template -template +template void AgedOrderedContainer::touch( beast::detail::AgedContainerIterator pos, clock_type::time_point const& now) + requires(!IsBoostReverseIterator::value) { auto& e(*pos.iterator()); e.when = now; @@ -1742,9 +1749,10 @@ AgedOrderedContainer::touch( template template -std::enable_if_t +void AgedOrderedContainer::swapData( AgedOrderedContainer& other) noexcept + requires MaybePropagate { std::swap(config_.keyCompare(), other.config_.keyCompare()); std::swap(config_.alloc(), other.config_.alloc()); @@ -1753,9 +1761,10 @@ AgedOrderedContainer::swapDat template template -std::enable_if_t +void AgedOrderedContainer::swapData( AgedOrderedContainer& other) noexcept + requires(!MaybePropagate) { std::swap(config_.keyCompare(), other.config_.keyCompare()); std::swap(config_.clock, other.config_.clock); diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index 782f36cd52..4c0882b04a 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -117,10 +117,9 @@ private: { } - template < - class... Args, - class = std::enable_if_t>> + template Element(time_point const& when, Args&&... args) + requires(std::is_constructible_v) : value(std::forward(args)...), when(when) { } @@ -841,35 +840,25 @@ public: // //-------------------------------------------------------------------------- - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - at(K const& k); + at(K const& k) + requires(MaybeMap && !MaybeMulti); - template < - class K, - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional::type const& - at(K const& k) const; + at(K const& k) const + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key const& key); + operator[](Key const& key) + requires(MaybeMap && !MaybeMulti); - template < - bool MaybeMulti = IsMulti, - bool MaybeMap = IsMap, - class = std::enable_if_t> + template std::conditional_t& - operator[](Key&& key); + operator[](Key&& key) + requires(MaybeMap && !MaybeMulti); //-------------------------------------------------------------------------- // @@ -967,28 +956,32 @@ public: // map, set template auto - insert(value_type const& value) -> std::enable_if_t>; + insert(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insert(value_type const& value) -> std::enable_if_t; + insert(value_type const& value) -> iterator + requires MaybeMulti; // map, set template auto - insert(value_type&& value) - -> std::enable_if_t>; + insert(value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap); // multimap, multiset template auto - insert(value_type&& value) -> std::enable_if_t; + insert(value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap); // map, set template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires(!MaybeMulti) { // Hint is ignored but we provide the interface so // callers may use ordered and unordered interchangeably. @@ -997,8 +990,9 @@ public: // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type const& value) + requires MaybeMulti { // VFALCO TODO The hint could be used to let // the client order equal ranges @@ -1007,8 +1001,9 @@ public: // map, set template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires(!MaybeMulti) { // Hint is ignored but we provide the interface so // callers may use ordered and unordered interchangeably. @@ -1017,8 +1012,9 @@ public: // multimap, multiset template - std::enable_if_t + iterator insert(const_iterator /*hint*/, value_type&& value) + requires MaybeMulti { // VFALCO TODO The hint could be used to let // the client order equal ranges @@ -1027,20 +1023,18 @@ public: // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplace(std::forward

(value)); } // map, multimap template - std::enable_if_t< - MaybeMap && std::is_constructible_v, - std::conditional_t>> + std::conditional_t> insert(const_iterator hint, P&& value) + requires(MaybeMap && std::is_constructible_v) { return emplaceHint(hint, std::forward

(value)); } @@ -1061,23 +1055,26 @@ public: // set, map template auto - emplace(Args&&... args) -> std::enable_if_t>; + emplace(Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template auto - emplace(Args&&... args) -> std::enable_if_t; + emplace(Args&&... args) -> iterator + requires MaybeMulti; // set, map template auto - emplaceHint(const_iterator /*hint*/, Args&&... args) - -> std::enable_if_t>; + emplaceHint(const_iterator /*hint*/, Args&&... args) -> std::pair + requires(!MaybeMulti); // multiset, multimap template - std::enable_if_t + iterator emplaceHint(const_iterator /*hint*/, Args&&... args) + requires MaybeMulti { // VFALCO TODO The hint could be used for multi, to let // the client order equal ranges @@ -1308,7 +1305,7 @@ public: class OtherHash, class OtherAllocator, bool MaybeMulti = IsMulti> - std::enable_if_t + bool operator==(AgedUnorderedContainer< false, OtherIsMap, @@ -1317,7 +1314,8 @@ public: OtherDuration, OtherHash, KeyEqual, - OtherAllocator> const& other) const; + OtherAllocator> const& other) const + requires(!MaybeMulti); template < bool OtherIsMap, @@ -1327,7 +1325,7 @@ public: class OtherHash, class OtherAllocator, bool MaybeMulti = IsMulti> - std::enable_if_t + bool operator==(AgedUnorderedContainer< true, OtherIsMap, @@ -1336,7 +1334,8 @@ public: OtherDuration, OtherHash, KeyEqual, - OtherAllocator> const& other) const; + OtherAllocator> const& other) const + requires MaybeMulti; template < bool OtherIsMulti, @@ -1381,13 +1380,14 @@ private: // map, set template auto - insertUnchecked(value_type const& value) - -> std::enable_if_t>; + insertUnchecked(value_type const& value) -> std::pair + requires(!MaybeMulti); // multimap, multiset template auto - insertUnchecked(value_type const& value) -> std::enable_if_t; + insertUnchecked(value_type const& value) -> iterator + requires MaybeMulti; template void @@ -1428,8 +1428,9 @@ private: template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t + void swapData(AgedUnorderedContainer& other) noexcept + requires MaybePropagate { std::swap(config_.hashFunction(), other.config_.hashFunction()); std::swap(config_.keyEq(), other.config_.keyEq()); @@ -1439,8 +1440,9 @@ private: template < bool MaybePropagate = std::allocator_traits::propagate_on_container_swap::value> - std::enable_if_t + void swapData(AgedUnorderedContainer& other) noexcept + requires(!MaybePropagate) { std::swap(config_.hashFunction(), other.config_.hashFunction()); std::swap(config_.keyEq(), other.config_.keyEq()); @@ -2094,9 +2096,10 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::at(K const& k) + requires(MaybeMap && !MaybeMulti) { auto const iter( cont_.find(k, std::cref(config_.hashFunction()), std::cref(config_.keyValueEqual()))); @@ -2114,10 +2117,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional::type const& AgedUnorderedContainer::at( K const& k) const + requires(MaybeMap && !MaybeMulti) { auto const iter( cont_.find(k, std::cref(config_.hashFunction()), std::cref(config_.keyValueEqual()))); @@ -2135,10 +2139,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::operator[]( Key const& key) + requires(MaybeMap && !MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2164,10 +2169,11 @@ template < class Hash, class KeyEqual, class Allocator> -template +template std::conditional_t& AgedUnorderedContainer::operator[]( Key&& key) + requires(MaybeMap && !MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2220,7 +2226,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2249,7 +2256,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { maybeRehash(1); Element* const p(newElement(value)); @@ -2271,7 +2279,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type&& value) -> std::enable_if_t> + value_type&& value) -> std::pair + requires(!MaybeMulti && !MaybeMap) { maybeRehash(1); typename cont_type::insert_commit_data d; @@ -2300,7 +2309,8 @@ template < template auto AgedUnorderedContainer::insert( - value_type&& value) -> std::enable_if_t + value_type&& value) -> iterator + requires(MaybeMulti && !MaybeMap) { maybeRehash(1); Element* const p(newElement(std::move(value))); @@ -2323,7 +2333,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2352,7 +2363,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> typename std::enable_if>::type + Args&&... args) -> std::pair + requires(!maybe_multi) { maybe_rehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2388,7 +2400,8 @@ template < template auto AgedUnorderedContainer::emplace( - Args&&... args) -> std::enable_if_t + Args&&... args) -> iterator + requires MaybeMulti { maybeRehash(1); Element* const p(newElement(std::forward(args)...)); @@ -2411,7 +2424,8 @@ template auto AgedUnorderedContainer::emplaceHint( const_iterator /*hint*/, - Args&&... args) -> std::enable_if_t> + Args&&... args) -> std::pair + requires(!MaybeMulti) { maybeRehash(1); // VFALCO NOTE Its unfortunate that we need to @@ -2562,7 +2576,7 @@ template < class OtherHash, class OtherAllocator, bool MaybeMulti> -std::enable_if_t +bool AgedUnorderedContainer::operator==( AgedUnorderedContainer< false, @@ -2573,6 +2587,7 @@ AgedUnorderedContainer OtherHash, KeyEqual, OtherAllocator> const& other) const + requires(!MaybeMulti) { if (size() != other.size()) return false; @@ -2602,7 +2617,7 @@ template < class OtherHash, class OtherAllocator, bool MaybeMulti> -std::enable_if_t +bool AgedUnorderedContainer::operator==( AgedUnorderedContainer< true, @@ -2613,6 +2628,7 @@ AgedUnorderedContainer OtherHash, KeyEqual, OtherAllocator> const& other) const + requires MaybeMulti { if (size() != other.size()) return false; @@ -2649,7 +2665,8 @@ template < template auto AgedUnorderedContainer::insertUnchecked( - value_type const& value) -> std::enable_if_t> + value_type const& value) -> std::pair + requires(!MaybeMulti) { typename cont_type::insert_commit_data d; auto const result(cont_.insert_check( @@ -2677,7 +2694,8 @@ template < template auto AgedUnorderedContainer::insertUnchecked( - value_type const& value) -> std::enable_if_t + value_type const& value) -> iterator + requires MaybeMulti { Element* const p(newElement(value)); chronological.list_.push_back(*p); diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 8faf90f53d..1162d83078 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -29,16 +29,18 @@ struct LexicalCast explicit LexicalCast() = default; template - std::enable_if_t, bool> + bool operator()(std::string& out, Arithmetic in) + requires(std::is_arithmetic_v) { out = std::to_string(in); return true; } template - std::enable_if_t, bool> + bool operator()(std::string& out, Enumeration in) + requires(std::is_enum_v) { out = std::to_string(static_cast>(in)); return true; @@ -56,8 +58,9 @@ struct LexicalCast "beast::LexicalCast can only be used with integral types"); template - std::enable_if_t && !std::is_same_v, bool> + bool operator()(Integral& out, std::string_view in) const + requires(std::is_integral_v && !std::is_same_v) { auto first = in.data(); auto last = in.data() + in.size(); diff --git a/include/xrpl/beast/hash/hash_append.h b/include/xrpl/beast/hash/hash_append.h index 5f77f41e5d..da499dc612 100644 --- a/include/xrpl/beast/hash/hash_append.h +++ b/include/xrpl/beast/hash/hash_append.h @@ -200,26 +200,29 @@ struct IsContiguouslyHashable // scalars template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, T const& t) noexcept + requires(IsContiguouslyHashable::value) { // NOLINTNEXTLINE(bugprone-sizeof-expression) h(static_cast(std::addressof(t)), sizeof(t)); } template -inline std::enable_if_t< - !IsContiguouslyHashable::value && - (std::is_integral_v || std::is_pointer_v || std::is_enum_v)> +inline void hash_append(Hasher& h, T t) noexcept + requires( + !IsContiguouslyHashable::value && + (std::is_integral_v || std::is_pointer_v || std::is_enum_v)) { detail::reverseBytes(t); h(std::addressof(t), sizeof(t)); } template -inline std::enable_if_t> +inline void hash_append(Hasher& h, T t) noexcept + requires(std::is_floating_point_v) { if (t == 0) t = 0; @@ -239,36 +242,44 @@ hash_append(Hasher& h, std::nullptr_t) noexcept // Forward declarations for ADL purposes template -std::enable_if_t::value> -hash_append(Hasher& h, T (&a)[N]) noexcept; +void +hash_append(Hasher& h, T (&a)[N]) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::basic_string const& s) noexcept; +void +hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::basic_string const& s) noexcept; +void +hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(IsContiguouslyHashable::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::pair const& p) noexcept; +void +hash_append(Hasher& h, std::pair const& p) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::vector const& v) noexcept; +void +hash_append(Hasher& h, std::vector const& v) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, std::vector const& v) noexcept; +void +hash_append(Hasher& h, std::vector const& v) noexcept + requires(IsContiguouslyHashable::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::array const& a) noexcept; +void +hash_append(Hasher& h, std::array const& a) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template -std::enable_if_t, Hasher>::value> -hash_append(Hasher& h, std::tuple const& t) noexcept; +void +hash_append(Hasher& h, std::tuple const& t) noexcept + requires(!IsContiguouslyHashable, Hasher>::value); template void @@ -279,11 +290,13 @@ void hash_append(Hasher& h, std::unordered_set const& s); template -std::enable_if_t::value> -hash_append(Hasher& h, boost::container::flat_set const& v) noexcept; +void +hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(!IsContiguouslyHashable::value); template -std::enable_if_t::value> -hash_append(Hasher& h, boost::container::flat_set const& v) noexcept; +void +hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(IsContiguouslyHashable::value); template void hash_append(Hasher& h, T0 const& t0, T1 const& t1, T const&... t) noexcept; @@ -291,8 +304,9 @@ hash_append(Hasher& h, T0 const& t0, T1 const& t1, T const&... t) noexcept; // c-array template -std::enable_if_t::value> +void hash_append(Hasher& h, T (&a)[N]) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : a) hash_append(h, t); @@ -301,8 +315,9 @@ hash_append(Hasher& h, T (&a)[N]) noexcept // basic_string template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(!IsContiguouslyHashable::value) { for (auto c : s) hash_append(h, c); @@ -310,8 +325,9 @@ hash_append(Hasher& h, std::basic_string const& s) noexcep } template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::basic_string const& s) noexcept + requires(IsContiguouslyHashable::value) { h(s.data(), s.size() * sizeof(CharT)); hash_append(h, s.size()); @@ -320,8 +336,9 @@ hash_append(Hasher& h, std::basic_string const& s) noexcep // pair template -inline std::enable_if_t, Hasher>::value> +inline void hash_append(Hasher& h, std::pair const& p) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { hash_append(h, p.first, p.second); } @@ -329,8 +346,9 @@ hash_append(Hasher& h, std::pair const& p) noexcept // vector template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::vector const& v) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : v) hash_append(h, t); @@ -338,8 +356,9 @@ hash_append(Hasher& h, std::vector const& v) noexcept } template -inline std::enable_if_t::value> +inline void hash_append(Hasher& h, std::vector const& v) noexcept + requires(IsContiguouslyHashable::value) { h(v.data(), v.size() * sizeof(T)); hash_append(h, v.size()); @@ -348,23 +367,26 @@ hash_append(Hasher& h, std::vector const& v) noexcept // array template -std::enable_if_t, Hasher>::value> +void hash_append(Hasher& h, std::array const& a) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { for (auto const& t : a) hash_append(h, t); } template -std::enable_if_t::value> +void hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(!IsContiguouslyHashable::value) { for (auto const& t : v) hash_append(h, t); } template -std::enable_if_t::value> +void hash_append(Hasher& h, boost::container::flat_set const& v) noexcept + requires(IsContiguouslyHashable::value) { h(&(v.begin()), v.size() * sizeof(Key)); } @@ -395,8 +417,9 @@ tuple_hash(Hasher& h, std::tuple const& t, std::index_sequence) noex } // namespace detail template -inline std::enable_if_t, Hasher>::value> +inline void hash_append(Hasher& h, std::tuple const& t) noexcept + requires(!IsContiguouslyHashable, Hasher>::value) { detail::tuple_hash(h, t, std::index_sequence_for{}); } diff --git a/include/xrpl/beast/hash/xxhasher.h b/include/xrpl/beast/hash/xxhasher.h index 978bbc6917..73dbb8e8ab 100644 --- a/include/xrpl/beast/hash/xxhasher.h +++ b/include/xrpl/beast/hash/xxhasher.h @@ -124,14 +124,18 @@ public: } } - template >* = nullptr> - explicit Xxhasher(Seed seed) : seed_(seed) + template + explicit Xxhasher(Seed seed) + requires(std::is_unsigned_v) + : seed_(seed) { resetBuffers(); } - template >* = nullptr> - Xxhasher(Seed seed, Seed) : seed_(seed) + template + Xxhasher(Seed seed, Seed) + requires(std::is_unsigned_v) + : seed_(seed) { resetBuffers(); } diff --git a/include/xrpl/beast/utility/rngfill.h b/include/xrpl/beast/utility/rngfill.h index ee7a5e2434..5bd9d8bc5c 100644 --- a/include/xrpl/beast/utility/rngfill.h +++ b/include/xrpl/beast/utility/rngfill.h @@ -3,7 +3,6 @@ #include #include #include -#include namespace beast { @@ -33,12 +32,10 @@ rngfill(void* const buffer, std::size_t const bytes, Generator& g) } } -template < - class Generator, - std::size_t N, - class = std::enable_if_t> +template void rngfill(std::array& a, Generator& g) + requires(N % sizeof(typename Generator::result_type) == 0) { using result_type = Generator::result_type; auto i = N / sizeof(result_type); diff --git a/include/xrpl/core/JobQueue.h b/include/xrpl/core/JobQueue.h index 14170a39be..e4b64546f3 100644 --- a/include/xrpl/core/JobQueue.h +++ b/include/xrpl/core/JobQueue.h @@ -154,16 +154,14 @@ public: @param type The type of job. @param name Name of the job. - @param jobHandler Lambda with signature void (Job&). Called when the - job is executed. + @param jobHandler Callable with signature void(). Called when the job is executed. @return true if jobHandler added to queue. */ - template < - typename JobHandler, - typename = std::enable_if_t()()), void>>> + template bool addJob(JobType type, std::string const& name, JobHandler&& jobHandler) + requires(std::is_void_v>) { if (auto optionalCountedJob = jobCounter_.wrap(std::forward(jobHandler))) { diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h b/include/xrpl/ledger/helpers/DirectoryHelpers.h index 76a5f3bdad..a95b9bc95a 100644 --- a/include/xrpl/ledger/helpers/DirectoryHelpers.h +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h @@ -19,11 +19,7 @@ namespace xrpl { namespace detail { -template < - class V, - class N, - class = std::enable_if_t< - std::is_same_v, SLE> && std::is_base_of_v>> +template bool internalDirNext( V& view, @@ -31,6 +27,7 @@ internalDirNext( std::shared_ptr& page, unsigned int& index, uint256& entry) + requires(std::is_same_v, SLE> && std::is_base_of_v) { auto const& svIndexes = page->getFieldV256(sfIndexes); XRPL_ASSERT(index <= svIndexes.size(), "xrpl::detail::internalDirNext : index inside range"); @@ -68,11 +65,7 @@ internalDirNext( return true; } -template < - class V, - class N, - class = std::enable_if_t< - std::is_same_v, SLE> && std::is_base_of_v>> +template bool internalDirFirst( V& view, @@ -80,6 +73,7 @@ internalDirFirst( std::shared_ptr& page, unsigned int& index, uint256& entry) + requires(std::is_same_v, SLE> && std::is_base_of_v) { if constexpr (std::is_const_v) { diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h index 2b26d7e404..51b50a084c 100644 --- a/include/xrpl/net/HTTPClientSSLContext.h +++ b/include/xrpl/net/HTTPClientSSLContext.h @@ -83,13 +83,12 @@ public: * * @return error_code indicating failures, if any */ - template < - class T, - class = std::enable_if_t< - std::is_same_v> || - std::is_same_v>>> + template boost::system::error_code preConnectVerify(T& strm, std::string const& host) + requires( + std::is_same_v> || + std::is_same_v>) { boost::system::error_code ec; if (!SSL_set_tlsext_host_name(strm.native_handle(), host.c_str())) @@ -103,11 +102,7 @@ public: return ec; } - template < - class T, - class = std::enable_if_t< - std::is_same_v> || - std::is_same_v>>> + template /** * @brief invoked after connect/async_connect but before sending data * on an ssl stream - to setup name verification. @@ -117,6 +112,9 @@ public: */ boost::system::error_code postConnectVerify(T& strm, std::string const& host) + requires( + std::is_same_v> || + std::is_same_v>) { boost::system::error_code ec; diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index 6102c0cf2d..5a65545d3a 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -68,9 +68,10 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t) return used; } -template >* = nullptr> +template std::size_t sizeVarint(T v) + requires(std::is_unsigned_v) { std::size_t n = 0; do @@ -100,9 +101,10 @@ writeVarint(void* p0, std::size_t v) // input stream -template >* = nullptr> +template void read(nudb::detail::istream& is, std::size_t& u) + requires(std::is_same_v) { auto p0 = is(1); auto p1 = p0; @@ -113,9 +115,10 @@ read(nudb::detail::istream& is, std::size_t& u) // output stream -template >* = nullptr> +template void write(nudb::detail::ostream& os, std::size_t t) + requires(std::is_same_v) { writeVarint(os.data(sizeVarint(t)), t); } diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index ed39e65026..573bb6dad8 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -32,17 +32,13 @@ public: STArray() = default; STArray(STArray const&) = default; - template < - class Iter, - class = std::enable_if_t< - std::is_convertible_v::reference, STObject>>> - explicit STArray(Iter first, Iter last); + template + explicit STArray(Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>); - template < - class Iter, - class = std::enable_if_t< - std::is_convertible_v::reference, STObject>>> - STArray(SField const& f, Iter first, Iter last); + template + STArray(SField const& f, Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>); STArray& operator=(STArray const&) = default; @@ -170,13 +166,17 @@ private: friend class detail::STVar; }; -template -STArray::STArray(Iter first, Iter last) : v_(first, last) +template +STArray::STArray(Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>) + : v_(first, last) { } -template -STArray::STArray(SField const& f, Iter first, Iter last) : STBase(f), v_(first, last) +template +STArray::STArray(SField const& f, Iter first, Iter last) + requires(std::is_convertible_v::reference, STObject>) + : STBase(f), v_(first, last) { } diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index a60e8f7fe8..c96086b1d0 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -556,8 +556,9 @@ public: operator=(ValueProxy const&) = delete; template - std::enable_if_t, ValueProxy&> - operator=(U&& u); + ValueProxy& + operator=(U&& u) + requires(std::is_assignable_v); // Convenience operators for value types supporting // arithmetic operations @@ -691,8 +692,9 @@ public: operator=(optional_type const& v); template - std::enable_if_t, OptionalProxy&> - operator=(U&& u); + OptionalProxy& + operator=(U&& u) + requires(std::is_assignable_v); private: friend class STObject; @@ -798,8 +800,9 @@ STObject::Proxy::assign(U&& u) template template -std::enable_if_t, STObject::ValueProxy&> +STObject::ValueProxy& STObject::ValueProxy::operator=(U&& u) + requires(std::is_assignable_v) { this->assign(std::forward(u)); return *this; @@ -902,8 +905,9 @@ STObject::OptionalProxy::operator=(optional_type const& v) -> OptionalProxy& template template -std::enable_if_t, STObject::OptionalProxy&> +STObject::OptionalProxy& STObject::OptionalProxy::operator=(U&& u) + requires(std::is_assignable_v) { this->assign(std::forward(u)); return *this; diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b29cb11e15..54b081f358 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -411,7 +411,7 @@ TERtoInt(TECcodes v) //------------------------------------------------------------------------------ // Template class that is specific to selected ranges of error codes. The -// Trait tells std::enable_if which ranges are allowed. +// Trait tells the requires-clause which ranges are allowed. template