diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index bd86e7aacf..5b13aaa6b0 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -404,48 +404,39 @@ Buffer generateBlindingFactor(); /** - * @brief Verifies the cryptographic link between an ElGamal Ciphertext and a - * Pedersen Commitment for a transaction Amount. + * @brief Distinguishes the two algebraic structures used in + * ElGamal-Pedersen linkage proofs. * - * It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m` - * as the Pedersen Commitment `pcmSlice`, using the randomness `r`. - * Proves Enc(m) <-> Pcm(m) - * - * @param proof The Zero Knowledge Proof bytes. - * @param encAmt The ElGamal ciphertext of the amount (C1, C2). - * @param pubKeySlice The sender's public key. - * @param pcmSlice The Pedersen Commitment to the amount. - * @param contextHash The unique context hash for this transaction. - * @return tesSUCCESS if the proof is valid, or an error code otherwise. + * - amount: The ciphertext was created with randomness `r`. + * Verification order: C1, C2, Pk, Pcm. + * - balance: The ciphertext was created with the secret key `s`. + * Verification order: Pk, C2, C1, Pcm (swaps Pk <-> C1). */ -TER -verifyAmountPcmLinkage( - Slice const& proof, - Slice const& encAmt, - Slice const& pubKeySlice, - Slice const& pcmSlice, - uint256 const& contextHash); +enum class PcmLinkageType { amount, balance }; /** * @brief Verifies the cryptographic link between an ElGamal Ciphertext and a - * Pedersen Commitment for an account Balance. + * Pedersen Commitment. * - * It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b` - * as the Pedersen Commitment `pcmSlice`, using the secret key `s`. - * Proves Enc(b) <-> Pcm(b) + * Proves that the ElGamal ciphertext `encAmt` encrypts the same value + * as the Pedersen Commitment `pcmSlice`. * - * Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic - * structure. + * The `type` parameter selects the argument ordering passed to the + * underlying secp256k1 verification call to accommodate the different + * algebraic structures used for amounts (randomness `r`) vs balances + * (secret key `s`). * + * @param type Whether this is an amount or balance linkage proof. * @param proof The Zero Knowledge Proof bytes. - * @param encAmt The ElGamal ciphertext of the balance (C1, C2). + * @param encAmt The ElGamal ciphertext (C1, C2). * @param pubKeySlice The sender's public key. - * @param pcmSlice The Pedersen Commitment to the balance. + * @param pcmSlice The Pedersen Commitment. * @param contextHash The unique context hash for this transaction. * @return tesSUCCESS if the proof is valid, or an error code otherwise. */ TER -verifyBalancePcmLinkage( +verifyPcmLinkage( + PcmLinkageType type, Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 4f57f19e60..fca3304aa7 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1739,6 +1739,13 @@ removeEmptyHolding( if (mptoken->at(sfMPTAmount) != 0) return tecHAS_OBLIGATIONS; + // Don't delete if the token still has confidential balances + if (mptoken->isFieldPresent(sfConfidentialBalanceInbox) || + mptoken->isFieldPresent(sfConfidentialBalanceSpending)) + { + return tecHAS_OBLIGATIONS; + } + return authorizeMPToken( view, {}, // priorBalance diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index d0e5c1d20a..d37f0cdf84 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -512,7 +512,8 @@ checkEncryptedAmountFormat(STObject const& object) } TER -verifyAmountPcmLinkage( +verifyPcmLinkage( + PcmLinkageType type, Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, @@ -548,7 +549,13 @@ verifyAmountPcmLinkage( return tecINTERNAL; // LCOV_EXCL_LINE } - if (auto res = secp256k1_elgamal_pedersen_link_verify( + // For amount linkage (randomness r): order is C1, C2, Pk, Pcm. + // For balance linkage (secret key s): order is Pk, C2, C1, Pcm + // (swaps Pk <-> C1 to accommodate the different algebraic structure). + int res; + if (type == PcmLinkageType::amount) + { + res = secp256k1_elgamal_pedersen_link_verify( secp256k1Context(), proof.data(), &pair->c1, @@ -556,54 +563,10 @@ verifyAmountPcmLinkage( &pubKey, &pcm, contextHash.data()); - res != 1) - { - return tecBAD_PROOF; } - - return tesSUCCESS; -} - -TER -verifyBalancePcmLinkage( - Slice const& proof, - Slice const& encAmt, - Slice const& pubKeySlice, - Slice const& pcmSlice, - uint256 const& contextHash) -{ - if (proof.length() != ecPedersenProofLength) - return tecINTERNAL; - - auto const pair = makeEcPair(encAmt); - if (!pair) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (pubKeySlice.size() != ecPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (pcmSlice.size() != ecPedersenCommitmentLength) - return tecINTERNAL; // LCOV_EXCL_LINE - - secp256k1_pubkey pubKey; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength); - res != 1) + else { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - secp256k1_pubkey pcm; - if (auto res = secp256k1_ec_pubkey_parse( - secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength); - res != 1) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - // Note: c2, c1 order - the linkage proof expects the message-containing - // component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G). - if (auto res = secp256k1_elgamal_pedersen_link_verify( + res = secp256k1_elgamal_pedersen_link_verify( secp256k1Context(), proof.data(), &pubKey, @@ -611,11 +574,11 @@ verifyBalancePcmLinkage( &pair->c1, &pcm, contextHash.data()); - res != 1) - { - return tecBAD_PROOF; } + if (res != 1) + return tecBAD_PROOF; + return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 93b27b2ea4..13653852ee 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -124,7 +124,8 @@ verifyProofs( return tecINTERNAL; // LCOV_EXCL_LINE // verify el gamal pedersen linkage - if (auto const ter = verifyBalancePcmLinkage( + if (auto const ter = verifyPcmLinkage( + PcmLinkageType::balance, pedersenProof, (*mptoken)[sfConfidentialBalanceSpending], holderPubKey, diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp index 33f8f2dd54..d8c4111b50 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -46,6 +47,16 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx) !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; } diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index 68e45aa2b4..826cf53201 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -157,7 +157,8 @@ verifySendProofs( } // Verify amount linkage - if (auto const ter = verifyAmountPcmLinkage( + if (auto const ter = verifyPcmLinkage( + PcmLinkageType::amount, amountLinkageProof, ctx.tx[sfSenderEncryptedAmount], (*sleSenderMPToken)[sfHolderEncryptionKey], @@ -169,7 +170,8 @@ verifySendProofs( } // Verify balance linkage - if (auto const ter = verifyBalancePcmLinkage( + if (auto const ter = verifyPcmLinkage( + PcmLinkageType::balance, balanceLinkageProof, (*sleSenderMPToken)[sfConfidentialBalanceSpending], (*sleSenderMPToken)[sfHolderEncryptionKey], diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index ac1ba58530..da787c065f 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -137,6 +137,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (hasAuditorElGamalKey && !hasIssuerElGamalKey) return temMALFORMED; + // Cannot set keys while clearing confidential amount + if ((hasIssuerElGamalKey || hasAuditorElGamalKey) && mutableFlags && + (*mutableFlags & tmfMPTClearCanConfidentialAmount)) + return temINVALID_FLAG; + if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey])) return temMALFORMED; @@ -291,14 +296,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; // LCOV_EXCL_LINE } + // Check if the transaction is enabling confidential amounts + bool const enablesConfidentialAmount = + mutableFlags && (*mutableFlags & tmfMPTSetCanConfidentialAmount); + + // 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(lsfMPTCanConfidentialAmount)) + !sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount) && !enablesConfidentialAmount) { return tecNO_PERMISSION; } if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) && - !sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount)) + !sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount) && !enablesConfidentialAmount) { return tecNO_PERMISSION; } diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index ba77abb2ee..53b436e839 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -536,6 +537,85 @@ class ConfidentialTransfer_test : public beast::unit_test::suite } } + 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 | tfMPTCanConfidentialAmount, + }); + + 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 = tmfMPTSetCanConfidentialAmount, + }); + } + + // 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 = tmfMPTSetCanConfidentialAmount, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // Verify lsfMPTCanConfidentialAmount flag is set + BEAST_EXPECT(mptAlice.checkFlags( + lsfMPTCanTransfer | lsfMPTCanLock | lsfMPTCanConfidentialAmount)); + + // Verify keys are persisted on the issuance + auto const sle = env.le(keylet::mptIssuance(mptAlice.issuanceID())); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->isFieldPresent(sfIssuerEncryptionKey)); + BEAST_EXPECT(sle->isFieldPresent(sfAuditorEncryptionKey)); + } + } + void testSetPreflight(FeatureBitset features) { @@ -633,6 +713,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .err = temMALFORMED, }); + // Cannot set keys while clearing confidential amount + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTClearCanConfidentialAmount, + .issuerPubKey = mptAlice.getPubKey(alice), + .err = temINVALID_FLAG, + }); + // Cannot set Holder and auditor Keys in the same transaction mptAlice.set({ .account = alice, @@ -641,31 +729,174 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .err = temMALFORMED, }); } + } - // issuance has disabled confidential transfer + 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}}); - // no tfMPTCanConfidentialAmount flag enabled + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount, + }); + + 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 | tfMPTCanConfidentialAmount, + }); + + 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.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), + .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 tmfMPTCannotMutateCanConfidentialAmount + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCannotMutateCanConfidentialAmount, + }); + + mptAlice.generateKeyPair(alice); + + // Trying to enable confidential amounts and set keys fails + // because the issuance cannot mutate canConfidentialAmount + mptAlice.set({ + .account = alice, + .mutableFlags = tmfMPTSetCanConfidentialAmount, + .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 | tfMPTCanConfidentialAmount, + }); + + 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, }); } @@ -1340,6 +1571,116 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .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 | tfMPTCanConfidentialAmount, + }); + + 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 | tfMPTCanConfidentialAmount | 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 @@ -2599,6 +2940,85 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .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 asset = mptt.issuanceID(); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + + test::jtx::Vault 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); + MPTID 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().openLedger().modify([&](OpenView& view, beast::Journal) { + // Set lsfMPTCanConfidentialAmount on the share issuance + // so the invariant allows encrypted fields on the MPToken + auto issuance = std::const_pointer_cast(view.read(keylet::mptIssuance(share))); + if (!issuance) + return false; + issuance->setFlag(lsfMPTCanConfidentialAmount); + view.rawReplace(issuance); + + auto const k = keylet::mptoken(share, depositor.id()); + auto sle = std::const_pointer_cast(view.read(k)); + if (!sle) + return false; + // Inject dummy confidential balance fields + Buffer dummyCiphertext(ecGamalEncryptedTotalLength); + std::memset(dummyCiphertext.data(), 0, ecGamalEncryptedTotalLength); + dummyCiphertext.data()[0] = ecCompressedPrefixEvenY; + dummyCiphertext.data()[ecGamalEncryptedLength] = ecCompressedPrefixEvenY; + dummyCiphertext.data()[ecGamalEncryptedLength - 1] = 0x01; + dummyCiphertext.data()[ecGamalEncryptedTotalLength - 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 @@ -6102,7 +6522,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testMergeInboxPreflight(features); testMergeInboxPreclaim(features); + testSet(features); testSetPreflight(features); + testSetPreclaim(features); // ConfidentialMPTSend testSend(features);