diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 66be1f6699..7633b6e45d 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -448,10 +448,13 @@ ValidConfidentialMPToken::visitEntry( bool const hasAnyHolder = hasHolderInbox || hasHolderSpending; - if (hasAnyHolder != hasIssuerBalance) - { + // sfIssuerEncryptedBalance, sfConfidentialBalanceInbox, and sfConfidentialBalanceSpending + // must all exist or not exist same time. + if (hasHolderInbox != hasHolderSpending) + changes_[id].badConsistency = true; + + if (hasHolderInbox != hasIssuerBalance) changes_[id].badConsistency = true; - } // Privacy flag consistency bool const hasEncrypted = hasAnyHolder || hasIssuerBalance; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 059120cdfb..744d8773a3 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4306,8 +4306,10 @@ class Invariants_test : public beast::unit_test::suite auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id())); if (!sleToken) return false; - // Inject fields correctly, but the Issuance was built without the privacy flag. + // 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;