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