From 5bd7ce1dfd3ac9fcde50b4f6efb9b614943f540e Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Wed, 8 Apr 2026 13:20:59 -0400 Subject: [PATCH] Add tests for confidential delegation with tickets (#6808) --- src/test/app/ConfidentialTransfer_test.cpp | 262 +++++++++++++++++++++ src/test/jtx/impl/mpt.cpp | 5 +- src/test/jtx/mpt.h | 1 + 3 files changed, 265 insertions(+), 3 deletions(-) diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index bf1b4b6151..86cb189f2e 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -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: diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 75a7568907..a3e953cf6f 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -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) diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 096a1a2750..21924a68fe 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -274,6 +274,7 @@ struct MPTConfidentialClawback std::optional amt = std::nullopt; std::optional proof = std::nullopt; std::optional delegate = std::nullopt; + std::optional ticketSeq = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt;