support ConfidentialSend (#5921)

This commit is contained in:
yinyiqian1
2025-10-22 12:02:00 -04:00
committed by GitHub
parent 18d92058e3
commit 8e9cb3c1da
9 changed files with 901 additions and 3 deletions

View File

@@ -133,6 +133,9 @@ serializeEcPair(
TER
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out);
TER
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out);
TER
proveEquality(
Slice const& proof,
@@ -150,6 +153,20 @@ encryptCanonicalZeroAmount(
Slice const& pubKeySlice,
AccountID const& account,
MPTID const& mptId);
TER
verifyConfidentialSendProof(
Slice const& proof,
Slice const& encSenderBalance,
Slice const& encSenderAmt,
Slice const& encDestAmt,
Slice const& encIssuerAmt,
Slice const& senderPubKey,
Slice const& destPubKey,
Slice const& issuerPubKey,
std::uint32_t const version,
uint256 const& txHash);
} // namespace ripple
#endif

View File

@@ -294,6 +294,8 @@ TYPED_SFIELD(sfHolderElGamalPublicKey, VL, 36)
TYPED_SFIELD(sfZKProof, VL, 37)
TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38)
TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39)
TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40)
TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -990,6 +990,22 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 74, ConfidentialConvertBack,
{sfZKProof, soeREQUIRED},
}))
#if TRANSACTION_INCLUDE
#include <xrpld/app/tx/detail/ConfidentialSend.h>
#endif
TRANSACTION(ttCONFIDENTIAL_SEND, 75, ConfidentialSend,
Delegation::delegatable,
featureConfidentialTransfer,
noPriv,
({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfSenderEncryptedAmount, soeREQUIRED},
{sfDestinationEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html

View File

@@ -401,6 +401,40 @@ homomorphicAdd(Slice const& a, Slice const& b, Buffer& out)
return tesSUCCESS;
}
TER
homomorphicSubtract(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 diff_c1;
secp256k1_pubkey diff_c2;
if (secp256k1_elgamal_subtract(
secp256k1Context(),
&diff_c1,
&diff_c2,
&a_c1,
&a_c2,
&b_c1,
&b_c2) != 1)
return tecINTERNAL;
if (!serializeEcPair(diff_c1, diff_c2, out))
return tecINTERNAL;
return tesSUCCESS;
}
TER
proveEquality(
Slice const& proof,
@@ -510,4 +544,65 @@ encryptCanonicalZeroAmount(
return buf;
}
TER
verifyConfidentialSendProof(
Slice const& proof,
Slice const& encSenderBalance,
Slice const& encSenderAmt,
Slice const& encDestAmt,
Slice const& encIssuerAmt,
Slice const& senderPubKey,
Slice const& destPubKey,
Slice const& issuerPubKey,
std::uint32_t const version,
uint256 const& txHash)
{
// if (proof.length() != ecConfidentialSendProofLength)
// return tecINTERNAL;
secp256k1_pubkey bal_c1, bal_c2;
if (!makeEcPair(encSenderBalance, bal_c1, bal_c2))
return tecINTERNAL;
secp256k1_pubkey sender_c1, sender_c2;
if (!makeEcPair(encSenderAmt, sender_c1, sender_c2))
return tecINTERNAL;
secp256k1_pubkey dest_c1, dest_c2;
if (!makeEcPair(encDestAmt, dest_c1, dest_c2))
return tecINTERNAL;
secp256k1_pubkey issuer_c1, issuer_c2;
if (!makeEcPair(encIssuerAmt, issuer_c1, issuer_c2))
return tecINTERNAL;
Serializer s;
s.addRaw(txHash.data(), txHash.bytes);
s.add32(version);
auto const txContextId = s.getSHA512Half();
// todo: equality and range proof verification
// if (secp256k1_equal_range_verify(
// secp256k1Context(),
// reinterpret_cast<unsigned char const*>(proof.data()),
// proof.length(),
// txContextId.data(),
// &bal_c1,
// &bal_c2,
// &sender_c1,
// &sender_c2,
// reinterpret_cast<unsigned char const*>(senderPubKey.data()),
// &dest_c1,
// &dest_c2,
// reinterpret_cast<unsigned char const*>(destPubKey.data()),
// &issuer_c1,
// &issuer_c2,
// reinterpret_cast<unsigned char const*>(issuerPubKey.data()),
// txContextId.data(),
// txContextId.bytes) != 1)
// return tecBAD_PROOF;
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
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
@@ -457,6 +457,343 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.printMPT(bob);
}
void
testSend(FeatureBitset features)
{
testcase("test confidential send");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanLock});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 50);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)});
// Convert 60 out of 100
mptAlice.convert(
{.account = bob,
.amt = 60,
.proof = "123",
.holderPubKey = mptAlice.getPubKey(bob),
.err = tesSUCCESS});
BEAST_EXPECT(mptAlice.getBalance(bob) == 40);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
bob, MPTTester::HOLDER_ENCRYPTED_INBOX) == 60);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
bob, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
bob, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 60);
// bob merge inbox
mptAlice.mergeInbox({
.account = bob,
});
mptAlice.convert(
{.account = carol,
.amt = 20,
.proof = "123",
.holderPubKey = mptAlice.getPubKey(carol),
.err = tesSUCCESS});
BEAST_EXPECT(mptAlice.getBalance(carol) == 30);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
carol, MPTTester::HOLDER_ENCRYPTED_INBOX) == 20);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
carol, MPTTester::HOLDER_ENCRYPTED_SPENDING) == 0);
BEAST_EXPECT(
mptAlice.getDecryptedBalance(
carol, MPTTester::ISSUER_ENCRYPTED_BALANCE) == 20);
// carol merge inbox
mptAlice.mergeInbox({
.account = carol,
});
// bob sends 10 to carol
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10, // will be encrypted internally
.proof = "123",
.err = tesSUCCESS});
// bob sends 1 to carol again
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 1,
.proof = "123",
.err = tesSUCCESS});
mptAlice.mergeInbox({
.account = carol,
});
// carol sends 15 backto bob
mptAlice.send(
{.account = carol,
.dest = bob,
.amt = 15,
.proof = "123",
.err = tesSUCCESS});
}
void
testSendPreflight(FeatureBitset features)
{
testcase("test ConfidentialSend Preflight");
using namespace test::jtx;
// test disabled
{
Env env{*this, features - featureConfidentialTransfer};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
// Basic setup just to have accounts and MPT ID
mptAlice.create();
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
env.close();
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = "123",
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temDISABLED});
}
// test malformed
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create();
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.set(
{.account = alice, .pubKey = mptAlice.getPubKey(alice)});
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 50);
env.close();
// issuer can not be the same as sender
mptAlice.send(
{.account = alice, // Issuer is sender
.dest = carol,
.amt = 10,
.proof = "123",
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
// can not send to self
mptAlice.send(
{.account = bob,
.dest = bob,
.amt = 10,
.proof = "123",
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
// sender encrypted amount wrong length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = "123",
.senderEncryptedAmt = Buffer(10),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
// dest encrypted amount wrong length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = "123",
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt = Buffer(10), // Incorrect length
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
// issuer encrypted amount wrong length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = "123",
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt = Buffer(10),
.err = temMALFORMED});
// todo: proof length check
}
}
void
testSendPreclaim(FeatureBitset features)
{
testcase("test ConfidentialSend Preclaim");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
Account const dave("dave");
Account const eve("eve");
MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave, eve}});
// authorize bob, carol, dave (not eve)
mptAlice.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.authorize({.account = dave});
env.close();
// fund bob, carol (not dave or eve)
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 50);
env.close();
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.generateKeyPair(dave);
mptAlice.set({.account = alice, .pubKey = mptAlice.getPubKey(alice)});
env.close();
// bob and carol convert some funds to confidential
mptAlice.convert(
{.account = bob,
.amt = 60,
.proof = "123",
.holderPubKey = mptAlice.getPubKey(bob),
.err = tesSUCCESS});
mptAlice.convert(
{.account = carol,
.amt = 20,
.proof = "123",
.holderPubKey = mptAlice.getPubKey(carol),
.err = tesSUCCESS});
// // sender does not exist
// {
// Json::Value jv;
// jv[jss::Account] = Account("unknown").human();
// jv[jss::Destination] = carol.human();
// jv[jss::TransactionType] = jss::ConfidentialSend;
// jv[jss::Sequence] = 1;
// jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
// jv[sfSenderEncryptedAmount.jsonName] =
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
// jv[sfDestinationEncryptedAmount.jsonName] =
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
// jv[sfIssuerEncryptedAmount.jsonName] =
// strHex(Buffer(ripple::ecGamalEncryptedTotalLength));
// jv[sfZKProof.jsonName] = "123";
// env(jv, ter(terNO_ACCOUNT));
// env.close();
// }
// destination does not exist
{
Account const unknown("unknown");
mptAlice.send(
{.account = bob,
.dest = unknown,
.amt = 10,
.proof = "123",
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = tecNO_TARGET});
}
// dave exists, but has no confidential fields (never converted)
{
mptAlice.send(
{.account = bob,
.dest = dave,
.amt = 10,
.proof = "123",
.err = tecNO_PERMISSION});
mptAlice.send(
{.account = dave,
.dest = carol,
.amt = 10,
.proof = "123",
.err = tecNO_PERMISSION});
}
// destination exists but has no MPT object.
{
mptAlice.send(
{.account = bob,
.dest = eve,
.amt = 10,
.proof = "123",
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = tecOBJECT_NOT_FOUND});
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -466,6 +803,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
testMergeInbox(features);
testSetPreflight(features);
// ConfidentialSend
testSend(features);
testSendPreflight(features);
testSendPreclaim(features);
}
public:

