mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
support ConfidentialSend (#5921)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
211
src/xrpld/app/tx/detail/ConfidentialSend.cpp
Normal file
211
src/xrpld/app/tx/detail/ConfidentialSend.cpp
Normal 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
|
||||
48
src/xrpld/app/tx/detail/ConfidentialSend.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialSend.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_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
|
||||
Reference in New Issue
Block a user