Add delegation tests for Confidential Transfers (#6729)

This commit is contained in:
yinyiqian1
2026-04-07 17:45:26 -04:00
committed by GitHub
parent 58e38e05f9
commit 0d7bf6948e
2 changed files with 349 additions and 8 deletions

View File

@@ -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<std::string>{}));
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:

View File

@@ -200,6 +200,7 @@ struct MPTConvert
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
@@ -211,6 +212,7 @@ struct MPTMergeInbox
{
std::optional<Account> account = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
@@ -235,6 +237,7 @@ struct MPTConfidentialSend
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Buffer> amountCommitment = std::nullopt;
std::optional<Buffer> balanceCommitment = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
@@ -255,6 +258,7 @@ struct MPTConvertBack
// not an txn param, only used for autofilling
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Buffer> pedersenCommitment = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<std::uint32_t> ticketSeq = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
@@ -269,6 +273,7 @@ struct MPTConfidentialClawback
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint64_t> amt = std::nullopt;
std::optional<std::string> proof = std::nullopt;
std::optional<Account> delegate = 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;
@@ -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<std::uint32_t> 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<Account> 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();