View File

@@ -666,8 +666,8 @@ MPTTester::convert(MPTConvert const& arg)
uint64_t postSpendingBalance =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
std::cout << "\n postIssuerBalance is " << postIssuerBalance << '\n';
std::cout << "\n postInboxBalance is " << postInboxBalance << '\n';
// std::cout << "\n postIssuerBalance is " << postIssuerBalance << '\n';
// std::cout << "\n postInboxBalance is " << postInboxBalance << '\n';
// spending balance should not change
env_.require(requireAny([&]() -> bool {
@@ -708,6 +708,135 @@ MPTTester::convert(MPTConvert const& arg)
}
}
void
MPTTester::send(MPTConfidentialSend const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
Throw<std::runtime_error>("Account not specified");
if (arg.dest)
jv[sfDestination] = arg.dest->human();
else
Throw<std::runtime_error>("Destination not specified");
jv[jss::TransactionType] = jss::ConfidentialSend;
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_);
}
// Generate the encrypted amounts if not provided
if (arg.senderEncryptedAmt)
jv[sfSenderEncryptedAmount.jsonName] = strHex(*arg.senderEncryptedAmt);
else
jv[sfSenderEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.account, *arg.amt));
if (arg.destEncryptedAmt)
jv[sfDestinationEncryptedAmount.jsonName] =
strHex(*arg.destEncryptedAmt);
else
jv[sfDestinationEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.dest, *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 senderPubAmt = getBalance(*arg.account);
auto const destPubAmt = getBalance(*arg.dest);
auto const prevCOA = getIssuanceConfidentialBalance();
auto const prevOA = getIssuanceOutstandingBalance();
// Sender's previous confidential state
uint64_t prevSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t prevSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
// Destination's previous confidential state
uint64_t prevDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
uint64_t prevDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
uint64_t prevDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
auto const postOA = getIssuanceOutstandingBalance();
// Sender's post confidential state
uint64_t postSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
uint64_t postSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
uint64_t postSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
// Destination's post confidential state
uint64_t postDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
uint64_t postDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
uint64_t postDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
// Public balances unchanged
env_.require(mptbalance(*this, *arg.account, senderPubAmt));
env_.require(mptbalance(*this, *arg.dest, destPubAmt));
// OA and COA unchanged
env_.require(requireAny([&]() -> bool { return prevOA == postOA; }));
env_.require(requireAny([&]() -> bool { return prevCOA == postCOA; }));
// Verify sender changes
env_.require(requireAny([&]() -> bool {
return prevSenderSpending >= *arg.amt &&
postSenderSpending == prevSenderSpending - *arg.amt;
}));
env_.require(requireAny(
[&]() -> bool { return postSenderInbox == prevSenderInbox; }));
env_.require(requireAny([&]() -> bool {
return prevSenderIssuer >= *arg.amt &&
postSenderIssuer == prevSenderIssuer - *arg.amt;
}));
// Verify destination changes
env_.require(requireAny([&]() -> bool {
return postDestInbox == prevDestInbox + *arg.amt;
}));
env_.require(requireAny(
[&]() -> bool { return postDestSpending == prevDestSpending; }));
env_.require(requireAny([&]() -> bool {
return postDestIssuer == prevDestIssuer + *arg.amt;
}));
// Cross checks
env_.require(requireAny([&]() -> bool {
return postSenderInbox + postSenderSpending == postSenderIssuer;
}));
env_.require(requireAny([&]() -> bool {
return postDestInbox + postDestSpending == postDestIssuer;
}));
}
}
void
MPTTester::generateKeyPair(Account const& account)
{
@@ -824,6 +953,21 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg)
}
}
std::int64_t
MPTTester::getIssuanceOutstandingBalance() const
{
if (!id_)
Throw<std::runtime_error>("Issuance ID does not exist");
auto const sle = env_.current()->read(keylet::mptIssuance(*id_));
if (!sle || !sle->isFieldPresent(sfOutstandingAmount))
Throw<std::runtime_error>(
"Issuance object does not contain outstanding amount");
return (*sle)[sfOutstandingAmount];
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -176,6 +176,23 @@ struct MPTMergeInbox
std::optional<TER> err = std::nullopt;
};
struct MPTConfidentialSend
{
std::optional<Account> account = std::nullopt;
std::optional<Account> dest = std::nullopt;
std::optional<MPTID> id = std::nullopt;
// amt is to generate encrypted amounts for testing purposes
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Buffer> senderEncryptedAmt = std::nullopt;
std::optional<Buffer> destEncryptedAmt = 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;
};
class MPTTester
{
Env& env_;
@@ -213,6 +230,9 @@ public:
void
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
void
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -313,6 +333,9 @@ public:
Account const& account,
EncryptedBalanceType balanceType) const;
std::int64_t
getIssuanceOutstandingBalance() const;
private:
using SLEP = std::shared_ptr<SLE const>;
bool

