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