diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h new file mode 100644 index 0000000000..776b66e542 --- /dev/null +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED +#define RIPPLE_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +SECP256K1_API int +secp256k1_elgamal_generate_keypair( + secp256k1_context const* ctx, + unsigned char* privkey, + secp256k1_pubkey* pubkey); + +SECP256K1_API int +secp256k1_elgamal_encrypt( + secp256k1_context const* ctx, + secp256k1_pubkey* c1, + secp256k1_pubkey* c2, + secp256k1_pubkey const* pubkey_Q, + uint64_t amount, + unsigned char const* blinding_factor); + +SECP256K1_API int +secp256k1_elgamal_decrypt( + secp256k1_context const* ctx, + uint64_t* amount, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + unsigned char const* privkey); + +SECP256K1_API int +secp256k1_elgamal_add( + secp256k1_context const* ctx, + secp256k1_pubkey* sum_c1, + secp256k1_pubkey* sum_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2); + +SECP256K1_API int +secp256k1_elgamal_subtract( + secp256k1_context const* ctx, + secp256k1_pubkey* diff_c1, + secp256k1_pubkey* diff_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2); + +// breaks a 66-byte encrypted amount into two 33-byte components +// then parses each 33-byte component into 64-byte secp256k1_pubkey format +bool +makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2); + +// serialize two secp256k1_pubkey components back into compressed 66-byte form +bool +serializeEcPair( + secp256k1_pubkey const& in1, + secp256k1_pubkey const& in2, + Buffer& buffer); + +TER +homomorphicAdd(Slice const& a, Slice const& b, Buffer& out); + +TER +proveEquality( + Slice const& proof, + Slice const& encAmt, // encrypted amount + Slice const& pubkey, + uint64_t const amount, + uint256 const& txHash, // Transaction context data + std::uint32_t const spendVersion); + +TER +encryptAmount( + AccountID const& account, + uint64_t amt, + Slice const& pubKeySlice, + Buffer& out); + +} // namespace ripple + +#endif diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index a0895ff0d9..66f156e1b7 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -183,6 +183,15 @@ std::size_t constexpr ecGamalEncryptedLength = 33; /** EC ElGamal ciphertext length: two 33-byte components concatenated */ std::size_t constexpr ecGamalEncryptedTotalLength = 66; + +/** Length of equality ZKProof */ +std::size_t constexpr ecEqualityProofLength = 98; + +/** Length of EC public key */ +std::size_t constexpr ecPubKeyLength = 64; + +/** Length of EC private key */ +std::size_t constexpr ecPrivKeyLength = 32; } // namespace ripple #endif diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 0a3a3b999e..abf5f26aa4 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -362,6 +362,7 @@ enum TECcodes : TERUnderlyingType { tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, tecNO_DELEGATE_PERMISSION = 198, + tecBAD_PROOF = 199 }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dbe687e232..490501788b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -741,6 +741,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, {sfMPTokenMetadata, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMutableFlags, soeOPTIONAL}, + {sfIssuerElGamalPublicKey, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ @@ -944,18 +945,22 @@ TRANSACTION(ttBATCH, 71, Batch, {sfBatchSigners, soeOPTIONAL}, })) -// /** This transaction type converts into confidential MPT balance. */ -// TRANSACTION(ttCONFIDENTIAL_CONVERT, 72, ConfidentialConvert, -// Delegation::delegatable, -// featureConfidentialTransfer, -// ({ -// {sfMPTokenIssuanceID, soeREQUIRED}, -// {sfMPTAmount, soeREQUIRED}, -// {sfHolderElGamalPublicKey, soeOPTIONAL}, -// {sfHolderEncryptedAmount, soeREQUIRED}, -// {sfIssuerEncryptedAmount, soeREQUIRED}, -// {sfZKProof, soeREQUIRED}, -// })) +/** This transaction type converts into confidential MPT balance. */ +#if TRANSACTION_INCLUDE +#include +#endif +TRANSACTION(ttCONFIDENTIAL_CONVERT, 72, ConfidentialConvert, + Delegation::delegatable, + featureConfidentialTransfer, + noPriv, + ({ + {sfMPTokenIssuanceID, soeREQUIRED}, + {sfMPTAmount, soeREQUIRED}, + {sfHolderElGamalPublicKey, soeOPTIONAL}, + {sfHolderEncryptedAmount, soeREQUIRED}, + {sfIssuerEncryptedAmount, soeREQUIRED}, + {sfZKProof, soeREQUIRED}, +})) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp new file mode 100644 index 0000000000..2711061914 --- /dev/null +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -0,0 +1,375 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include + +namespace ripple { + +int +secp256k1_elgamal_generate_keypair( + secp256k1_context const* ctx, + unsigned char* privkey, + secp256k1_pubkey* pubkey) +{ + // 1. Generate 32 random bytes for the private key + do + { + if (RAND_bytes(privkey, 32) != 1) + { + return 0; // Failure + } + // 2. Verify the random data is a valid private key. + } while (secp256k1_ec_seckey_verify(ctx, privkey) != 1); + + // 3. Create the corresponding public key. + if (secp256k1_ec_pubkey_create(ctx, pubkey, privkey) != 1) + { + return 0; // Failure + } + + return 1; // Success +} + +// ... implementation of secp256k1_elgamal_encrypt ... + +int +secp256k1_elgamal_encrypt( + secp256k1_context const* ctx, + secp256k1_pubkey* c1, + secp256k1_pubkey* c2, + secp256k1_pubkey const* pubkey_Q, + uint64_t amount, + unsigned char const* blinding_factor) +{ + unsigned char amount_scalar[32] = {0}; + secp256k1_pubkey M, S; + secp256k1_pubkey const* points_to_add[2]; + + // CORRECTED: Convert uint64_t to a 32-byte BIG-ENDIAN scalar. + for (int i = 0; i < 8; ++i) + { + amount_scalar[31 - i] = (amount >> (i * 8)) & 0xFF; + } + + if (secp256k1_ec_pubkey_create(ctx, &M, amount_scalar) != 1) + return 0; + if (secp256k1_ec_pubkey_create(ctx, c1, blinding_factor) != 1) + return 0; + + S = *pubkey_Q; + if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, blinding_factor) != 1) + return 0; + + points_to_add[0] = &M; + points_to_add[1] = &S; + if (secp256k1_ec_pubkey_combine(ctx, c2, points_to_add, 2) != 1) + return 0; + + return 1; +} +// ... implementation of secp256k1_elgamal_encrypt ... +int +secp256k1_elgamal_decrypt( + secp256k1_context const* ctx, + uint64_t* amount, + secp256k1_pubkey const* c1, + secp256k1_pubkey const* c2, + unsigned char const* privkey) +{ + secp256k1_pubkey S, M, G_point, current_M, next_M; + secp256k1_pubkey const* points_to_add[2]; + unsigned char c2_bytes[33], s_bytes[33], m_bytes[33], current_m_bytes[33]; + size_t len; + uint64_t i; + + // CORRECTED: Create the scalar '1' in big-endian format. + unsigned char one_scalar[32] = {0}; + one_scalar[31] = 1; + + S = *c1; + if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, privkey) != 1) + return 0; + + // CORRECTED: Reset 'len' before each serialize call. + len = sizeof(c2_bytes); + if (secp256k1_ec_pubkey_serialize( + ctx, c2_bytes, &len, c2, SECP256K1_EC_COMPRESSED) != 1) + return 0; + len = sizeof(s_bytes); + if (secp256k1_ec_pubkey_serialize( + ctx, s_bytes, &len, &S, SECP256K1_EC_COMPRESSED) != 1) + return 0; + if (memcmp(c2_bytes, s_bytes, sizeof(c2_bytes)) == 0) + { + *amount = 0; + return 1; + } + + if (secp256k1_ec_pubkey_negate(ctx, &S) != 1) + return 0; + points_to_add[0] = c2; + points_to_add[1] = &S; + if (secp256k1_ec_pubkey_combine(ctx, &M, points_to_add, 2) != 1) + return 0; + + len = sizeof(m_bytes); + if (secp256k1_ec_pubkey_serialize( + ctx, m_bytes, &len, &M, SECP256K1_EC_COMPRESSED) != 1) + return 0; + + if (secp256k1_ec_pubkey_create(ctx, &G_point, one_scalar) != 1) + return 0; + current_M = G_point; + + for (i = 1; i <= 100000; ++i) + { + len = sizeof(current_m_bytes); + if (secp256k1_ec_pubkey_serialize( + ctx, + current_m_bytes, + &len, + ¤t_M, + SECP256K1_EC_COMPRESSED) != 1) + return 0; + if (memcmp(m_bytes, current_m_bytes, sizeof(m_bytes)) == 0) + { + *amount = i; + return 1; + } + + points_to_add[0] = ¤t_M; + points_to_add[1] = &G_point; + if (secp256k1_ec_pubkey_combine(ctx, &next_M, points_to_add, 2) != 1) + return 0; + current_M = next_M; + } + + return 0; +} + +int +secp256k1_elgamal_add( + secp256k1_context const* ctx, + secp256k1_pubkey* sum_c1, + secp256k1_pubkey* sum_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2) +{ + secp256k1_pubkey const* c1_points[2] = {a_c1, b_c1}; + if (secp256k1_ec_pubkey_combine(ctx, sum_c1, c1_points, 2) != 1) + { + return 0; + } + + secp256k1_pubkey const* c2_points[2] = {a_c2, b_c2}; + if (secp256k1_ec_pubkey_combine(ctx, sum_c2, c2_points, 2) != 1) + { + return 0; + } + return 1; +} + +int +secp256k1_elgamal_subtract( + secp256k1_context const* ctx, + secp256k1_pubkey* diff_c1, + secp256k1_pubkey* diff_c2, + secp256k1_pubkey const* a_c1, + secp256k1_pubkey const* a_c2, + secp256k1_pubkey const* b_c1, + secp256k1_pubkey const* b_c2) +{ + // To subtract, we add the negation: (A - B) is (A + (-B)) + // Make a local, modifiable copy of B's points. + secp256k1_pubkey neg_b_c1 = *b_c1; + secp256k1_pubkey neg_b_c2 = *b_c2; + + // Negate the copies + if (secp256k1_ec_pubkey_negate(ctx, &neg_b_c1) != 1 || + secp256k1_ec_pubkey_negate(ctx, &neg_b_c2) != 1) + { + return 0; // Negation failed + } + + // Now, add A and the negated copies of B + secp256k1_pubkey const* c1_points[2] = {a_c1, &neg_b_c1}; + if (secp256k1_ec_pubkey_combine(ctx, diff_c1, c1_points, 2) != 1) + { + return 0; + } + + secp256k1_pubkey const* c2_points[2] = {a_c2, &neg_b_c2}; + if (secp256k1_ec_pubkey_combine(ctx, diff_c2, c2_points, 2) != 1) + { + return 0; + } + + return 1; // Success +} + +bool +makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2) +{ + auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) { + return secp256k1_ec_pubkey_parse( + secp256k1Context(), + &out, + reinterpret_cast(slice.data()), + slice.length()); + }; + + Slice s1{buffer.data(), ecGamalEncryptedLength}; + Slice s2{buffer.data() + ecGamalEncryptedLength, ecGamalEncryptedLength}; + + int const ret1 = parsePubKey(s1, out1); + int const ret2 = parsePubKey(s2, out2); + + return ret1 == 1 && ret2 == 1; +} + +bool +serializeEcPair( + secp256k1_pubkey const& in1, + secp256k1_pubkey const& in2, + Buffer& buffer) +{ + auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) { + size_t outLen = ecGamalEncryptedLength; // 33 bytes + int const ret = secp256k1_ec_pubkey_serialize( + secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED); + return ret == 1 && outLen == ecGamalEncryptedLength; + }; + + unsigned char* ptr = buffer.data(); + bool const res1 = serializePubKey(in1, ptr); + bool const res2 = serializePubKey(in2, ptr + ecGamalEncryptedLength); + + return res1 && res2; +} + +TER +homomorphicAdd(Slice const& a, Slice const& b, Buffer& out) +{ + if (a.length() != ecGamalEncryptedTotalLength || + b.length() != ecGamalEncryptedTotalLength) + return tecINTERNAL; + + secp256k1_pubkey a_c1; + secp256k1_pubkey a_c2; + secp256k1_pubkey b_c1; + secp256k1_pubkey b_c2; + + if (!makeEcPair(a, a_c1, a_c2) || !makeEcPair(b, b_c1, b_c2)) + return tecINTERNAL; + + secp256k1_pubkey sum_c1; + secp256k1_pubkey sum_c2; + + // todo:: support addition after it's supported + if (secp256k1_elgamal_add( + secp256k1Context(), &sum_c1, &sum_c2, &a_c1, &a_c2, &b_c1, &b_c2) != + 1) + return tecINTERNAL; + + if (!serializeEcPair(sum_c1, sum_c2, out)) + return tecINTERNAL; + + return tesSUCCESS; +} + +TER +proveEquality( + Slice const& proof, + Slice const& encAmt, // encrypted amount + Slice const& pubkey, + uint64_t const amount, + uint256 const& txHash, // Transaction context data + std::uint32_t const spendVersion) +{ + if (proof.length() != ecEqualityProofLength) + return tecINTERNAL; + + secp256k1_pubkey c1; + secp256k1_pubkey c2; + + if (!makeEcPair(encAmt, c1, c2)) + return tecINTERNAL; + + // todo: might need to change how its hashed + Serializer s; + s.addRaw(txHash.data(), txHash.bytes); + s.add32(spendVersion); + auto const txContextId = s.getSHA512Half(); + + // todo: support equality + // if (secp256k1_equality_verify( + // secp256k1Context(), + // reinterpret_cast(proof.data()), + // proof.length(), // Length of the proof byte array (98 bytes) + // &c1, + // &c2, + // reinterpret_cast(pubkey.data()), + // amount, + // txContextId.data(), // Transaction context data + // txContextId.bytes // Length of context data + // ) != 1) + // return tecBAD_PROOF; + + return tesSUCCESS; +} + +TER +encryptAmount( + AccountID const& account, + uint64_t amt, + Slice const& pubKeySlice, + Buffer& out) +{ + // Allocate ciphertext placeholders + secp256k1_pubkey c1, c2; + + // Prepare a random blinding factor + unsigned char blinding_factor[32]; + if (RAND_bytes(blinding_factor, 32) != 1) + return tecINTERNAL; + + secp256k1_pubkey pubKey; + + std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength); + std::cout << "\n encryptAmount pub key " << strHex(pubKeySlice) + << std::endl; + // Encrypt the amount + if (!secp256k1_elgamal_encrypt( + secp256k1Context(), &c1, &c2, &pubKey, amt, blinding_factor)) + return tecINTERNAL; + + // Serialize the ciphertext pair into the buffer + if (!serializeEcPair(c1, c2, out)) + return tecINTERNAL; + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index a396949afe..9f2fe4fb85 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -128,6 +128,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), + MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp new file mode 100644 index 0000000000..1c89fc82c5 --- /dev/null +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -0,0 +1,206 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include + +#include + +namespace ripple { + +class ConfidentialTransfer_test : public beast::unit_test::suite +{ + void + testConvert(FeatureBitset features) + { + testcase("test convert"); + using namespace test::jtx; + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + mptAlice.generateKeyPair(alice); + + mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)}); + + mptAlice.generateKeyPair(bob); + + auto const issuerAmt = mptAlice.encryptAmount(alice, 10); + auto const holderAmt = mptAlice.encryptAmount(bob, 10); + + mptAlice.convert({ + .account = bob, + .amt = 10, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = holderAmt, + .issuerEncryptedAmt = issuerAmt, + }); + env.close(); + + mptAlice.printMPT(bob); + + mptAlice.convert({ + .account = bob, + .amt = 20, + .proof = "123", + }); + + env.close(); + mptAlice.printMPT(bob); + } + + void + testConvertPreflight(FeatureBitset features) + { + testcase("test convert"); + using namespace test::jtx; + + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set( + {.account = alice, + .pubKey = mptAlice.getPubKey(alice), + .err = temDISABLED}); + + mptAlice.convert( + {.account = bob, + .amt = 10, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + .err = temDISABLED}); + + env.close(); + + env.enableFeature(featureConfidentialTransfer); + env.close(); + + mptAlice.convert( + {.account = alice, + .amt = 10, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + .err = temMALFORMED}); + + mptAlice.convert( + {.account = bob, + .amt = 10, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + .holderEncryptedAmt = Buffer{}, + .err = temMALFORMED}); + + mptAlice.convert( + {.account = bob, + .amt = 10, + .proof = "123", + .holderPubKey = mptAlice.getPubKey(bob), + .issuerEncryptedAmt = Buffer{}, + .err = temMALFORMED}); + + // todo: change to to check proof size + // mptAlice.convert( + // {.account = bob, + // .amt = 10, + // .proof = "123", + // .holderPubKey = mptAlice.getPubKey(bob), + // .err = temMALFORMED}); + } + + void + testSetPreflight(FeatureBitset features) + { + testcase("Set preflight"); + using namespace test::jtx; + + Env env{*this, features - featureConfidentialTransfer}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanTransfer | tfMPTCanLock}); + + mptAlice.authorize({.account = bob}); + env.close(); + mptAlice.pay(alice, bob, 100); + env.close(); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + + mptAlice.set( + {.account = alice, + .pubKey = mptAlice.getPubKey(alice), + .err = temDISABLED}); + } + + void + testWithFeats(FeatureBitset features) + { + testConvert(features); + testConvertPreflight(features); + testSetPreflight(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{testable_amendments()}; + + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, ripple); +} // namespace ripple diff --git a/src/test/jtx/confidentialTransfer.h b/src/test/jtx/confidentialTransfer.h new file mode 100644 index 0000000000..9bf9391032 --- /dev/null +++ b/src/test/jtx/confidentialTransfer.h @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_CONFIDENTIALTRANSFER_H_INCLUDED +#define RIPPLE_TEST_JTX_CONFIDENTIALTRANSFER_H_INCLUDED + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +Json::Value +convert( + MPTID const mptId, + jtx::Account const& account, + std::uint64_t const amount, + std::optional holderPk, + std::string holderEncAmt, + std::string issuerEncAmt, + std::string zkp); + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index f2f51492e3..783c983481 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -19,9 +19,16 @@ #include +#include #include #include +#include "test/jtx/mpt.h" +#include + +#include +#include + namespace ripple { namespace test { namespace jtx { @@ -248,6 +255,8 @@ MPTTester::set(MPTSet const& arg) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) jv[sfMPTokenMetadata] = strHex(*arg.metadata); + if (arg.pubKey) + jv[sfIssuerElGamalPublicKey] = strHex(*arg.pubKey); if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags)) { auto require = [&](std::optional const& holder, @@ -329,6 +338,15 @@ MPTTester::checkDomainID(std::optional expected) const }); } +[[nodiscard]] bool +MPTTester::printMPT(Account const& holder_) const +{ + return forObject( + [&](SLEP const& sle) -> bool { std::cout << "\n" + << sle->getJson(); }, + holder_); +} + [[nodiscard]] bool MPTTester::checkMPTokenAmount( Account const& holder_, @@ -347,6 +365,15 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const }); } +[[nodiscard]] bool +MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const +{ + return forObject([&](SLEP const& sle) { + return expectedAmount == + (*sle)[~sfConfidentialOutstandingAmount].value_or(0); + }); +} + [[nodiscard]] bool MPTTester::checkFlags( uint32_t const expectedFlags, @@ -492,6 +519,50 @@ MPTTester::getBalance(Account const& account) const return 0; } +std::int64_t +MPTTester::getIssuanceConfidentialBalance() const +{ + if (!id_) + Throw("MPT has not been created"); + + if (auto const sle = env_.le(keylet::mptIssuance(*id_))) + return (*sle)[~sfConfidentialOutstandingAmount].value_or(0); + + return 0; +} + +std::optional +MPTTester::getEncryptedBalance( + Account const& account, + EncryptedBalanceType option) const +{ + if (!id_) + Throw("MPT has not been created"); + + if (auto const sle = env_.le(keylet::mptoken(*id_, account.id()))) + { + if (option == HOLDER_ENCRYPTED_INBOX && + sle->isFieldPresent(sfConfidentialBalanceInbox)) + return Buffer( + (*sle)[sfConfidentialBalanceInbox].data(), + (*sle)[sfConfidentialBalanceInbox].size()); + if (option == HOLDER_ENCRYPTED_SPENDING && + sle->isFieldPresent(sfConfidentialBalanceSpending)) + return Buffer( + (*sle)[sfConfidentialBalanceSpending].data(), + (*sle)[sfConfidentialBalanceSpending].size()); + if (option == ISSUER_ENCRYPTED_BALANCE && + sle->isFieldPresent(sfIssuerEncryptedBalance)) + return Buffer( + (*sle)[sfIssuerEncryptedBalance].data(), + (*sle)[sfIssuerEncryptedBalance].size()); + + return {}; + } + + return {}; +} + std::uint32_t MPTTester::getFlags(std::optional const& holder) const { @@ -512,6 +583,168 @@ MPTTester::operator[](std::string const& name) return MPT(name, issuanceID()); } +void +MPTTester::convert(MPTConvert const& arg) +{ + Json::Value jv; + if (arg.account) + jv[sfAccount] = arg.account->human(); + else + Throw("Account not specified"); + + jv[jss::TransactionType] = jss::ConfidentialConvert; + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + + if (arg.amt) + jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + if (arg.holderPubKey) + jv[sfHolderElGamalPublicKey.jsonName] = strHex(*arg.holderPubKey); + + if (arg.holderEncryptedAmt) + jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt); + else + jv[sfHolderEncryptedAmount.jsonName] = + strHex(encryptAmount(*arg.account, *arg.amt)); + + if (arg.issuerEncryptedAmt) + jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt); + else + jv[sfIssuerEncryptedAmount.jsonName] = + strHex(encryptAmount(issuer_, *arg.amt)); + + if (arg.proof) + jv[sfZKProof.jsonName] = *arg.proof; + + auto const holderAmt = getBalance(*arg.account); + auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); + + auto maybeEncrypted = + getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + + uint64_t prevInboxBalance = + maybeEncrypted ? decryptAmount(*arg.account, *maybeEncrypted) : 0; + + if (submit(arg, jv) == tesSUCCESS) + { + auto const curConfidentialOutstanding = + getIssuanceConfidentialBalance(); + env_.require(mptbalance(*this, *arg.account, holderAmt - *arg.amt)); + env_.require(requireAny([&]() -> bool { + return prevConfidentialOutstanding + *arg.amt == + curConfidentialOutstanding; + })); + env_.require(requireAny([&]() -> bool { + auto maybeEncrypted = + getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX); + + uint64_t decryptedAmt = maybeEncrypted + ? decryptAmount(*arg.account, *maybeEncrypted) + : 0; + std::cout << "\n decrpypted amt is " << decryptedAmt << '\n'; + return prevInboxBalance + *arg.amt == decryptedAmt; + })); + } +} + +void +MPTTester::generateKeyPair(Account const& account) +{ + unsigned char privKey[ecPrivKeyLength]; + secp256k1_pubkey pubKey; + if (!secp256k1_elgamal_generate_keypair( + secp256k1Context(), privKey, &pubKey)) + Throw("failed to generate key pair"); + + pubKeys.insert({account.id(), Buffer{pubKey.data, ecPubKeyLength}}); + privKeys.insert({account.id(), Buffer{privKey, ecPrivKeyLength}}); +} + +Buffer +MPTTester::getPubKey(Account const& account) const +{ + auto it = pubKeys.find(account.id()); + if (it != pubKeys.end()) + { + return it->second; + } + + Throw("Account does not have public key"); +} + +Buffer +MPTTester::getPrivKey(Account const& account) const +{ + auto it = privKeys.find(account.id()); + if (it != privKeys.end()) + { + return it->second; + } + + Throw("Account does not have private key"); +} + +Buffer +MPTTester::encryptAmount(Account const& account, uint64_t amt) const +{ + Buffer buf(ecGamalEncryptedTotalLength); + + // Allocate ciphertext placeholders + secp256k1_pubkey c1, c2; + + // Prepare a random blinding factor + unsigned char blinding_factor[32]; + if (RAND_bytes(blinding_factor, 32) != 1) + Throw("Failed to generate random number"); + + secp256k1_pubkey pubKey; + + auto keyData = getPubKey(account); + + std::memcpy(pubKey.data, keyData.data(), ecPubKeyLength); + + // Encrypt the amount + if (!secp256k1_elgamal_encrypt( + secp256k1Context(), &c1, &c2, &pubKey, amt, blinding_factor)) + Throw("Failed to encrypt amount"); + + // Serialize the ciphertext pair into the buffer + if (!serializeEcPair(c1, c2, buf)) + Throw( + "Failed to serialize into 66 byte compressed format"); + + return buf; +} + +uint64_t +MPTTester::decryptAmount(Account const& account, Buffer const& amt) const +{ + secp256k1_pubkey c1; + secp256k1_pubkey c2; + + uint64_t decryptedAmt; + + if (!makeEcPair(amt, c1, c2)) + Throw( + "Failed to convert into individual EC components"); + + if (!secp256k1_elgamal_decrypt( + secp256k1Context(), + &decryptedAmt, + &c1, + &c2, + getPrivKey(account).data())) + Throw("Failed to decrypt amount"); + + return decryptedAmt; +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 2eacac68ec..e031e661cb 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -27,6 +27,8 @@ #include +#include + namespace ripple { namespace test { namespace jtx { @@ -145,6 +147,22 @@ struct MPTSet std::optional metadata = std::nullopt; std::optional delegate = std::nullopt; std::optional domainID = std::nullopt; + std::optional pubKey = std::nullopt; + std::optional err = std::nullopt; +}; + +struct MPTConvert +{ + std::optional account = std::nullopt; + std::optional id = std::nullopt; + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional holderPubKey = std::nullopt; + std::optional holderEncryptedAmt = std::nullopt; + std::optional issuerEncryptedAmt = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; std::optional err = std::nullopt; }; @@ -155,8 +173,16 @@ class MPTTester std::unordered_map const holders_; std::optional id_; bool close_; + std::unordered_map pubKeys; + std::unordered_map privKeys; public: + enum EncryptedBalanceType { + ISSUER_ENCRYPTED_BALANCE, + HOLDER_ENCRYPTED_INBOX, + HOLDER_ENCRYPTED_SPENDING, + }; + MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); void @@ -171,6 +197,9 @@ public: void set(MPTSet const& set = {}); + void + convert(MPTConvert const& arg = MPTConvert{}); + [[nodiscard]] bool checkDomainID(std::optional expected) const; @@ -181,6 +210,9 @@ public: [[nodiscard]] bool checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const; + [[nodiscard]] bool + checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const; + [[nodiscard]] bool checkFlags( uint32_t const expectedFlags, @@ -234,9 +266,35 @@ public: std::int64_t getBalance(Account const& account) const; + std::int64_t + getIssuanceConfidentialBalance() const; + + std::optional + getEncryptedBalance( + Account const& account, + EncryptedBalanceType option = HOLDER_ENCRYPTED_INBOX) const; + MPT operator[](std::string const& name); + bool + printMPT(Account const& holder_) const; + + void + generateKeyPair(Account const& account); + + Buffer + getPubKey(Account const& account) const; + + Buffer + getPrivKey(Account const& account) const; + + Buffer + encryptAmount(Account const& account, uint64_t amt) const; + + uint64_t + decryptAmount(Account const& account, Buffer const& amt) const; + private: using SLEP = std::shared_ptr; bool diff --git a/src/xrpld/app/tx/detail/ConfidentialConvert.cpp b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp new file mode 100644 index 0000000000..ba10568535 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialConvert.cpp @@ -0,0 +1,204 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +ConfidentialConvert::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureConfidentialTransfer)) + return temDISABLED; + + // issuer cannot convert + if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount]) + return temMALFORMED; + + if (ctx.tx[sfHolderEncryptedAmount].length() != + ecGamalEncryptedTotalLength || + ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) + return temMALFORMED; + + // if (ctx.tx[sfZKProof].length() != ecEqualityProofLength) + // return temMALFORMED; + + return tesSUCCESS; +} + +TER +ConfidentialConvert::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleIssuance = + ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer)) + return tecNO_PERMISSION; + + // issuer has not uploaded their pub key yet + if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey)) + return tecNO_PERMISSION; + + auto const sleMptoken = ctx.view.read( + keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + // we still allow conversion of zero amount + if ((*sleMptoken)[~sfMPTAmount].value_or(0) < ctx.tx[sfMPTAmount]) + return tecINSUFFICIENT_FUNDS; + + // must have pk to convert + if (!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) && + !ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)) + return tecNO_PERMISSION; + + // can't update if there's already a pk + if (sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) && + ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)) + return tecNO_PERMISSION; + + auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey) + ? ctx.tx[sfHolderElGamalPublicKey] + : (*sleMptoken)[sfHolderElGamalPublicKey]; + + // todo: check zkproof/well formed + + // check equality proof + // auto checkEqualityProof = [&](auto const& encryptedAmount, + // auto const& pubKey) -> TER { + // return proveEquality( + // ctx.tx[sfZKProof], + // encryptedAmount, + // pubKey, + // ctx.tx[sfMPTAmount], + // ctx.tx.getTransactionID(), + // (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0)); + // }; + + // if (!isTesSuccess(checkEqualityProof( + // ctx.tx[sfHolderEncryptedAmount], holderPubKey)) || + // !isTesSuccess(checkEqualityProof( + // ctx.tx[sfIssuerEncryptedAmount], + // (*sleIssuance)[sfIssuerElGamalPublicKey]))) + // { + // return tecBAD_PROOF; + // } + + return tesSUCCESS; +} + +TER +ConfidentialConvert::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_)); + if (!sleMptoken) + return tecINTERNAL; + + auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecINTERNAL; + + auto const amtToConvert = ctx_.tx[sfMPTAmount]; + auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0); + + if (ctx_.tx.isFieldPresent(sfHolderElGamalPublicKey)) + (*sleMptoken)[sfHolderElGamalPublicKey] = + ctx_.tx[sfHolderElGamalPublicKey]; + + (*sleMptoken)[sfMPTAmount] = amt - amtToConvert; + (*sleIssuance)[sfConfidentialOutstandingAmount] = + (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) + + amtToConvert; + + Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount]; + Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount]; + + // todo: we should check sfConfidentialBalanceSpending depending on if we + // encrypt zero amount + if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && + sleMptoken->isFieldPresent(sfConfidentialBalanceInbox)) + { + // homomorphically add holder's encrypted balance + { + Buffer sum(ecGamalEncryptedTotalLength); + if (TER const ter = homomorphicAdd( + holderEc, (*sleMptoken)[sfConfidentialBalanceInbox], sum); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleMptoken)[sfConfidentialBalanceInbox] = sum; + } + + // homomorphically add issuer's encrypted balance + { + Buffer sum(ecGamalEncryptedTotalLength); + if (TER const ter = homomorphicAdd( + issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance], sum); + !isTesSuccess(ter)) + return tecINTERNAL; + + (*sleMptoken)[sfIssuerEncryptedBalance] = sum; + } + } + else if ( + !sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && + !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox)) + { + (*sleMptoken)[sfConfidentialBalanceInbox] = holderEc; + (*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc; + (*sleMptoken)[sfConfidentialBalanceVersion] = 0; + + // // encrypt sfConfidentialBalanceSpending with zero balance + // Buffer out(ecGamalEncryptedTotalLength); + // if (TER res = encryptAmount( + // account_, 1, (*sleMptoken)[sfHolderElGamalPublicKey], out); + // !isTesSuccess(res)) + // { + // return tecINTERNAL; + // } + + // (*sleMptoken)[sfConfidentialBalanceSpending] = out; + } + else + { + // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should + // exist together + return tecINTERNAL; + } + + view().update(sleIssuance); + view().update(sleMptoken); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/ConfidentialConvert.h b/src/xrpld/app/tx/detail/ConfidentialConvert.h new file mode 100644 index 0000000000..cc10764f52 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialConvert.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CONFIDENTIALCONVERT_H_INCLUDED +#define RIPPLE_TX_CONFIDENTIALCONVERT_H_INCLUDED + +#include + +namespace ripple { + +class ConfidentialConvert : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit ConfidentialConvert(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index edeb12e5c0..07a113c08d 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -93,6 +93,26 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) sleMpt->isFlag(lsfMPTLocked)) return tecNO_PERMISSION; + if (ctx.view.rules().enabled(featureConfidentialTransfer)) + { + auto const sleMptIssuance = ctx.view.read( + keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + + // if there still existing encrypted balances of MPT in + // circulation + if (sleMptIssuance && + (*sleMptIssuance)[~sfConfidentialOutstandingAmount] + .value_or(0) != 0) + { + // this MPT still has encrypted balance, since we don't know + // if it's non-zero or not, we won't allow deletion of + // MPToken + if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) || + sleMpt->isFieldPresent(sfConfidentialBalanceSpending)) + return tecHAS_OBLIGATIONS; + } + } + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 6fb87711c8..db4e770a1d 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -78,6 +78,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder)) return temMALFORMED; + if (!ctx.rules.enabled(featureConfidentialTransfer) && + ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey)) + return temDISABLED; + + if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && + ctx.tx.isFieldPresent(sfHolder)) + return temMALFORMED; + // todo: check pubkey length + auto const txFlags = ctx.tx.getFlags(); // fails if both flags are set @@ -90,10 +99,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) return temMALFORMED; if (ctx.rules.enabled(featureSingleAssetVault) || - ctx.rules.enabled(featureDynamicMPT)) + ctx.rules.enabled(featureDynamicMPT) || + ctx.rules.enabled(featureConfidentialTransfer)) { // Is this transaction actually changing anything ? - if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate) + if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && + !ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !isMutate) return temMALFORMED; } @@ -264,6 +275,19 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + // cannot update public key + if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && + sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey)) + { + return tecNO_PERMISSION; + } + + if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && + sleMptIssuance->isFlag(tfMPTNoConfidentialTransfer)) + { + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -351,6 +375,16 @@ MPTokenIssuanceSet::doApply() } } + if (auto const pubKey = ctx_.tx[~sfIssuerElGamalPublicKey]) + { + // This is enforced in preflight. + XRPL_ASSERT( + sle->getType() == ltMPTOKEN_ISSUANCE, + "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance"); + + sle->setFieldVL(sfIssuerElGamalPublicKey, *pubKey); + } + view().update(sle); return tesSUCCESS;