From 2c94e213c209a82cbb6412d943641766ad5a3ffe Mon Sep 17 00:00:00 2001 From: Shawn Xie Date: Mon, 9 Mar 2026 15:27:25 -0400 Subject: [PATCH] Resolve conflicts --- include/xrpl/protocol/LedgerFormats.h | 2 +- include/xrpl/protocol/TxFlags.h | 15 +- include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/transactions.macro | 10 +- include/xrpl/tx/invariants/InvariantCheck.h | 3 +- include/xrpl/tx/invariants/MPTInvariant.h | 43 ++++ src/libxrpl/tx/invariants/MPTInvariant.cpp | 204 ++++++++++++++++++ .../token/ConfidentialMPTClawback.cpp | 2 +- .../token/ConfidentialMPTConvert.cpp | 2 +- .../token/ConfidentialMPTConvertBack.cpp | 2 +- .../token/ConfidentialMPTMergeInbox.cpp | 2 +- .../transactors/token/ConfidentialMPTSend.cpp | 2 +- 12 files changed, 270 insertions(+), 18 deletions(-) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index eada8509e7..4e2806b6f0 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -185,7 +185,7 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsmfMPTCanMutateCanClawback, 0x00000040) \ LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \ LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000) \ - LSF_FLAG(lsmfMPTCannotMutatePrivacy, 0x00040000)) \ \ + LSF_FLAG(lsmfMPTCannotMutatePrivacy, 0x00040000)) \ \ LEDGER_OBJECT(MPToken, \ LSF_FLAG2(lsfMPTLocked, 0x00000001) \ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 7d31b8c4aa..42cade072c 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -138,7 +138,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(tfMPTCanPrivacy, lsfMPTCanPrivacy), \ MASK_ADJ(0)) \ \ TRANSACTION(MPTokenAuthorize, \ @@ -347,10 +348,11 @@ inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTrans inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback; inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; +inline constexpr FlagValue tmfMPTCannotMutatePrivacy = lsmfMPTCannotMutatePrivacy; inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = ~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | - tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee); + tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee | tmfMPTCannotMutatePrivacy); // MPTokenIssuanceSet MutableFlags: // Set or Clear flags. @@ -369,10 +371,11 @@ inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400; inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800; inline constexpr FlagValue tmfMPTSetPrivacy = 0x00001000; inline constexpr FlagValue tmfMPTClearPrivacy = 0x00002000; -inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~( - tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | - tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade | - tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback | tmfMPTSetPrivacy | tmfMPTClearPrivacy); +inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = + ~(tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | + tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade | + tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | + tmfMPTClearCanClawback | tmfMPTSetPrivacy | tmfMPTClearPrivacy); // 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 4a8cb8ffe6..667608b8ca 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. + XRPL_FEATURE(ConfidentialTransfer, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 1bd9ab1ce2..94aed8a797 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1062,7 +1062,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, /** This transaction type converts into confidential MPT balance. */ #if TRANSACTION_INCLUDE -#include +#include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, Delegation::delegable, @@ -1081,7 +1081,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, /** This transaction type merges MPT inbox. */ #if TRANSACTION_INCLUDE -#include +#include #endif TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, Delegation::delegable, @@ -1093,7 +1093,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, /** This transaction type converts back into public MPT balance. */ #if TRANSACTION_INCLUDE -#include +#include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, Delegation::delegable, @@ -1111,7 +1111,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, })) #if TRANSACTION_INCLUDE -#include +#include #endif TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, Delegation::delegable, @@ -1131,7 +1131,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, })) #if TRANSACTION_INCLUDE -#include +#include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, Delegation::delegable, diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index 5ded5980da..f1b3e872b7 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -366,7 +366,8 @@ using InvariantChecks = std::tuple< ValidPseudoAccounts, ValidLoanBroker, ValidLoan, - ValidVault>; + ValidVault, + ValidConfidentialMPToken>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h index b6533c263d..eb341ecdba 100644 --- a/include/xrpl/tx/invariants/MPTInvariant.h +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -28,4 +28,47 @@ public: finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); }; +/** + * @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 can only have encrypted fields if lsfMPTCanPrivacy is set on + * issuance. + * - Encrypted field existence consistency: + * If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then + * sfIssuerEncryptedBalance must also exist (and vice versa). + * - 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 requiresPrivacyFlag = false; + bool badVersion = false; + }; + std::map changes_; + +public: + void + visitEntry(bool, std::shared_ptr const&, std::shared_ptr const&); + + bool + finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&); +}; + } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 20957b8d43..9fff9451d8 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -189,4 +189,208 @@ ValidMPTIssuance::finalize( mptokensDeleted_ == 0; } +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::zero; + if (sle->getType() == ltMPTOKEN) + return sle->getFieldH192(sfMPTokenIssuanceID); + if (sle->getType() == ltMPTOKEN_ISSUANCE) + return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer)); + return beast::zero; + }; + + if (before && before->getType() == ltMPTOKEN) + { + uint192 const id = getMptID(before); + changes_[id].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); + + if (hasPublicBalance || hasEncryptedFields) + changes_[id].deletedWithEncrypted = true; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + uint192 const id = getMptID(after); + changes_[id].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 hasAnyHolder = hasHolderInbox || hasHolderSpending; + + if (hasAnyHolder != hasIssuerBalance) + { + changes_[id].badConsistency = true; + } + + // Privacy flag consistency + bool const hasEncrypted = hasAnyHolder || hasIssuerBalance; + if (hasEncrypted) + changes_[id].requiresPrivacyFlag = true; + } + + if (before && before->getType() == ltMPTOKEN_ISSUANCE) + { + uint192 const id = getMptID(before); + if (before->isFieldPresent(sfConfidentialOutstandingAmount)) + changes_[id].coaDelta -= before->getFieldU64(sfConfidentialOutstandingAmount); + changes_[id].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 += coa; + + 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::mptIssuance(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; + } + + // Privacy flag consistency + if (checks.requiresPrivacyFlag) + { + if (!issuance->isFlag(lsfMPTCanPrivacy)) + { + JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted " + "fields but Issuance does not have " + "lsfMPTCanPrivacy 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; + } + } + + if (checks.badVersion) + { + JLOG(j.fatal()) + << "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when " + "sfConfidentialBalanceSpending changed"; + return false; + } + } + + return true; +} + } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp index 53aae73f8b..f29ee67550 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 0da62acdfb..4f11ca5f02 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 7756da7c3d..dcebe29c9b 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp index c42ceff827..d7414dbb78 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index 96df540d9f..bf3672933d 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include namespace xrpl {