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 // ConfidentialMPTConvert is not delegable: attempting to grant it as a
1657 // delegated permission is rejected at preflight of DelegateSet.
1658 env(delegate::set(bob, dave, {"ConfidentialMPTConvert"}), Ter(temMALFORMED));
1659 env.close();
1660
1661 // Bob delegates MergeInbox to dave.
1662 env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"}));
1663 env.close();
1664
1665 // A Convert carrying a Delegate is rejected at preflight because the
1666 // transaction type is not delegable at all.
1667 mptAlice.convert({
1668 .account = bob,
1669 .amt = 10,
1670 .holderPubKey = mptAlice.getPubKey(bob),
1671 .delegate = dave,
1672 .err = temINVALID,
1673 });
1674
1675 // Bob converts, registering bob's key.
1676 mptAlice.convert({
1677 .account = bob,
1678 .amt = 100,
1679 .holderPubKey = mptAlice.getPubKey(bob),
1680 });
1681 env.require(MptBalance(mptAlice, bob, 100));
1682
1683 // Bob converts again (no key registration).
1684 mptAlice.convert({.account = bob, .amt = 50});
1685
1686 // Dave executes MergeInbox on behalf of bob.
1687 mptAlice.mergeInbox({.account = bob, .delegate = dave});
1688
1689 // Carol converts and merge inbox.
1690 mptAlice.convert({
1691 .account = carol,
1692 .amt = 100,
1693 .holderPubKey = mptAlice.getPubKey(carol),
1694 });
1695 mptAlice.mergeInbox({.account = carol});
1696
1697 // Dave does not have permission to send on behalf of bob.
1698 mptAlice.send(
1699 {.account = bob,
1700 .dest = carol,
1701 .amt = 10,
1702 .delegate = dave,
1704
1705 // Bob delegates ConfidentialMPTSend to dave.
1706 env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend"}));
1707 env.close();
1708
1709 // Dave executes Send on behalf of bob.
1710 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
1711 mptAlice.mergeInbox({.account = carol});
1712
1713 // Dave does not have permission to convert back on behalf of bob.
1714 mptAlice.convertBack(
1715 {.account = bob, .amt = 10, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
1716
1717 // Bob delegates ConfidentialMPTConvertBack to dave.
1718 env(delegate::set(
1719 bob,
1720 dave,
1721 {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend", "ConfidentialMPTConvertBack"}));
1722 env.close();
1723
1724 // Dave executes ConvertBack on behalf of bob.
1725 mptAlice.convertBack({.account = bob, .amt = 10, .delegate = dave});
1726
1727 // Dave does not have permission to clawback on behalf of alice.
1728 mptAlice.confidentialClaw(
1729 {.holder = bob, .amt = 130, .delegate = dave, .err = terNO_DELEGATE_PERMISSION});
1730
1731 // Alice delegates ConfidentialMPTClawback to dave.
1732 env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
1733 env.close();
1734
1735 // Dave executes Clawback on behalf of alice.
1736 mptAlice.confidentialClaw({.holder = bob, .amt = 130, .delegate = dave});
1737 }
1738
1739 // Verifies that revoking delegation prevents further delegated operations.
1740 void
1742 {
1743 testcase("Confidential delegation revocation");
1744 using namespace test::jtx;
1745
1746 Env env{*this, features};
1747 Account const alice{"alice"};
1748 Account const bob{"bob"};
1749 Account const carol{"carol"};
1750
1751 MPTTester mptAlice(env, alice, {.holders = {bob}});
1752 env.fund(XRP(10000), carol);
1753 env.close();
1754
1755 mptAlice.create({
1756 .ownerCount = 1,
1757 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
1758 });
1759 mptAlice.authorize({.account = bob});
1760 mptAlice.pay(alice, bob, 100);
1761
1762 mptAlice.generateKeyPair(alice);
1763 mptAlice.generateKeyPair(bob);
1764 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
1765
1766 // Creating the Delegate SLE consumes one owner reserve slot for bob.
1767 auto const bobOwnersBefore = ownerCount(env, bob);
1768 env(delegate::set(bob, carol, {"ConfidentialMPTMergeInbox"}));
1769 env.close();
1770 env.require(Owners(bob, bobOwnersBefore + 1));
1771
1772 // Bob converts; carol merges inbox on behalf of bob.
1773 mptAlice.convert({
1774 .account = bob,
1775 .amt = 50,
1776 .holderPubKey = mptAlice.getPubKey(bob),
1777 });
1778 mptAlice.mergeInbox({.account = bob, .delegate = carol});
1779
1780 // Bob revokes all permissions, deletes the Delegate SLE, releasing the reserve.
1781 env(delegate::set(bob, carol, std::vector<std::string>{}));
1782 env.close();
1783 env.require(Owners(bob, bobOwnersBefore));
1784
1785 // Bob converts again to populate a fresh inbox.
1786 mptAlice.convert({.account = bob, .amt = 30});
1787
1788 // Carol can no longer merge inbox on behalf of bob.
1789 mptAlice.mergeInbox({
1790 .account = bob,
1791 .delegate = carol,
1793 });
1794
1795 // Bob can still merge his inbox.
1796 mptAlice.mergeInbox({.account = bob});
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 Send permission to dave (Convert is not delegable).
1837 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
1838 env.close();
1839
1840 // Bob converts.
1841 mptAlice.convert({
1842 .account = bob,
1843 .amt = 50,
1844 .holderPubKey = mptAlice.getPubKey(bob),
1845 });
1846 mptAlice.mergeInbox({.account = bob});
1847
1848 mptAlice.convert({
1849 .account = carol,
1850 .amt = 50,
1851 .holderPubKey = mptAlice.getPubKey(carol),
1852 });
1853 mptAlice.mergeInbox({.account = carol});
1854
1855 // Dave sends on behalf of bob.
1856 mptAlice.send({.account = bob, .dest = carol, .amt = 20, .delegate = dave});
1857 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = dave});
1858
1859 // Bob delegates ConvertBack and Send permissions to auditor.
1860 env(delegate::set(bob, auditor, {"ConfidentialMPTSend", "ConfidentialMPTConvertBack"}));
1861 env.close();
1862
1863 // auditor can send and convert back on behalf of bob as well.
1864 mptAlice.send({.account = bob, .dest = carol, .amt = 10, .delegate = auditor});
1865 mptAlice.convertBack({.account = bob, .amt = 10, .delegate = auditor});
1866 }
1867
1868 // Verifies that a non-issuer delegating clawback to a third party does not
1869 // allow that party to execute clawback, since clawback is issuer-only.
1870 void
1872 {
1873 testcase("Confidential clawback delegation requires issuer");
1874 using namespace test::jtx;
1875
1876 Env env{*this, features};
1877 Account const alice{"alice"};
1878 Account const bob{"bob"};
1879 Account const carol{"carol"};
1880 Account const dave{"dave"};
1881
1882 ConfidentialEnv const confEnv{
1883 env,
1884 alice,
1885 {{.account = bob, .payAmount = 100, .convertAmount = 50},
1886 {.account = carol, .payAmount = 100, .convertAmount = 100}},
1887 tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance};
1888 auto& mptAlice = confEnv.mpt;
1889 env.fund(XRP(10000), dave);
1890 env.close();
1891
1892 // Bob delegates Clawback permission to dave.
1893 env(delegate::set(bob, dave, {"ConfidentialMPTClawback"}));
1894 env.close();
1895
1896 // Dave attempts clawback on behalf of bob targetting bob, but since bob is not the issuer,
1897 // the transaction should be rejected.
1898 {
1899 json::Value jv;
1900 jv[jss::Account] = bob.human();
1901 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
1902 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
1903 jv[sfHolder] = bob.human();
1904 jv[sfMPTAmount.jsonName] = "50";
1905 jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
1906 env(jv, delegate::As(dave), Ter(temMALFORMED));
1907 }
1908
1909 // Dave attempts clawback on behalf of bob targeting carol, but since bob is not the issuer,
1910 // the transaction should be rejected.
1911 {
1912 json::Value jv;
1913 jv[jss::Account] = bob.human();
1914 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
1915 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
1916 jv[sfHolder] = carol.human();
1917 jv[sfMPTAmount.jsonName] = "100";
1918 jv[sfZKProof.jsonName] = std::string(kEcClawbackProofLength * 2, '0');
1919 env(jv, delegate::As(dave), Ter(temMALFORMED));
1920 }
1921 }
1922
1923 // Batch with delegated ConfidentialMPTSend txs, covering stale and updated inner
1924 // send proofs.
1925 void
1927 {
1928 testcase("Batch ConfidentialMPTSend with delegation");
1929 using namespace test::jtx;
1930
1931 // AllOrNothing: two delegated sends from bob via dave, second proof is
1932 // stale once the first send updates bob's spending, whole batch rolls back.
1933 {
1934 Env env{*this, features};
1935 Account const alice("alice");
1936 Account const bob("bob");
1937 Account const carol("carol");
1938 Account const dave("dave");
1939
1940 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
1941 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
1942
1943 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
1944 env.close();
1945
1946 auto const bobSeq = env.seq(bob);
1947 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
1948
1949 // jv1: proof against spending balance 100
1950 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 60}, bobSeq + 1);
1951 jv1[jss::Delegate] = dave.human();
1952 // jv2: proof also against spending balance 100, which is stale once jv1 applies
1953 auto jv2 = mpt.sendJV({.account = bob, .dest = dave, .amt = 60}, bobSeq + 2);
1954 jv2[jss::Delegate] = dave.human();
1955
1956 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
1957 batch::Inner(jv1, bobSeq + 1),
1958 batch::Inner(jv2, bobSeq + 2),
1959 batch::Sig(dave),
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, 1, 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 batch::Sig(dave),
1998 Ter(tesSUCCESS));
1999 env.close();
2000
2001 // Both inner tx applied
2002 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 20);
2003 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
2004 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 40);
2005 }
2006 }
2007
2008 // Test missing delegation permission inside a batch.
2009 void
2011 {
2012 testcase("Batch delegation missing permission");
2013 using namespace test::jtx;
2014
2015 // AllOrNothing: dave has no Send permission from bob, so the delegated
2016 // inner send fails. The whole batch rolls back.
2017 {
2018 Env env{*this, features};
2019 Account const alice("alice");
2020 Account const bob("bob");
2021 Account const carol("carol");
2022 Account const dave("dave");
2023
2024 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2025 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2026
2027 // Bob grants dave only MergeInbox, not Send.
2028 env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"}));
2029 env.close();
2030
2031 auto const bobSeq = env.seq(bob);
2032 auto const carolSeq = env.seq(carol);
2033 auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
2034
2035 // jv1: direct send from carol (valid proof).
2036 auto const jv1 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2037 // jv2: delegated send, fails because dave has no Send permission.
2038 auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
2039 jv2[jss::Delegate] = dave.human();
2040
2041 env(batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
2042 batch::Inner(jv1, carolSeq),
2043 batch::Inner(jv2, bobSeq + 1),
2044 batch::Sig(carol, dave),
2045 Ter(tesSUCCESS));
2046 env.close();
2047
2048 // jv1 applied in the batch view, then jv2 failed, so
2049 // AllOrNothing discards both inner effects.
2050 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2051 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
2052 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2053 }
2054
2055 // Independent: the delegated confidential send is skipped because lack of permission. The
2056 // send from carol still applies.
2057 {
2058 Env env{*this, features};
2059 Account const alice("alice");
2060 Account const bob("bob");
2061 Account const carol("carol");
2062 Account const dave("dave");
2063
2064 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2065 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2066
2067 // Bob does not grant dave any permissions.
2068 auto const bobSeq = env.seq(bob);
2069 auto const carolSeq = env.seq(carol);
2070 auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
2071
2072 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq + 1);
2073 jv1[jss::Delegate] = dave.human();
2074 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2075
2076 env(batch::outer(bob, bobSeq, batchFee, tfIndependent),
2077 batch::Inner(jv1, bobSeq + 1),
2078 batch::Inner(jv2, carolSeq),
2079 batch::Sig(carol, dave),
2080 Ter(tesSUCCESS));
2081 env.close();
2082
2083 // jv1 failed and jv2 applied.
2084 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2085 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 30);
2086 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 30);
2087 }
2088 }
2089
2090 // Test batch outer signer is the delegated account.
2091 void
2093 {
2094 testcase("Test batch delegated send with delegate as outer account");
2095 using namespace test::jtx;
2096
2097 // Dave holds bob's ConfidentialMPTSend delegation and is the outer batch
2098 // signer, so dave's outer signature consents to the delegated inner.
2099 // The batch applies.
2100 {
2101 Env env{*this, features};
2102 Account const alice("alice");
2103 Account const bob("bob");
2104 Account const carol("carol");
2105 Account const dave("dave");
2106
2107 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2108 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
2109
2110 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
2111 env.close();
2112
2113 auto const daveSeq = env.seq(dave);
2114 auto const bobSeq = env.seq(bob);
2115 auto const batchFee = batch::calcConfidentialBatchFee(env, 0, 2);
2116
2117 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
2118 jv1[jss::Delegate] = dave.human();
2119 auto const jv2 = mpt.mergeInboxJV({.account = dave});
2120
2121 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2122 batch::Inner(jv1, bobSeq),
2123 batch::Inner(jv2, daveSeq + 1),
2124 Ter(tesSUCCESS));
2125 env.close();
2126
2127 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 60);
2128 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 40);
2129 }
2130
2131 // Dave submits a mixed batch: bob signs inner tx1, and
2132 // dave is the Delegate account signing for inner tx2.
2133 {
2134 Env env{*this, features};
2135 Account const alice("alice");
2136 Account const bob("bob");
2137 Account const carol("carol");
2138 Account const dave("dave");
2139
2140 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2141 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 0);
2142
2143 env(delegate::set(bob, dave, {"ConfidentialMPTSend"}));
2144 env.close();
2145
2146 auto const daveSeq = env.seq(dave);
2147 auto const bobSeq = env.seq(bob);
2148 auto const batchFee = batch::calcConfidentialBatchFee(env, 1, 2);
2149
2150 auto const jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 40}, bobSeq);
2151 auto const chain1 = mpt.chainAfterSend(bob, 40, jv1);
2152 auto jv2 = mpt.sendJV({.account = bob, .dest = carol, .amt = 30}, bobSeq + 1, chain1);
2153 jv2[jss::Delegate] = dave.human();
2154
2155 // Dave is outer; bob signs because his account appears in inner txns.
2156 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2157 batch::Inner(jv1, bobSeq),
2158 batch::Inner(jv2, bobSeq + 1),
2159 batch::Sig(bob),
2160 Ter(tesSUCCESS));
2161 env.close();
2162
2163 // Both sends applied: bob 100→30, carol inbox=70.
2164 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 30);
2165 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 70);
2166 }
2167
2168 // The delegated inner's required signer is the delegate (dave), not bob.
2169 // Bob signs but is not a required signer, so the batch is rejected as an
2170 // extra signer. The delegator's signature cannot stand in for the
2171 // delegate's.
2172 {
2173 Env env{*this, features};
2174 Account const alice("alice");
2175 Account const bob("bob");
2176 Account const carol("carol");
2177 Account const dave("dave");
2178
2179 MPTTester mpt(env, alice, {.holders = {bob, carol, dave}});
2180 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2181
2182 // Bob does not grant dave any permissions.
2183 auto const daveSeq = env.seq(dave);
2184 auto const bobSeq = env.seq(bob);
2185 auto const carolSeq = env.seq(carol);
2186 auto const batchFee = batch::calcConfidentialBatchFee(env, 2, 2);
2187
2188 auto jv1 = mpt.sendJV({.account = bob, .dest = carol, .amt = 50}, bobSeq);
2189 jv1[jss::Delegate] = dave.human();
2190 auto const jv2 = mpt.sendJV({.account = carol, .dest = dave, .amt = 30}, carolSeq);
2191
2192 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2193 batch::Inner(jv1, bobSeq),
2194 batch::Inner(jv2, carolSeq),
2195 batch::Sig(bob, carol),
2196 Ter(temBAD_SIGNER));
2197 env.close();
2198
2199 // jv1 fails before jv2 is attempted.
2200 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
2201 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 60);
2202 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2203 }
2204 }
2205
2206 // Mixed batch with delegated and non-delegated inner confidential MPT transactions.
2207 void
2209 {
2210 testcase("Batch delegated confidential multiple operations");
2211 using namespace test::jtx;
2212
2213 Env env{*this, features};
2214 Account const alice("alice");
2215 Account const bob("bob");
2216 Account const carol("carol");
2217 Account const dave("dave");
2218 Account const erin("erin");
2219 Account const frank("frank");
2220
2221 MPTTester mpt(env, alice, {.holders = {bob, carol, dave, frank}});
2222 setupBatchEnv(mpt, alice, bob, carol, dave, 100, 60);
2223 mpt.pay(alice, bob, 50);
2224 env.fund(XRP(10000), erin);
2225 env.close();
2226
2227 mpt.authorize({.account = frank});
2228 mpt.pay(alice, frank, 40);
2229 mpt.generateKeyPair(frank);
2230
2231 env(delegate::set(bob, dave, {"ConfidentialMPTConvertBack"}));
2232 env(delegate::set(carol, erin, {"ConfidentialMPTSend"}));
2233 env(delegate::set(bob, erin, {"ConfidentialMPTMergeInbox"}));
2234 env.close();
2235
2236 auto const daveSeq = env.seq(dave);
2237 auto const bobSeq = env.seq(bob);
2238 auto const carolSeq = env.seq(carol);
2239 auto const frankSeq = env.seq(frank);
2240 auto const batchFee = batch::calcConfidentialBatchFee(env, 4, 6);
2241
2242 // Dave submits the batch. Bob's convertback uses Dave as Delegate;
2243 // Convert is not delegable, so Bob signs his own convert inner tx.
2244 // Carol's send and Bob's mergeInbox use Erin as Delegate. Frank's
2245 // convert and mergeInbox are non-delegated.
2246 auto jv1 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq);
2247 jv1[jss::Delegate] = dave.human();
2248 auto const jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1);
2249 auto jv3 = mpt.sendJV({.account = carol, .dest = bob, .amt = 15}, carolSeq);
2250 jv3[jss::Delegate] = erin.human();
2251 auto const jv4 = mpt.convertJV(
2252 {.account = frank, .amt = 25, .holderPubKey = mpt.getPubKey(frank)}, frankSeq);
2253 auto const jv5 = mpt.mergeInboxJV({.account = frank});
2254 auto jv6 = mpt.mergeInboxJV({.account = bob});
2255 jv6[jss::Delegate] = erin.human();
2256
2257 env(batch::outer(dave, daveSeq, batchFee, tfAllOrNothing),
2258 batch::Inner(jv1, bobSeq),
2259 batch::Inner(jv2, bobSeq + 1),
2260 batch::Inner(jv3, carolSeq),
2261 batch::Inner(jv4, frankSeq),
2262 batch::Inner(jv5, frankSeq + 1),
2263 batch::Inner(jv6, bobSeq + 2),
2264 batch::Sig(erin, frank, bob),
2265 Ter(tesSUCCESS));
2266 env.close();
2267
2268 env.require(MptBalance(mpt, bob, 60));
2269 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 105);
2270 BEAST_EXPECT(mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == 0);
2271 env.require(MptBalance(mpt, carol, 0));
2272 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedSpending) == 45);
2273 BEAST_EXPECT(mpt.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
2274 env.require(MptBalance(mpt, frank, 15));
2275 BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedSpending) == 25);
2276 BEAST_EXPECT(mpt.getDecryptedBalance(frank, MPTTester::holderEncryptedInbox) == 0);
2277 env.require(MptBalance(mpt, dave, 0));
2278 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedSpending) == 0);
2279 BEAST_EXPECT(mpt.getDecryptedBalance(dave, MPTTester::holderEncryptedInbox) == 0);
2280 auto const outstandingBalance = mpt.getIssuanceOutstandingBalance();
2281 BEAST_EXPECT(outstandingBalance && *outstandingBalance == 250);
2282 BEAST_EXPECT(mpt.getIssuanceConfidentialBalance() == 175);
2283 }
2284
2285 // Test invalid scenarios for delegation with tickets. ConfidentialMPTConvert
2286 // is not delegable, so ConfidentialMPTConvertBack (which is delegable and
2287 // whose ZK proof also binds to the transaction/ticket sequence) is used as
2288 // the delegated operation. Carol acts as bob's delegate throughout.
2289 void
2291 {
2292 testcase("Invalid cases for delegation with tickets");
2293 using namespace test::jtx;
2294
2295 Env env{*this, features};
2296 Account const alice("alice");
2297 Account const bob("bob");
2298 Account const carol("carol");
2299 MPTTester mptAlice(env, alice, {.holders = {bob}});
2300 env.fund(XRP(10000), carol);
2301 env.close();
2302
2303 mptAlice.create({
2304 .ownerCount = 1,
2305 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
2306 });
2307 mptAlice.authorize({.account = bob});
2308 mptAlice.pay(alice, bob, 200);
2309
2310 mptAlice.generateKeyPair(alice);
2311 mptAlice.generateKeyPair(bob);
2312 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
2313
2314 // Give bob a confidential spending balance to convert back from.
2315 mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)});
2316 mptAlice.mergeInbox({.account = bob});
2317
2318 // Bob delegates ConfidentialMPTConvertBack to carol.
2319 env(delegate::set(bob, carol, {"ConfidentialMPTConvertBack"}));
2320 env.close();
2321
2322 uint64_t const amt = 10;
2323
2324 // Every case below fails, so bob's spending balance and version never
2325 // change; capture the crypto material needed to build proofs once.
2326 auto const spendingBalance = requireOptional(
2327 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
2328 "Missing spending balance.");
2329 auto const encSpending = requireOptional(
2330 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
2331 "Missing encrypted spending balance.");
2332 auto const version = mptAlice.getMPTokenVersion(bob);
2333 auto const pcBf = generateBlindingFactor();
2334 auto const pc = mptAlice.getPedersenCommitment(spendingBalance, pcBf);
2335
2336 // Build a ConvertBack proof bound to a given sequence.
2337 auto proofForSeq = [&](std::uint32_t seq) {
2338 return mptAlice.getConvertBackProof(
2339 bob,
2340 amt,
2341 getConvertBackContextHash(bob, mptAlice.issuanceID(), seq, version),
2342 {
2343 .pedersenCommitment = pc,
2344 .amt = spendingBalance,
2345 .encryptedAmt = encSpending,
2346 .blindingFactor = pcBf,
2347 });
2348 };
2349
2350 // Invalid: proof built with wrong ticket sequence (ticketSeq + 1).
2351 {
2352 auto const ticketSeq = env.seq(bob) + 1;
2353 env(ticket::create(bob, 1));
2354
2355 mptAlice.convertBack({
2356 .account = bob,
2357 .amt = amt,
2358 .proof = proofForSeq(ticketSeq + 1),
2359 .pedersenCommitment = pc,
2360 .delegate = carol,
2361 .ticketSeq = ticketSeq,
2362 .err = tecBAD_PROOF,
2363 });
2364 }
2365
2366 // Invalid: proof built with account sequence instead of ticket sequence.
2367 {
2368 auto const ticketSeq = env.seq(bob) + 1;
2369 env(ticket::create(bob, 1));
2370
2371 mptAlice.convertBack({
2372 .account = bob,
2373 .amt = amt,
2374 .proof = proofForSeq(env.seq(bob)),
2375 .pedersenCommitment = pc,
2376 .delegate = carol,
2377 .ticketSeq = ticketSeq,
2378 .err = tecBAD_PROOF,
2379 });
2380 }
2381
2382 // Invalid: ticket sequence is far in the future and hasn't been created yet.
2383 {
2384 mptAlice.convertBack({
2385 .account = bob,
2386 .amt = amt,
2387 .delegate = carol,
2388 .ticketSeq = env.seq(bob) + 100,
2389 .err = terPRE_TICKET,
2390 });
2391 }
2392
2393 // Invalid: ticket sequence is in the past but was never created.
2394 {
2395 mptAlice.convertBack({
2396 .account = bob,
2397 .amt = amt,
2398 .delegate = carol,
2399 .ticketSeq = 1,
2400 .err = tefNO_TICKET,
2401 });
2402 }
2403
2404 // Invalid: the delegated account, carol, creates a ticket and uses it.
2405 // The ticket must belong to the delegator (bob), not the delegate.
2406 {
2407 auto const carolTicketSeq = env.seq(carol) + 1;
2408 env(ticket::create(carol, 1));
2409
2410 mptAlice.convertBack({
2411 .account = bob,
2412 .amt = amt,
2413 .delegate = carol,
2414 .ticketSeq = carolTicketSeq,
2415 .err = tefNO_TICKET,
2416 });
2417 }
2418
2419 // Invalid: proof bound to a ticket sequence but submitted without a ticket,
2420 // using account sequence.
2421 {
2422 auto const ticketSeq = env.seq(bob) + 1;
2423 env(ticket::create(bob, 1));
2424
2425 // Submit without a ticket; proof is bound to ticketSeq.
2426 mptAlice.convertBack({
2427 .account = bob,
2428 .amt = amt,
2429 .proof = proofForSeq(ticketSeq),
2430 .pedersenCommitment = pc,
2431 .delegate = carol,
2432 .err = tecBAD_PROOF,
2433 });
2434 }
2435
2436 // Valid: carol converts back on bob's behalf using a ticket owned by bob,
2437 // with a proof correctly bound to that ticket sequence. bob's spending
2438 // balance drops from 100 to 90.
2439 {
2440 auto const ticketSeq = env.seq(bob) + 1;
2441 env(ticket::create(bob, 1));
2442
2443 mptAlice.convertBack({
2444 .account = bob,
2445 .amt = amt,
2446 .delegate = carol,
2447 .ticketSeq = ticketSeq,
2448 });
2449 }
2450 }
2451
2452 // Verifies that delegation works correctly when the delegating account uses
2453 // tickets instead of regular sequence numbers. The proof must bind to the
2454 // ticket sequence, not the account sequence.
2455 void
2457 {
2458 testcase("Confidential delegation with tickets");
2459 using namespace test::jtx;
2460
2461 Env env{*this, features};
2462 Account const alice("alice");
2463 Account const bob("bob");
2464 Account const carol("carol");
2465 Account const dave("dave");
2466 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
2467 env.fund(XRP(10000), dave);
2468 env.close();
2469
2470 mptAlice.create({
2471 .ownerCount = 1,
2472 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback,
2473 });
2474 mptAlice.authorize({.account = bob});
2475 mptAlice.authorize({.account = carol});
2476 mptAlice.pay(alice, bob, 200);
2477 mptAlice.pay(alice, carol, 100);
2478
2479 mptAlice.generateKeyPair(alice);
2480 mptAlice.generateKeyPair(bob);
2481 mptAlice.generateKeyPair(carol);
2482 mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)});
2483
2484 // Bob grants dave permissions (Convert is not delegable).
2485 env(delegate::set(
2486 bob,
2487 dave,
2488 {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend", "ConfidentialMPTConvertBack"}));
2489 // Alice grants dave permission to clawback on her behalf.
2490 env(delegate::set(alice, dave, {"ConfidentialMPTClawback"}));
2491 env.close();
2492
2493 // Bob converts using a ticket.
2494 auto ticketSeq = env.seq(bob) + 1;
2495 env(ticket::create(bob, 1));
2496 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2497 mptAlice.convert({
2498 .account = bob,
2499 .amt = 100,
2500 .holderPubKey = mptAlice.getPubKey(bob),
2501 .ticketSeq = ticketSeq,
2502 });
2503 env.require(MptBalance(mptAlice, bob, 100));
2504
2505 // MergeInbox using ticket with delegation.
2506 ticketSeq = env.seq(bob) + 1;
2507 env(ticket::create(bob, 1));
2508 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2509 mptAlice.mergeInbox({.account = bob, .delegate = dave, .ticketSeq = ticketSeq});
2510
2511 // Carol converts and merges inbox to receive from bob.
2512 mptAlice.convert({
2513 .account = carol,
2514 .amt = 50,
2515 .holderPubKey = mptAlice.getPubKey(carol),
2516 });
2517 mptAlice.mergeInbox({.account = carol});
2518
2519 // Send using ticket with delegation.
2520 ticketSeq = env.seq(bob) + 1;
2521 env(ticket::create(bob, 1));
2522 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2523 mptAlice.send({
2524 .account = bob,
2525 .dest = carol,
2526 .amt = 20,
2527 .delegate = dave,
2528 .ticketSeq = ticketSeq,
2529 });
2530
2531 // ConvertBack using ticket with delegation.
2532 ticketSeq = env.seq(bob) + 1;
2533 env(ticket::create(bob, 1));
2534 BEAST_EXPECT(env.seq(bob) != ticketSeq);
2535 mptAlice.convertBack({
2536 .account = bob,
2537 .amt = 10,
2538 .delegate = dave,
2539 .ticketSeq = ticketSeq,
2540 });
2541
2542 // Clawback using ticket with delegation.
2543 ticketSeq = env.seq(alice) + 1;
2544 env(ticket::create(alice, 1));
2545 BEAST_EXPECT(env.seq(alice) != ticketSeq);
2546 mptAlice.confidentialClaw({
2547 .holder = bob,
2548 .amt = 70,
2549 .delegate = dave,
2550 .ticketSeq = ticketSeq,
2551 });
2552 }
2553
2554 void
2556 {
2557 // DepositAuth, credentials, and destination tag interactions.
2558 testSendDepositPreauth(features);
2560 testDestinationTag(features);
2561
2562 // AMM/pseudo-account interaction.
2564
2565 // Ticket interactions.
2566 testWithTickets(features);
2568 testTicketErrors(features);
2569
2570 // Batch interactions.
2571 testBatchConfidentialSend(features);
2574 testBatchAllOrNothing(features);
2575 testBatchOnlyOne(features);
2576 testBatchUntilFailure(features);
2577 testBatchIndependent(features);
2578 testBatchWithTickets(features);
2579
2580 // Permission delegation interactions.
2582 testDelegationRevocation(features);
2583 testDelegationWithAuditor(features);
2585 testBatchDelegatedSend(features);
2590 testDelegationWithTickets(features);
2591 }
2592
2593public:
2594 void
2595 run() override
2596 {
2597 using namespace test::jtx;
2598 FeatureBitset const all{testableAmendments()};
2599
2600 testWithFeats(all);
2601 }
2602};
2603
2604BEAST_DEFINE_TESTSUITE(ConfidentialTransferExtended, app, xrpl);
2605
2606} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
Like std::vector<char> but better.
Definition Buffer.h:19
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:1305
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_DELEGATE_PERMISSION
Definition TER.h:226
@ terPRE_TICKET
Definition TER.h:222
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
@ tefNO_TICKET
Definition TER.h:177
constexpr std::size_t kEcClawbackProofLength
Length of the ZKProof for ConfidentialMPTClawback.
Definition Protocol.h:529
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
uint256 getConvertBackContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence, std::uint32_t version)
Generates the context hash for ConfidentialMPTConvertBack transactions.
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).
@ temINVALID
Definition TER.h:98
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
@ temBAD_SIGNER
Definition TER.h:103
@ tecBAD_CREDENTIALS
Definition TER.h:362
@ tecBAD_PROOF
Definition TER.h:371
@ tecEXPIRED
Definition TER.h:317
@ tecNO_PERMISSION
Definition TER.h:308
@ tecDST_TAG_NEEDED
Definition TER.h:312
std::uint32_t ownerCount(SLE::const_ref sle, beast::Journal j, std::int32_t ownerCountAdj=0)
Return number of the objects which reserve is covered by the account(sle) (so called "ownercount").
BaseUInt< 256 > uint256
Definition base_uint.h:580
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:245
T push_back(T... args)
T reserve(T... args)