mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-24 15:40:26 +00:00
Add delegation tests for Confidential Transfers (#6729)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user