xrpld
Loading...
Searching...
No Matches
ConfidentialMPTKeyRotation_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/mpt.h>
6
7#include <xrpl/basics/Buffer.h>
8#include <xrpl/basics/strHex.h>
9#include <xrpl/beast/unit_test/suite.h>
10#include <xrpl/beast/utility/Journal.h>
11#include <xrpl/ledger/OpenView.h>
12#include <xrpl/protocol/ConfidentialTransfer.h>
13#include <xrpl/protocol/Feature.h>
14#include <xrpl/protocol/Indexes.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/STLedgerEntry.h>
18#include <xrpl/protocol/TER.h>
19#include <xrpl/protocol/TxFlags.h>
20
21#include <cstdint>
22#include <memory>
23#include <optional>
24#include <utility>
25#include <vector>
26
27namespace xrpl {
28
30{
31 void
33 {
34 testcase("MPTokenIssuanceSet rotate issuer key");
35 using namespace test::jtx;
36
37 Env env{*this, features};
38 Account const alice("alice");
39 Account const bob("bob");
40 MPTTester mptAlice(env, alice, {.holders = {bob}});
41
42 mptAlice.create({
43 .ownerCount = 1,
44 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
45 });
46
47 mptAlice.generateKeyPair(alice);
48 mptAlice.generateKeyPair(bob);
49
50 // First-time registration.
51 mptAlice.set({
52 .account = alice,
53 .issuerPubKey = mptAlice.getPubKey(alice),
54 });
55
56 // Verify that no epochs are set when registering for the first time.
57 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
58
59 // Rotating the issuer key requires the key rotation amendment
60 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
61 mptAlice.set({
62 .account = alice,
63 .issuerPubKey = mptAlice.getPubKey(bob),
64 .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
65 });
66
67 // A rotation replaces the issuer key and bumps its epoch. The auditor
68 // key was never registered, so it and its epoch stay absent.
69 if (rotationEnabled)
70 {
71 BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, std::nullopt));
72 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
73 }
74 else
75 {
76 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
77 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
78 }
79
80 if (rotationEnabled)
81 {
82 // A second rotation increments the epoch again
83 mptAlice.set({
84 .account = alice,
85 .issuerPubKey = mptAlice.getPubKey(alice),
86 });
87
88 BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
89 }
90 }
91
92 void
94 {
95 testcase("MPTokenIssuanceSet rotate both issuer and auditor keys");
96 using namespace test::jtx;
97
98 Env env{*this, features};
99 Account const alice("alice");
100 Account const bob("bob");
101 Account const auditor("auditor");
102 MPTTester mptAlice(env, alice, {.holders = {bob}});
103
104 mptAlice.create({
105 .ownerCount = 1,
106 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
107 });
108
109 mptAlice.generateKeyPair(alice);
110 mptAlice.generateKeyPair(bob);
111 mptAlice.generateKeyPair(auditor);
112
113 // Register both keys together.
114 mptAlice.set({
115 .account = alice,
116 .issuerPubKey = mptAlice.getPubKey(alice),
117 .auditorPubKey = mptAlice.getPubKey(auditor),
118 });
119
120 // Verify that no epochs are set when registering for the first time.
121 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
122
123 // Rotating both keys requires the amendment
124 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
125 mptAlice.set({
126 .account = alice,
127 .issuerPubKey = mptAlice.getPubKey(bob),
128 .auditorPubKey = mptAlice.getPubKey(alice),
129 .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
130 });
131
132 if (rotationEnabled)
133 {
134 BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, alice));
135 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
136 }
137 else
138 {
139 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
140 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
141 }
142
143 if (rotationEnabled)
144 {
145 // Rotating the issuer key to its current value fails.
146 // Current issuer key is bob, duplicate.
147 mptAlice.set({
148 .account = alice,
149 .issuerPubKey = mptAlice.getPubKey(bob),
150 .err = tecDUPLICATE,
151 });
152
153 // Rotating the auditor key to its current value fails.
154 // Current auditor key is alice, duplicate.
155 mptAlice.set({
156 .account = alice,
157 .auditorPubKey = mptAlice.getPubKey(alice),
158 .err = tecDUPLICATE,
159 });
160
161 // The whole transaction fails when one key is unchanged, even if
162 // the other key is rotated to a new value.
163 // Current issuer key is bob, duplicate.
164 mptAlice.set({
165 .account = alice,
166 .issuerPubKey = mptAlice.getPubKey(bob),
167 .auditorPubKey = mptAlice.getPubKey(auditor),
168 .err = tecDUPLICATE,
169 });
170
171 // Current auditor key is alice, duplicate.
172 mptAlice.set({
173 .account = alice,
174 .issuerPubKey = mptAlice.getPubKey(auditor),
175 .auditorPubKey = mptAlice.getPubKey(alice),
176 .err = tecDUPLICATE,
177 });
178
179 // Nothing changed: keys and epochs are untouched
180 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
181
182 // A second rotation increments both epochs again
183 mptAlice.set({
184 .account = alice,
185 .issuerPubKey = mptAlice.getPubKey(alice),
186 .auditorPubKey = mptAlice.getPubKey(auditor),
187 });
188
189 BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 2u));
190 }
191 }
192
193 void
195 {
196 testcase("MPTokenIssuanceSet rotate auditor key only");
197 using namespace test::jtx;
198
199 Env env{*this, features};
200 Account const alice("alice");
201 Account const bob("bob");
202 Account const auditor("auditor");
203 MPTTester mptAlice(env, alice, {.holders = {bob}});
204
205 mptAlice.create({
206 .ownerCount = 1,
207 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
208 });
209
210 mptAlice.generateKeyPair(alice);
211 mptAlice.generateKeyPair(bob);
212 mptAlice.generateKeyPair(auditor);
213
214 // Register both keys together.
215 mptAlice.set({
216 .account = alice,
217 .issuerPubKey = mptAlice.getPubKey(alice),
218 .auditorPubKey = mptAlice.getPubKey(auditor),
219 });
220
221 // A transaction carrying only the auditor key fails preflight
222 // pre-ConfidentialMPTKeyRotation; post-ConfidentialMPTKeyRotation it rotates the auditor
223 // key
224 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
225 mptAlice.set({
226 .account = alice,
227 .auditorPubKey = mptAlice.getPubKey(bob),
228 .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
229 });
230
231 // The issuer key keeps unchanged, and rotating only the auditor key
232 // bumps only its epoch.
233 if (rotationEnabled)
234 {
235 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, bob));
236 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
237 }
238 else
239 {
240 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
241 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
242 }
243
244 if (rotationEnabled)
245 {
246 // A second rotation increments the epoch again
247 mptAlice.set({
248 .account = alice,
249 .auditorPubKey = mptAlice.getPubKey(auditor),
250 });
251
252 // The issuer key epoch is still untouched.
253 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
254 }
255 }
256
257 void
259 {
260 testcase("MPTokenIssuanceSet register auditor key after issuer key");
261 using namespace test::jtx;
262
263 Env env{*this, features};
264 Account const alice("alice");
265 Account const auditor("auditor");
266 MPTTester mptAlice(env, alice);
267
268 mptAlice.create({
269 .ownerCount = 1,
270 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
271 });
272
273 mptAlice.generateKeyPair(alice);
274 mptAlice.generateKeyPair(auditor);
275
276 // Register the issuer key first. We'll register the auditor key in a separate transaction.
277 mptAlice.set({
278 .account = alice,
279 .issuerPubKey = mptAlice.getPubKey(alice),
280 });
281
282 // Register the auditor key separately.
283 // pre-ConfidentialMPTKeyRotation it fails preflight; post-ConfidentialMPTKeyRotation it
284 // succeeds without touching any epoch because it's a first-time registration.
285 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
286 mptAlice.set({
287 .account = alice,
288 .auditorPubKey = mptAlice.getPubKey(auditor),
289 .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
290 });
291
292 BEAST_EXPECT(mptAlice.checkEncryptionKeys(
293 alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
294 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
295 }
296
297 void
299 {
300 testcase("MPTokenIssuanceSet register auditor key later with circulating supply");
301 using namespace test::jtx;
302
303 Env env{*this, features};
304 Account const alice("alice");
305 Account const bob("bob");
306 Account const auditor("auditor");
307 MPTTester mptAlice(env, alice, {.holders = {bob}});
308
309 mptAlice.create({
310 .ownerCount = 1,
311 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
312 });
313
314 mptAlice.authorize({.account = bob});
315 mptAlice.pay(alice, bob, 100);
316
317 mptAlice.generateKeyPair(alice);
318 mptAlice.generateKeyPair(bob);
319 mptAlice.generateKeyPair(auditor);
320
321 mptAlice.set({
322 .account = alice,
323 .issuerPubKey = mptAlice.getPubKey(alice),
324 });
325
326 // Convert some of bob's balance so that COA > 0
327 mptAlice.convert({
328 .account = bob,
329 .amt = 50,
330 .holderPubKey = mptAlice.getPubKey(bob),
331 });
332
333 auto const sleIssuanceBefore = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
334 if (!BEAST_EXPECT(sleIssuanceBefore))
335 return;
336 auto const coaBefore = (*sleIssuanceBefore)[~sfConfidentialOutstandingAmount].value_or(0);
337 BEAST_EXPECT(coaBefore > 0);
338
339 // Registering the auditor key for the first time while confidential
340 // supply is circulating: pre-ConfidentialMPTKeyRotation an auditor-only
341 // transaction fails preflight; post-ConfidentialMPTKeyRotation it
342 // succeeds as a first-time late-registration even COA > 0.
343 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
344 mptAlice.set({
345 .account = alice,
346 .auditorPubKey = mptAlice.getPubKey(auditor),
347 .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
348 });
349
350 auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
351 if (!BEAST_EXPECT(sleIssuance))
352 return;
353 BEAST_EXPECT(mptAlice.checkEncryptionKeys(
354 alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
355 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
356
357 // The circulating supply itself is not affected.
358 BEAST_EXPECT((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBefore);
359 }
360
361 void
363 {
364 testcase("MPTokenIssuanceSet auditor key requires issuer key");
365 using namespace test::jtx;
366
367 Env env{*this, features};
368 Account const alice("alice");
369 Account const auditor("auditor");
370 MPTTester mptAlice(env, alice);
371
372 mptAlice.create({
373 .ownerCount = 1,
374 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
375 });
376
377 mptAlice.generateKeyPair(auditor);
378 // The issuer key was never registered. pre-ConfidentialMPTKeyRotation an auditor-only
379 // transaction fails preflight; post-ConfidentialMPTKeyRotation it passes preflight
380 // but preclaim rejects registering an auditor key on an issuance
381 // without an issuer key.
382 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
383 mptAlice.set({
384 .account = alice,
385 .auditorPubKey = mptAlice.getPubKey(auditor),
386 .err = rotationEnabled ? TER(tecNO_PERMISSION) : TER(temMALFORMED),
387 });
388
389 // The rejected transaction leaves the issuance without either key.
390 BEAST_EXPECT(mptAlice.checkEncryptionKeys(std::nullopt, std::nullopt));
391 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
392 }
393
394 void
396 {
397 testcase("MPTokenIssuanceSet rotate with circulating confidential supply");
398 using namespace test::jtx;
399
400 Env env{*this, features};
401 Account const alice("alice");
402 Account const bob("bob");
403 Account const carol("carol");
404 MPTTester mptAlice(env, alice, {.holders = {bob}});
405
406 mptAlice.create({
407 .ownerCount = 1,
408 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
409 });
410
411 mptAlice.authorize({.account = bob});
412 mptAlice.pay(alice, bob, 100);
413
414 mptAlice.generateKeyPair(alice);
415 mptAlice.generateKeyPair(bob);
416 mptAlice.generateKeyPair(carol);
417
418 mptAlice.set({
419 .account = alice,
420 .issuerPubKey = mptAlice.getPubKey(alice),
421 });
422
423 // Convert some of bob's balance to confidential spending, so that the
424 // issuance has confidential supply. COA > 0.
425 mptAlice.convert({
426 .account = bob,
427 .amt = 50,
428 .holderPubKey = mptAlice.getPubKey(bob),
429 });
430
431 auto const sleIssuanceBeforeRotation =
432 env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
433 if (!BEAST_EXPECT(sleIssuanceBeforeRotation))
434 return;
435 auto const coaBeforeRotation =
436 (*sleIssuanceBeforeRotation)[~sfConfidentialOutstandingAmount].value_or(0);
437 BEAST_EXPECT(coaBeforeRotation > 0);
438
439 // Rotating key requires the
440 // amendment.
441 bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
442 mptAlice.set({
443 .account = alice,
444 .issuerPubKey = mptAlice.getPubKey(carol),
445 .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
446 });
447
448 auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
449 if (!BEAST_EXPECT(sleIssuance))
450 return;
451 if (rotationEnabled)
452 {
453 BEAST_EXPECT(mptAlice.checkEncryptionKeys(carol, std::nullopt));
454 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
455 }
456 else
457 {
458 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
459 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
460 }
461
462 // The confidential outstanding amount is not affected by the rotation
463 BEAST_EXPECT(
464 (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBeforeRotation);
465
466 // Re-enabling confidential balances while supply is circulating is
467 // rejected regardless of the ConfidentialMPTKeyRotation amendment.
468 mptAlice.set({
469 .account = alice,
470 .flags = tfMPTSetCanHoldConfidentialBalance,
471 .err = tecNO_PERMISSION,
472 });
473 }
474
475 void
477 {
478 using namespace test::jtx;
479 if (!features[featureConfidentialMPTKeyRotation])
480 return;
481
482 testcase("MPTokenIssuanceSet key epoch cannot wrap");
483
484 Env env{*this, features};
485 Account const alice("alice");
486 Account const bob("bob");
487 Account const carol("carol");
488 Account const auditor("auditor");
489
490 // Keep the ledger open so that we can write the key epochs directly into it.
491 MPTTester mptAlice(env, alice, {.holders = {bob}, .close = false});
492
493 mptAlice.create({
494 .ownerCount = 1,
495 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
496 });
497
498 mptAlice.generateKeyPair(alice);
499 mptAlice.generateKeyPair(bob);
500 mptAlice.generateKeyPair(carol);
501 mptAlice.generateKeyPair(auditor);
502
503 mptAlice.set({
504 .account = alice,
505 .issuerPubKey = mptAlice.getPubKey(alice),
506 .auditorPubKey = mptAlice.getPubKey(auditor),
507 });
508
509 auto const issuanceKeylet = keylet::mptokenIssuance(mptAlice.issuanceID());
510
511 // Writes the supplied key epochs straight into the open ledger so that
512 // the maximum epoch is reachable without submitting four billion
513 // rotations.
514 auto setEpochs = [&](std::optional<std::uint32_t> const& issuerKeyEpoch,
515 std::optional<std::uint32_t> const& auditorKeyEpoch) {
516 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
517 auto const sle = view.read(issuanceKeylet);
518 if (!sle)
519 return false; // LCOV_EXCL_LINE
520
521 auto replacement = std::make_shared<SLE>(*sle);
522 if (issuerKeyEpoch)
523 (*replacement)[sfIssuerKeyEpoch] = *issuerKeyEpoch;
524 if (auditorKeyEpoch)
525 (*replacement)[sfAuditorKeyEpoch] = *auditorKeyEpoch;
526 view.rawReplace(replacement);
527 return true;
528 });
529 };
530
531 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
532 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
533
534 // Increment the auditor epoch to kMaxKeyEpoch - 1, leaving the issuer epoch absent.
535 setEpochs(std::nullopt, kMaxKeyEpoch - 1);
536 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
537 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch - 1));
538
539 // Rotating the auditor key to kMaxKeyEpoch succeeds.
540 mptAlice.set({
541 .account = alice,
542 .auditorPubKey = mptAlice.getPubKey(carol),
543 });
544
545 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
546 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
547
548 // A further auditor rotation is rejected because the epoch is exhausted.
549 mptAlice.set({
550 .account = alice,
551 .auditorPubKey = mptAlice.getPubKey(bob),
552 .err = tecNO_PERMISSION,
553 });
554
555 // Rotating both keys at once is rejected as a whole because the auditor
556 // epoch is exhausted.
557 mptAlice.set({
558 .account = alice,
559 .issuerPubKey = mptAlice.getPubKey(auditor),
560 .auditorPubKey = mptAlice.getPubKey(bob),
561 .err = tecNO_PERMISSION,
562 });
563
564 // Both rejections leave every key and epoch as it was.
565 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
566 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
567
568 // The issuer key is unaffected by the exhausted auditor epoch.
569 mptAlice.set({
570 .account = alice,
571 .issuerPubKey = mptAlice.getPubKey(bob),
572 });
573
574 BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
575 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, kMaxKeyEpoch));
576
577 // Increment the issuer epoch to kMaxKeyEpoch - 1.
578 setEpochs(kMaxKeyEpoch - 1, std::nullopt);
579 BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
580 BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch - 1, kMaxKeyEpoch));
581
582 // Rotating the issuer key to kMaxKeyEpoch succeeds.
583 mptAlice.set({
584 .account = alice,
585 .issuerPubKey = mptAlice.getPubKey(auditor),
586 });
587
588 BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
589 BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
590
591 // With both epochs exhausted neither key can be rotated again.
592 mptAlice.set({
593 .account = alice,
594 .issuerPubKey = mptAlice.getPubKey(alice),
595 .err = tecNO_PERMISSION,
596 });
597 mptAlice.set({
598 .account = alice,
599 .auditorPubKey = mptAlice.getPubKey(bob),
600 .err = tecNO_PERMISSION,
601 });
602 mptAlice.set({
603 .account = alice,
604 .issuerPubKey = mptAlice.getPubKey(alice),
605 .auditorPubKey = mptAlice.getPubKey(bob),
606 .err = tecNO_PERMISSION,
607 });
608
609 BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
610 BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
611 }
612
613 void
615 {
616 testcase("ConfidentialMPTConvert mirror epoch");
617 using namespace test::jtx;
618
619 Account const alice("alice");
620 Account const bob("bob");
621 Account const carol("carol");
622 Account const auditor("auditor");
623
624 // A first-time convert with no rotation leaves both mirror
625 // epochs absent.
626 {
627 Env env{*this, features};
628 MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
629 setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
630
631 mptAlice.set({
632 .account = alice,
633 .issuerPubKey = mptAlice.getPubKey(alice),
634 .auditorPubKey = mptAlice.getPubKey(auditor),
635 });
636
637 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
638
639 mptAlice.convert({
640 .account = bob,
641 .amt = 50,
642 .holderPubKey = mptAlice.getPubKey(bob),
643 });
644
645 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
646
647 // Both mirrors are current, so converting again is allowed and
648 // leaves the epochs untouched.
649 mptAlice.convert({
650 .account = bob,
651 .amt = 20,
652 });
653
654 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
655 }
656
657 // Every remaining case needs key rotation to be enabled.
658 if (!features[featureConfidentialMPTKeyRotation])
659 return;
660
661 // A first-time convert stamps the mirrors with whatever epochs the
662 // issuance currently sits at. Only issuer key rotated in this case.
663 {
664 Env env{*this, features};
665 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
666 setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
667
668 mptAlice.set({
669 .account = alice,
670 .issuerPubKey = mptAlice.getPubKey(alice),
671 .auditorPubKey = mptAlice.getPubKey(auditor),
672 });
673
674 // Ten rotations, issuance's issuer epoch is 10.
675 for (int i = 0; i < 10; ++i)
676 {
677 mptAlice.generateKeyPair(alice);
678 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
679 }
680
681 BEAST_EXPECT(mptAlice.checkKeyEpochs(10u, std::nullopt));
682 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
683
684 // carol converts for the first time, and her mirrors are stamped with the current
685 // issuer epoch of 10.
686 mptAlice.convert({
687 .account = carol,
688 .amt = 50,
689 .holderPubKey = mptAlice.getPubKey(carol),
690 });
691
692 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 10u, std::nullopt));
693 }
694
695 // A first-time convert stamps the mirrors with whatever epochs the
696 // issuance currently sits at. Both keys rotated in this case.
697 {
698 Env env{*this, features};
699 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
700 setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
701
702 mptAlice.set({
703 .account = alice,
704 .issuerPubKey = mptAlice.getPubKey(alice),
705 .auditorPubKey = mptAlice.getPubKey(auditor),
706 });
707
708 // 100 rotations of both keys, so both epochs are 100.
709 for (int i = 0; i < 100; ++i)
710 {
711 mptAlice.generateKeyPair(alice);
712 mptAlice.generateKeyPair(auditor);
713 mptAlice.set({
714 .account = alice,
715 .issuerPubKey = mptAlice.getPubKey(alice),
716 .auditorPubKey = mptAlice.getPubKey(auditor),
717 });
718 }
719
720 // 5 more rotations of the auditor key alone, so the auditor epoch is 105 now.
721 for (int i = 0; i < 5; ++i)
722 {
723 mptAlice.generateKeyPair(auditor);
724 mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
725 }
726
727 BEAST_EXPECT(mptAlice.checkKeyEpochs(100u, 105u));
728 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
729
730 // carol converts for the first time, and each of her mirrors is stamped with the epoch
731 // of the key it was encrypted under.
732 mptAlice.convert({
733 .account = carol,
734 .amt = 50,
735 .holderPubKey = mptAlice.getPubKey(carol),
736 });
737
738 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 100u, 105u));
739 }
740
741 // An issuer key rotation leaves an existing holder's issuer mirror
742 // behind, converting will be blocked until the holder's mirror is updated to the new epoch.
743 {
744 Env env{*this, features};
745 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
746 setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
747
748 mptAlice.set({
749 .account = alice,
750 .issuerPubKey = mptAlice.getPubKey(alice),
751 .auditorPubKey = mptAlice.getPubKey(auditor),
752 });
753
754 // carol initializes before any rotation, so her mirrors carry no epoch
755 // at all, the state every holder is in before the amendment.
756 mptAlice.convert({
757 .account = carol,
758 .amt = 50,
759 .holderPubKey = mptAlice.getPubKey(carol),
760 });
761
762 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
763
764 // Rotate the issuer key to epoch 1.
765 mptAlice.generateKeyPair(alice);
766 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
767
768 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
769
770 // bob converts for the first time which is allowed when registering the key.
771 mptAlice.convert({
772 .account = bob,
773 .amt = 50,
774 .holderPubKey = mptAlice.getPubKey(bob),
775 });
776
777 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
778
779 // carol's absent epoch reads as 0 which is stale.
780 mptAlice.convert({
781 .account = carol,
782 .amt = 20,
783 .err = tecNO_PERMISSION,
784 });
785
786 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
787
788 // Rotate the issuer key to epoch 2, leaving bob's issuer mirror stale.
789 mptAlice.generateKeyPair(alice);
790 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
791
792 BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
793
794 // This is not the first time convert, and bob's issuer mirror is behind the current
795 // epoch, so the convert is rejected.
796 mptAlice.convert({
797 .account = bob,
798 .amt = 20,
799 .err = tecNO_PERMISSION,
800 });
801
802 // The rejected convert leaves bob's mirrors exactly as they were.
803 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
804
805 // carol still cannot convert.
806 mptAlice.convert({
807 .account = carol,
808 .amt = 20,
809 .err = tecNO_PERMISSION,
810 });
811
812 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
813 }
814
815 // The auditor mirror is checked the same way, so rotating only the
816 // auditor key blocks the convert on its own, with the issuer epoch
817 // untouched.
818 {
819 Env env{*this, features};
820 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
821 setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
822
823 mptAlice.set({
824 .account = alice,
825 .issuerPubKey = mptAlice.getPubKey(alice),
826 .auditorPubKey = mptAlice.getPubKey(auditor),
827 });
828
829 // bob initializes his confidential balance at epoch 0, so both of his
830 // mirrors are current.
831 mptAlice.convert({
832 .account = bob,
833 .amt = 50,
834 .holderPubKey = mptAlice.getPubKey(bob),
835 });
836
837 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
838
839 // Rotate the auditor key only, leaving bob's auditor mirror behind
840 // while his issuer mirror stays current.
841 mptAlice.generateKeyPair(auditor);
842 mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
843
844 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
845 BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
846
847 mptAlice.convert({
848 .account = bob,
849 .amt = 20,
850 .err = tecNO_PERMISSION,
851 });
852
853 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
854
855 // Carol converts for the first time, and her auditor mirror is stamped with the current
856 // auditor epoch of 1.
857 mptAlice.convert({
858 .account = carol,
859 .amt = 50,
860 .holderPubKey = mptAlice.getPubKey(carol),
861 });
862
863 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, 1u));
864 }
865
866 // A late auditor key registration bumps no epoch.
867 // Although both epochs are still zero, the convert is blocked
868 // because auditor mirror is missing.
869 {
870 Env env{*this, features};
871 MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
872 setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
873
874 // Register the issuer key only.
875 mptAlice.set({
876 .account = alice,
877 .issuerPubKey = mptAlice.getPubKey(alice),
878 });
879
880 // The issuance has no auditor yet, so no auditor mirror is created.
881 mptAlice.convert({
882 .account = bob,
883 .amt = 50,
884 .fillAuditorEncryptedAmt = false,
885 .holderPubKey = mptAlice.getPubKey(bob),
886 });
887
888 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
889
890 // Register the auditor key later, which bumps no epoch.
891 mptAlice.set({
892 .account = alice,
893 .auditorPubKey = mptAlice.getPubKey(auditor),
894 });
895
896 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
897
898 // bob's auditor mirror is still missing, so the convert is rejected.
899 mptAlice.convert({
900 .account = bob,
901 .amt = 20,
902 .err = tecNO_PERMISSION,
903 });
904
905 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
906 }
907 }
908
909 void
911 {
912 testcase("ConfidentialMPTSend mirror epoch");
913 using namespace test::jtx;
914
915 Account const alice("alice");
916 Account const bob("bob");
917 Account const carol("carol");
918 Account const auditor("auditor");
919
920 // Two holders that both initialized after a rotation are current, so a
921 // send between them succeeds and leaves both mirrors untouched.
922 {
923 Env env{*this, features};
924 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
925 setupConfidentialIssuance(mptAlice, alice, {bob, carol});
926 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
927
928 // Rotate the issuer key to epoch 1 before anyone holds a confidential
929 // balance.
930 mptAlice.generateKeyPair(alice);
931 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
932
933 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
934
935 for (auto const& holder : {bob, carol})
936 {
937 mptAlice.convert({
938 .account = holder,
939 .amt = 50,
940 .holderPubKey = mptAlice.getPubKey(holder),
941 });
942 mptAlice.mergeInbox({.account = holder});
943 }
944
945 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
946 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt));
947
948 mptAlice.send({.account = bob, .dest = carol, .amt = 10});
949
950 // The epochs are unchanged after send.
951 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
952 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt));
953 }
954
955 // Either the sender or the destination being stale will be rejected.
956 {
957 Env env{*this, features};
958 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
959 setupConfidentialIssuance(mptAlice, alice, {bob, carol});
960 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
961
962 // carol initializes at epoch 0.
963 mptAlice.convert({
964 .account = carol,
965 .amt = 50,
966 .holderPubKey = mptAlice.getPubKey(carol),
967 });
968 mptAlice.mergeInbox({.account = carol});
969
970 // Rotate the issuer key to epoch 1, leaving carol behind.
971 mptAlice.generateKeyPair(alice);
972 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
973
974 // bob initializes after the rotation, so his mirrors are current.
975 mptAlice.convert({
976 .account = bob,
977 .amt = 50,
978 .holderPubKey = mptAlice.getPubKey(bob),
979 });
980 mptAlice.mergeInbox({.account = bob});
981
982 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
983 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
984
985 // This is rejected because the sender is the stale even though the destination is
986 // current.
987 mptAlice.send({
988 .account = carol,
989 .dest = bob,
990 .amt = 10,
991 .err = tecNO_PERMISSION,
992 });
993
994 // This is rejected because the destination is the stale even though the sender is
995 // current.
996 mptAlice.send({
997 .account = bob,
998 .dest = carol,
999 .amt = 10,
1000 .err = tecNO_PERMISSION,
1001 });
1002
1003 // The rejected sends leave both mirrors as they were.
1004 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
1005 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
1006 }
1007
1008 // Auditor mirror is stale, the send will be rejected.
1009 {
1010 Env env{*this, features};
1011 MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
1012 setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
1013 mptAlice.set({
1014 .account = alice,
1015 .issuerPubKey = mptAlice.getPubKey(alice),
1016 .auditorPubKey = mptAlice.getPubKey(auditor),
1017 });
1018
1019 for (auto const& holder : {bob, carol})
1020 {
1021 mptAlice.convert({
1022 .account = holder,
1023 .amt = 50,
1024 .holderPubKey = mptAlice.getPubKey(holder),
1025 });
1026 mptAlice.mergeInbox({.account = holder});
1027 }
1028
1029 // Rotate the auditor key only
1030 mptAlice.generateKeyPair(auditor);
1031 mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
1032
1033 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
1034
1035 mptAlice.send({
1036 .account = bob,
1037 .dest = carol,
1038 .amt = 10,
1039 .err = tecNO_PERMISSION,
1040 });
1041
1042 mptAlice.send({
1043 .account = carol,
1044 .dest = bob,
1045 .amt = 10,
1046 .err = tecNO_PERMISSION,
1047 });
1048
1049 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
1050 BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
1051 }
1052 }
1053
1054 void
1056 {
1057 testcase("ConfidentialMPTConvertBack mirror epoch");
1058 using namespace test::jtx;
1059
1060 Account const alice("alice");
1061 Account const bob("bob");
1062 Account const auditor("auditor");
1063
1064 // A holder who initialized after a rotation is current, so converting
1065 // back is allowed and leaves the epoch it was stamped with alone.
1066 {
1067 Env env{*this, features};
1068 MPTTester mptAlice(env, alice, {.holders = {bob}});
1069 setupConfidentialIssuance(mptAlice, alice, {bob});
1070 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1071
1072 // Rotate the issuer key to epoch 1 before bob holds a confidential
1073 // balance.
1074 mptAlice.generateKeyPair(alice);
1075 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1076
1077 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
1078
1079 mptAlice.convert({
1080 .account = bob,
1081 .amt = 50,
1082 .holderPubKey = mptAlice.getPubKey(bob),
1083 });
1084 mptAlice.mergeInbox({.account = bob});
1085
1086 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
1087
1088 mptAlice.convertBack({.account = bob, .amt = 20});
1089
1090 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
1091 }
1092
1093 // Converting back with stale mirrors is rejected.
1094 {
1095 Env env{*this, features};
1096 MPTTester mptAlice(env, alice, {.holders = {bob}});
1097 setupConfidentialIssuance(mptAlice, alice, {bob});
1098 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1099
1100 // bob initializes at epoch 0.
1101 mptAlice.convert({
1102 .account = bob,
1103 .amt = 50,
1104 .holderPubKey = mptAlice.getPubKey(bob),
1105 });
1106 mptAlice.mergeInbox({.account = bob});
1107
1108 // Converting back is allowed while his mirrors are still current.
1109 mptAlice.convertBack({.account = bob, .amt = 20});
1110
1111 // Rotate the issuer key to epoch 1, leaving bob behind.
1112 mptAlice.generateKeyPair(alice);
1113 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1114
1115 mptAlice.convertBack({
1116 .account = bob,
1117 .amt = 10,
1118 .err = tecNO_PERMISSION,
1119 });
1120
1121 // The rejected convert back leaves bob's mirrors as they were.
1122 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
1123 }
1124
1125 // Converting back with a stale auditor mirror is rejected, even if the issuer mirror is
1126 // current.
1127 {
1128 Env env{*this, features};
1129 MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
1130 setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
1131 mptAlice.set({
1132 .account = alice,
1133 .issuerPubKey = mptAlice.getPubKey(alice),
1134 .auditorPubKey = mptAlice.getPubKey(auditor),
1135 });
1136
1137 mptAlice.convert({
1138 .account = bob,
1139 .amt = 50,
1140 .holderPubKey = mptAlice.getPubKey(bob),
1141 });
1142 mptAlice.mergeInbox({.account = bob});
1143
1144 // Rotate the auditor key only, leaving bob behind on that mirror alone.
1145 mptAlice.generateKeyPair(auditor);
1146 mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
1147
1148 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
1149
1150 mptAlice.convertBack({
1151 .account = bob,
1152 .amt = 10,
1153 .err = tecNO_PERMISSION,
1154 });
1155
1156 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
1157 }
1158 }
1159
1160 void
1162 {
1163 testcase("ConfidentialMPTClawback mirror epoch");
1164 using namespace test::jtx;
1165
1166 Account const alice("alice");
1167 Account const bob("bob");
1168 Account const auditor("auditor");
1169
1170 std::uint32_t const clawbackFlags =
1171 tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback;
1172
1173 // Clawback is not blocked on
1174 // a stale auditor mirror.
1175 {
1176 Env env{*this, features};
1177 MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
1178 setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}, clawbackFlags);
1179 mptAlice.set({
1180 .account = alice,
1181 .issuerPubKey = mptAlice.getPubKey(alice),
1182 .auditorPubKey = mptAlice.getPubKey(auditor),
1183 });
1184
1185 // bob initializes both mirrors at epoch 0.
1186 mptAlice.convert({
1187 .account = bob,
1188 .amt = 50,
1189 .holderPubKey = mptAlice.getPubKey(bob),
1190 });
1191
1192 // Rotate the auditor key twice, leaving bob's auditor mirror behind.
1193 for (int i = 0; i < 2; ++i)
1194 {
1195 mptAlice.generateKeyPair(auditor);
1196 mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
1197 }
1198
1199 BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
1200 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
1201
1202 mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
1203 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, 2u));
1204 }
1205
1206 // A holder who initialized after a rotation is clawed back successfully, and
1207 // the issuer mirror is updated to the current epoch.
1208 {
1209 Env env{*this, features};
1210 MPTTester mptAlice(env, alice, {.holders = {bob}});
1211 setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags);
1212 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1213
1214 // Rotate the issuer key five times, issuance's issuer epoch is 5.
1215 for (int i = 0; i < 5; ++i)
1216 {
1217 mptAlice.generateKeyPair(alice);
1218 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1219 }
1220
1221 mptAlice.convert({
1222 .account = bob,
1223 .amt = 50,
1224 .holderPubKey = mptAlice.getPubKey(bob),
1225 });
1226 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt));
1227
1228 mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
1229 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt));
1230 }
1231
1232 // Clawback is not blocked on
1233 // a stale issuer mirror. For now the proof cannot verify: it is checked
1234 // against the key registered on the issuance, while the mirror is still
1235 // encrypted under the key it was written with, and that older key is
1236 // nowhere on the ledger yet. This will be added in a separate PR.
1237 {
1238 Env env{*this, features};
1239 MPTTester mptAlice(env, alice, {.holders = {bob}});
1240 setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags);
1241 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1242
1243 // bob initializes at epoch 0.
1244 mptAlice.convert({
1245 .account = bob,
1246 .amt = 50,
1247 .holderPubKey = mptAlice.getPubKey(bob),
1248 });
1249
1250 // Rotate the issuer key to epoch 1, leaving bob behind.
1251 mptAlice.generateKeyPair(alice);
1252 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1253
1254 BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
1255
1256 mptAlice.confidentialClaw({
1257 .account = alice,
1258 .holder = bob,
1259 .amt = 50,
1260 .err = tecBAD_PROOF,
1261 });
1262
1263 BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
1264 }
1265 }
1266
1267 void
1269 {
1270 testcase("ConfidentialMPTMirrorUpdate preflight");
1271 using namespace test::jtx;
1272
1273 Env env{*this, features};
1274 Account const alice("alice");
1275 Account const bob("bob");
1276 Account const carol("carol");
1277 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1278
1279 // A well-formed 66-byte ElGamal ciphertext
1280 Buffer const& validCipher = getTrivialCiphertext();
1281
1282 // Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
1283 if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
1284 {
1285 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
1286 mptAlice.mirrorUpdate({
1287 .account = bob,
1288 .issuerEncryptedAmount = validCipher,
1289 .err = temDISABLED,
1290 });
1291 return;
1292 }
1293
1294 mptAlice.create({
1295 .ownerCount = 1,
1296 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
1297 });
1298 // Issuer mode but account is not the issuer.
1299 mptAlice.mirrorUpdate({
1300 .account = bob,
1301 .holder = carol,
1302 .issuerEncryptedAmount = validCipher,
1303 .err = temMALFORMED,
1304 });
1305
1306 // Issuer mode but the holder is the same as the issuer.
1307 mptAlice.mirrorUpdate({
1308 .account = alice,
1309 .holder = alice,
1310 .issuerEncryptedAmount = validCipher,
1311 .err = temMALFORMED,
1312 });
1313
1314 // Issuer mode but holder is not provided.
1315 mptAlice.mirrorUpdate({
1316 .account = alice,
1317 .issuerEncryptedAmount = validCipher,
1318 .err = temMALFORMED,
1319 });
1320
1321 // At least one of issuer or auditor amount must be present.
1322 mptAlice.mirrorUpdate({
1323 .account = alice,
1324 .holder = bob,
1325 .err = temMALFORMED,
1326 });
1327
1328 // Issuer amount has the wrong length.
1329 mptAlice.mirrorUpdate({
1330 .account = alice,
1331 .holder = bob,
1332 .issuerEncryptedAmount = gMakeZeroBuffer(10),
1333 .err = temBAD_CIPHERTEXT,
1334 });
1335
1336 // Auditor amount has the wrong length.
1337 mptAlice.mirrorUpdate({
1338 .account = alice,
1339 .holder = bob,
1340 .auditorEncryptedAmount = gMakeZeroBuffer(10),
1341 .err = temBAD_CIPHERTEXT,
1342 });
1343
1344 // The proof has the wrong length.
1345 mptAlice.mirrorUpdate({
1346 .account = alice,
1347 .holder = bob,
1348 .issuerEncryptedAmount = validCipher,
1349 .zkProof = gMakeZeroBuffer(kEcEqualityProofLength - 1),
1350 .err = temMALFORMED,
1351 });
1352
1353 // Issuer amount is the right length but not a valid ciphertext.
1354 mptAlice.mirrorUpdate({
1355 .account = alice,
1356 .holder = bob,
1357 .issuerEncryptedAmount = getBadCiphertext(),
1358 .err = temBAD_CIPHERTEXT,
1359 });
1360
1361 // Auditor amount is the right length but not a valid ciphertext.
1362 mptAlice.mirrorUpdate({
1363 .account = alice,
1364 .holder = bob,
1365 .issuerEncryptedAmount = validCipher,
1366 .auditorEncryptedAmount = getBadCiphertext(),
1367 .err = temBAD_CIPHERTEXT,
1368 });
1369 }
1370
1371 void
1373 {
1374 testcase("ConfidentialMPTMirrorUpdate preclaim");
1375 using namespace test::jtx;
1376
1377 Buffer const& validCipher = getTrivialCiphertext();
1378
1379 // The issuance does not exist.
1380 {
1381 Env env{*this, features};
1382 Account const alice("alice");
1383 Account const bob("bob");
1384 MPTTester mptAlice(env, alice, {.holders = {bob}});
1385
1386 mptAlice.create({
1387 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
1388 });
1389 // Destroy the issuance to test issuance not found.
1390 mptAlice.destroy();
1391
1392 mptAlice.mirrorUpdate({
1393 .account = bob,
1394 .issuerEncryptedAmount = validCipher,
1395 .err = tecOBJECT_NOT_FOUND,
1396 });
1397 }
1398
1399 // The issuance has not enabled confidential balances.
1400 {
1401 Env env{*this, features};
1402 Account const alice("alice");
1403 Account const bob("bob");
1404 MPTTester mptAlice(env, alice, {.holders = {bob}});
1405 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
1406
1407 mptAlice.mirrorUpdate({
1408 .account = alice,
1409 .holder = bob,
1410 .issuerEncryptedAmount = validCipher,
1411 .err = tecNO_PERMISSION,
1412 });
1413 }
1414
1415 // The issuer encryption key was not already registered.
1416 {
1417 Env env{*this, features};
1418 Account const alice("alice");
1419 Account const bob("bob");
1420 MPTTester mptAlice(env, alice, {.holders = {bob}});
1421 mptAlice.create(
1422 {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1423 mptAlice.authorize({.account = bob});
1424
1425 mptAlice.mirrorUpdate({
1426 .account = alice,
1427 .holder = bob,
1428 .issuerEncryptedAmount = validCipher,
1429 .err = tecNO_PERMISSION,
1430 });
1431 }
1432
1433 // In issuer mode, the specified holder account does not exist.
1434 {
1435 Env env{*this, features};
1436 Account const alice("alice");
1437 Account const carol("carol");
1438 MPTTester mptAlice(env, alice);
1439 mptAlice.create(
1440 {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1441 mptAlice.generateKeyPair(alice);
1442 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1443
1444 // Carol never got funded so it does not exist.
1445 mptAlice.mirrorUpdate({
1446 .account = alice,
1447 .holder = carol,
1448 .issuerEncryptedAmount = validCipher,
1449 .err = tecNO_TARGET,
1450 });
1451 }
1452
1453 // The holder's MPToken does not exist (holder never authorized).
1454 {
1455 Env env{*this, features};
1456 Account const alice("alice");
1457 Account const bob("bob");
1458 MPTTester mptAlice(env, alice, {.holders = {bob}});
1459 mptAlice.create(
1460 {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1461 mptAlice.generateKeyPair(alice);
1462 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1463
1464 mptAlice.mirrorUpdate({
1465 .account = alice,
1466 .holder = bob,
1467 .issuerEncryptedAmount = validCipher,
1468 .err = tecOBJECT_NOT_FOUND,
1469 });
1470 }
1471
1472 // The holder has an MPToken but no confidential issuer balance (sfIssuerEncryptedBalance).
1473 {
1474 Env env{*this, features};
1475 Account const alice("alice");
1476 Account const bob("bob");
1477 MPTTester mptAlice(env, alice, {.holders = {bob}});
1478 mptAlice.create(
1479 {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1480 mptAlice.authorize({.account = bob});
1481 mptAlice.generateKeyPair(alice);
1482 mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
1483
1484 mptAlice.mirrorUpdate({
1485 .account = alice,
1486 .holder = bob,
1487 .issuerEncryptedAmount = validCipher,
1488 .err = tecNO_PERMISSION,
1489 });
1490 }
1491
1492 // Auditor mirror migration on an issuance with no auditor key.
1493 {
1494 Env env{*this, features};
1495 Account const alice("alice");
1496 Account const bob("bob");
1497
1498 // This setup has issuer key but no auditor key.
1499 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1500
1501 ct.mpt.mirrorUpdate({
1502 .account = alice,
1503 .holder = bob,
1504 .auditorEncryptedAmount = validCipher,
1505 .err = tecNO_PERMISSION,
1506 });
1507 }
1508
1509 // Issuer mirror is already most up-to-date so
1510 // there is nothing to migrate.
1511 {
1512 Env env{*this, features};
1513 Account const alice("alice");
1514 Account const bob("bob");
1515 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1516
1517 ct.mpt.mirrorUpdate({
1518 .account = alice,
1519 .holder = bob,
1520 .issuerEncryptedAmount = validCipher,
1521 .err = tecNO_PERMISSION,
1522 });
1523 }
1524
1525 // Issuer-mode auditor-only migration while the issuer mirror is stale:
1526 // the issuer mirror must be brought up to date before the auditor
1527 // mirror can be migrated.
1528 {
1529 Env env{*this, features};
1530 Account const alice("alice");
1531 Account const bob("bob");
1532 Account const auditor("auditor");
1533 Account const newIssuerKey("newIssuerKey");
1534
1535 // Issuance has both an issuer key and an auditor key, and bob holds
1536 // both mirrors at epoch 0.
1537 ConfidentialEnv ct{
1538 env,
1539 alice,
1540 {{.account = bob}},
1541 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1542 auditor};
1543
1544 // Rotate the issuer key: issuer key epoch 0 -> 1, while bob's
1545 // issuer-mirror epoch stays 0 (stale).
1546 ct.mpt.generateKeyPair(newIssuerKey);
1547 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1548
1549 ct.mpt.mirrorUpdate({
1550 .account = alice,
1551 .holder = bob,
1552 .auditorEncryptedAmount = validCipher,
1553 .err = tecNO_PERMISSION,
1554 });
1555 }
1556
1557 // Auditor mirror is already current (the auditor key has not rotated),
1558 // so there is nothing to migrate.
1559 {
1560 Env env{*this, features};
1561 Account const alice("alice");
1562 Account const bob("bob");
1563 Account const auditor("auditor");
1564
1565 // Issuance has both keys and bob holds both mirrors at epoch 0.
1566 ConfidentialEnv ct{
1567 env,
1568 alice,
1569 {{.account = bob}},
1570 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1571 auditor};
1572
1573 // No key has rotated, so the auditor mirror is up to date
1574 // so there is nothing to migrate.
1575 ct.mpt.mirrorUpdate({
1576 .account = alice,
1577 .holder = bob,
1578 .auditorEncryptedAmount = validCipher,
1579 .err = tecNO_PERMISSION,
1580 });
1581 }
1582
1583 // In an issuer-mode simultaneous migration, both mirrors must be stale. Here
1584 // only the issuer key has rotated so its mirror is stale but the auditor mirror is not.
1585 {
1586 Env env{*this, features};
1587 Account const alice("alice");
1588 Account const bob("bob");
1589 Account const auditor("auditor");
1590 Account const newIssuerKey("newIssuerKey");
1591
1592 ConfidentialEnv ct{
1593 env,
1594 alice,
1595 {{.account = bob}},
1596 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1597 auditor};
1598
1599 // Rotate only the issuer key: issuer key epoch 0 -> 1, auditor key
1600 // epoch stays 0. The issuer mirror is now stale but the auditor
1601 // mirror is still current.
1602 ct.mpt.generateKeyPair(newIssuerKey);
1603 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1604
1605 ct.mpt.mirrorUpdate({
1606 .account = alice,
1607 .holder = bob,
1608 .issuerEncryptedAmount = validCipher,
1609 .auditorEncryptedAmount = validCipher,
1610 .err = tecNO_PERMISSION,
1611 });
1612 }
1613
1614 // In an issuer-mode simultaneous migration, both mirrors must be stale.
1615 // Here only the auditor key has rotated so its mirror is stale but the
1616 // issuer mirror is not.
1617 {
1618 Env env{*this, features};
1619 Account const alice("alice");
1620 Account const bob("bob");
1621 Account const auditor("auditor");
1622 Account const newAuditorKey("newAuditorKey");
1623
1624 ConfidentialEnv ct{
1625 env,
1626 alice,
1627 {{.account = bob}},
1628 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1629 auditor};
1630
1631 // Rotate only the auditor key: auditor key epoch 0 -> 1, issuer key
1632 // epoch stays 0. The auditor mirror is now stale but the issuer
1633 // mirror is still current.
1634 ct.mpt.generateKeyPair(newAuditorKey);
1635 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)});
1636
1637 ct.mpt.mirrorUpdate({
1638 .account = alice,
1639 .holder = bob,
1640 .issuerEncryptedAmount = validCipher,
1641 .auditorEncryptedAmount = validCipher,
1642 .err = tecNO_PERMISSION,
1643 });
1644 }
1645
1646 // Holder self-migration mode runs the same staleness checks.
1647 // No key has rotated, so the holder's own issuer mirror is current and
1648 // there is nothing to migrate.
1649 {
1650 Env env{*this, features};
1651 Account const alice("alice");
1652 Account const bob("bob");
1653 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1654
1655 ct.mpt.mirrorUpdate({
1656 .account = bob,
1657 .issuerEncryptedAmount = validCipher,
1658 .err = tecNO_PERMISSION,
1659 });
1660 }
1661
1662 // Holder self-migration mode, simultaneously migrating both keys: only the issuer key
1663 // has rotated, so the holder's issuer mirror is stale but the auditor
1664 // mirror is still current.
1665 {
1666 Env env{*this, features};
1667 Account const alice("alice");
1668 Account const bob("bob");
1669 Account const auditor("auditor");
1670 Account const newIssuerKey("newIssuerKey");
1671
1672 ConfidentialEnv ct{
1673 env,
1674 alice,
1675 {{.account = bob}},
1676 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1677 auditor};
1678
1679 // Rotate only the issuer key: issuer key epoch 0 -> 1, auditor key
1680 // epoch stays 0.
1681 ct.mpt.generateKeyPair(newIssuerKey);
1682 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1683
1684 // Holder mode (no Holder field) needs no previous issuer key.
1685 ct.mpt.mirrorUpdate({
1686 .account = bob,
1687 .issuerEncryptedAmount = validCipher,
1688 .auditorEncryptedAmount = validCipher,
1689 .err = tecNO_PERMISSION,
1690 });
1691 }
1692
1693 // Holder self-migration mode, simultaneously migrating both keys:
1694 // only the auditor key has rotated, so the holder's auditor mirror is stale but the issuer
1695 // mirror is still current.
1696 {
1697 Env env{*this, features};
1698 Account const alice("alice");
1699 Account const bob("bob");
1700 Account const auditor("auditor");
1701 Account const newAuditorKey("newAuditorKey");
1702
1703 ConfidentialEnv ct{
1704 env,
1705 alice,
1706 {{.account = bob}},
1707 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1708 auditor};
1709
1710 // Rotate only the auditor key: auditor key epoch 0 -> 1, issuer key
1711 // epoch stays 0.
1712 ct.mpt.generateKeyPair(newAuditorKey);
1713 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)});
1714
1715 // Auditor mirror is stale but issuer mirror is current so this is rejected.
1716 ct.mpt.mirrorUpdate({
1717 .account = bob,
1718 .issuerEncryptedAmount = validCipher,
1719 .auditorEncryptedAmount = validCipher,
1720 .err = tecNO_PERMISSION,
1721 });
1722 }
1723
1724 // Holder self-migration requires the holder's inbox to be canonical
1725 // zero, because the cross-key equality proof anchors on the spending
1726 // balance, which only reflects the full balance after the inbox is
1727 // merged. A holder with a non-zero inbox is rejected.
1728 {
1729 Env env{*this, features};
1730 Account const alice("alice");
1731 Account const bob("bob");
1732 Account const carol("carol");
1733 Account const newIssuerKey("newIssuerKey");
1734
1735 ConfidentialEnv ct{env, alice, {{.account = bob}, {.account = carol}}};
1736
1737 // Carol sends Bob a confidential amount; Bob does NOT merge it, so
1738 // his inbox is no longer canonical zero.
1739 ct.mpt.send({.account = carol, .dest = bob, .amt = 10});
1740
1741 // Rotate the issuer key so the issuer mirror is stale and the
1742 // migration gets past the epoch check to reach the inbox check.
1743 ct.mpt.generateKeyPair(newIssuerKey);
1744 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1745
1746 ct.mpt.mirrorUpdate({
1747 .account = bob,
1748 .issuerEncryptedAmount = validCipher,
1749 .err = tecNO_PERMISSION,
1750 });
1751
1752 // Merging the inbox makes the migration succeed.
1753 ct.mpt.mergeInbox({.account = bob});
1754 ct.mpt.mirrorUpdate({
1755 .account = bob,
1756 .issuerEncryptedAmount = validCipher,
1757 .err = tesSUCCESS,
1758 });
1759 }
1760
1761 // A lock does not block a migration.
1762 {
1763 Env env{*this, features};
1764 Account const alice("alice");
1765 Account const bob("bob");
1766 Account const carol("carol");
1767 Account const newIssuerKey("newIssuerKey");
1768 Account const newerIssuerKey("newerIssuerKey");
1769
1770 ConfidentialEnv ct{env, alice, {{.account = bob}, {.account = carol}}};
1771 ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
1772 ct.mpt.set({.account = alice, .holder = carol, .flags = tfMPTLock});
1773
1774 // Rotate the issuer key so both holders' mirrors are stale.
1775 ct.mpt.generateKeyPair(newIssuerKey);
1776 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1777
1778 // The issuer migrates an individually locked holder.
1779 ct.mpt.mirrorUpdate({
1780 .account = alice,
1781 .holder = bob,
1782 .issuerEncryptedAmount = validCipher,
1783 .err = tesSUCCESS,
1784 });
1785
1786 // An individually locked holder migrates itself.
1787 ct.mpt.mirrorUpdate({
1788 .account = carol,
1789 .issuerEncryptedAmount = validCipher,
1790 .err = tesSUCCESS,
1791 });
1792
1793 // Release the individual locks and lock the whole issuance instead. Rotate again so
1794 // both mirrors are stale once more.
1795 ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
1796 ct.mpt.set({.account = alice, .holder = carol, .flags = tfMPTUnlock});
1797 ct.mpt.set({.account = alice, .flags = tfMPTLock});
1798 ct.mpt.generateKeyPair(newerIssuerKey);
1799 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newerIssuerKey)});
1800
1801 ct.mpt.mirrorUpdate({
1802 .account = alice,
1803 .holder = bob,
1804 .issuerEncryptedAmount = validCipher,
1805 .err = tesSUCCESS,
1806 });
1807
1808 ct.mpt.mirrorUpdate({
1809 .account = carol,
1810 .issuerEncryptedAmount = validCipher,
1811 .err = tesSUCCESS,
1812 });
1813 }
1814 }
1815
1816 void
1818 {
1819 testcase("ConfidentialMPTMirrorUpdate doApply");
1820 using namespace test::jtx;
1821
1822 // The holder's confidential balance, matching the ConfidentialEnv default
1823 // convertAmount. The migration re-encrypts this amount under the new key.
1824 std::uint64_t const amount = 100;
1825
1826 // Issuer mode issuer-mirror migration. The new issuer mirror is written
1827 // and the auditor mirror epoch advances to the issuer key epoch.
1828 {
1829 Env env{*this, features};
1830 Account const alice("alice");
1831 Account const bob("bob");
1832 Account const newIssuerKey("newIssuerKey");
1833 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1834
1835 // Rotate the issuer key: issuer key epoch 0 -> 1.
1836 ct.mpt.generateKeyPair(newIssuerKey);
1837 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
1838
1839 // Re-encrypt Bob's balance under the new issuer key.
1840 Buffer const newIssuerCipher =
1841 ct.mpt.encryptAmount(newIssuerKey, amount, generateBlindingFactor());
1842
1843 // The previous issuer key is the pre-rotation issuer key (alice's),
1844 // no longer on-ledger after the rotation, provide it in the transaction.
1845 ct.mpt.mirrorUpdate({
1846 .account = alice,
1847 .holder = bob,
1848 .issuerEncryptedAmount = newIssuerCipher,
1849 });
1850
1851 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
1852 if (!BEAST_EXPECT(sle))
1853 return;
1854
1855 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
1856 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u);
1857
1858 // The issuer mirror is now current, so re-migrating it is rejected.
1859 ct.mpt.mirrorUpdate({
1860 .account = alice,
1861 .holder = bob,
1862 .issuerEncryptedAmount = newIssuerCipher,
1863 .err = tecNO_PERMISSION,
1864 });
1865 }
1866
1867 // Issuer mode auditor-mirror migration. The new auditor mirror is written
1868 // and the auditor mirror epoch advances to the auditor key epoch.
1869 {
1870 Env env{*this, features};
1871 Account const alice("alice");
1872 Account const bob("bob");
1873 Account const auditor("auditor");
1874 Account const newAuditorKey("newAuditorKey");
1875 ConfidentialEnv ct{
1876 env,
1877 alice,
1878 {{.account = bob}},
1879 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1880 auditor};
1881
1882 // Rotate only the auditor key: auditor key epoch 0 -> 1.
1883 ct.mpt.generateKeyPair(newAuditorKey);
1884 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)});
1885
1886 // Re-encrypt Bob's balance under the new auditor key.
1887 Buffer const newAuditorCipher =
1888 ct.mpt.encryptAmount(newAuditorKey, amount, generateBlindingFactor());
1889
1890 ct.mpt.mirrorUpdate({
1891 .account = alice,
1892 .holder = bob,
1893 .auditorEncryptedAmount = newAuditorCipher,
1894 });
1895
1896 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
1897 if (!BEAST_EXPECT(sle))
1898 return;
1899
1900 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
1901 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u);
1902 }
1903
1904 // Issuer mode simultaneous migration: both mirrors are written in one transaction and
1905 // both epochs advance.
1906 {
1907 Env env{*this, features};
1908 Account const alice("alice");
1909 Account const bob("bob");
1910 Account const auditor("auditor");
1911 Account const newIssuerKey("newIssuerKey");
1912 Account const newAuditorKey("newAuditorKey");
1913 ConfidentialEnv ct{
1914 env,
1915 alice,
1916 {{.account = bob}},
1917 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
1918 auditor};
1919
1920 // Rotate both keys: both key epochs 0 -> 1.
1921 ct.mpt.generateKeyPair(newIssuerKey);
1922 ct.mpt.generateKeyPair(newAuditorKey);
1923 ct.mpt.set({
1924 .account = alice,
1925 .issuerPubKey = ct.mpt.getPubKey(newIssuerKey),
1926 .auditorPubKey = ct.mpt.getPubKey(newAuditorKey),
1927 });
1928
1929 // Re-encrypt Bob's balance under each new key.
1930 Buffer const bf = generateBlindingFactor();
1931 Buffer const newIssuerCipher = ct.mpt.encryptAmount(newIssuerKey, amount, bf);
1932 Buffer const newAuditorCipher = ct.mpt.encryptAmount(newAuditorKey, amount, bf);
1933
1934 ct.mpt.mirrorUpdate({
1935 .account = alice,
1936 .holder = bob,
1937 .issuerEncryptedAmount = newIssuerCipher,
1938 .auditorEncryptedAmount = newAuditorCipher,
1939 });
1940
1941 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
1942 if (!BEAST_EXPECT(sle))
1943 return;
1944
1945 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
1946 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
1947 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u);
1948 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u);
1949 }
1950
1951 // Issuer mode auditor late-registration: the auditor key is registered for the first
1952 // time (key epoch absent), so setting the initial auditor mirror leaves
1953 // the auditor mirror epoch absent as well.
1954 {
1955 Env env{*this, features};
1956 Account const alice("alice");
1957 Account const bob("bob");
1958 Account const auditor("auditor");
1959 // No auditor in the confidential setup, so bob has no auditor mirror.
1960 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1961
1962 // Register an auditor key for the first time (auditor key epoch stays
1963 // absent).
1964 ct.mpt.generateKeyPair(auditor);
1965 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditor)});
1966
1967 // Encrypt Bob's balance under the newly registered auditor key.
1968 Buffer const auditorCipher =
1969 ct.mpt.encryptAmount(auditor, amount, generateBlindingFactor());
1970
1971 ct.mpt.mirrorUpdate({
1972 .account = alice,
1973 .holder = bob,
1974 .auditorEncryptedAmount = auditorCipher,
1975 });
1976
1977 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
1978 if (!BEAST_EXPECT(sle))
1979 return;
1980 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(auditorCipher));
1981 // First-time registration leaves the mirror epoch absent (== 0).
1982 BEAST_EXPECT(!sle->isFieldPresent(sfAuditorKeyMirrorEpoch));
1983 }
1984
1985 // Holder self-migration migrates from the holder's own spending balance
1986 // (Holder being Account field, no Holder field, and no previous issuer key in any flow
1987 // because the anchor is the spending balance, not the old issuer mirror). ConfidentialEnv
1988 // already merged the inbox so the holder's inbox is canonical zero.
1989
1990 // Holder issuer-mirror migration.
1991 {
1992 Env env{*this, features};
1993 Account const alice("alice");
1994 Account const bob("bob");
1995 Account const newIssuerKey("newIssuerKey");
1996 ConfidentialEnv ct{env, alice, {{.account = bob}}};
1997
1998 ct.mpt.generateKeyPair(newIssuerKey);
1999 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)});
2000
2001 // The holder re-encrypts their own balance under the new issuer key.
2002 Buffer const newIssuerCipher =
2003 ct.mpt.encryptAmount(newIssuerKey, amount, generateBlindingFactor());
2004
2005 ct.mpt.mirrorUpdate({
2006 .account = bob,
2007 .issuerEncryptedAmount = newIssuerCipher,
2008 });
2009
2010 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2011 if (!BEAST_EXPECT(sle))
2012 return;
2013 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2014 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u);
2015 }
2016
2017 // Holder auditor-mirror migration.
2018 {
2019 Env env{*this, features};
2020 Account const alice("alice");
2021 Account const bob("bob");
2022 Account const auditor("auditor");
2023 Account const newAuditorKey("newAuditorKey");
2024 ConfidentialEnv ct{
2025 env,
2026 alice,
2027 {{.account = bob}},
2028 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
2029 auditor};
2030
2031 ct.mpt.generateKeyPair(newAuditorKey);
2032 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)});
2033
2034 // The holder re-encrypts their own balance under the new auditor key.
2035 Buffer const newAuditorCipher =
2036 ct.mpt.encryptAmount(newAuditorKey, amount, generateBlindingFactor());
2037
2038 ct.mpt.mirrorUpdate({
2039 .account = bob,
2040 .auditorEncryptedAmount = newAuditorCipher,
2041 });
2042
2043 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2044 if (!BEAST_EXPECT(sle))
2045 return;
2046 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
2047 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u);
2048 }
2049
2050 // Holder simultaneous migration of both mirrors.
2051 {
2052 Env env{*this, features};
2053 Account const alice("alice");
2054 Account const bob("bob");
2055 Account const auditor("auditor");
2056 Account const newIssuerKey("newIssuerKey");
2057 Account const newAuditorKey("newAuditorKey");
2058 ConfidentialEnv ct{
2059 env,
2060 alice,
2061 {{.account = bob}},
2062 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
2063 auditor};
2064
2065 ct.mpt.generateKeyPair(newIssuerKey);
2066 ct.mpt.generateKeyPair(newAuditorKey);
2067 ct.mpt.set({
2068 .account = alice,
2069 .issuerPubKey = ct.mpt.getPubKey(newIssuerKey),
2070 .auditorPubKey = ct.mpt.getPubKey(newAuditorKey),
2071 });
2072
2073 Buffer const bf = generateBlindingFactor();
2074 Buffer const newIssuerCipher = ct.mpt.encryptAmount(newIssuerKey, amount, bf);
2075 Buffer const newAuditorCipher = ct.mpt.encryptAmount(newAuditorKey, amount, bf);
2076
2077 // Holder mode needs no previous issuer key even for the issuer mirror.
2078 ct.mpt.mirrorUpdate({
2079 .account = bob,
2080 .issuerEncryptedAmount = newIssuerCipher,
2081 .auditorEncryptedAmount = newAuditorCipher,
2082 });
2083
2084 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2085 if (!BEAST_EXPECT(sle))
2086 return;
2087 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2088 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
2089 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u);
2090 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u);
2091 }
2092
2093 // Holder auditor late-registration: the auditor key is registered for the first time (key
2094 // epoch absent), so the holder setting their initial auditor mirror leaves the auditor
2095 // mirror epoch absent as well.
2096 {
2097 Env env{*this, features};
2098 Account const alice("alice");
2099 Account const bob("bob");
2100 Account const auditor("auditor");
2101 // No auditor in the confidential setup, so bob has no auditor mirror.
2102 ConfidentialEnv ct{env, alice, {{.account = bob}}};
2103
2104 // Register an auditor key for the first time (auditor key epoch stays
2105 // absent).
2106 ct.mpt.generateKeyPair(auditor);
2107 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditor)});
2108
2109 // The holder encrypts their own balance under the newly registered auditor key.
2110 Buffer const auditorCipher =
2111 ct.mpt.encryptAmount(auditor, amount, generateBlindingFactor());
2112
2113 ct.mpt.mirrorUpdate({
2114 .account = bob,
2115 .auditorEncryptedAmount = auditorCipher,
2116 });
2117
2118 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2119 if (!BEAST_EXPECT(sle))
2120 return;
2121 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(auditorCipher));
2122 // First-time registration leaves the mirror epoch absent.
2123 BEAST_EXPECT(!sle->isFieldPresent(sfAuditorKeyMirrorEpoch));
2124 }
2125 }
2126
2127 void
2129 {
2130 testcase("ConfidentialMPTMirrorUpdate issuer migrates after several rotations");
2131 using namespace test::jtx;
2132
2133 std::uint64_t const amount = 100;
2134
2135 Env env{*this, features};
2136 Account const alice("alice");
2137 Account const bob("bob");
2138 Account const auditor("auditor");
2139 Account const issuerKey1("issuerKey1");
2140 Account const issuerKey2("issuerKey2");
2141 Account const issuerKey3("issuerKey3");
2142 Account const issuerKey4("issuerKey4");
2143 Account const issuerKey5("issuerKey5");
2144 Account const auditorKey1("auditorKey1");
2145 Account const auditorKey2("auditorKey2");
2146 Account const auditorKey3("auditorKey3");
2147 Account const auditorKey4("auditorKey4");
2148 ConfidentialEnv ct{
2149 env,
2150 alice,
2151 {{.account = bob}},
2152 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
2153 auditor};
2154
2155 // Rotate the issuer key three times: issuer key epoch 0 -> 3. Bob never
2156 // migrates in between, so his issuer mirror stays at mirror epoch 0 and
2157 // is still encrypted under the original issuer key (alice's).
2158 ct.mpt.generateKeyPair(issuerKey1);
2159 ct.mpt.generateKeyPair(issuerKey2);
2160 ct.mpt.generateKeyPair(issuerKey3);
2161 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey1)});
2162 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey2)});
2163 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey3)});
2164
2165 {
2166 auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID()));
2167 BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 3u);
2168 }
2169
2170 // A single migration re-encrypts the mirror under the newest key and
2171 // jumps the mirror epoch straight to the current key epoch (3), rather
2172 // than advancing one rotation at a time. The previous issuer key is the
2173 // original key (alice's) that the stale mirror is still encrypted under,
2174 // not any intermediate rotation.
2175 Buffer const newIssuerCipher =
2176 ct.mpt.encryptAmount(issuerKey3, amount, generateBlindingFactor());
2177
2178 ct.mpt.mirrorUpdate({
2179 .account = alice,
2180 .holder = bob,
2181 .issuerEncryptedAmount = newIssuerCipher,
2182 });
2183
2184 {
2185 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2186 if (!BEAST_EXPECT(sle))
2187 return;
2188 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2189 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u);
2190 }
2191
2192 // The issuer mirror is now current (epoch 3 == key epoch 3), so a second
2193 // issuer migration is rejected.
2194 ct.mpt.mirrorUpdate({
2195 .account = alice,
2196 .holder = bob,
2197 .issuerEncryptedAmount = newIssuerCipher,
2198 .err = tecNO_PERMISSION,
2199 });
2200
2201 // Now rotate the auditor key twice: auditor key epoch 0 -> 2. Bob's
2202 // auditor mirror is still at mirror epoch 0, under the original auditor
2203 // key. The issuer key and its epoch are untouched.
2204 ct.mpt.generateKeyPair(auditorKey1);
2205 ct.mpt.generateKeyPair(auditorKey2);
2206 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey1)});
2207 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey2)});
2208
2209 {
2210 auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID()));
2211 BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 2u);
2212 BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 3u);
2213 }
2214
2215 // A single auditor-only migration jumps the auditor mirror epoch straight
2216 // to the current auditor key epoch (2). This is an issuer-mode
2217 // auditor-only migration, which is allowed because the issuer mirror is
2218 // already current; no previous issuer key is needed for an auditor
2219 // migration.
2220 Buffer const newAuditorCipher =
2221 ct.mpt.encryptAmount(auditorKey2, amount, generateBlindingFactor());
2222
2223 ct.mpt.mirrorUpdate({
2224 .account = alice,
2225 .holder = bob,
2226 .auditorEncryptedAmount = newAuditorCipher,
2227 });
2228
2229 {
2230 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2231 if (!BEAST_EXPECT(sle))
2232 return;
2233 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
2234 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 2u);
2235 // The issuer mirror and its epoch are unaffected by the auditor
2236 // migration.
2237 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2238 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u);
2239 }
2240
2241 // The auditor mirror is now current (epoch 2 == key epoch 2), so a second
2242 // auditor migration is rejected.
2243 ct.mpt.mirrorUpdate({
2244 .account = alice,
2245 .holder = bob,
2246 .auditorEncryptedAmount = newAuditorCipher,
2247 .err = tecNO_PERMISSION,
2248 });
2249
2250 // Now rotate BOTH keys together twice: issuer key epoch 3 -> 5, auditor
2251 // key epoch 2 -> 4. Bob's mirrors stay at epoch 3 / 2 (stale again).
2252 ct.mpt.generateKeyPair(issuerKey4);
2253 ct.mpt.generateKeyPair(issuerKey5);
2254 ct.mpt.generateKeyPair(auditorKey3);
2255 ct.mpt.generateKeyPair(auditorKey4);
2256 ct.mpt.set({
2257 .account = alice,
2258 .issuerPubKey = ct.mpt.getPubKey(issuerKey4),
2259 .auditorPubKey = ct.mpt.getPubKey(auditorKey3),
2260 });
2261 ct.mpt.set({
2262 .account = alice,
2263 .issuerPubKey = ct.mpt.getPubKey(issuerKey5),
2264 .auditorPubKey = ct.mpt.getPubKey(auditorKey4),
2265 });
2266
2267 {
2268 auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID()));
2269 BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 5u);
2270 BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 4u);
2271 }
2272
2273 // A single simultaneous migration brings both mirrors current in one
2274 // transaction: issuer mirror epoch 3 -> 5, auditor mirror epoch 2 -> 4.
2275 // The previous issuer key is issuerKey3, which is the key Bob's current
2276 // (stale) issuer mirror is encrypted under after the earlier issuer
2277 // migration, not alice's original key nor any intermediate rotation.
2278 Buffer const bothIssuerCipher =
2279 ct.mpt.encryptAmount(issuerKey5, amount, generateBlindingFactor());
2280 Buffer const bothAuditorCipher =
2281 ct.mpt.encryptAmount(auditorKey4, amount, generateBlindingFactor());
2282
2283 ct.mpt.mirrorUpdate({
2284 .account = alice,
2285 .holder = bob,
2286 .issuerEncryptedAmount = bothIssuerCipher,
2287 .auditorEncryptedAmount = bothAuditorCipher,
2288 });
2289
2290 {
2291 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2292 if (!BEAST_EXPECT(sle))
2293 return;
2294 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(bothIssuerCipher));
2295 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(bothAuditorCipher));
2296 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 5u);
2297 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 4u);
2298 }
2299
2300 // Both mirrors are current now, so a second simultaneous migration is
2301 // rejected.
2302 ct.mpt.mirrorUpdate({
2303 .account = alice,
2304 .holder = bob,
2305 .issuerEncryptedAmount = bothIssuerCipher,
2306 .auditorEncryptedAmount = bothAuditorCipher,
2307 .err = tecNO_PERMISSION,
2308 });
2309 }
2310
2311 void
2313 {
2314 testcase("ConfidentialMPTMirrorUpdate holder migrates after several rotations");
2315 using namespace test::jtx;
2316
2317 std::uint64_t const amount = 100;
2318
2319 Env env{*this, features};
2320 Account const alice("alice");
2321 Account const bob("bob");
2322 Account const auditor("auditor");
2323 Account const issuerKey1("issuerKey1");
2324 Account const issuerKey2("issuerKey2");
2325 Account const issuerKey3("issuerKey3");
2326 Account const issuerKey4("issuerKey4");
2327 Account const issuerKey5("issuerKey5");
2328 Account const auditorKey1("auditorKey1");
2329 Account const auditorKey2("auditorKey2");
2330 Account const auditorKey3("auditorKey3");
2331 Account const auditorKey4("auditorKey4");
2332 ConfidentialEnv ct{
2333 env,
2334 alice,
2335 {{.account = bob}},
2336 tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer,
2337 auditor};
2338
2339 // In holder self-migration mode the holder submits (account = bob, no
2340 // Holder field) and never provides a previous issuer key.
2341 // Bob's inbox is canonical zero after the ConfidentialEnv merge.
2342
2343 // Rotate the issuer key three times: issuer key epoch 0 -> 3.
2344 ct.mpt.generateKeyPair(issuerKey1);
2345 ct.mpt.generateKeyPair(issuerKey2);
2346 ct.mpt.generateKeyPair(issuerKey3);
2347 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey1)});
2348 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey2)});
2349 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey3)});
2350
2351 // A single holder migration jumps the issuer mirror epoch straight to 3.
2352 Buffer const newIssuerCipher =
2353 ct.mpt.encryptAmount(issuerKey3, amount, generateBlindingFactor());
2354
2355 ct.mpt.mirrorUpdate({
2356 .account = bob,
2357 .issuerEncryptedAmount = newIssuerCipher,
2358 });
2359
2360 {
2361 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2362 if (!BEAST_EXPECT(sle))
2363 return;
2364 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2365 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u);
2366 }
2367
2368 // The issuer mirror is current, so a second holder issuer migration is
2369 // rejected.
2370 ct.mpt.mirrorUpdate({
2371 .account = bob,
2372 .issuerEncryptedAmount = newIssuerCipher,
2373 .err = tecNO_PERMISSION,
2374 });
2375
2376 // Rotate the auditor key twice: auditor key epoch 0 -> 2.
2377 ct.mpt.generateKeyPair(auditorKey1);
2378 ct.mpt.generateKeyPair(auditorKey2);
2379 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey1)});
2380 ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey2)});
2381
2382 // A single holder auditor migration jumps the auditor mirror epoch to 2.
2383 Buffer const newAuditorCipher =
2384 ct.mpt.encryptAmount(auditorKey2, amount, generateBlindingFactor());
2385
2386 ct.mpt.mirrorUpdate({
2387 .account = bob,
2388 .auditorEncryptedAmount = newAuditorCipher,
2389 });
2390
2391 {
2392 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2393 if (!BEAST_EXPECT(sle))
2394 return;
2395 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher));
2396 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 2u);
2397 // The issuer mirror is unaffected.
2398 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher));
2399 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u);
2400 }
2401
2402 // The auditor mirror is current, so a second holder auditor migration is
2403 // rejected.
2404 ct.mpt.mirrorUpdate({
2405 .account = bob,
2406 .auditorEncryptedAmount = newAuditorCipher,
2407 .err = tecNO_PERMISSION,
2408 });
2409
2410 // Rotate both keys together twice: issuer key epoch 3 -> 5, auditor key
2411 // epoch 2 -> 4.
2412 ct.mpt.generateKeyPair(issuerKey4);
2413 ct.mpt.generateKeyPair(issuerKey5);
2414 ct.mpt.generateKeyPair(auditorKey3);
2415 ct.mpt.generateKeyPair(auditorKey4);
2416 ct.mpt.set({
2417 .account = alice,
2418 .issuerPubKey = ct.mpt.getPubKey(issuerKey4),
2419 .auditorPubKey = ct.mpt.getPubKey(auditorKey3),
2420 });
2421 ct.mpt.set({
2422 .account = alice,
2423 .issuerPubKey = ct.mpt.getPubKey(issuerKey5),
2424 .auditorPubKey = ct.mpt.getPubKey(auditorKey4),
2425 });
2426
2427 // A single holder migration brings both mirrors current: issuer mirror
2428 // epoch 3 -> 5, auditor mirror epoch 2 -> 4. Still no previous issuer key.
2429 Buffer const bothIssuerCipher =
2430 ct.mpt.encryptAmount(issuerKey5, amount, generateBlindingFactor());
2431 Buffer const bothAuditorCipher =
2432 ct.mpt.encryptAmount(auditorKey4, amount, generateBlindingFactor());
2433
2434 ct.mpt.mirrorUpdate({
2435 .account = bob,
2436 .issuerEncryptedAmount = bothIssuerCipher,
2437 .auditorEncryptedAmount = bothAuditorCipher,
2438 });
2439
2440 {
2441 auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2442 if (!BEAST_EXPECT(sle))
2443 return;
2444 BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(bothIssuerCipher));
2445 BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(bothAuditorCipher));
2446 BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 5u);
2447 BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 4u);
2448 }
2449
2450 // Both mirrors are current, so a second holder migration is rejected.
2451 ct.mpt.mirrorUpdate({
2452 .account = bob,
2453 .issuerEncryptedAmount = bothIssuerCipher,
2454 .auditorEncryptedAmount = bothAuditorCipher,
2455 .err = tecNO_PERMISSION,
2456 });
2457 }
2458
2459 void
2461 {
2462 testcase("ConfidentialMPTHolderKeyUpdate preflight");
2463 using namespace test::jtx;
2464
2465 Env env{*this, features};
2466 Account const alice("alice");
2467 Account const bob("bob");
2468
2469 // Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer.
2470 if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer])
2471 {
2472 MPTTester mptAlice(env, alice, {.holders = {bob}});
2473 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
2474 mptAlice.holderKeyUpdate({
2475 .account = bob,
2476 .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
2477 .flags = tfHolderKeyRecovery,
2478 .err = temDISABLED,
2479 });
2480 return;
2481 }
2482
2483 ConfidentialEnv ct{env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2484
2485 Account const bobNewKey("bobNewKey");
2486 ct.mpt.generateKeyPair(bobNewKey);
2487
2488 // The flag contains a value outside the recognized mode bits. This is
2489 // rejected by the flags-mask check
2490 for (auto const flags : {0x00080000u, 0x00080000u | tfHolderKeyRecovery, 0x00100000u})
2491 {
2492 ct.mpt.holderKeyUpdate({
2493 .account = bob,
2494 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2495 .proof = gMakeZeroBuffer(1),
2496 .flags = flags,
2497 .err = temINVALID_FLAG,
2498 });
2499 }
2500
2501 // Exactly one of Rotation, Recovery, and Cancel must be set.
2502 for (auto const flags :
2503 {0u,
2504 tfHolderKeyRotation | tfHolderKeyRecovery,
2505 tfHolderKeyRotation | tfCancelRecovery,
2506 tfHolderKeyRecovery | tfCancelRecovery,
2507 tfHolderKeyRotation | tfHolderKeyRecovery | tfCancelRecovery})
2508 {
2509 ct.mpt.holderKeyUpdate({
2510 .account = bob,
2511 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2512 .proof = gMakeZeroBuffer(1),
2513 .flags = flags,
2514 .err = temINVALID_FLAG,
2515 });
2516 }
2517
2518 // The issuer cannot rotate or recover a confidential balance it cannot hold.
2519 Account const aliceNewKey("aliceNewKey");
2520 ct.mpt.generateKeyPair(aliceNewKey);
2521 ct.mpt.holderKeyUpdate({
2522 .account = alice,
2523 .holderPubKey = ct.mpt.getPubKey(aliceNewKey),
2524 .proof = gMakeZeroBuffer(1),
2525 .flags = tfHolderKeyRecovery,
2526 .err = temMALFORMED,
2527 });
2528
2529 // HolderEncryptionKey one byte short of the required length.
2530 ct.mpt.holderKeyUpdate({
2531 .account = bob,
2532 .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength - 1),
2533 .proof = gMakeZeroBuffer(1),
2534 .flags = tfHolderKeyRecovery,
2535 .err = temMALFORMED,
2536 });
2537
2538 // HolderEncryptionKey the correct length, but not a well-formed
2539 // compressed secp256k1 point.
2540 ct.mpt.holderKeyUpdate({
2541 .account = bob,
2542 .holderPubKey = gMakeZeroBuffer(kEcPubKeyLength),
2543 .proof = gMakeZeroBuffer(1),
2544 .flags = tfHolderKeyRecovery,
2545 .err = temMALFORMED,
2546 });
2547
2548 // HolderEncryptionKey is entirely absent (as opposed to present with
2549 // the wrong length or format).
2550 ct.mpt.holderKeyUpdate({
2551 .account = bob,
2552 .proof = gMakeZeroBuffer(1),
2553 .flags = tfHolderKeyRecovery,
2554 .err = temMALFORMED,
2555 });
2556
2558 std::optional<Buffer> const none;
2559
2560 // Rotation mode requires both the spending and inbox ciphertexts;
2561 for (auto const& [spending, inbox] :
2562 {std::pair{none, none}, std::pair{cipher, none}, std::pair{none, cipher}})
2563 {
2564 ct.mpt.holderKeyUpdate({
2565 .account = bob,
2566 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2567 .spendingCiphertext = spending,
2568 .inboxCiphertext = inbox,
2569 .proof = gMakeZeroBuffer(1),
2570 .flags = tfHolderKeyRotation,
2571 .err = temMALFORMED,
2572 });
2573 }
2574
2575 // Recovery and Cancel modes must not include either.
2576 for (auto const& [spending, inbox] :
2577 {std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
2578 {
2579 ct.mpt.holderKeyUpdate({
2580 .account = bob,
2581 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2582 .spendingCiphertext = spending,
2583 .inboxCiphertext = inbox,
2584 .proof = gMakeZeroBuffer(1),
2585 .flags = tfHolderKeyRecovery,
2586 .err = temMALFORMED,
2587 });
2588 }
2589
2590 for (auto const& [spending, inbox] :
2591 {std::pair{cipher, none}, std::pair{none, cipher}, std::pair{cipher, cipher}})
2592 {
2593 ct.mpt.holderKeyUpdate({
2594 .account = bob,
2595 .spendingCiphertext = spending,
2596 .inboxCiphertext = inbox,
2597 .flags = tfCancelRecovery,
2598 .err = temMALFORMED,
2599 });
2600 }
2601
2602 // Spending ciphertext has the wrong length.
2603 ct.mpt.holderKeyUpdate({
2604 .account = bob,
2605 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2606 .spendingCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
2607 .inboxCiphertext = getTrivialCiphertext(),
2608 .proof = gMakeZeroBuffer(1),
2609 .flags = tfHolderKeyRotation,
2610 .err = temBAD_CIPHERTEXT,
2611 });
2612
2613 // Inbox ciphertext has the wrong length.
2614 ct.mpt.holderKeyUpdate({
2615 .account = bob,
2616 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2617 .spendingCiphertext = getTrivialCiphertext(),
2618 .inboxCiphertext = gMakeZeroBuffer(kEcGamalEncryptedTotalLength - 1),
2619 .proof = gMakeZeroBuffer(1),
2620 .flags = tfHolderKeyRotation,
2621 .err = temBAD_CIPHERTEXT,
2622 });
2623
2624 // Spending ciphertext has the correct length, but is not a
2625 // well-formed EC ElGamal ciphertext.
2626 ct.mpt.holderKeyUpdate({
2627 .account = bob,
2628 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2629 .spendingCiphertext = getBadCiphertext(),
2630 .inboxCiphertext = getTrivialCiphertext(),
2631 .proof = gMakeZeroBuffer(1),
2632 .flags = tfHolderKeyRotation,
2633 .err = temBAD_CIPHERTEXT,
2634 });
2635
2636 // Inbox ciphertext has the correct length, but is not a well-formed
2637 // EC ElGamal ciphertext.
2638 ct.mpt.holderKeyUpdate({
2639 .account = bob,
2640 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2641 .spendingCiphertext = getTrivialCiphertext(),
2642 .inboxCiphertext = getBadCiphertext(),
2643 .proof = gMakeZeroBuffer(1),
2644 .flags = tfHolderKeyRotation,
2645 .err = temBAD_CIPHERTEXT,
2646 });
2647
2648 // Rotation/Recovery mode requires a ZKProof.
2649 ct.mpt.holderKeyUpdate({
2650 .account = bob,
2651 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2652 .flags = tfHolderKeyRecovery,
2653 .err = temMALFORMED,
2654 });
2655
2656 // Cancel mode must not include HolderEncryptionKey.
2657 ct.mpt.holderKeyUpdate({
2658 .account = bob,
2659 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2660 .flags = tfCancelRecovery,
2661 .err = temMALFORMED,
2662 });
2663
2664 // Cancel mode must not include a ZKProof.
2665 ct.mpt.holderKeyUpdate({
2666 .account = bob,
2667 .proof = gMakeZeroBuffer(1),
2668 .flags = tfCancelRecovery,
2669 .err = temMALFORMED,
2670 });
2671 }
2672
2673 void
2675 {
2676 testcase("ConfidentialMPTHolderKeyUpdate preclaim");
2677 using namespace test::jtx;
2678
2679 // The issuance does not exist.
2680 {
2681 Env env{*this, features};
2682 Account const alice("alice");
2683 Account const bob("bob");
2684 MPTTester mptAlice(env, alice, {.holders = {bob}});
2685
2686 mptAlice.create({
2687 .ownerCount = 1,
2688 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2689 });
2690 // Destroy the issuance to test issuance not found.
2691 mptAlice.destroy();
2692
2693 mptAlice.holderKeyUpdate({
2694 .account = bob,
2695 .flags = tfCancelRecovery,
2696 .err = tecOBJECT_NOT_FOUND,
2697 });
2698 }
2699
2700 // The issuance has not enabled confidential balances. The holder is
2701 // authorized so this reaches the confidential-balance-flag check
2702 // rather than failing earlier on a missing MPToken.
2703 {
2704 Env env{*this, features};
2705 Account const alice("alice");
2706 Account const bob("bob");
2707 MPTTester mptAlice(env, alice, {.holders = {bob}});
2708 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
2709 mptAlice.authorize({.account = bob});
2710
2711 mptAlice.holderKeyUpdate({
2712 .account = bob,
2713 .flags = tfCancelRecovery,
2714 .err = tecNO_PERMISSION,
2715 });
2716 }
2717
2718 // carol exists as an account but was never authorized to hold this
2719 // issuance, so she has no MPToken for it at all.
2720 {
2721 Env env{*this, features};
2722 Account const alice("alice");
2723 Account const carol("carol");
2724 MPTTester mptAlice(env, alice);
2725 mptAlice.create({
2726 .ownerCount = 1,
2727 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2728 });
2729 env.fund(XRP(1000), carol);
2730 env.close();
2731
2732 Account const carolNewKey("carolNewKey");
2733 mptAlice.generateKeyPair(carolNewKey);
2734
2735 mptAlice.holderKeyUpdate({
2736 .account = carol,
2737 .holderPubKey = mptAlice.getPubKey(carolNewKey),
2738 .proof = gMakeZeroBuffer(1),
2739 .flags = tfHolderKeyRecovery,
2740 .err = tecOBJECT_NOT_FOUND,
2741 });
2742 }
2743
2744 // The holder has an MPToken but no confidential state yet - never
2745 // registered a key or converted anything.
2746 {
2747 Env env{*this, features};
2748 Account const alice("alice");
2749 Account const bob("bob");
2750 MPTTester mptAlice(env, alice, {.holders = {bob}});
2751 mptAlice.create({
2752 .ownerCount = 1,
2753 .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
2754 });
2755 mptAlice.authorize({.account = bob});
2756 mptAlice.pay(alice, bob, 100);
2757
2758 Account const bobNewKey("bobNewKey");
2759 mptAlice.generateKeyPair(bobNewKey);
2760
2761 mptAlice.holderKeyUpdate({
2762 .account = bob,
2763 .holderPubKey = mptAlice.getPubKey(bobNewKey),
2764 .proof = gMakeZeroBuffer(1),
2765 .flags = tfHolderKeyRecovery,
2766 .err = tecNO_PERMISSION,
2767 });
2768 }
2769
2770 // Submitting the holder's own current key as the "new" key is a no-op.
2771 {
2772 Env env{*this, features};
2773 Account const alice("alice");
2774 Account const bob("bob");
2775 ConfidentialEnv ct{
2776 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2777
2778 ct.mpt.holderKeyUpdate({
2779 .account = bob,
2780 .holderPubKey = ct.mpt.getPubKey(bob),
2781 .proof = gMakeZeroBuffer(1),
2782 .flags = tfHolderKeyRecovery,
2783 .err = tecDUPLICATE,
2784 });
2785 }
2786
2787 // A second Recovery-mode transaction must not silently overwrite an
2788 // already-pending RecoveryKey.
2789 {
2790 Env env{*this, features};
2791 Account const alice("alice");
2792 Account const bob("bob");
2793 ConfidentialEnv ct{
2794 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2795
2796 Account const bobRecoveryKey("bobRecoveryKey");
2797 ct.mpt.generateKeyPair(bobRecoveryKey);
2798 ct.mpt.holderKeyUpdate({
2799 .account = bob,
2800 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
2801 .proof = gMakeZeroBuffer(1),
2802 .flags = tfHolderKeyRecovery,
2803 });
2804
2805 Account const bobRecoveryKey2("bobRecoveryKey2");
2806 ct.mpt.generateKeyPair(bobRecoveryKey2);
2807 ct.mpt.holderKeyUpdate({
2808 .account = bob,
2809 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey2),
2810 .proof = gMakeZeroBuffer(1),
2811 .flags = tfHolderKeyRecovery,
2812 .err = tecNO_PERMISSION,
2813 });
2814 }
2815
2816 // bob never submitted a Recovery-mode transaction, so there is no
2817 // sfRecoveryKey to cancel.
2818 {
2819 Env env{*this, features};
2820 Account const alice("alice");
2821 Account const bob("bob");
2822 ConfidentialEnv ct{
2823 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2824
2825 ct.mpt.holderKeyUpdate({
2826 .account = bob,
2827 .flags = tfCancelRecovery,
2828 .err = tecNO_PERMISSION,
2829 });
2830 }
2831
2832 // Runs rotation, recovery, and cancel in sequence and expects each to
2833 // succeed.
2834 auto const allModesSucceed = [&](ConfidentialEnv& ct, Account const& bob) {
2835 Account const bobNewKey("bobNewKey");
2836 ct.mpt.generateKeyPair(bobNewKey);
2837
2838 auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
2839 BEAST_EXPECT(reEnc.has_value());
2840 if (!reEnc)
2841 return;
2842
2843 ct.mpt.holderKeyUpdate({
2844 .account = bob,
2845 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2846 .spendingCiphertext = reEnc->first,
2847 .inboxCiphertext = reEnc->second,
2848 .proof = gMakeZeroBuffer(1),
2849 .flags = tfHolderKeyRotation,
2850 });
2851
2852 Account const bobRecoveryKey("bobRecoveryKey");
2853 ct.mpt.generateKeyPair(bobRecoveryKey);
2854 ct.mpt.holderKeyUpdate({
2855 .account = bob,
2856 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
2857 .proof = gMakeZeroBuffer(1),
2858 .flags = tfHolderKeyRecovery,
2859 });
2860
2861 ct.mpt.holderKeyUpdate({
2862 .account = bob,
2863 .flags = tfCancelRecovery,
2864 });
2865 };
2866
2867 // Individual (per-holder) lock.
2868 {
2869 Env env{*this, features};
2870 Account const alice("alice");
2871 Account const bob("bob");
2872 ConfidentialEnv ct{
2873 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2874 ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock});
2875
2876 allModesSucceed(ct, bob);
2877 }
2878
2879 // Global (issuance-wide) lock.
2880 {
2881 Env env{*this, features};
2882 Account const alice("alice");
2883 Account const bob("bob");
2884 ConfidentialEnv ct{
2885 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2886 ct.mpt.set({.account = alice, .flags = tfMPTLock});
2887
2888 allModesSucceed(ct, bob);
2889 }
2890
2891 // A stale mirror epoch does not block rotation, recovery, or cancel:
2892 {
2893 Env env{*this, features};
2894 Account const alice("alice");
2895 Account const bob("bob");
2896 ConfidentialEnv ct{
2897 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2898
2899 // Rotate the issuer's key without refreshing bob's mirror,
2900 // leaving bob's mirror epoch stale relative to the issuance.
2901 Account const aliceNewIssuerKey("aliceNewIssuerKey");
2902 ct.mpt.generateKeyPair(aliceNewIssuerKey);
2903 ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(aliceNewIssuerKey)});
2904 BEAST_EXPECT(ct.mpt.checkKeyEpochs(1u, std::nullopt));
2905 BEAST_EXPECT(ct.mpt.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
2906
2907 allModesSucceed(ct, bob);
2908 }
2909 }
2910
2911 void
2913 {
2914 testcase("ConfidentialMPTHolderKeyUpdate doApply");
2915 using namespace test::jtx;
2916
2917 // Rotation mode updates the key and re-encrypted balances, bumps the
2918 // version, and clears any pending recovery.
2919 {
2920 Env env{*this, features};
2921 Account const alice("alice");
2922 Account const bob("bob");
2923 ConfidentialEnv ct{
2924 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
2925
2926 auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
2927
2928 Account const bobNewKey("bobNewKey");
2929 ct.mpt.generateKeyPair(bobNewKey);
2930
2931 auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
2932 BEAST_EXPECT(reEnc.has_value());
2933 if (!reEnc)
2934 return;
2935
2936 ct.mpt.holderKeyUpdate({
2937 .account = bob,
2938 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2939 .spendingCiphertext = reEnc->first,
2940 .inboxCiphertext = reEnc->second,
2941 .proof = gMakeZeroBuffer(1),
2942 .flags = tfHolderKeyRotation,
2943 });
2944
2945 auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
2946 if (!BEAST_EXPECT(sleMptoken))
2947 return;
2948
2949 auto const newPubKey = ct.mpt.getPubKey(bobNewKey);
2950 BEAST_EXPECT(
2951 newPubKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*newPubKey));
2952 BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
2953 BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion + 1);
2954
2955 // The rotated balances must still decrypt to the same amounts,
2956 // but now only under the NEW private key.
2957 auto const spendingCt =
2958 ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
2959 auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
2960 BEAST_EXPECT(spendingCt.has_value());
2961 BEAST_EXPECT(inboxCt.has_value());
2962 if (!spendingCt || !inboxCt)
2963 return;
2964
2965 auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
2966 auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
2967 BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
2968 BEAST_EXPECT(inboxAmt && *inboxAmt == 0);
2969 }
2970
2971 // Rotation mode re-encrypts both balances correctly when spending and
2972 // inbox are both non-zero and differ from each other.
2973 {
2974 Env env{*this, features};
2975 Account const alice("alice");
2976 Account const bob("bob");
2977 Account const carol("carol");
2978 ConfidentialEnv ct{
2979 env,
2980 alice,
2981 {{.account = bob, .payAmount = 100, .convertAmount = 40},
2982 {.account = carol, .payAmount = 100, .convertAmount = 50}}};
2983
2984 // carol sends 15 into bob's inbox, which is not merged into
2985 // spending, leaving bob with spending=40, inbox=15.
2986 ct.mpt.send({.account = carol, .dest = bob, .amt = 15});
2987
2988 Account const bobNewKey("bobNewKey");
2989 ct.mpt.generateKeyPair(bobNewKey);
2990
2991 auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
2992 BEAST_EXPECT(reEnc.has_value());
2993 if (!reEnc)
2994 return;
2995
2996 ct.mpt.holderKeyUpdate({
2997 .account = bob,
2998 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
2999 .spendingCiphertext = reEnc->first,
3000 .inboxCiphertext = reEnc->second,
3001 .proof = gMakeZeroBuffer(1),
3002 .flags = tfHolderKeyRotation,
3003 });
3004
3005 auto const spendingCt =
3006 ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending);
3007 auto const inboxCt = ct.mpt.getEncryptedBalance(bob, MPTTester::holderEncryptedInbox);
3008 BEAST_EXPECT(spendingCt.has_value());
3009 BEAST_EXPECT(inboxCt.has_value());
3010 if (!spendingCt || !inboxCt)
3011 return;
3012
3013 auto const spendingAmt = ct.mpt.decryptAmount(bobNewKey, *spendingCt);
3014 auto const inboxAmt = ct.mpt.decryptAmount(bobNewKey, *inboxCt);
3015 BEAST_EXPECT(spendingAmt && *spendingAmt == 40);
3016 BEAST_EXPECT(inboxAmt && *inboxAmt == 15);
3017 }
3018
3019 // Recovery mode only records the pending recovery key; the current
3020 // key, balances, and version are untouched.
3021 {
3022 Env env{*this, features};
3023 Account const alice("alice");
3024 Account const bob("bob");
3025 ConfidentialEnv ct{
3026 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
3027
3028 auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
3029 auto const prevSpending =
3030 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
3031 auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
3032 auto const prevKey = ct.mpt.getPubKey(bob);
3033
3034 Account const bobRecoveryKey("bobRecoveryKey");
3035 ct.mpt.generateKeyPair(bobRecoveryKey);
3036
3037 ct.mpt.holderKeyUpdate({
3038 .account = bob,
3039 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
3040 .proof = gMakeZeroBuffer(1),
3041 .flags = tfHolderKeyRecovery,
3042 });
3043
3044 auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
3045 if (!BEAST_EXPECT(sleMptoken))
3046 return;
3047
3048 auto const recoveryKey = ct.mpt.getPubKey(bobRecoveryKey);
3049 BEAST_EXPECT(
3050 sleMptoken->isFieldPresent(sfRecoveryKey) && recoveryKey &&
3051 strHex((*sleMptoken)[sfRecoveryKey]) == strHex(*recoveryKey));
3052
3053 BEAST_EXPECT(
3054 prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
3055 BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
3056 BEAST_EXPECT(
3057 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
3058 prevSpending);
3059 BEAST_EXPECT(
3060 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
3061 }
3062
3063 // Cancel mode clears the pending recovery key only; the current key,
3064 // balances, and version are untouched.
3065 {
3066 Env env{*this, features};
3067 Account const alice("alice");
3068 Account const bob("bob");
3069 ConfidentialEnv ct{
3070 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
3071
3072 Account const bobRecoveryKey("bobRecoveryKey");
3073 ct.mpt.generateKeyPair(bobRecoveryKey);
3074
3075 ct.mpt.holderKeyUpdate({
3076 .account = bob,
3077 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
3078 .proof = gMakeZeroBuffer(1),
3079 .flags = tfHolderKeyRecovery,
3080 });
3081
3082 auto const prevVersion = ct.mpt.getMPTokenVersion(bob);
3083 auto const prevSpending =
3084 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending);
3085 auto const prevInbox = ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox);
3086 auto const prevKey = ct.mpt.getPubKey(bob);
3087
3088 ct.mpt.holderKeyUpdate({
3089 .account = bob,
3090 .flags = tfCancelRecovery,
3091 });
3092
3093 auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
3094 if (!BEAST_EXPECT(sleMptoken))
3095 return;
3096
3097 BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
3098 BEAST_EXPECT(
3099 prevKey && strHex((*sleMptoken)[sfHolderEncryptionKey]) == strHex(*prevKey));
3100 BEAST_EXPECT(ct.mpt.getMPTokenVersion(bob) == prevVersion);
3101 BEAST_EXPECT(
3102 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending) ==
3103 prevSpending);
3104 BEAST_EXPECT(
3105 ct.mpt.getDecryptedBalance(bob, MPTTester::holderEncryptedInbox) == prevInbox);
3106 }
3107
3108 // Rotation mode clears a pending recovery key.
3109 {
3110 Env env{*this, features};
3111 Account const alice("alice");
3112 Account const bob("bob");
3113 ConfidentialEnv ct{
3114 env, alice, {{.account = bob, .payAmount = 100, .convertAmount = 40}}};
3115
3116 Account const bobRecoveryKey("bobRecoveryKey");
3117 ct.mpt.generateKeyPair(bobRecoveryKey);
3118 ct.mpt.holderKeyUpdate({
3119 .account = bob,
3120 .holderPubKey = ct.mpt.getPubKey(bobRecoveryKey),
3121 .proof = gMakeZeroBuffer(1),
3122 .flags = tfHolderKeyRecovery,
3123 });
3124
3125 {
3126 auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
3127 if (!BEAST_EXPECT(sleMptoken))
3128 return;
3129 BEAST_EXPECT(sleMptoken->isFieldPresent(sfRecoveryKey));
3130 }
3131
3132 Account const bobNewKey("bobNewKey");
3133 ct.mpt.generateKeyPair(bobNewKey);
3134
3135 auto const reEnc = reencryptHolderBalances(ct.mpt, bob, bob, bobNewKey);
3136 BEAST_EXPECT(reEnc.has_value());
3137 if (!reEnc)
3138 return;
3139
3140 ct.mpt.holderKeyUpdate({
3141 .account = bob,
3142 .holderPubKey = ct.mpt.getPubKey(bobNewKey),
3143 .spendingCiphertext = reEnc->first,
3144 .inboxCiphertext = reEnc->second,
3145 .proof = gMakeZeroBuffer(1),
3146 .flags = tfHolderKeyRotation,
3147 });
3148
3149 auto const sleMptoken = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id()));
3150 if (!BEAST_EXPECT(sleMptoken))
3151 return;
3152 BEAST_EXPECT(!sleMptoken->isFieldPresent(sfRecoveryKey));
3153 }
3154 }
3155
3156public:
3157 void
3169
3170 void
3171 run() override
3172 {
3173 using namespace test::jtx;
3174 FeatureBitset const all{testableAmendments()};
3175
3177 testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
3178
3180 testConfidentialMPTConvertEpoch(all - featureConfidentialMPTKeyRotation);
3184
3186 testConfidentialMPTMirrorUpdatePreflight(all - featureConfidentialMPTKeyRotation);
3187 testConfidentialMPTMirrorUpdatePreflight(all - featureConfidentialTransfer);
3192
3194 testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialMPTKeyRotation);
3195 testConfidentialMPTHolderKeyUpdatePreflight(all - featureConfidentialTransfer);
3198 }
3199};
3200
3201BEAST_DEFINE_TESTSUITE(ConfidentialMPTKeyRotation, app, xrpl);
3202
3203} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Like std::vector<char> but better.
Definition Buffer.h:19
void testConfidentialMPTMirrorUpdateDoApply(FeatureBitset features)
void testMPTokenIssuanceSetRotateAuditorKeyOnly(FeatureBitset features)
void testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(FeatureBitset features)
void testMPTokenIssuanceSetRegisterAuditorKeyLater(FeatureBitset features)
void testConfidentialMPTHolderKeyUpdateDoApply(FeatureBitset features)
void testConfidentialMPTMirrorUpdatePreflight(FeatureBitset features)
void testConfidentialMPTMirrorUpdatePreclaim(FeatureBitset features)
void testMPTokenIssuanceSetRotateIssuerKey(FeatureBitset features)
void testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(FeatureBitset features)
void testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(FeatureBitset features)
void testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(FeatureBitset features)
void testConfidentialMPTHolderKeyUpdatePreflight(FeatureBitset features)
void testConfidentialMPTHolderKeyUpdatePreclaim(FeatureBitset features)
void testMPTokenIssuanceSetRotateBothKeys(FeatureBitset features)
static std::optional< std::pair< Buffer, Buffer > > reencryptHolderBalances(test::jtx::MPTTester &mpt, test::jtx::Account const &holder, test::jtx::Account const &currentKey, test::jtx::Account const &newKey)
static void setupConfidentialIssuance(test::jtx::MPTTester &mpt, test::jtx::Account const &issuer, std::vector< test::jtx::Account > const &holders, std::vector< test::jtx::Account > const &keyOwners={}, std::uint32_t flags=tfMPTCanTransfer|tfMPTCanHoldConfidentialBalance)
FeatureBitset & set(UInt256 const &f, bool value=true)
Definition Feature.h:267
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawReplace(SLE::Ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:244
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:168
void mirrorUpdate(MPTMirrorUpdate const &arg=MPTMirrorUpdate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:2266
Buffer encryptAmount(Account const &account, uint64_t const amt, Buffer const &blindingFactor) const
Definition mpt.cpp:1914
void mergeInbox(MPTMergeInbox const &arg=MPTMergeInbox{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:2004
std::optional< Buffer > getEncryptedBalance(Account const &account, EncryptedBalanceType option=holderEncryptedInbox) const
Definition mpt.cpp:1136
std::optional< uint64_t > getDecryptedBalance(Account const &account, EncryptedBalanceType balanceType) const
Definition mpt.cpp:1962
bool checkMirrorEpochs(Account const &holder, std::optional< std::uint32_t > issuerKeyMirrorEpoch, std::optional< std::uint32_t > auditorKeyMirrorEpoch) const
Definition mpt.cpp:778
std::uint32_t generateKeyPair(Account const &account)
Definition mpt.cpp:1877
void holderKeyUpdate(MPTHolderKeyUpdate const &arg=MPTHolderKeyUpdate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:2295
std::uint32_t getMPTokenVersion(Account const account) const
Definition mpt.cpp:2097
MPTID const & issuanceID() const
Definition mpt.h:768
std::optional< uint64_t > decryptAmount(Account const &account, Buffer const &amt, std::optional< std::uint32_t > epoch=std::nullopt) const
Definition mpt.cpp:1929
void send(MPTConfidentialSend const &arg=MPTConfidentialSend{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:1432
std::optional< Buffer > getPubKey(Account const &account, std::optional< std::uint32_t > epoch=std::nullopt) const
Definition mpt.cpp:1902
void set(MPTSet const &set={}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:577
bool checkKeyEpochs(std::optional< std::uint32_t > issuerKeyEpoch, std::optional< std::uint32_t > auditorKeyEpoch) const
Definition mpt.cpp:767
T make_shared(T... args)
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr std::size_t kEcPubKeyLength
Length of EC public key (compressed).
Definition Protocol.h:485
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:480
constexpr std::size_t kEcEqualityProofLength
Length of compact equality proof.
Definition Protocol.h:546
constexpr std::uint32_t kMaxKeyEpoch
Maximum value a confidential MPT key epoch may reach.
Definition Protocol.h:556
Buffer generateBlindingFactor()
Generates a cryptographically secure blinding factor (size=xrpl::kEcBlindingFactorLength).
@ temBAD_CIPHERTEXT
Definition TER.h:134
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecNO_TARGET
Definition TER.h:312
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecBAD_PROOF
Definition TER.h:376
@ tecNO_PERMISSION
Definition TER.h:313
@ tecDUPLICATE
Definition TER.h:323
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:250