diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 6df4f01397..b31bd3f5ea 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -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 diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 7fa786aba1..0c127e21a9 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -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) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 23f2473f57..8d73890d74 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -990,6 +990,22 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 74, ConfidentialConvertBack, {sfZKProof, soeREQUIRED}, })) +#if TRANSACTION_INCLUDE +#include +#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 diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index fdcf79351e..802d6077f8 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -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(proof.data()), + // proof.length(), + // txContextId.data(), + // &bal_c1, + // &bal_c2, + // &sender_c1, + // &sender_c2, + // reinterpret_cast(senderPubKey.data()), + // &dest_c1, + // &dest_c2, + // reinterpret_cast(destPubKey.data()), + // &issuer_c1, + // &issuer_c2, + // reinterpret_cast(issuerPubKey.data()), + // txContextId.data(), + // txContextId.bytes) != 1) + // return tecBAD_PROOF; + + return tesSUCCESS; +} + } // namespace ripple diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index b61c8b0b29..d9cee0f216 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -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: diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 0478b07cc4..50a2256ba3 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -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("Account not specified"); + + if (arg.dest) + jv[sfDestination] = arg.dest->human(); + else + Throw("Destination not specified"); + + jv[jss::TransactionType] = jss::ConfidentialSend; + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else + { + if (!id_) + Throw("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("Issuance ID does not exist"); + + auto const sle = env_.current()->read(keylet::mptIssuance(*id_)); + + if (!sle || !sle->isFieldPresent(sfOutstandingAmount)) + Throw( + "Issuance object does not contain outstanding amount"); + + return (*sle)[sfOutstandingAmount]; +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index c1558c5dc6..32e37fdeff 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -176,6 +176,23 @@ struct MPTMergeInbox std::optional err = std::nullopt; }; +struct MPTConfidentialSend +{ + std::optional account = std::nullopt; + std::optional dest = std::nullopt; + std::optional id = std::nullopt; + // amt is to generate encrypted amounts for testing purposes + std::optional amt = std::nullopt; + std::optional proof = std::nullopt; + std::optional senderEncryptedAmt = std::nullopt; + std::optional destEncryptedAmt = std::nullopt; + std::optional issuerEncryptedAmt = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional flags = std::nullopt; + std::optional err = std::nullopt; +}; + 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 expected) const; @@ -313,6 +333,9 @@ public: Account const& account, EncryptedBalanceType balanceType) const; + std::int64_t + getIssuanceOutstandingBalance() const; + private: using SLEP = std::shared_ptr; bool diff --git a/src/xrpld/app/tx/detail/ConfidentialSend.cpp b/src/xrpld/app/tx/detail/ConfidentialSend.cpp new file mode 100644 index 0000000000..9c49fd3d0a --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialSend.cpp @@ -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 +#include + +#include +#include +#include +#include +#include +#include + +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 diff --git a/src/xrpld/app/tx/detail/ConfidentialSend.h b/src/xrpld/app/tx/detail/ConfidentialSend.h new file mode 100644 index 0000000000..b86114bb52 --- /dev/null +++ b/src/xrpld/app/tx/detail/ConfidentialSend.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED +#define RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED + +#include + +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