mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
ConfidentialConvert (#5901)
ConfidentialConvert and some test framework update
This commit is contained in:
114
include/xrpl/protocol/ConfidentialTransfer.h
Normal file
114
include/xrpl/protocol/ConfidentialTransfer.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -362,6 +362,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecPSEUDO_ACCOUNT = 196,
|
||||
tecPRECISION_LOSS = 197,
|
||||
tecNO_DELEGATE_PERMISSION = 198,
|
||||
tecBAD_PROOF = 199
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
375
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal file
375
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal 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,
|
||||
¤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<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
|
||||
@@ -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."),
|
||||
|
||||
206
src/test/app/ConfidentialTransfer_test.cpp
Normal file
206
src/test/app/ConfidentialTransfer_test.cpp
Normal 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
|
||||
45
src/test/jtx/confidentialTransfer.h
Normal file
45
src/test/jtx/confidentialTransfer.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
204
src/xrpld/app/tx/detail/ConfidentialConvert.cpp
Normal file
204
src/xrpld/app/tx/detail/ConfidentialConvert.cpp
Normal 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
|
||||
48
src/xrpld/app/tx/detail/ConfidentialConvert.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialConvert.h
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user