View File

@@ -0,0 +1,211 @@
//------------------------------------------------------------------------------
/*
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/ConfidentialSend.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
ConfidentialSend::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureConfidentialTransfer))
return temDISABLED;
auto const account = ctx.tx[sfAccount];
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
// ConfidentialSend only allows holder to holder, holder to second account,
// and second account to holder transfers. So issuer cannot be the sender.
if (account == issuer)
return temMALFORMED;
// Can not send to self
if (account == ctx.tx[sfDestination])
return temMALFORMED;
if (ctx.tx[sfSenderEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfDestinationEncryptedAmount].length() !=
ecGamalEncryptedTotalLength ||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
return temMALFORMED;
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
// return temMALFORMED;
return tesSUCCESS;
}
TER
ConfidentialSend::preclaim(PreclaimContext const& ctx)
{
// Check if sender account exists
auto const account = ctx.tx[sfAccount];
if (!ctx.view.exists(keylet::account(account)))
return terNO_ACCOUNT;
// Check if destination account exists
auto const destination = ctx.tx[sfDestination];
if (!ctx.view.exists(keylet::account(destination)))
return tecNO_TARGET;
// Check if MPT issuance exists
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// Check if issuance allows confidential transfer
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
return tecNO_PERMISSION;
// Check if issuance has issuer ElGamal public key
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// Check sender's MPToken
auto const sleSenderMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleSenderMPToken)
return tecOBJECT_NOT_FOUND;
// Check sender's MPToken has necessary fields for confidential send
if (!sleSenderMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
!sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
!sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// Check destination's MPToken
auto const sleDestinationMPToken =
ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
if (!sleDestinationMPToken)
return tecOBJECT_NOT_FOUND;
// Check destination's MPToken has necessary fields for confidential send
if (!sleDestinationMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
!sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
!sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
return tecNO_PERMISSION;
// todo: check zkproof. equality proof and range proof, combined or separate
// TBD. TER const terProof = verifyConfidentialSendProof(
// ctx.tx[sfZKProof],
// (*sleSender)[sfConfidentialBalanceSpending],
// ctx.tx[sfSenderEncryptedAmount],
// ctx.tx[sfDestinationEncryptedAmount],
// ctx.tx[sfIssuerEncryptedAmount],
// (*sleSender)[sfHolderElGamalPublicKey],
// (*sleDestination)[sfHolderElGamalPublicKey],
// (*sleIssuance)[sfIssuerElGamalPublicKey],
// (*sleSender)[~sfConfidentialBalanceVersion].value_or(0),
// ctx.tx.getTransactionID()
// );
// if (!isTesSuccess(terProof))
// return tecBAD_PROOF;
return tesSUCCESS;
}
TER
ConfidentialSend::doApply()
{
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const account = ctx_.tx[sfAccount];
auto const destination = ctx_.tx[sfDestination];
auto sleSender = view().peek(keylet::mptoken(mptIssuanceID, account));
auto sleDestination =
view().peek(keylet::mptoken(mptIssuanceID, destination));
if (!sleSender || !sleDestination)
return tecINTERNAL;
Slice const senderEc = ctx_.tx[sfSenderEncryptedAmount];
Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount];
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
// Substract from sender's spending balance
{
Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending];
Buffer newSpending(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicSubtract(curSpending, senderEc, newSpending);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfConfidentialBalanceSpending] = newSpending;
}
// Substract from issuer's balance
{
Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicSubtract(curIssuerEnc, issuerEc, newIssuerEnc);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Increment version
(*sleSender)[sfConfidentialBalanceVersion] =
(*sleSender)[sfConfidentialBalanceVersion] + 1;
// Add to destination's inbox balance
{
Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox];
Buffer newInbox(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfConfidentialBalanceInbox] = newInbox;
}
// Add to issuer's balance
{
Slice const curIssuerEnc = (*sleDestination)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
homomorphicAdd(curIssuerEnc, issuerEc, newIssuerEnc);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
view().update(sleSender);
view().update(sleDestination);
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_CONFIDENTIALSEND_H_INCLUDED
#define RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class ConfidentialSend : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ConfidentialSend(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
} // namespace ripple
#endif