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