xrpld
Loading...
Searching...
No Matches
ConfidentialTransferExtended_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/ConfidentialTransfer.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/batch.h>
8#include <test/jtx/credentials.h>
9#include <test/jtx/delegate.h>
10#include <test/jtx/deposit.h>
11#include <test/jtx/flags.h>
12#include <test/jtx/mpt.h>
13#include <test/jtx/owners.h>
14#include <test/jtx/ter.h>
15#include <test/jtx/ticket.h>
16
17#include <xrpl/basics/Buffer.h>
18#include <xrpl/basics/base_uint.h>
19#include <xrpl/basics/strHex.h>
20#include <xrpl/beast/unit_test/suite.h>
21#include <xrpl/json/json_value.h>
22#include <xrpl/protocol/ConfidentialTransfer.h>
23#include <xrpl/protocol/Feature.h>
24#include <xrpl/protocol/Indexes.h>
25#include <xrpl/protocol/Protocol.h>
26#include <xrpl/protocol/SField.h>
27#include <xrpl/protocol/TER.h>
28#include <xrpl/protocol/TxFlags.h>
29#include <xrpl/protocol/jss.h>
30
31#include <chrono>
32#include <cstdint>
33#include <string>
34#include <vector>
35
36namespace xrpl {
37
39{
40 void
42 {
43 testcase("Send deposit preauth");
44 using namespace test::jtx;
45
46 // When an account enables lsfDepositAuth (via asfDepositAuth flag),
47 // it requires explicit authorization before accepting incoming payments.
48 //
49 // There are two authorization mechanisms:
50 //
51 // 1. DIRECT ACCOUNT AUTHORIZATION (deposit::auth)
52 // - Bob directly authorizes Carol: deposit::auth(bob, carol)
53 // - Simple 1-to-1 trust relationship
54 // - Carol can send to Bob without credentials
55 //
56 // 2. CREDENTIAL-BASED AUTHORIZATION (deposit::authCredentials)
57 // - A trusted third party (dpIssuer) issues credentials
58 // - Bob authorizes a credential TYPE from an issuer
59 // - Anyone holding that credential can send to Bob
60 // - Requires sender to include credential ID in transaction
61
62 Account const alice("alice");
63 Account const bob("bob");
64 Account const carol("carol");
65 Account const dpIssuer("dpIssuer");
66 char const credType[] = "KYC_VERIFIED";
67
68 // Create and accept credential for an account
69 auto createCredential = [&](Env& env, Account const& subject) -> std::string {
70 env(credentials::create(subject, dpIssuer, credType));
71 env.close();
72 env(credentials::accept(subject, dpIssuer, credType));
73 env.close();
74 auto const jv = credentials::ledgerEntry(env, subject, dpIssuer, credType);
75 return jv[jss::result][jss::index].asString();
76 };
77
78 // TEST 1: Direct Account Authorization
79 {
80 Env env(*this, features);
81 ConfidentialEnv confEnv{
82 env,
83 alice,
84 {{.account = bob, .payAmount = 100, .convertAmount = 50},
85 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
86 auto& mpt = confEnv.mpt;
87 env(fset(bob, asfDepositAuth));
88 env.close();
89
90 // Carol cannot send to Bob without authorization
91 mpt.send({
92 .account = carol,
93 .dest = bob,
94 .amt = 10,
95 .err = tecNO_PERMISSION,
96 });
97
98 // Bob directly authorizes Carol
99 env(deposit::auth(bob, carol));
100 env.close();
101
102 // Now Carol can send to Bob
103 mpt.send({
104 .account = carol,
105 .dest = bob,
106 .amt = 10,
107 });
108 mpt.mergeInbox({
109 .account = bob,
110 });
111
112 // Bob revokes Carol's authorization
113 env(deposit::unauth(bob, carol));
114 env.close();
115
116 // Carol can no longer send to Bob
117 mpt.send({
118 .account = carol,
119 .dest = bob,
120 .amt = 10,
121 .err = tecNO_PERMISSION,
122 });
123 }
124
125 // TEST 2: Credential-Based Authorization
126 {
127 Env env(*this, features);
128 env.fund(XRP(50000), dpIssuer);
129 env.close();
130
131 ConfidentialEnv confEnv{
132 env,
133 alice,
134 {{.account = bob, .payAmount = 100, .convertAmount = 50},
135 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
136 auto& mpt = confEnv.mpt;
137 env(fset(bob, asfDepositAuth));
138 env.close();
139
140 auto const credIdx = createCredential(env, carol);
141
142 // Carol cannot send yet - Bob hasn't authorized this credential type
143 mpt.send({
144 .account = carol,
145 .dest = bob,
146 .amt = 10,
147 .credentials = {{credIdx}},
148 .err = tecNO_PERMISSION,
149 });
150
151 // Bob authorizes the credential type from dpIssuer
152 env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
153 env.close();
154
155 // Carol still cannot send without including credential
156 mpt.send({
157 .account = carol,
158 .dest = bob,
159 .amt = 10,
160 .err = tecNO_PERMISSION,
161 });
162
163 // Carol CAN send when including her credential
164 mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
165 mpt.mergeInbox({
166 .account = bob,
167 });
168 }
169
170 // TEST 3: Direct Auth Takes Precedence Over Credentials
171 {
172 Env env(*this, features);
173 env.fund(XRP(50000), dpIssuer);
174 env.close();
175
176 ConfidentialEnv confEnv{
177 env,
178 alice,
179 {{.account = bob, .payAmount = 100, .convertAmount = 50},
180 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
181 auto& mpt = confEnv.mpt;
182 env(fset(bob, asfDepositAuth));
183 env.close();
184
185 auto const credIdx = createCredential(env, carol);
186
187 // Bob directly authorizes Carol (no credential needed)
188 env(deposit::auth(bob, carol));
189 env.close();
190
191 // Carol can send without credentials (direct auth)
192 mpt.send({
193 .account = carol,
194 .dest = bob,
195 .amt = 10,
196 });
197 mpt.mergeInbox({
198 .account = bob,
199 });
200
201 // Carol can also send WITH credentials (still works)
202 mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
203 mpt.mergeInbox({
204 .account = bob,
205 });
206
207 // Bob revokes direct authorization
208 env(deposit::unauth(bob, carol));
209 env.close();
210
211 // Carol cannot send without credentials anymore
212 mpt.send({
213 .account = carol,
214 .dest = bob,
215 .amt = 10,
216 .err = tecNO_PERMISSION,
217 });
218
219 // But credential-based auth not set up, so this also fails
220 mpt.send({
221 .account = carol,
222 .dest = bob,
223 .amt = 10,
224 .credentials = {{credIdx}},
225 .err = tecNO_PERMISSION,
226 });
227
228 // Bob authorizes the credential type
229 env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
230 env.close();
231
232 // Now Carol can send with credentials
233 mpt.send({.account = carol, .dest = bob, .amt = 10, .credentials = {{credIdx}}});
234 }
235
236 auto const expireTime = 30;
237
238 // Lambda function that returns the credential index after creating a
239 // credential that expires shortly after the current ledger time.
240 auto createExpiringCredential = [&](Env& env, Account const& subject) -> std::string {
241 auto jv = credentials::create(subject, dpIssuer, credType);
242 auto const expiry =
243 env.current()->header().parentCloseTime.time_since_epoch().count() + expireTime;
244 jv[sfExpiration.jsonName] = expiry;
245 env(jv);
246 env.close();
247 env(credentials::accept(subject, dpIssuer, credType));
248 env.close();
249 auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
250 return credentials[jss::result][jss::index].asString();
251 };
252
253 auto credentialDeleted = [&](Env& env, Account const& subject) -> bool {
254 auto const credentials = credentials::ledgerEntry(env, subject, dpIssuer, credType);
255 return credentials[jss::result].isMember(jss::error) &&
256 credentials[jss::result][jss::error] == "entryNotFound";
257 };
258
259 // TEST 4: Expired credential with matching depositPreauth entry.
260 // checkDepositPreauth in preclaim returns tesSUCCESS (the expired
261 // credential still exists and matches the depositPreauth key), so ZK
262 // proofs run. cleanupExpiredCredentials in doApply then removes the
263 // expired credential and returns tecEXPIRED.
264 {
265 Env env(*this, features);
266 env.fund(XRP(50000), dpIssuer);
267 env.close();
268
269 ConfidentialEnv confEnv{
270 env,
271 alice,
272 {{.account = bob, .payAmount = 100, .convertAmount = 50},
273 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
274 auto& mpt = confEnv.mpt;
275 env(fset(bob, asfDepositAuth));
276 env.close();
277
278 auto const credIdx = createExpiringCredential(env, carol);
279
280 // Bob authorizes carol's credential type
281 env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
282 env.close();
283
284 // Advance ledger past credential expiration
285 env.close(std::chrono::seconds(expireTime));
286
287 // Send fails with tecEXPIRED; the expired credential is cleaned up
288 mpt.send({
289 .account = carol,
290 .dest = bob,
291 .amt = 10,
292 .credentials = {{credIdx}},
293 .err = tecEXPIRED,
294 });
295 env.close();
296
297 BEAST_EXPECT(credentialDeleted(env, carol));
298 }
299
300 // TEST 5: Expired credential, destination has no depositAuth.
301 // checkDepositPreauth in preclaim returns tesSUCCESS even with expired credentials,
302 // because we want to keep the checkDepositPreauth part before the expensive proof
303 // verification. cleanupExpiredCredentials in doApply removes the expired credential and
304 // returns tecEXPIRED.
305 {
306 Env env(*this, features);
307 env.fund(XRP(50000), dpIssuer);
308 env.close();
309
310 ConfidentialEnv confEnv{
311 env,
312 alice,
313 {{.account = bob, .payAmount = 100, .convertAmount = 50},
314 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
315 auto& mpt = confEnv.mpt;
316
317 auto const credIdx = createExpiringCredential(env, carol);
318
319 // Advance ledger past credential expiration
320 env.close(std::chrono::seconds(expireTime));
321
322 // Send fails with tecEXPIRED; the expired credential is cleaned up
323 mpt.send({
324 .account = carol,
325 .dest = bob,
326 .amt = 10,
327 .credentials = {{credIdx}},
328 .err = tecEXPIRED,
329 });
330 env.close();
331
332 BEAST_EXPECT(credentialDeleted(env, carol));
333 }
334
335 // TEST 6: Expired credential, depositAuth enabled but credential
336 // not authorized by bob.
337 // checkDepositPreauth in preclaim calls checkDepositPreauth which
338 // finds no match and returns tecNO_PERMISSION. doApply never runs, so
339 // the expired credential is not cleaned up by this transaction. This is
340 // a deliberate tradeoff: allowing doApply to run solely for cleanup
341 // would require bypassing the preclaim short-circuit, forcing every
342 // validator to run the expensive ZK proof verification before
343 // discovering the authorization failure. Expired credentials here will
344 // be cleaned up opportunistically by a future transaction that
345 // references them.
346 {
347 Env env(*this, features);
348 env.fund(XRP(50000), dpIssuer);
349 env.close();
350
351 ConfidentialEnv confEnv{
352 env,
353 alice,
354 {{.account = bob, .payAmount = 100, .convertAmount = 50},
355 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
356 auto& mpt = confEnv.mpt;
357 env(fset(bob, asfDepositAuth));
358 env.close();
359
360 auto const credIdx = createExpiringCredential(env, carol);
361
362 // Advance ledger past credential expiration
363 env.close(std::chrono::seconds(expireTime));
364
365 // Fails with tecNO_PERMISSION.
366 mpt.send({
367 .account = carol,
368 .dest = bob,
369 .amt = 10,
370 .credentials = {{credIdx}},
371 .err = tecNO_PERMISSION,
372 });
373 env.close();
374
375 // Expired credential is not deleted
376 BEAST_EXPECT(!credentialDeleted(env, carol));
377 }
378 }
379
380 void
382 {
383 testcase("Send credential validation");
384 using namespace test::jtx;
385
386 // Tests for credentials::checkFields (preflight) and
387 // credentials::valid (preclaim) validation.
388 //
389 // Preflight checks (temMALFORMED):
390 // - Empty credentials array
391 // - Array size exceeds maxCredentialsArraySize (8)
392 // - Duplicate credential IDs in array
393 //
394 // Preclaim checks (tecBAD_CREDENTIALS):
395 // - Credential doesn't exist
396 // - Credential doesn't belong to source account
397 // - Credential not accepted (lsfAccepted flag not set)
398
399 Account const alice("alice");
400 Account const bob("bob");
401 Account const carol("carol");
402 Account const dpIssuer("dpIssuer");
403 char const credType[] = "KYC";
404
405 // TEST 1: Preflight - Empty Credentials Array
406 {
407 Env env(*this, features);
408 ConfidentialEnv confEnv{
409 env,
410 alice,
411 {{.account = bob, .payAmount = 100, .convertAmount = 50},
412 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
413 auto& mpt = confEnv.mpt;
414
415 mpt.send({
416 .account = carol,
417 .dest = bob,
418 .amt = 10,
419 .credentials = std::vector<std::string>{},
420 .err = temMALFORMED,
421 });
422 }
423
424 // TEST 2: Preflight - Credentials Array Too Large
425 {
426 Env env(*this, features);
427 ConfidentialEnv confEnv{
428 env,
429 alice,
430 {{.account = bob, .payAmount = 100, .convertAmount = 50},
431 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
432 auto& mpt = confEnv.mpt;
433
434 std::vector<std::string> tooManyCredentials;
435 tooManyCredentials.reserve(9);
436 for (int i = 0; i < 9; ++i)
437 tooManyCredentials.push_back(to_string(uint256(i)));
438
439 mpt.send({
440 .account = carol,
441 .dest = bob,
442 .amt = 10,
443 .credentials = tooManyCredentials,
444 .err = temMALFORMED,
445 });
446 }
447
448 // TEST 3: Preflight - Duplicate Credentials
449 {
450 Env env(*this, features);
451 env.fund(XRP(50000), dpIssuer);
452 env.close();
453 ConfidentialEnv confEnv{
454 env,
455 alice,
456 {{.account = bob, .payAmount = 100, .convertAmount = 50},
457 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
458 auto& mpt = confEnv.mpt;
459
460 env(credentials::create(carol, dpIssuer, credType));
461 env.close();
462 env(credentials::accept(carol, dpIssuer, credType));
463 env.close();
464
465 auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType);
466 std::string const credIdx = jv[jss::result][jss::index].asString();
467
468 mpt.send({
469 .account = carol,
470 .dest = bob,
471 .amt = 10,
472 .credentials = {{credIdx, credIdx}},
473 .err = temMALFORMED,
474 });
475 }
476
477 // TEST 4: Preclaim - Credential Doesn't Exist
478 {
479 Env env(*this, features);
480 ConfidentialEnv confEnv{
481 env,
482 alice,
483 {{.account = bob, .payAmount = 100, .convertAmount = 50},
484 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
485 auto& mpt = confEnv.mpt;
486
487 std::string const fakeCredIdx = to_string(uint256(999));
488 mpt.send({
489 .account = carol,
490 .dest = bob,
491 .amt = 10,
492 .credentials = {{fakeCredIdx}},
493 .err = tecBAD_CREDENTIALS,
494 });
495 }
496
497 // TEST 5: Preclaim - Credential Doesn't Belong to Source Account
498 {
499 Env env(*this, features);
500 env.fund(XRP(50000), dpIssuer);
501 env.close();
502 ConfidentialEnv confEnv{
503 env,
504 alice,
505 {{.account = bob, .payAmount = 100, .convertAmount = 50},
506 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
507 auto& mpt = confEnv.mpt;
508
509 // Create credential for BOB (not carol)
510 env(credentials::create(bob, dpIssuer, credType));
511 env.close();
512 env(credentials::accept(bob, dpIssuer, credType));
513 env.close();
514
515 auto const jv = credentials::ledgerEntry(env, bob, dpIssuer, credType);
516 std::string const credIdx = jv[jss::result][jss::index].asString();
517
518 mpt.send({
519 .account = carol,
520 .dest = bob,
521 .amt = 10,
522 .credentials = {{credIdx}},
523 .err = tecBAD_CREDENTIALS,
524 });
525 }
526
527 // TEST 6: Preclaim - Credential Not Accepted
528 {
529 Env env(*this, features);
530 env.fund(XRP(50000), dpIssuer);
531 env.close();
532 ConfidentialEnv confEnv{
533 env,
534 alice,
535 {{.account = bob, .payAmount = 100, .convertAmount = 50},
536 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
537 auto& mpt = confEnv.mpt;
538
539 // Create credential but DON'T accept it
540 env(credentials::create(carol, dpIssuer, credType));
541 env.close();
542
543 auto const jv = credentials::ledgerEntry(env, carol, dpIssuer, credType);
544 std::string const credIdx = jv[jss::result][jss::index].asString();
545
546 mpt.send({
547 .account = carol,
548 .dest = bob,
549 .amt = 10,
550 .credentials = {{credIdx}},
551 .err = tecBAD_CREDENTIALS,
552 });
553 }
554
555 // TEST 7: Preflight - sfCredentialIDs requires featureCredentials.
556 // Even with featureConfidentialTransfer enabled, supplying
557 // CredentialIDs while featureCredentials is disabled must be
558 // rejected in preflight via checkExtraFeatures.
559 {
560 Env env(*this, features - featureCredentials);
561 ConfidentialEnv confEnv{
562 env,
563 alice,
564 {{.account = bob, .payAmount = 100, .convertAmount = 50},
565 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
566 auto& mpt = confEnv.mpt;
567
568 auto constexpr kCredIdx =
569 "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288BE4";
570
571 mpt.send({
572 .account = carol,
573 .dest = bob,
574 .amt = 10,
575 .credentials = {{kCredIdx}},
576 .err = temDISABLED,
577 });
578 }
579 }
580
581 // Bob creates the AMM, but Bob is not the MPT holder checked below.
582 // The AMM has its own pseudo-account (`ammHolder`) that can hold the
583 // public MPT pool balance. That pseudo-account cannot normally
584 // initialize confidential state because the confidential txn's must be
585 // signed by sfAccount, and the AMM pseudo-account has no signing key.
586 // So this is a construction/impossibility test: public AMM MPT state exists
587 // but the corresponding confidential AMM clawback flow is not normally reachable.
588 void
590 {
591 testcase("AMM holder cannot have confidential state");
592 using namespace test::jtx;
593
594 Account const alice("alice");
595 Account const bob("bob");
596
597 for (bool const enablePseudoAccount : {false, true})
598 {
599 Env env{
600 *this,
601 enablePseudoAccount ? features | featureSingleAssetVault
602 : features - featureSingleAssetVault};
603
604 MPTTester mptAlice(env, alice, {.holders = {bob}});
605
606 mptAlice.create({
607 .flags = kMptDexFlags | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
608 });
609 mptAlice.authorize({.account = bob});
610 mptAlice.pay(alice, bob, 1'000);
611
612 mptAlice.generateKeyPair(alice);
613 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
614
615 AMM const amm(env, bob, XRP(100), mptAlice(100));
616 Account const ammHolder("amm", amm.ammAccount());
617 auto const ammSle = env.le(keylet::account(ammHolder.id()));
618
619 BEAST_EXPECT(ammSle && ammSle->isFieldPresent(sfAMMID));
620 BEAST_EXPECT(mptAlice.getBalance(ammHolder) == 100);
621
622 BEAST_EXPECT(!mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedInbox));
623 BEAST_EXPECT(
624 !mptAlice.getEncryptedBalance(ammHolder, MPTTester::holderEncryptedSpending));
625 BEAST_EXPECT(
626 !mptAlice.getEncryptedBalance(ammHolder, MPTTester::issuerEncryptedBalance));
627 BEAST_EXPECT(
628 !mptAlice.getEncryptedBalance(ammHolder, MPTTester::auditorEncryptedBalance));
629
630 mptAlice.confidentialClaw({
631 .account = alice,
632 .holder = ammHolder,
633 .amt = 100,
634 .proof = strHex(gMakeZeroBuffer(kEcClawbackProofLength)),
635 .err = tecNO_PERMISSION,
636 });
637 }
638 }
639
640 // Exercises every Confidential Transfer transaction type (MPTokenIssuanceSet,
641 // Convert, MergeInbox, Send, ConvertBack) using tickets instead of regular account
642 // sequence numbers.
643 void
645 {
646 testcase("Confidential transfer with tickets");
647 using namespace test::jtx;
648
649 Env env{*this, features};
650 Account const alice("alice");
651 Account const bob("bob");
652 Account const carol("carol");
653 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
654
655 mptAlice.create({
656 .ownerCount = 1,
657 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
658 });
659 mptAlice.authorize({.account = bob});
660 mptAlice.authorize({.account = carol});
661 mptAlice.pay(alice, bob, 100);
662 mptAlice.pay(alice, carol, 100);
663
664 mptAlice.generateKeyPair(alice);
665 mptAlice.generateKeyPair(bob);
666 mptAlice.generateKeyPair(carol);
667
668 // MPTokenIssuanceSet with ticket, registers alice's issuer key.
669 {
670 std::uint32_t const ticketSeq = env.seq(alice) + 1;
671 env(ticket::create(alice, 1));
672 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice), .ticketSeq = ticketSeq});
673 }
674
675 // ConfidentialMPTConvert with ticket, first convert registers bob's key.
676 {
677 std::uint32_t const ticketSeq = env.seq(bob) + 1;
678 env(ticket::create(bob, 1));
679 mptAlice.convert({
680 .account = bob,
681 .amt = 50,
682 .holderPubKey = mptAlice.getPubKey(bob),
683 .ticketSeq = ticketSeq,
684 });
685 env.require(MptBalance(mptAlice, bob, 50));
686 }
687
688 // ConfidentialMPTConvert with ticket
689 {
690 std::uint32_t const ticketSeq = env.seq(bob) + 1;
691 env(ticket::create(bob, 1));
692 mptAlice.convert({.account = bob, .amt = 20, .ticketSeq = ticketSeq});
693 env.require(MptBalance(mptAlice, bob, 30));
694 }
695
696 // ConfidentialMPTMergeInbox with ticket.
697 {
698 std::uint32_t const ticketSeq = env.seq(bob) + 1;
699 env(ticket::create(bob, 1));
700 mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq});
701 }
702
703 mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
704 mptAlice.mergeInbox({.account = carol});
705
706 // ConfidentialMPTSend with ticket.
707 {
708 std::uint32_t const ticketSeq = env.seq(bob) + 1;
709 env(ticket::create(bob, 1));
710 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .ticketSeq = ticketSeq});
711 }
712
713 // Merge carol's inbox so her spending balance includes the received send.
714 mptAlice.mergeInbox({.account = carol});
715
716 // ConfidentialMPTConvertBack with ticket.
717 // The convertBack proof context hash must use the ticket sequence.
718 {
719 std::uint32_t const ticketSeq = env.seq(carol) + 1;
720 env(ticket::create(carol, 1));
721 mptAlice.convertBack({.account = carol, .amt = 10, .ticketSeq = ticketSeq});
722 // carol converted 50, received 10 from bob, then converted back 10 → public 60
723 env.require(MptBalance(mptAlice, carol, 60));
724 }
725 }
726
727 // Verifies that cryptographic proofs in Convert transactions are bound to
728 // the ticket sequence rather than the account sequence.
729 // A proof built with the ticket sequence passes.
730 void
732 {
733 testcase("Convert proof binds to ticket sequence");
734 using namespace test::jtx;
735
736 Env env{*this, features};
737 Account const alice("alice");
738 Account const bob("bob");
739 MPTTester mptAlice(env, alice, {.holders = {bob}});
740
741 mptAlice.create({
742 .ownerCount = 1,
743 .holderCount = 0,
744 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
745 });
746 mptAlice.authorize({.account = bob});
747 mptAlice.pay(alice, bob, 100);
748
749 mptAlice.generateKeyPair(alice);
750 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
751 mptAlice.generateKeyPair(bob);
752
753 uint64_t const amt = 30;
754 Buffer const bf = generateBlindingFactor();
755 Buffer const holderCt = mptAlice.encryptAmount(bob, amt, bf);
756 Buffer const issuerCt = mptAlice.encryptAmount(alice, amt, bf);
757
758 std::uint32_t const ticketSeq1 = env.seq(bob) + 1;
759 env(ticket::create(bob, 1));
760
761 // Invalid: Schnorr proof built with the account seq (env.seq(bob)) rather
762 // than the ticket seq (ticketSeq1).
763 {
764 BEAST_EXPECT(env.seq(bob) != ticketSeq1);
765 uint256 const badCtxHash =
766 getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob));
767 auto const badProof = requireOptional(
768 mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
769
770 mptAlice.convert({
771 .account = bob,
772 .amt = amt,
773 .proof = strHex(badProof),
774 .holderPubKey = mptAlice.getPubKey(bob),
775 .holderEncryptedAmt = holderCt,
776 .issuerEncryptedAmt = issuerCt,
777 .blindingFactor = bf,
778 .ticketSeq = ticketSeq1,
779 .err = tecBAD_PROOF,
780 });
781 }
782
783 std::uint32_t const ticketSeq2 = env.seq(bob) + 1;
784 env(ticket::create(bob, 1));
785
786 // Valid: proof auto-generated by convert() using ticketSeq2; context hashes match.
787 mptAlice.convert({
788 .account = bob,
789 .amt = amt,
790 .holderPubKey = mptAlice.getPubKey(bob),
791 .holderEncryptedAmt = holderCt,
792 .issuerEncryptedAmt = issuerCt,
793 .blindingFactor = bf,
794 .ticketSeq = ticketSeq2,
795 });
796 env.require(MptBalance(mptAlice, bob, 70));
797 }
798
799 // Exercises ticket-specific error codes for confidential transfer transactions:
800 void
802 {
803 testcase("test Destination Tag");
804
805 using namespace test::jtx;
806 Env env{*this, features};
807 Account const alice("alice"), bob("bob"), carol("carol");
808 ConfidentialEnv confEnv{
809 env,
810 alice,
811 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
812 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
813 auto& mptAlice = confEnv.mpt;
814
815 // Set RequireDest on carol
816 env(fset(carol, asfRequireDest));
817 env.close();
818
819 // Send without destination tag — rejected
820 mptAlice.send({
821 .account = bob,
822 .dest = carol,
823 .amt = 10,
824 .proof = getTrivialSendProofHex(),
825 .senderEncryptedAmt = getTrivialCiphertext(),
826 .destEncryptedAmt = getTrivialCiphertext(),
827 .issuerEncryptedAmt = getTrivialCiphertext(),
828 .amountCommitment = getTrivialCommitment(),
829 .balanceCommitment = getTrivialCommitment(),
830 .err = tecDST_TAG_NEEDED,
831 });
832
833 // Send with destination tag — succeeds (passes preclaim,
834 // reaches ZKP verification with the real proof)
835 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .destinationTag = 42});
836
837 // Verify the destination tag is in the confirmed transaction
838 auto const tx = env.tx();
839 BEAST_EXPECT(tx);
840 BEAST_EXPECT(tx->isFieldPresent(sfDestinationTag));
841 BEAST_EXPECT((*tx)[sfDestinationTag] == 42);
842
843 env(fclear(carol, asfRequireDest));
844 env.close();
845
846 // Send without destination tag when not required — succeeds
847 mptAlice.mergeInbox({.account = carol});
848 mptAlice.send({.account = bob, .dest = carol, .amt = 10});
849 }
850
851 // terPRE_TICKET when the ticket doesn't exist yet, and tefNO_TICKET when
852 // the ticket has already been consumed or was never created.
853 void
855 {
856 testcase("Confidential transfer ticket errors");
857 using namespace test::jtx;
858
859 Env env{*this, features};
860 Account const alice("alice");
861 Account const bob("bob");
862 MPTTester mptAlice(env, alice, {.holders = {bob}});
863
864 mptAlice.create({
865 .ownerCount = 1,
866 .holderCount = 0,
867 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
868 });
869 mptAlice.authorize({.account = bob});
870 mptAlice.pay(alice, bob, 100);
871
872 mptAlice.generateKeyPair(alice);
873 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
874 mptAlice.generateKeyPair(bob);
875
876 // Give bob an inbox balance so MergeInbox has something to merge.
877 mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)});
878
879 // Use MergeInbox as the confidential transfer transaction under test
880 // so that ticket errors are isolated from cryptographic verification.
881
882 // terPRE_TICKET: ticket sequence is far in the future and hasn't been created.
883 mptAlice.mergeInbox(
884 {.account = bob, .ticketSeq = env.seq(bob) + 100, .err = terPRE_TICKET});
885
886 // Create one ticket and use it successfully.
887 std::uint32_t const ticketSeq = env.seq(bob) + 1;
888 env(ticket::create(bob, 1));
889 mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq});
890
891 // tefNO_TICKET: attempt to reuse the same (already-consumed) ticket.
892 mptAlice.mergeInbox({.account = bob, .ticketSeq = ticketSeq, .err = tefNO_TICKET});
893
894 // tefNO_TICKET: ticket sequence is in the past but was never created.
895 mptAlice.mergeInbox({.account = bob, .ticketSeq = 1, .err = tefNO_TICKET});
896 }
897
898 // Bob sends 100 MPT to Carol. Carol Merge Inbox. Carol sends 50 MPT to Dave.
899 // Inner 3rd txn (Carol sends to Dave) fails because the proof is built with
900 // when Carols's spending balance is 0. (before she received funds from Bob)
901 //
902 // Also tests Bob sending to two recipients (Carol and Dave) in a single
903 // batch. Even though Bob has enough balance for both, the second send's
904 // balance-linkage proof becomes incorrect once inner 1 updates Bob's encrypted
905 // spending, so fails
906 void
908 {
909 testcase("Batch confidential send - merge inbox dependency");
910 using namespace test::jtx;
911
912 {
913 Env env{*this, features};
914 Account const alice("alice");
915 Account const bob("bob");
916 Account const carol("carol");
917 Account const dave("dave");
918
919 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
920 // bob = A (100 spending), carol = B (0), dave = C (0)
921 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
922
923 // Build the batch:
924 // Batch Txn 1 bob -> carol 100 : valid proof, bob spending=100
925 // Batch Txn 2 carol -> mergeInbox : valid JV
926 // Batch Txn 3 carol->dave 50 : Invalid
927 auto const bobSeq = env.seq(bob);
928 auto const carolSeq = env.seq(carol);
929 // 3 signers, Bob, Carol, Dave
930 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3);
931
932 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1);
933 auto const jv2 = mpt.mergeInboxJV({.account = carol});
934 auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 50}, carolSeq + 1);
935
936 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
937 batch::Inner(jv1, bobSeq + 1),
938 batch::Inner(jv2, carolSeq),
939 batch::Inner(jv3, carolSeq + 1),
940 batch::Sig(carol),
941 Ter(tesSUCCESS));
942 env.close();
943
944 // AllOrNothing: inner 3 fails
945 // bob's spending must remain 100; carol's inbox must remain 0.
946 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
947 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
948 }
949
950 // Bob sends to two recipients (Carol and Dave) in one batch.
951 // Bob has 150, enough for both sends individually. However, batch txn 1
952 // changes Bob's encrypted spending on the ledger; batch txn 2 was built
953 // against the old enc(150) so its balance-linkage proof is stale.
954 {
955 Env env{*this, features};
956 Account const alice("alice");
957 Account const bob("bob");
958 Account const carol("carol");
959 Account const dave("dave");
960
961 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
962 setupBatchEnv(mpt, alice, bob, carol, dave, 150, 0);
963
964 // tfAllOrNothing — rejects the whole batch as 2nd txn proof is incorrect
965 {
966 auto const bobSeq = env.seq(bob);
967 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
968
969 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
970 auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
971
972 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
973 batch::Inner(jv1, bobSeq + 1),
974 batch::Inner(jv2, bobSeq + 2),
975 Ter(tesSUCCESS));
976 env.close();
977
978 // Nothing applied: bob stays 150, carol and dave inbox stay 0.
979 BEAST_EXPECT(
980 mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 150);
981 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
982 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
983 }
984
985 // If we change batch mode to be tfIndependent — txn 1 applies, inner 2 fails.
986 {
987 auto const bobSeq = env.seq(bob);
988 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
989
990 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
991 auto const jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
992
993 env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
994 batch::Inner(jv1, bobSeq + 1),
995 batch::Inner(jv2, bobSeq + 2),
996 Ter(tesSUCCESS));
997 env.close();
998
999 // bob 150→100, carol inbox 0→50
1000 BEAST_EXPECT(
1001 mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1002 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 50);
1003 // dave gets nothing
1004 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
1005 }
1006 }
1007
1008 // Now, Bob sends Confidential MPT to 2 accounts in one batch.
1009 // However this time, the second txn proof is calculated using the
1010 // correct encrypted(spending) proof, so it should pass.
1011 {
1012 // bob has exactly enough for both sends.
1013 Env env{*this, features};
1014 Account const alice("alice");
1015 Account const bob("bob");
1016 Account const carol("carol");
1017 Account const dave("dave");
1018
1019 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1020 setupBatchEnv(mpt, alice, bob, carol, dave, 200, 0);
1021
1022 {
1023 auto const bobSeq = env.seq(bob);
1024 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1025
1026 // jv1 is built against the current ledger state (spending=200).
1027 auto const jv1 =
1028 mpt.sendJV({.account = bob, .dest = carol, .amt = 100}, bobSeq + 1);
1029
1030 // Compute post-jv1 state without touching the ledger.
1031 auto const chain1 = mpt.chainAfterSend(bob, 100, jv1);
1032
1033 // jv2 proof is built against predicted spending=100, version=N+1.
1034 auto const jv2 =
1035 mpt.sendJV({.account = bob, .dest = dave, .amt = 100}, bobSeq + 2, chain1);
1036
1037 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1038 batch::Inner(jv1, bobSeq + 1),
1039 batch::Inner(jv2, bobSeq + 2),
1040 Ter(tesSUCCESS));
1041 env.close();
1042
1043 // Both txns applied: bob 200→0, carol inbox=100, dave inbox=100.
1044 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 0);
1045 BEAST_EXPECT(
1046 mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 100);
1047 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 100);
1048 }
1049
1050 // Now Bob has 150, but tries to send two 100 in one batch.
1051 // This fails because Bob doesn't have enough MPT balance.
1052 {
1053 Env env2{*this, features};
1054 Account const alice2("alice");
1055 Account const bob2("bob");
1056 Account const carol2("carol");
1057 Account const dave2("dave");
1058
1059 MPTTester mpt2(env2, alice2, {.holders = {bob2, carol2, dave2}});
1060 setupBatchEnv(mpt2, alice2, bob2, carol2, dave2, 150, 0);
1061
1062 auto const bobSeq = env2.seq(bob2);
1063 auto const batchFee = batch::calcConfidentialBatchFee(env2, 0, 2);
1064
1065 auto const jv1 =
1066 mpt2.sendJV({.account = bob2, .dest = carol2, .amt = 100}, bobSeq + 1);
1067 auto const chain1 = mpt2.chainAfterSend(bob2, 100, jv1);
1068
1069 auto const jv2 =
1070 mpt2.sendJV({.account = bob2, .dest = dave2, .amt = 100}, bobSeq + 2, chain1);
1071
1072 env2(
1073 batch::outer(bob2, bobSeq, batchFee, tfAllOrNothing),
1074 batch::Inner(jv1, bobSeq + 1),
1075 batch::Inner(jv2, bobSeq + 2),
1076 Ter(tesSUCCESS));
1077 env2.close();
1078
1079 // AllOrNothing: inner 2 fails → nothing applied.
1080 BEAST_EXPECT(
1081 mpt2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 150);
1082 BEAST_EXPECT(
1083 mpt2.getDecryptedBalance(carol2, MPTTester::holderEncryptedInbox) == 0);
1084 BEAST_EXPECT(mpt2.getDecryptedBalance(dave2, MPTTester::holderEncryptedInbox) == 0);
1085 }
1086 }
1087 }
1088 void
1090 {
1091 testcase("Batch confidential convert and convertBack");
1092 using namespace test::jtx;
1093
1094 // convert + convertBack in one AllOrNothing batch, both valid.
1095 //
1096 // Bob has regular=50, spending=100.
1097 // jv1: convert 50 regular → inbox (Schnorr proof; does NOT touch spending/version)
1098 // jv2: convertBack 30 spending → regular (proof against spending=100, version=V)
1099 //
1100 // Since jv1 leaves spending and version unchanged, jv2's proof is still
1101 // valid when it executes, so both inner txns succeed.
1102 {
1103 Env env{*this, features};
1104 Account const alice("alice");
1105 Account const bob("bob");
1106 Account const carol("carol");
1107 Account const dave("dave");
1108
1109 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1110 // bob: spending=100, regular=0 after setupBatchEnv;
1111 // pay 50 more to give bob regular MPT to convert in the batch.
1112 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1113 mpt.pay(alice, bob, 50);
1114
1115 auto const bobSeq = env.seq(bob);
1116 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1117
1118 // jv1: convert 50 regular MPT into confidential inbox
1119 auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1);
1120 // jv2: convert 30 spending back to regular MPT
1121 auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 2);
1122
1123 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1124 batch::Inner(jv1, bobSeq + 1),
1125 batch::Inner(jv2, bobSeq + 2),
1126 Ter(tesSUCCESS));
1127 env.close();
1128
1129 // regular (mptAmount): 50 (pre) - 50 (convert) + 30 (convertBack) = 30
1130 // spending balance: 100 - 30 = 70
1131 // inbox: 0 + 50 (from convert) = 50
1132 env.require(MptBalance(mpt, bob, 30));
1133 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70);
1134 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 50);
1135 }
1136
1137 // convert + mergeInbox + convertBack, stale convertBack proof.
1138 //
1139 // jv1: convert 50 regular → inbox
1140 // jv2: mergeInbox (inbox 50 → spending, version V → V+1)
1141 // jv3: convertBack 30 (proof built against spending=100, version=V)
1142 //
1143 // After jv2 applies, spending=150 and version=V+1, so jv3's
1144 // proof is stale. AllOrNothing rejects the whole batch.
1145 {
1146 Env env{*this, features};
1147 Account const alice("alice");
1148 Account const bob("bob");
1149 Account const carol("carol");
1150 Account const dave("dave");
1151
1152 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1153 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1154 mpt.pay(alice, bob, 50);
1155
1156 auto const bobSeq = env.seq(bob);
1157 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 3);
1158
1159 auto const jv1 = mpt.convertJV({.account = bob, .amt = 50}, bobSeq + 1);
1160 auto const jv2 = mpt.mergeInboxJV({.account = bob});
1161 // jv3 proof is built against spending=100, version=V (pre-batch)
1162 auto const jv3 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq + 3);
1163
1164 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1165 batch::Inner(jv1, bobSeq + 1),
1166 batch::Inner(jv2, bobSeq + 2),
1167 batch::Inner(jv3, bobSeq + 3),
1168 Ter(tesSUCCESS));
1169 env.close();
1170
1171 // jv3 fails so nothing is applied.
1172 env.require(MptBalance(mpt, bob, 50));
1173 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1174 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0);
1175 }
1176 }
1177
1178 // Tests a batch containing all four confidential MPT operations, Send,
1179 // Convert, ConvertBack, and MergeInbox in a single AllOrNothing batch.
1180 void
1182 {
1183 testcase("Batch confidential mixed operations");
1184 using namespace test::jtx;
1185
1186 // send(bob→carol) + convert(carol) + convertBack(dave)
1187 // + mergeInbox(carol) in one AllOrNothing batch.
1188 //
1189 // Setup:
1190 // bob: spending=100, regular=0
1191 // carol: spending=0, regular=50
1192 // dave: spending=50, regular=0
1193 //
1194 // After the batch:
1195 // bob spending: 100 -> 70 (sent 30 to carol)
1196 // carol inbox: 0+30(send)+50(convert)=80 -> merged -> spending=80, inbox=0
1197 // dave spending: 50 -> 30; regular: 0 -> 20
1198 {
1199 Env env{*this, features};
1200 Account const alice("alice");
1201 Account const bob("bob");
1202 Account const carol("carol");
1203 Account const dave("dave");
1204
1205 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1206 // bob: spending=100. carol: key registered, spending=0.
1207 // dave: key registered, spending=0 initially.
1208 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1209 // Give carol 50 regular MPT to convert in the batch.
1210 mpt.pay(alice, carol, 50);
1211 // Give dave 50 regular MPT then convert to confidential spending.
1212 mpt.pay(alice, dave, 50);
1213 mpt.convert({.account = dave, .amt = 50});
1214 mpt.mergeInbox({.account = dave});
1215
1216 auto const bobSeq = env.seq(bob);
1217 auto const carolSeq = env.seq(carol);
1218 auto const daveSeq = env.seq(dave);
1219 // 2 extra signers (carol, dave), 4 inner txns
1220 auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 4);
1221
1222 // jv1: bob sends 30 to carol
1223 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1);
1224 // jv2: carol converts her 50 regular MPT to confidential
1225 auto const jv2 = mpt.convertJV({.account = carol, .amt = 50}, carolSeq);
1226 // jv3: dave converts 20 spending back to regular MPT
1227 auto const jv3 = mpt.convertBackJV({.account = dave, .amt = 20}, daveSeq);
1228 // jv4: carol merges inbox into spending
1229 // (inbox = 30 from jv1 + 50 from jv2 = 80 at execution time)
1230 auto const jv4 = mpt.mergeInboxJV({.account = carol});
1231
1232 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1233 batch::Inner(jv1, bobSeq + 1),
1234 batch::Inner(jv2, carolSeq),
1235 batch::Inner(jv3, daveSeq),
1236 batch::Inner(jv4, carolSeq + 1),
1237 batch::Sig(carol, dave),
1238 Ter(tesSUCCESS));
1239 env.close();
1240
1241 // All four applied:
1242 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 70);
1243 // carol's inbox was merged: spending=80, inbox=0
1244 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 80);
1245 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
1246 // dave: spending=30, regular=20
1247 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 30);
1248 env.require(MptBalance(mpt, dave, 20));
1249 }
1250
1251 // bob send + bob convertBack in one AllOrNothing batch.
1252 //
1253 // The Send applies first and increments Bob's version counter.
1254 // The ConvertBack proof was built against the pre-Send (spending=100,
1255 // version=V), so batch txn is rejected.
1256 {
1257 Env env{*this, features};
1258 Account const alice("alice");
1259 Account const bob("bob");
1260 Account const carol("carol");
1261 Account const dave("dave");
1262
1263 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1264 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1265
1266 auto const bobSeq = env.seq(bob);
1267 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1268
1269 // jv1: bob sends 30 to carol (spending 100->70, version V->V+1)
1270 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1);
1271 // jv2: bob convertBack 40 , proof built against spending=100, version=V
1272 auto const jv2 = mpt.convertBackJV({.account = bob, .amt = 40}, bobSeq + 2);
1273
1274 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1275 batch::Inner(jv1, bobSeq + 1),
1276 batch::Inner(jv2, bobSeq + 2),
1277 Ter(tesSUCCESS));
1278 env.close();
1279
1280 // AllOrNothing: jv2 fails (stale proof) → nothing applied.
1281 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1282 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
1283 }
1284 }
1285
1286 // Verifies that batch transactions work correctly when tickets are used instead
1287 // of sequence numbers
1288 void
1290 {
1291 testcase("Batch confidential MPT - all or nothing");
1292 using namespace test::jtx;
1293
1294 Env env{*this, features};
1295 Account const alice("alice");
1296 Account const bob("bob");
1297 Account const carol("carol");
1298 Account const dave("dave");
1299
1300 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1301 // bob=100 spending, carol=60 spending, dave=0
1302 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
1303
1304 // bob sends dave 10, carol sends dave 5, independent, both valid.
1305 {
1306 auto const bobSeq = env.seq(bob);
1307 auto const carolSeq = env.seq(carol);
1308 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1309
1310 auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
1311 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
1312
1313 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1314 batch::Inner(jv1, bobSeq + 1),
1315 batch::Inner(jv2, carolSeq),
1316 batch::Sig(carol),
1317 Ter(tesSUCCESS));
1318 env.close();
1319
1320 // Both txn applied: bob's balance 100→90, carol 60→55, dave inbox 0→15
1321 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
1322 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
1323 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
1324 }
1325 }
1326
1327 void
1329 {
1330 testcase("Batch confidential MPT - only one");
1331 using namespace test::jtx;
1332
1333 Env env{*this, features};
1334 Account const alice("alice");
1335 Account const bob("bob");
1336 Account const carol("carol");
1337 Account const dave("dave");
1338
1339 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1340 // bob=100 spending, carol=60 spending, dave=0
1341 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
1342
1343 // bob sends dave 200 (invalid), carol sends dave 300 (invalid)
1344 {
1345 auto const bobSeq = env.seq(bob);
1346 auto const carolSeq = env.seq(carol);
1347 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1348
1349 // Both proofs fail range check (amount > balance)
1350 auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
1351 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq);
1352
1353 env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne),
1354 batch::Inner(jv1, bobSeq + 1),
1355 batch::Inner(jv2, carolSeq),
1356 batch::Sig(carol),
1357 Ter(tesSUCCESS));
1358 env.close();
1359
1360 // No success found → nothing applied; balances unchanged
1361 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1362 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
1363 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
1364 }
1365
1366 // bob sends dave 200 (invalid), carol sends dave 5 (valid)
1367 {
1368 auto const bobSeq = env.seq(bob);
1369 auto const carolSeq = env.seq(carol);
1370 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1371
1372 auto jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
1373 auto jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
1374
1375 env(batch::outer(bob, bobSeq, batchFee, tfOnlyOne),
1376 batch::Inner(jv1, bobSeq + 1),
1377 batch::Inner(jv2, carolSeq),
1378 batch::Sig(carol),
1379 Ter(tesSUCCESS));
1380 env.close();
1381
1382 // Only carol's send applied: carol 60→55, dave inbox 0→5, bob unchanged
1383 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1384 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
1385 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 5);
1386 }
1387 }
1388
1389 void
1391 {
1392 testcase("Batch confidential MPT - until failure");
1393 using namespace test::jtx;
1394
1395 Env env{*this, features};
1396 Account const alice("alice");
1397 Account const bob("bob");
1398 Account const carol("carol");
1399 Account const dave("dave");
1400
1401 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1402 // bob=100 spending, carol=60 spending, dave=0
1403 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
1404
1405 // first fails → none applied
1406 // Bob sends Dave 200 (invalid — stops immediately)
1407 {
1408 auto const bobSeq = env.seq(bob);
1409 auto const carolSeq = env.seq(carol);
1410 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1411
1412 auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 200}, bobSeq + 1);
1413 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
1414
1415 env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure),
1416 batch::Inner(jv1, bobSeq + 1),
1417 batch::Inner(jv2, carolSeq),
1418 batch::Sig(carol),
1419 Ter(tesSUCCESS));
1420 env.close();
1421
1422 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1423 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
1424 }
1425
1426 // Bob sends dave 10, Carol sends dave 5 — both valid and independent
1427 {
1428 auto const bobSeq = env.seq(bob);
1429 auto const carolSeq = env.seq(carol);
1430 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1431
1432 auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
1433 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq);
1434
1435 env(batch::outer(bob, bobSeq, batchFee, tfUntilFailure),
1436 batch::Inner(jv1, bobSeq + 1),
1437 batch::Inner(jv2, carolSeq),
1438 batch::Sig(carol),
1439 Ter(tesSUCCESS));
1440 env.close();
1441
1442 // Both applied: bob 100→90, carol 60→55, dave inbox 0→15
1443 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
1444 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
1445 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
1446 }
1447 }
1448
1449 void
1451 {
1452 testcase("Batch confidential MPT - independent");
1453 using namespace test::jtx;
1454
1455 Env env{*this, features};
1456 Account const alice("alice");
1457 Account const bob("bob");
1458 Account const carol("carol");
1459 Account const dave("dave");
1460
1461 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1462 // bob=100 spending, carol=60 spending, dave=0
1463 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
1464
1465 // Bob sends dave 10 (valid), Carol sends dave 300
1466 // (invalid), Carol sends Dave 5 (valid). Carol's
1467 // balance is still 60 because the preceding send failed).
1468 {
1469 auto const bobSeq = env.seq(bob);
1470 auto const carolSeq = env.seq(carol);
1471 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 3);
1472
1473 auto const jv1 = mpt.sendJV({.account = bob, .dest = dave, .amt = 10}, bobSeq + 1);
1474
1475 // Carol trying to send dave 300 but own balance only 60
1476 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 300}, carolSeq);
1477 auto const jv3 = mpt.sendJV({.account = carol, .dest = dave, .amt = 5}, carolSeq + 1);
1478
1479 env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
1480 batch::Inner(jv1, bobSeq + 1),
1481 batch::Inner(jv2, carolSeq),
1482 batch::Inner(jv3, carolSeq + 1),
1483 batch::Sig(carol),
1484 Ter(tesSUCCESS));
1485 env.close();
1486
1487 // inner 1 (bob→dave 10) applied: bob 100→90
1488 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 90);
1489 // inner 2 failed (carol not changed), inner 3 applied: carol 60→55
1490 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 55);
1491 // dave inbox: 10 (from bob) + 5 (from carol inner 3) = 15
1492 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 15);
1493 }
1494 }
1495
1496 // Tests batching ConfidentialMPTConvert and a ConfidentialMPTConvertBack
1497 // in the same batch transaction. Because Convert only modifies the inbox
1498 // (never the spending balance or the version counter), a ConvertBack proof
1499 // built against the pre-batch spending balance is still valid when both
1500 // appear in the same batch.
1501 void
1503 {
1504 testcase("Batch confidential MPT with tickets");
1505 using namespace test::jtx;
1506
1507 // outer batch uses a ticket.
1508 // The inner send proofs are still bound to regular account sequences.
1509 {
1510 Env env{*this, features};
1511 Account const alice("alice");
1512 Account const bob("bob");
1513 Account const carol("carol");
1514 Account const dave("dave");
1515
1516 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1517 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1518
1519 // Bob creates one ticket to use for the outer batch.
1520 std::uint32_t const outerTicketSeq = env.seq(bob) + 1;
1521 env(ticket::create(bob, 1));
1522 env.close();
1523
1524 auto const bobSeq = env.seq(bob);
1525 // 0 extra signers: all inner txns are from bob;
1526 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1527
1528 // When the outer uses a ticket (seq=0), inner txns start from bobSeq, bobSeq+1.
1529 // jv2 must use chain state predicted after jv1 since both sends are from bob.
1530 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
1531 auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
1532 auto const jv2 =
1533 mpt.sendJV({.account = bob, .dest = dave, .amt = 20}, bobSeq + 1, chain1);
1534
1535 env(batch::outer(bob, 0, batchFee, tfAllOrNothing),
1536 batch::Inner(jv1, bobSeq),
1537 batch::Inner(jv2, bobSeq + 1),
1538 ticket::Use(outerTicketSeq),
1539 Ter(tesSUCCESS));
1540 env.close();
1541
1542 // Both sends applied: bob 100→40, carol inbox=40, dave inbox=20.
1543 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 40);
1544 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
1545 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 20);
1546 }
1547
1548 // inner transactions each consume their own ticket.
1549 // The send proof context hash must be bound to the ticket sequence, not the
1550 // account sequence. sendJV receives the ticket seq as its `seq` parameter.
1551 {
1552 Env env{*this, features};
1553 Account const alice("alice");
1554 Account const bob("bob");
1555 Account const carol("carol");
1556 Account const dave("dave");
1557
1558 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1559 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1560
1561 // Bob creates two tickets for the two inner sends.
1562 std::uint32_t const ticketSeq1 = env.seq(bob) + 1;
1563 std::uint32_t const ticketSeq2 = env.seq(bob) + 2;
1564 env(ticket::create(bob, 2));
1565 env.close();
1566
1567 auto const bobSeq = env.seq(bob);
1568 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1569
1570 // jv1: proof bound to ticketSeq1.
1571 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, ticketSeq1);
1572 // jv2: proof bound to ticketSeq2, spending state predicted after jv1.
1573 auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
1574 auto const jv2 =
1575 mpt.sendJV({.account = bob, .dest = dave, .amt = 30}, ticketSeq2, chain1);
1576
1577 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1578 batch::Inner(jv1, 0, ticketSeq1),
1579 batch::Inner(jv2, 0, ticketSeq2),
1580 Ter(tesSUCCESS));
1581 env.close();
1582
1583 // Both sends applied: bob 100→30, carol inbox=40, dave inbox=30.
1584 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30);
1585 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
1586 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30);
1587 }
1588
1589 // inner send uses wrong sequence (account seq instead of ticket seq)
1590 {
1591 Env env{*this, features};
1592 Account const alice("alice");
1593 Account const bob("bob");
1594 Account const carol("carol");
1595 Account const dave("dave");
1596
1597 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1598 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1599
1600 std::uint32_t const ticketSeq = env.seq(bob) + 1;
1601 env(ticket::create(bob, 1));
1602 env.close();
1603
1604 auto const bobSeq = env.seq(bob);
1605 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1606
1607 // Proof intentionally built with account seq (bobSeq+1) instead of ticketSeq.
1608 auto const badJV = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1);
1609 auto const jv2 = mpt.mergeInboxJV({.account = bob});
1610
1611 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1612 batch::Inner(badJV, 0, ticketSeq),
1613 batch::Inner(jv2, bobSeq + 1),
1614 Ter(tesSUCCESS));
1615 env.close();
1616
1617 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1618 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
1619 }
1620 }
1621
1622 // Basic tests of confidential transfer through delegation. Verifies that a delegated account
1623 // with the appropriate permissions can execute confidential transfer transactions
1624 // on behalf of the delegator.
1625 void
1627 {
1628 testcase("Confidential transfers through delegation");
1629 using namespace test::jtx;
1630
1631 Env env{*this, features};
1632 Account const alice{"alice"};
1633 Account const bob{"bob"};
1634 Account const carol{"carol"};
1635 Account const dave{"dave"};
1636
1637 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1638 env.fund(XRP(10000), dave);
1639 env.close();
1640
1641 mptAlice.create({
1642 .ownerCount = 1,
1643 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
1644 tfMPTCanHoldConfidentialBalance,
1645 });
1646 mptAlice.authorize({.account = bob});
1647 mptAlice.authorize({.account = carol});
1648 mptAlice.pay(alice, bob, 200);
1649 mptAlice.pay(alice, carol, 100);
1650
1651 mptAlice.generateKeyPair(alice);
1652 mptAlice.generateKeyPair(bob);
1653 mptAlice.generateKeyPair(carol);
1654 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
1655
1656 // Bob delegates Convert, MergeInbox to dave.
1657 env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"}));
1658 env.close();
1659
1660 // Carol has no permission from bob to convert on his behalf.
1661 mptAlice.convert({
1662 .account = bob,
1663 .amt = 10,
1664 .holderPubKey = mptAlice.getPubKey(bob),
1665 .delegate = carol,
1667 });
1668
1669 // Dave executes Convert on behalf of bob, registering bob's key.
1670 mptAlice.convert({
1671 .account = bob,
1672 .amt = 100,
1673 .holderPubKey = mptAlice.getPubKey(bob),
1674 .delegate = dave,
1675 });
1676 env.require(MptBalance(mptAlice, bob, 100));
1677
1678 // Dave executes Convert again on behalf of bob (no key registration).
1679 mptAlice.convert({.account = bob, .amt = 50, .delegate = dave});
1680
1681 // Dave executes MergeInbox on behalf of bob.
1682 mptAlice.mergeInbox({.account = bob, .delegate = dave});
1683
1684 // Carol converts and merge inbox.
1685 mptAlice.convert({
1686 .account = carol,
1687 .amt = 100,
1688 .holderPubKey = mptAlice.getPubKey(carol),
1689 });
1690 mptAlice.mergeInbox({.account = carol});
1691
1692 // Dave does not have permission to send on behalf of bob.
1693 mptAlice.send(
1694 {.account = bob,
1695 .dest = carol,
1696 .amt = 10,
1697 .delegate = dave,
1699
1700 // Bob delegates ConfidentialMPTSend to dave.
1701 env(delegate::set(
1702 bob,
1703 dave,
1704 {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox", "ConfidentialMPTSend"}));
1705 env.close();
1706
1707 // Dave executes Send on behalf of bob.
1708 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
1709 mptAlice.mergeInbox({.account = carol});
1710
1711 // Dave does not have permission to convert back on behalf of bob.
1712 mptAlice.convertBack(
1713 {.account = bob, .amt = 10, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
1714
1715 // Bob delegates ConfidentialMPTConvertBack to dave.
1716 env(delegate::set(
1717 bob,
1718 dave,
1719 {"ConfidentialMPTConvert",
1720 "ConfidentialMPTMergeInbox",
1721 "ConfidentialMPTSend",
1722 "ConfidentialMPTConvertBack"}));
1723 env.close();
1724
1725 // Dave executes ConvertBack on behalf of bob.
1726 mptAlice.convertBack({.account = bob, .amt = 10, .delegate = dave});
1727
1728 // Dave does not have permission to clawback on behalf of alice.
1729 mptAlice.confidentialClaw(
1730 {.holder = bob, .amt = 130, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
1731
1732 // Alice delegates ConfidentialMPTClawback to dave.
1733 env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
1734 env.close();
1735
1736 // Dave executes Clawback on behalf of alice.
1737 mptAlice.confidentialClaw({.holder = bob, .amt = 130, .delegate = dave});
1738 }
1739
1740 // Verifies that revoking delegation prevents further delegated operations.
1741 void
1743 {
1744 testcase("Confidential delegation revocation");
1745 using namespace test::jtx;
1746
1747 Env env{*this, features};
1748 Account const alice{"alice"};
1749 Account const bob{"bob"};
1750 Account const carol{"carol"};
1751
1752 MPTTester mptAlice(env, alice, {.holders = {bob}});
1753 env.fund(XRP(10000), carol);
1754 env.close();
1755
1756 mptAlice.create({
1757 .ownerCount = 1,
1758 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
1759 });
1760 mptAlice.authorize({.account = bob});
1761 mptAlice.pay(alice, bob, 100);
1762
1763 mptAlice.generateKeyPair(alice);
1764 mptAlice.generateKeyPair(bob);
1765 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
1766
1767 // Creating the Delegate SLE consumes one owner reserve slot for bob.
1768 auto const bobOwnersBefore = ownerCount(env, bob);
1769 env(delegate::set(bob, carol, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"}));
1770 env.close();
1771 env.require(Owners(bob, bobOwnersBefore + 1));
1772
1773 // Carol converts and merge inbox on behalf of bob.
1774 mptAlice.convert({
1775 .account = bob,
1776 .amt = 50,
1777 .holderPubKey = mptAlice.getPubKey(bob),
1778 .delegate = carol,
1779 });
1780 mptAlice.mergeInbox({.account = bob, .delegate = carol});
1781
1782 // Bob revokes all permissions, deletes the Delegate SLE, releasing the reserve.
1783 env(delegate::set(bob, carol, std::vector<std::string>{}));
1784 env.close();
1785 env.require(Owners(bob, bobOwnersBefore));
1786
1787 // Carol can no longer convert on behalf of bob.
1788 mptAlice.convert({
1789 .account = bob,
1790 .amt = 30,
1791 .delegate = carol,
1793 });
1794
1795 // Bob can still convert by himself.
1796 mptAlice.convert({.account = bob, .amt = 30});
1797 }
1798
1799 // Verifies that a delegated confidential transfer works correctly when an
1800 // auditor is configured on the issuance.
1801 void
1803 {
1804 testcase("Confidential delegation with auditor");
1805 using namespace test::jtx;
1806
1807 Env env{*this, features};
1808 Account const alice{"alice"};
1809 Account const bob{"bob"};
1810 Account const carol{"carol"};
1811 Account const dave{"dave"};
1812 Account const auditor{"auditor"};
1813
1814 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
1815 env.fund(XRP(10000), dave);
1816 env.close();
1817
1818 mptAlice.create({
1819 .ownerCount = 1,
1820 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
1821 });
1822 mptAlice.authorize({.account = bob});
1823 mptAlice.authorize({.account = carol});
1824 mptAlice.pay(alice, bob, 100);
1825 mptAlice.pay(alice, carol, 100);
1826
1827 mptAlice.generateKeyPair(alice);
1828 mptAlice.generateKeyPair(bob);
1829 mptAlice.generateKeyPair(carol);
1830 mptAlice.generateKeyPair(auditor);
1831 mptAlice.set({
1832 .issuerPubKey = mptAlice.getPubKey(alice),
1833 .auditorPubKey = mptAlice.getPubKey(auditor),
1834 });
1835
1836 // Bob delegates Convert and Send permissions to dave.
1837 env(delegate::set(bob, dave, {"ConfidentialMPTSend", "ConfidentialMPTConvert"}));
1838 env.close();
1839
1840 // Dave converts on behalf of bob.
1841 mptAlice.convert({
1842 .account = bob,
1843 .amt = 50,
1844 .holderPubKey = mptAlice.getPubKey(bob),
1845 .delegate = dave,
1846 });
1847 mptAlice.mergeInbox({.account = bob});
1848
1849 mptAlice.convert({
1850 .account = carol,
1851 .amt = 50,
1852 .holderPubKey = mptAlice.getPubKey(carol),
1853 });
1854 mptAlice.mergeInbox({.account = carol});
1855
1856 // Dave sends on behalf of bob.
1857 mptAlice.send({.account = bob, .dest = carol, .amt = 20, .delegate = dave});
1858 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
1859
1860 // Bob delegates ConvertBack and Send permissions to auditor.
1861 env(delegate::set(bob, auditor, {"ConfidentialMPTSend", "ConfidentialMPTConvertBack"}));
1862 env.close();
1863
1864 // auditor can send and convert back on behalf of bob as well.
1865 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = auditor});
1866 mptAlice.convertBack({.account = bob, .amt = 10, .delegate = auditor});
1867 }
1868
1869 // Verifies that a non-issuer delegating clawback to a third party does not
1870 // allow that party to execute clawback, since clawback is issuer-only.
1871 void
1873 {
1874 testcase("Confidential clawback delegation requires issuer");
1875 using namespace test::jtx;
1876
1877 Env env{*this, features};
1878 Account const alice{"alice"};
1879 Account const bob{"bob"};
1880 Account const carol{"carol"};
1881 Account const dave{"dave"};
1882
1883 ConfidentialEnv const confEnv{
1884 env,
1885 alice,
1886 {{.account = bob, .payAmount = 100, .convertAmount = 50},
1887 {.account = carol, .payAmount = 100, .convertAmount = 100}},
1888 tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance};
1889 auto& mptAlice = confEnv.mpt;
1890 env.fund(XRP(10000), dave);
1891 env.close();
1892
1893 // Bob delegates Clawback permission to dave.
1894 env(delegate::set(bob, dave, {"ConfidentialMPTClawback"}));
1895 env.close();
1896
1897 // Dave attempts clawback on behalf of bob targetting bob, but since bob is not the issuer,
1898 // the transaction should be rejected.
1899 {
1900 json::Value jv;
1901 jv[jss::Account] = bob.human();
1902 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
1903 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
1904 jv[sfHolder] = bob.human();
1905 jv[sfMPTAmount.jsonName] = "50";
1906 jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
1907 env(jv, delegate::As(dave), Ter(temMALFORMED));
1908 }
1909
1910 // Dave attempts clawback on behalf of bob targeting carol, but since bob is not the issuer,
1911 // the transaction should be rejected.
1912 {
1913 json::Value jv;
1914 jv[jss::Account] = bob.human();
1915 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
1916 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
1917 jv[sfHolder] = carol.human();
1918 jv[sfMPTAmount.jsonName] = "100";
1919 jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
1920 env(jv, delegate::As(dave), Ter(temMALFORMED));
1921 }
1922 }
1923
1924 // Batch with delegated ConfidentialMPTSend txs, covering stale and updated inner
1925 // send proofs.
1926 void
1928 {
1929 testcase("Batch ConfidentialMPTSend with delegation");
1930 using namespace test::jtx;
1931
1932 // AllOrNothing: two delegated sends from bob via dave, second proof is
1933 // stale once the first send updates bob's spending, whole batch rolls back.
1934 {
1935 Env env{*this, features};
1936 Account const alice("alice");
1937 Account const bob("bob");
1938 Account const carol("carol");
1939 Account const dave("dave");
1940
1941 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1942 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1943
1944 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
1945 env.close();
1946
1947 auto const bobSeq = env.seq(bob);
1948 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1949
1950 // jv1: proof against spending balance 100
1951 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1);
1952 jv1[jss::Delegate] = dave.human();
1953 // jv2: proof also against spending balance 100, which is stale once jv1 applies
1954 auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
1955 jv2[jss::Delegate] = dave.human();
1956
1957 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1958 batch::Inner(jv1, bobSeq + 1),
1959 batch::Inner(jv2, bobSeq + 2),
1960 Ter(tesSUCCESS));
1961 env.close();
1962
1963 // Stale proof on jv2, AllOrNothing rolls back everything.
1964 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
1965 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
1966 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
1967 }
1968
1969 // AllOrNothing: two delegated sends with correctly chained proofs both apply.
1970 {
1971 Env env{*this, features};
1972 Account const alice("alice");
1973 Account const bob("bob");
1974 Account const carol("carol");
1975 Account const dave("dave");
1976
1977 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1978 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1979
1980 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
1981 env.close();
1982
1983 auto const bobSeq = env.seq(bob);
1984 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
1985
1986 // jv1: proof against spending balance 100.
1987 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq + 1);
1988 jv1[jss::Delegate] = dave.human();
1989 auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
1990 // jv2: proof against predicted spending balance 60.
1991 auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 40}, bobSeq + 2, chain1);
1992 jv2[jss::Delegate] = dave.human();
1993
1994 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1995 batch::Inner(jv1, bobSeq + 1),
1996 batch::Inner(jv2, bobSeq + 2),
1997 Ter(tesSUCCESS));
1998 env.close();
1999
2000 // Both inner tx applied
2001 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 20);
2002 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
2003 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 40);
2004 }
2005 }
2006
2007 // Test missing delegation permission inside a batch.
2008 void
2010 {
2011 testcase("Batch delegation missing permission");
2012 using namespace test::jtx;
2013
2014 // AllOrNothing: dave has no Send permission from bob, so the delegated
2015 // inner send fails. The whole batch rolls back.
2016 {
2017 Env env{*this, features};
2018 Account const alice("alice");
2019 Account const bob("bob");
2020 Account const carol("carol");
2021 Account const dave("dave");
2022
2023 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2024 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2025
2026 // Bob grants dave only MergeInbox, not Send.
2027 env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"}));
2028 env.close();
2029
2030 auto const bobSeq = env.seq(bob);
2031 auto const carolSeq = env.seq(carol);
2032 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
2033
2034 // jv1: direct send from carol (valid proof).
2035 auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2036 // jv2: delegated send, fails because dave has no Send permission.
2037 auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
2038 jv2[jss::Delegate] = dave.human();
2039
2040 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
2041 batch::Inner(jv1, carolSeq),
2042 batch::Inner(jv2, bobSeq + 1),
2043 batch::Sig(carol),
2044 Ter(tesSUCCESS));
2045 env.close();
2046
2047 // jv1 applied in the batch view, then jv2 failed, so
2048 // AllOrNothing discards both inner effects.
2049 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2050 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
2051 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2052 }
2053
2054 // Independent: the delegated confidential send is skipped because lack of permission. The
2055 // send from carol still applies.
2056 {
2057 Env env{*this, features};
2058 Account const alice("alice");
2059 Account const bob("bob");
2060 Account const carol("carol");
2061 Account const dave("dave");
2062
2063 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2064 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2065
2066 // Bob does not grant dave any permissions.
2067 auto const bobSeq = env.seq(bob);
2068 auto const carolSeq = env.seq(carol);
2069 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
2070
2071 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
2072 jv1[jss::Delegate] = dave.human();
2073 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2074
2075 env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
2076 batch::Inner(jv1, bobSeq + 1),
2077 batch::Inner(jv2, carolSeq),
2078 batch::Sig(carol),
2079 Ter(tesSUCCESS));
2080 env.close();
2081
2082 // jv1 failed and jv2 applied.
2083 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2084 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 30);
2085 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30);
2086 }
2087 }
2088
2089 // Test batch outer signer is the delegated account.
2090 void
2092 {
2093 testcase("Test batch delegated send with delegate as outer account");
2094 using namespace test::jtx;
2095
2096 // Dave has delegation permission, but the inner Account is bob.
2097 // Without bob's BatchSigner, the batch is rejected.
2098 {
2099 Env env{*this, features};
2100 Account const alice("alice");
2101 Account const bob("bob");
2102 Account const carol("carol");
2103 Account const dave("dave");
2104
2105 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2106 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
2107
2108 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
2109 env.close();
2110
2111 auto const daveSeq = env.seq(dave);
2112 auto const bobSeq = env.seq(bob);
2113 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
2114
2115 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
2116 jv1[jss::Delegate] = dave.human();
2117 auto const jv2 = mpt.mergeInboxJV({.account = dave});
2118
2119 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2120 batch::Inner(jv1, bobSeq),
2121 batch::Inner(jv2, daveSeq + 1),
2122 Ter(temBAD_SIGNER));
2123 env.close();
2124
2125 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2126 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
2127 }
2128
2129 // Dave submits a mixed batch: bob signs inner tx1, and
2130 // dave is the Delegate account signing for inner tx2.
2131 {
2132 Env env{*this, features};
2133 Account const alice("alice");
2134 Account const bob("bob");
2135 Account const carol("carol");
2136 Account const dave("dave");
2137
2138 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2139 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
2140
2141 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
2142 env.close();
2143
2144 auto const daveSeq = env.seq(dave);
2145 auto const bobSeq = env.seq(bob);
2146 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
2147
2148 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
2149 auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
2150 auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1, chain1);
2151 jv2[jss::Delegate] = dave.human();
2152
2153 // Dave is outer; bob signs because his account appears in inner txns.
2154 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2155 batch::Inner(jv1, bobSeq),
2156 batch::Inner(jv2, bobSeq + 1),
2157 batch::Sig(bob),
2158 Ter(tesSUCCESS));
2159 env.close();
2160
2161 // Both sends applied: bob 100→30, carol inbox=70.
2162 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30);
2163 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70);
2164 }
2165
2166 // Verify the delegator Bob's BatchSigner does not bypass the missing delegation permission.
2167 // The delegated inner send fails.
2168 {
2169 Env env{*this, features};
2170 Account const alice("alice");
2171 Account const bob("bob");
2172 Account const carol("carol");
2173 Account const dave("dave");
2174
2175 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2176 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2177
2178 // Bob does not grant dave any permissions.
2179 auto const daveSeq = env.seq(dave);
2180 auto const bobSeq = env.seq(bob);
2181 auto const carolSeq = env.seq(carol);
2182 auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
2183
2184 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq);
2185 jv1[jss::Delegate] = dave.human();
2186 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2187
2188 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2189 batch::Inner(jv1, bobSeq),
2190 batch::Inner(jv2, carolSeq),
2191 batch::Sig(bob, carol),
2192 Ter(tesSUCCESS));
2193 env.close();
2194
2195 // jv1 fails before jv2 is attempted.
2196 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2197 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
2198 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2199 }
2200 }
2201
2202 // Mixed batch with delegated and non-delegated inner confidential MPT transactions.
2203 void
2205 {
2206 testcase("Batch delegated confidential multiple operations");
2207 using namespace test::jtx;
2208
2209 Env env{*this, features};
2210 Account const alice("alice");
2211 Account const bob("bob");
2212 Account const carol("carol");
2213 Account const dave("dave");
2214 Account const erin("erin");
2215 Account const frank("frank");
2216
2217 MPTTester mpt(env, alice, {.holders = {bob, carol, dave, frank}});
2218 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2219 mpt.pay(alice, bob, 50);
2220 env.fund(XRP(10000), erin);
2221 env.close();
2222
2223 mpt.authorize({.account = frank});
2224 mpt.pay(alice, frank, 40);
2225 mpt.generateKeyPair(frank);
2226
2227 env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTConvertBack"}));
2228 env(delegate::set(carol, erin, {"ConfidentialMPTSend"}));
2229 env(delegate::set(bob, erin, {"ConfidentialMPTMergeInbox"}));
2230 env.close();
2231
2232 auto const daveSeq = env.seq(dave);
2233 auto const bobSeq = env.seq(bob);
2234 auto const carolSeq = env.seq(carol);
2235 auto const frankSeq = env.seq(frank);
2236 auto const batchFee = batch::calcConfidentialBatchFee(env, 3, 6);
2237
2238 // Dave submits the batch. Bob's convert and convertback use Dave as Delegate;
2239 // Carol's send and Bob's mergeInbox use Erin as Delegate. Frank's
2240 // convert and mergeInbox are non-delegated.
2241 auto jv1 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq);
2242 jv1[jss::Delegate] = dave.human();
2243 auto jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1);
2244 jv2[jss::Delegate] = dave.human();
2245 auto jv3 = mpt.sendJV({.account = carol, .dest = bob, .amt = 15}, carolSeq);
2246 jv3[jss::Delegate] = erin.human();
2247 auto const jv4 = mpt.convertJV(
2248 {.account = frank, .amt = 25, .holderPubKey = mpt.getPubKey(frank)}, frankSeq);
2249 auto const jv5 = mpt.mergeInboxJV({.account = frank});
2250 auto jv6 = mpt.mergeInboxJV({.account = bob});
2251 jv6[jss::Delegate] = erin.human();
2252
2253 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2254 batch::Inner(jv1, bobSeq),
2255 batch::Inner(jv2, bobSeq + 1),
2256 batch::Inner(jv3, carolSeq),
2257 batch::Inner(jv4, frankSeq),
2258 batch::Inner(jv5, frankSeq + 1),
2259 batch::Inner(jv6, bobSeq + 2),
2260 batch::Sig(bob, carol, frank),
2261 Ter(tesSUCCESS));
2262 env.close();
2263
2264 env.require(MptBalance(mpt, bob, 60));
2265 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 105);
2266 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0);
2267 env.require(MptBalance(mpt, carol, 0));
2268 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 45);
2269 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
2270 env.require(MptBalance(mpt, frank, 15));
2271 BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedSpending) == 25);
2272 BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedInbox) == 0);
2273 env.require(MptBalance(mpt, dave, 0));
2274 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 0);
2275 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2276 auto const outstandingBalance = mpt.getIssuanceOutstandingBalance();
2277 BEAST_EXPECT(outstandingBalance && *outstandingBalance == 250);
2278 BEAST_EXPECT(mpt.getIssuanceConfidentialBalance() == 175);
2279 }
2280
2281 // Test invalid scenarios for delegation with tickets.
2282 void
2284 {
2285 testcase("Invalid cases for delegation with tickets");
2286 using namespace test::jtx;
2287
2288 Env env{*this, features};
2289 Account const alice("alice");
2290 Account const bob("bob");
2291 Account const carol("carol");
2292 MPTTester mptAlice(env, alice, {.holders = {bob}});
2293 env.fund(XRP(10000), carol);
2294 env.close();
2295
2296 mptAlice.create({
2297 .ownerCount = 1,
2298 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
2299 });
2300 mptAlice.authorize({.account = bob});
2301 mptAlice.pay(alice, bob, 200);
2302
2303 mptAlice.generateKeyPair(alice);
2304 mptAlice.generateKeyPair(bob);
2305 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
2306
2307 // Bob grants carol permissions.
2308 env(delegate::set(bob, carol, {"ConfidentialMPTConvert"}));
2309 env.close();
2310
2311 uint64_t const amt = 10;
2312 auto const bf = generateBlindingFactor();
2313 auto const holderCt = mptAlice.encryptAmount(bob, amt, bf);
2314 auto const issuerCt = mptAlice.encryptAmount(alice, amt, bf);
2315
2316 // Invalid: proof built with wrong ticket sequence (ticketSeq + 1).
2317 {
2318 auto const ticketSeq = env.seq(bob) + 1;
2319 env(ticket::create(bob, 1));
2320
2321 auto const badCtxHash =
2322 getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq + 1);
2323 auto const badProof = requireOptional(
2324 mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
2325
2326 mptAlice.convert({
2327 .account = bob,
2328 .amt = amt,
2329 .proof = strHex(badProof),
2330 .holderPubKey = mptAlice.getPubKey(bob),
2331 .holderEncryptedAmt = holderCt,
2332 .issuerEncryptedAmt = issuerCt,
2333 .blindingFactor = bf,
2334 .delegate = carol,
2335 .ticketSeq = ticketSeq,
2336 .err = tecBAD_PROOF,
2337 });
2338 }
2339
2340 // Invalid: proof built with account sequence instead of ticket sequence.
2341 {
2342 auto const ticketSeq = env.seq(bob) + 1;
2343 env(ticket::create(bob, 1));
2344 auto const badCtxHash = getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob));
2345 auto const badProof = requireOptional(
2346 mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof.");
2347
2348 mptAlice.convert({
2349 .account = bob,
2350 .amt = amt,
2351 .proof = strHex(badProof),
2352 .holderPubKey = mptAlice.getPubKey(bob),
2353 .holderEncryptedAmt = holderCt,
2354 .issuerEncryptedAmt = issuerCt,
2355 .blindingFactor = bf,
2356 .delegate = carol,
2357 .ticketSeq = ticketSeq,
2358 .err = tecBAD_PROOF,
2359 });
2360 }
2361
2362 // Invalid: ticket sequence is far in the future and hasn't been created yet.
2363 {
2364 mptAlice.convert({
2365 .account = bob,
2366 .amt = amt,
2367 .holderPubKey = mptAlice.getPubKey(bob),
2368 .holderEncryptedAmt = holderCt,
2369 .issuerEncryptedAmt = issuerCt,
2370 .blindingFactor = bf,
2371 .delegate = carol,
2372 .ticketSeq = env.seq(bob) + 100,
2373 .err = terPRE_TICKET,
2374 });
2375 }
2376
2377 // Invalid: ticket sequence is in the past but was never created.
2378 {
2379 mptAlice.convert({
2380 .account = bob,
2381 .amt = amt,
2382 .holderPubKey = mptAlice.getPubKey(bob),
2383 .holderEncryptedAmt = holderCt,
2384 .issuerEncryptedAmt = issuerCt,
2385 .blindingFactor = bf,
2386 .delegate = carol,
2387 .ticketSeq = 1,
2388 .err = tefNO_TICKET,
2389 });
2390 }
2391
2392 // Invalid: the delegated account, carol, creates a ticket and uses it.
2393 {
2394 auto const carolTicketSeq = env.seq(carol) + 1;
2395 env(ticket::create(carol, 1));
2396
2397 mptAlice.convert({
2398 .account = bob,
2399 .amt = amt,
2400 .holderPubKey = mptAlice.getPubKey(bob),
2401 .holderEncryptedAmt = holderCt,
2402 .issuerEncryptedAmt = issuerCt,
2403 .blindingFactor = bf,
2404 .delegate = carol,
2405 .ticketSeq = carolTicketSeq,
2406 .err = tefNO_TICKET,
2407 });
2408 }
2409
2410 // Invalid: proof bound to a ticket sequence but submitted without a ticket,
2411 // using account sequence.
2412 {
2413 auto const ticketSeq = env.seq(bob) + 1;
2414 env(ticket::create(bob, 1));
2415
2416 // Build proof using ticketSeq.
2417 auto const ctxHashForTicket =
2418 getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq);
2419 auto const proof = requireOptional(
2420 mptAlice.getSchnorrProof(bob, ctxHashForTicket), "Missing Schnorr Proof.");
2421
2422 // Submit without ticket.
2423 mptAlice.convert({
2424 .account = bob,
2425 .amt = amt,
2426 .proof = strHex(proof),
2427 .holderPubKey = mptAlice.getPubKey(bob),
2428 .holderEncryptedAmt = holderCt,
2429 .issuerEncryptedAmt = issuerCt,
2430 .blindingFactor = bf,
2431 .delegate = carol,
2432 .err = tecBAD_PROOF,
2433 });
2434 }
2435 }
2436
2437 // Verifies that delegation works correctly when the delegating account uses
2438 // tickets instead of regular sequence numbers. The proof must bind to the
2439 // ticket sequence, not the account sequence.
2440 void
2442 {
2443 testcase("Confidential delegation with tickets");
2444 using namespace test::jtx;
2445
2446 Env env{*this, features};
2447 Account const alice("alice");
2448 Account const bob("bob");
2449 Account const carol("carol");
2450 Account const dave("dave");
2451 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
2452 env.fund(XRP(10000), dave);
2453 env.close();
2454
2455 mptAlice.create({
2456 .ownerCount = 1,
2457 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
2458 });
2459 mptAlice.authorize({.account = bob});
2460 mptAlice.authorize({.account = carol});
2461 mptAlice.pay(alice, bob, 200);
2462 mptAlice.pay(alice, carol, 100);
2463
2464 mptAlice.generateKeyPair(alice);
2465 mptAlice.generateKeyPair(bob);
2466 mptAlice.generateKeyPair(carol);
2467 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
2468
2469 // Bob grants dave permissions.
2470 env(delegate::set(
2471 bob,
2472 dave,
2473 {"ConfidentialMPTConvert",
2474 "ConfidentialMPTMergeInbox",
2475 "ConfidentialMPTSend",
2476 "ConfidentialMPTConvertBack"}));
2477 // Alice grants dave permission to clawback on her behalf.
2478 env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
2479 env.close();
2480
2481 // Dave executes Convert on behalf of bob using ticket.
2482 auto ticketSeq = env.seq(bob) + 1;
2483 env(ticket::create(bob, 1));
2484 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2485 mptAlice.convert({
2486 .account = bob,
2487 .amt = 100,
2488 .holderPubKey = mptAlice.getPubKey(bob),
2489 .delegate = dave,
2490 .ticketSeq = ticketSeq,
2491 });
2492 env.require(MptBalance(mptAlice, bob, 100));
2493
2494 // MergeInbox using ticket with delegation.
2495 ticketSeq = env.seq(bob) + 1;
2496 env(ticket::create(bob, 1));
2497 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2498 mptAlice.mergeInbox({.account = bob, .delegate = dave, .ticketSeq = ticketSeq});
2499
2500 // Carol converts and merges inbox to receive from bob.
2501 mptAlice.convert({
2502 .account = carol,
2503 .amt = 50,
2504 .holderPubKey = mptAlice.getPubKey(carol),
2505 });
2506 mptAlice.mergeInbox({.account = carol});
2507
2508 // Send using ticket with delegation.
2509 ticketSeq = env.seq(bob) + 1;
2510 env(ticket::create(bob, 1));
2511 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2512 mptAlice.send({
2513 .account = bob,
2514 .dest = carol,
2515 .amt = 20,
2516 .delegate = dave,
2517 .ticketSeq = ticketSeq,
2518 });
2519
2520 // ConvertBack using ticket with delegation.
2521 ticketSeq = env.seq(bob) + 1;
2522 env(ticket::create(bob, 1));
2523 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2524 mptAlice.convertBack({
2525 .account = bob,
2526 .amt = 10,
2527 .delegate = dave,
2528 .ticketSeq = ticketSeq,
2529 });
2530
2531 // Clawback using ticket with delegation.
2532 ticketSeq = env.seq(alice) + 1;
2533 env(ticket::create(alice, 1));
2534 BEAST_EXPECT(env.seq(alice) != ticketSeq);
2535 mptAlice.confidentialClaw({
2536 .holder = bob,
2537 .amt = 70,
2538 .delegate = dave,
2539 .ticketSeq = ticketSeq,
2540 });
2541 }
2542
2543 void
2545 {
2546 // DepositAuth, credentials, and destination tag interactions.
2547 testSendDepositPreauth(features);
2549 testDestinationTag(features);
2550
2551 // AMM/pseudo-account interaction.
2553
2554 // Ticket interactions.
2555 testWithTickets(features);
2557 testTicketErrors(features);
2558
2559 // Batch interactions.
2560 testBatchConfidentialSend(features);
2563 testBatchAllOrNothing(features);
2564 testBatchOnlyOne(features);
2565 testBatchUntilFailure(features);
2566 testBatchIndependent(features);
2567 testBatchWithTickets(features);
2568
2569 // Permission delegation interactions.
2571 testDelegationRevocation(features);
2572 testDelegationWithAuditor(features);
2574 testBatchDelegatedSend(features);
2579 testDelegationWithTickets(features);
2580 }
2581
2582public:
2583 void
2584 run() override
2585 {
2586 using namespace test::jtx;
2587 FeatureBitset const all{testableAmendments()};
2588
2589 testWithFeats(all);
2590 }
2591};
2592
2593BEAST_DEFINE_TESTSUITE(ConfidentialTransferExtended, app, xrpl);
2594
2595} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
Like std::vector<char> but better.
Definition Buffer.h:16
void testBatchDelegatedSendWithDelegateAsOuterAccount(FeatureBitset features)
void testAMMHolderCannotHaveConfidentialStateClawback(FeatureBitset features)
static T requireOptional(std::optional< T > value, char const *message)
static void setupBatchEnv(test::jtx::MPTTester &mpt, test::jtx::Account const &alice, test::jtx::Account const &bob, test::jtx::Account const &carol, test::jtx::Account const &dave, std::uint64_t bobAmt, std::uint64_t carolAmt)
void send(MPTConfidentialSend const &arg=MPTConfidentialSend{})
Definition mpt.cpp:1321
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_DELEGATE_PERMISSION
Definition TER.h:222
@ terPRE_TICKET
Definition TER.h:218
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
@ tefNO_TICKET
Definition TER.h:175
constexpr std::size_t kEcClawbackProofLength
Length of the ZKProof for ConfidentialMPTClawback.
Definition Protocol.h:361
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
uint256 getConvertContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence)
Generates the context hash for ConfidentialMPTConvert transactions.
Buffer generateBlindingFactor()
Generates a cryptographically secure blinding factor (size=xrpl::kEcBlindingFactorLength).
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
@ temBAD_SIGNER
Definition TER.h:101
@ tecBAD_CREDENTIALS
Definition TER.h:357
@ tecBAD_PROOF
Definition TER.h:366
@ tecEXPIRED
Definition TER.h:312
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDST_TAG_NEEDED
Definition TER.h:307
BaseUInt< 256 > uint256
Definition base_uint.h:562
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:240
T push_back(T... args)
T reserve(T... args)