diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index 7977add019..bf1b4b6151 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -7311,6 +7311,325 @@ class ConfidentialTransfer_test : public beast::unit_test::suite mptAlice.mergeInbox({.account = bob, .ticketSeq = 1, .err = tefNO_TICKET}); } + // Basic tests of confidential transfer through delegation. Verifies that a delegated account + // with the appropriate permissions can execute confidential transfer transactions + // on behalf of the delegator. + void + testConfidentialDelegation(FeatureBitset features) + { + testcase("Confidential transfers through delegation"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dave{"dave"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = + tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback | tfMPTCanConfidentialAmount, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 200); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Bob delegates Convert, MergeInbox to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + env.close(); + + // Carol has no permission from bob to convert on his behalf. + mptAlice.convert({ + .account = bob, + .amt = 10, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = carol, + .err = terNO_DELEGATE_PERMISSION, + }); + + // Dave executes Convert on behalf of bob, registering bob's key. + mptAlice.convert({ + .account = bob, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = dave, + }); + env.require(mptbalance(mptAlice, bob, 100)); + + // Dave executes Convert again on behalf of bob (no key registration). + mptAlice.convert({.account = bob, .amt = 50, .delegate = dave}); + + // Dave executes MergeInbox on behalf of bob. + mptAlice.mergeInbox({.account = bob, .delegate = dave}); + + // Carol converts and merge inbox. + mptAlice.convert({ + .account = carol, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Dave does not have permission to send on behalf of bob. + mptAlice.send( + {.account = bob, + .dest = carol, + .amt = 10, + .delegate = dave, + .err = terNO_DELEGATE_PERMISSION}); + + // Bob delegates ConfidentialMPTSend to dave. + env(delegate::set( + bob, + dave, + {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox", "ConfidentialMPTSend"})); + env.close(); + + // Dave executes Send on behalf of bob. + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave}); + mptAlice.mergeInbox({.account = carol}); + + // Dave does not have permission to convert back on behalf of bob. + mptAlice.convertBack( + {.account = bob, .amt = 10, .delegate = dave, .err = terNO_DELEGATE_PERMISSION}); + + // Bob delegates ConfidentialMPTConvertBack to dave. + env(delegate::set( + bob, + dave, + {"ConfidentialMPTConvert", + "ConfidentialMPTMergeInbox", + "ConfidentialMPTSend", + "ConfidentialMPTConvertBack"})); + env.close(); + + // Dave executes ConvertBack on behalf of bob. + mptAlice.convertBack({.account = bob, .amt = 10, .delegate = dave}); + + // Dave does not have permission to clawback on behalf of alice. + mptAlice.confidentialClaw( + {.holder = bob, .amt = 130, .delegate = dave, .err = terNO_DELEGATE_PERMISSION}); + + // Alice delegates ConfidentialMPTClawback to dave. + env(delegate::set(alice, dave, {"ConfidentialMPTClawback"})); + env.close(); + + // Dave executes Clawback on behalf of alice. + mptAlice.confidentialClaw({.holder = bob, .amt = 130, .delegate = dave}); + } + + // Verifies that revoking delegation prevents further delegated operations. + void + testDelegationRevocation(FeatureBitset features) + { + testcase("Confidential delegation revocation"); + 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}}); + env.fund(XRP(10000), carol); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanConfidentialAmount, + }); + mptAlice.authorize({.account = bob}); + mptAlice.pay(alice, bob, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + // Creating the Delegate SLE consumes one owner reserve slot for bob. + auto const bobOwnersBefore = ownerCount(env, bob); + env(delegate::set(bob, carol, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + env.close(); + env.require(owners(bob, bobOwnersBefore + 1)); + + // Carol converts and merge inbox on behalf of bob. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + .delegate = carol, + }); + mptAlice.mergeInbox({.account = bob, .delegate = carol}); + + // Bob revokes all permissions, deletes the Delegate SLE, releasing the reserve. + env(delegate::set(bob, carol, std::vector{})); + env.close(); + env.require(owners(bob, bobOwnersBefore)); + + // Carol can no longer convert on behalf of bob. + mptAlice.convert({ + .account = bob, + .amt = 30, + .delegate = carol, + .err = terNO_DELEGATE_PERMISSION, + }); + + // Bob can still convert by himself. + mptAlice.convert({.account = bob, .amt = 30}); + } + + // Verifies that a delegated confidential transfer works correctly when an + // auditor is configured on the issuance. + void + testDelegationWithAuditor(FeatureBitset features) + { + testcase("Confidential delegation with auditor"); + 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 auditor{"auditor"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanConfidentialAmount, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.generateKeyPair(auditor); + mptAlice.set( + {.issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor)}); + + // Bob delegates Convert and Send permissions to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTSend", "ConfidentialMPTConvert"})); + env.close(); + + // Dave converts on behalf of bob. + mptAlice.convert( + {.account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob), .delegate = dave}); + mptAlice.mergeInbox({.account = bob}); + + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Dave sends on behalf of bob. + mptAlice.send({.account = bob, .dest = carol, .amt = 20, .delegate = dave}); + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave}); + + // Bob delegates ConvertBack and Send permissions to auditor. + env(delegate::set(bob, auditor, {"ConfidentialMPTSend", "ConfidentialMPTConvertBack"})); + env.close(); + + // auditor can send and convert back on behalf of bob as well. + mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = auditor}); + mptAlice.convertBack({.account = bob, .amt = 10, .delegate = auditor}); + } + + // Verifies that a non-issuer delegating clawback to a third party does not + // allow that party to execute clawback, since clawback is issuer-only. + void + testDelegationClawbackIssuerOnly(FeatureBitset features) + { + testcase("Confidential clawback delegation requires issuer"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; + Account const dave{"dave"}; + + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + env.fund(XRP(10000), dave); + env.close(); + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanConfidentialAmount, + }); + mptAlice.authorize({.account = bob}); + mptAlice.authorize({.account = carol}); + mptAlice.pay(alice, bob, 100); + mptAlice.pay(alice, carol, 100); + + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(bob); + mptAlice.generateKeyPair(carol); + mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + + mptAlice.convert({ + .account = carol, + .amt = 100, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Bob delegates Clawback permission to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTClawback"})); + env.close(); + + // Dave attempts clawback on behalf of bob targetting bob, but since bob is not the issuer, + // the transaction should be rejected. + { + Json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfHolder] = bob.human(); + jv[sfMPTAmount.jsonName] = "50"; + jv[sfZKProof.jsonName] = std::string(ecEqualityProofLength * 2, '0'); + env(jv, delegate::as(dave), ter(temMALFORMED)); + } + + // Dave attempts clawback on behalf of bob targeting carol, but since bob is not the issuer, + // the transaction should be rejected. + { + Json::Value jv; + jv[jss::Account] = bob.human(); + jv[jss::TransactionType] = jss::ConfidentialMPTClawback; + jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID()); + jv[sfHolder] = carol.human(); + jv[sfMPTAmount.jsonName] = "100"; + jv[sfZKProof.jsonName] = std::string(ecEqualityProofLength * 2, '0'); + env(jv, delegate::as(dave), ter(temMALFORMED)); + } + } + void testWithFeats(FeatureBitset features) { @@ -7377,6 +7696,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite testWithTickets(features); testConvertTicketProofBinding(features); testTicketErrors(features); + + // Delegation Tests + testConfidentialDelegation(features); + testDelegationRevocation(features); + testDelegationWithAuditor(features); + testDelegationClawbackIssuerOnly(features); } public: diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index a9583355de..096a1a2750 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -200,6 +200,7 @@ struct MPTConvert std::optional auditorEncryptedAmt = std::nullopt; std::optional blindingFactor = std::nullopt; + std::optional delegate = std::nullopt; std::optional ticketSeq = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -211,6 +212,7 @@ struct MPTMergeInbox { std::optional account = std::nullopt; std::optional id = std::nullopt; + std::optional delegate = std::nullopt; std::optional ticketSeq = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -235,6 +237,7 @@ struct MPTConfidentialSend std::optional blindingFactor = std::nullopt; std::optional amountCommitment = std::nullopt; std::optional balanceCommitment = std::nullopt; + std::optional delegate = std::nullopt; std::optional ticketSeq = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -255,6 +258,7 @@ struct MPTConvertBack // not an txn param, only used for autofilling std::optional blindingFactor = std::nullopt; std::optional pedersenCommitment = std::nullopt; + std::optional delegate = std::nullopt; std::optional ticketSeq = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -269,6 +273,7 @@ struct MPTConfidentialClawback std::optional id = std::nullopt; std::optional amt = std::nullopt; std::optional proof = std::nullopt; + std::optional delegate = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; @@ -530,17 +535,28 @@ private: { auto const expectedFlags = txflags(arg.flags.value_or(0)); auto const expectedTer = ter(arg.err.value_or(tesSUCCESS)); + + std::optional ticketSeq; if constexpr (requires { arg.ticketSeq; }) - { - if (arg.ticketSeq) - env_(jv, expectedFlags, expectedTer, ticket::use(*arg.ticketSeq)); - else - env_(jv, expectedFlags, expectedTer); - } + ticketSeq = arg.ticketSeq; + + std::optional delegateAcct; + if constexpr (requires { arg.delegate; }) + delegateAcct = arg.delegate; + + if (ticketSeq && delegateAcct) + env_( + jv, + expectedFlags, + expectedTer, + ticket::use(*ticketSeq), + delegate::as(*delegateAcct)); + else if (ticketSeq) + env_(jv, expectedFlags, expectedTer, ticket::use(*ticketSeq)); + else if (delegateAcct) + env_(jv, expectedFlags, expectedTer, delegate::as(*delegateAcct)); else - { env_(jv, expectedFlags, expectedTer); - } auto const err = env_.ter(); if (close_) env_.close();