diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index da257e2bd7..15c3266656 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -17,6 +17,12 @@ namespace ripple { +struct CiphertextComponents +{ + Buffer ciphertext; + Buffer randomness; +}; + void addCommonZKPFields( Serializer& s, @@ -236,8 +242,8 @@ proveEquality( std::uint32_t const spendVersion); // returns ciphertext and the blinding factor used -std::pair -encryptAmount(uint64_t amt, Slice const& pubKeySlice); +CiphertextComponents +encryptAmount(uint64_t const amt, Slice const& pubKeySlice); Buffer encryptCanonicalZeroAmount( @@ -277,6 +283,9 @@ verifyClawbackEqualityProof( std::vector getEqualityProofs(Slice const& zkp); +NotTEC +checkEncryptedAmountFormat(STObject const& object); + } // namespace ripple #endif diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 6f98160c03..3971a35437 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -418,6 +418,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfConfidentialBalanceSpending, soeOPTIONAL}, {sfConfidentialBalanceVersion, soeDEFAULT}, {sfIssuerEncryptedBalance, soeOPTIONAL}, + {sfAuditorEncryptedBalance, soeOPTIONAL}, {sfHolderElGamalPublicKey, soeOPTIONAL}, })) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 8a9479c79c..ace51f6278 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -723,6 +723,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, {sfTransferFee, soeOPTIONAL}, {sfMutableFlags, soeOPTIONAL}, {sfIssuerElGamalPublicKey, soeOPTIONAL}, + {sfAuditorElGamalPublicKey, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ @@ -1073,6 +1074,7 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT, 85, ConfidentialConvert, {sfHolderElGamalPublicKey, soeOPTIONAL}, {sfHolderEncryptedAmount, soeREQUIRED}, {sfIssuerEncryptedAmount, soeREQUIRED}, + {sfAuditorEncryptedAmount, soeOPTIONAL}, {sfZKProof, soeREQUIRED}, })) @@ -1101,6 +1103,7 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 87, ConfidentialConvertBack, {sfMPTAmount, soeREQUIRED}, {sfHolderEncryptedAmount, soeREQUIRED}, {sfIssuerEncryptedAmount, soeREQUIRED}, + {sfAuditorEncryptedAmount, soeOPTIONAL}, {sfZKProof, soeREQUIRED}, })) @@ -1119,6 +1122,7 @@ TRANSACTION(ttCONFIDENTIAL_SEND, 88, ConfidentialSend, {sfIssuerEncryptedAmount, soeREQUIRED}, {sfZKProof, soeREQUIRED}, {sfCredentialIDs, soeOPTIONAL}, + {sfAuditorEncryptedAmount, soeOPTIONAL}, })) #if TRANSACTION_INCLUDE diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 89178a0dda..a55c9e759e 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -856,8 +856,8 @@ proveEquality( return tesSUCCESS; } -std::pair -encryptAmount(uint64_t amt, Slice const& pubKeySlice) +CiphertextComponents +encryptAmount(uint64_t const amt, Slice const& pubKeySlice) { Buffer buf(ecGamalEncryptedTotalLength); @@ -884,7 +884,7 @@ encryptAmount(uint64_t amt, Slice const& pubKeySlice) Throw( "Failed to serialize into 66 byte compressed format"); - return std::make_pair(buf, Buffer(blindingFactor, 32)); + return {std::move(buf), Buffer(blindingFactor, 32)}; } Buffer @@ -1057,4 +1057,28 @@ getEqualityProofs(Slice const& zkp) return zkps; } +NotTEC +checkEncryptedAmountFormat(STObject const& object) +{ + if (object[sfHolderEncryptedAmount].length() != + ecGamalEncryptedTotalLength || + object[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount); + if (hasAuditor && + object[sfAuditorEncryptedAmount].length() != + ecGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + if (!isValidCiphertext(object[sfHolderEncryptedAmount]) || + !isValidCiphertext(object[sfIssuerEncryptedAmount])) + return temBAD_CIPHERTEXT; + + if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount])) + return temBAD_CIPHERTEXT; + + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index 3382bd54d7..92b2f41aa6 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -23,6 +23,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite { testcase("Convert"); using namespace test::jtx; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); @@ -40,7 +41,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -66,6 +68,55 @@ class ConfidentialTransfer_test : public beast::unit_test::suite }); } + 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, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + 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) { @@ -93,7 +144,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.set( {.account = alice, - .pubKey = mptAlice.getPubKey(alice), + .issuerPubKey = mptAlice.getPubKey(alice), .err = temDISABLED}); mptAlice.convert( @@ -208,7 +259,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.set( {.account = alice, - .pubKey = mptAlice.getPubKey(alice), + .issuerPubKey = mptAlice.getPubKey(alice), .err = temDISABLED}); } @@ -233,7 +284,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = Buffer{}, .err = temMALFORMED}); + {.account = alice, + .issuerPubKey = Buffer{}, + .err = temMALFORMED}); } // issuance has disabled confidential transfer @@ -259,7 +312,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.set( {.account = alice, - .pubKey = mptAlice.getPubKey(alice), + .issuerPubKey = mptAlice.getPubKey(alice), .err = tecNO_PERMISSION}); } } @@ -340,7 +393,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.destroy(); mptAlice.generateKeyPair(bob); @@ -368,7 +421,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.convert( {.account = bob, @@ -397,7 +450,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -428,7 +481,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -465,7 +518,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); @@ -509,7 +562,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -561,7 +614,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -598,7 +652,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -638,7 +693,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.destroy(); mptAlice.generateKeyPair(bob); @@ -684,7 +739,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.mergeInbox({.account = bob, .err = tecOBJECT_NOT_FOUND}); } @@ -709,7 +764,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -743,7 +798,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // Convert 60 out of 100 mptAlice.convert( @@ -819,6 +875,120 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .err = tesSUCCESS}); } + 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"); + MPTTester mptAlice( + env, alice, {.holders = {bob, carol}, .auditor = auditor}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); + + 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.generateKeyPair(auditor); + + mptAlice.set( + {.account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + // Convert 60 out of 100 + mptAlice.convert( + {.account = bob, + .amt = 60, + .holderPubKey = mptAlice.getPubKey(bob), + .err = tesSUCCESS}); + + BEAST_EXPECT(mptAlice.getBalance(bob) == 40); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + bob, MPTTester::HOLDER_ENCRYPTED_INBOX) == 60); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + bob, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + bob, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 60); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + bob, MPTTester::AUDITOR_ENCRYPTED_BALANCE) == 60); + + // bob merge inbox + mptAlice.mergeInbox({ + .account = bob, + }); + + mptAlice.convert( + {.account = carol, + .amt = 20, + .holderPubKey = mptAlice.getPubKey(carol), + .err = tesSUCCESS}); + + BEAST_EXPECT(mptAlice.getBalance(carol) == 30); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + carol, MPTTester::HOLDER_ENCRYPTED_INBOX) == 20); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + carol, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + carol, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 20); + BEAST_EXPECT( + mptAlice.getDecryptedBalance( + carol, MPTTester::AUDITOR_ENCRYPTED_BALANCE) == 20); + + // carol merge inbox + mptAlice.mergeInbox({ + .account = carol, + }); + + // bob sends 10 to carol + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, // will be encrypted internally + .proof = "123", + .err = tesSUCCESS}); + + // bob sends 1 to carol again + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 1, + .proof = "123", + .err = tesSUCCESS}); + + mptAlice.mergeInbox({ + .account = carol, + }); + + // carol sends 15 backto bob + mptAlice.send( + {.account = carol, + .dest = bob, + .amt = 15, + .proof = "123", + .err = tesSUCCESS}); + } + void testSendPreflight(FeatureBitset features) { @@ -871,7 +1041,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.pay(alice, bob, 100); mptAlice.pay(alice, carol, 50); env.close(); @@ -1015,7 +1185,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); mptAlice.generateKeyPair(dave); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); env.close(); // bob and carol convert some funds to confidential @@ -1053,7 +1224,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.authorize({.account = carol}); mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // destroy the issuance mptAlice.destroy(); @@ -1234,7 +1405,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(carol); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // Convert 60 out of 100 mptAlice.convert( @@ -1295,7 +1466,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1333,7 +1504,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); @@ -1376,7 +1547,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1392,8 +1563,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite }); } - // can delete mptoken if issuance has been destroyed and has encrypted - // zero balance + // can delete mptoken if issuance has been destroyed and has + // encrypted zero balance { Env env{*this, features}; Account const alice("alice"); @@ -1412,7 +1583,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1437,6 +1608,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite { testcase("Convert back"); using namespace test::jtx; + Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); @@ -1454,7 +1626,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1473,12 +1646,55 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .amt = 30, .proof = "123", }); + } - // mptAlice.convertBack({ - // .account = bob, - // .amt = 10, - // .proof = "123", - // }); + 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"); + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + 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 = 40, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + mptAlice.mergeInbox({ + .account = bob, + }); + + mptAlice.convertBack({ + .account = bob, + .amt = 30, + .proof = "123", + }); } void @@ -1532,7 +1748,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1618,7 +1834,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.destroy(); mptAlice.generateKeyPair(bob); @@ -1673,7 +1889,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.convertBack( {.account = bob, @@ -1702,7 +1918,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1736,7 +1952,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); @@ -1786,7 +2002,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.generateKeyPair(bob); @@ -1859,7 +2075,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // Bob require preauthorization env(fset(bob, asfDepositAuth)); @@ -1986,7 +2203,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); mptAlice.generateKeyPair(dave); - mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + mptAlice.set( + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // setup bob. // after setup, bob's spending balance is 60, inbox balance is 0. @@ -2027,15 +2245,114 @@ class ConfidentialTransfer_test : public beast::unit_test::suite .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. + // 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, .proof = "123"}); // 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. + // 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 + testClawbackWithAuditor(FeatureBitset features) + { + testcase("test ConfidentialClawback 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 | + tfMPTCanPrivacy}); + 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, .proof = "123"}); + + // 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}); @@ -2095,7 +2412,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.pay(alice, bob, 100); mptAlice.pay(alice, carol, 50); env.close(); @@ -2158,8 +2475,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite { // set up, alice is the issuer, bob and carol are authorized - // holders. dave is not authorized. bob has confidential balance, - // carol does not. + // holders. dave is not authorized. bob has confidential + // balance, carol does not. Env env{*this, features}; Account const alice("alice"); Account const bob("bob"); @@ -2181,7 +2498,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(bob); mptAlice.generateKeyPair(carol); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.convert({ .account = bob, @@ -2232,7 +2549,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.authorize({.account = bob}); mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); env.close(); mptAlice.confidentialClaw( @@ -2270,7 +2587,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.authorize({.account = bob}); mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // destroy the issuance mptAlice.destroy(); @@ -2304,7 +2621,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); mptAlice.convert( {.account = bob, .amt = 60, @@ -2383,8 +2700,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite 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. + // 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}}); @@ -2402,7 +2720,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); return mptAlice; }; @@ -2493,8 +2811,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite // 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 + // 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}); @@ -2582,7 +2900,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); auto holderPubKeySet = false; auto verifyToggle = [&](TER expectedResult, uint64_t amt) { @@ -2613,8 +2931,8 @@ class ConfidentialTransfer_test : public beast::unit_test::suite } }; - // set lsfMPTCanPrivacy, but no effect because lsfMPTCanPrivacy was - // already set + // set lsfMPTCanPrivacy, but no effect because lsfMPTCanPrivacy + // was already set mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetPrivacy}); verifyToggle(tesSUCCESS, 10); @@ -2654,7 +2972,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.generateKeyPair(alice); mptAlice.generateKeyPair(bob); mptAlice.set( - {.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + {.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); // bob convert 50 to confidential mptAlice.convert( @@ -2710,6 +3028,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testConvert(features); testConvertPreflight(features); testConvertPreclaim(features); + testConvertWithAuditor(features); testMergeInbox(features); testMergeInboxPreflight(features); @@ -2722,18 +3041,21 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testSendPreflight(features); testSendPreclaim(features); testSendDepositPreauth(features); + testSendWithAuditor(features); // ConfidentialClawback testClawback(features); testClawbackPreflight(features); testClawbackPreclaim(features); testClawbackProof(features); + testClawbackWithAuditor(features); testDelete(features); testConvertBack(features); testConvertBackPreflight(features); testConvertBackPreclaim(features); + testConvertBackWithAuditor(features); testMutatePrivacy(features); } diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index cfc66fc8e7..70929c4e23 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -64,6 +64,7 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) : env_(env) , issuer_(issuer) , holders_(makeHolders(arg.holders)) + , auditor_(arg.auditor) , close_(arg.close) { if (arg.fund) @@ -71,6 +72,9 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) env_.fund(arg.xrp, issuer_); for (auto it : holders_) env_.fund(arg.xrpHolders, it.second); + + if (arg.auditor) + env_.fund(arg.xrp, *arg.auditor); } if (close_) env.close(); @@ -83,6 +87,9 @@ MPTTester::MPTTester(Env& env, Account const& 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); @@ -125,9 +132,11 @@ MPTTester::MPTTester(MPTInitDef const& arg) arg.env, arg.issuer, MPTInit{ + .auditor = arg.auditor, .fund = arg.fund, .close = arg.close, - .create = makeMPTCreate(arg)}} + .create = makeMPTCreate(arg), + }} { } @@ -374,8 +383,10 @@ MPTTester::setjv(MPTSet const& arg) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) jv[sfMPTokenMetadata] = strHex(*arg.metadata); - if (arg.pubKey) - jv[sfIssuerElGamalPublicKey] = strHex(*arg.pubKey); + if (arg.issuerPubKey) + jv[sfIssuerElGamalPublicKey] = strHex(*arg.issuerPubKey); + if (arg.auditorPubKey) + jv[sfAuditorElGamalPublicKey] = strHex(*arg.auditorPubKey); jv[sfTransactionType] = jss::MPTokenIssuanceSet; return jv; @@ -395,7 +406,8 @@ MPTTester::set(MPTSet const& arg) .metadata = arg.metadata, .delegate = arg.delegate, .domainID = arg.domainID, - .pubKey = arg.pubKey}); + .issuerPubKey = arg.issuerPubKey, + .auditorPubKey = arg.auditorPubKey}); if (submit(arg, jv) == tesSUCCESS) { if ((arg.flags.value_or(0) || arg.mutableFlags)) @@ -461,7 +473,7 @@ MPTTester::set(MPTSet const& arg) require(*account, false); } - if (arg.pubKey) + if (arg.issuerPubKey) { env_.require(requireAny([&]() -> bool { return forObject([&](SLEP const& sle) -> bool { @@ -474,6 +486,23 @@ MPTTester::set(MPTSet const& arg) }); })); } + + if (arg.auditorPubKey) + { + env_.require(requireAny([&]() -> bool { + return forObject([&](SLEP const& sle) -> bool { + if (sle) + { + if (!auditor_) + Throw( + "MPTTester::set: auditor is not set"); + return strHex((*sle)[sfAuditorElGamalPublicKey]) == + strHex(getPubKey(*auditor_)); + } + return false; + }); + })); + } } } @@ -780,9 +809,9 @@ MPTTester::getConvertProof( Account const& holder, std::uint64_t amount, uint256 const& ctxHash, - std::pair holderCiphertext, - std::pair issuerCiphertext, - std::optional> auditorCiphertext) const + CiphertextComponents holderCiphertext, + CiphertextComponents issuerCiphertext, + std::optional auditorCiphertext) const { if (!id_) Throw("MPT has not been created"); @@ -793,8 +822,8 @@ MPTTester::getConvertProof( size_t const zkpSize = auditorCiphertext ? 3 : 2; size_t const zkpByteLength = zkpSize * ecEqualityProofLength; - if (!sleHolder || !sleIssuance || holderCiphertext.first.size() == 0 || - issuerCiphertext.first.size() == 0) + if (!sleHolder || !sleIssuance || holderCiphertext.ciphertext.size() == 0 || + issuerCiphertext.ciphertext.size() == 0) return Buffer(zkpByteLength); auto const generateProof = [amount, ctxHash]( @@ -836,22 +865,14 @@ MPTTester::getConvertProof( Buffer zkp(zkpByteLength); Buffer holderZkp = generateProof( - holderCiphertext.first, getPubKey(holder), holderCiphertext.second); + holderCiphertext.ciphertext, + getPubKey(holder), + holderCiphertext.randomness); Buffer issuerZkp = generateProof( - issuerCiphertext.first, getPubKey(issuer_), issuerCiphertext.second); - - // std::optional auditorZkp; - // if (auditor) - // { - // Slice auditorPubKey( - // sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).data(), - // sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).size()); - // Buffer auditorZkp = txArgs.auditorEncryptedAmt && - // sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey) - // ? generateProof(*txArgs.issuerEncryptedAmt, issuerPubKey) - // : getDummyProof(); - // } + issuerCiphertext.ciphertext, + getPubKey(issuer_), + issuerCiphertext.randomness); // Pointer arithmetic to copy data into place std::uint8_t* ptr = zkp.data(); @@ -864,6 +885,26 @@ MPTTester::getConvertProof( std::memcpy(ptr, issuerZkp.data(), issuerZkp.size()); ptr += issuerZkp.size(); + if (auditorCiphertext) + { + Buffer auditorZkp(ecEqualityProofLength); + + if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey)) + { + Buffer const auditorPubKey( + sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).data(), + sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).size()); + auditorZkp = generateProof( + auditorCiphertext->ciphertext, + auditorPubKey, + auditorCiphertext->randomness); + } + + // Copy auditor + std::memcpy(ptr, auditorZkp.data(), auditorZkp.size()); + ptr += auditorZkp.size(); + } + return zkp; } @@ -892,6 +933,11 @@ MPTTester::getEncryptedBalance( return Buffer( (*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size()); + if (option == AUDITOR_ENCRYPTED_BALANCE && + sle->isFieldPresent(sfAuditorEncryptedBalance)) + return Buffer( + (*sle)[sfAuditorEncryptedBalance].data(), + (*sle)[sfAuditorEncryptedBalance].size()); } return {}; @@ -947,28 +993,43 @@ MPTTester::convert(MPTConvert const& arg) if (arg.holderPubKey) jv[sfHolderElGamalPublicKey.jsonName] = strHex(*arg.holderPubKey); - std::pair holderCiphertext; + CiphertextComponents holderCiphertext; if (arg.holderEncryptedAmt) jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt); else { holderCiphertext = encryptAmount(*arg.account, *arg.amt); - jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext.first); + jv[sfHolderEncryptedAmount.jsonName] = + strHex(holderCiphertext.ciphertext); } - std::pair issuerCiphertext; + CiphertextComponents issuerCiphertext; if (arg.issuerEncryptedAmt) jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt); else { issuerCiphertext = encryptAmount(issuer_, *arg.amt); - jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext.first); + jv[sfIssuerEncryptedAmount.jsonName] = + strHex(issuerCiphertext.ciphertext); + } + + std::optional auditorCiphertext; + if (arg.auditorEncryptedAmt) + jv[sfAuditorEncryptedAmount.jsonName] = + strHex(*arg.auditorEncryptedAmt); + else if (auditor()) + { + auditorCiphertext = encryptAmount(*auditor(), *arg.amt); + jv[sfAuditorEncryptedAmount.jsonName] = + strHex(auditorCiphertext->ciphertext); } if (arg.proof) jv[sfZKProof.jsonName] = *arg.proof; else { + // if the caller generated ciphertexts themselves, they should also + // generate the proof themselves from the randomness factor uint256 const ctxHash = getConvertContextHash( arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt); Buffer proof = getConvertProof( @@ -977,7 +1038,7 @@ MPTTester::convert(MPTConvert const& arg) ctxHash, holderCiphertext, issuerCiphertext, - {}); + auditorCiphertext); jv[sfZKProof] = strHex(proof); } @@ -990,6 +1051,8 @@ MPTTester::convert(MPTConvert const& arg) getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); uint64_t prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + [[maybe_unused]] uint64_t prevAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); if (submit(arg, jv) == tesSUCCESS) { @@ -1008,6 +1071,15 @@ MPTTester::convert(MPTConvert const& arg) uint64_t postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + if (arg.auditorEncryptedAmt || auditor_) + { + uint64_t postAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + // 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; @@ -1076,19 +1148,25 @@ MPTTester::send(MPTConfidentialSend const& arg) jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt); else jv[sfSenderEncryptedAmount] = - strHex(encryptAmount(*arg.account, *arg.amt).first); + strHex(encryptAmount(*arg.account, *arg.amt).ciphertext); if (arg.destEncryptedAmt) jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt); else jv[sfDestinationEncryptedAmount] = - strHex(encryptAmount(*arg.dest, *arg.amt).first); + strHex(encryptAmount(*arg.dest, *arg.amt).ciphertext); if (arg.issuerEncryptedAmt) jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt); else jv[sfIssuerEncryptedAmount] = - strHex(encryptAmount(issuer_, *arg.amt).first); + strHex(encryptAmount(issuer_, *arg.amt).ciphertext); + + if (arg.auditorEncryptedAmt) + jv[sfAuditorEncryptedAmount] = strHex(*arg.auditorEncryptedAmt); + else if (auditor()) + jv[sfAuditorEncryptedAmount] = + strHex(encryptAmount(*auditor(), *arg.amt).ciphertext); if (arg.proof) jv[sfZKProof] = *arg.proof; @@ -1112,6 +1190,8 @@ MPTTester::send(MPTConfidentialSend const& arg) getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); uint64_t prevSenderIssuer = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + [[maybe_unused]] uint64_t prevSenderAuditor = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); // Destination's previous confidential state uint64_t prevDestInbox = @@ -1120,6 +1200,8 @@ MPTTester::send(MPTConfidentialSend const& arg) getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING); uint64_t prevDestIssuer = getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE); + [[maybe_unused]] uint64_t prevDestAuditor = + getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE); if (submit(arg, jv) == tesSUCCESS) { @@ -1179,6 +1261,30 @@ MPTTester::send(MPTConfidentialSend const& arg) env_.require(requireAny([&]() -> bool { return postDestInbox + postDestSpending == postDestIssuer; })); + + if (arg.auditorEncryptedAmt || auditor_) + { + uint64_t postSenderAuditor = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); + uint64_t postDestAuditor = + getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_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; + })); + } } } @@ -1251,6 +1357,10 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) return getDecryptedBalance(*arg.holder, ISSUER_ENCRYPTED_BALANCE) == 0; })); + env_.require(requireAny([&]() -> bool { + return getDecryptedBalance( + *arg.holder, AUDITOR_ENCRYPTED_BALANCE) == 0; + })); } } @@ -1291,8 +1401,8 @@ MPTTester::getPrivKey(Account const& account) const Throw("Account does not have private key"); } -std::pair -MPTTester::encryptAmount(Account const& account, uint64_t amt) const +CiphertextComponents +MPTTester::encryptAmount(Account const& account, uint64_t const amt) const { return ripple::encryptAmount(amt, getPubKey(account)); } @@ -1327,8 +1437,17 @@ MPTTester::getDecryptedBalance( { auto maybeEncrypted = getEncryptedBalance(account, balanceType); - auto accountToDecrypt = - balanceType == ISSUER_ENCRYPTED_BALANCE ? issuer_ : account; + Account accountToDecrypt = account; + + if (balanceType == ISSUER_ENCRYPTED_BALANCE) + accountToDecrypt = issuer_; + else if (balanceType == AUDITOR_ENCRYPTED_BALANCE) + { + if (!auditor_) + return 0; + accountToDecrypt = *auditor_; + } + return maybeEncrypted ? decryptAmount(accountToDecrypt, *maybeEncrypted) : 0; }; @@ -1420,13 +1539,24 @@ MPTTester::convertBack(MPTConvertBack const& arg) jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt); else jv[sfHolderEncryptedAmount.jsonName] = - strHex(encryptAmount(*arg.account, *arg.amt).first); + strHex(encryptAmount(*arg.account, *arg.amt).ciphertext); if (arg.issuerEncryptedAmt) jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt); else jv[sfIssuerEncryptedAmount.jsonName] = - strHex(encryptAmount(issuer_, *arg.amt).first); + strHex(encryptAmount(issuer_, *arg.amt).ciphertext); + + std::optional auditorCiphertext; + if (arg.auditorEncryptedAmt) + jv[sfAuditorEncryptedAmount.jsonName] = + strHex(*arg.auditorEncryptedAmt); + else if (auditor()) + { + auditorCiphertext = encryptAmount(*auditor(), *arg.amt); + jv[sfAuditorEncryptedAmount.jsonName] = + strHex(auditorCiphertext->ciphertext); + } if (arg.proof) jv[sfZKProof.jsonName] = *arg.proof; @@ -1440,6 +1570,8 @@ MPTTester::convertBack(MPTConvertBack const& arg) getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); uint64_t prevIssuerBalance = getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE); + [[maybe_unused]] uint64_t prevAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE); if (submit(arg, jv) == tesSUCCESS) { @@ -1458,6 +1590,16 @@ MPTTester::convertBack(MPTConvertBack const& arg) uint64_t postSpendingBalance = getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING); + if (arg.auditorEncryptedAmt || auditor_) + { + uint64_t postAuditorBalance = + getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_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; })); diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index f46198f0c1..bb52e715e5 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -104,6 +105,7 @@ struct MPTCreate struct MPTInit { Holders holders = {}; + std::optional auditor = std::nullopt; PrettyAmount const xrp = XRP(10'000); PrettyAmount const xrpHolders = XRP(10'000); bool fund = true; @@ -118,6 +120,7 @@ struct MPTInitDef Env& env; Account issuer; Holders holders = {}; + std::optional auditor = std::nullopt; std::uint16_t transferFee = 0; std::optional pay = std::nullopt; std::uint32_t flags = MPTDEXFlags; @@ -162,7 +165,8 @@ struct MPTSet std::optional metadata = std::nullopt; std::optional delegate = std::nullopt; std::optional domainID = std::nullopt; - std::optional pubKey = std::nullopt; + std::optional issuerPubKey = std::nullopt; + std::optional auditorPubKey = std::nullopt; std::optional err = std::nullopt; }; @@ -203,6 +207,7 @@ struct MPTConfidentialSend std::optional senderEncryptedAmt = std::nullopt; std::optional destEncryptedAmt = std::nullopt; std::optional issuerEncryptedAmt = std::nullopt; + std::optional auditorEncryptedAmt = std::nullopt; std::optional> credentials = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -218,6 +223,7 @@ struct MPTConvertBack std::optional proof = std::nullopt; std::optional holderEncryptedAmt = std::nullopt; std::optional issuerEncryptedAmt = std::nullopt; + std::optional auditorEncryptedAmt = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; @@ -242,6 +248,7 @@ 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; @@ -252,6 +259,7 @@ public: ISSUER_ENCRYPTED_BALANCE, HOLDER_ENCRYPTED_INBOX, HOLDER_ENCRYPTED_SPENDING, + AUDITOR_ENCRYPTED_BALANCE, }; MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); @@ -342,6 +350,13 @@ public: { return issuer_; } + + std::optional const& + auditor() const + { + return auditor_; + } + Account const& holder(std::string const& h) const; @@ -401,8 +416,8 @@ public: Buffer getPrivKey(Account const& account) const; - std::pair - encryptAmount(Account const& account, uint64_t amt) const; + CiphertextComponents + encryptAmount(Account const& account, uint64_t const amt) const; uint64_t decryptAmount(Account const& account, Buffer const& amt) const; @@ -427,9 +442,9 @@ public: Account const& holder, std::uint64_t amount, uint256 const& ctxHash, - std::pair holderCiphertext, - std::pair issuerCiphertext, - std::optional> auditorCiphertext) const; + CiphertextComponents holderCiphertext, + CiphertextComponents issuerCiphertext, + std::optional auditorCiphertext) const; private: using SLEP = SLE::const_pointer; diff --git a/src/xrpld/app/tx/detail/ConfidentialClawback.cpp b/src/xrpld/app/tx/detail/ConfidentialClawback.cpp index f32b14a7e9..7117d71892 100644 --- a/src/xrpld/app/tx/detail/ConfidentialClawback.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialClawback.cpp @@ -126,7 +126,7 @@ ConfidentialClawback::doApply() JLOG(ctx_.journal.error()) << "ConfidentialClawback: Failed to generate canonical zero: " << e.what(); - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE } // Set holder's confidential balances to encrypted zero @@ -135,6 +135,30 @@ ConfidentialClawback::doApply() (*sleHolderMPToken)[sfIssuerEncryptedBalance] = encZeroForIssuer; (*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0; + if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance)) + { + // check that issuance has auditor's pubkey before accessing it, if the + // field is absent, something has gone bad + if (!sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey)) + return tecINTERNAL; // LCOV_EXCL_LINE + + Slice const auditorPubKey = (*sleIssuance)[sfAuditorElGamalPublicKey]; + + try + { + Buffer const encZeroForAuditor = encryptCanonicalZeroAmount( + auditorPubKey, holder, mptIssuanceID); + (*sleHolderMPToken)[sfAuditorEncryptedBalance] = encZeroForAuditor; + } + catch (std::exception const& e) + { + JLOG(ctx_.journal.error()) + << "ConfidentialClawback: Failed to generate canonical zero: " + << e.what(); + return tecINTERNAL; // LCOV_EXCL_LINE + } + } + // Decrease Global Confidential Outstanding Amount auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount]; (*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount; diff --git a/src/xrpld/app/tx/detail/ConfidentialConvert.cpp b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp index d7369e2176..9c4dd61216 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvert.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp @@ -21,18 +21,12 @@ ConfidentialConvert::preflight(PreflightContext const& ctx) if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) return temMALFORMED; - if (ctx.tx[sfHolderEncryptedAmount].length() != - ecGamalEncryptedTotalLength || - ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) - return temBAD_CIPHERTEXT; + if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res)) + return res; if (ctx.tx[sfMPTAmount] > maxMPTokenAmount) return temBAD_AMOUNT; - if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) || - !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount])) - return temBAD_CIPHERTEXT; - if (ctx.tx.isFieldPresent(sfHolderElGamalPublicKey) && ctx.tx[sfHolderElGamalPublicKey].length() != ecPubKeyLength) return temMALFORMED; @@ -66,6 +60,12 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx) if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey)) return tecNO_PERMISSION; + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + + // tx must include auditor ciphertext if the issuance has enabled auditing + if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) && !hasAuditor) + return tecNO_PERMISSION; + auto const sleMptoken = ctx.view.read( keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); if (!sleMptoken) @@ -106,8 +106,6 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx) ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfMPTAmount]); - bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); - std::vector const zkps = getEqualityProofs(ctx.tx[sfZKProof]); auto const& amount = ctx.tx[sfMPTAmount]; @@ -178,8 +176,10 @@ ConfidentialConvert::doApply() Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount]; Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount]; - // todo: we should check sfConfidentialBalanceSpending depending on if we - // encrypt zero amount + std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + + // todo: we should check sfConfidentialBalanceSpending depending on + // if we encrypt zero amount if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) && sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) @@ -205,6 +205,18 @@ ConfidentialConvert::doApply() (*sleMptoken)[sfIssuerEncryptedBalance] = sum; } + + // homomorphically add auditor's encrypted balance + if (auditorEc) + { + Buffer sum(ecGamalEncryptedTotalLength); + if (TER const ter = homomorphicAdd( + *auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance], sum); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleMptoken)[sfAuditorEncryptedBalance] = sum; + } } else if ( !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && @@ -215,12 +227,15 @@ ConfidentialConvert::doApply() (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc; (*sleMptoken)[sfConfidentialBalanceVersion] = 0; + if (auditorEc) + (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc; + try { // encrypt sfConfidentialBalanceSpending with zero balance Buffer out; - out = - encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]).first; + out = encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]) + .ciphertext; (*sleMptoken)[sfConfidentialBalanceSpending] = out; } catch (std::exception const& e) diff --git a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp index 11244bb5cc..f32bd33450 100644 --- a/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp @@ -20,18 +20,12 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx) if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) return temMALFORMED; - if (ctx.tx[sfHolderEncryptedAmount].length() != - ecGamalEncryptedTotalLength || - ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) - return temBAD_CIPHERTEXT; + if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res)) + return res; if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount) return temBAD_AMOUNT; - if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) || - !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount])) - return temBAD_CIPHERTEXT; - // todo: update with correct size of proof since it might also contain range // proof // if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) @@ -52,8 +46,13 @@ ConfidentialConvertBack::preclaim(PreclaimContext const& ctx) if (!sleIssuance->isFlag(lsfMPTCanPrivacy)) return tecNO_PERMISSION; - // already checked in preflight, but should also check that issuer on the - // issuance isn't the account either + // tx must include auditor ciphertext if the issuance has enabled auditing + if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) && + !ctx.tx.isFieldPresent(sfAuditorEncryptedAmount)) + 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 @@ -144,6 +143,8 @@ ConfidentialConvertBack::doApply() (*sleMptoken)[sfConfidentialBalanceVersion] = (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; + std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + // homomorphically subtract holder's encrypted balance { Buffer res(ecGamalEncryptedTotalLength); @@ -170,6 +171,19 @@ ConfidentialConvertBack::doApply() (*sleMptoken)[sfIssuerEncryptedBalance] = res; } + if (auditorEc) + { + Buffer res(ecGamalEncryptedTotalLength); + if (TER const ter = homomorphicSubtract( + (*sleMptoken)[sfAuditorEncryptedBalance], + ctx_.tx[sfAuditorEncryptedAmount], + res); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleMptoken)[sfAuditorEncryptedBalance] = res; + } + view().update(sleIssuance); view().update(sleMptoken); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/ConfidentialSend.cpp b/src/xrpld/app/tx/detail/ConfidentialSend.cpp index 93985b6181..9502e68353 100644 --- a/src/xrpld/app/tx/detail/ConfidentialSend.cpp +++ b/src/xrpld/app/tx/detail/ConfidentialSend.cpp @@ -37,11 +37,20 @@ ConfidentialSend::preflight(PreflightContext const& ctx) ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) return temBAD_CIPHERTEXT; + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + if (hasAuditor && + ctx.tx[sfAuditorEncryptedAmount].length() != + ecGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + 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 (ctx.tx[sfZKProof].length() != ecEqualityProofLength) // return temMALFORMED; @@ -79,6 +88,11 @@ ConfidentialSend::preclaim(PreclaimContext const& ctx) if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey)) return tecNO_PERMISSION; + // tx must include auditor ciphertext if the issuance has enabled auditing + if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) && + !ctx.tx.isFieldPresent(sfAuditorEncryptedAmount)) + 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]) @@ -176,6 +190,8 @@ ConfidentialSend::doApply() Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount]; Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount]; + std::optional const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount]; + // Subtract from sender's spending balance { Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending]; @@ -202,6 +218,20 @@ ConfidentialSend::doApply() (*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc; } + // Subtract from auditor's balance if present + if (auditorEc) + { + Slice const curAuditorEnc = (*sleSender)[sfAuditorEncryptedBalance]; + Buffer newAuditorEnc(ecGamalEncryptedTotalLength); + + if (TER const ter = + homomorphicSubtract(curAuditorEnc, *auditorEc, newAuditorEnc); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleSender)[sfAuditorEncryptedBalance] = newAuditorEnc; + } + // Increment version (*sleSender)[sfConfidentialBalanceVersion] = (*sleSender)[~sfConfidentialBalanceVersion].value_or(0u) + 1u; @@ -231,6 +261,21 @@ ConfidentialSend::doApply() (*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc; } + // Add to auditor's balance if present + if (auditorEc) + { + Slice const curAuditorEnc = + (*sleDestination)[sfAuditorEncryptedBalance]; + Buffer newAuditorEnc(ecGamalEncryptedTotalLength); + + if (TER const ter = + homomorphicAdd(curAuditorEnc, *auditorEc, newAuditorEnc); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleDestination)[sfAuditorEncryptedBalance] = newAuditorEnc; + } + view().update(sleSender); view().update(sleDestination); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 1b9115adf1..e33a59a26f 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -71,7 +71,10 @@ 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 hasElGamalKey = ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey); + auto const hasIssuerElGamalKey = + ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey); + auto const hasAuditorElGamalKey = + ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey); auto const txFlags = ctx.tx.getFlags(); auto const mutatePrivacy = mutableFlags && @@ -83,7 +86,7 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (isMutate && !ctx.rules.enabled(featureDynamicMPT)) return temDISABLED; - if ((hasElGamalKey || mutatePrivacy) && + if ((hasIssuerElGamalKey || hasAuditorElGamalKey || mutatePrivacy) && !ctx.rules.enabled(featureConfidentialTransfer)) return temDISABLED; @@ -93,13 +96,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (mutatePrivacy && hasHolder) return temMALFORMED; - if (hasElGamalKey && hasHolder) - return temMALFORMED; - - if (hasElGamalKey && - ctx.tx[sfIssuerElGamalPublicKey].length() != ecPubKeyLength) - return temMALFORMED; - // fails if both flags are set if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) return temINVALID_FLAG; @@ -114,7 +110,8 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) ctx.rules.enabled(featureConfidentialTransfer)) { // Is this transaction actually changing anything ? - if (txFlags == 0 && !hasDomain && !hasElGamalKey && !isMutate) + if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && + !hasAuditorElGamalKey && !isMutate) return temMALFORMED; } @@ -158,6 +155,20 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) } } + if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey)) + return temMALFORMED; + + if (hasAuditorElGamalKey && !hasIssuerElGamalKey) + return temMALFORMED; + + if (hasIssuerElGamalKey && + ctx.tx[sfIssuerElGamalPublicKey].length() != ecPubKeyLength) + return temMALFORMED; + + if (hasAuditorElGamalKey && + ctx.tx[sfAuditorElGamalPublicKey].length() != ecPubKeyLength) + return temMALFORMED; + return tesSUCCESS; } @@ -301,19 +312,40 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } - // cannot update public key + // cannot update issuer public key if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey)) { return tecNO_PERMISSION; } + // cannot update auditor public key + if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && + sleMptIssuance->isFieldPresent(sfAuditorElGamalPublicKey)) + { + return tecNO_PERMISSION; + } + if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !sleMptIssuance->isFlag(lsfMPTCanPrivacy)) { return tecNO_PERMISSION; } + if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && + !sleMptIssuance->isFlag(lsfMPTCanPrivacy)) + { + return tecNO_PERMISSION; + } + + // cannot upload key if there's circulating supply of COA + if ((ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) || + ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey)) && + sleMptIssuance->isFieldPresent(sfConfidentialOutstandingAmount)) + { + return tecNO_PERMISSION; // LCOV_EXCL_LINE + } + return tesSUCCESS; } @@ -411,6 +443,16 @@ MPTokenIssuanceSet::doApply() sle->setFieldVL(sfIssuerElGamalPublicKey, *pubKey); } + if (auto const pubKey = ctx_.tx[~sfAuditorElGamalPublicKey]) + { + // This is enforced in preflight. + XRPL_ASSERT( + sle->getType() == ltMPTOKEN_ISSUANCE, + "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance"); + + sle->setFieldVL(sfAuditorElGamalPublicKey, *pubKey); + } + view().update(sle); return tesSUCCESS;