xrpld
Loading...
Searching...
No Matches
ConfidentialTransfer_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/ConfidentialTransfer.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/amount.h>
5#include <test/jtx/flags.h>
6#include <test/jtx/mpt.h>
7#include <test/jtx/pay.h>
8#include <test/jtx/ter.h>
9#include <test/jtx/vault.h>
10
11#include <xrpl/basics/Buffer.h>
12#include <xrpl/basics/Slice.h>
13#include <xrpl/basics/base_uint.h>
14#include <xrpl/basics/contract.h>
15#include <xrpl/basics/strHex.h>
16#include <xrpl/beast/unit_test/suite.h>
17#include <xrpl/beast/utility/Journal.h>
18#include <xrpl/core/ServiceRegistry.h>
19#include <xrpl/json/json_value.h>
20#include <xrpl/ledger/ApplyView.h>
21#include <xrpl/ledger/OpenView.h>
22#include <xrpl/protocol/AccountID.h>
23#include <xrpl/protocol/ConfidentialTransfer.h>
24#include <xrpl/protocol/Feature.h>
25#include <xrpl/protocol/Indexes.h>
26#include <xrpl/protocol/LedgerFormats.h>
27#include <xrpl/protocol/Protocol.h>
28#include <xrpl/protocol/SField.h>
29#include <xrpl/protocol/STObject.h>
30#include <xrpl/protocol/Serializer.h>
31#include <xrpl/protocol/TER.h>
32#include <xrpl/protocol/TxFlags.h>
33#include <xrpl/protocol/UintTypes.h>
34#include <xrpl/protocol/jss.h>
35#include <xrpl/tx/apply.h>
36
37#include <openssl/evp.h>
38#include <utility/mpt_utility.h>
39
40#include <secp256k1.h>
41#include <secp256k1_mpt.h>
42
43#include <algorithm>
44#include <array>
45#include <cstddef>
46#include <cstdint>
47#include <cstring>
48#include <functional>
49#include <initializer_list>
50#include <limits>
51#include <memory>
52#include <optional>
53#include <stdexcept>
54#include <string>
55#include <utility>
56
57namespace xrpl {
58
60{
61 void
63 {
64 testcase("Convert");
65 using namespace test::jtx;
66
67 // Basic convert test
68 {
69 Env env{*this, features};
70 Account const alice("alice");
71 Account const bob("bob");
72 MPTTester mptAlice(env, alice, {.holders = {bob}});
73
74 mptAlice.create({
75 .ownerCount = 1,
76 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
77 });
78
79 mptAlice.authorize({
80 .account = bob,
81 });
82 mptAlice.pay(alice, bob, 100);
83
84 mptAlice.generateKeyPair(alice);
85
86 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
87
88 mptAlice.generateKeyPair(bob);
89
90 mptAlice.convert({
91 .account = bob,
92 .amt = 0,
93 .holderPubKey = mptAlice.getPubKey(bob),
94 });
95
96 mptAlice.convert({
97 .account = bob,
98 .amt = 20,
99 });
100
101 mptAlice.convert({
102 .account = bob,
103 .amt = 40,
104 });
105
106 mptAlice.convert({
107 .account = bob,
108 .amt = 40,
109 });
110 }
111
112 // Edge case: minimum amount (1)
113 {
114 Env env{*this, features};
115 Account const alice("alice");
116 Account const bob("bob");
117 MPTTester mptAlice(env, alice, {.holders = {bob}});
118
119 mptAlice.create({
120 .ownerCount = 1,
121 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
122 });
123
124 mptAlice.authorize({
125 .account = bob,
126 });
127 mptAlice.pay(alice, bob, 1);
128
129 mptAlice.generateKeyPair(alice);
130 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
131
132 mptAlice.generateKeyPair(bob);
133 mptAlice.convert({
134 .account = bob,
135 .amt = 0,
136 .holderPubKey = mptAlice.getPubKey(bob),
137 });
138
139 mptAlice.convert({
140 .account = bob,
141 .amt = 1,
142 });
143 }
144
145 // Edge case: kMaxMpTokenAmount
146 // Using raw JSON to avoid automatic decryption checks in MPTTester
147 // which don't work for very large amounts (brute-force decryption is slow)
148 {
149 Env env{*this, features};
150 Account const alice("alice");
151 Account const bob("bob");
152 MPTTester mptAlice(env, alice, {.holders = {bob}});
153
154 mptAlice.create({
155 .ownerCount = 1,
156 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
157 });
158
159 mptAlice.authorize({
160 .account = bob,
161 });
162 mptAlice.pay(alice, bob, kMaxMpTokenAmount);
163
164 mptAlice.generateKeyPair(alice);
165 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
166
167 mptAlice.generateKeyPair(bob);
168
169 // First convert with amt=0 to register public key (uses MPTTester)
170 mptAlice.convert({
171 .account = bob,
172 .amt = 0,
173 .holderPubKey = mptAlice.getPubKey(bob),
174 });
175
176 // Second convert with kMaxMpTokenAmount using raw JSON
177 Buffer const blindingFactor = generateBlindingFactor();
178 auto const holderCiphertext =
179 mptAlice.encryptAmount(bob, kMaxMpTokenAmount, blindingFactor);
180 auto const issuerCiphertext =
181 mptAlice.encryptAmount(alice, kMaxMpTokenAmount, blindingFactor);
182
183 json::Value jv;
184 jv[jss::Account] = bob.human();
185 jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
186 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
187 jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount);
188 jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext);
189 jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext);
190 jv[sfBlindingFactor.jsonName] = strHex(blindingFactor);
191
192 env(jv, Ter(tesSUCCESS));
193
194 // Verify the public balance was reduced
195 env.require(MptBalance(mptAlice, bob, 0));
196 }
197 }
198
199 void
201 {
202 testcase("Convert with auditor");
203 using namespace test::jtx;
204
205 Env env{*this, features};
206 Account const alice("alice");
207 Account const bob("bob");
208 Account const auditor("auditor");
209 MPTTester mptAlice(
210 env,
211 alice,
212 {
213 .holders = {bob},
214 .auditor = auditor,
215 });
216
217 mptAlice.create({
218 .ownerCount = 1,
219 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
220 });
221
222 mptAlice.authorize({
223 .account = bob,
224 });
225 mptAlice.pay(alice, bob, 100);
226
227 mptAlice.generateKeyPair(alice);
228 mptAlice.generateKeyPair(auditor);
229
230 mptAlice.set({
231 .account = alice,
232 .issuerPubKey = mptAlice.getPubKey(alice),
233 .auditorPubKey = mptAlice.getPubKey(auditor),
234 });
235
236 mptAlice.generateKeyPair(bob);
237
238 mptAlice.convert({
239 .account = bob,
240 .amt = 0,
241 .holderPubKey = mptAlice.getPubKey(bob),
242 });
243
244 mptAlice.convert({
245 .account = bob,
246 .amt = 20,
247 });
248
249 mptAlice.convert({
250 .account = bob,
251 .amt = 30,
252 });
253 }
254
255 void
257 {
258 testcase("Convert preflight");
259 using namespace test::jtx;
260
261 // Alice (issuer) tries to convert her own tokens - should fail
262 {
263 Env env{*this, features};
264 Account const alice("alice");
265 MPTTester mptAlice(env, alice);
266
267 mptAlice.create({
268 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
269 });
270 mptAlice.generateKeyPair(alice);
271
272 mptAlice.convert({
273 .account = alice,
274 .amt = 10,
275 .holderPubKey = mptAlice.getPubKey(alice),
276 .err = temMALFORMED,
277 });
278 }
279
280 {
281 Env env{*this, features - featureConfidentialTransfer};
282 Account const alice("alice");
283 Account const bob("bob");
284 MPTTester mptAlice(env, alice, {.holders = {bob}});
285
286 mptAlice.create({
287 .ownerCount = 1,
288 .flags = tfMPTCanTransfer | tfMPTCanLock,
289 });
290
291 mptAlice.authorize({
292 .account = bob,
293 });
294 mptAlice.pay(alice, bob, 100);
295
296 mptAlice.generateKeyPair(alice);
297 mptAlice.generateKeyPair(bob);
298
299 mptAlice.set({
300 .account = alice,
301 .issuerPubKey = mptAlice.getPubKey(alice),
302 .err = temDISABLED,
303 });
304
305 mptAlice.convert({
306 .account = bob,
307 .amt = 10,
308 .holderPubKey = mptAlice.getPubKey(bob),
309 .err = temDISABLED,
310 });
311 }
312
313 {
314 Env env{*this, features};
315 Account const alice("alice");
316 Account const bob("bob");
317 MPTTester mptAlice(env, alice, {.holders = {bob}});
318
319 mptAlice.create({
320 .ownerCount = 1,
321 .flags = tfMPTCanTransfer | tfMPTCanLock,
322 });
323
324 mptAlice.authorize({
325 .account = bob,
326 });
327 mptAlice.pay(alice, bob, 100);
328
329 mptAlice.generateKeyPair(alice);
330 mptAlice.generateKeyPair(bob);
331
332 mptAlice.convert({
333 .account = alice,
334 .amt = 10,
335 .holderPubKey = mptAlice.getPubKey(bob),
336 .err = temMALFORMED,
337 });
338
339 // Holder encrypted amount is empty (length 0)
340 mptAlice.convert({
341 .account = bob,
342 .amt = 10,
343 .holderPubKey = mptAlice.getPubKey(bob),
344 .holderEncryptedAmt = Buffer{},
345 .err = temBAD_CIPHERTEXT,
346 });
347
348 // Issuer encrypted amount is empty (length 0)
349 mptAlice.convert({
350 .account = bob,
351 .amt = 10,
352 .holderPubKey = mptAlice.getPubKey(bob),
353 .issuerEncryptedAmt = Buffer{},
354 .err = temBAD_CIPHERTEXT,
355 });
356
357 // Auditor encrypted amount has invalid length (must be 66 bytes)
358 mptAlice.convert({
359 .account = bob,
360 .amt = 10,
361 .holderPubKey = mptAlice.getPubKey(bob),
362 .auditorEncryptedAmt = gMakeZeroBuffer(10),
363 .err = temBAD_CIPHERTEXT,
364 });
365
366 // Auditor encrypted amount has correct length but invalid data
367 mptAlice.convert({
368 .account = bob,
369 .amt = 10,
370 .holderPubKey = mptAlice.getPubKey(bob),
371 .auditorEncryptedAmt = getBadCiphertext(),
372 .err = temBAD_CIPHERTEXT,
373 });
374
375 // Amount exceeds maximum allowed MPT amount
376 mptAlice.convert({
377 .account = bob,
378 .amt = kMaxMpTokenAmount + 1,
379 .holderPubKey = mptAlice.getPubKey(bob),
380 .err = temBAD_AMOUNT,
381 });
382
383 // Holder encrypted amount has correct length but invalid data
384 mptAlice.convert({
385 .account = bob,
386 .amt = 1,
387 .holderPubKey = mptAlice.getPubKey(bob),
388 .holderEncryptedAmt = getBadCiphertext(),
389 .err = temBAD_CIPHERTEXT,
390 });
391
392 // Issuer encrypted amount has correct length but invalid data (not
393 // a valid EC point)
394 mptAlice.convert({
395 .account = bob,
396 .amt = 1,
397 .holderPubKey = mptAlice.getPubKey(bob),
398 .issuerEncryptedAmt = getBadCiphertext(),
399 .err = temBAD_CIPHERTEXT,
400 });
401
402 // Holder public key is invalid (empty buffer)
403 mptAlice.convert({
404 .account = bob,
405 .amt = 10,
406 .holderPubKey = Buffer{},
407 .err = temMALFORMED,
408 });
409
410 // Holder public key has correct length but invalid EC point data
411 mptAlice.convert({
412 .account = bob,
413 .amt = 10,
414 .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
415 .err = temMALFORMED,
416 });
417 }
418
419 // when registering holder pub key, the transaction must include a
420 // Schnorr proof of knowledge for the corresponding secret key
421 {
422 Env env{*this, features};
423 Account const alice("alice");
424 Account const bob("bob");
425 MPTTester mptAlice(env, alice, {.holders = {bob}});
426
427 mptAlice.create({
428 .ownerCount = 1,
429 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
430 });
431
432 mptAlice.authorize({
433 .account = bob,
434 });
435 mptAlice.pay(alice, bob, 100);
436
437 mptAlice.generateKeyPair(alice);
438 mptAlice.generateKeyPair(bob);
439
440 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
441
442 mptAlice.convert({
443 .account = bob,
444 .amt = 10,
445 .fillSchnorrProof = false,
446 .holderPubKey = mptAlice.getPubKey(bob),
447 .err = temMALFORMED,
448 });
449
450 mptAlice.convert({
451 .account = bob,
452 .amt = 0,
453 .fillSchnorrProof = false,
454 .holderPubKey = mptAlice.getPubKey(bob),
455 .err = temMALFORMED,
456 });
457
458 // proof length is invalid
459 mptAlice.convert({
460 .account = bob,
461 .amt = 10,
462 .proof = std::string(10, 'A'),
463 .holderPubKey = mptAlice.getPubKey(bob),
464 .err = temMALFORMED,
465 });
466 }
467
468 // when holder pub key already registered, Schnorr proof must not be
469 // provided
470 {
471 Env env{*this, features};
472 Account const alice("alice");
473 Account const bob("bob");
474 MPTTester mptAlice(env, alice, {.holders = {bob}});
475
476 mptAlice.create({
477 .ownerCount = 1,
478 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
479 });
480
481 mptAlice.authorize({
482 .account = bob,
483 });
484 mptAlice.pay(alice, bob, 100);
485
486 mptAlice.generateKeyPair(alice);
487 mptAlice.generateKeyPair(bob);
488
489 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
490
491 // this will register bob's pub key,
492 // and convert 10 to confidential balance
493 mptAlice.convert({
494 .account = bob,
495 .amt = 10,
496 .holderPubKey = mptAlice.getPubKey(bob),
497 });
498
499 // proof must not be provided after pub key was registered
500 mptAlice.convert({
501 .account = bob,
502 .amt = 20,
503 .fillSchnorrProof = true,
504 .err = temMALFORMED,
505 });
506 }
507 }
508
509 void
511 {
512 testcase("Convert proof context binding");
513 using namespace test::jtx;
514
515 auto runBadProof = [&](auto makeContextHash) {
516 Env env{*this, features};
517 Account const alice("alice");
518 Account const bob("bob");
519 Account const carol("carol");
520 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
521
522 mptAlice.create({
523 .ownerCount = 1,
524 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
525 });
526 mptAlice.authorize({.account = bob});
527 mptAlice.authorize({.account = carol});
528 mptAlice.pay(alice, bob, 100);
529
530 mptAlice.generateKeyPair(alice);
531 mptAlice.generateKeyPair(bob);
532 mptAlice.generateKeyPair(carol);
533 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
534
535 auto const proof =
536 mptAlice.getSchnorrProof(bob, makeContextHash(env, mptAlice, alice, bob, carol));
537 if (!BEAST_EXPECT(proof.has_value()))
538 return;
539
540 mptAlice.convert({
541 .account = bob,
542 .amt = 10,
543 .proof = strHex(requireOptional(proof, "Missing proof")),
544 .holderPubKey = mptAlice.getPubKey(bob),
545 .err = tecBAD_PROOF,
546 });
547 };
548
549 // Wrong account in the proof context.
550 runBadProof([&](Env& env,
551 MPTTester const& mpt,
552 Account const&,
553 Account const& bob,
554 Account const& carol) {
555 return getConvertContextHash(carol.id(), mpt.issuanceID(), env.seq(bob));
556 });
557
558 // Wrong issuance ID in the proof context.
559 runBadProof([&](Env& env,
560 MPTTester const&,
561 Account const& alice,
562 Account const& bob,
563 Account const&) {
565 bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob));
566 });
567
568 // Wrong transaction sequence in the proof context.
569 runBadProof([&](Env& env,
570 MPTTester const& mpt,
571 Account const&,
572 Account const& bob,
573 Account const&) {
574 return getConvertContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1);
575 });
576 }
577
578 void
580 {
581 testcase("Set");
582 using namespace test::jtx;
583
584 // Set keys on issuance that already has confidential amounts enabled
585 {
586 Env env{*this, features};
587 Account const alice("alice");
588 Account const auditor("auditor");
589 MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
590
591 mptAlice.create({
592 .ownerCount = 1,
593 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
594 });
595
596 mptAlice.generateKeyPair(alice);
597 mptAlice.generateKeyPair(auditor);
598
599 mptAlice.set({
600 .account = alice,
601 .issuerPubKey = mptAlice.getPubKey(alice),
602 .auditorPubKey = mptAlice.getPubKey(auditor),
603 });
604 }
605
606 // Enable confidential amounts flag only (no keys)
607 {
608 Env env{*this, features};
609 Account const alice("alice");
610 MPTTester mptAlice(env, alice, {.holders = {}});
611
612 mptAlice.create({
613 .ownerCount = 1,
614 .flags = tfMPTCanTransfer | tfMPTCanLock,
615 });
616
617 mptAlice.set({
618 .account = alice,
620 });
621 }
622
623 // Set keys when enabling confidential amounts in the same tx
624 {
625 Env env{*this, features};
626 Account const alice("alice");
627 Account const auditor("auditor");
628 MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
629
630 mptAlice.create({
631 .ownerCount = 1,
632 .flags = tfMPTCanTransfer | tfMPTCanLock,
633 });
634
635 mptAlice.generateKeyPair(alice);
636 mptAlice.generateKeyPair(auditor);
637
638 mptAlice.set({
639 .account = alice,
641 .issuerPubKey = mptAlice.getPubKey(alice),
642 .auditorPubKey = mptAlice.getPubKey(auditor),
643 });
644
645 // Verify lsfMPTCanHoldConfidentialBalance flag is set
646 BEAST_EXPECT(mptAlice.checkFlags(
647 lsfMPTCanTransfer | lsfMPTCanLock | lsfMPTCanHoldConfidentialBalance));
648
649 // Verify keys are persisted on the issuance
650 auto const sle = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
651 BEAST_EXPECT(sle);
652 BEAST_EXPECT(sle->isFieldPresent(sfIssuerEncryptionKey));
653 BEAST_EXPECT(sle->isFieldPresent(sfAuditorEncryptionKey));
654 }
655 }
656
657 void
659 {
660 testcase("Set preflight");
661 using namespace test::jtx;
662
663 {
664 Env env{*this, features - featureConfidentialTransfer};
665 Account const alice("alice");
666 Account const bob("bob");
667 MPTTester mptAlice(env, alice, {.holders = {bob}});
668
669 mptAlice.create({
670 .ownerCount = 1,
671 .flags = tfMPTCanTransfer | tfMPTCanLock,
672 });
673
674 mptAlice.authorize({
675 .account = bob,
676 });
677 mptAlice.pay(alice, bob, 100);
678
679 mptAlice.generateKeyPair(alice);
680 mptAlice.generateKeyPair(bob);
681
682 mptAlice.set({
683 .account = alice,
684 .issuerPubKey = mptAlice.getPubKey(alice),
685 .err = temDISABLED,
686 });
687 }
688
689 // pub key is invalid
690 {
691 Env env{*this, features};
692 Account const alice("alice");
693 Account const bob("bob");
694 MPTTester mptAlice(env, alice, {.holders = {bob}});
695
696 mptAlice.create({
697 .ownerCount = 1,
698 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
699 });
700
701 mptAlice.authorize({
702 .account = bob,
703 });
704 mptAlice.pay(alice, bob, 100);
705
706 mptAlice.generateKeyPair(alice);
707 mptAlice.generateKeyPair(bob);
708
709 // Issuer pub key is invalid (empty)
710 mptAlice.set({
711 .account = alice,
712 .issuerPubKey = Buffer{},
713 .err = temMALFORMED,
714 });
715
716 // Issuer pub key has correct length but invalid EC point data
717 mptAlice.set({
718 .account = alice,
719 .issuerPubKey = gMakeZeroBuffer(kEcPubKeyLength),
720 .err = temMALFORMED,
721 });
722
723 // Auditor key is invalid length
724 mptAlice.set({
725 .account = alice,
726 .issuerPubKey = mptAlice.getPubKey(alice),
727 .auditorPubKey = gMakeZeroBuffer(10),
728 .err = temMALFORMED,
729 });
730
731 // Auditor key has correct length but invalid EC point data
732 mptAlice.set({
733 .account = alice,
734 .issuerPubKey = mptAlice.getPubKey(alice),
735 .auditorPubKey = gMakeZeroBuffer(kEcPubKeyLength),
736 .err = temMALFORMED,
737 });
738
739 // Cannot set auditor key without issuer key
740 mptAlice.set({
741 .account = alice,
742 .auditorPubKey = mptAlice.getPubKey(alice),
743 .err = temMALFORMED,
744 });
745
746 // Cannot set Holder and issuer Keys in the same transaction
747 mptAlice.set({
748 .account = alice,
749 .holder = bob,
750 .issuerPubKey = mptAlice.getPubKey(alice),
751 .err = temMALFORMED,
752 });
753
754 // Cannot set Holder and auditor Keys in the same transaction
755 mptAlice.set({
756 .account = alice,
757 .holder = bob,
758 .auditorPubKey = mptAlice.getPubKey(alice),
759 .err = temMALFORMED,
760 });
761 }
762 }
763
764 void
766 {
767 testcase("Set preclaim");
768 using namespace test::jtx;
769
770 // Cannot set issuer key if confidential amounts not enabled
771 {
772 Env env{*this, features};
773 Account const alice("alice");
774 MPTTester mptAlice(env, alice, {.holders = {}});
775
776 mptAlice.create({
777 .ownerCount = 1,
778 .flags = tfMPTCanTransfer | tfMPTCanLock,
779 });
780
781 mptAlice.generateKeyPair(alice);
782
783 mptAlice.set({
784 .account = alice,
785 .issuerPubKey = mptAlice.getPubKey(alice),
786 .err = tecNO_PERMISSION,
787 });
788 }
789
790 // Cannot update issuer public key once set
791 {
792 Env env{*this, features};
793 Account const alice("alice");
794 Account const bob("bob");
795 MPTTester mptAlice(env, alice, {.holders = {bob}});
796
797 mptAlice.create({
798 .ownerCount = 1,
799 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
800 });
801
802 mptAlice.generateKeyPair(alice);
803 mptAlice.generateKeyPair(bob);
804
805 // First set issuer key - should succeed
806 mptAlice.set({
807 .account = alice,
808 .issuerPubKey = mptAlice.getPubKey(alice),
809 });
810
811 // Try to update issuer key - should fail
812 mptAlice.set({
813 .account = alice,
814 .issuerPubKey = mptAlice.getPubKey(bob),
815 .err = tecNO_PERMISSION,
816 });
817 }
818
819 // Cannot update issuer and auditor public keys once set
820 // Note: trying to set only auditor key fails in preflight (temMALFORMED)
821 // so we must provide both keys, which fails on issuer key check first
822 {
823 Env env{*this, features};
824 Account const alice("alice");
825 Account const bob("bob");
826 Account const auditor("auditor");
827 MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
828
829 mptAlice.create({
830 .ownerCount = 1,
831 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
832 });
833
834 mptAlice.generateKeyPair(alice);
835 mptAlice.generateKeyPair(bob);
836 mptAlice.generateKeyPair(auditor);
837
838 // Set issuer and auditor keys - should succeed
839 mptAlice.set({
840 .account = alice,
841 .issuerPubKey = mptAlice.getPubKey(alice),
842 .auditorPubKey = mptAlice.getPubKey(auditor),
843 });
844
845 // Try to update both keys - fails on issuer key check first
846 mptAlice.set({
847 .account = alice,
848 .issuerPubKey = mptAlice.getPubKey(bob),
849 .auditorPubKey = mptAlice.getPubKey(alice),
850 .err = tecNO_PERMISSION,
851 });
852 }
853
854 // Cannot set auditor key if confidential amounts not enabled
855 {
856 Env env{*this, features};
857 Account const alice("alice");
858 Account const auditor("auditor");
859 MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
860
861 mptAlice.create({
862 .ownerCount = 1,
863 .flags = tfMPTCanTransfer | tfMPTCanLock,
864 });
865
866 mptAlice.generateKeyPair(alice);
867 mptAlice.generateKeyPair(auditor);
868
869 mptAlice.set({
870 .account = alice,
871 .issuerPubKey = mptAlice.getPubKey(alice),
872 .auditorPubKey = mptAlice.getPubKey(auditor),
873 .err = tecNO_PERMISSION,
874 });
875 }
876
877 // Cannot set keys when mutation of canConfidentialAmount is disallowed
878 {
879 Env env{*this, features};
880 Account const alice("alice");
881 MPTTester mptAlice(env, alice, {.holders = {}});
882
883 // Create with tmfMPTCannotEnableCanHoldConfidentialBalance
884 mptAlice.create({
885 .ownerCount = 1,
886 .flags = tfMPTCanTransfer | tfMPTCanLock,
888 });
889
890 mptAlice.generateKeyPair(alice);
891
892 // Trying to enable confidential amounts and set keys fails
893 // because the issuance cannot mutate canConfidentialAmount
894 mptAlice.set({
895 .account = alice,
897 .issuerPubKey = mptAlice.getPubKey(alice),
898 .err = tecNO_PERMISSION,
899 });
900 }
901
902 // Set issuer key first, then auditor key in a separate tx
903 {
904 Env env{*this, features};
905 Account const alice("alice");
906 Account const auditor("auditor");
907 MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
908
909 mptAlice.create({
910 .ownerCount = 1,
911 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
912 });
913
914 mptAlice.generateKeyPair(alice);
915 mptAlice.generateKeyPair(auditor);
916
917 // Set issuer key only
918 mptAlice.set({
919 .account = alice,
920 .issuerPubKey = mptAlice.getPubKey(alice),
921 });
922
923 // Set auditor key in a separate tx - requires issuer key in tx
924 // (preflight enforces auditor key requires issuer key)
925 // This fails because issuer key is already set on ledger
926 mptAlice.set({
927 .account = alice,
928 .issuerPubKey = mptAlice.getPubKey(alice),
929 .auditorPubKey = mptAlice.getPubKey(auditor),
930 .err = tecNO_PERMISSION,
931 });
932 }
933 }
934
935 void
937 {
938 testcase("test transfer fee");
939 using namespace test::jtx;
940
941 // MPTokenIssuanceCreate: cannot create with both TransferFee > 0 and
942 // tfMPTCanHoldConfidentialBalance
943 {
944 Env env{*this, features};
945 Account const alice("alice");
946 MPTTester mptAlice(env, alice, {.holders = {}});
947
948 mptAlice.create({
949 .transferFee = 100,
950 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
951 .err = temBAD_TRANSFER_FEE,
952 });
953
954 // transferFee being 0 is allowed, even with tfMPTCanHoldConfidentialBalance
955 mptAlice.create({
956 .transferFee = 0,
957 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
958 });
959 }
960
961 // MPTokenIssuanceSet (preflight): cannot enable confidential amounts and
962 // set TransferFee > 0 in the same transaction
963 {
964 Env env{*this, features};
965 Account const alice("alice");
966 MPTTester mptAlice(env, alice, {.holders = {}});
967
968 mptAlice.create({
969 .ownerCount = 1,
970 .flags = tfMPTCanTransfer | tfMPTCanLock,
971 .mutableFlags = tmfMPTCanMutateTransferFee,
972 });
973
974 mptAlice.set({
975 .account = alice,
977 .transferFee = 100,
978 .err = temBAD_TRANSFER_FEE,
979 });
980 }
981
982 // MPTokenIssuanceSet (preclaim): cannot enable confidential amounts on
983 // an issuance that already has a non-zero TransferFee
984 {
985 Env env{*this, features};
986 Account const alice("alice");
987 MPTTester mptAlice(env, alice, {.holders = {}});
988
989 mptAlice.create({
990 .transferFee = 100,
991 .ownerCount = 1,
992 .flags = tfMPTCanTransfer | tfMPTCanLock,
993 .mutableFlags = tmfMPTCanMutateTransferFee,
994 });
995
996 mptAlice.set({
997 .account = alice,
999 .err = tecNO_PERMISSION,
1000 });
1001 }
1002
1003 // MPTokenIssuanceSet (preclaim): cannot set TransferFee > 0 on an
1004 // issuance that already has lsfMPTCanHoldConfidentialBalance
1005 {
1006 Env env{*this, features};
1007 Account const alice("alice");
1008 MPTTester mptAlice(env, alice, {.holders = {}});
1009
1010 mptAlice.create({
1011 .ownerCount = 1,
1012 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1013 .mutableFlags = tmfMPTCanMutateTransferFee,
1014 });
1015
1016 mptAlice.set({
1017 .account = alice,
1018 .transferFee = 100,
1019 .err = tecNO_PERMISSION,
1020 });
1021
1022 // Setting transfer fee to 0 is allowed, but have no effect.
1023 mptAlice.set({
1024 .account = alice,
1025 .transferFee = 0,
1026 });
1027 }
1028 }
1029
1030 void
1032 {
1033 testcase("Convert preclaim");
1034 using namespace test::jtx;
1035
1036 // tfMPTCanHoldConfidentialBalance is not set on issuance
1037 {
1038 Env env{*this, features};
1039 Account const alice("alice");
1040 Account const bob("bob");
1041 MPTTester mptAlice(env, alice, {.holders = {bob}});
1042
1043 mptAlice.create({
1044 .ownerCount = 1,
1045 .flags = tfMPTCanTransfer | tfMPTCanLock,
1046 });
1047
1048 mptAlice.authorize({
1049 .account = bob,
1050 });
1051 mptAlice.pay(alice, bob, 100);
1052
1053 mptAlice.generateKeyPair(alice);
1054 mptAlice.generateKeyPair(bob);
1055
1056 mptAlice.convert({
1057 .account = bob,
1058 .amt = 10,
1059 .holderPubKey = mptAlice.getPubKey(bob),
1060 .err = tecNO_PERMISSION,
1061 });
1062 }
1063
1064 // issuer has not uploaded their sfIssuerEncryptionKey
1065 {
1066 Env env{*this, features};
1067 Account const alice("alice");
1068 Account const bob("bob");
1069 MPTTester mptAlice(env, alice, {.holders = {bob}});
1070
1071 mptAlice.create({
1072 .ownerCount = 1,
1073 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1074 });
1075
1076 mptAlice.authorize({
1077 .account = bob,
1078 });
1079 mptAlice.pay(alice, bob, 100);
1080
1081 mptAlice.generateKeyPair(alice);
1082 mptAlice.generateKeyPair(bob);
1083
1084 mptAlice.convert({
1085 .account = bob,
1086 .amt = 10,
1087 .holderPubKey = mptAlice.getPubKey(bob),
1088 .err = tecNO_PERMISSION,
1089 });
1090 }
1091
1092 // issuance does not exist
1093 {
1094 Env env{*this, features};
1095 Account const alice("alice");
1096 Account const bob("bob");
1097 MPTTester mptAlice(env, alice, {.holders = {bob}});
1098
1099 mptAlice.create({
1100 .ownerCount = 1,
1101 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1102 });
1103
1104 mptAlice.authorize({
1105 .account = bob,
1106 });
1107 mptAlice.generateKeyPair(alice);
1108
1109 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1110
1111 mptAlice.destroy();
1112 mptAlice.generateKeyPair(bob);
1113
1114 mptAlice.convert({
1115 .account = bob,
1116 .amt = 10,
1117 .holderPubKey = mptAlice.getPubKey(bob),
1118 .err = tecOBJECT_NOT_FOUND,
1119 });
1120 }
1121
1122 // bob has not created MPToken
1123 {
1124 Env env{*this, features};
1125 Account const alice("alice");
1126 Account const bob("bob");
1127 MPTTester mptAlice(env, alice, {.holders = {bob}});
1128
1129 mptAlice.create({
1130 .ownerCount = 1,
1131 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1132 });
1133
1134 mptAlice.generateKeyPair(alice);
1135 mptAlice.generateKeyPair(bob);
1136
1137 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1138
1139 mptAlice.convert({
1140 .account = bob,
1141 .amt = 10,
1142 .holderPubKey = mptAlice.getPubKey(bob),
1143 .err = tecOBJECT_NOT_FOUND,
1144 });
1145 }
1146
1147 // Verification of Issuer and and holder ciphertexts
1148 {
1149 Env env{*this, features};
1150 Account const alice("alice");
1151 Account const bob("bob");
1152 Account const carol("carol");
1153 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1154
1155 mptAlice.create({
1156 .ownerCount = 1,
1157 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1158 });
1159
1160 mptAlice.authorize({
1161 .account = bob,
1162 });
1163 mptAlice.pay(alice, bob, 100);
1164
1165 mptAlice.generateKeyPair(alice);
1166 mptAlice.generateKeyPair(bob);
1167 mptAlice.generateKeyPair(carol);
1168
1169 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1170
1171 mptAlice.convert({
1172 .account = bob,
1173 .amt = 10,
1174 .holderPubKey = mptAlice.getPubKey(bob),
1175 .holderEncryptedAmt = getTrivialCiphertext(),
1176 .err = tecBAD_PROOF,
1177 });
1178
1179 mptAlice.convert({
1180 .account = bob,
1181 .amt = 10,
1182 .holderPubKey = mptAlice.getPubKey(bob),
1183 .issuerEncryptedAmt = getTrivialCiphertext(),
1184 .err = tecBAD_PROOF,
1185 });
1186
1187 std::uint64_t const amount = 10;
1188 Buffer const blindingFactor = generateBlindingFactor();
1189 Buffer const holderCiphertext = mptAlice.encryptAmount(bob, amount, blindingFactor);
1190
1191 // Holder ciphertext is valid for the amount and
1192 // blinding factor, but the issuer ciphertext is encrypted under a
1193 // different public key than the registered issuer key.
1194 Buffer const wrongIssuerCiphertext =
1195 mptAlice.encryptAmount(carol, amount, blindingFactor);
1196
1197 mptAlice.convert({
1198 .account = bob,
1199 .amt = amount,
1200 .holderPubKey = mptAlice.getPubKey(bob),
1201 .holderEncryptedAmt = holderCiphertext,
1202 .issuerEncryptedAmt = wrongIssuerCiphertext,
1203 .blindingFactor = blindingFactor,
1204 .err = tecBAD_PROOF,
1205 });
1206 }
1207
1208 // trying to convert more than what bob has
1209 {
1210 Env env{*this, features};
1211 Account const alice("alice");
1212 Account const bob("bob");
1213 MPTTester mptAlice(env, alice, {.holders = {bob}});
1214
1215 mptAlice.create({
1216 .ownerCount = 1,
1217 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1218 });
1219
1220 mptAlice.authorize({
1221 .account = bob,
1222 });
1223 mptAlice.pay(alice, bob, 100);
1224
1225 mptAlice.generateKeyPair(alice);
1226
1227 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1228
1229 mptAlice.generateKeyPair(bob);
1230
1231 mptAlice.convert({
1232 .account = bob,
1233 .amt = 200,
1234 .holderPubKey = mptAlice.getPubKey(bob),
1235 .err = tecINSUFFICIENT_FUNDS,
1236 });
1237 }
1238
1239 // holder cannot upload pk again
1240 {
1241 Env env{*this, features};
1242 Account const alice("alice");
1243 Account const bob("bob");
1244 MPTTester mptAlice(env, alice, {.holders = {bob}});
1245
1246 mptAlice.create({
1247 .ownerCount = 1,
1248 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1249 });
1250
1251 mptAlice.authorize({
1252 .account = bob,
1253 });
1254 mptAlice.pay(alice, bob, 100);
1255
1256 mptAlice.generateKeyPair(alice);
1257
1258 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1259
1260 mptAlice.generateKeyPair(bob);
1261
1262 mptAlice.convert({.account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob)});
1263
1264 // cannot upload pk again
1265 mptAlice.convert({
1266 .account = bob,
1267 .amt = 10,
1268 .holderPubKey = mptAlice.getPubKey(bob),
1269 .err = tecDUPLICATE,
1270 });
1271 }
1272
1273 // cannot convert if locked
1274 {
1275 Env env{*this, features};
1276 Account const alice("alice");
1277 Account const bob("bob");
1278 MPTTester mptAlice(env, alice, {.holders = {bob}});
1279
1280 mptAlice.create({
1281 .ownerCount = 1,
1282 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1283 });
1284
1285 mptAlice.authorize({
1286 .account = bob,
1287 });
1288 mptAlice.pay(alice, bob, 100);
1289
1290 mptAlice.generateKeyPair(alice);
1291
1292 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1293
1294 mptAlice.set({
1295 .account = alice,
1296 .holder = bob,
1297 .flags = tfMPTLock,
1298 });
1299
1300 mptAlice.generateKeyPair(bob);
1301
1302 mptAlice.convert({
1303 .account = bob,
1304 .amt = 10,
1305 .holderPubKey = mptAlice.getPubKey(bob),
1306 .err = tecLOCKED,
1307 });
1308
1309 mptAlice.set({
1310 .account = alice,
1311 .holder = bob,
1312 .flags = tfMPTUnlock,
1313 });
1314
1315 mptAlice.convert({
1316 .account = bob,
1317 .amt = 10,
1318 .holderPubKey = mptAlice.getPubKey(bob),
1319 });
1320 }
1321
1322 // cannot convert if unauth
1323 {
1324 Env env{*this, features};
1325 Account const alice("alice");
1326 Account const bob("bob");
1327 MPTTester mptAlice(env, alice, {.holders = {bob}});
1328
1329 mptAlice.create({
1330 .ownerCount = 1,
1331 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
1332 tfMPTCanHoldConfidentialBalance,
1333 });
1334
1335 mptAlice.authorize({
1336 .account = bob,
1337 });
1338 mptAlice.authorize({
1339 .account = alice,
1340 .holder = bob,
1341 });
1342 mptAlice.pay(alice, bob, 100);
1343
1344 mptAlice.generateKeyPair(alice);
1345
1346 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1347
1348 mptAlice.generateKeyPair(bob);
1349
1350 // Unauthorize bob
1351 mptAlice.authorize({
1352 .account = alice,
1353 .holder = bob,
1354 .flags = tfMPTUnauthorize,
1355 });
1356
1357 mptAlice.convert({
1358 .account = bob,
1359 .amt = 10,
1360 .holderPubKey = mptAlice.getPubKey(bob),
1361 .err = tecNO_AUTH,
1362 });
1363
1364 // auth bob
1365 mptAlice.authorize({
1366 .account = alice,
1367 .holder = bob,
1368 });
1369
1370 mptAlice.convert({
1371 .account = bob,
1372 .amt = 10,
1373 .holderPubKey = mptAlice.getPubKey(bob),
1374 });
1375 }
1376
1377 // frozen account cannot bypass freeze check with amount=0
1378 {
1379 Env env{*this, features};
1380 Account const alice("alice");
1381 Account const bob("bob");
1382 MPTTester mptAlice(env, alice, {.holders = {bob}});
1383
1384 mptAlice.create({
1385 .ownerCount = 1,
1386 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1387 });
1388
1389 mptAlice.authorize({
1390 .account = bob,
1391 });
1392 mptAlice.pay(alice, bob, 100);
1393
1394 mptAlice.generateKeyPair(alice);
1395
1396 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1397
1398 // lock bob
1399 mptAlice.set({
1400 .account = alice,
1401 .holder = bob,
1402 .flags = tfMPTLock,
1403 });
1404
1405 mptAlice.generateKeyPair(bob);
1406
1407 // amount=0 should still be rejected when locked
1408 mptAlice.convert({
1409 .account = bob,
1410 .amt = 0,
1411 .holderPubKey = mptAlice.getPubKey(bob),
1412 .err = tecLOCKED,
1413 });
1414 }
1415
1416 // unauthorized account cannot bypass auth check with amount=0
1417 {
1418 Env env{*this, features};
1419 Account const alice("alice");
1420 Account const bob("bob");
1421 MPTTester mptAlice(env, alice, {.holders = {bob}});
1422
1423 mptAlice.create({
1424 .ownerCount = 1,
1425 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
1426 tfMPTCanHoldConfidentialBalance,
1427 });
1428
1429 mptAlice.authorize({
1430 .account = bob,
1431 });
1432 mptAlice.authorize({
1433 .account = alice,
1434 .holder = bob,
1435 });
1436 mptAlice.pay(alice, bob, 100);
1437
1438 mptAlice.generateKeyPair(alice);
1439
1440 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1441
1442 mptAlice.generateKeyPair(bob);
1443
1444 // Unauthorize bob
1445 mptAlice.authorize({
1446 .account = alice,
1447 .holder = bob,
1448 .flags = tfMPTUnauthorize,
1449 });
1450
1451 // amount=0 should still be rejected when unauthorized
1452 mptAlice.convert({
1453 .account = bob,
1454 .amt = 0,
1455 .holderPubKey = mptAlice.getPubKey(bob),
1456 .err = tecNO_AUTH,
1457 });
1458 }
1459
1460 // cannot convert if auditor key is set, but auditor amount is not
1461 // provided
1462 {
1463 Env env{*this, features};
1464 Account const alice("alice");
1465 Account const bob("bob");
1466 Account const auditor("auditor");
1467 MPTTester mptAlice(
1468 env,
1469 alice,
1470 {
1471 .holders = {bob},
1472 .auditor = auditor,
1473 });
1474
1475 mptAlice.create({
1476 .ownerCount = 1,
1477 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1478 });
1479
1480 mptAlice.authorize({
1481 .account = bob,
1482 });
1483 mptAlice.pay(alice, bob, 100);
1484
1485 mptAlice.generateKeyPair(alice);
1486 mptAlice.generateKeyPair(bob);
1487 mptAlice.generateKeyPair(auditor);
1488
1489 mptAlice.set(
1490 {.account = alice,
1491 .issuerPubKey = mptAlice.getPubKey(alice),
1492 .auditorPubKey = mptAlice.getPubKey(auditor)});
1493
1494 // no auditor encrypted amt provided
1495 mptAlice.convert({
1496 .account = bob,
1497 .amt = 10,
1498 .fillAuditorEncryptedAmt = false,
1499 .holderPubKey = mptAlice.getPubKey(bob),
1500 .err = tecNO_PERMISSION,
1501 });
1502 }
1503
1504 // cannot convert if tx include auditor ciphertext, but does not have
1505 // auditing enabled
1506 {
1507 Env env{*this, features};
1508 Account const alice("alice");
1509 Account const bob("bob");
1510 MPTTester mptAlice(env, alice, {.holders = {bob}});
1511
1512 mptAlice.create({
1513 .ownerCount = 1,
1514 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1515 });
1516
1517 mptAlice.authorize({
1518 .account = bob,
1519 });
1520 mptAlice.pay(alice, bob, 100);
1521
1522 mptAlice.generateKeyPair(alice);
1523 mptAlice.generateKeyPair(bob);
1524
1525 // there is no auditor key set
1526 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1527
1528 mptAlice.convert({
1529 .account = bob,
1530 .amt = 10,
1531 .holderPubKey = mptAlice.getPubKey(bob),
1532 .auditorEncryptedAmt = getTrivialCiphertext(),
1533 .err = tecNO_PERMISSION,
1534 });
1535 }
1536
1537 // Auditor key set successfully, auditor ciphertext mathematically
1538 // correct, but contains invalid data (mismatching amount).
1539 {
1540 Env env{*this, features};
1541 Account const alice("alice");
1542 Account const bob("bob");
1543 Account const auditor("auditor");
1544 MPTTester mptAlice(
1545 env,
1546 alice,
1547 {
1548 .holders = {bob},
1549 .auditor = auditor,
1550 });
1551
1552 mptAlice.create({
1553 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1554 });
1555
1556 mptAlice.authorize({
1557 .account = bob,
1558 });
1559 mptAlice.pay(alice, bob, 100);
1560
1561 mptAlice.generateKeyPair(alice);
1562 mptAlice.generateKeyPair(bob);
1563 mptAlice.generateKeyPair(auditor);
1564
1565 mptAlice.set(
1566 {.account = alice,
1567 .issuerPubKey = mptAlice.getPubKey(alice),
1568 .auditorPubKey = mptAlice.getPubKey(auditor)});
1569
1570 mptAlice.convert({
1571 .account = bob,
1572 .amt = 10,
1573 .holderPubKey = mptAlice.getPubKey(bob),
1574 .auditorEncryptedAmt = getTrivialCiphertext(),
1575 .err = tecBAD_PROOF,
1576 });
1577 }
1578
1579 // invalid proof when registering holder pub key
1580 {
1581 Env env{*this, features};
1582 Account const alice("alice");
1583 Account const bob("bob");
1584 MPTTester mptAlice(env, alice, {.holders = {bob}});
1585
1586 mptAlice.create({
1587 .ownerCount = 1,
1588 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1589 });
1590
1591 mptAlice.authorize({
1592 .account = bob,
1593 });
1594 mptAlice.pay(alice, bob, 100);
1595
1596 mptAlice.generateKeyPair(alice);
1597 mptAlice.generateKeyPair(bob);
1598
1599 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1600
1601 mptAlice.convert({
1602 .account = bob,
1603 .amt = 10,
1604 .proof = std::string(kEcSchnorrProofLength * 2, 'A'),
1605 .holderPubKey = mptAlice.getPubKey(bob),
1606 .err = tecBAD_PROOF,
1607 });
1608 }
1609
1610 // no holder key on ledger and no key in tx
1611 {
1612 Env env{*this, features};
1613 Account const alice("alice");
1614 Account const bob("bob");
1615 MPTTester mptAlice(env, alice, {.holders = {bob}});
1616
1617 mptAlice.create({
1618 .ownerCount = 1,
1619 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1620 });
1621
1622 mptAlice.authorize({
1623 .account = bob,
1624 });
1625 mptAlice.pay(alice, bob, 100);
1626
1627 mptAlice.generateKeyPair(alice);
1628 mptAlice.generateKeyPair(bob);
1629
1630 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1631
1632 // bob has not registered a holder key, and doesn't provide one
1633 mptAlice.convert({
1634 .account = bob,
1635 .amt = 10,
1636 .err = tecNO_PERMISSION,
1637 });
1638 }
1639
1640 // all public balance already converted, try to convert more
1641 {
1642 Env env{*this, features};
1643 Account const alice("alice");
1644 Account const bob("bob");
1645 MPTTester mptAlice(env, alice, {.holders = {bob}});
1646
1647 mptAlice.create({
1648 .ownerCount = 1,
1649 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1650 });
1651
1652 mptAlice.authorize({
1653 .account = bob,
1654 });
1655 mptAlice.pay(alice, bob, 100);
1656
1657 mptAlice.generateKeyPair(alice);
1658
1659 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1660
1661 mptAlice.generateKeyPair(bob);
1662
1663 // convert entire public balance
1664 mptAlice.convert({
1665 .account = bob,
1666 .amt = 100,
1667 .holderPubKey = mptAlice.getPubKey(bob),
1668 });
1669
1670 env.require(MptBalance(mptAlice, bob, 0));
1671
1672 // try to convert 1 more — no public balance left
1673 mptAlice.convert({
1674 .account = bob,
1675 .amt = 1,
1676 .err = tecINSUFFICIENT_FUNDS,
1677 });
1678 }
1679 }
1680
1681 void
1683 {
1684 testcase("Merge inbox");
1685 using namespace test::jtx;
1686
1687 // Merge with an empty inbox should succeed as a no-op.
1688 {
1689 Env env{*this, features};
1690 Account const alice("alice");
1691 Account const bob("bob");
1692 MPTTester mptAlice(env, alice, {.holders = {bob}});
1693
1694 mptAlice.create({
1695 .ownerCount = 1,
1696 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1697 });
1698 mptAlice.authorize({.account = bob});
1699 mptAlice.pay(alice, bob, 100);
1700
1701 mptAlice.generateKeyPair(alice);
1702 mptAlice.generateKeyPair(bob);
1703 mptAlice.set({
1704 .account = alice,
1705 .issuerPubKey = mptAlice.getPubKey(alice),
1706 });
1707
1708 mptAlice.convert({
1709 .account = bob,
1710 .amt = 40,
1711 .holderPubKey = mptAlice.getPubKey(bob),
1712 });
1713
1714 mptAlice.mergeInbox({.account = bob});
1715 // Inbox is empty after the first merge; the second merge is a no-op.
1716 mptAlice.mergeInbox({.account = bob});
1717 }
1718
1719 // Makes sure if merge inbox version is UINT32_MAX, the next merge wraps
1720 // the version back to 0.
1721 {
1722 Env env{*this, features};
1723 Account const alice("alice");
1724 Account const bob("bob");
1725 MPTTester mptAlice(env, alice, {.holders = {bob}});
1726
1727 mptAlice.create({
1728 .ownerCount = 1,
1729 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1730 });
1731 mptAlice.authorize({.account = bob});
1732 mptAlice.pay(alice, bob, 100);
1733
1734 mptAlice.generateKeyPair(alice);
1735 mptAlice.generateKeyPair(bob);
1736 mptAlice.set({
1737 .account = alice,
1738 .issuerPubKey = mptAlice.getPubKey(alice),
1739 });
1740
1741 mptAlice.convert({
1742 .account = bob,
1743 .amt = 40,
1744 .holderPubKey = mptAlice.getPubKey(bob),
1745 });
1746
1747 // Force the on-ledger version to UINT32_MAX, then apply a merge and
1748 // confirm the version wraps around to 0.
1749 auto const wrappedFrom = std::numeric_limits<std::uint32_t>::max();
1750 auto const jt = env.jt(mptAlice.mergeInboxJV({.account = bob}));
1751 BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
1752 auto const sle = std::const_pointer_cast<SLE>(
1753 view.read(keylet::mptoken(mptAlice.issuanceID(), bob.id())));
1754 if (!sle)
1755 return false;
1756
1757 (*sle)[sfConfidentialBalanceVersion] = wrappedFrom;
1758 view.rawReplace(sle);
1759
1760 auto const result = xrpl::apply(env.app(), view, *jt.stx, TapNone, env.journal);
1761 BEAST_EXPECT(result.ter == tesSUCCESS);
1762 return result.applied;
1763 }));
1764
1765 BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) == 0);
1766 }
1767 }
1768
1769 void
1771 {
1772 testcase("Merge inbox preflight");
1773 using namespace test::jtx;
1774 Env env{*this, features};
1775 Account const alice("alice");
1776 Account const bob("bob");
1777 MPTTester mptAlice(env, alice, {.holders = {bob}});
1778
1779 mptAlice.create({
1780 .ownerCount = 1,
1781 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1782 });
1783
1784 mptAlice.authorize({
1785 .account = bob,
1786 });
1787 mptAlice.pay(alice, bob, 100);
1788
1789 mptAlice.generateKeyPair(alice);
1790
1791 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1792
1793 mptAlice.generateKeyPair(bob);
1794
1795 mptAlice.convert({
1796 .account = bob,
1797 .amt = 40,
1798 .holderPubKey = mptAlice.getPubKey(bob),
1799 });
1800
1801 mptAlice.mergeInbox({
1802 .account = alice,
1803 .err = temMALFORMED,
1804 });
1805
1806 env.disableFeature(featureConfidentialTransfer);
1807 env.close();
1808
1809 mptAlice.mergeInbox({
1810 .account = bob,
1811 .err = temDISABLED,
1812 });
1813 }
1814
1815 void
1817 {
1818 testcase("Merge inbox preclaim");
1819 using namespace test::jtx;
1820
1821 // issuance does not exist
1822 {
1823 Env env{*this, features};
1824 Account const alice("alice");
1825 Account const bob("bob");
1826 MPTTester mptAlice(env, alice, {.holders = {bob}});
1827
1828 mptAlice.create({
1829 .ownerCount = 1,
1830 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1831 });
1832
1833 mptAlice.authorize({
1834 .account = bob,
1835 });
1836 mptAlice.generateKeyPair(alice);
1837
1838 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1839
1840 mptAlice.destroy();
1841 mptAlice.generateKeyPair(bob);
1842
1843 mptAlice.mergeInbox({
1844 .account = bob,
1845 .err = tecOBJECT_NOT_FOUND,
1846 });
1847 }
1848
1849 // tfMPTCanHoldConfidentialBalance is not set on issuance
1850 {
1851 Env env{*this, features};
1852 Account const alice("alice");
1853 Account const bob("bob");
1854 MPTTester mptAlice(env, alice, {.holders = {bob}});
1855
1856 mptAlice.create({
1857 .ownerCount = 1,
1858 .flags = tfMPTCanTransfer | tfMPTCanLock,
1859 });
1860
1861 mptAlice.authorize({
1862 .account = bob,
1863 });
1864 mptAlice.pay(alice, bob, 100);
1865
1866 mptAlice.generateKeyPair(alice);
1867 mptAlice.generateKeyPair(bob);
1868
1869 mptAlice.mergeInbox({
1870 .account = bob,
1871 .err = tecNO_PERMISSION,
1872 });
1873 }
1874
1875 // no mptoken
1876 {
1877 Env env{*this, features};
1878 Account const alice("alice");
1879 Account const bob("bob");
1880 MPTTester mptAlice(env, alice, {.holders = {bob}});
1881
1882 mptAlice.create({
1883 .ownerCount = 1,
1884 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1885 });
1886
1887 mptAlice.generateKeyPair(alice);
1888
1889 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1890
1891 mptAlice.mergeInbox({
1892 .account = bob,
1893 .err = tecOBJECT_NOT_FOUND,
1894 });
1895 }
1896
1897 // bob doesn't have encrypted balances
1898 {
1899 Env env{*this, features};
1900 Account const alice("alice");
1901 Account const bob("bob");
1902 MPTTester mptAlice(env, alice, {.holders = {bob}});
1903
1904 mptAlice.create({
1905 .ownerCount = 1,
1906 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1907 });
1908
1909 mptAlice.authorize({
1910 .account = bob,
1911 });
1912 mptAlice.pay(alice, bob, 100);
1913
1914 mptAlice.generateKeyPair(alice);
1915
1916 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1917
1918 mptAlice.generateKeyPair(bob);
1919
1920 mptAlice.mergeInbox({
1921 .account = bob,
1922 .err = tecNO_PERMISSION,
1923 });
1924 }
1925
1926 // holder is locked
1927 {
1928 Env env{*this, features};
1929 Account const alice("alice");
1930 Account const bob("bob");
1931 MPTTester mptAlice(env, alice, {.holders = {bob}});
1932
1933 mptAlice.create({
1934 .ownerCount = 1,
1935 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
1936 });
1937
1938 mptAlice.authorize({
1939 .account = bob,
1940 });
1941 mptAlice.pay(alice, bob, 100);
1942
1943 mptAlice.generateKeyPair(alice);
1944 mptAlice.generateKeyPair(bob);
1945
1946 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1947
1948 mptAlice.convert({
1949 .account = bob,
1950 .amt = 50,
1951 .holderPubKey = mptAlice.getPubKey(bob),
1952 });
1953
1954 // lock bob
1955 mptAlice.set({
1956 .account = alice,
1957 .holder = bob,
1958 .flags = tfMPTLock,
1959 });
1960
1961 mptAlice.mergeInbox({
1962 .account = bob,
1963 .err = tecLOCKED,
1964 });
1965
1966 // unlock bob
1967 mptAlice.set({
1968 .account = alice,
1969 .holder = bob,
1970 .flags = tfMPTUnlock,
1971 });
1972
1973 // should succeed now
1974 mptAlice.mergeInbox({
1975 .account = bob,
1976 });
1977 }
1978
1979 // holder not authorized
1980 {
1981 Env env{*this, features};
1982 Account const alice("alice");
1983 Account const bob("bob");
1984 MPTTester mptAlice(env, alice, {.holders = {bob}});
1985
1986 mptAlice.create({
1987 .ownerCount = 1,
1988 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance |
1989 tfMPTRequireAuth,
1990 });
1991
1992 mptAlice.authorize({
1993 .account = bob,
1994 });
1995 mptAlice.authorize({
1996 .account = alice,
1997 .holder = bob,
1998 });
1999 mptAlice.pay(alice, bob, 100);
2000
2001 mptAlice.generateKeyPair(alice);
2002 mptAlice.generateKeyPair(bob);
2003
2004 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
2005
2006 mptAlice.convert({
2007 .account = bob,
2008 .amt = 50,
2009 .holderPubKey = mptAlice.getPubKey(bob),
2010 });
2011
2012 // unauthorize bob
2013 mptAlice.authorize({
2014 .account = alice,
2015 .holder = bob,
2016 .flags = tfMPTUnauthorize,
2017 });
2018
2019 mptAlice.mergeInbox({
2020 .account = bob,
2021 .err = tecNO_AUTH,
2022 });
2023
2024 // authorize bob again
2025 mptAlice.authorize({
2026 .account = alice,
2027 .holder = bob,
2028 });
2029
2030 // should succeed now
2031 mptAlice.mergeInbox({
2032 .account = bob,
2033 });
2034 }
2035 }
2036
2037 void
2039 {
2040 testcase("test confidential send");
2041 using namespace test::jtx;
2042 Env env{*this, features};
2043 Account const alice("alice"), bob("bob"), carol("carol");
2044 ConfidentialEnv confEnv{
2045 env,
2046 alice,
2047 {{.account = bob, .payAmount = 100, .convertAmount = 60},
2048 {.account = carol, .payAmount = 50, .convertAmount = 20}}};
2049 auto& mptAlice = confEnv.mpt;
2050
2051 // bob sends 10 to carol
2052 mptAlice.send({
2053 .account = bob,
2054 .dest = carol,
2055 .amt = 10,
2056 });
2057
2058 // bob sends 1 to carol again
2059 mptAlice.send({
2060 .account = bob,
2061 .dest = carol,
2062 .amt = 1,
2063 });
2064
2065 mptAlice.mergeInbox({
2066 .account = carol,
2067 });
2068
2069 // carol sends 15 back to bob
2070 mptAlice.send({
2071 .account = carol,
2072 .dest = bob,
2073 .amt = 15,
2074 });
2075 }
2076
2077 void
2079 {
2080 testcase("test confidential send with auditor");
2081 using namespace test::jtx;
2082 Env env{*this, features};
2083 Account const alice("alice");
2084 Account const bob("bob");
2085 Account const carol("carol");
2086 Account const auditor("auditor");
2087 ConfidentialEnv confEnv{
2088 env,
2089 alice,
2090 {{.account = bob, .payAmount = 100, .convertAmount = 60},
2091 {.account = carol, .payAmount = 50, .convertAmount = 20}},
2092 tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
2093 auditor};
2094 auto& mptAlice = confEnv.mpt;
2095
2096 // bob sends 10 to carol
2097 mptAlice.send({
2098 .account = bob,
2099 .dest = carol,
2100 .amt = 10,
2101 });
2102
2103 // bob sends 1 to carol again
2104 mptAlice.send({
2105 .account = bob,
2106 .dest = carol,
2107 .amt = 1,
2108 });
2109
2110 mptAlice.mergeInbox({
2111 .account = carol,
2112 });
2113
2114 // carol sends 15 back to bob
2115 mptAlice.send({
2116 .account = carol,
2117 .dest = bob,
2118 .amt = 15,
2119 });
2120 }
2121
2122 void
2124 {
2125 testcase("test ConfidentialMPTSend Preflight");
2126 using namespace test::jtx;
2127
2128 // test disabled
2129 {
2130 Env env{*this, features - featureConfidentialTransfer};
2131 Account const alice("alice");
2132 Account const bob("bob");
2133 Account const carol("carol");
2134 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
2135
2136 mptAlice.create();
2137 mptAlice.authorize({
2138 .account = bob,
2139 });
2140 mptAlice.authorize({
2141 .account = carol,
2142 });
2143
2144 mptAlice.send({
2145 .account = bob,
2146 .dest = carol,
2147 .amt = 10,
2148 .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2149 .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2150 .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2151 .err = temDISABLED,
2152 });
2153 }
2154
2155 // test malformed
2156 {
2157 Env env{*this, features};
2158 Account const alice("alice");
2159 Account const bob("bob");
2160 Account const carol("carol");
2161 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
2162
2163 mptAlice.create({
2164 .ownerCount = 1,
2165 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2166 });
2167
2168 mptAlice.authorize({
2169 .account = bob,
2170 });
2171 mptAlice.authorize({
2172 .account = carol,
2173 });
2174 mptAlice.generateKeyPair(alice);
2175 mptAlice.generateKeyPair(bob);
2176 mptAlice.generateKeyPair(carol);
2177 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
2178 mptAlice.pay(alice, bob, 100);
2179 mptAlice.pay(alice, carol, 50);
2180
2181 mptAlice.convert({
2182 .account = bob,
2183 .amt = 50,
2184 .holderPubKey = mptAlice.getPubKey(bob),
2185 });
2186
2187 mptAlice.convert({
2188 .account = carol,
2189 .amt = 40,
2190 .holderPubKey = mptAlice.getPubKey(carol),
2191 });
2192
2193 // issuer can not be the same as sender
2194 mptAlice.send({
2195 .account = alice,
2196 .dest = carol,
2197 .amt = 10,
2198 .err = temMALFORMED,
2199 });
2200
2201 // can not send to self
2202 mptAlice.send({
2203 .account = bob,
2204 .dest = bob,
2205 .amt = 10,
2206 .err = temMALFORMED,
2207 });
2208
2209 // can not send to issuer
2210 mptAlice.send({
2211 .account = bob,
2212 .dest = alice,
2213 .amt = 10,
2214 .err = temMALFORMED,
2215 });
2216
2217 // sender encrypted amount wrong length
2218 mptAlice.send({
2219 .account = bob,
2220 .dest = carol,
2221 .amt = 10,
2222 .senderEncryptedAmt = gMakeZeroBuffer(10),
2223 .err = temBAD_CIPHERTEXT,
2224 });
2225
2226 // dest encrypted amount wrong length
2227 mptAlice.send({
2228 .account = bob,
2229 .dest = carol,
2230 .amt = 10,
2231 .destEncryptedAmt = gMakeZeroBuffer(10),
2232 .err = temBAD_CIPHERTEXT,
2233 });
2234
2235 // issuer encrypted amount wrong length
2236 mptAlice.send({
2237 .account = bob,
2238 .dest = carol,
2239 .amt = 10,
2240 .issuerEncryptedAmt = gMakeZeroBuffer(10),
2241 .err = temBAD_CIPHERTEXT,
2242 });
2243
2244 // sender encrypted amount malformed
2245 mptAlice.send({
2246 .account = bob,
2247 .dest = carol,
2248 .amt = 10,
2249 .proof = getTrivialSendProofHex(),
2250 .senderEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2251 .amountCommitment = getTrivialCommitment(),
2252 .balanceCommitment = getTrivialCommitment(),
2253 .err = temBAD_CIPHERTEXT,
2254 });
2255
2256 // dest encrypted amount malformed
2257 mptAlice.send({
2258 .account = bob,
2259 .dest = carol,
2260 .amt = 10,
2261 .proof = getTrivialSendProofHex(),
2262 .destEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2263 .amountCommitment = getTrivialCommitment(),
2264 .balanceCommitment = getTrivialCommitment(),
2265 .err = temBAD_CIPHERTEXT,
2266 });
2267
2268 // issuer encrypted amount malformed
2269 mptAlice.send({
2270 .account = bob,
2271 .dest = carol,
2272 .amt = 10,
2273 .proof = getTrivialSendProofHex(),
2274 .issuerEncryptedAmt = gMakeZeroBuffer(kEcGamalEncryptedTotalLength),
2275 .amountCommitment = getTrivialCommitment(),
2276 .balanceCommitment = getTrivialCommitment(),
2277 .err = temBAD_CIPHERTEXT,
2278 });
2279
2280 // invalid proof length
2281 mptAlice.send({
2282 .account = bob,
2283 .dest = carol,
2284 .amt = 10,
2285 .proof = std::string(10, 'A'),
2286 .amountCommitment = getTrivialCommitment(),
2287 .balanceCommitment = getTrivialCommitment(),
2288 .err = temMALFORMED,
2289 });
2290
2291 // invalid amount Pedersen commitment length
2292 mptAlice.send({
2293 .account = bob,
2294 .dest = carol,
2295 .amt = 10,
2296 .proof = getTrivialSendProofHex(),
2297 .amountCommitment = gMakeZeroBuffer(100),
2298 .balanceCommitment = getTrivialCommitment(),
2299 .err = temMALFORMED,
2300 });
2301
2302 // invalid balance Pedersen commitment length
2303 mptAlice.send({
2304 .account = bob,
2305 .dest = carol,
2306 .amt = 10,
2307 .proof = getTrivialSendProofHex(),
2308 .amountCommitment = getTrivialCommitment(),
2309 .balanceCommitment = gMakeZeroBuffer(100),
2310 .err = temMALFORMED,
2311 });
2312
2313 // amount Pedersen commitment has correct length but invalid EC point data
2314 mptAlice.send({
2315 .account = bob,
2316 .dest = carol,
2317 .amt = 10,
2318 .proof = getTrivialSendProofHex(),
2319 .amountCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
2320 .balanceCommitment = getTrivialCommitment(),
2321 .err = temMALFORMED,
2322 });
2323
2324 // balance Pedersen commitment has correct length but invalid EC point data
2325 mptAlice.send({
2326 .account = bob,
2327 .dest = carol,
2328 .amt = 10,
2329 .proof = getTrivialSendProofHex(),
2330 .amountCommitment = getTrivialCommitment(),
2331 .balanceCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
2332 .err = temMALFORMED,
2333 });
2334 }
2335
2336 // test bad ciphertext
2337 {
2338 Env env{*this, features};
2339 Account const alice("alice");
2340 Account const bob("bob");
2341 Account const carol("carol");
2342 Account const auditor("auditor");
2343 MPTTester mptAlice(
2344 env,
2345 alice,
2346 {
2347 .holders = {bob, carol},
2348 .auditor = auditor,
2349 });
2350
2351 mptAlice.create({
2352 .ownerCount = 1,
2353 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2354 });
2355
2356 mptAlice.authorize({
2357 .account = bob,
2358 });
2359 mptAlice.authorize({
2360 .account = carol,
2361 });
2362 mptAlice.generateKeyPair(alice);
2363 mptAlice.generateKeyPair(bob);
2364 mptAlice.generateKeyPair(carol);
2365 mptAlice.generateKeyPair(auditor);
2366
2367 mptAlice.set(
2368 {.account = alice,
2369 .issuerPubKey = mptAlice.getPubKey(alice),
2370 .auditorPubKey = mptAlice.getPubKey(auditor)});
2371 mptAlice.pay(alice, bob, 100);
2372 mptAlice.pay(alice, carol, 50);
2373
2374 mptAlice.convert({
2375 .account = bob,
2376 .amt = 50,
2377 .holderPubKey = mptAlice.getPubKey(bob),
2378 });
2379
2380 mptAlice.convert({
2381 .account = carol,
2382 .amt = 40,
2383 .holderPubKey = mptAlice.getPubKey(carol),
2384 });
2385
2386 // auditor encrypted amount wrong length
2387 mptAlice.send({
2388 .account = bob,
2389 .dest = carol,
2390 .amt = 10,
2391 .proof = getTrivialSendProofHex(),
2392 .auditorEncryptedAmt = gMakeZeroBuffer(10),
2393 .amountCommitment = getTrivialCommitment(),
2394 .balanceCommitment = getTrivialCommitment(),
2395 .err = temBAD_CIPHERTEXT,
2396 });
2397
2398 // auditor encrypted amount (correct length, invalid data)
2399 mptAlice.send({
2400 .account = bob,
2401 .dest = carol,
2402 .amt = 10,
2403 .proof = getTrivialSendProofHex(),
2404 .auditorEncryptedAmt = getBadCiphertext(),
2405 .amountCommitment = getTrivialCommitment(),
2406 .balanceCommitment = getTrivialCommitment(),
2407 .err = temBAD_CIPHERTEXT,
2408 });
2409 }
2410 }
2411
2412 void
2414 {
2415 testcase("test ConfidentialMPTSend Preclaim");
2416
2417 using namespace test::jtx;
2418 Env env{*this, features};
2419 Account const alice("alice");
2420 Account const bob("bob");
2421 Account const carol("carol");
2422 Account const dave("dave");
2423 Account const eve("eve");
2424 MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave, eve}});
2425
2426 // authorize bob, carol, dave (not eve)
2427 mptAlice.create({
2428 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
2429 tfMPTCanHoldConfidentialBalance,
2430 });
2431 mptAlice.authorize({
2432 .account = bob,
2433 });
2434 mptAlice.authorize({
2435 .account = alice,
2436 .holder = bob,
2437 });
2438 mptAlice.authorize({
2439 .account = carol,
2440 });
2441 mptAlice.authorize({
2442 .account = alice,
2443 .holder = carol,
2444 });
2445 mptAlice.authorize({
2446 .account = dave,
2447 });
2448 mptAlice.authorize({
2449 .account = alice,
2450 .holder = dave,
2451 });
2452
2453 // fund bob, carol (not dave or eve)
2454 mptAlice.pay(alice, bob, 100);
2455 mptAlice.pay(alice, carol, 50);
2456
2457 mptAlice.generateKeyPair(alice);
2458 mptAlice.generateKeyPair(bob);
2459 mptAlice.generateKeyPair(carol);
2460 mptAlice.generateKeyPair(dave);
2461 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
2462
2463 // bob and carol convert some funds to confidential
2464 mptAlice.convert({
2465 .account = bob,
2466 .amt = 60,
2467 .holderPubKey = mptAlice.getPubKey(bob),
2468 .err = tesSUCCESS,
2469 });
2470 mptAlice.convert({
2471 .account = carol,
2472 .amt = 20,
2473 .holderPubKey = mptAlice.getPubKey(carol),
2474 .err = tesSUCCESS,
2475 });
2476
2477 // bob and carol merge inbox
2478 mptAlice.mergeInbox({
2479 .account = bob,
2480 });
2481 mptAlice.mergeInbox({
2482 .account = carol,
2483 });
2484
2485 // issuance not found
2486 {
2487 Env env{*this, features};
2488 Account const alice("alice");
2489 Account const bob("bob");
2490 Account const carol("carol");
2491 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
2492
2493 mptAlice.create({
2494 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2495 });
2496 mptAlice.authorize({
2497 .account = bob,
2498 });
2499 mptAlice.authorize({
2500 .account = carol,
2501 });
2502 mptAlice.generateKeyPair(alice);
2503 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
2504
2505 // destroy the issuance
2506 mptAlice.destroy();
2507
2508 json::Value jv;
2509 jv[jss::Account] = bob.human();
2510 jv[jss::Destination] = carol.human();
2511 jv[jss::TransactionType] = jss::ConfidentialMPTSend;
2512 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
2513 jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext());
2514 jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext());
2515 jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext());
2516 jv[sfAmountCommitment] = strHex(getTrivialCommitment());
2517 jv[sfBalanceCommitment] = strHex(getTrivialCommitment());
2518 jv[sfZKProof] = getTrivialSendProofHex();
2519
2520 env(jv, Ter(tecOBJECT_NOT_FOUND));
2521 }
2522
2523 // destination does not exist
2524 {
2525 Account const unknown("unknown");
2526 mptAlice.send({
2527 .account = bob,
2528 .dest = unknown,
2529 .amt = 10,
2530 .proof = getTrivialSendProofHex(),
2531 .senderEncryptedAmt = getTrivialCiphertext(),
2532 .destEncryptedAmt = getTrivialCiphertext(),
2533 .issuerEncryptedAmt = getTrivialCiphertext(),
2534 .amountCommitment = getTrivialCommitment(),
2535 .balanceCommitment = getTrivialCommitment(),
2536 .err = tecNO_TARGET,
2537 });
2538 }
2539
2540 // destination requires destination tag but none provided
2541 {
2542 env(fset(carol, asfRequireDest));
2543 env.close();
2544
2545 mptAlice.send({
2546 .account = bob,
2547 .dest = carol,
2548 .amt = 10,
2549 .proof = getTrivialSendProofHex(),
2550 .senderEncryptedAmt = getTrivialCiphertext(),
2551 .destEncryptedAmt = getTrivialCiphertext(),
2552 .issuerEncryptedAmt = getTrivialCiphertext(),
2553 .amountCommitment = getTrivialCommitment(),
2554 .balanceCommitment = getTrivialCommitment(),
2555 .err = tecDST_TAG_NEEDED,
2556 });
2557
2558 env(fclear(carol, asfRequireDest));
2559 env.close();
2560 }
2561
2562 // dave exists, but has no confidential fields (never converted)
2563 {
2564 mptAlice.send({
2565 .account = bob,
2566 .dest = dave,
2567 .amt = 10,
2568 .proof = getTrivialSendProofHex(),
2569 .senderEncryptedAmt = getTrivialCiphertext(),
2570 .destEncryptedAmt = getTrivialCiphertext(),
2571 .issuerEncryptedAmt = getTrivialCiphertext(),
2572 .amountCommitment = getTrivialCommitment(),
2573 .balanceCommitment = getTrivialCommitment(),
2574 .err = tecNO_PERMISSION,
2575 });
2576 mptAlice.send({
2577 .account = dave,
2578 .dest = carol,
2579 .amt = 10,
2580 .proof = getTrivialSendProofHex(),
2581 .senderEncryptedAmt = getTrivialCiphertext(),
2582 .destEncryptedAmt = getTrivialCiphertext(),
2583 .issuerEncryptedAmt = getTrivialCiphertext(),
2584 .amountCommitment = getTrivialCommitment(),
2585 .balanceCommitment = getTrivialCommitment(),
2586 .err = tecNO_PERMISSION,
2587 });
2588 }
2589
2590 // destination exists but has no MPT object.
2591 {
2592 mptAlice.send({
2593 .account = bob,
2594 .dest = eve,
2595 .amt = 10,
2596 .proof = getTrivialSendProofHex(),
2597 .senderEncryptedAmt = getTrivialCiphertext(),
2598 .destEncryptedAmt = getTrivialCiphertext(),
2599 .issuerEncryptedAmt = getTrivialCiphertext(),
2600 .amountCommitment = getTrivialCommitment(),
2601 .balanceCommitment = getTrivialCommitment(),
2602 .err = tecOBJECT_NOT_FOUND,
2603 });
2604 }
2605
2606 // issuance is locked globally
2607 {
2608 // lock issuance
2609 mptAlice.set({
2610 .account = alice,
2611 .flags = tfMPTLock,
2612 });
2613 mptAlice.send({
2614 .account = bob,
2615 .dest = carol,
2616 .amt = 10,
2617 .err = tecLOCKED,
2618 });
2619 // unlock issuance
2620 mptAlice.set({
2621 .account = alice,
2622 .flags = tfMPTUnlock,
2623 });
2624 // now can send
2625 mptAlice.send({
2626 .account = bob,
2627 .dest = carol,
2628 .amt = 1,
2629 });
2630 }
2631
2632 // sender is locked
2633 {
2634 // lock bob
2635 mptAlice.set({
2636 .account = alice,
2637 .holder = bob,
2638 .flags = tfMPTLock,
2639 });
2640 mptAlice.send({
2641 .account = bob,
2642 .dest = carol,
2643 .amt = 10,
2644 .err = tecLOCKED,
2645 });
2646 // unlock bob
2647 mptAlice.set({
2648 .account = alice,
2649 .holder = bob,
2650 .flags = tfMPTUnlock,
2651 });
2652 // now can send
2653 mptAlice.send({
2654 .account = bob,
2655 .dest = carol,
2656 .amt = 2,
2657 });
2658 }
2659
2660 // destination is locked
2661 {
2662 // lock carol
2663 mptAlice.set({
2664 .account = alice,
2665 .holder = carol,
2666 .flags = tfMPTLock,
2667 });
2668 mptAlice.send({
2669 .account = bob,
2670 .dest = carol,
2671 .amt = 10,
2672 .err = tecLOCKED,
2673 });
2674 // unlock carol
2675 mptAlice.set({
2676 .account = alice,
2677 .holder = carol,
2678 .flags = tfMPTUnlock,
2679 });
2680 // now can send
2681 mptAlice.send({
2682 .account = bob,
2683 .dest = carol,
2684 .amt = 3,
2685 });
2686 }
2687
2688 // sender not authorized
2689 {
2690 // unauthorize bob
2691 mptAlice.authorize({
2692 .account = alice,
2693 .holder = bob,
2694 .flags = tfMPTUnauthorize,
2695 });
2696 mptAlice.send({
2697 .account = bob,
2698 .dest = carol,
2699 .amt = 10,
2700 .err = tecNO_AUTH,
2701 });
2702 // authorize bob again
2703 mptAlice.authorize({
2704 .account = alice,
2705 .holder = bob,
2706 });
2707 // now can send
2708 mptAlice.send({
2709 .account = bob,
2710 .dest = carol,
2711 .amt = 4,
2712 });
2713 }
2714
2715 // destination not authorized
2716 {
2717 // unauthorize carol
2718 mptAlice.authorize({
2719 .account = alice,
2720 .holder = carol,
2721 .flags = tfMPTUnauthorize,
2722 });
2723 mptAlice.send({
2724 .account = bob,
2725 .dest = carol,
2726 .amt = 10,
2727 .err = tecNO_AUTH,
2728 });
2729 // authorize carol again
2730 mptAlice.authorize({
2731 .account = alice,
2732 .holder = carol,
2733 });
2734 // now can send
2735 mptAlice.send({
2736 .account = bob,
2737 .dest = carol,
2738 .amt = 5,
2739 });
2740 }
2741
2742 // cannot send when MPTCanTransfer is not set
2743 {
2744 Env env{*this, features};
2745 Account const alice("alice");
2746 Account const bob("bob");
2747 Account const carol("carol");
2748 ConfidentialEnv confEnv{
2749 env,
2750 alice,
2751 {{.account = bob, .payAmount = 100, .convertAmount = 60},
2752 {.account = carol, .payAmount = 50, .convertAmount = 20}},
2753 tfMPTCanLock | tfMPTCanHoldConfidentialBalance};
2754 auto& mptAlice = confEnv.mpt;
2755
2756 // bob sends 10 to carol
2757 mptAlice.send({
2758 .account = bob,
2759 .dest = carol,
2760 .amt = 10, // will be encrypted internally
2761 .err = tecNO_AUTH,
2762 });
2763 }
2764
2765 // Confidential MPTs should not have a transfer fee. Force malformed
2766 // ledger state to cover the defensive preclaim check.
2767 {
2768 Env env{*this, features};
2769 Account const alice("alice");
2770 Account const bob("bob");
2771 Account const carol("carol");
2772 ConfidentialEnv confEnv{
2773 env,
2774 alice,
2775 {{.account = bob, .payAmount = 100, .convertAmount = 60},
2776 {.account = carol, .payAmount = 50, .convertAmount = 20}}};
2777 auto& mptAlice = confEnv.mpt;
2778
2779 BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
2780 auto const issuance = std::const_pointer_cast<SLE>(
2781 view.read(keylet::mptokenIssuance(mptAlice.issuanceID())));
2782 if (!issuance)
2783 return false;
2784
2785 issuance->setFieldU16(sfTransferFee, 1);
2786 view.rawReplace(issuance);
2787 return true;
2788 }));
2789
2790 mptAlice.send({
2791 .account = bob,
2792 .dest = carol,
2793 .amt = 10,
2794 .proof = getTrivialSendProofHex(),
2795 .err = tecNO_PERMISSION,
2796 });
2797 }
2798
2799 // bad proof
2800 {
2801 Env env{*this, features};
2802 Account const alice("alice");
2803 Account const bob("bob");
2804 Account const carol("carol");
2805 ConfidentialEnv confEnv{
2806 env,
2807 alice,
2808 {{.account = bob, .payAmount = 100, .convertAmount = 60},
2809 {.account = carol, .payAmount = 50, .convertAmount = 20}}};
2810 auto& mptAlice = confEnv.mpt;
2811
2812 mptAlice.send({
2813 .account = bob,
2814 .dest = carol,
2815 .amt = 10,
2816 .proof = getTrivialSendProofHex(),
2817 .err = tecBAD_PROOF,
2818 });
2819 }
2820
2821 // No Auditor key set, but auditor encrypted amt provided
2822 {
2823 mptAlice.send({
2824 .account = bob,
2825 .dest = carol,
2826 .amt = 10,
2827 .proof = getTrivialSendProofHex(),
2828 .auditorEncryptedAmt = getTrivialCiphertext(),
2829 .err = tecNO_PERMISSION,
2830 });
2831 }
2832
2833 // Auditor CipherText is Valid, but does not match the Txn Amount
2834 {
2835 Env env{*this, features};
2836 Account const alice("alice");
2837 Account const bob("bob");
2838 Account const carol("carol");
2839 Account const auditor("auditor");
2840 MPTTester mptAlice(
2841 env,
2842 alice,
2843 {
2844 .holders = {bob, carol},
2845 .auditor = auditor,
2846 });
2847
2848 mptAlice.create({
2849 .ownerCount = 1,
2850 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2851 });
2852
2853 mptAlice.authorize({
2854 .account = bob,
2855 });
2856 mptAlice.authorize({
2857 .account = carol,
2858 });
2859 mptAlice.generateKeyPair(alice);
2860 mptAlice.generateKeyPair(bob);
2861 mptAlice.generateKeyPair(carol);
2862 mptAlice.generateKeyPair(auditor);
2863
2864 mptAlice.set(
2865 {.account = alice,
2866 .issuerPubKey = mptAlice.getPubKey(alice),
2867 .auditorPubKey = mptAlice.getPubKey(auditor)});
2868 mptAlice.pay(alice, bob, 100);
2869 mptAlice.pay(alice, carol, 50);
2870
2871 mptAlice.convert({
2872 .account = bob,
2873 .amt = 50,
2874 .holderPubKey = mptAlice.getPubKey(bob),
2875 });
2876
2877 mptAlice.convert({
2878 .account = carol,
2879 .amt = 40,
2880 .holderPubKey = mptAlice.getPubKey(carol),
2881 });
2882
2883 mptAlice.send({
2884 .account = bob,
2885 .dest = carol,
2886 .amt = 10,
2887 .proof = getTrivialSendProofHex(),
2888 .auditorEncryptedAmt = getTrivialCiphertext(),
2889 .amountCommitment = getTrivialCommitment(),
2890 .balanceCommitment = getTrivialCommitment(),
2891 .err = tecBAD_PROOF,
2892 });
2893 }
2894 }
2895
2896 void
2898 {
2899 testcase("test ConfidentialMPTSend Range Proof");
2900
2901 using namespace test::jtx;
2902 Env env{*this, features};
2903 Account const alice("alice"), bob("bob"), carol("carol");
2904 ConfidentialEnv confEnv{
2905 env,
2906 alice,
2907 {{.account = bob, .payAmount = 1000, .convertAmount = 60},
2908 {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
2909 auto& mptAlice = confEnv.mpt;
2910
2911 {
2912 // Bob has 60, tries to send 70. Invalid remaining balance.
2913 mptAlice.send({
2914 .account = bob,
2915 .dest = carol,
2916 .amt = 70,
2917 .err = tecBAD_PROOF,
2918 });
2919
2920 // Bob has 60, tries to send 61. Invalid remaining balance.
2921 mptAlice.send({
2922 .account = bob,
2923 .dest = carol,
2924 .amt = 61,
2925 .err = tecBAD_PROOF,
2926 });
2927
2928 // Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance.
2929 mptAlice.send({
2930 .account = bob,
2931 .dest = carol,
2932 .amt = 60,
2933 .err = tesSUCCESS,
2934 });
2935 }
2936
2937 {
2938 // Bob converts 100.
2939 mptAlice.convert({
2940 .account = bob,
2941 .amt = 100,
2942 });
2943 mptAlice.mergeInbox({
2944 .account = bob,
2945 });
2946
2947 // Bob has 100, tries to send 2^64-1. Invalid remaining balance.
2948 mptAlice.send({
2949 .account = bob,
2950 .dest = carol,
2952 .err = tecBAD_PROOF,
2953 });
2954
2955 // Bob sends 1, remaining 99.
2956 mptAlice.send({
2957 .account = bob,
2958 .dest = carol,
2959 .amt = 1,
2960 .err = tesSUCCESS,
2961 });
2962
2963 // Bob sends 100, but only has 99. Invalid remaining balance.
2964 mptAlice.send({
2965 .account = bob,
2966 .dest = carol,
2967 .amt = 100,
2968 .err = tecBAD_PROOF,
2969 });
2970 }
2971
2972 // send when spending balance is 0 (key registered, inbox merged, but nothing converted)
2973 {
2974 // Register keys only (amt=0) for both parties — spending stays 0.
2975 Env env2{*this, features};
2976 Account const alice2("alice"), bob2("bob"), carol2("carol");
2977 ConfidentialEnv zeroEnv{
2978 env2,
2979 alice2,
2980 {{.account = bob2, .payAmount = 100, .convertAmount = 0},
2981 {.account = carol2, .payAmount = 50, .convertAmount = 0}}};
2982 auto& mptAlice2 = zeroEnv.mpt;
2983
2984 // Trying to send any amount with 0 spending balance must fail:
2985 // the range proof for < 0 is invalid.
2986 mptAlice2.send({
2987 .account = bob2,
2988 .dest = carol2,
2989 .amt = 1,
2990 .err = tecBAD_PROOF,
2991 });
2992
2993 BEAST_EXPECT(
2994 mptAlice2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 0);
2995 }
2996
2997 // todo: test m exceeding range, require using scala and refactor
2998 }
2999
3000 /* The equality proof library and range proof library do not
3001 * support generating proofs for amt=0 (they require a positive witness).
3002 * To test the VERIFIER without crashing the helper, we bypass normal proof
3003 * generation by supplying explicit ciphertexts, commitments, and a dummy
3004 * (all-zero) proof. The preflight has no temBAD_AMOUNT guard for
3005 * ConfidentialMPTSend, so all validation occurs in verifySendProofs.
3006 */
3007 void
3009 {
3010 testcase("Send: zero amount — equality and range proof verifier behavior");
3011 using namespace test::jtx;
3012
3013 Env env{*this, features};
3014 Account const alice("alice");
3015 Account const bob("bob");
3016 Account const carol("carol");
3017 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
3018
3019 mptAlice.create({
3020 .ownerCount = 1,
3021 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3022 });
3023 mptAlice.authorize({.account = bob});
3024 mptAlice.authorize({.account = carol});
3025 mptAlice.pay(alice, bob, 100);
3026 mptAlice.pay(alice, carol, 50);
3027
3028 mptAlice.generateKeyPair(alice);
3029 mptAlice.generateKeyPair(bob);
3030 mptAlice.generateKeyPair(carol);
3031
3032 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3033
3034 mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)});
3035 mptAlice.mergeInbox({.account = bob});
3036
3037 mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
3038 mptAlice.mergeInbox({.account = carol});
3039
3040 Buffer const bf = generateBlindingFactor();
3041
3042 // equality proof verification for amt=0.
3043 // Encrypt 0 under each participant's key. The amount commitment is
3044 // getTrivialCommitment() — a valid EC point that passes preflight's
3045 // isValidCompressedECPoint check but is not the true PC for amt=0.
3046 // The dummy ZKProof's equality component must be rejected by
3047 // verifyMultiCiphertextEqualityProof.
3048 mptAlice.send({
3049 .account = bob,
3050 .dest = carol,
3051 .amt = 0,
3052 .proof = getTrivialSendProofHex(),
3053 .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf),
3054 .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf),
3055 .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf),
3056 .amountCommitment = getTrivialCommitment(),
3057 .balanceCommitment = getTrivialCommitment(),
3058 .err = tecBAD_PROOF,
3059 });
3060
3061 // range proof verification for amt=0.
3062 // Identical construction; focuses on the bulletproof range check
3063 // embedded in ZKProof. The range proof for amount=0 with a dummy
3064 // (all-zero) proof must also be rejected.
3065 Buffer const bf2 = generateBlindingFactor();
3066 mptAlice.send({
3067 .account = bob,
3068 .dest = carol,
3069 .amt = 0,
3070 .proof = getTrivialSendProofHex(),
3071 .senderEncryptedAmt = mptAlice.encryptAmount(bob, 0, bf2),
3072 .destEncryptedAmt = mptAlice.encryptAmount(carol, 0, bf2),
3073 .issuerEncryptedAmt = mptAlice.encryptAmount(alice, 0, bf2),
3074 .amountCommitment = getTrivialCommitment(),
3075 .balanceCommitment = getTrivialCommitment(),
3076 .err = tecBAD_PROOF,
3077 });
3078
3079 // All rejected sends must leave balances unchanged.
3080 BEAST_EXPECT(mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) == 100);
3081 BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
3082 }
3083
3084 void
3086 {
3087 testcase("Delete");
3088 using namespace test::jtx;
3089
3090 // cannot delete mptoken where it has encrypted balance
3091 {
3092 Env env{*this, features};
3093 Account const alice("alice");
3094 Account const bob("bob");
3095 MPTTester mptAlice(env, alice, {.holders = {bob}});
3096
3097 mptAlice.create({
3098 .ownerCount = 1,
3099 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3100 });
3101
3102 mptAlice.authorize({
3103 .account = bob,
3104 });
3105 mptAlice.pay(alice, bob, 100);
3106
3107 mptAlice.generateKeyPair(alice);
3108
3109 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3110
3111 mptAlice.generateKeyPair(bob);
3112
3113 mptAlice.convert({
3114 .account = bob,
3115 .amt = 100,
3116 .holderPubKey = mptAlice.getPubKey(bob),
3117 });
3118
3119 mptAlice.authorize({
3120 .account = bob,
3121 .flags = tfMPTUnauthorize,
3122 .err = tecHAS_OBLIGATIONS,
3123 });
3124 }
3125
3126 // cannot delete mptoken where it has encrypted balance
3127 {
3128 Env env{*this, features};
3129 Account const alice("alice");
3130 Account const bob("bob");
3131 Account const carol("carol");
3132 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
3133
3134 mptAlice.create({
3135 .ownerCount = 1,
3136 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3137 });
3138
3139 mptAlice.authorize({
3140 .account = bob,
3141 });
3142 mptAlice.authorize({
3143 .account = carol,
3144 });
3145 mptAlice.pay(alice, bob, 100);
3146
3147 mptAlice.generateKeyPair(alice);
3148
3149 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3150
3151 mptAlice.generateKeyPair(bob);
3152 mptAlice.generateKeyPair(carol);
3153
3154 mptAlice.convert({
3155 .account = bob,
3156 .amt = 100,
3157 .holderPubKey = mptAlice.getPubKey(bob),
3158 });
3159
3160 mptAlice.convert({
3161 .account = carol,
3162 .amt = 0,
3163 .holderPubKey = mptAlice.getPubKey(carol),
3164 });
3165
3166 // carol cannot delete even if he has encrypted zero amount
3167 mptAlice.authorize({
3168 .account = carol,
3169 .flags = tfMPTUnauthorize,
3170 .err = tecHAS_OBLIGATIONS,
3171 });
3172 }
3173
3174 // can delete mptoken if outstanding confidential balance is zero
3175 {
3176 Env env{*this, features};
3177 Account const alice("alice");
3178 Account const bob("bob");
3179 MPTTester mptAlice(env, alice, {.holders = {bob}});
3180
3181 mptAlice.create({
3182 .ownerCount = 1,
3183 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3184 });
3185
3186 mptAlice.authorize({
3187 .account = bob,
3188 });
3189 mptAlice.generateKeyPair(alice);
3190
3191 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3192
3193 mptAlice.generateKeyPair(bob);
3194
3195 mptAlice.convert({
3196 .account = bob,
3197 .amt = 0,
3198 .holderPubKey = mptAlice.getPubKey(bob),
3199 });
3200
3201 mptAlice.authorize({
3202 .account = bob,
3203 .flags = tfMPTUnauthorize,
3204 });
3205 }
3206
3207 // can delete mptoken if issuance has been destroyed and has
3208 // encrypted zero balance
3209 {
3210 Env env{*this, features};
3211 Account const alice("alice");
3212 Account const bob("bob");
3213 MPTTester mptAlice(env, alice, {.holders = {bob}});
3214
3215 mptAlice.create({
3216 .ownerCount = 1,
3217 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3218 });
3219
3220 mptAlice.authorize({
3221 .account = bob,
3222 });
3223 mptAlice.generateKeyPair(alice);
3224
3225 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3226
3227 mptAlice.generateKeyPair(bob);
3228
3229 mptAlice.convert({
3230 .account = bob,
3231 .amt = 0,
3232 .holderPubKey = mptAlice.getPubKey(bob),
3233 });
3234
3235 mptAlice.destroy();
3236
3237 mptAlice.authorize({
3238 .account = bob,
3239 .flags = tfMPTUnauthorize,
3240 });
3241 }
3242 // test with convert back and delete
3243 // can delete mptoken if converted back (COA returns to zero)
3244 {
3245 Env env{*this, features};
3246 Account const alice("alice");
3247 Account const bob("bob");
3248 ConfidentialEnv confEnv{
3249 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 100}}};
3250 auto& mptAlice = confEnv.mpt;
3251
3252 mptAlice.convertBack({
3253 .account = bob,
3254 .amt = 100,
3255 });
3256
3257 mptAlice.pay(bob, alice, 100);
3258
3259 // Should be able to delete as Confidential Outstanding amount is 0
3260 mptAlice.authorize({
3261 .account = bob,
3262 .flags = tfMPTUnauthorize,
3263 });
3264 }
3265
3266 // removeEmptyHolding: vault share MPToken with confidential balance
3267 // fields should not be deleted on VaultWithdraw
3268 {
3269 Env env{*this, features | featureSingleAssetVault};
3270 Account const issuer("issuer");
3271 Account const owner("owner");
3272 Account const depositor("depositor");
3273
3274 MPTTester mptt{env, issuer, {.holders = {owner, depositor}}};
3275 mptt.create({
3276 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback,
3277 });
3278 PrettyAsset const asset = mptt.issuanceID();
3279 mptt.authorize({.account = owner});
3280 mptt.authorize({.account = depositor});
3281 env(pay(issuer, depositor, asset(1000)));
3282 env.close();
3283
3284 test::jtx::Vault const vault{env};
3285 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
3286 env(tx);
3287 env.close();
3288
3289 // Get the share MPTID from vault
3290 auto const vaultSle = env.le(vaultKeylet);
3291 BEAST_EXPECT(vaultSle != nullptr);
3292 auto const share = vaultSle->at(sfShareMPTID);
3293
3294 // Depositor deposits into vault
3295 tx = vault.deposit(
3296 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
3297 env(tx);
3298 env.close();
3299
3300 // Verify depositor has share tokens
3301 auto shareMpt = env.le(keylet::mptoken(share, depositor.id()));
3302 BEAST_EXPECT(shareMpt != nullptr);
3303
3304 // Inject confidential balance fields on the share MPToken
3305 // to simulate a scenario where vault shares somehow have
3306 // confidential balances
3307 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
3308 // Set lsfMPTCanHoldConfidentialBalance on the share issuance
3309 // so the invariant allows encrypted fields on the MPToken
3310 auto issuance =
3312 if (!issuance)
3313 return false;
3314 issuance->setFlag(lsfMPTCanHoldConfidentialBalance);
3315 view.rawReplace(issuance);
3316
3317 auto const k = keylet::mptoken(share, depositor.id());
3318 auto const sle = std::const_pointer_cast<SLE>(view.read(k));
3319 if (!sle)
3320 return false;
3321 // Inject dummy confidential balance fields
3322 Buffer dummyCiphertext(kEcGamalEncryptedTotalLength);
3323 std::memset(dummyCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
3324 dummyCiphertext.data()[0] = kEcCompressedPrefixEvenY;
3326 dummyCiphertext.data()[kEcCiphertextComponentLength - 1] = 0x01;
3327 dummyCiphertext.data()[kEcGamalEncryptedTotalLength - 1] = 0x01;
3328 sle->setFieldVL(sfConfidentialBalanceSpending, dummyCiphertext);
3329 sle->setFieldVL(sfConfidentialBalanceInbox, dummyCiphertext);
3330 sle->setFieldVL(sfIssuerEncryptedBalance, dummyCiphertext);
3331 view.rawReplace(sle);
3332 return true;
3333 });
3334
3335 // Withdraw everything - which should fail because of the confidential balance fields
3336 tx = vault.withdraw(
3337 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
3338 env(tx);
3339
3340 // The share MPToken should still exist because the
3341 // withdrawal failed due to confidential balance obligations
3342 shareMpt = env.le(keylet::mptoken(share, depositor.id()));
3343 BEAST_EXPECT(shareMpt != nullptr);
3344 }
3345 }
3346
3347 void
3349 {
3350 testcase("Convert back");
3351 using namespace test::jtx;
3352
3353 // Basic convert back test
3354 {
3355 Env env{*this, features};
3356 Account const alice("alice");
3357 Account const bob("bob");
3358 ConfidentialEnv confEnv{
3359 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
3360 auto& mptAlice = confEnv.mpt;
3361
3362 mptAlice.convertBack({
3363 .account = bob,
3364 .amt = 30,
3365 });
3366
3367 mptAlice.convertBack({
3368 .account = bob,
3369 .amt = 10,
3370 });
3371 }
3372
3373 // Edge case: minimum amount (1)
3374 {
3375 Env env{*this, features};
3376 Account const alice("alice");
3377 Account const bob("bob");
3378 ConfidentialEnv confEnv{
3379 env, alice, {{.account = bob, .payAmount = 2, .convertAmount = 2}}};
3380 auto& mptAlice = confEnv.mpt;
3381
3382 mptAlice.convertBack({
3383 .account = bob,
3384 .amt = 1,
3385 });
3386 }
3387
3388 // Edge case: kMaxMpTokenAmount
3389 // Using raw JSON to avoid automatic decryption checks in MPTTester
3390 // which don't work for very large amounts (brute-force decryption is slow)
3391 // TODO: improve this test once there is bounded decryption or optimized decryption for
3392 // large amounts
3393 {
3394 Env env{*this, features};
3395 Account const alice("alice");
3396 Account const bob("bob");
3397 MPTTester mptAlice(env, alice, {.holders = {bob}});
3398
3399 mptAlice.create({
3400 .ownerCount = 1,
3401 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3402 });
3403
3404 mptAlice.authorize({
3405 .account = bob,
3406 });
3407 mptAlice.pay(alice, bob, kMaxMpTokenAmount);
3408
3409 mptAlice.generateKeyPair(alice);
3410 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3411
3412 mptAlice.generateKeyPair(bob);
3413
3414 // Convert kMaxMpTokenAmount to confidential using raw JSON
3415 Buffer const convertBlindingFactor = generateBlindingFactor();
3416 auto const convertHolderCiphertext =
3417 mptAlice.encryptAmount(bob, kMaxMpTokenAmount, convertBlindingFactor);
3418 auto const convertIssuerCiphertext =
3419 mptAlice.encryptAmount(alice, kMaxMpTokenAmount, convertBlindingFactor);
3420 auto const convertContextHash =
3421 getConvertContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob));
3422 auto const schnorrProof = requireOptional(
3423 mptAlice.getSchnorrProof(bob, convertContextHash), "Missing schnorr proof");
3424
3425 {
3426 json::Value jv;
3427 jv[jss::Account] = bob.human();
3428 jv[jss::TransactionType] = jss::ConfidentialMPTConvert;
3429 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
3430 jv[sfMPTAmount.jsonName] = std::to_string(kMaxMpTokenAmount);
3431 jv[sfHolderEncryptionKey.jsonName] =
3432 strHex(requireOptional(mptAlice.getPubKey(bob), "Missing holder public key"));
3433 jv[sfHolderEncryptedAmount.jsonName] = strHex(convertHolderCiphertext);
3434 jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertIssuerCiphertext);
3435 jv[sfBlindingFactor.jsonName] = strHex(convertBlindingFactor);
3436 jv[sfZKProof.jsonName] = strHex(schnorrProof);
3437
3438 env(jv, Ter(tesSUCCESS));
3439 }
3440
3441 // Merge inbox using raw JSON - moves funds from inbox to spending balance
3442 {
3443 json::Value jv;
3444 jv[jss::Account] = bob.human();
3445 jv[jss::TransactionType] = jss::ConfidentialMPTMergeInbox;
3446 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
3447
3448 env(jv, Ter(tesSUCCESS));
3449 }
3450
3451 // ConvertBack kMaxMpTokenAmount - 1 using raw JSON
3452 // After convert + merge, spending balance = kMaxMpTokenAmount
3453 // We convert back kMaxMpTokenAmount - 1 to leave remainder of 1
3454 std::uint64_t const convertBackAmt = kMaxMpTokenAmount - 1;
3455
3456 Buffer const convertBackBlindingFactor = generateBlindingFactor();
3457 auto const convertBackHolderCiphertext =
3458 mptAlice.encryptAmount(bob, convertBackAmt, convertBackBlindingFactor);
3459 auto const convertBackIssuerCiphertext =
3460 mptAlice.encryptAmount(alice, convertBackAmt, convertBackBlindingFactor);
3461
3462 // Get the encrypted spending balance from ledger (no decryption needed)
3463 auto const encryptedSpendingBalance = requireOptional(
3464 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
3465 "Missing encrypted spending balance");
3466
3467 // Generate pedersen commitment for the known spending balance
3468 Buffer const pcBlindingFactor = generateBlindingFactor();
3469 Buffer const pedersenCommitment =
3470 mptAlice.getPedersenCommitment(kMaxMpTokenAmount, pcBlindingFactor);
3471
3472 // Generate the proof using known spending balance value
3473 auto const version = mptAlice.getMPTokenVersion(bob);
3474 uint256 const convertBackContextHash =
3475 getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
3476
3477 Buffer const proof = mptAlice.getConvertBackProof(
3478 bob,
3479 convertBackAmt,
3480 convertBackContextHash,
3481 {
3482 .pedersenCommitment = pedersenCommitment,
3483 .amt = kMaxMpTokenAmount,
3484 .encryptedAmt = encryptedSpendingBalance,
3485 .blindingFactor = pcBlindingFactor,
3486 });
3487
3488 {
3489 json::Value jv;
3490 jv[jss::Account] = bob.human();
3491 jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack;
3492 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
3493 jv[sfMPTAmount.jsonName] = std::to_string(convertBackAmt);
3494 jv[sfHolderEncryptedAmount.jsonName] = strHex(convertBackHolderCiphertext);
3495 jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertBackIssuerCiphertext);
3496 jv[sfBlindingFactor.jsonName] = strHex(convertBackBlindingFactor);
3497 jv[sfBalanceCommitment.jsonName] = strHex(pedersenCommitment);
3498 jv[sfZKProof.jsonName] = strHex(proof);
3499
3500 env(jv, Ter(tesSUCCESS));
3501 }
3502
3503 // Verify the public balance was restored (minus 1 remaining in confidential)
3504 env.require(MptBalance(mptAlice, bob, convertBackAmt));
3505 }
3506 }
3507
3508 void
3510 {
3511 testcase("Convert back with auditor");
3512 using namespace test::jtx;
3513
3514 Env env{*this, features};
3515 Account const alice("alice");
3516 Account const bob("bob");
3517 Account const auditor("auditor");
3518 ConfidentialEnv confEnv{
3519 env,
3520 alice,
3521 {{.account = bob, .payAmount = 100, .convertAmount = 40}},
3522 tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3523 auditor};
3524 auto& mptAlice = confEnv.mpt;
3525
3526 mptAlice.convertBack({
3527 .account = bob,
3528 .amt = 30,
3529 });
3530 }
3531
3532 void
3534 {
3535 testcase("Convert back preflight");
3536 using namespace test::jtx;
3537
3538 {
3539 Env env{*this, features - featureConfidentialTransfer};
3540 Account const alice("alice");
3541 Account const bob("bob");
3542 MPTTester mptAlice(env, alice, {.holders = {bob}});
3543
3544 mptAlice.create({
3545 .ownerCount = 1,
3546 .flags = tfMPTCanTransfer | tfMPTCanLock,
3547 });
3548
3549 mptAlice.authorize({
3550 .account = bob,
3551 });
3552 mptAlice.pay(alice, bob, 100);
3553
3554 mptAlice.generateKeyPair(alice);
3555 mptAlice.generateKeyPair(bob);
3556
3557 mptAlice.convertBack({
3558 .account = bob,
3559 .amt = 30,
3560 .err = temDISABLED,
3561 });
3562 }
3563
3564 {
3565 Env env{*this, features};
3566 Account const alice("alice");
3567 Account const bob("bob");
3568 ConfidentialEnv confEnv{
3569 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
3570 auto& mptAlice = confEnv.mpt;
3571
3572 mptAlice.convertBack({
3573 .account = alice,
3574 .amt = 30,
3575 .err = temMALFORMED,
3576 });
3577
3578 mptAlice.convertBack({
3579 .account = bob,
3580 .amt = 0,
3581 .err = temBAD_AMOUNT,
3582 });
3583
3584 mptAlice.convertBack({
3585 .account = bob,
3586 .amt = kMaxMpTokenAmount + 1,
3587 .err = temBAD_AMOUNT,
3588 });
3589
3590 // Balance commitment has correct length but invalid EC point data
3591 mptAlice.convertBack({
3592 .account = bob,
3593 .amt = 30,
3594 .pedersenCommitment = gMakeZeroBuffer(kEcPedersenCommitmentLength),
3595 .err = temMALFORMED,
3596 });
3597
3598 mptAlice.convertBack({
3599 .account = bob,
3600 .amt = 30,
3601 .holderEncryptedAmt = Buffer{},
3602 .err = temBAD_CIPHERTEXT,
3603 });
3604
3605 mptAlice.convertBack({
3606 .account = bob,
3607 .amt = 30,
3608 .issuerEncryptedAmt = Buffer{},
3609 .err = temBAD_CIPHERTEXT,
3610 });
3611
3612 mptAlice.convertBack({
3613 .account = bob,
3614 .amt = 30,
3615 .holderEncryptedAmt = getBadCiphertext(),
3616 .err = temBAD_CIPHERTEXT,
3617 });
3618
3619 mptAlice.convertBack({
3620 .account = bob,
3621 .amt = 30,
3622 .issuerEncryptedAmt = getBadCiphertext(),
3623 .err = temBAD_CIPHERTEXT,
3624 });
3625
3626 mptAlice.convertBack({
3627 .account = bob,
3628 .amt = 30,
3629 .auditorEncryptedAmt = gMakeZeroBuffer(10),
3630 .err = temBAD_CIPHERTEXT,
3631 });
3632
3633 mptAlice.convertBack({
3634 .account = bob,
3635 .amt = 30,
3636 .auditorEncryptedAmt = getBadCiphertext(),
3637 .err = temBAD_CIPHERTEXT,
3638 });
3639
3640 // invalid proof length
3641 mptAlice.convertBack({
3642 .account = bob,
3643 .amt = 30,
3644 .proof = Buffer{},
3645 .err = temMALFORMED,
3646 });
3647
3648 mptAlice.convertBack({
3649 .account = bob,
3650 .amt = 30,
3651 .proof = gMakeZeroBuffer(100),
3652 .err = temMALFORMED,
3653 });
3654 }
3655 }
3656
3657 void
3659 {
3660 testcase("Convert back preclaim");
3661 using namespace test::jtx;
3662
3663 // issuance does not exist
3664 {
3665 Env env{*this, features};
3666 Account const alice("alice");
3667 Account const bob("bob");
3668 MPTTester mptAlice(env, alice, {.holders = {bob}});
3669
3670 mptAlice.create({
3671 .ownerCount = 1,
3672 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3673 });
3674
3675 mptAlice.authorize({
3676 .account = bob,
3677 });
3678 mptAlice.generateKeyPair(alice);
3679
3680 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3681
3682 mptAlice.destroy();
3683 mptAlice.generateKeyPair(bob);
3684
3685 mptAlice.convertBack({
3686 .account = bob,
3687 .amt = 30,
3688 .err = tecOBJECT_NOT_FOUND,
3689 });
3690 }
3691
3692 // tfMPTCanHoldConfidentialBalance is not set on issuance
3693 {
3694 Env env{*this, features};
3695 Account const alice("alice");
3696 Account const bob("bob");
3697 MPTTester mptAlice(env, alice, {.holders = {bob}});
3698
3699 mptAlice.create({
3700 .ownerCount = 1,
3701 .flags = tfMPTCanTransfer | tfMPTCanLock,
3702 });
3703
3704 mptAlice.authorize({
3705 .account = bob,
3706 });
3707 mptAlice.pay(alice, bob, 100);
3708
3709 mptAlice.generateKeyPair(alice);
3710 mptAlice.generateKeyPair(bob);
3711
3712 mptAlice.convertBack({
3713 .account = bob,
3714 .amt = 30,
3715 .err = tecNO_PERMISSION,
3716 });
3717 }
3718
3719 // no mptoken
3720 {
3721 Env env{*this, features};
3722 Account const alice("alice");
3723 Account const bob("bob");
3724 MPTTester mptAlice(env, alice, {.holders = {bob}});
3725
3726 mptAlice.create({
3727 .ownerCount = 1,
3728 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3729 });
3730
3731 mptAlice.generateKeyPair(alice);
3732 mptAlice.generateKeyPair(bob);
3733
3734 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3735
3736 mptAlice.convertBack({
3737 .account = bob,
3738 .amt = 30,
3739 .err = tecOBJECT_NOT_FOUND,
3740 });
3741 }
3742
3743 // mptoken exists but lacks confidential fields
3744 {
3745 Env env{*this, features};
3746 Account const alice("alice");
3747 Account const bob("bob");
3748 MPTTester mptAlice(env, alice, {.holders = {bob}});
3749
3750 mptAlice.create({
3751 .ownerCount = 1,
3752 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
3753 });
3754
3755 mptAlice.authorize({
3756 .account = bob,
3757 });
3758
3759 mptAlice.pay(alice, bob, 100);
3760 mptAlice.generateKeyPair(alice);
3761 mptAlice.generateKeyPair(bob);
3762 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3763
3764 // Bob's MPToken lacks the confidential fields
3765 auto const sleBobMpt = env.le(keylet::mptoken(mptAlice.issuanceID(), bob.id()));
3766 BEAST_EXPECT(sleBobMpt);
3767 BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfHolderEncryptionKey));
3768 BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfConfidentialBalanceSpending));
3769 BEAST_EXPECT(!sleBobMpt->isFieldPresent(sfIssuerEncryptedBalance));
3770
3771 mptAlice.convertBack({
3772 .account = bob,
3773 .amt = 30,
3774 .err = tecNO_PERMISSION,
3775 });
3776 }
3777
3778 // bob tries to convert back more than COA
3779 {
3780 Env env{*this, features};
3781 Account const alice("alice");
3782 Account const bob("bob");
3783 Account const carol("carol");
3784 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
3785
3786 mptAlice.create({
3787 .ownerCount = 1,
3788 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3789 });
3790
3791 mptAlice.authorize({
3792 .account = bob,
3793 });
3794 mptAlice.authorize({
3795 .account = carol,
3796 });
3797 mptAlice.pay(alice, bob, 100);
3798 mptAlice.pay(alice, carol, 100);
3799
3800 mptAlice.generateKeyPair(alice);
3801
3802 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3803
3804 mptAlice.generateKeyPair(bob);
3805 mptAlice.generateKeyPair(carol);
3806
3807 mptAlice.convert({
3808 .account = bob,
3809 .amt = 40,
3810 .holderPubKey = mptAlice.getPubKey(bob),
3811 });
3812
3813 mptAlice.mergeInbox({
3814 .account = bob,
3815 });
3816
3817 mptAlice.convert({
3818 .account = carol,
3819 .amt = 40,
3820 .holderPubKey = mptAlice.getPubKey(carol),
3821 });
3822
3823 mptAlice.convertBack({
3824 .account = bob,
3825 .amt = 300,
3826 .err = tecINSUFFICIENT_FUNDS,
3827 });
3828 }
3829
3830 // cannot convert if locked or unauth
3831 {
3832 Env env{*this, features};
3833 Account const alice("alice");
3834 Account const bob("bob");
3835 MPTTester mptAlice(env, alice, {.holders = {bob}});
3836
3837 mptAlice.create({
3838 .ownerCount = 1,
3839 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth |
3840 tfMPTCanHoldConfidentialBalance,
3841 });
3842
3843 mptAlice.authorize({
3844 .account = bob,
3845 });
3846 mptAlice.authorize({
3847 .account = alice,
3848 .holder = bob,
3849 });
3850 mptAlice.pay(alice, bob, 100);
3851
3852 mptAlice.generateKeyPair(alice);
3853
3854 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3855
3856 mptAlice.generateKeyPair(bob);
3857
3858 mptAlice.convert({
3859 .account = bob,
3860 .amt = 40,
3861 .holderPubKey = mptAlice.getPubKey(bob),
3862 });
3863
3864 mptAlice.mergeInbox({
3865 .account = bob,
3866 });
3867
3868 mptAlice.set({
3869 .account = alice,
3870 .holder = bob,
3871 .flags = tfMPTLock,
3872 });
3873
3874 mptAlice.convertBack({
3875 .account = bob,
3876 .amt = 10,
3877 .err = tecLOCKED,
3878 });
3879
3880 mptAlice.set({
3881 .account = alice,
3882 .holder = bob,
3883 .flags = tfMPTUnlock,
3884 });
3885
3886 mptAlice.convertBack({
3887 .account = bob,
3888 .amt = 10,
3889 });
3890
3891 mptAlice.authorize({
3892 .account = alice,
3893 .holder = bob,
3894 .flags = tfMPTUnauthorize,
3895 });
3896
3897 mptAlice.convertBack({
3898 .account = bob,
3899 .amt = 10,
3900 .err = tecNO_AUTH,
3901 });
3902
3903 mptAlice.authorize({
3904 .account = alice,
3905 .holder = bob,
3906 });
3907
3908 mptAlice.convertBack({
3909 .account = bob,
3910 .amt = 10,
3911 });
3912 }
3913
3914 // Verification of holder and issuer ciphertexts during convertBack
3915 {
3916 Env env{*this, features};
3917 Account const alice("alice");
3918 Account const bob("bob");
3919 ConfidentialEnv confEnv{
3920 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
3921 auto& mptAlice = confEnv.mpt;
3922
3923 // Holder encrypted amount is valid format but mathematically incorrect for this
3924 // convertBack
3925 mptAlice.convertBack({
3926 .account = bob,
3927 .amt = 10,
3928 .holderEncryptedAmt = getTrivialCiphertext(),
3929 .err = tecBAD_PROOF,
3930 });
3931
3932 // Issuer encrypted amount is valid format but mathematically incorrect for this
3933 // convertBack
3934 mptAlice.convertBack({
3935 .account = bob,
3936 .amt = 10,
3937 .issuerEncryptedAmt = getTrivialCiphertext(),
3938 .err = tecBAD_PROOF,
3939 });
3940 }
3941
3942 // Alice has NOT set an auditor key, but Bob provides
3943 // auditorEncryptedAmt
3944 {
3945 Env env{*this, features};
3946 Account const alice("alice");
3947 Account const bob("bob");
3948 ConfidentialEnv confEnv{
3949 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
3950 auto& mptAlice = confEnv.mpt;
3951
3952 mptAlice.convertBack({
3953 .account = bob,
3954 .amt = 10,
3955 // Provide valid ciphertext to pass preflight
3956 .auditorEncryptedAmt = getTrivialCiphertext(),
3957 .err = tecNO_PERMISSION,
3958 });
3959 }
3960
3961 // we set the auditor key, but convertBack omits auditorEncryptedAmt
3962 {
3963 Env env{*this, features};
3964 Account const alice("alice");
3965 Account const bob("bob");
3966 Account const auditor("auditor");
3967 ConfidentialEnv confEnv{
3968 env,
3969 alice,
3970 {{.account = bob, .payAmount = 100, .convertAmount = 50}},
3971 tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
3972 auditor};
3973 auto& mptAlice = confEnv.mpt;
3974
3975 // ConvertBack WITHOUT auditorEncryptedAmt
3976 mptAlice.convertBack({
3977 .account = bob,
3978 .amt = 10,
3979 .fillAuditorEncryptedAmt = false,
3980 .err = tecNO_PERMISSION,
3981 });
3982
3983 // ConvertBack where auditor ciphertext mathematically
3984 // correct, but contains invalid data (mismatching amount).
3985 mptAlice.convertBack({
3986 .account = bob,
3987 .amt = 10,
3988 .auditorEncryptedAmt = getTrivialCiphertext(),
3989 .err = tecBAD_PROOF,
3990 });
3991 }
3992 }
3993
3994 void
3996 {
3997 testcase("test ConfidentialMPTClawback");
3998 using namespace test::jtx;
3999
4000 Env env{*this, features};
4001 Account const alice("alice");
4002 Account const bob("bob");
4003 Account const carol("carol");
4004 Account const dave("dave");
4005 MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
4006
4007 mptAlice.create({
4008 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
4009 tfMPTCanHoldConfidentialBalance,
4010 });
4011 mptAlice.authorize({
4012 .account = bob,
4013 });
4014 mptAlice.pay(alice, bob, 100);
4015 mptAlice.authorize({
4016 .account = carol,
4017 });
4018 mptAlice.pay(alice, carol, 200);
4019 mptAlice.authorize({
4020 .account = dave,
4021 });
4022 mptAlice.pay(alice, dave, 300);
4023
4024 mptAlice.generateKeyPair(alice);
4025 mptAlice.generateKeyPair(bob);
4026 mptAlice.generateKeyPair(carol);
4027 mptAlice.generateKeyPair(dave);
4028 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4029
4030 // setup bob.
4031 // after setup, bob's spending balance is 60, inbox balance is 0.
4032 {
4033 // bob converts 60 to confidential
4034 mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
4035
4036 // bob merge inbox
4037 mptAlice.mergeInbox({
4038 .account = bob,
4039 });
4040 }
4041
4042 // setup carol.
4043 // after setup, carol's spending balance is 120, inbox balance is 0.
4044 {
4045 // carol converts 120 to confidential
4046 mptAlice.convert(
4047 {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)});
4048
4049 // carol merge inbox
4050 mptAlice.mergeInbox({
4051 .account = carol,
4052 });
4053 }
4054
4055 // setup dave.
4056 // dave will not merge inbox.
4057 // after setup, dave's inbox balance is 200, spending balance is 0.
4058 mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)});
4059
4060 // setup: carol confidential send 50 to bob.
4061 // after send, bob's inbox balance is 50, spending balance
4062 // remains 60. carol's inbox balance remains 0, spending balance
4063 // drops to 70.
4064 mptAlice.send({
4065 .account = carol,
4066 .dest = bob,
4067 .amt = 50,
4068 });
4069
4070 // Confidential clawback is burn/reduce outstanding amount.
4071 // The holder public balance is unchanged, and OA/COA decrease.
4072 auto const preBobPublicBalance = mptAlice.getBalance(bob);
4073 auto const preOutstandingAmount = mptAlice.getIssuanceOutstandingBalance();
4074 auto const preConfidentialOutstandingAmount = mptAlice.getIssuanceConfidentialBalance();
4075 BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id())));
4076
4077 // alice clawback all confidential balance from bob, 110 in total.
4078 // bob has balance in both inbox and spending. These balances should
4079 // become zero after clawback, which is verified in the
4080 // confidentialClaw function.
4081 mptAlice.confidentialClaw({
4082 .account = alice,
4083 .holder = bob,
4084 .amt = 110,
4085 });
4086 BEAST_EXPECT(mptAlice.getBalance(bob) == preBobPublicBalance);
4087 auto const postOutstandingAmount = mptAlice.getIssuanceOutstandingBalance();
4088 BEAST_EXPECT(
4089 preOutstandingAmount && postOutstandingAmount &&
4090 *postOutstandingAmount == *preOutstandingAmount - 110);
4091 BEAST_EXPECT(
4092 mptAlice.getIssuanceConfidentialBalance() == preConfidentialOutstandingAmount - 110);
4093 BEAST_EXPECT(!env.le(keylet::mptoken(mptAlice.issuanceID(), alice.id())));
4094
4095 // alice clawback all confidential balance from carol, which is 70.
4096 // carol only has balance in spending.
4097 mptAlice.confidentialClaw({
4098 .account = alice,
4099 .holder = carol,
4100 .amt = 70,
4101 });
4102
4103 // alice clawback all confidential balance from dave, which is 200.
4104 // dave only has balance in inbox.
4105 mptAlice.confidentialClaw({
4106 .account = alice,
4107 .holder = dave,
4108 .amt = 200,
4109 });
4110 }
4111
4112 void
4114 {
4115 testcase("test ConfidentialMPTClawback with auditor");
4116 using namespace test::jtx;
4117
4118 Env env{*this, features};
4119 Account const alice("alice");
4120 Account const bob("bob");
4121 Account const carol("carol");
4122 Account const dave("dave");
4123 Account const auditor("auditor");
4124 MPTTester mptAlice(
4125 env,
4126 alice,
4127 {
4128 .holders = {bob, carol, dave},
4129 .auditor = auditor,
4130 });
4131
4132 mptAlice.create({
4133 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
4134 tfMPTCanHoldConfidentialBalance,
4135 });
4136 mptAlice.authorize({
4137 .account = bob,
4138 });
4139 mptAlice.pay(alice, bob, 100);
4140 mptAlice.authorize({
4141 .account = carol,
4142 });
4143 mptAlice.pay(alice, carol, 200);
4144 mptAlice.authorize({
4145 .account = dave,
4146 });
4147 mptAlice.pay(alice, dave, 300);
4148
4149 mptAlice.generateKeyPair(alice);
4150 mptAlice.generateKeyPair(bob);
4151 mptAlice.generateKeyPair(carol);
4152 mptAlice.generateKeyPair(dave);
4153 mptAlice.generateKeyPair(auditor);
4154 mptAlice.set(
4155 {.account = alice,
4156 .issuerPubKey = mptAlice.getPubKey(alice),
4157 .auditorPubKey = mptAlice.getPubKey(auditor)});
4158
4159 // setup bob.
4160 // after setup, bob's spending balance is 60, inbox balance is 0.
4161 {
4162 // bob converts 60 to confidential
4163 mptAlice.convert({.account = bob, .amt = 60, .holderPubKey = mptAlice.getPubKey(bob)});
4164
4165 // bob merge inbox
4166 mptAlice.mergeInbox({
4167 .account = bob,
4168 });
4169 }
4170
4171 // setup carol.
4172 // after setup, carol's spending balance is 120, inbox balance is 0.
4173 {
4174 // carol converts 120 to confidential
4175 mptAlice.convert(
4176 {.account = carol, .amt = 120, .holderPubKey = mptAlice.getPubKey(carol)});
4177
4178 // carol merge inbox
4179 mptAlice.mergeInbox({
4180 .account = carol,
4181 });
4182 }
4183
4184 // setup dave.
4185 // dave will not merge inbox.
4186 // after setup, dave's inbox balance is 200, spending balance is 0.
4187 mptAlice.convert({.account = dave, .amt = 200, .holderPubKey = mptAlice.getPubKey(dave)});
4188
4189 // setup: carol confidential send 50 to bob.
4190 // after send, bob's inbox balance is 50, spending balance
4191 // remains 60. carol's inbox balance remains 0, spending balance
4192 // drops to 70.
4193 mptAlice.send({
4194 .account = carol,
4195 .dest = bob,
4196 .amt = 50,
4197 });
4198
4199 // alice clawback all confidential balance from bob, 110 in total.
4200 // bob has balance in both inbox and spending. These balances should
4201 // become zero after clawback, which is verified in the
4202 // confidentialClaw function.
4203 mptAlice.confidentialClaw({
4204 .account = alice,
4205 .holder = bob,
4206 .amt = 110,
4207 });
4208
4209 // alice clawback all confidential balance from carol, which is 70.
4210 // carol only has balance in spending.
4211 mptAlice.confidentialClaw({
4212 .account = alice,
4213 .holder = carol,
4214 .amt = 70,
4215 });
4216
4217 // alice clawback all confidential balance from dave, which is 200.
4218 // dave only has balance in inbox.
4219 mptAlice.confidentialClaw({
4220 .account = alice,
4221 .holder = dave,
4222 .amt = 200,
4223 });
4224 }
4225
4226 void
4228 {
4229 testcase("ConfidentialMPTClawback context binding");
4230 using namespace test::jtx;
4231
4232 auto runBadProof = [&](auto makeContextHash) {
4233 Env env{*this, features};
4234 Account const alice("alice");
4235 Account const bob("bob");
4236 Account const carol("carol");
4237 ConfidentialEnv confEnv{
4238 env,
4239 alice,
4240 {{.account = bob, .payAmount = 100, .convertAmount = 60}},
4241 tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback |
4242 tfMPTCanHoldConfidentialBalance};
4243 auto& mptAlice = confEnv.mpt;
4244
4245 auto const privKey = mptAlice.getPrivKey(alice);
4246 if (!BEAST_EXPECT(privKey.has_value()))
4247 return;
4248
4249 auto const proof = mptAlice.getClawbackProof(
4250 bob,
4251 60,
4252 requireOptionalRef(privKey, "Missing private key"),
4253 makeContextHash(env, mptAlice, alice, bob, carol));
4254 if (!BEAST_EXPECT(proof.has_value()))
4255 return;
4256
4257 mptAlice.confidentialClaw({
4258 .account = alice,
4259 .holder = bob,
4260 .amt = 60,
4261 .proof = strHex(requireOptional(proof, "Missing proof")),
4262 .err = tecBAD_PROOF,
4263 });
4264 };
4265
4266 // Wrong account (issuer) in the proof context.
4267 runBadProof([&](Env& env,
4268 MPTTester const& mpt,
4269 Account const& alice,
4270 Account const& bob,
4271 Account const& carol) {
4272 return getClawbackContextHash(carol.id(), mpt.issuanceID(), env.seq(alice), bob.id());
4273 });
4274
4275 // Wrong issuance ID in the proof context.
4276 runBadProof([&](Env& env,
4277 MPTTester const&,
4278 Account const& alice,
4279 Account const& bob,
4280 Account const&) {
4282 alice.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(alice), bob.id());
4283 });
4284
4285 // Wrong transaction sequence in the proof context.
4286 runBadProof([&](Env& env,
4287 MPTTester const& mpt,
4288 Account const& alice,
4289 Account const& bob,
4290 Account const&) {
4292 alice.id(), mpt.issuanceID(), env.seq(alice) + 1, bob.id());
4293 });
4294
4295 // Wrong holder in the proof context.
4296 runBadProof([&](Env& env,
4297 MPTTester const& mpt,
4298 Account const& alice,
4299 Account const&,
4300 Account const& carol) {
4301 return getClawbackContextHash(alice.id(), mpt.issuanceID(), env.seq(alice), carol.id());
4302 });
4303 }
4304
4305 // Bob creates the AMM, but Bob is not the MPT holder checked below.
4306 // The AMM has its own pseudo-account (`ammHolder`) that can hold the
4307 // public MPT pool balance. That pseudo-account cannot normally
4308 // initialize confidential state because the confidential txn's must be
4309 // signed by sfAccount, and the AMM pseudo-account has no signing key.
4310 // So this is a construction/impossibility test: public AMM MPT state exists
4311 // but the corresponding confidential AMM clawback flow is not normally reachable.
4312 void
4314 {
4315 testcase("test ConfidentialMPTClawback Preflight");
4316 using namespace test::jtx;
4317
4318 // test feature disabled
4319 {
4320 Env env{*this, features - featureConfidentialTransfer};
4321 Account const alice("alice");
4322 Account const bob("bob");
4323 MPTTester mptAlice(env, alice, {.holders = {bob}});
4324
4325 mptAlice.create();
4326 mptAlice.authorize({
4327 .account = bob,
4328 });
4329
4330 mptAlice.confidentialClaw({
4331 .account = alice,
4332 .holder = bob,
4333 .amt = 10,
4334 .proof = "123",
4335 .err = temDISABLED,
4336 });
4337 }
4338
4339 // test malformed
4340 {
4341 // set up
4342 Env env{*this, features};
4343 Account const alice("alice");
4344 Account const bob("bob");
4345 Account const carol("carol");
4346 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
4347
4348 mptAlice.create({
4349 .ownerCount = 1,
4350 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
4351 });
4352
4353 mptAlice.authorize({
4354 .account = bob,
4355 });
4356 mptAlice.authorize({
4357 .account = carol,
4358 });
4359 mptAlice.generateKeyPair(alice);
4360 mptAlice.generateKeyPair(bob);
4361 mptAlice.generateKeyPair(carol);
4362 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4363 mptAlice.pay(alice, bob, 100);
4364 mptAlice.pay(alice, carol, 50);
4365
4366 // only issuer can clawback
4367 mptAlice.confidentialClaw({
4368 .account = carol,
4369 .holder = bob,
4370 .amt = 10,
4371 .err = temMALFORMED,
4372 });
4373
4374 // invalid issuance ID, whose issuer is not alice
4375 {
4376 json::Value jv;
4377 jv[jss::Account] = alice.human();
4378 jv[sfHolder] = bob.human();
4379 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
4380 jv[sfMPTAmount] = std::to_string(10);
4381 jv[sfZKProof] = "123";
4382
4383 // wrong issuance ID
4384 jv[sfMPTokenIssuanceID] = "00000004AE123A8556F3CF91154711376AFB0F894F832B3E";
4385
4386 env(jv, Ter(temMALFORMED));
4387 }
4388
4389 // issuer cannot clawback from self
4390 mptAlice.confidentialClaw({
4391 .account = alice,
4392 .holder = alice,
4393 .amt = 10,
4394 .err = temMALFORMED,
4395 });
4396
4397 // invalid amount
4398 mptAlice.confidentialClaw({
4399 .account = alice,
4400 .holder = bob,
4401 .amt = 0,
4402 .err = temBAD_AMOUNT,
4403 });
4404
4405 // invalid proof length
4406 mptAlice.confidentialClaw({
4407 .account = alice,
4408 .holder = bob,
4409 .amt = 10,
4410 .proof = "123",
4411 .err = temMALFORMED,
4412 });
4413 }
4414 }
4415
4416 void
4418 {
4419 testcase("Clawback Preclaim Errors");
4420 using namespace test::jtx;
4421
4422 {
4423 // set up, alice is the issuer, bob and carol are authorized
4424 // holders. dave is not authorized. bob has confidential
4425 // balance, carol does not.
4426 Env env{*this, features};
4427 Account const alice("alice");
4428 Account const bob("bob");
4429 Account const carol("carol");
4430 Account const dave("dave");
4431 MPTTester mptAlice(env, alice, {.holders = {bob, carol, dave}});
4432
4433 mptAlice.create({
4434 .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth |
4435 tfMPTCanHoldConfidentialBalance,
4436 });
4437 mptAlice.authorize({
4438 .account = bob,
4439 });
4440 mptAlice.authorize({
4441 .account = alice,
4442 .holder = bob,
4443 });
4444 mptAlice.authorize({
4445 .account = carol,
4446 });
4447 mptAlice.authorize({
4448 .account = alice,
4449 .holder = carol,
4450 });
4451
4452 mptAlice.pay(alice, bob, 100);
4453 mptAlice.pay(alice, carol, 50);
4454 mptAlice.generateKeyPair(alice);
4455 mptAlice.generateKeyPair(bob);
4456 mptAlice.generateKeyPair(carol);
4457 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4458
4459 mptAlice.convert({
4460 .account = bob,
4461 .amt = 60,
4462 .holderPubKey = mptAlice.getPubKey(bob),
4463 });
4464 mptAlice.mergeInbox({
4465 .account = bob,
4466 });
4467
4468 // holder does not exist
4469 {
4470 Account const unknown("unknown");
4471 mptAlice.confidentialClaw({
4472 .account = alice,
4473 .holder = unknown,
4474 .amt = 10,
4475 .err = tecNO_TARGET,
4476 });
4477 }
4478
4479 // dave does not hold mpt at all, no MPT object
4480 {
4481 mptAlice.confidentialClaw({
4482 .account = alice,
4483 .holder = dave,
4484 .amt = 10,
4485 .err = tecOBJECT_NOT_FOUND,
4486 });
4487 }
4488
4489 // carol has no confidential balance
4490 {
4491 mptAlice.confidentialClaw({
4492 .account = alice,
4493 .holder = carol,
4494 .amt = 10,
4495 .err = tecNO_PERMISSION,
4496 });
4497 }
4498 }
4499
4500 // lsfMPTCanClawback not set
4501 {
4502 Env env{*this, features};
4503 Account const alice("alice");
4504 Account const bob("bob");
4505 MPTTester mptAlice(env, alice, {.holders = {bob}});
4506
4507 mptAlice.create({
4508 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
4509 });
4510 mptAlice.authorize({
4511 .account = bob,
4512 });
4513 mptAlice.generateKeyPair(alice);
4514 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4515
4516 mptAlice.confidentialClaw({
4517 .account = alice,
4518 .holder = bob,
4519 .amt = 10,
4520 .err = tecNO_PERMISSION,
4521 });
4522 }
4523
4524 // no issuer key
4525 {
4526 Env env{*this, features};
4527 Account const alice("alice");
4528 Account const bob("bob");
4529 MPTTester mptAlice(env, alice, {.holders = {bob}});
4530 mptAlice.create({
4531 .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
4532 });
4533 mptAlice.authorize({
4534 .account = bob,
4535 });
4536 mptAlice.generateKeyPair(alice);
4537
4538 mptAlice.confidentialClaw({
4539 .account = alice,
4540 .holder = bob,
4541 .amt = 10,
4542 .err = tecNO_PERMISSION,
4543 });
4544 }
4545
4546 // issuance not found
4547 {
4548 Env env{*this, features};
4549 Account const alice("alice");
4550 Account const bob("bob");
4551 MPTTester mptAlice(env, alice, {.holders = {bob}});
4552 mptAlice.create({
4553 .flags = tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
4554 });
4555 mptAlice.authorize({
4556 .account = bob,
4557 });
4558 mptAlice.generateKeyPair(alice);
4559 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4560
4561 // destroy the issuance
4562 mptAlice.destroy();
4563
4564 json::Value jv;
4565 jv[jss::Account] = alice.human();
4566 jv[sfHolder] = bob.human();
4567 jv[jss::TransactionType] = jss::ConfidentialMPTClawback;
4568 jv[sfMPTAmount] = std::to_string(10);
4569 std::string const dummyProof(kEcClawbackProofLength * 2, '0');
4570 jv[sfZKProof] = dummyProof;
4571 jv[sfMPTokenIssuanceID] = to_string(mptAlice.issuanceID());
4572
4573 env(jv, Ter(tecOBJECT_NOT_FOUND));
4574 }
4575
4576 // After setup, bob has confidential balance 60 in spending.
4577 std::uint32_t const setupFlags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTRequireAuth |
4578 tfMPTCanLock | tfMPTCanHoldConfidentialBalance;
4579 std::string const dummyClawbackProof(kEcClawbackProofLength * 2, '0');
4580
4581 auto removeMPTokenField =
4582 [&](Env& env, MPTTester const& mpt, Account const& holder, SField const& field) {
4583 BEAST_EXPECT(env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
4584 auto const sle = std::const_pointer_cast<SLE>(
4585 view.read(keylet::mptoken(mpt.issuanceID(), holder.id())));
4586 if (!sle)
4587 return false;
4588
4589 sle->makeFieldAbsent(field);
4590 view.rawReplace(sle);
4591 return true;
4592 }));
4593 };
4594
4595 // After global COA is drained to zero, a further confidential clawback
4596 // fails because the amount exceeds the remaining confidential
4597 // outstanding amount.
4598 {
4599 Env env{*this, features};
4600 Account const alice("alice");
4601 Account const bob("bob");
4602 ConfidentialEnv confEnv{
4603 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4604 auto& mptAlice = confEnv.mpt;
4605
4606 mptAlice.confidentialClaw({
4607 .account = alice,
4608 .holder = bob,
4609 .amt = 60,
4610 });
4611
4612 mptAlice.confidentialClaw({
4613 .account = alice,
4614 .holder = bob,
4615 .amt = 1,
4616 .proof = dummyClawbackProof,
4617 .err = tecINSUFFICIENT_FUNDS,
4618 });
4619 }
4620
4621 // Missing issuer encrypted balance should fail before proof
4622 // verification.
4623 {
4624 Env env{*this, features};
4625 Account const alice("alice");
4626 Account const bob("bob");
4627 ConfidentialEnv confEnv{
4628 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4629 auto& mptAlice = confEnv.mpt;
4630
4631 removeMPTokenField(env, mptAlice, bob, sfIssuerEncryptedBalance);
4632 mptAlice.confidentialClaw({
4633 .account = alice,
4634 .holder = bob,
4635 .amt = 60,
4636 .proof = dummyClawbackProof,
4637 .err = tecNO_PERMISSION,
4638 });
4639 }
4640
4641 // Missing holder encryption key should fail before proof verification.
4642 {
4643 Env env{*this, features};
4644 Account const alice("alice");
4645 Account const bob("bob");
4646 ConfidentialEnv confEnv{
4647 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4648 auto& mptAlice = confEnv.mpt;
4649
4650 removeMPTokenField(env, mptAlice, bob, sfHolderEncryptionKey);
4651 mptAlice.confidentialClaw({
4652 .account = alice,
4653 .holder = bob,
4654 .amt = 60,
4655 .proof = dummyClawbackProof,
4656 .err = tecNO_PERMISSION,
4657 });
4658 }
4659
4660 // lock should not block clawback. lock bob individually
4661 {
4662 Env env{*this, features};
4663 Account const alice("alice");
4664 Account const bob("bob");
4665 ConfidentialEnv confEnv{
4666 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4667 auto& mptAlice = confEnv.mpt;
4668 mptAlice.set({
4669 .account = alice,
4670 .holder = bob,
4671 .flags = tfMPTLock,
4672 });
4673
4674 // clawback should still work
4675 mptAlice.confidentialClaw({
4676 .account = alice,
4677 .holder = bob,
4678 .amt = 60,
4679 });
4680 }
4681
4682 // lock globally
4683 {
4684 Env env{*this, features};
4685 Account const alice("alice");
4686 Account const bob("bob");
4687 ConfidentialEnv confEnv{
4688 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4689 auto& mptAlice = confEnv.mpt;
4690 mptAlice.set({
4691 .account = alice,
4692 .flags = tfMPTLock,
4693 });
4694
4695 // clawback should still work
4696 mptAlice.confidentialClaw({
4697 .account = alice,
4698 .holder = bob,
4699 .amt = 60,
4700 });
4701 }
4702
4703 // unauthorize should not block clawback
4704 {
4705 Env env{*this, features};
4706 Account const alice("alice");
4707 Account const bob("bob");
4708 ConfidentialEnv confEnv{
4709 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4710 auto& mptAlice = confEnv.mpt;
4711
4712 // unauthorize bob
4713 mptAlice.authorize({
4714 .account = alice,
4715 .holder = bob,
4716 .flags = tfMPTUnauthorize,
4717 });
4718 // clawback should still work
4719 mptAlice.confidentialClaw({
4720 .account = alice,
4721 .holder = bob,
4722 .amt = 60,
4723 });
4724 }
4725
4726 // insufficient funds, clawback amount exceeding confidential
4727 // outstanding amount
4728 {
4729 Env env{*this, features};
4730 Account const alice("alice");
4731 Account const bob("bob");
4732 ConfidentialEnv confEnv{
4733 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 60}}, setupFlags};
4734 auto& mptAlice = confEnv.mpt;
4735
4736 mptAlice.confidentialClaw({
4737 .account = alice,
4738 .holder = bob,
4739 .amt = 10000,
4740 .err = tecINSUFFICIENT_FUNDS,
4741 });
4742 }
4743 }
4744
4745 void
4747 {
4748 testcase("ConfidentialMPTClawback Proof");
4749 using namespace test::jtx;
4750
4751 Account const alice("alice");
4752 Account const bob("bob");
4753 Account const carol("carol");
4754
4755 // lambda function to set up MPT with alice as issuer, bob and carol
4756 // as authorized holders, and fund 1000 mpt to bob and 2000 mpt to
4757 // carol.
4758 auto setupEnv = [&](Env& env) -> MPTTester {
4759 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
4760
4761 mptAlice.create({
4762 .flags = tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance,
4763 });
4764
4765 for (auto const& [acct, amt] : {std::pair{bob, 1000}, {carol, 2000}})
4766 {
4767 mptAlice.authorize({
4768 .account = acct,
4769 });
4770 mptAlice.pay(alice, acct, amt);
4771 mptAlice.generateKeyPair(acct);
4772 }
4773
4774 mptAlice.generateKeyPair(alice);
4775 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4776
4777 return mptAlice;
4778 };
4779
4780 // lambda function to test a set of bad clawback amounts that should
4781 // return tecBAD_PROOF
4782 auto checkBadProofs =
4783 [&](MPTTester& mpt, Account const& holder, std::initializer_list<uint64_t> amts) {
4784 for (auto const badAmt : amts)
4785 {
4786 mpt.confidentialClaw({
4787 .account = alice,
4788 .holder = holder,
4789 .amt = badAmt,
4790 .err = tecBAD_PROOF,
4791 });
4792 }
4793 };
4794
4795 // SCENARIO 1: clawback from inbox only or spending only balances.
4796 // bob converts 500 and merge inbox,
4797 // carol converts 1000, but not merge inbox.
4798 // after setup, bob has 500 in spending, carol has 1000 in inbox.
4799 {
4800 Env env{*this, features};
4801 auto mptAlice = setupEnv(env);
4802
4803 // bob converts and merges
4804 mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)});
4805 mptAlice.mergeInbox({
4806 .account = bob,
4807 });
4808 // carol converts without merge
4809 mptAlice.convert(
4810 {.account = carol, .amt = 1000, .holderPubKey = mptAlice.getPubKey(carol)});
4811
4812 // verify proof fails with invalid clawback amount
4813 // bob: 500 in Spending, 0 in Inbox
4814 checkBadProofs(
4815 mptAlice,
4816 bob,
4817 {
4818 1,
4819 10,
4820 70,
4821 100,
4822 110,
4823 200,
4824 499,
4825 501,
4826 600,
4827 });
4828
4829 // carol: 1000 in Inbox, 0 in Spending
4830 checkBadProofs(
4831 mptAlice,
4832 carol,
4833 {
4834 1,
4835 10,
4836 50,
4837 500,
4838 777,
4839 850,
4840 999,
4841 1001,
4842 1200,
4843 });
4844
4845 // clawback with correct amount that passes proof verification
4846 mptAlice.confidentialClaw({
4847 .account = alice,
4848 .holder = bob,
4849 .amt = 500,
4850 });
4851 mptAlice.confidentialClaw({
4852 .account = alice,
4853 .holder = carol,
4854 .amt = 1000,
4855 });
4856 }
4857
4858 // SCENARIO 2: clawback from mixed inbox and spending balances.
4859 // bob converts 300 to confidential and merge inbox,
4860 // carol converts 400 to confidential and merge inbox,
4861 // bob sends 100 to carol, carol sends 100 to bob.
4862 // After setup, bob has 100 in inbox and 200 in spending;
4863 // carol has 100 in inbox and 300 in spending.
4864 {
4865 Env env{*this, features};
4866 auto mptAlice = setupEnv(env);
4867
4868 mptAlice.convert({.account = bob, .amt = 300, .holderPubKey = mptAlice.getPubKey(bob)});
4869 mptAlice.mergeInbox({
4870 .account = bob,
4871 });
4872 mptAlice.convert(
4873 {.account = carol, .amt = 400, .holderPubKey = mptAlice.getPubKey(carol)});
4874 mptAlice.mergeInbox({
4875 .account = carol,
4876 });
4877 mptAlice.send({
4878 .account = bob,
4879 .dest = carol,
4880 .amt = 100,
4881 });
4882 mptAlice.send({
4883 .account = carol,
4884 .dest = bob,
4885 .amt = 100,
4886 });
4887
4888 // verify proof fails with invalid clawback amount
4889 // bob: 100 in inbox, 200 in spending
4890 checkBadProofs(
4891 mptAlice,
4892 bob,
4893 {
4894 1,
4895 10,
4896 50,
4897 100,
4898 200,
4899 299,
4900 301,
4901 400,
4902 });
4903
4904 // proof failure for incorrect amount when clawbacking from
4905 // carol carol: 100 in inbox, 300 in spending
4906 checkBadProofs(
4907 mptAlice,
4908 carol,
4909 {
4910 1,
4911 10,
4912 50,
4913 100,
4914 300,
4915 399,
4916 401,
4917 501,
4918 });
4919
4920 // clawback with correct amount that passes proof verification
4921 mptAlice.confidentialClaw({
4922 .account = alice,
4923 .holder = bob,
4924 .amt = 300,
4925 });
4926 mptAlice.confidentialClaw({
4927 .account = alice,
4928 .holder = carol,
4929 .amt = 400,
4930 });
4931 }
4932
4933 // SCENARIO 3: the clawback proof omits the holder's confidential
4934 // balance version. A proof generated before the version advances is
4935 // still accepted, because getClawbackContextHash has no version
4936 // component.
4937 {
4938 Env env{*this, features};
4939 auto mptAlice = setupEnv(env);
4940
4941 mptAlice.convert({.account = bob, .amt = 500, .holderPubKey = mptAlice.getPubKey(bob)});
4942 mptAlice.mergeInbox({
4943 .account = bob,
4944 });
4945
4946 auto const privKey = mptAlice.getPrivKey(alice);
4947 if (!BEAST_EXPECT(privKey.has_value()))
4948 return;
4949
4950 auto const proof = mptAlice.getClawbackProof(
4951 bob,
4952 500,
4953 requireOptionalRef(privKey, "Missing private key"),
4955 alice.id(), mptAlice.issuanceID(), env.seq(alice), bob.id()));
4956 if (!BEAST_EXPECT(proof.has_value()))
4957 return;
4958
4959 // Advance bob's balance version after the proof is generated. An
4960 // empty-inbox merge leaves the balance unchanged but still bumps
4961 // sfConfidentialBalanceVersion.
4962 auto const versionBefore = mptAlice.getMPTokenVersion(bob);
4963 mptAlice.mergeInbox({.account = bob});
4964 BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) != versionBefore);
4965
4966 // The stale-version proof is still accepted.
4967 mptAlice.confidentialClaw({
4968 .account = alice,
4969 .holder = bob,
4970 .amt = 500,
4971 .proof = strHex(requireOptional(proof, "Missing proof")),
4972 });
4973 }
4974 }
4975
4976 void
4978 {
4979 testcase("Public transfers after clearing Confidential Flag");
4980 using namespace test::jtx;
4981
4982 Account const alice("alice");
4983 Account const bob("bob");
4984 Account const carol("carol");
4985
4986 // After clearing the confidential flag, all four public MPT operations
4987 // must succeed regardless of which confidential path left encrypted-zero
4988 // fields on bob's MPToken.
4989 auto runPublicPayments = [&](MPTTester& mpt) {
4990 mpt.pay(bob, carol, 10);
4991 mpt.pay(carol, bob, 5);
4992 mpt.pay(alice, bob, 1);
4993 mpt.pay(carol, alice, 5);
4994 };
4995
4996 auto drainAndDeleteBobMPToken = [&](Env& env, MPTTester& mpt) {
4997 auto const bobBalance = mpt.getBalance(bob);
4998 BEAST_EXPECT(bobBalance > 0);
4999
5000 mpt.pay(bob, alice, bobBalance);
5001 BEAST_EXPECT(mpt.getBalance(bob) == 0);
5002
5003 mpt.authorize({.account = bob, .flags = tfMPTUnauthorize});
5004 BEAST_EXPECT(!env.le(keylet::mptoken(mpt.issuanceID(), bob.id())));
5005 };
5006
5007 // Alice pays Bob 100 public, Bob converts 50 confidential
5008 // Bob converts 50 back to public, and make sure can receive public payments
5009 {
5010 Env env{*this, features};
5011 ConfidentialEnv ct{
5012 env,
5013 alice,
5014 {{.account = bob, .payAmount = 100, .convertAmount = 50}},
5015 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
5016
5017 env.fund(XRP(1'000), carol);
5018 ct.mpt.authorize({.account = carol});
5019 ct.mpt.pay(alice, carol, 50);
5020
5021 ct.mpt.convertBack({.account = bob, .amt = 50});
5022
5023 runPublicPayments(ct.mpt);
5024 drainAndDeleteBobMPToken(env, ct.mpt);
5025 }
5026
5027 // Same path as above but with Auditor
5028 {
5029 Env env{*this, features};
5030 Account const auditor("auditor");
5031 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
5032
5033 mptAlice.create({
5034 .ownerCount = 1,
5035 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
5036 });
5037
5038 mptAlice.authorize({.account = bob});
5039 mptAlice.authorize({.account = carol});
5040 mptAlice.pay(alice, bob, 100);
5041 mptAlice.pay(alice, carol, 50);
5042
5043 mptAlice.generateKeyPair(alice);
5044 mptAlice.generateKeyPair(bob);
5045 mptAlice.generateKeyPair(auditor);
5046 mptAlice.set(
5047 {.account = alice,
5048 .issuerPubKey = mptAlice.getPubKey(alice),
5049 .auditorPubKey = mptAlice.getPubKey(auditor)});
5050
5051 mptAlice.convert({
5052 .account = bob,
5053 .amt = 50,
5054 .holderPubKey = mptAlice.getPubKey(bob),
5055 });
5056 mptAlice.mergeInbox({.account = bob});
5057 mptAlice.convertBack({.account = bob, .amt = 50});
5058
5059 runPublicPayments(mptAlice);
5060 drainAndDeleteBobMPToken(env, mptAlice);
5061 }
5062
5063 // Confidential clawback leaves encrypted-zero fields;
5064 // the public balance remaining after the clawback must stay usable.
5065 {
5066 Env env{*this, features};
5067 ConfidentialEnv ct{
5068 env,
5069 alice,
5070 {{.account = bob, .payAmount = 100, .convertAmount = 50}},
5071 tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanHoldConfidentialBalance};
5072
5073 env.fund(XRP(1'000), carol);
5074 ct.mpt.authorize({.account = carol});
5075 ct.mpt.pay(alice, carol, 50);
5076
5077 ct.mpt.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
5078
5079 runPublicPayments(ct.mpt);
5080 drainAndDeleteBobMPToken(env, ct.mpt);
5081 }
5082 }
5083
5084 void
5086 {
5087 testcase("mutate lsfMPTCanHoldConfidentialBalance");
5088 using namespace test::jtx;
5089
5090 // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance
5091 // when featureDynamicMPT is disabled
5092 {
5093 Env env{*this, features - featureDynamicMPT};
5094 Account const alice("alice");
5095 Account const bob("bob");
5096 MPTTester mptAlice(env, alice, {.holders = {bob}});
5097
5098 mptAlice.create({
5099 .ownerCount = 0,
5101 .err = temDISABLED,
5102 });
5103 }
5104
5105 // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance when
5106 // featureConfidentialTransfer is disabled
5107 {
5108 Env env{*this, features - featureConfidentialTransfer};
5109 Account const alice("alice");
5110 Account const bob("bob");
5111 MPTTester mptAlice(env, alice, {.holders = {bob}});
5112
5113 mptAlice.create({
5114 .ownerCount = 0,
5116 .err = temDISABLED,
5117 });
5118 }
5119
5120 // if lsmfMPTCannotEnableCanHoldConfidentialBalance is set, can not set/clear
5121 // lsfMPTCanHoldConfidentialBalance
5122 {
5123 Env env{*this, features};
5124 Account const alice("alice");
5125 Account const bob("bob");
5126 MPTTester mptAlice(env, alice, {.holders = {bob}});
5127
5128 mptAlice.create({
5129 .ownerCount = 1,
5130 .flags = tfMPTCanTransfer,
5132 });
5133
5134 mptAlice.set({
5135 .account = alice,
5137 .err = tecNO_PERMISSION,
5138 });
5139 }
5140
5141 // Toggle lsfMPTCanHoldConfidentialBalance
5142 {
5143 Env env{*this, features};
5144 Account const alice("alice");
5145 Account const bob("bob");
5146 MPTTester mptAlice(env, alice, {.holders = {bob}});
5147
5148 mptAlice.create({
5149 .ownerCount = 1,
5150 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
5151 .mutableFlags = tmfMPTCanEnableCanLock,
5152 });
5153
5154 mptAlice.authorize({
5155 .account = bob,
5156 });
5157 mptAlice.pay(alice, bob, 100);
5158
5159 mptAlice.generateKeyPair(alice);
5160 mptAlice.generateKeyPair(bob);
5161 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
5162
5163 auto holderPubKeySet = false;
5164 auto verifyToggle = [&](TER expectedResult, uint64_t amt) {
5165 if (!holderPubKeySet)
5166 {
5167 mptAlice.convert({
5168 .account = bob,
5169 .amt = amt,
5170 .holderPubKey = mptAlice.getPubKey(bob),
5171 .err = expectedResult,
5172 });
5173 }
5174 else
5175 {
5176 mptAlice.convert({
5177 .account = bob,
5178 .amt = amt,
5179 .err = expectedResult,
5180 });
5181 }
5182
5183 if (expectedResult == tesSUCCESS)
5184 {
5185 holderPubKeySet = true;
5186 mptAlice.mergeInbox({
5187 .account = bob,
5188 });
5189
5190 // make sure there's no confidential outstanding balance
5191 // for the next toggle test
5192 mptAlice.convertBack({
5193 .account = bob,
5194 .amt = amt,
5195 });
5196 }
5197 };
5198
5199 // set lsfMPTCanHoldConfidentialBalance, but no effect because
5200 // lsfMPTCanHoldConfidentialBalance was already set
5201 mptAlice.set({
5202 .account = alice,
5204 });
5205 verifyToggle(tesSUCCESS, 10);
5206
5207 // set tmfMPTSetCanHoldConfidentialBalance again
5208 mptAlice.set({
5209 .account = alice,
5211 });
5212 verifyToggle(tesSUCCESS, 30);
5213 }
5214
5215 // can not mutate lsfPrivacy when there's confidential
5216 // outstanding amount
5217 {
5218 Env env{*this, features};
5219 Account const alice("alice");
5220 Account const bob("bob");
5221 MPTTester mptAlice(env, alice, {.holders = {bob}});
5222
5223 // lsmfMPTCannotEnableCanHoldConfidentialBalance is false by default,
5224 // so that lsfMPTCanHoldConfidentialBalance can be mutated
5225 mptAlice.create({
5226 .ownerCount = 1,
5227 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
5228 });
5229
5230 mptAlice.authorize({
5231 .account = bob,
5232 });
5233 mptAlice.pay(alice, bob, 100);
5234
5235 mptAlice.generateKeyPair(alice);
5236 mptAlice.generateKeyPair(bob);
5237 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
5238
5239 // bob convert 50 to confidential
5240 mptAlice.convert({.account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob)});
5241
5242 // set lsfMPTCanHoldConfidentialBalance should fail because of
5243 // confidential outstanding balance
5244 mptAlice.set({
5245 .account = alice,
5247 .err = tecNO_PERMISSION,
5248 });
5249 }
5250 }
5251
5252 void
5254 {
5255 testcase("Convert back pedersen proof");
5256 using namespace test::jtx;
5257
5258 Env env{*this, features};
5259 Account const alice("alice");
5260 Account const bob("bob");
5261 ConfidentialEnv confEnv{
5262 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
5263 auto& mptAlice = confEnv.mpt;
5264
5265 // for ease of understanding, generate all the fields here instead of
5266 // autofilling
5267 uint64_t const amt = 10;
5268 Buffer const blindingFactor = generateBlindingFactor();
5269 Buffer const pcBlindingFactor = generateBlindingFactor();
5270
5271 auto const spendingBalance = requireOptional(
5272 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
5273 "Missing spending balance");
5274 auto const encryptedSpendingBalance = requireOptional(
5275 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
5276 "Missing encrypted spending balance");
5277 BEAST_EXPECT(!encryptedSpendingBalance.empty());
5278
5279 Buffer const pedersenCommitment =
5280 mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
5281 Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
5282 Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
5283 auto const version = mptAlice.getMPTokenVersion(bob);
5284
5285 // These tests verify that the compact ConvertBack proof validation
5286 // correctly rejects proofs generated with incorrect parameters.
5287 // The compact proof simultaneously verifies balance ownership,
5288 // commitment linkage, and that remaining balance is non-negative.
5289
5290 // Test 1: Proof generated with wrong pedersen commitment value.
5291 // The proof uses PC(1, rho) but the transaction submits PC(balance, rho).
5292 // Verification fails because the proof doesn't match the submitted commitment.
5293 {
5294 uint256 const contextHash =
5295 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5296 Buffer const badPedersenCommitment =
5297 mptAlice.getPedersenCommitment(1, pcBlindingFactor);
5298 Buffer const proof = mptAlice.getConvertBackProof(
5299 bob,
5300 amt,
5301 contextHash,
5302 {
5303 .pedersenCommitment = badPedersenCommitment, // wrong pedersen commitment
5304 .amt = spendingBalance,
5305 .encryptedAmt = encryptedSpendingBalance,
5306 .blindingFactor = pcBlindingFactor,
5307 });
5308
5309 mptAlice.convertBack({
5310 .account = bob,
5311 .amt = amt,
5312 .proof = proof,
5313 .holderEncryptedAmt = bobCiphertext,
5314 .issuerEncryptedAmt = issuerCiphertext,
5315 .blindingFactor = blindingFactor,
5316 .pedersenCommitment = pedersenCommitment,
5317 .err = tecBAD_PROOF,
5318 });
5319 }
5320
5321 // Test 2: Proof generated with wrong blinding factor (rho).
5322 // The pedersen commitment PC = balance*G + rho*H requires the same rho
5323 // used in proof generation. Using a different rho breaks the linkage.
5324 {
5325 uint256 const contextHash =
5326 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5327
5328 Buffer const proof = mptAlice.getConvertBackProof(
5329 bob,
5330 amt,
5331 contextHash,
5332 {
5333 .pedersenCommitment = pedersenCommitment,
5334 .amt = spendingBalance,
5335 .encryptedAmt = encryptedSpendingBalance,
5336 .blindingFactor = generateBlindingFactor(), // wrong blinding factor
5337 });
5338
5339 mptAlice.convertBack({
5340 .account = bob,
5341 .amt = amt,
5342 .proof = proof,
5343 .holderEncryptedAmt = bobCiphertext,
5344 .issuerEncryptedAmt = issuerCiphertext,
5345 .blindingFactor = blindingFactor,
5346 .pedersenCommitment = pedersenCommitment,
5347 .err = tecBAD_PROOF,
5348 });
5349 }
5350
5351 // Test 3: Proof generated with wrong balance value.
5352 // The proof claims balance=1 but the encrypted spending balance contains
5353 // the actual balance. Verification fails because the values don't match.
5354 {
5355 uint256 const contextHash =
5356 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5357
5358 Buffer const proof = mptAlice.getConvertBackProof(
5359 bob,
5360 amt,
5361 contextHash,
5362 {
5363 .pedersenCommitment = pedersenCommitment,
5364 .amt = 1, // wrong balance
5365 .encryptedAmt = encryptedSpendingBalance,
5366 .blindingFactor = pcBlindingFactor,
5367 });
5368
5369 mptAlice.convertBack({
5370 .account = bob,
5371 .amt = amt,
5372 .proof = proof,
5373 .holderEncryptedAmt = bobCiphertext,
5374 .issuerEncryptedAmt = issuerCiphertext,
5375 .blindingFactor = blindingFactor,
5376 .pedersenCommitment = pedersenCommitment,
5377 .err = tecBAD_PROOF,
5378 });
5379 }
5380
5381 // Test 4: Correct proof but wrong pedersen commitment in transaction.
5382 // The proof is generated correctly, but the transaction submits a
5383 // different pedersen commitment. Verification fails because the
5384 // submitted commitment doesn't match what the proof was generated for.
5385 {
5386 uint256 const contextHash =
5387 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5388 Buffer const badPedersenCommitment =
5389 mptAlice.getPedersenCommitment(1, pcBlindingFactor);
5390 Buffer const proof = mptAlice.getConvertBackProof(
5391 bob,
5392 amt,
5393 contextHash,
5394 {
5395 .pedersenCommitment = pedersenCommitment,
5396 .amt = spendingBalance,
5397 .encryptedAmt = encryptedSpendingBalance,
5398 .blindingFactor = pcBlindingFactor,
5399 });
5400
5401 mptAlice.convertBack({
5402 .account = bob,
5403 .amt = amt,
5404 .proof = proof,
5405 .holderEncryptedAmt = bobCiphertext,
5406 .issuerEncryptedAmt = issuerCiphertext,
5407 .blindingFactor = blindingFactor,
5408 .pedersenCommitment = badPedersenCommitment, // wrong pedersen commitment
5409 .err = tecBAD_PROOF,
5410 });
5411 }
5412
5413 // Test 5: Proof generated with wrong context hash.
5414 // The context hash binds the proof to a specific transaction (account,
5415 // sequence, issuanceID, amount, version). Using a different context hash
5416 // makes the proof invalid for this transaction, preventing replay attacks.
5417 {
5418 uint256 const badContextHash{1};
5419
5420 Buffer const proof = mptAlice.getConvertBackProof(
5421 bob,
5422 amt,
5423 badContextHash, // wrong context hash
5424 {
5425 .pedersenCommitment = pedersenCommitment,
5426 .amt = spendingBalance,
5427 .encryptedAmt = encryptedSpendingBalance,
5428 .blindingFactor = pcBlindingFactor,
5429 });
5430
5431 mptAlice.convertBack({
5432 .account = bob,
5433 .amt = amt,
5434 .proof = proof,
5435 .holderEncryptedAmt = bobCiphertext,
5436 .issuerEncryptedAmt = issuerCiphertext,
5437 .blindingFactor = blindingFactor,
5438 .pedersenCommitment = pedersenCommitment,
5439 .err = tecBAD_PROOF,
5440 });
5441 }
5442
5443 // Test 6: Correct proof to verify the test setup is valid.
5444 // All parameters are correct, so the transaction should succeed.
5445 {
5446 uint256 const contextHash =
5447 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5448
5449 Buffer const proof = mptAlice.getConvertBackProof(
5450 bob,
5451 amt,
5452 contextHash,
5453 {
5454 .pedersenCommitment = pedersenCommitment,
5455 .amt = spendingBalance,
5456 .encryptedAmt = encryptedSpendingBalance,
5457 .blindingFactor = pcBlindingFactor,
5458 });
5459
5460 mptAlice.convertBack({
5461 .account = bob,
5462 .amt = amt,
5463 .proof = proof,
5464 .holderEncryptedAmt = bobCiphertext,
5465 .issuerEncryptedAmt = issuerCiphertext,
5466 .blindingFactor = blindingFactor,
5467 .pedersenCommitment = pedersenCommitment,
5468 });
5469 }
5470 }
5471
5472 void
5474 {
5475 testcase("Convert back bulletproof");
5476 using namespace test::jtx;
5477
5478 Env env{*this, features};
5479 Account const alice("alice");
5480 Account const bob("bob");
5481 ConfidentialEnv confEnv{
5482 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
5483 auto& mptAlice = confEnv.mpt;
5484
5485 // for ease of understanding, generate all the fields here instead of
5486 // autofilling
5487 uint64_t const amt = 10;
5488 Buffer const blindingFactor = generateBlindingFactor();
5489 Buffer const pcBlindingFactor = generateBlindingFactor();
5490
5491 auto const spendingBalance = requireOptional(
5492 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
5493 "Missing spending balance");
5494 auto const encryptedSpendingBalance = requireOptional(
5495 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
5496 "Missing encrypted spending balance");
5497 BEAST_EXPECT(!encryptedSpendingBalance.empty());
5498
5499 Buffer const pedersenCommitment =
5500 mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
5501 Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
5502 Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
5503 auto const version = mptAlice.getMPTokenVersion(bob);
5504
5505 // These tests verify that the compact ConvertBack proof (sigma + bulletproof)
5506 // correctly rejects proofs generated with incorrect parameters.
5507 // The compact proof simultaneously verifies balance ownership, commitment
5508 // linkage, and that the remaining balance is non-negative.
5509
5510 // Test 1: Proof generated with wrong balance value.
5511 // The sigma proof claims balance=1 but the spending balance contains the
5512 // actual balance. The compact proof's balance-linkage check fails.
5513 {
5514 uint256 const contextHash =
5515 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5516
5517 Buffer const proof = mptAlice.getConvertBackProof(
5518 bob,
5519 amt,
5520 contextHash,
5521 {
5522 .pedersenCommitment = pedersenCommitment,
5523 .amt = 1, // wrong balance (actual balance is ~40)
5524 .encryptedAmt = encryptedSpendingBalance,
5525 .blindingFactor = pcBlindingFactor,
5526 });
5527
5528 mptAlice.convertBack({
5529 .account = bob,
5530 .amt = amt,
5531 .proof = proof,
5532 .holderEncryptedAmt = bobCiphertext,
5533 .issuerEncryptedAmt = issuerCiphertext,
5534 .blindingFactor = blindingFactor,
5535 .pedersenCommitment = pedersenCommitment,
5536 .err = tecBAD_PROOF,
5537 });
5538 }
5539
5540 // Test 2: Proof generated with wrong blinding factor (rho).
5541 // The compact sigma proof must use the same blinding factor (rho) as the
5542 // Pedersen commitment PC = balance*G + rho*H. Using a different rho
5543 // creates an inconsistency the verifier detects.
5544 {
5545 uint256 const contextHash =
5546 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5547
5548 Buffer const proof = mptAlice.getConvertBackProof(
5549 bob,
5550 amt,
5551 contextHash,
5552 {
5553 .pedersenCommitment = pedersenCommitment,
5554 .amt = spendingBalance,
5555 .encryptedAmt = encryptedSpendingBalance,
5556 .blindingFactor = generateBlindingFactor(), // wrong blinding factor
5557 });
5558
5559 mptAlice.convertBack({
5560 .account = bob,
5561 .amt = amt,
5562 .proof = proof,
5563 .holderEncryptedAmt = bobCiphertext,
5564 .issuerEncryptedAmt = issuerCiphertext,
5565 .blindingFactor = blindingFactor,
5566 .pedersenCommitment = pedersenCommitment,
5567 .err = tecBAD_PROOF,
5568 });
5569 }
5570
5571 // Test 3: Proof generated with wrong context hash.
5572 // The context hash binds the proof to a specific transaction (account,
5573 // sequence, issuanceID, amount, version). Using a different context hash
5574 // makes the proof invalid for this transaction, preventing replay attacks.
5575 {
5576 uint256 const badContextHash{1};
5577 Buffer const proof = mptAlice.getConvertBackProof(
5578 bob,
5579 amt,
5580 badContextHash, // wrong context hash
5581 {
5582 .pedersenCommitment = pedersenCommitment,
5583 .amt = spendingBalance,
5584 .encryptedAmt = encryptedSpendingBalance,
5585 .blindingFactor = pcBlindingFactor,
5586 });
5587
5588 mptAlice.convertBack({
5589 .account = bob,
5590 .amt = amt,
5591 .proof = proof,
5592 .holderEncryptedAmt = bobCiphertext,
5593 .issuerEncryptedAmt = issuerCiphertext,
5594 .blindingFactor = blindingFactor,
5595 .pedersenCommitment = pedersenCommitment,
5596 .err = tecBAD_PROOF,
5597 });
5598 }
5599
5600 // Test 4: Correct proof to verify the test setup is valid.
5601 // All parameters are correct, so the transaction should succeed.
5602 {
5603 uint256 const contextHash =
5604 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version);
5605
5606 Buffer const proof = mptAlice.getConvertBackProof(
5607 bob,
5608 amt,
5609 contextHash,
5610 {
5611 .pedersenCommitment = pedersenCommitment,
5612 .amt = spendingBalance,
5613 .encryptedAmt = encryptedSpendingBalance,
5614 .blindingFactor = pcBlindingFactor,
5615 });
5616
5617 mptAlice.convertBack({
5618 .account = bob,
5619 .amt = amt,
5620 .proof = proof,
5621 .holderEncryptedAmt = bobCiphertext,
5622 .issuerEncryptedAmt = issuerCiphertext,
5623 .blindingFactor = blindingFactor,
5624 .pedersenCommitment = pedersenCommitment,
5625 });
5626 }
5627 }
5628
5629 // A convert-back proof is bound to (account, issuance, sequence, version) via
5630 // the Fiat-Shamir context hash. Crafting a proof against any single wrong
5631 // variable and submitting it with the real parameters must be rejected
5632 // with tecBAD_PROOF
5633 void
5635 {
5636 testcase("ConvertBack proof context binding");
5637 using namespace test::jtx;
5638
5639 auto runBadProof = [&](auto makeContextHash) {
5640 Env env{*this, features};
5641 Account const alice("alice");
5642 Account const bob("bob");
5643 Account const carol("carol");
5644 ConfidentialEnv confEnv{
5645 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
5646 auto& mptAlice = confEnv.mpt;
5647
5648 std::uint64_t const amt = 10;
5649 Buffer const blindingFactor = generateBlindingFactor();
5650 Buffer const pcBlindingFactor = generateBlindingFactor();
5651
5652 auto const spendingBalance =
5653 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
5654 auto const encryptedSpendingBalance =
5655 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
5656 if (!BEAST_EXPECT(spendingBalance && encryptedSpendingBalance))
5657 return;
5658
5659 Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(
5660 requireOptional(spendingBalance, "Missing spending balance"), pcBlindingFactor);
5661 Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
5662 Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
5663 auto const version = mptAlice.getMPTokenVersion(bob);
5664
5665 Buffer const proof = mptAlice.getConvertBackProof(
5666 bob,
5667 amt,
5668 makeContextHash(env, mptAlice, alice, bob, carol, version),
5669 {
5670 .pedersenCommitment = pedersenCommitment,
5671 .amt = requireOptional(spendingBalance, "Missing spending balance"),
5672 .encryptedAmt = requireOptionalRef(
5673 encryptedSpendingBalance, "Missing encrypted spending balance"),
5674 .blindingFactor = pcBlindingFactor,
5675 });
5676
5677 mptAlice.convertBack({
5678 .account = bob,
5679 .amt = amt,
5680 .proof = proof,
5681 .holderEncryptedAmt = bobCiphertext,
5682 .issuerEncryptedAmt = issuerCiphertext,
5683 .blindingFactor = blindingFactor,
5684 .pedersenCommitment = pedersenCommitment,
5685 .err = tecBAD_PROOF,
5686 });
5687 };
5688
5689 // Wrong account in the proof context.
5690 runBadProof([&](Env& env,
5691 MPTTester const& mpt,
5692 Account const&,
5693 Account const& bob,
5694 Account const& carol,
5695 std::uint32_t version) {
5696 return getConvertBackContextHash(carol.id(), mpt.issuanceID(), env.seq(bob), version);
5697 });
5698
5699 // Wrong issuance ID in the proof context.
5700 runBadProof([&](Env& env,
5701 MPTTester const&,
5702 Account const& alice,
5703 Account const& bob,
5704 Account const&,
5705 std::uint32_t version) {
5707 bob.id(), makeMptID(env.seq(alice) + 100, alice), env.seq(bob), version);
5708 });
5709
5710 // Wrong transaction sequence in the proof context.
5711 runBadProof([&](Env& env,
5712 MPTTester const& mpt,
5713 Account const&,
5714 Account const& bob,
5715 Account const&,
5716 std::uint32_t version) {
5717 return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob) + 1, version);
5718 });
5719
5720 // Wrong balance version in the proof context.
5721 runBadProof([&](Env& env,
5722 MPTTester const& mpt,
5723 Account const&,
5724 Account const& bob,
5725 Account const&,
5726 std::uint32_t version) {
5727 return getConvertBackContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), version + 1);
5728 });
5729 }
5730
5731 // This test simulates a valid proof π extracted from a transaction
5732 // for amount m1 is reused in a new transaction for a different
5733 // amount m2 with different ciphertexts. It confirms the context hash
5734 // recomputation fails due to the ciphertext binding mismatch, resulting
5735 // in tecBAD_PROOF.
5736 void
5738 {
5739 testcase("ConvertBack: proof ciphertext binding");
5740 using namespace test::jtx;
5741
5742 Env env{*this, features};
5743 Account const alice("alice"), bob("bob");
5744 ConfidentialEnv confEnv{
5745 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
5746 auto& mptAlice = confEnv.mpt;
5747
5748 auto const spendingBalance = requireOptional(
5749 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
5750 "Missing spending balance");
5751 auto const encryptedSpendingBalance = requireOptional(
5752 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
5753 "Missing encrypted spending balance");
5754 auto const version = mptAlice.getMPTokenVersion(bob);
5755 Buffer const pcBlindingFactor = generateBlindingFactor();
5756 Buffer const pedersenCommitment =
5757 mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
5758
5759 // Generate a valid proof pi for Amount m1 = 10
5760 uint64_t const amtA = 10;
5761 uint32_t const currentSeq = env.seq(bob);
5762 uint256 const contextHashA =
5763 getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, version);
5764
5765 Buffer const proofA = mptAlice.getConvertBackProof(
5766 bob,
5767 amtA,
5768 contextHashA,
5769 {
5770 .pedersenCommitment = pedersenCommitment,
5771 .amt = spendingBalance,
5772 .encryptedAmt = encryptedSpendingBalance,
5773 .blindingFactor = pcBlindingFactor,
5774 });
5775
5776 // Construct Transaction B with Amount m2 = 20 and attach Proof pi
5777 uint64_t const amtB = 20;
5778 Buffer const blindingFactorB = generateBlindingFactor();
5779 Buffer const bobCiphertextB = mptAlice.encryptAmount(bob, amtB, blindingFactorB);
5780 Buffer const issuerCiphertextB = mptAlice.encryptAmount(alice, amtB, blindingFactorB);
5781
5782 // We attempt to verify the proof pi (for amt 10) against the new ciphertexts (for amt 20).
5783 mptAlice.convertBack({
5784 .account = bob,
5785 .amt = amtB,
5786 .proof = proofA, // Extracted/Reused proof from Transaction A
5787 .holderEncryptedAmt = bobCiphertextB,
5788 .issuerEncryptedAmt = issuerCiphertextB,
5789 .blindingFactor = blindingFactorB,
5790 .pedersenCommitment = pedersenCommitment,
5791 .err = tecBAD_PROOF, // Expected failure
5792 });
5793 }
5794
5795 // This test simulates a valid proof π and ciphertext are
5796 // tied to version v, but are reused after an inbox merge has incremented
5797 // the CBS version to v+1. It confirms the validator rejects the transaction
5798 // before acceptance due to the ContextID mismatch.
5799 void
5801 {
5802 testcase("ConvertBack: proof version mismatch");
5803 using namespace test::jtx;
5804
5805 Env env{*this, features};
5806 Account const alice("alice"), bob("bob");
5807 ConfidentialEnv confEnv{
5808 env, alice, {{.account = bob, .payAmount = 1000, .convertAmount = 100}}};
5809 auto& mptAlice = confEnv.mpt;
5810
5811 auto const versionV = mptAlice.getMPTokenVersion(bob);
5812 auto const spendingBalanceV = requireOptional(
5813 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
5814 "Missing spending balance");
5815 auto const encryptedSpendingBalanceV = requireOptional(
5816 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
5817 "Missing encrypted spending balance");
5818
5819 // Parameters for the intended ConvertBack transaction
5820 uint64_t const amt = 10;
5821 Buffer const blindingFactor = generateBlindingFactor();
5822 Buffer const pcBlindingFactor = generateBlindingFactor();
5823 Buffer const pedersenCommitment =
5824 mptAlice.getPedersenCommitment(spendingBalanceV, pcBlindingFactor);
5825 Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
5826 Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
5827
5828 // State Change: Increment version to v+1
5829 // Converting more funds and merging increments the sfConfidentialBalanceVersion
5830 mptAlice.convert({
5831 .account = bob,
5832 .amt = 50,
5833 });
5834 mptAlice.mergeInbox({
5835 .account = bob,
5836 });
5837
5838 BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) > versionV);
5839
5840 // Attack: Attempt to reuse proof tied to Version v at ledger Version v+1
5841 uint32_t const currentSeq = env.seq(bob);
5842 // Proof is explicitly generated using the outdated Version v
5843 uint256 const oldContextHash =
5844 getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, versionV);
5845
5846 Buffer const oldProof = mptAlice.getConvertBackProof(
5847 bob,
5848 amt,
5849 oldContextHash,
5850 {
5851 .pedersenCommitment = pedersenCommitment,
5852 .amt = spendingBalanceV,
5853 .encryptedAmt = encryptedSpendingBalanceV,
5854 .blindingFactor = pcBlindingFactor,
5855 });
5856
5857 // Submit and verify failure
5858 mptAlice.convertBack({
5859 .account = bob,
5860 .amt = amt,
5861 .proof = oldProof,
5862 .holderEncryptedAmt = bobCiphertext,
5863 .issuerEncryptedAmt = issuerCiphertext,
5864 .blindingFactor = blindingFactor,
5865 .pedersenCommitment = pedersenCommitment,
5866 .err = tecBAD_PROOF, // Fails because TransactionContextID differs
5867 });
5868 }
5869
5870 /* This test simulates an attack where the holder ciphertext is modified
5871 * via homomorphic addition (adding Encrypted_amt(1)) while leaving the issuer
5872 * ciphertext unchanged. It confirms that the validator detects the
5873 * mismatch between the re-computed ciphertexts and the submitted ones,
5874 * resulting in tecBAD_PROOF. */
5875 void
5877 {
5878 testcase("ConvertBack: homomorphic ciphertext modification");
5879 using namespace test::jtx;
5880
5881 Env env{*this, features};
5882 Account const alice("alice"), bob("bob");
5883 ConfidentialEnv confEnv{
5884 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 50}}};
5885 auto& mptAlice = confEnv.mpt;
5886
5887 // Prepare valid parameters for a ConvertBack of 10
5888 uint64_t const amt = 10;
5889 Buffer const bf = generateBlindingFactor();
5890
5891 auto const holderCipherText = mptAlice.encryptAmount(bob, amt, bf);
5892 auto const issuerCipherText = mptAlice.encryptAmount(alice, amt, bf);
5893
5894 // Generate a "Delta" ciphertext (Encrypting 1)
5895 // We use Bob's key because we are tampering with Bob's (Holder's) field
5896 Buffer const deltaBf = generateBlindingFactor();
5897 auto const deltaCipherText = mptAlice.encryptAmount(bob, 1, deltaBf);
5898
5899 // Homomorphically add Delta to HolderCipherText: Tampered = Enc(10) + Enc(1) = Enc(11)
5900 Buffer tamperedHolderCipherText = requireOptional(
5901 homomorphicAdd(holderCipherText, deltaCipherText), "Missing tampered ciphertext");
5902
5903 // Generate a valid proof for the ORIGINAL amount (10)
5904 auto const spendingBal = requireOptional(
5905 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
5906 "Missing spending balance");
5907 auto const spendingBalEnc = requireOptional(
5908 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
5909 "Missing encrypted spending balance");
5910 Buffer const pcBf = generateBlindingFactor();
5911 auto const pedersenCommitment = mptAlice.getPedersenCommitment(spendingBal, pcBf);
5912
5913 auto const currentVersion = mptAlice.getMPTokenVersion(bob);
5914 // Uses the new signature: Account, IssuanceID, Sequence, Version
5915 uint256 const contextHash =
5916 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
5917
5918 Buffer const proof = mptAlice.getConvertBackProof(
5919 bob,
5920 amt,
5921 contextHash,
5922 {
5923 .pedersenCommitment = pedersenCommitment,
5924 .amt = spendingBal,
5925 .encryptedAmt = spendingBalEnc,
5926 .blindingFactor = pcBf,
5927 });
5928
5929 // Submit transaction with Divergent Ciphertexts
5930 // Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10.
5931 // The consistency check (re-encryption of `amt` with `bf`) will match Issuer but FAIL for
5932 // Holder.
5933 mptAlice.convertBack({
5934 .account = bob,
5935 .amt = amt,
5936 .proof = proof,
5937 .holderEncryptedAmt = tamperedHolderCipherText, // Tampered (11)
5938 .issuerEncryptedAmt = issuerCipherText, // Original (10)
5939 .blindingFactor = bf,
5940 .pedersenCommitment = pedersenCommitment,
5941 .err = tecBAD_PROOF,
5942 });
5943 }
5944
5945 /* This test verifies that xrpld correctly rejects attempts to
5946 * overflow the maximum allowable token amount via homomorphic manipulation.
5947 * It simulates an attack where an individual takes a valid ciphertext encrypting
5948 * the maximum amount (kMaxMpTokenAmount) and homomorphically adds an encryption of
5949 * 1 to it, producing a ciphertext for MAX+1. The test confirms that the Bulletproof
5950 * range proof or inner-product constraints detect this overflow and invalidate the
5951 * transaction, preserving the supply invariant. */
5952 void
5954 {
5955 testcase("Send: homomorphic overflow attack via Enc(MAX) + Enc(1)");
5956 using namespace test::jtx;
5957
5958 Env env{*this, features};
5959 Account const alice("alice"), bob("bob"), carol("carol");
5960 ConfidentialEnv confEnv{
5961 env,
5962 alice,
5963 {{.account = bob, .payAmount = 100, .convertAmount = 100},
5964 {.account = carol, .payAmount = 50, .convertAmount = 50}}};
5965 auto& mptAlice = confEnv.mpt;
5966
5967 // Bob sends 10 to carol. The send amount (10) and Bob's remaining balance
5968 // (90) are both within [0, kMaxMpTokenAmount]. Range proof passes.
5969 mptAlice.send({.account = bob, .dest = carol, .amt = 10});
5970
5971 // Bob's spending balance is 90 after the baseline send.
5972 auto const bobSpendingBefore =
5973 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
5974 BEAST_EXPECT(bobSpendingBefore == 90);
5975
5976 // Construct Enc(kMaxMpTokenAmount) with Bob's public key.
5977 Buffer const bf1 = generateBlindingFactor();
5978 Buffer const encMax = mptAlice.encryptAmount(bob, kMaxMpTokenAmount, bf1);
5979
5980 // Construct Enc(1) with a separate blinding factor.
5981 Buffer const bf2 = generateBlindingFactor();
5982 Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
5983
5984 // Homomorphically add to produce CB_S_holder' = Enc(MAX) + Enc(1)
5985 Buffer overflowedCt =
5986 requireOptional(homomorphicAdd(encMax, encOne), "Missing overflowed ciphertext");
5987
5988 // Submit the send transaction with the tampered ciphertext.
5989 // Setting amt = kMaxMpTokenAmount + 1 drives proof generation for the
5990 // overflowed value. The bulletproof range check [0, kMaxMpTokenAmount]
5991 // rejects MAX+1; the validator must return tecBAD_PROOF.
5992 mptAlice.send({
5993 .account = bob,
5994 .dest = carol,
5995 .amt = kMaxMpTokenAmount + 1,
5996 .senderEncryptedAmt = overflowedCt,
5997 .err = tecBAD_PROOF,
5998 });
5999
6000 auto const bobSpendingAfter =
6001 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
6002 BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
6003 }
6004
6005 /* This test ensures that the system prevents underflow attacks where a user
6006 * attempts to create a negative balance through homomorphic subtraction. It
6007 * simulates a scenario where an attacker takes a ciphertext encrypting zero
6008 * and subtracts an encryption of 1, resulting in a value of -1.
6009 * The test asserts that the range proof verification fails because the resulting
6010 * value falls outside the valid non-negative range [0, kMaxMpTokenAmount],
6011 * causing the validator to reject the transaction with tecBAD_PROOF. */
6012 void
6014 {
6015 testcase("ConvertBack: homomorphic underflow attack via Enc(0) - Enc(1)");
6016 using namespace test::jtx;
6017
6018 Env env{*this, features};
6019 Account const alice("alice"), bob("bob");
6020 ConfidentialEnv confEnv{
6021 env, alice, {{.account = bob, .payAmount = 10, .convertAmount = 10}}};
6022 auto& mptAlice = confEnv.mpt;
6023
6024 // Converting back 1 from 10 leaves remaining balance = 9 (non-negative).
6025 // Range proof [0, kMaxMpTokenAmount] passes.
6026 mptAlice.convertBack({.account = bob, .amt = 1});
6027
6028 // Bob's spending balance is now 9; public balance is 1.
6029 auto const bobSpendingBefore =
6030 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
6031 BEAST_EXPECT(bobSpendingBefore == 9);
6032 auto const bobPublicBefore = mptAlice.getBalance(bob);
6033 BEAST_EXPECT(bobPublicBefore == 1);
6034
6035 // Construct Enc(0) — the zero encrypted balance using Bob's key.
6036 Buffer const bf1 = generateBlindingFactor();
6037 Buffer const encZero = mptAlice.encryptAmount(bob, 0, bf1);
6038
6039 // Construct Enc(1) with a separate blinding factor.
6040 Buffer const bf2 = generateBlindingFactor();
6041 Buffer const encOne = mptAlice.encryptAmount(bob, 1, bf2);
6042
6043 // Homomorphically subtract to produce CB_S_holder' = Enc(0) − Enc(1)
6044 // = Enc(−1), which lies below [0, kMaxMpTokenAmount].
6045 Buffer underflowedCt =
6046 requireOptional(homomorphicSubtract(encZero, encOne), "Missing underflowed ciphertext");
6047
6048 // The underflowed value as uint64_t: 0 - 1 wraps to 0xFFFFFFFFFFFFFFFF.
6049 // Generate a real proof using this wrapped value. The validator must still reject it
6050 // because 0xFFFFFFFFFFFFFFFE (remaining balance) is outside [0, kMaxMpTokenAmount].
6051 constexpr std::uint64_t kUnderflowedAmt =
6052 static_cast<std::uint64_t>(0) - static_cast<std::uint64_t>(1);
6053
6054 Buffer const pcBf = generateBlindingFactor();
6055 Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(kUnderflowedAmt, pcBf);
6056
6057 auto const currentVersion = mptAlice.getMPTokenVersion(bob);
6058 uint256 const contextHash =
6059 getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion);
6060
6061 Buffer const proof = mptAlice.getConvertBackProof(
6062 bob,
6063 1,
6064 contextHash,
6065 {
6066 .pedersenCommitment = pedersenCommitment,
6067 .amt = kUnderflowedAmt,
6068 .encryptedAmt = underflowedCt,
6069 .blindingFactor = pcBf,
6070 });
6071
6072 mptAlice.convertBack({
6073 .account = bob,
6074 .amt = 1,
6075 .proof = proof,
6076 .holderEncryptedAmt = underflowedCt,
6077 .pedersenCommitment = pedersenCommitment,
6078 .err = tecBAD_PROOF,
6079 });
6080
6081 // Supply invariant: both public and confidential balances must be unchanged
6082 // after the rejected attack.
6083 BEAST_EXPECT(mptAlice.getBalance(bob) == bobPublicBefore);
6084 auto const bobSpendingAfter =
6085 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
6086 BEAST_EXPECT(bobSpendingBefore == bobSpendingAfter);
6087 }
6088
6089 // Confidential sends carry encrypted amounts and a zero-knowledge proof.
6090 // Both are built from elliptic-curve math, so every coordinate in the
6091 // transaction must be a real point on the secp256k1 curve. These three
6092 // variants confirm the validator rejects garbage coordinates at the right
6093 // stage before any expensive cryptographic verification runs.
6094 void
6096 {
6097 testcase("Send: off-curve EC points");
6098 using namespace test::jtx;
6099
6100 // Variant A: garbage coordinate in ciphertext / commitment fields
6101 // getBadCiphertext() looks structurally valid (correct length, right
6102 // prefix byte 0x02) but its x-coordinate is 0xFF...FF, which does not
6103 // lie on secp256k1. Preflight must reject before any ledger access.
6104 {
6105 Account const alice("alice"), bob("bob"), carol("carol");
6106 Env env{*this, features};
6107 ConfidentialEnv confEnv{
6108 env,
6109 alice,
6110 {{.account = bob, .payAmount = 100, .convertAmount = 60},
6111 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
6112 auto& mptAlice = confEnv.mpt;
6113
6114 // sender's encrypted amount has an invalid coordinate
6115 mptAlice.send({
6116 .account = bob,
6117 .dest = carol,
6118 .amt = 10,
6119 .proof = getTrivialSendProofHex(),
6120 .senderEncryptedAmt = getBadCiphertext(),
6121 .amountCommitment = getTrivialCommitment(),
6122 .balanceCommitment = getTrivialCommitment(),
6123 .err = temBAD_CIPHERTEXT,
6124 });
6125
6126 // recipient's encrypted amount has an invalid coordinate
6127 mptAlice.send({
6128 .account = bob,
6129 .dest = carol,
6130 .amt = 10,
6131 .proof = getTrivialSendProofHex(),
6132 .destEncryptedAmt = getBadCiphertext(),
6133 .amountCommitment = getTrivialCommitment(),
6134 .balanceCommitment = getTrivialCommitment(),
6135 .err = temBAD_CIPHERTEXT,
6136 });
6137
6138 // issuer's encrypted amount has an invalid coordinate
6139 mptAlice.send({
6140 .account = bob,
6141 .dest = carol,
6142 .amt = 10,
6143 .proof = getTrivialSendProofHex(),
6144 .issuerEncryptedAmt = getBadCiphertext(),
6145 .amountCommitment = getTrivialCommitment(),
6146 .balanceCommitment = getTrivialCommitment(),
6147 .err = temBAD_CIPHERTEXT,
6148 });
6149
6150 // The amount and balance commitments are single curve coordinates
6151 // used to tie the proof to the transfer amount and sender balance.
6152 // A commitment with a valid-looking prefix but an impossible
6153 // x-coordinate must also be rejected.
6154 Buffer badCommitment(kEcPedersenCommitmentLength);
6155 std::memset(badCommitment.data(), 0xFF, kEcPedersenCommitmentLength);
6156 badCommitment.data()[0] = kEcCompressedPrefixEvenY;
6157
6158 mptAlice.send({
6159 .account = bob,
6160 .dest = carol,
6161 .amt = 10,
6162 .proof = getTrivialSendProofHex(),
6163 .amountCommitment = badCommitment,
6164 .balanceCommitment = getTrivialCommitment(),
6165 .err = temMALFORMED,
6166 });
6167
6168 mptAlice.send({
6169 .account = bob,
6170 .dest = carol,
6171 .amt = 10,
6172 .proof = getTrivialSendProofHex(),
6173 .amountCommitment = getTrivialCommitment(),
6174 .balanceCommitment = badCommitment,
6175 .err = temMALFORMED,
6176 });
6177 }
6178
6179 // Variant B: garbage coordinates inside the ZKP proof blob
6180 // The proof blob has the right total byte length (so it passes the
6181 // length check at preflight), but every embedded coordinate is
6182 // 0xFF...FF — impossible on secp256k1. The proof verifier must detect
6183 // this and return tecBAD_PROOF without crashing.
6184 {
6185 Account const alice("alice"), bob("bob"), carol("carol");
6186 Env env{*this, features};
6187 ConfidentialEnv confEnv{
6188 env,
6189 alice,
6190 {{.account = bob, .payAmount = 100, .convertAmount = 60},
6191 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
6192 auto& mptAlice = confEnv.mpt;
6193
6194 Buffer badProof(kEcSendProofLength);
6195 std::memset(badProof.data(), 0xFF, kEcSendProofLength);
6196 badProof.data()[0] = kEcCompressedPrefixEvenY;
6197
6198 mptAlice.send({
6199 .account = bob,
6200 .dest = carol,
6201 .amt = 10,
6202 .proof = strHex(badProof),
6203 .err = tecBAD_PROOF,
6204 });
6205 }
6206
6207 // Variant C: only one of the two ciphertext coordinates is bad
6208 // Each encrypted amount is two coordinates back-to-back: C1 then C2.
6209 // Both must be valid. These tests corrupt only one at a time to
6210 // confirm both are checked independently.
6211 {
6212 Account const alice("alice"), bob("bob"), carol("carol");
6213 Env env{*this, features};
6214 ConfidentialEnv confEnv{
6215 env,
6216 alice,
6217 {{.account = bob, .payAmount = 100, .convertAmount = 60},
6218 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
6219 auto& mptAlice = confEnv.mpt;
6220
6221 // getTrivialCiphertext() has both C1 and C2 as valid (but trivial)
6222 // curve coordinates. We replace one half at a time with 0xFF...FF.
6223 auto const& tc = getTrivialCiphertext();
6224
6225 // C1 = bad (0xFF...FF), C2 = valid trivial point
6227 std::memset(badC1goodC2.data(), 0xFF, kEcGamalEncryptedTotalLength);
6228 badC1goodC2.data()[0] = kEcCompressedPrefixEvenY;
6230 badC1goodC2.data() + kEcCiphertextComponentLength,
6231 tc.data() + kEcCiphertextComponentLength,
6233
6234 // C1 = valid trivial point, C2 = bad (0xFF...FF)
6236 std::memset(goodC1badC2.data(), 0xFF, kEcGamalEncryptedTotalLength);
6237 std::memcpy(goodC1badC2.data(), tc.data(), kEcCiphertextComponentLength);
6239
6240 // sender's encrypted amount — bad C1
6241 mptAlice.send({
6242 .account = bob,
6243 .dest = carol,
6244 .amt = 10,
6245 .proof = getTrivialSendProofHex(),
6246 .senderEncryptedAmt = badC1goodC2,
6247 .amountCommitment = getTrivialCommitment(),
6248 .balanceCommitment = getTrivialCommitment(),
6249 .err = temBAD_CIPHERTEXT,
6250 });
6251
6252 // sender's encrypted amount — bad C2
6253 mptAlice.send({
6254 .account = bob,
6255 .dest = carol,
6256 .amt = 10,
6257 .proof = getTrivialSendProofHex(),
6258 .senderEncryptedAmt = goodC1badC2,
6259 .amountCommitment = getTrivialCommitment(),
6260 .balanceCommitment = getTrivialCommitment(),
6261 .err = temBAD_CIPHERTEXT,
6262 });
6263
6264 // recipient's encrypted amount — bad C1
6265 mptAlice.send({
6266 .account = bob,
6267 .dest = carol,
6268 .amt = 10,
6269 .proof = getTrivialSendProofHex(),
6270 .destEncryptedAmt = badC1goodC2,
6271 .amountCommitment = getTrivialCommitment(),
6272 .balanceCommitment = getTrivialCommitment(),
6273 .err = temBAD_CIPHERTEXT,
6274 });
6275
6276 // recipient's encrypted amount — bad C2
6277 mptAlice.send({
6278 .account = bob,
6279 .dest = carol,
6280 .amt = 10,
6281 .proof = getTrivialSendProofHex(),
6282 .destEncryptedAmt = goodC1badC2,
6283 .amountCommitment = getTrivialCommitment(),
6284 .balanceCommitment = getTrivialCommitment(),
6285 .err = temBAD_CIPHERTEXT,
6286 });
6287 }
6288 }
6289
6290 // Reject points from the wrong elliptic curve (wrong-group injection).
6291 //
6292 // An attacker might submit coordinates that come from a completely
6293 // different elliptic curve, for example, the one used in TLS
6294 // certificates (NIST P-256). If those coordinates happen to also be
6295 // valid points on secp256k1 (which is possible since both curves use
6296 // 256-bit fields), the format check at preflight will pass. However,
6297 // the zero-knowledge proof is built specifically for secp256k1: the
6298 // math inside the proof only holds for the right curve, so any
6299 // transaction carrying cross-curve data will still be rejected at
6300 // proof verification (tecBAD_PROOF).
6301 void
6303 {
6304 testcase("Send: wrong-group point injection rejected");
6305 using namespace test::jtx;
6306
6307 Env env{*this, features};
6308 Account const alice("alice"), bob("bob"), carol("carol");
6309 ConfidentialEnv confEnv{
6310 env,
6311 alice,
6312 {{.account = bob, .payAmount = 100, .convertAmount = 60},
6313 {.account = carol, .payAmount = 50, .convertAmount = 30}}};
6314 auto& mptAlice = confEnv.mpt;
6315
6316 // The x-coordinate of the NIST P-256 generator point — a real,
6317 // well-known value from a different elliptic curve (used in TLS
6318 // and certificates). This x-coordinate is also a valid secp256k1
6319 // point, so it passes preflight. Rejection happens at proof
6320 // verification because the ZKP is secp256k1-specific.
6321 //
6322 // P-256 generator x:
6323 // 6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
6324 static constexpr std::uint8_t kP256GeneratorX[32] = {
6325 0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6,
6326 0xE5, 0x63, 0xA4, 0x40, 0xF2, 0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB,
6327 0x33, 0xA0, 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
6328 };
6329
6330 // A 66-byte encrypted amount using the P-256 x-coordinate for both halves.
6332 wrongGroupCt.data()[0] = kEcCompressedPrefixEvenY;
6333 std::memcpy(wrongGroupCt.data() + 1, kP256GeneratorX, 32);
6335 std::memcpy(wrongGroupCt.data() + kEcCiphertextComponentLength + 1, kP256GeneratorX, 32);
6336
6337 // A 33-byte commitment using the same wrong-curve x-coordinate.
6338 Buffer wrongGroupCommitment(kEcPedersenCommitmentLength);
6339 wrongGroupCommitment.data()[0] = kEcCompressedPrefixEvenY;
6340 std::memcpy(wrongGroupCommitment.data() + 1, kP256GeneratorX, 32);
6341
6342 // sender's encrypted amount uses a coordinate from the wrong curve
6343 mptAlice.send({
6344 .account = bob,
6345 .dest = carol,
6346 .amt = 10,
6347 .proof = getTrivialSendProofHex(),
6348 .senderEncryptedAmt = wrongGroupCt,
6349 .amountCommitment = getTrivialCommitment(),
6350 .balanceCommitment = getTrivialCommitment(),
6351 .err = tecBAD_PROOF,
6352 });
6353
6354 // recipient's encrypted amount uses a coordinate from the wrong curve
6355 mptAlice.send({
6356 .account = bob,
6357 .dest = carol,
6358 .amt = 10,
6359 .proof = getTrivialSendProofHex(),
6360 .destEncryptedAmt = wrongGroupCt,
6361 .amountCommitment = getTrivialCommitment(),
6362 .balanceCommitment = getTrivialCommitment(),
6363 .err = tecBAD_PROOF,
6364 });
6365
6366 // issuer's encrypted amount uses a coordinate from the wrong curve
6367 mptAlice.send({
6368 .account = bob,
6369 .dest = carol,
6370 .amt = 10,
6371 .proof = getTrivialSendProofHex(),
6372 .issuerEncryptedAmt = wrongGroupCt,
6373 .amountCommitment = getTrivialCommitment(),
6374 .balanceCommitment = getTrivialCommitment(),
6375 .err = tecBAD_PROOF,
6376 });
6377
6378 // amount commitment uses a coordinate from the wrong curve
6379 mptAlice.send({
6380 .account = bob,
6381 .dest = carol,
6382 .amt = 10,
6383 .proof = getTrivialSendProofHex(),
6384 .amountCommitment = wrongGroupCommitment,
6385 .balanceCommitment = getTrivialCommitment(),
6386 .err = tecBAD_PROOF,
6387 });
6388
6389 // balance commitment uses a coordinate from the wrong curve
6390 mptAlice.send({
6391 .account = bob,
6392 .dest = carol,
6393 .amt = 10,
6394 .proof = getTrivialSendProofHex(),
6395 .amountCommitment = getTrivialCommitment(),
6396 .balanceCommitment = wrongGroupCommitment,
6397 .err = tecBAD_PROOF,
6398 });
6399 }
6400
6401 // Reject an all-zero "null" public key.
6402 //
6403 // Every account in a confidential transfer needs a real public key —
6404 // a specific point on the secp256k1 curve derived from a secret number
6405 // only that account knows. An all-zero key (33 bytes of 0x00) is not
6406 // a real key. It has no secret behind it, and encrypting data to it
6407 // would not actually hide anything. The validator must reject it at
6408 // preflight so no account can ever register a broken key.
6409 void
6411 {
6412 testcase("Convert: all-zero public key rejected");
6413 using namespace test::jtx;
6414
6415 // 33 zero bytes — not a real public key; no valid secret maps to this.
6416 Buffer const nullKey = gMakeZeroBuffer(kEcPubKeyLength);
6417
6418 // Recipient (holder) tries to register an all-zero key.
6419 // Must be rejected so no account ends up with an unprotected balance.
6420 {
6421 Env env{*this, features};
6422 Account const alice("alice"), bob("bob"), carol("carol");
6423 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
6424 mptAlice.create({
6425 .ownerCount = 1,
6426 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
6427 });
6428 mptAlice.authorize({.account = bob});
6429 mptAlice.authorize({.account = carol});
6430 mptAlice.pay(alice, bob, 100);
6431 mptAlice.pay(alice, carol, 50);
6432 mptAlice.generateKeyPair(alice);
6433 mptAlice.generateKeyPair(bob);
6434 mptAlice.generateKeyPair(carol);
6435 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
6436
6437 // recipient (carol) tries to register an all-zero key
6438 mptAlice.convert({
6439 .account = carol,
6440 .amt = 10,
6441 .holderPubKey = nullKey,
6442 .err = temMALFORMED,
6443 });
6444
6445 // sender (bob) tries to register an all-zero key
6446 mptAlice.convert({
6447 .account = bob,
6448 .amt = 10,
6449 .holderPubKey = nullKey,
6450 .err = temMALFORMED,
6451 });
6452 }
6453
6454 // Issuer tries to register an all-zero key.
6455 // The issuer's key is used to encrypt the issuer's copy of every
6456 // transfer amount.
6457 {
6458 Env env{*this, features};
6459 Account const alice("alice"), bob("bob");
6460 MPTTester mptAlice(env, alice, {.holders = {bob}});
6461 mptAlice.create({
6462 .ownerCount = 1,
6463 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
6464 });
6465 mptAlice.authorize({.account = bob});
6466 mptAlice.pay(alice, bob, 100);
6467 mptAlice.generateKeyPair(alice);
6468 mptAlice.generateKeyPair(bob);
6469
6470 mptAlice.set({
6471 .account = alice,
6472 .issuerPubKey = nullKey,
6473 .err = temMALFORMED,
6474 });
6475 }
6476 }
6477
6478 /* This test ensures that when sending confidential tokens, the encrypted
6479 * amounts are securely locked to the correct accounts' official public keys.
6480 *
6481 * Attack scenario — Encrypting the issuer's copy with the wrong key:
6482 * A sender correctly encrypts the hidden transfer amount for themselves
6483 * and the receiver. However, they intentionally encrypt the issuer's
6484 * copy of the data using the wrong public key (for example, using the
6485 * receiver's key instead of the official issuer's key). */
6486 void
6488 {
6489 testcase("Send: issuer ciphertext encrypted under wrong public key");
6490 using namespace test::jtx;
6491
6492 Env env{*this, features};
6493 Account const alice("alice"), bob("bob"), carol("carol");
6494 ConfidentialEnv confEnv{
6495 env,
6496 alice,
6497 {{.account = bob, .payAmount = 100, .convertAmount = 100},
6498 {.account = carol, .payAmount = 50, .convertAmount = 50}}};
6499 auto& mptAlice = confEnv.mpt;
6500
6501 auto const bobSpendingBefore =
6502 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
6503
6504 // issuer ciphertext encrypted under carol's holder key
6505 // (should be under alice's registered issuer key).
6506 {
6507 Buffer const bf = generateBlindingFactor();
6508 Buffer const wrongIssuerCt = mptAlice.encryptAmount(carol, 10, bf);
6509
6510 mptAlice.send({
6511 .account = bob,
6512 .dest = carol,
6513 .amt = 10,
6514 .issuerEncryptedAmt = wrongIssuerCt,
6515 .err = tecBAD_PROOF,
6516 });
6517 }
6518
6519 // issuer ciphertext encrypted under bob's holder key
6520 // (the sender's own key — still not the registered issuer key).
6521 {
6522 Buffer const bf = generateBlindingFactor();
6523 Buffer const wrongIssuerCt = mptAlice.encryptAmount(bob, 10, bf);
6524
6525 mptAlice.send({
6526 .account = bob,
6527 .dest = carol,
6528 .amt = 10,
6529 .issuerEncryptedAmt = wrongIssuerCt,
6530 .err = tecBAD_PROOF,
6531 });
6532 }
6533
6534 // all balances unchanged
6535 BEAST_EXPECT(
6536 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
6537 bobSpendingBefore);
6538 BEAST_EXPECT(mptAlice.getDecryptedBalance(carol, MPTTester::holderEncryptedInbox) == 0);
6539 }
6540
6541 // This test verifies that the compact AND-composed Send sigma proof
6542 // enforces the shared-randomness invariant across participants.
6543 void
6545 {
6546 testcase("divergent C1 across participants in ConfidentialMPTSend");
6547 using namespace test::jtx;
6548
6549 Env env{*this, features};
6550 Account const alice("alice");
6551 Account const bob("bob");
6552 Account const carol("carol");
6553 Account const auditor("auditor");
6554 ConfidentialEnv confEnv{
6555 env,
6556 alice,
6557 {{.account = bob, .payAmount = 100, .convertAmount = 50},
6558 {.account = carol, .payAmount = 50, .convertAmount = 50}},
6559 tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
6560 auditor};
6561 auto& mptAlice = confEnv.mpt;
6562
6563 // Send amount is 10.
6564 uint64_t const amt = 10;
6565
6566 enum class Participant { Sender, Dest, Issuer, Auditor };
6567
6568 // This lambda submits a send transaction where one of the four ciphertexts
6569 // is encrypted with different randomness than the one used to build the proof.
6570 // Note: When divergent is nullopt, all participants
6571 // will use the same randomness and expected to succeed, this is the
6572 // control case that confirms the test setup itself is sound, the bad proof
6573 // is actually from divergent randomness, not other causes.
6574 auto submitWithDivergentC1 = [&](std::optional<Participant> divergent) {
6575 ConfidentialSendSetup setup(mptAlice, bob, carol, alice, amt, std::cref(auditor));
6576
6577 auto const proofOpt =
6578 requireOptional(setup.generateProof(mptAlice, env, bob, carol), "Missing proof");
6579
6580 // Re-encrypt one participant's ciphertext with divergent randomness.
6581 Buffer senderCt = setup.senderAmt;
6582 Buffer destCt = setup.destAmt;
6583 Buffer issuerCt = setup.issuerAmt;
6584 Buffer auditorCt =
6585 requireOptionalRef(setup.auditorAmt, "Missing auditor encrypted amount");
6586 if (divergent)
6587 {
6588 Buffer const bfDivergent = generateBlindingFactor();
6589 switch (*divergent)
6590 {
6591 case Participant::Sender:
6592 senderCt = mptAlice.encryptAmount(bob, amt, bfDivergent);
6593 break;
6594 case Participant::Dest:
6595 destCt = mptAlice.encryptAmount(carol, amt, bfDivergent);
6596 break;
6597 case Participant::Issuer:
6598 issuerCt = mptAlice.encryptAmount(alice, amt, bfDivergent);
6599 break;
6600 case Participant::Auditor:
6601 auditorCt = mptAlice.encryptAmount(auditor, amt, bfDivergent);
6602 break;
6603 }
6604 }
6605
6606 TER const expectedErr = divergent ? TER{tecBAD_PROOF} : TER{tesSUCCESS};
6607
6608 mptAlice.send({
6609 .account = bob,
6610 .dest = carol,
6611 .amt = amt,
6612 .proof = strHex(proofOpt),
6613 .senderEncryptedAmt = senderCt,
6614 .destEncryptedAmt = destCt,
6615 .issuerEncryptedAmt = issuerCt,
6616 .auditorEncryptedAmt = auditorCt,
6617 .blindingFactor = setup.blindingFactor,
6618 .amountCommitment = setup.amountCommitment,
6619 .balanceCommitment = setup.balanceCommitment,
6620 .err = expectedErr,
6621 });
6622
6623 // Verify balances.
6624 auto const spendingAfter =
6625 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
6626 if (divergent)
6627 {
6628 BEAST_EXPECT(spendingAfter == setup.prevSpending);
6629 }
6630 else
6631 {
6632 BEAST_EXPECT(spendingAfter == setup.prevSpending - amt);
6633 }
6634 };
6635
6636 // This confirms the test setup is sound, if any of the divergent cases below
6637 // fail, it is due to the C1 mismatch and not a setup bug.
6638 submitWithDivergentC1(std::nullopt);
6639
6640 // Divergent C1 for different participants should all fail with tecBAD_PROOF:
6641 submitWithDivergentC1(Participant::Sender);
6642 submitWithDivergentC1(Participant::Dest);
6643 submitWithDivergentC1(Participant::Issuer);
6644 submitWithDivergentC1(Participant::Auditor);
6645 }
6646
6647 void
6649 {
6650 testcase("test confidential transactions fee");
6651 using namespace test::jtx;
6652
6653 auto setup =
6654 [&](MPTTester& mpt, Account const& alice, Account const& bob, Account const& carol) {
6655 mpt.create({
6656 .ownerCount = 1,
6657 .flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer |
6658 tfMPTCanClawback,
6659 });
6660 mpt.authorize({.account = bob});
6661 mpt.authorize({.account = carol});
6662 mpt.pay(alice, bob, 100);
6663 mpt.pay(alice, carol, 50);
6664 mpt.generateKeyPair(alice);
6665 mpt.generateKeyPair(bob);
6666 mpt.generateKeyPair(carol);
6667 mpt.set({.account = alice, .issuerPubKey = mpt.getPubKey(alice)});
6668 };
6669
6670 // test expected base fee for confidential transactions
6671 {
6672 Env env{*this, features};
6673 Account const alice("alice"), bob("bob"), carol("carol");
6674 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
6675 setup(mptAlice, alice, bob, carol);
6676
6677 auto const baseFee = env.current()->fees().base;
6678 auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1);
6679
6680 // lambda function to submit confidential transaction and check fee charged to the
6681 // account
6682 auto checkFee = [&](Account const& acct, auto&& submitFn) {
6683 auto const before = env.balance(acct);
6684 submitFn();
6685 auto const after = env.balance(acct);
6686 BEAST_EXPECT(before - after == expectedFee);
6687 };
6688
6689 checkFee(bob, [&]() {
6690 mptAlice.convert(
6691 {.account = bob,
6692 .amt = 50,
6693 .holderPubKey = mptAlice.getPubKey(bob),
6694 .fee = expectedFee});
6695 });
6696 checkFee(carol, [&]() {
6697 mptAlice.convert(
6698 {.account = carol,
6699 .amt = 10,
6700 .holderPubKey = mptAlice.getPubKey(carol),
6701 .fee = expectedFee});
6702 });
6703 checkFee(bob, [&]() { mptAlice.mergeInbox({.account = bob, .fee = expectedFee}); });
6704 checkFee(carol, [&]() { mptAlice.mergeInbox({.account = carol, .fee = expectedFee}); });
6705 checkFee(bob, [&]() {
6706 mptAlice.send({.account = bob, .dest = carol, .amt = 5, .fee = expectedFee});
6707 });
6708 checkFee(bob, [&]() {
6709 mptAlice.convertBack({.account = bob, .amt = 5, .fee = expectedFee});
6710 });
6711 checkFee(alice, [&]() {
6712 mptAlice.confidentialClaw(
6713 {.account = alice, .holder = carol, .amt = 15, .fee = expectedFee});
6714 });
6715 }
6716
6717 // test insufficient fee for confidential transactions
6718 {
6719 Env env{*this, features};
6720 Account const alice("alice"), bob("bob"), carol("carol");
6721 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
6722 setup(mptAlice, alice, bob, carol);
6723 auto const baseFee = env.current()->fees().base;
6724 auto const expectedFee = baseFee * (kConfidentialFeeMultiplier + 1);
6725
6726 mptAlice.convert(
6727 {.account = bob,
6728 .amt = 1,
6729 .holderPubKey = mptAlice.getPubKey(bob),
6730 .fee = expectedFee - 1,
6731 .err = telINSUF_FEE_P});
6732 mptAlice.mergeInbox({.account = bob, .fee = baseFee, .err = telINSUF_FEE_P});
6733 mptAlice.send(
6734 {.account = bob,
6735 .dest = carol,
6736 .amt = 1,
6737 .fee = baseFee * kConfidentialFeeMultiplier,
6738 .err = telINSUF_FEE_P});
6739 mptAlice.convertBack({.account = bob, .amt = 1, .fee = baseFee, .err = telINSUF_FEE_P});
6740 mptAlice.confidentialClaw(
6741 {.account = alice,
6742 .holder = carol,
6743 .amt = 1,
6744 .fee = baseFee,
6745 .err = telINSUF_FEE_P});
6746 }
6747
6748 // test excessive fee for confidential transactions
6749 {
6750 Env env{*this, features};
6751 Account const alice("alice"), bob("bob"), carol("carol");
6752 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
6753 setup(mptAlice, alice, bob, carol);
6754
6755 auto const baseFee = env.current()->fees().base;
6756 auto const highFee = baseFee * (kConfidentialFeeMultiplier + 1) * 2;
6757 auto const bobBefore = env.balance(bob);
6758 mptAlice.convert(
6759 {.account = bob,
6760 .amt = 1,
6761 .holderPubKey = mptAlice.getPubKey(bob),
6762 .fee = highFee});
6763 BEAST_EXPECT(env.balance(bob) == bobBefore - highFee);
6764 }
6765 }
6766
6767 void
6769 {
6770 testcase("Send: forged equality proof");
6771
6772 // Test that modifying a ciphertext after proof generation causes
6773 // verification to fail. The Fiat-Shamir challenge binds ciphertexts
6774 // to the proof, so any modification invalidates the proof.
6775
6776 using namespace test::jtx;
6777 Env env{*this, features};
6778 Account const alice("alice"), bob("bob"), carol("carol");
6779 ConfidentialEnv confEnv{
6780 env,
6781 alice,
6782 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
6783 auto& mptAlice = confEnv.mpt;
6784
6785 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
6786
6787 // Forge destination ciphertext (Enc(20) instead of Enc(10))
6788 {
6789 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
6790 if (!BEAST_EXPECT(proof.has_value()))
6791 return;
6792
6793 Buffer const forgedBlindingFactor = generateBlindingFactor();
6794 auto const forgedDestAmt = mptAlice.encryptAmount(carol, 20, forgedBlindingFactor);
6795
6796 auto args = setup.sendArgs(
6797 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
6798 args.destEncryptedAmt = forgedDestAmt;
6799 mptAlice.send(args);
6800 }
6801
6802 // Forge sender's ciphertext (Enc(5) instead of Enc(10))
6803 {
6804 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
6805 if (!BEAST_EXPECT(proof.has_value()))
6806 return;
6807
6808 Buffer const forgedBlindingFactor = generateBlindingFactor();
6809 auto const forgedSenderAmt = mptAlice.encryptAmount(bob, 5, forgedBlindingFactor);
6810
6811 auto args = setup.sendArgs(
6812 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
6813 args.senderEncryptedAmt = forgedSenderAmt;
6814 mptAlice.send(args);
6815 }
6816
6817 // Forge issuer's ciphertext (Enc(100) instead of Enc(10))
6818 {
6819 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
6820 if (!BEAST_EXPECT(proof.has_value()))
6821 return;
6822
6823 Buffer const forgedBlindingFactor = generateBlindingFactor();
6824 auto const forgedIssuerAmt = mptAlice.encryptAmount(alice, 100, forgedBlindingFactor);
6825
6826 auto args = setup.sendArgs(
6827 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
6828 args.issuerEncryptedAmt = forgedIssuerAmt;
6829 mptAlice.send(args);
6830 }
6831 }
6832
6833 void
6835 {
6836 testcase("Send: forged range proof");
6837
6838 // Attack: send uint64_max tokens using Enc(uint64_max) ciphertexts
6839 // and a corrupted bulletproof. Verifier rejects due to inner-product
6840 // mismatch and Fiat-Shamir transcript divergence. Supply invariant
6841 // is preserved.
6842
6843 using namespace test::jtx;
6844 Env env{*this, features};
6845 Account const alice("alice"), bob("bob"), carol("carol");
6846 ConfidentialEnv confEnv{
6847 env,
6848 alice,
6849 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
6850 auto& mptAlice = confEnv.mpt;
6851
6852 uint64_t const badAmount = std::numeric_limits<uint64_t>::max();
6853 Buffer const blindingFactor = generateBlindingFactor();
6854
6855 // Construct Enc(uint64_max) ciphertexts and commitment.
6856 auto const senderAmt = mptAlice.encryptAmount(bob, badAmount, blindingFactor);
6857 auto const destAmt = mptAlice.encryptAmount(carol, badAmount, blindingFactor);
6858 auto const issuerAmt = mptAlice.encryptAmount(alice, badAmount, blindingFactor);
6859 auto const amountCommitment = mptAlice.getPedersenCommitment(badAmount, blindingFactor);
6860
6861 // Balance commitment for Bob's actual balance.
6862 auto const prevSpending = requireOptional(
6863 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
6864 "Missing previous spending balance");
6865 auto const balanceBlindingFactor = generateBlindingFactor();
6866 auto const balanceCommitment =
6867 mptAlice.getPedersenCommitment(prevSpending, balanceBlindingFactor);
6868
6869 // Generate a valid proof for a legitimate amount, then corrupt
6870 // the bulletproof segment to simulate a forged range proof.
6871 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
6872 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
6873 if (!BEAST_EXPECT(validProof.has_value()))
6874 return;
6875
6876 // Corrupt bulletproof bytes.
6877 Buffer forgedProof = requireOptional(validProof, "Missing valid proof");
6878 for (size_t i = kBulletproofOffset; i < forgedProof.size(); i += 7)
6879 forgedProof.data()[i] ^= 0xFF;
6880
6881 // Submit — rejected due to commitment mismatch.
6882 mptAlice.send(
6883 {.account = bob,
6884 .dest = carol,
6885 .amt = badAmount,
6886 .proof = strHex(forgedProof),
6887 .senderEncryptedAmt = senderAmt,
6888 .destEncryptedAmt = destAmt,
6889 .issuerEncryptedAmt = issuerAmt,
6890 .amountCommitment = amountCommitment,
6891 .balanceCommitment = balanceCommitment,
6892 .err = tecBAD_PROOF});
6893
6894 // Supply invariant: Bob's balance unchanged.
6895 auto const postSpending = requireOptional(
6896 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
6897 "Missing post spending balance");
6898 BEAST_EXPECT(postSpending == prevSpending);
6899 }
6900
6901 void
6903 {
6904 testcase("Send: negative value malleability");
6905
6906 // Attack: forge a bulletproof claiming remaining = (uint64_t)(-10).
6907 // Bob has 10 tokens, sends 10. Honest remaining is 0, but the
6908 // forged proof claims 0xFFFFFFFFFFFFFFF6. Rejected because
6909 // PC(0) != PC(0xFFFFFFFFFFFFFFF6).
6910
6911 using namespace test::jtx;
6912 // Bob converts exactly 10 tokens, leaving honest remaining = 0.
6913 Env env{*this, features};
6914 Account const alice("alice"), bob("bob"), carol("carol");
6915 ConfidentialEnv confEnv{
6916 env,
6917 alice,
6918 {{.account = bob, .payAmount = 1000, .convertAmount = 10},
6919 {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
6920 auto& mptAlice = confEnv.mpt;
6921
6922 uint64_t const sendAmount = 10;
6923 uint64_t const negativeRemaining = static_cast<uint64_t>(-10); // 0xFFFFFFFFFFFFFFF6
6924
6925 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
6926
6927 auto const ctxHash = getSendContextHash(
6928 bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version);
6929
6930 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
6931 if (!BEAST_EXPECT(validProof.has_value()))
6932 return;
6933
6934 // Forge bulletproof for {10, 0xFFFFFFFFFFFFFFF6} and splice it in.
6935 auto const forgedBulletproof = getForgedBulletproof(
6936 {sendAmount, negativeRemaining},
6938 ctxHash);
6939
6940 Buffer forgedProof(requireOptionalRef(validProof, "Missing valid proof").size());
6942 forgedProof.data(),
6943 requireOptionalRef(validProof, "Missing valid proof").data(),
6946 forgedProof.data() + kBulletproofOffset,
6947 forgedBulletproof.data(),
6949
6950 mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
6951
6952 // Supply invariant: Bob's balance unchanged.
6953 auto const postSpending = requireOptional(
6954 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
6955 "Missing post spending balance");
6956 BEAST_EXPECT(postSpending == setup.prevSpending);
6957 }
6958
6959 void
6961 {
6962 testcase("Send proof context binding");
6963 using namespace test::jtx;
6964
6965 auto runBadProof = [&](auto makeContextHash) {
6966 Env env{*this, features};
6967 Account const alice("alice");
6968 Account const bob("bob");
6969 Account const carol("carol");
6970 ConfidentialEnv confEnv{
6971 env,
6972 alice,
6973 {{.account = bob, .payAmount = 100, .convertAmount = 40}, {.account = carol}}};
6974 auto& mptAlice = confEnv.mpt;
6975
6976 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
6977
6978 auto const proof = mptAlice.getConfidentialSendProof(
6979 bob,
6980 setup.sendAmount,
6981 setup.recipients,
6982 setup.blindingFactor,
6983 makeContextHash(env, mptAlice, alice, bob, carol, setup.version),
6984 {
6985 .pedersenCommitment = setup.amountCommitment,
6986 .amt = setup.sendAmount,
6987 .encryptedAmt = setup.senderAmt,
6988 .blindingFactor = setup.amountBlindingFactor,
6989 },
6990 {
6991 .pedersenCommitment = setup.balanceCommitment,
6992 .amt = setup.prevSpending,
6993 .encryptedAmt = setup.prevEncryptedSpending,
6994 .blindingFactor = setup.balanceBlindingFactor,
6995 });
6996 if (!BEAST_EXPECT(proof.has_value()))
6997 return;
6998
6999 mptAlice.send(setup.sendArgs(
7000 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
7001 };
7002
7003 // Wrong sender account in the proof context.
7004 runBadProof([&](Env& env,
7005 MPTTester const& mpt,
7006 Account const&,
7007 Account const& bob,
7008 Account const& carol,
7009 std::uint32_t version) {
7010 return getSendContextHash(
7011 carol.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version);
7012 });
7013
7014 // Wrong issuance ID in the proof context.
7015 runBadProof([&](Env& env,
7016 MPTTester const&,
7017 Account const& alice,
7018 Account const& bob,
7019 Account const& carol,
7020 std::uint32_t version) {
7021 return getSendContextHash(
7022 bob.id(),
7023 makeMptID(env.seq(alice) + 100, alice),
7024 env.seq(bob),
7025 carol.id(),
7026 version);
7027 });
7028
7029 // Wrong transaction sequence in the proof context.
7030 runBadProof([&](Env& env,
7031 MPTTester const& mpt,
7032 Account const&,
7033 Account const& bob,
7034 Account const& carol,
7035 std::uint32_t version) {
7036 return getSendContextHash(
7037 bob.id(), mpt.issuanceID(), env.seq(bob) + 1, carol.id(), version);
7038 });
7039
7040 // Wrong destination in the proof context.
7041 runBadProof([&](Env& env,
7042 MPTTester const& mpt,
7043 Account const&,
7044 Account const& bob,
7045 Account const&,
7046 std::uint32_t version) {
7047 return getSendContextHash(bob.id(), mpt.issuanceID(), env.seq(bob), bob.id(), version);
7048 });
7049
7050 // Wrong balance version in the proof context.
7051 runBadProof([&](Env& env,
7052 MPTTester const& mpt,
7053 Account const&,
7054 Account const& bob,
7055 Account const& carol,
7056 std::uint32_t version) {
7057 return getSendContextHash(
7058 bob.id(), mpt.issuanceID(), env.seq(bob), carol.id(), version + 1);
7059 });
7060 }
7061
7062 void
7064 {
7065 testcase("Send: Fiat-Shamir Binding");
7066
7067 using namespace test::jtx;
7068 Env env{*this, features};
7069 Account const alice("alice"), bob("bob"), carol("carol");
7070 ConfidentialEnv confEnv{
7071 env,
7072 alice,
7073 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
7074 auto& mptAlice = confEnv.mpt;
7075
7076 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
7077
7078 // Variant A: forged amount commitment.
7079 {
7080 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7081 if (!BEAST_EXPECT(proof.has_value()))
7082 return;
7083
7084 auto const forgedBlindingFactor = generateBlindingFactor();
7085 auto const forgedCommitment =
7086 mptAlice.getPedersenCommitment(setup.sendAmount + 5, forgedBlindingFactor);
7087
7088 auto args = setup.sendArgs(
7089 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
7090 args.amountCommitment = forgedCommitment;
7091 mptAlice.send(args);
7092 }
7093
7094 // Variant B: proof replay at a different sequence.
7095 {
7096 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7097 if (!BEAST_EXPECT(proof.has_value()))
7098 return;
7099
7100 mptAlice.pay(bob, carol, 1);
7101 env.close();
7102
7103 mptAlice.send(setup.sendArgs(
7104 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
7105 }
7106
7107 // Variant C: tampered response scalars.
7108 {
7109 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7110 if (!BEAST_EXPECT(proof.has_value()))
7111 return;
7112
7113 auto const& proofRef = requireOptionalRef(proof, "Missing proof");
7114 Buffer tamperedProof(proofRef.size());
7115 std::memcpy(tamperedProof.data(), proofRef.data(), proofRef.size());
7116 size_t const tamperOffset = tamperedProof.size() / 2;
7117 tamperedProof.data()[tamperOffset] ^= 0xFF;
7118
7119 mptAlice.send(setup.sendArgs(bob, carol, tamperedProof, tecBAD_PROOF));
7120 }
7121 }
7122
7123 void
7125 {
7126 testcase("Send: Proof Component Reuse");
7127
7128 using namespace test::jtx;
7129 Env env{*this, features};
7130 Account const alice("alice"), bob("bob"), carol("carol"), dan("dan");
7131 ConfidentialEnv confEnv{
7132 env,
7133 alice,
7134 {{.account = bob},
7135 {.account = carol, .payAmount = 1000, .convertAmount = 50},
7136 {.account = dan, .payAmount = 1000, .convertAmount = 50}}};
7137 auto& mptAlice = confEnv.mpt;
7138
7139 uint64_t const sendAmount = 10;
7140
7141 // Variant A: replay proof to same destination after sequence changes.
7142 {
7143 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7144
7145 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7146 if (!BEAST_EXPECT(proof.has_value()))
7147 return;
7148
7149 mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof")));
7150 mptAlice.mergeInbox({.account = carol});
7151
7152 mptAlice.send(setup.sendArgs(
7153 bob, carol, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF));
7154 }
7155
7156 // Variant B: replay proof to a different destination.
7157 {
7158 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7159
7160 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7161 if (!BEAST_EXPECT(proof.has_value()))
7162 return;
7163
7164 mptAlice.send(setup.sendArgs(bob, carol, requireOptionalRef(proof, "Missing proof")));
7165 mptAlice.mergeInbox({.account = carol});
7166
7167 auto const destAmtDan = mptAlice.encryptAmount(dan, sendAmount, setup.blindingFactor);
7168 auto const issuerAmtDan =
7169 mptAlice.encryptAmount(alice, sendAmount, setup.blindingFactor);
7170
7171 auto args =
7172 setup.sendArgs(bob, dan, requireOptionalRef(proof, "Missing proof"), tecBAD_PROOF);
7173 args.destEncryptedAmt = destAmtDan;
7174 args.issuerEncryptedAmt = issuerAmtDan;
7175 mptAlice.send(args);
7176 }
7177 }
7178
7179 void
7181 {
7182 testcase("Send: special witness values");
7183
7184 using namespace test::jtx;
7185 Env env{*this, features};
7186 Account const alice("alice"), bob("bob"), carol("carol");
7187 ConfidentialEnv confEnv{
7188 env,
7189 alice,
7190 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}}};
7191 auto& mptAlice = confEnv.mpt;
7192
7193 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, 10);
7194
7195 // Variant A: zero-valued response scalars.
7196 {
7197 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7198 if (!BEAST_EXPECT(proof.has_value()))
7199 return;
7200
7201 Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
7202
7203 static constexpr size_t kSigmaScalarSize = 32;
7204 static constexpr size_t kChallengeOffset = 0;
7205 static constexpr size_t kResponseOffset = kChallengeOffset + kSigmaScalarSize;
7206 static constexpr size_t kResponseSize = 5 * kSigmaScalarSize; // z_m..z_sk
7207 std::memset(forgedProof.data() + kResponseOffset, 0, kResponseSize);
7208
7209 mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
7210 }
7211
7212 // Variant B: identity element in ciphertext.
7213 {
7214 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7215 if (!BEAST_EXPECT(proof.has_value()))
7216 return;
7217
7218 Buffer invalidCiphertext(kEcGamalEncryptedTotalLength);
7219 std::memset(invalidCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
7220
7221 auto args = setup.sendArgs(
7222 bob, carol, requireOptionalRef(proof, "Missing proof"), temBAD_CIPHERTEXT);
7223 args.senderEncryptedAmt = invalidCiphertext;
7224 mptAlice.send(args);
7225 }
7226
7227 // Variant B2: identity element in commitment.
7228 {
7229 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7230 if (!BEAST_EXPECT(proof.has_value()))
7231 return;
7232
7233 Buffer invalidCommitment(kEcPedersenCommitmentLength);
7234 std::memset(invalidCommitment.data(), 0, kEcPedersenCommitmentLength);
7235
7236 auto args = setup.sendArgs(
7237 bob, carol, requireOptionalRef(proof, "Missing proof"), temMALFORMED);
7238 args.amountCommitment = invalidCommitment;
7239 mptAlice.send(args);
7240 }
7241
7242 // Variant C: boundary scalar (curve order).
7243 {
7244 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7245 if (!BEAST_EXPECT(proof.has_value()))
7246 return;
7247
7248 Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
7249
7250 static constexpr unsigned char kCurveOrder[32] = {
7251 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
7252 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, //
7253 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, //
7254 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41 //
7255 };
7256
7257 std::memcpy(forgedProof.data() + 32, kCurveOrder, 32);
7258
7259 mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
7260 }
7261
7262 // Variant C2: overflow scalar (curve order + 1).
7263 {
7264 auto const proof = setup.generateProof(mptAlice, env, bob, carol);
7265 if (!BEAST_EXPECT(proof.has_value()))
7266 return;
7267
7268 Buffer forgedProof = requireOptionalRef(proof, "Missing proof");
7269
7270 static constexpr unsigned char kOverflowScalar[32] = {
7271 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
7272 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, //
7273 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, //
7274 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x42 //
7275 };
7276
7277 std::memcpy(forgedProof.data() + 32, kOverflowScalar, 32);
7278
7279 mptAlice.send(setup.sendArgs(bob, carol, forgedProof, tecBAD_PROOF));
7280 }
7281 }
7282
7283 void
7285 {
7286 testcase("Send: cross-statement proof substitution");
7287
7288 // This test verifies that proofs generated for one protocol component
7289 // cannot be used in place of another, and that proofs bound to
7290 // different public parameters are rejected.
7291
7292 using namespace test::jtx;
7293 Env env{*this, features};
7294 Account const alice("alice"), bob("bob"), carol("carol");
7295 ConfidentialEnv confEnv{
7296 env,
7297 alice,
7298 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
7299 tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer | tfMPTCanClawback};
7300 auto& mptAlice = confEnv.mpt;
7301
7302 uint64_t const sendAmount = 10;
7303
7304 // Variant A: Swap proof type (cross-statement substitution)
7305 // -----------------------------------------------------------------
7306 // Attack: Generate a valid convertBack proof (compact sigma +
7307 // single bulletproof) and attempt to use it as the ZK proof in a
7308 // ConfidentialMPTSend transaction.
7309 //
7310 // Expected: The send proof has a different structure
7311 // (equality + 2×pedersen + double bulletproof). Even if sized to
7312 // match, the domain-separated Fiat-Shamir transcript differs,
7313 // so verification equations fail.
7314 {
7315 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7316
7317 // Generate a valid convertBack proof for bob
7318 auto const spendingBalance = requireOptional(
7319 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending),
7320 "Missing spending balance");
7321 auto const encryptedSpending = requireOptional(
7322 mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending),
7323 "Missing encrypted spending balance");
7324
7325 Buffer const pcBlindingFactor = generateBlindingFactor();
7326 Buffer const pedersenCommitment =
7327 mptAlice.getPedersenCommitment(spendingBalance, pcBlindingFactor);
7328
7329 auto const version = mptAlice.getMPTokenVersion(bob);
7330 uint256 const convertBackCtxHash =
7331 getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version);
7332
7333 Buffer const convertBackProof = mptAlice.getConvertBackProof(
7334 bob,
7335 sendAmount,
7336 convertBackCtxHash,
7337 {
7338 .pedersenCommitment = pedersenCommitment,
7339 .amt = spendingBalance,
7340 .encryptedAmt = encryptedSpending,
7341 .blindingFactor = pcBlindingFactor,
7342 });
7343
7344 // Resize the convertBack proof to match the expected send proof
7345 // size so it passes preflight's size check and reaches the actual
7346 // ZK verification in doApply.
7347 auto const expectedSendSize = kEcSendProofLength;
7348 Buffer resizedProof(expectedSendSize);
7349 auto const copyLen = std::min(convertBackProof.size(), expectedSendSize);
7350 std::memcpy(resizedProof.data(), convertBackProof.data(), copyLen);
7351 // Zero-pad the rest (if convertBack proof is shorter)
7352 if (copyLen < expectedSendSize)
7353 std::memset(resizedProof.data() + copyLen, 0, expectedSendSize - copyLen);
7354
7355 mptAlice.send(setup.sendArgs(bob, carol, resizedProof, tecBAD_PROOF));
7356 }
7357
7358 // Variant B: Valid proof bound to wrong public parameters
7359 // -----------------------------------------------------------------
7360 // Attack: Generate a valid send proof using a wrong context hash
7361 // (computed with a different issuanceID). The proof is
7362 // mathematically valid for the wrong statement, but when the
7363 // verifier recomputes the Fiat-Shamir challenge using the correct
7364 // issuanceID, the challenge differs and verification fails.
7365 {
7366 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7367
7368 // Compute context hash with a fabricated (wrong) issuanceID
7369 uint192 const fakeIssuanceID{1};
7370 auto const wrongCtxHash = getSendContextHash(
7371 bob.id(), fakeIssuanceID, env.seq(bob), carol.id(), setup.version);
7372
7373 // Generate a proof that is valid for the wrong issuanceID
7374 auto const wrongProof = mptAlice.getConfidentialSendProof(
7375 bob,
7376 sendAmount,
7377 setup.recipients,
7378 setup.blindingFactor,
7379 wrongCtxHash,
7380 {
7381 .pedersenCommitment = setup.amountCommitment,
7382 .amt = sendAmount,
7383 .encryptedAmt = setup.senderAmt,
7384 .blindingFactor = setup.amountBlindingFactor,
7385 },
7386 {
7387 .pedersenCommitment = setup.balanceCommitment,
7388 .amt = setup.prevSpending,
7389 .encryptedAmt = setup.prevEncryptedSpending,
7390 .blindingFactor = setup.balanceBlindingFactor,
7391 });
7392
7393 if (!BEAST_EXPECT(wrongProof.has_value()))
7394 return;
7395
7396 // Submit with the correct issuanceID — verifier recomputes
7397 // the challenge using the real issuanceID, which differs from
7398 // the one baked into the proof.
7399 mptAlice.send(setup.sendArgs(
7400 bob, carol, requireOptionalRef(wrongProof, "Missing wrong proof"), tecBAD_PROOF));
7401 }
7402 }
7403
7404 void
7406 {
7407 testcase("Send: ciphertext malleability");
7408
7409 // Attack: replace ElGamal ciphertext Enc(m) with Enc(2m) to inflate
7410 // the amount credited to the recipient. ElGamal is homomorphic, so
7411 // scalar multiplication (C1, C2) → (k*C1, k*C2) decrypts to k*m.
7412
7413 using namespace test::jtx;
7414 Env env{*this, features};
7415 Account const alice("alice"), bob("bob"), carol("carol");
7416 ConfidentialEnv confEnv{
7417 env,
7418 alice,
7419 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
7420 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
7421 auto& mptAlice = confEnv.mpt;
7422
7423 uint64_t const sendAmount = 10;
7424
7425 // Variant A: Post-signature tampering.
7426 // Build a valid signed transaction, then replace the destination
7427 // ciphertext with Enc(2m) in the serialized blob. The original
7428 // signature no longer covers the modified data.
7429 {
7430 auto const seq = env.seq(bob);
7431 auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
7432 auto jtx = env.jt(jv);
7433 BEAST_EXPECT(jtx.stx);
7434
7435 // Serialize signed tx, deserialize into mutable STObject
7436 Serializer s;
7437 jtx.stx->add(s);
7438 SerialIter sit(s.slice());
7439 STObject obj(sit, sfTransaction);
7440
7441 // Replace dest ciphertext with Enc(2m) — a valid EC point
7442 // encrypting an inflated amount under carol's key
7443 Buffer const bf = generateBlindingFactor();
7444 auto const inflatedCiphertext = mptAlice.encryptAmount(carol, sendAmount * 2, bf);
7445 obj.setFieldVL(sfDestinationEncryptedAmount, inflatedCiphertext);
7446
7447 // Re-serialize with the original (now-stale) signature
7448 Serializer tampered;
7449 obj.add(tampered);
7450
7451 // Signature verification fails — rejected before ZKP check
7452 auto const jr = env.rpc("submit", strHex(tampered.slice()));
7453 BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
7454 }
7455
7456 // Variant B: Re-signed with inflated ciphertext.
7457 // Generate a valid proof for amount m, then replace the destination
7458 // ciphertext with Enc(2m) and re-sign. Signature passes, but the
7459 // compact sigma proof fails: the proof binds Enc(m) to the Pedersen
7460 // commitment PC(m, r), so substituting Enc(2m) breaks the linkage.
7461 {
7462 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7463
7464 auto const ctxHash = getSendContextHash(
7465 bob.id(), mptAlice.issuanceID(), env.seq(bob), carol.id(), setup.version);
7466
7467 auto const validProof = mptAlice.getConfidentialSendProof(
7468 bob,
7469 sendAmount,
7470 setup.recipients,
7471 setup.blindingFactor,
7472 ctxHash,
7473 {
7474 .pedersenCommitment = setup.amountCommitment,
7475 .amt = sendAmount,
7476 .encryptedAmt = setup.senderAmt,
7477 .blindingFactor = setup.amountBlindingFactor,
7478 },
7479 {
7480 .pedersenCommitment = setup.balanceCommitment,
7481 .amt = setup.prevSpending,
7482 .encryptedAmt = setup.prevEncryptedSpending,
7483 .blindingFactor = setup.balanceBlindingFactor,
7484 });
7485
7486 if (!BEAST_EXPECT(validProof.has_value()))
7487 return;
7488
7489 // Replace dest ciphertext with Enc(2m) using the same blinding
7490 // factor — even with matching randomness the proof rejects
7491 // because the committed plaintext differs
7492 auto const inflatedDestAmt =
7493 mptAlice.encryptAmount(carol, sendAmount * 2, setup.blindingFactor);
7494
7495 auto args = setup.sendArgs(
7496 bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
7497 args.destEncryptedAmt = inflatedDestAmt;
7498 mptAlice.send(args);
7499 }
7500 }
7501
7502 void
7504 {
7505 testcase("Send: ciphertext negation");
7506
7507 // Attack: negate ciphertext -Enc(m) = (-C1, -C2) to reverse the
7508 // transaction direction. Negation decrypts to the group-level
7509 // additive inverse of m*G, effectively turning a credit into a debit.
7510
7511 using namespace test::jtx;
7512 Env env{*this, features};
7513 Account const alice("alice"), bob("bob"), carol("carol");
7514 ConfidentialEnv confEnv{
7515 env,
7516 alice,
7517 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
7518 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
7519 auto& mptAlice = confEnv.mpt;
7520
7521 uint64_t const sendAmount = 10;
7522
7523 // Negate an ElGamal ciphertext by flipping the y-coordinate parity
7524 // of both compressed EC points. For secp256k1 compressed form,
7525 // prefix 0x02 means even-y and 0x03 means odd-y; negation
7526 // swaps them: -P has the same x but opposite y.
7527 auto negateCiphertext = [](Buffer const& ct) -> Buffer {
7528 Buffer neg = ct;
7529 neg.data()[0] ^= 0x01; // negate C1
7530 neg.data()[kEcCiphertextComponentLength] ^= 0x01; // negate C2
7531 return neg;
7532 };
7533
7534 // Variant A: Post-signature negation.
7535 // Negate the destination ciphertext in the signed blob.
7536 // Signature no longer covers the modified field.
7537 {
7538 auto const seq = env.seq(bob);
7539 auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
7540 auto jtx = env.jt(jv);
7541 BEAST_EXPECT(jtx.stx);
7542
7543 Serializer s;
7544 jtx.stx->add(s);
7545
7546 SerialIter sit(s.slice());
7547 STObject obj(sit, sfTransaction);
7548
7549 auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount);
7550 Buffer const origBuf(origDestAmt.data(), origDestAmt.size());
7551 auto const negDestAmt = negateCiphertext(origBuf);
7552 obj.setFieldVL(
7553 sfDestinationEncryptedAmount, Slice(negDestAmt.data(), negDestAmt.size()));
7554
7555 Serializer tampered;
7556 obj.add(tampered);
7557
7558 auto const jr = env.rpc("submit", strHex(tampered.slice()));
7559 BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
7560 }
7561
7562 // Variant B: Re-signed with all negated ciphertexts.
7563 // Signature passes, but the compact sigma proof fails — the proof
7564 // was generated for Enc(m), not Enc(-m).
7565 {
7566 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7567
7568 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
7569 if (!BEAST_EXPECT(validProof.has_value()))
7570 return;
7571
7572 // Negate all three ciphertexts: Enc(m) -> Enc(-m)
7573 auto const negSenderAmt = negateCiphertext(setup.senderAmt);
7574 auto const negDestAmt = negateCiphertext(setup.destAmt);
7575 auto const negIssuerAmt = negateCiphertext(setup.issuerAmt);
7576
7577 auto args = setup.sendArgs(
7578 bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
7579 args.senderEncryptedAmt = negSenderAmt;
7580 args.destEncryptedAmt = negDestAmt;
7581 args.issuerEncryptedAmt = negIssuerAmt;
7582 mptAlice.send(args);
7583 }
7584
7585 // Variant C: Negate only the sender ciphertext.
7586 // The verifier uses the sender ciphertext to derive the remainder
7587 // commitment: Enc(b) - Enc(m) becomes Enc(b) - (-Enc(m)) = Enc(b+m).
7588 // The bulletproof was generated for (b - m), not (b + m), so the
7589 // aggregated range proof fails.
7590 {
7591 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7592
7593 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
7594 if (!BEAST_EXPECT(validProof.has_value()))
7595 return;
7596
7597 auto const negSenderAmt = negateCiphertext(setup.senderAmt);
7598
7599 auto args = setup.sendArgs(
7600 bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
7601 args.senderEncryptedAmt = negSenderAmt;
7602 mptAlice.send(args);
7603 }
7604 }
7605
7606 void
7608 {
7609 testcase("Send: ciphertext combination");
7610
7611 // Attack: exploit ElGamal homomorphism to combine ciphertexts
7612 // Enc(m1) + Enc(m2) = Enc(m1+m2), inflating the credited amount
7613 // without knowing the private keys.
7614
7615 using namespace test::jtx;
7616 Env env{*this, features};
7617 Account const alice("alice"), bob("bob"), carol("carol");
7618 ConfidentialEnv confEnv{
7619 env,
7620 alice,
7621 {{.account = bob, .payAmount = 1000, .convertAmount = 200},
7622 {.account = carol, .payAmount = 1000, .convertAmount = 100}},
7623 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
7624 auto& mptAlice = confEnv.mpt;
7625
7626 uint64_t const m1 = 10;
7627 uint64_t const m2 = 5;
7628
7629 // Variant A: Post-signature combination.
7630 // Add Enc(m2) to the signed destination ciphertext Enc(m1).
7631 // The original signature doesn't cover the combined ciphertext.
7632 {
7633 auto const seq = env.seq(bob);
7634 auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = m1}, seq);
7635 auto jtx = env.jt(jv);
7636 BEAST_EXPECT(jtx.stx);
7637
7638 Serializer s;
7639 jtx.stx->add(s);
7640
7641 SerialIter sit(s.slice());
7642 STObject obj(sit, sfTransaction);
7643
7644 auto const origDestCt = obj.getFieldVL(sfDestinationEncryptedAmount);
7645
7646 // Homomorphically add Enc(m2) to the original Enc(m1)
7647 Buffer const bf2 = generateBlindingFactor();
7648 auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2);
7649 auto const combined = requireOptional(
7651 Slice(origDestCt.data(), origDestCt.size()), Slice(encM2.data(), encM2.size())),
7652 "Missing combined ciphertext");
7653
7654 obj.setFieldVL(sfDestinationEncryptedAmount, combined);
7655
7656 Serializer tampered;
7657 obj.add(tampered);
7658
7659 auto const jr = env.rpc("submit", strHex(tampered.slice()));
7660 BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
7661 }
7662
7663 // Variant B: Re-signed with combined ciphertext.
7664 // Generate a valid proof for m1, then replace dest ciphertext with
7665 // Enc(m1) + Enc(m2). Sigma proof fails because the proof was
7666 // generated for Enc(m1) only — the combined ciphertext has
7667 // different randomness.
7668 {
7669 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, m1);
7670
7671 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
7672 if (!BEAST_EXPECT(validProof.has_value()))
7673 return;
7674
7675 // Homomorphically add Enc(m2) to the valid dest ciphertext
7676 Buffer const bf2 = generateBlindingFactor();
7677 auto const encM2 = mptAlice.encryptAmount(carol, m2, bf2);
7678 auto const combinedDest = homomorphicAdd(setup.destAmt, encM2);
7679 BEAST_EXPECT(combinedDest.has_value());
7680
7681 auto args = setup.sendArgs(
7682 bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
7683 args.destEncryptedAmt = combinedDest;
7684 mptAlice.send(args);
7685 }
7686
7687 // Variant C: Cross-transaction ciphertext reuse.
7688 // Execute a valid send of m1, then build a new send for m2 using
7689 // a combined ciphertext oldEnc(m1) + newEnc(m2) = Enc(m1+m2),
7690 // where oldEnc(m1) is the actual ciphertext from the previous tx.
7691 // The proof was generated for the new transaction's context, but
7692 // the ciphertext includes stale randomness from the old Enc(m1).
7693 {
7694 // Execute a valid send of m1, capturing the actual ciphertext used
7695 ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, m1);
7696 auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol);
7697 if (!BEAST_EXPECT(proof1.has_value()))
7698 return;
7699 mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof")));
7700
7701 ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, m2);
7702
7703 auto const proof2 = setup2.generateProof(mptAlice, env, bob, carol);
7704 if (!BEAST_EXPECT(proof2.has_value()))
7705 return;
7706
7707 // Combine the actual prior-tx Enc(m1) with the new Enc(m2)
7708 auto const crossCombined = homomorphicAdd(setup1.destAmt, setup2.destAmt);
7709 BEAST_EXPECT(crossCombined.has_value());
7710
7711 auto args = setup2.sendArgs(
7712 bob, carol, requireOptionalRef(proof2, "Missing proof"), tecBAD_PROOF);
7713 args.destEncryptedAmt = crossCombined;
7714 mptAlice.send(args);
7715 }
7716 }
7717
7718 void
7720 {
7721 testcase("Send: ciphertext rerandomization");
7722
7723 // Attack: substitute the randomness component C1 of an ElGamal
7724 // ciphertext (C1, C2) while keeping the message component C2
7725 // unchanged. This "rerandomizes" the ciphertext to break
7726 // linkability or forge fresh-looking ciphertexts.
7727 //
7728 // The compact sigma proof binds C1 to the shared randomness used
7729 // across all recipients, so any C1 substitution breaks the proof.
7730
7731 using namespace test::jtx;
7732 Env env{*this, features};
7733 Account const alice("alice"), bob("bob"), carol("carol");
7734 ConfidentialEnv confEnv{
7735 env,
7736 alice,
7737 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
7738 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
7739 auto& mptAlice = confEnv.mpt;
7740
7741 uint64_t const sendAmount = 10;
7742
7743 // Helper: replace C1 in a ciphertext with C1 from another
7744 // ciphertext, keeping C2 unchanged. Returns a rerandomized
7745 // ciphertext (C1', C2).
7746 auto substituteC1 = [](Buffer const& target, Buffer const& source) -> Buffer {
7747 Buffer result = target;
7748 // Copy C1 (the first ciphertext component) from source.
7749 std::memcpy(result.data(), source.data(), kEcCiphertextComponentLength);
7750 return result;
7751 };
7752
7753 // Variant A: Post-signature C1 substitution.
7754 // Replace C1 in the dest ciphertext after signing.
7755 // Signature no longer covers the modified ciphertext.
7756 {
7757 auto const seq = env.seq(bob);
7758 auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
7759 auto jtx = env.jt(jv);
7760 BEAST_EXPECT(jtx.stx);
7761
7762 Serializer s;
7763 jtx.stx->add(s);
7764 SerialIter sit(s.slice());
7765 STObject obj(sit, sfTransaction);
7766
7767 // Generate a random C1' by encrypting a different amount
7768 Buffer const bf2 = generateBlindingFactor();
7769 auto const otherCt = mptAlice.encryptAmount(carol, 99, bf2);
7770
7771 // Replace C1 in the dest ciphertext
7772 auto const origDestAmt = obj.getFieldVL(sfDestinationEncryptedAmount);
7773 Buffer const origBuf(origDestAmt.data(), origDestAmt.size());
7774 auto const rerandomized = substituteC1(origBuf, otherCt);
7775 obj.setFieldVL(
7776 sfDestinationEncryptedAmount, Slice(rerandomized.data(), rerandomized.size()));
7777
7778 Serializer tampered;
7779 obj.add(tampered);
7780
7781 // Signature verification fails
7782 auto const jr = env.rpc("submit", strHex(tampered.slice()));
7783 BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
7784 }
7785
7786 // Variant B: Re-signed C1 substitution.
7787 // Replace C1 in the dest ciphertext with a fresh random point
7788 // and re-sign. Sigma proof fails because the shared-randomness
7789 // binding no longer holds — C1' wasn't generated with the same r
7790 // used in the proof.
7791 {
7792 ConfidentialSendSetup const setup(mptAlice, bob, carol, alice, sendAmount);
7793
7794 auto const validProof = setup.generateProof(mptAlice, env, bob, carol);
7795 if (!BEAST_EXPECT(validProof.has_value()))
7796 return;
7797
7798 // Create a ciphertext with different randomness to get C1'
7799 Buffer const bf2 = generateBlindingFactor();
7800 auto const otherCt = mptAlice.encryptAmount(carol, sendAmount, bf2);
7801
7802 // Replace C1 in dest ciphertext, keep C2
7803 auto const rerandomizedDest = substituteC1(setup.destAmt, otherCt);
7804
7805 auto args = setup.sendArgs(
7806 bob, carol, requireOptionalRef(validProof, "Missing valid proof"), tecBAD_PROOF);
7807 args.destEncryptedAmt = rerandomizedDest;
7808 mptAlice.send(args);
7809 }
7810 }
7811
7812 void
7814 {
7815 testcase("Send: zero randomness ciphertext");
7816
7817 // Setting r = 0 in ElGamal yields C1 = O (identity), C2 = mG —
7818 // a deterministic ciphertext that reveals the plaintext.
7819
7820 using namespace test::jtx;
7821 Env env{*this, features};
7822 Account const alice("alice"), bob("bob"), carol("carol");
7823 ConfidentialEnv confEnv{
7824 env,
7825 alice,
7826 {{.account = bob}, {.account = carol, .payAmount = 1000, .convertAmount = 50}},
7827 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance};
7828 auto& mptAlice = confEnv.mpt;
7829
7830 uint64_t const sendAmount = 10;
7831
7832 // -----------------------------------------------------------------
7833 // Variant A: Post-signature zero-randomness substitution
7834 // -----------------------------------------------------------------
7835 // Construct a valid ConfidentialMPTSend transaction with proper
7836 // ciphertexts and ZKPs, sign it, then replace the sender ciphertext
7837 // with a deterministic form (C1 = 0x00...00, C2 = arbitrary).
7838 // Since the identity element has no valid compressed encoding,
7839 // the modified blob fails deserialization / signature check.
7840 {
7841 auto const seq = env.seq(bob);
7842 auto jv = mptAlice.sendJV({.account = bob, .dest = carol, .amt = sendAmount}, seq);
7843 auto jtx = env.jt(jv);
7844 BEAST_EXPECT(jtx.stx);
7845
7846 // Serialize the signed transaction
7847 Serializer s;
7848 jtx.stx->add(s);
7849 SerialIter sit(s.slice());
7850 STObject obj(sit, sfTransaction);
7851
7852 // Replace sender ciphertext with zero-randomness form:
7853 // C1 = all zeros (identity element — invalid encoding)
7854 // C2 = valid trivial point (simulating mG)
7855 Buffer zeroCiphertext(kEcGamalEncryptedTotalLength);
7856 std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
7857 // C2 half: use a valid point so only C1 is the problem
7858 auto const& tc = getTrivialCiphertext();
7860 zeroCiphertext.data() + kEcCiphertextComponentLength,
7861 tc.data() + kEcCiphertextComponentLength,
7863 obj.setFieldVL(sfSenderEncryptedAmount, zeroCiphertext);
7864
7865 // Re-serialize with the original (now-stale) signature
7866 Serializer tampered;
7867 obj.add(tampered);
7868
7869 // Signature verification fails because ciphertext fields are
7870 // signed — transaction rejected before ZKP verification.
7871 auto const jr = env.rpc("submit", strHex(tampered.slice()));
7872 BEAST_EXPECT(jr[jss::result][jss::error] == "invalidTransaction");
7873 }
7874
7875 // -----------------------------------------------------------------
7876 // Variant B: Re-signed zero-randomness ciphertext
7877 // -----------------------------------------------------------------
7878 // Same zero-randomness ciphertext as Variant A (C1 = 0, C2 = mG),
7879 // but submitted normally via send() which re-signs the transaction.
7880 // Signature verification passes, but preflight's isValidCiphertext
7881 // rejects it: the identity element has no valid compressed encoding
7882 // on secp256k1, so secp256k1_ec_pubkey_parse fails on C1 = 0.
7883 {
7884 // Build zero-randomness ciphertext: C1 = all zeros (identity),
7885 // C2 = valid trivial point (simulating mG)
7886 Buffer zeroCiphertext(kEcGamalEncryptedTotalLength);
7887 std::memset(zeroCiphertext.data(), 0, kEcGamalEncryptedTotalLength);
7888 auto const& tc = getTrivialCiphertext();
7890 zeroCiphertext.data() + kEcCiphertextComponentLength,
7891 tc.data() + kEcCiphertextComponentLength,
7893
7894 mptAlice.send(
7895 {.account = bob,
7896 .dest = carol,
7897 .amt = sendAmount,
7898 .senderEncryptedAmt = zeroCiphertext,
7899 .err = temBAD_CIPHERTEXT});
7900 }
7901
7902 // -----------------------------------------------------------------
7903 // Variant C: Deterministic ciphertext reuse across transactions
7904 // -----------------------------------------------------------------
7905 // Construct two transactions using identical deterministic
7906 // ciphertexts (same fixed blinding factor). Even if a valid
7907 // proof could be generated for one, it cannot be reused because
7908 // the TransactionContextID (which includes account sequence)
7909 // differs between transactions.
7910 {
7911 // First transaction: generate valid proof for sendAmount
7912 ConfidentialSendSetup const setup1(mptAlice, bob, carol, alice, sendAmount);
7913
7914 auto const proof1 = setup1.generateProof(mptAlice, env, bob, carol);
7915 if (!BEAST_EXPECT(proof1.has_value()))
7916 return;
7917
7918 // Submit first transaction successfully
7919 mptAlice.send(setup1.sendArgs(bob, carol, requireOptionalRef(proof1, "Missing proof")));
7920
7921 mptAlice.mergeInbox({.account = carol});
7922
7923 // Second transaction: reuse the same proof from tx1.
7924 // The context hash includes the new account sequence, so the
7925 // proof generated for the old sequence is invalid.
7926 ConfidentialSendSetup const setup2(mptAlice, bob, carol, alice, sendAmount);
7927
7928 mptAlice.send(setup2.sendArgs(
7929 bob, carol, requireOptionalRef(proof1, "Missing proof"), tecBAD_PROOF));
7930 }
7931 }
7932
7933 void
7935 {
7936 testcase("Send: recipient inbox rerandomization prevents merge cancellation");
7937
7938 using namespace test::jtx;
7939
7940 // Derive the deterministic canonical-zero randomness r0 used for
7941 // Bob's first spending balance.
7942 auto getCanonicalZeroBlindingFactor = [](AccountID const& account, MPTID const& mptID) {
7945 std::memcpy(hashInput.data(), "EncZero", 7);
7946 std::memcpy(hashInput.data() + 7, account.data(), account.size());
7947 std::memcpy(hashInput.data() + 27, mptID.data(), mptID.size());
7948
7949 for (;;)
7950 {
7951 unsigned int mdLen = kEcBlindingFactorLength;
7952 if (EVP_Digest(
7953 hashInput.data(),
7954 hashInput.size(),
7955 scalar.data(),
7956 &mdLen,
7957 EVP_sha256(),
7958 nullptr) != 1)
7959 {
7960 Throw<std::runtime_error>("Failed to derive canonical zero blinding factor");
7961 }
7962
7963 if (secp256k1_ec_seckey_verify(mpt_secp256k1_context(), scalar.data()))
7964 return scalar;
7965
7966 std::memcpy(hashInput.data(), scalar.data(), scalar.size());
7967 }
7968 };
7969
7970 // Pick randomness that would cancel Bob's MergeInbox C1 to infinity
7971 // without receiver-side re-randomization.
7972 auto negateScalarSum = [](Buffer const& lhs, Buffer const& rhs) {
7975 secp256k1_mpt_scalar_add(sum.data(), lhs.data(), rhs.data());
7976 secp256k1_mpt_scalar_negate(negated.data(), sum.data());
7977 return negated;
7978 };
7979
7980 // Without an auditor, target Bob's holder inbox. The crafted send
7981 // randomness would make MergeInbox hit the point at infinity unless
7982 // ConfidentialMPTSend re-randomizes the recipient ciphertext.
7983 {
7984 Env env{*this, features};
7985 Account const alice("alice"), bob("bob"), carol("carol");
7986 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
7987
7988 mptAlice.create({
7989 .ownerCount = 1,
7990 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
7991 });
7992
7993 mptAlice.authorize({.account = bob});
7994 mptAlice.authorize({.account = carol});
7995 mptAlice.pay(alice, bob, 100);
7996 mptAlice.pay(alice, carol, 100);
7997
7998 mptAlice.generateKeyPair(alice);
7999 mptAlice.generateKeyPair(bob);
8000 mptAlice.generateKeyPair(carol);
8001 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
8002
8003 mptAlice.convert({
8004 .account = carol,
8005 .amt = 50,
8006 .holderPubKey = mptAlice.getPubKey(carol),
8007 });
8008 mptAlice.mergeInbox({.account = carol});
8009
8010 Buffer const convertBlindingFactor = generateBlindingFactor();
8011 mptAlice.convert({
8012 .account = bob,
8013 .amt = 20,
8014 .holderPubKey = mptAlice.getPubKey(bob),
8015 .blindingFactor = convertBlindingFactor,
8016 });
8017
8018 Buffer const canonicalZeroBlindingFactor =
8019 getCanonicalZeroBlindingFactor(bob.id(), mptAlice.issuanceID());
8020
8021 // Holder inbox cancellation happens later in MergeInbox, when
8022 // Bob's spending Enc(0; r0) is added to inbox Enc(25; -r0).
8023 Buffer const maliciousSendBlindingFactor =
8024 negateScalarSum(canonicalZeroBlindingFactor, convertBlindingFactor);
8025
8026 mptAlice.send({
8027 .account = carol,
8028 .dest = bob,
8029 .amt = 5,
8030 .blindingFactor = maliciousSendBlindingFactor,
8031 });
8032
8033 mptAlice.mergeInbox({.account = bob});
8034
8035 auto const bobSpending =
8036 mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
8037 BEAST_EXPECT(bobSpending && *bobSpending == 25);
8038 }
8039
8040 // With an auditor, verify the destination auditor balance is also
8041 // re-randomized. Auditor balance is updated during send, and this crafted
8042 // randomness would otherwise make that homomorphic sum hit infinity without
8043 // re-randomization.
8044 {
8045 Env env{*this, features};
8046 Account const alice("alice"), bob("bob"), carol("carol"), auditor("auditor");
8047 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
8048
8049 mptAlice.create({
8050 .ownerCount = 1,
8051 .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance,
8052 });
8053
8054 mptAlice.authorize({.account = bob});
8055 mptAlice.authorize({.account = carol});
8056 mptAlice.pay(alice, bob, 100);
8057 mptAlice.pay(alice, carol, 100);
8058
8059 mptAlice.generateKeyPair(alice);
8060 mptAlice.generateKeyPair(bob);
8061 mptAlice.generateKeyPair(carol);
8062 mptAlice.generateKeyPair(auditor);
8063 mptAlice.set({
8064 .account = alice,
8065 .issuerPubKey = mptAlice.getPubKey(alice),
8066 .auditorPubKey = mptAlice.getPubKey(auditor),
8067 });
8068
8069 mptAlice.convert({
8070 .account = carol,
8071 .amt = 50,
8072 .holderPubKey = mptAlice.getPubKey(carol),
8073 });
8074 mptAlice.mergeInbox({.account = carol});
8075
8076 Buffer const convertBlindingFactor = generateBlindingFactor();
8077 mptAlice.convert({
8078 .account = bob,
8079 .amt = 20,
8080 .holderPubKey = mptAlice.getPubKey(bob),
8081 .blindingFactor = convertBlindingFactor,
8082 });
8083
8084 // This would make the homomorphic sum hit infinity.
8085 Buffer const maliciousSendBlindingFactor =
8086 negateScalarSum(gMakeZeroBuffer(kEcBlindingFactorLength), convertBlindingFactor);
8087
8088 mptAlice.send({
8089 .account = carol,
8090 .dest = bob,
8091 .amt = 5,
8092 .blindingFactor = maliciousSendBlindingFactor,
8093 });
8094
8095 auto const bobAuditor =
8096 mptAlice.getDecryptedBalance(bob, MPTTester::auditorEncryptedBalance);
8097 BEAST_EXPECT(bobAuditor && *bobAuditor == 25);
8098 }
8099 }
8100
8101 void
8103 {
8104 // ConfidentialMPTConvert
8105 testConvert(features);
8106 testConvertPreflight(features);
8108 testConvertPreclaim(features);
8109 testConvertWithAuditor(features);
8110
8111 // ConfidentialMPTMergeInbox
8112 testMergeInbox(features);
8113 testMergeInboxPreflight(features);
8114 testMergeInboxPreclaim(features);
8115
8116 testSet(features);
8117 testSetPreflight(features);
8118 testSetPreclaim(features);
8119
8120 // ConfidentialMPTSend
8121 testSend(features);
8122 testSendPreflight(features);
8123 testSendPreclaim(features);
8124 testSendRangeProof(features);
8125
8126 testSendZeroAmount(features);
8127 testSendWithAuditor(features);
8128
8129 // ConfidentialMPTClawback
8130 testClawback(features);
8131 testClawbackPreflight(features);
8132 testClawbackPreclaim(features);
8133 testClawbackProof(features);
8134 testClawbackWithAuditor(features);
8136
8137 testDelete(features);
8138
8139 // ConfidentialMPTConvertBack
8140 testConvertBack(features);
8141 testConvertBackPreflight(features);
8142 testConvertBackPreclaim(features);
8146
8147 // Homomorphic operation tests
8151
8152 // Invalid curve points
8157
8158 // public and private txns
8160
8161 // Replay tests
8162 testMutatePrivacy(features);
8166
8167 // Crafted-proof Tests
8169
8170 // Transaction Fee Tests
8172
8173 // TransferFee (transfer rate) Tests
8174 testTransferFee(features);
8175
8176 // Zero knowledge proof tests
8179 testSendForgedRangeProof(features);
8181 testSendFiatShamirBinding(features);
8185
8186 // Ciphertext malleability tests
8193 }
8194
8195public:
8196 void
8197 run() override
8198 {
8199 using namespace test::jtx;
8200 FeatureBitset const all{testableAmendments()};
8201
8202 testWithFeats(all);
8203 }
8204};
8205
8206BEAST_DEFINE_TESTSUITE(ConfidentialTransfer, app, xrpl);
8207
8208} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
Like std::vector<char> but better.
Definition Buffer.h:16
std::size_t size() const noexcept
Returns the number of bytes in the buffer.
Definition Buffer.h:105
std::uint8_t const * data() const noexcept
Return a pointer to beginning of the storage.
Definition Buffer.h:129
static T requireOptional(std::optional< T > value, char const *message)
static T const & requireOptionalRef(std::optional< T > const &value, char const *message)
static Buffer getForgedBulletproof(std::array< uint64_t, 2 > const &values, std::array< Buffer, 2 > const &blindingFactors, uint256 const &contextHash)
void testConvertBackPreflight(FeatureBitset features)
void testConvertBackBulletproof(FeatureBitset features)
void testClawbackInvalidProofContextBinding(FeatureBitset features)
void testConfidentialMPTBaseFee(FeatureBitset features)
void testSendRangeProof(FeatureBitset features)
void testConvertBack(FeatureBitset features)
void testPublicTransfersAfterClearingConfidentialFlag(FeatureBitset features)
void testTransferFee(FeatureBitset features)
void testConvert(FeatureBitset features)
void testConvertBackPedersenProof(FeatureBitset features)
void testConvertBackHomomorphicCiphertextModification(FeatureBitset features)
void testSendRerandomizesRecipientInboxAgainstMergeCancellation(FeatureBitset features)
void testConvertIdentityElementRejection(FeatureBitset features)
void testSetPreclaim(FeatureBitset features)
void testSetPreflight(FeatureBitset features)
void testMutatePrivacy(FeatureBitset features)
void testSendCiphertextCombination(FeatureBitset features)
void testSendFiatShamirBinding(FeatureBitset features)
void testSendCiphertextRerandomization(FeatureBitset features)
void testSendPreclaim(FeatureBitset features)
void testConvertBackPreclaim(FeatureBitset features)
void testSendInvalidCurvePoints(FeatureBitset features)
void testClawbackPreclaim(FeatureBitset features)
void testSendWrongIssuerPublicKey(FeatureBitset features)
void testConvertBackWithAuditor(FeatureBitset features)
void testSendZeroRandomnessCiphertext(FeatureBitset features)
void testSendCiphertextMalleability(FeatureBitset features)
void testSendPreflight(FeatureBitset features)
void testClawbackPreflight(FeatureBitset features)
void testMergeInboxPreclaim(FeatureBitset features)
void testSendNegativeValueMalleability(FeatureBitset features)
void testMergeInbox(FeatureBitset features)
void testClawbackProof(FeatureBitset features)
void testSendCiphertextNegation(FeatureBitset features)
void testSendForgedRangeProof(FeatureBitset features)
void testSendInvalidProofContextBinding(FeatureBitset features)
void testSendForgedEqualityProof(FeatureBitset features)
void testClawbackWithAuditor(FeatureBitset features)
void testSendCrossStatementProofSubstitution(FeatureBitset features)
void testSendProofComponentReuse(FeatureBitset features)
void testSendWithAuditor(FeatureBitset features)
void testConvertWithAuditor(FeatureBitset features)
void testSendHomomorphicOverflow(FeatureBitset features)
void testConvertBackProofCiphertextBinding(FeatureBitset features)
void testSendSharedRandomnessViolation(FeatureBitset features)
void testConvertPreclaim(FeatureBitset features)
void testConvertBackHomomorphicUnderflow(FeatureBitset features)
void testConvertBackInvalidProofContextBinding(FeatureBitset features)
void testConvertInvalidProofContextBinding(FeatureBitset features)
void testMergeInboxPreflight(FeatureBitset features)
void testConvertBackProofVersionMismatch(FeatureBitset features)
void testSendWrongGroupPointInjection(FeatureBitset features)
void testSendZeroAmount(FeatureBitset features)
void testConvertPreflight(FeatureBitset features)
void testSendSpecialWitnessValues(FeatureBitset features)
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:45
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:167
void rawReplace(SLE::ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:243
Identifies fields.
Definition SField.h:130
Blob getFieldVL(SField const &field) const
Definition STObject.cpp:639
void setFieldVL(SField const &field, Blob const &)
Definition STObject.cpp:781
void add(Serializer &s) const override
Definition STObject.cpp:120
Slice slice() const noexcept
Definition Serializer.h:44
An immutable linear range of bytes.
Definition Slice.h:26
void send(MPTConfidentialSend const &arg=MPTConfidentialSend{})
Definition mpt.cpp:1321
void set(MPTSet const &set={})
Definition mpt.cpp:482
void convertBack(MPTConvertBack const &arg=MPTConvertBack{})
Definition mpt.cpp:2286
void pay(Account const &src, Account const &dest, std::int64_t amount, std::optional< TER > err=std::nullopt, std::optional< std::vector< std::string > > credentials=std::nullopt)
Definition mpt.cpp:668
std::optional< uint64_t > getDecryptedBalance(Account const &account, EncryptedBalanceType balanceType) const
Definition mpt.cpp:2112
std::optional< Buffer > getPrivKey(Account const &account) const
Definition mpt.cpp:2058
void confidentialClaw(MPTConfidentialClawback const &arg=MPTConfidentialClawback{})
Definition mpt.cpp:1925
std::uint32_t getMPTokenVersion(Account const account) const
Definition mpt.cpp:2270
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
T data(T... args)
T max(T... args)
T memcpy(T... args)
T memset(T... args)
T min(T... args)
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr std::size_t kEcPubKeyLength
Length of EC public key (compressed).
Definition Protocol.h:327
@ telINSUF_FEE_P
Definition TER.h:41
constexpr std::uint8_t kEcCompressedPrefixEvenY
Compressed EC point prefix for even y-coordinate.
Definition Protocol.h:367
BaseUInt< 192 > uint192
Definition base_uint.h:563
static auto sum(TCollection const &col)
Definition BookStep.cpp:993
constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance
Definition TxFlags.h:353
constexpr std::size_t kEcBlindingFactorLength
Length of the EC blinding factor in bytes.
Definition Protocol.h:333
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
constexpr std::uint32_t kConfidentialFeeMultiplier
Extra base fee multiplier charged to confidential MPT transactions.
Definition Protocol.h:364
constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance
Definition TxFlags.h:371
constexpr std::size_t kEcClawbackProofLength
Length of the ZKProof for ConfidentialMPTClawback.
Definition Protocol.h:361
constexpr FlagValue tmfMPTCanEnableCanLock
Definition TxFlags.h:345
constexpr std::size_t kEcSchnorrProofLength
Length of Schnorr ZKProof for public key registration (compact form) in bytes.
Definition Protocol.h:336
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:324
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
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 kEcPedersenCommitmentLength
Length of Pedersen Commitment (compressed).
Definition Protocol.h:339
constexpr std::size_t kEcCiphertextComponentLength
Length of one compressed EC point component in an EC ElGamal ciphertext.
Definition Protocol.h:321
std::optional< Buffer > homomorphicSubtract(Slice const &a, Slice const &b)
Homomorphically subtracts two ElGamal ciphertexts.
constexpr std::size_t kEcDoubleBulletproofLength
Length of double bulletproof (range proof for 2 commitments) in bytes.
Definition Protocol.h:345
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:44
uint256 getConvertContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence)
Generates the context hash for ConfidentialMPTConvert transactions.
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:554
uint256 getClawbackContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence, AccountID const &holder)
Generates the context hash for ConfidentialMPTClawback transactions.
constexpr FlagValue tmfMPTCanMutateTransferFee
Definition TxFlags.h:352
Buffer generateBlindingFactor()
Generates a cryptographically secure blinding factor (size=xrpl::kEcBlindingFactorLength).
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
uint256 getSendContextHash(AccountID const &account, uint192 const &issuanceID, std::uint32_t sequence, AccountID const &destination, std::uint32_t version)
Generates the context hash for ConfidentialMPTSend transactions.
constexpr std::size_t kEcSendProofLength
192 bytes compact sigma proof + 754 bytes double bulletproof.
Definition Protocol.h:351
@ temBAD_CIPHERTEXT
Definition TER.h:131
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
@ temBAD_TRANSFER_FEE
Definition TER.h:128
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecLOCKED
Definition TER.h:356
@ tecNO_TARGET
Definition TER.h:302
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecNO_AUTH
Definition TER.h:298
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecBAD_PROOF
Definition TER.h:366
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDST_TAG_NEEDED
Definition TER.h:307
@ tecDUPLICATE
Definition TER.h:313
@ tecHAS_OBLIGATIONS
Definition TER.h:315
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
BaseUInt< 256 > uint256
Definition base_uint.h:562
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
std::optional< Buffer > homomorphicAdd(Slice const &a, Slice const &b)
Homomorphically adds two ElGamal ciphertexts.
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)
Definition Indexes.cpp:172
@ tesSUCCESS
Definition TER.h:240
XRPL_NO_SANITIZE_ADDRESS void Throw(Args &&... args)
Definition contract.h:49
T const_pointer_cast(T... args)
T cref(T... args)
T size(T... args)
std::optional< Buffer > generateProof(test::jtx::MPTTester &mpt, test::jtx::Env &env, test::jtx::Account const &sender, test::jtx::Account const &dest) const
test::jtx::MPTConfidentialSend sendArgs(test::jtx::Account const &sender, test::jtx::Account const &dest, Buffer const &proof, std::optional< TER > err=std::nullopt) const
std::optional< Buffer > amountCommitment
Definition mpt.h:264
std::optional< Buffer > senderEncryptedAmt
Definition mpt.h:256
std::optional< Buffer > destEncryptedAmt
Definition mpt.h:257
std::optional< Buffer > issuerEncryptedAmt
Definition mpt.h:258
T to_string(T... args)