Add tests for confidential delegation with tickets (#6808)

This commit is contained in:
yinyiqian1
2026-04-08 13:20:59 -04:00
committed by GitHub
parent 0d7bf6948e
commit 5bd7ce1dfd
3 changed files with 265 additions and 3 deletions

View File

@@ -7630,6 +7630,264 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
}
}
// Test invalid scenarios for delegation with tickets.
void
testInvalidDelegationWithTickets(FeatureBitset features)
{
testcase("Invalid cases for delegation with tickets");
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 | tfMPTCanClawback,
});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 200);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
// Bob grants carol permissions.
env(delegate::set(bob, carol, {"ConfidentialMPTConvert"}));
env.close();
uint64_t const amt = 10;
auto const bf = generateBlindingFactor();
auto const holderCt = mptAlice.encryptAmount(bob, amt, bf);
auto const issuerCt = mptAlice.encryptAmount(alice, amt, bf);
// Invalid: proof built with wrong ticket sequence (ticketSeq + 1).
{
auto const ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
auto const badCtxHash =
getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq + 1);
auto const badProof = mptAlice.getSchnorrProof(bob, badCtxHash);
BEAST_EXPECT(badProof.has_value());
mptAlice.convert(
{.account = bob,
.amt = amt,
.proof = strHex(*badProof),
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.ticketSeq = ticketSeq,
.err = tecBAD_PROOF});
}
// Invalid: proof built with account sequence instead of ticket sequence.
{
auto const ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
auto const badCtxHash = getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob));
auto const badProof = mptAlice.getSchnorrProof(bob, badCtxHash);
BEAST_EXPECT(badProof.has_value());
mptAlice.convert(
{.account = bob,
.amt = amt,
.proof = strHex(*badProof),
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.ticketSeq = ticketSeq,
.err = tecBAD_PROOF});
}
// Invalid: ticket sequence is far in the future and hasn't been created yet.
{
mptAlice.convert({
.account = bob,
.amt = amt,
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.ticketSeq = env.seq(bob) + 100,
.err = terPRE_TICKET,
});
}
// Invalid: ticket sequence is in the past but was never created.
{
mptAlice.convert({
.account = bob,
.amt = amt,
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.ticketSeq = 1,
.err = tefNO_TICKET,
});
}
// Invalid: the delegated account, carol, creates a ticket and uses it.
{
auto const carolTicketSeq = env.seq(carol) + 1;
env(ticket::create(carol, 1));
mptAlice.convert(
{.account = bob,
.amt = amt,
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.ticketSeq = carolTicketSeq,
.err = tefNO_TICKET});
}
// Invalid: proof bound to a ticket sequence but submitted without a ticket,
// using account sequence.
{
auto const ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
// Build proof using ticketSeq.
auto const ctxHashForTicket =
getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq);
auto const proof = mptAlice.getSchnorrProof(bob, ctxHashForTicket);
BEAST_EXPECT(proof.has_value());
// Submit without ticket.
mptAlice.convert(
{.account = bob,
.amt = amt,
.proof = strHex(*proof),
.holderPubKey = mptAlice.getPubKey(bob),
.holderEncryptedAmt = holderCt,
.issuerEncryptedAmt = issuerCt,
.blindingFactor = bf,
.delegate = carol,
.err = tecBAD_PROOF});
}
}
// Verifies that delegation works correctly when the delegating account uses
// tickets instead of regular sequence numbers. The proof must bind to the
// ticket sequence, not the account sequence.
void
testDelegationWithTickets(FeatureBitset features)
{
testcase("Confidential delegation with tickets");
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 | tfMPTCanConfidentialAmount | tfMPTCanClawback,
});
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 grants dave permissions.
env(delegate::set(
bob,
dave,
{"ConfidentialMPTConvert",
"ConfidentialMPTMergeInbox",
"ConfidentialMPTSend",
"ConfidentialMPTConvertBack"}));
// Alice grants dave permission to clawback on her behalf.
env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
env.close();
// Dave executes Convert on behalf of bob using ticket.
auto ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
BEAST_EXPECT(env.seq(bob) != ticketSeq);
mptAlice.convert({
.account = bob,
.amt = 100,
.holderPubKey = mptAlice.getPubKey(bob),
.delegate = dave,
.ticketSeq = ticketSeq,
});
env.require(mptbalance(mptAlice, bob, 100));
// MergeInbox using ticket with delegation.
ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
BEAST_EXPECT(env.seq(bob) != ticketSeq);
mptAlice.mergeInbox({.account = bob, .delegate = dave, .ticketSeq = ticketSeq});
// Carol converts and merges inbox to receive from bob.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
mptAlice.mergeInbox({.account = carol});
// Send using ticket with delegation.
ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
BEAST_EXPECT(env.seq(bob) != ticketSeq);
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 20,
.delegate = dave,
.ticketSeq = ticketSeq,
});
// ConvertBack using ticket with delegation.
ticketSeq = env.seq(bob) + 1;
env(ticket::create(bob, 1));
BEAST_EXPECT(env.seq(bob) != ticketSeq);
mptAlice.convertBack({
.account = bob,
.amt = 10,
.delegate = dave,
.ticketSeq = ticketSeq,
});
// Clawback using ticket with delegation.
ticketSeq = env.seq(alice) + 1;
env(ticket::create(alice, 1));
BEAST_EXPECT(env.seq(alice) != ticketSeq);
mptAlice.confidentialClaw({
.holder = bob,
.amt = 70,
.delegate = dave,
.ticketSeq = ticketSeq,
});
}
void
testWithFeats(FeatureBitset features)
{
@@ -7702,6 +7960,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
testDelegationRevocation(features);
testDelegationWithAuditor(features);
testDelegationClawbackIssuerOnly(features);
// Delegation with Tickets Tests
testInvalidDelegationWithTickets(features);
testDelegationWithTickets(features);
}
public:

View File

@@ -1479,9 +1479,8 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
jv[sfZKProof] = *arg.proof;
else
{
std::uint32_t const seq = env_.seq(account);
uint256 const contextHash =
getClawbackContextHash(account.id(), *id_, seq, arg.holder->id());
auto const seq = arg.ticketSeq ? *arg.ticketSeq : env_.seq(account);
auto const contextHash = getClawbackContextHash(account.id(), *id_, seq, arg.holder->id());
auto const privKey = getPrivKey(account);
if (!privKey || privKey->size() != ecPrivKeyLength)

View File

@@ -274,6 +274,7 @@ struct MPTConfidentialClawback
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> ticketSeq = 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;