ConfidentialConvert (#5901)

ConfidentialConvert and some test framework update
This commit is contained in:
Shawn Xie
2025-10-16 14:31:14 -04:00
committed by GitHub
parent 5a89641d98
commit 8fdc639206
14 changed files with 1367 additions and 14 deletions

View File

@@ -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 <xrpl/basics/Slice.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/detail/secp256k1.h>
#include <secp256k1.h>
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

View File

@@ -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

View File

@@ -362,6 +362,7 @@ enum TECcodes : TERUnderlyingType {
tecPSEUDO_ACCOUNT = 196,
tecPRECISION_LOSS = 197,
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199
};
//------------------------------------------------------------------------------

View File

@@ -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 <xrpld/app/tx/detail/ConfidentialConvert.h>
#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.

View File

@@ -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 <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <openssl/rand.h>
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,
&current_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] = &current_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<unsigned char const*>(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<unsigned char const*>(proof.data()),
// proof.length(), // Length of the proof byte array (98 bytes)
// &c1,
// &c2,
// reinterpret_cast<unsigned char const*>(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

View File

@@ -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."),

View File

@@ -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 <test/jtx.h>
#include <test/jtx/confidentialTransfer.h>
#include <test/jtx/trust.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <openssl/rand.h>
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

View File

@@ -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 <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/owners.h>
namespace ripple {
namespace test {
namespace jtx {
Json::Value
convert(
MPTID const mptId,
jtx::Account const& account,
std::uint64_t const amount,
std::optional<std::string> holderPk,
std::string holderEncAmt,
std::string issuerEncAmt,
std::string zkp);
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -19,9 +19,16 @@
#include <test/jtx.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h>
#include "test/jtx/mpt.h"
#include <openssl/rand.h>
#include <cstdint>
#include <string>
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<Account> const& holder,
@@ -329,6 +338,15 @@ MPTTester::checkDomainID(std::optional<uint256> 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<std::runtime_error>("MPT has not been created");
if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
return (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
return 0;
}
std::optional<Buffer>
MPTTester::getEncryptedBalance(
Account const& account,
EncryptedBalanceType option) const
{
if (!id_)
Throw<std::runtime_error>("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<Account> 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<std::runtime_error>("Account not specified");
jv[jss::TransactionType] = jss::ConfidentialConvert;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("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<std::runtime_error>("Failed to encrypt amount");
// Serialize the ciphertext pair into the buffer
if (!serializeEcPair(c1, c2, buf))
Throw<std::runtime_error>(
"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<std::runtime_error>(
"Failed to convert into individual EC components");
if (!secp256k1_elgamal_decrypt(
secp256k1Context(),
&decryptedAmt,
&c1,
&c2,
getPrivKey(account).data()))
Throw<std::runtime_error>("Failed to decrypt amount");
return decryptedAmt;
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -27,6 +27,8 @@
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
namespace ripple {
namespace test {
namespace jtx {
@@ -145,6 +147,22 @@ struct MPTSet
std::optional<std::string> metadata = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<uint256> domainID = std::nullopt;
std::optional<Buffer> pubKey = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTConvert
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Buffer> holderPubKey = std::nullopt;
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<TER> err = std::nullopt;
};
@@ -155,8 +173,16 @@ class MPTTester
std::unordered_map<std::string, Account> const holders_;
std::optional<MPTID> id_;
bool close_;
std::unordered_map<AccountID, Buffer> pubKeys;
std::unordered_map<AccountID, Buffer> 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<uint256> 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<Buffer>
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<SLE const>;
bool

View File

@@ -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 <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/ConfidentialConvert.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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

View File

@@ -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;
}

View File

@@ -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;