xrpld
Loading...
Searching...
No Matches
InvariantsPermissioned_test.cpp
1#include <test/app/invariants/InvariantsBase.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/amount.h>
5#include <test/jtx/permissioned_domains.h>
6#include <test/jtx/vault.h>
7#include <test/unit_test/SuiteJournal.h>
8
9#include <xrpl/basics/Slice.h>
10#include <xrpl/basics/base_uint.h>
11#include <xrpl/beast/unit_test/suite.h>
12#include <xrpl/beast/utility/Journal.h>
13#include <xrpl/ledger/ApplyView.h>
14#include <xrpl/ledger/OpenView.h>
15#include <xrpl/protocol/Book.h>
16#include <xrpl/protocol/Feature.h>
17#include <xrpl/protocol/Indexes.h>
18#include <xrpl/protocol/Issue.h>
19#include <xrpl/protocol/Keylet.h>
20#include <xrpl/protocol/LedgerFormats.h>
21#include <xrpl/protocol/Protocol.h>
22#include <xrpl/protocol/Rules.h>
23#include <xrpl/protocol/SField.h>
24#include <xrpl/protocol/STAmount.h>
25#include <xrpl/protocol/STArray.h>
26#include <xrpl/protocol/STLedgerEntry.h>
27#include <xrpl/protocol/STObject.h>
28#include <xrpl/protocol/STTx.h>
29#include <xrpl/protocol/SeqProxy.h>
30#include <xrpl/protocol/TER.h>
31#include <xrpl/protocol/TxFormats.h>
32#include <xrpl/protocol/XRPAmount.h>
33#include <xrpl/tx/ApplyContext.h>
34#include <xrpl/tx/applySteps.h>
35#include <xrpl/tx/invariants/DirectoryInvariant.h>
36#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
37
38#include <cstddef>
39#include <cstdint>
40#include <functional>
41#include <initializer_list>
42#include <memory>
43#include <optional>
44#include <string>
45#include <utility>
46#include <vector>
47
48namespace xrpl::test {
49
51{
53
54 void
56 {
57 using namespace test::jtx;
58
59 bool const fixEnabled = features[fixCleanup3_1_3];
62
63 testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
64
66 makeEnv(features),
67 {{"permissioned domain with no rules."}},
68 [](Account const& a1, Account const& a2, ApplyContext& ac) {
69 return createPermissionedDomain(ac, a1, a2, 0).get();
70 },
71 XRPAmount{},
72 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
73 fixEnabled ? failTers : badTers);
74
75 testcase << "PermissionedDomain 2";
76
77 static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
79 makeEnv(features),
80 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
81 [](Account const& a1, Account const& a2, ApplyContext& ac) {
82 return !!createPermissionedDomain(ac, a1, a2, kTooBig);
83 },
84 XRPAmount{},
85 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
86 fixEnabled ? failTers : badTers);
87
88 testcase << "PermissionedDomain 3";
90 makeEnv(features),
91 {{"permissioned domain credentials aren't sorted"}},
92 [](Account const& a1, Account const& a2, ApplyContext& ac) {
93 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
94
95 STArray credentials(sfAcceptedCredentials, 2);
96 for (std::size_t n = 0; n < 2; ++n)
97 {
98 auto cred = STObject::makeInnerObject(sfCredential);
99 cred.setAccountID(sfIssuer, a2);
100 auto credType = std::string("cred_type") + std::to_string(9 - n);
101 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
102 credentials.pushBack(std::move(cred));
103 }
104 slePd->setFieldArray(sfAcceptedCredentials, credentials);
105 ac.view().update(slePd);
106 return true;
107 },
108 XRPAmount{},
109 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
110 fixEnabled ? failTers : badTers);
111
112 testcase << "PermissionedDomain 4";
114 makeEnv(features),
115 {{"permissioned domain credentials aren't unique"}},
116 [](Account const& a1, Account const& a2, ApplyContext& ac) {
117 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
118
119 STArray credentials(sfAcceptedCredentials, 2);
120 for (std::size_t n = 0; n < 2; ++n)
121 {
122 auto cred = STObject::makeInnerObject(sfCredential);
123 cred.setAccountID(sfIssuer, a2);
124 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
125 credentials.pushBack(std::move(cred));
126 }
127 slePd->setFieldArray(sfAcceptedCredentials, credentials);
128 ac.view().update(slePd);
129 return true;
130 },
131 XRPAmount{},
132 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
133 fixEnabled ? failTers : badTers);
134
135 testcase << "PermissionedDomain Set 1";
137 makeEnv(features),
138 {{"permissioned domain with no rules."}},
139 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
140 // create PD
141 auto slePd = createPermissionedDomain(ac, a1, a2);
142
143 // update PD with empty rules
144 {
145 STArray const credentials(sfAcceptedCredentials, 2);
146 slePd->setFieldArray(sfAcceptedCredentials, credentials);
147 ac.view().update(slePd);
148 }
149
150 return true;
151 },
152 XRPAmount{},
153 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
154 fixEnabled ? failTers : badTers);
155
156 testcase << "PermissionedDomain Set 2";
158 makeEnv(features),
159 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
160 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
161 // create PD
162 auto slePd = createPermissionedDomain(ac, a1, a2);
163
164 // update PD
165 {
166 STArray credentials(sfAcceptedCredentials, kTooBig);
167
168 for (std::size_t n = 0; n < kTooBig; ++n)
169 {
170 auto cred = STObject::makeInnerObject(sfCredential);
171 cred.setAccountID(sfIssuer, a2);
172 auto credType = "cred_type2" + std::to_string(n);
173 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
174 credentials.pushBack(std::move(cred));
175 }
176
177 slePd->setFieldArray(sfAcceptedCredentials, credentials);
178 ac.view().update(slePd);
179 }
180
181 return true;
182 },
183 XRPAmount{},
184 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
185 fixEnabled ? failTers : badTers);
186
187 testcase << "PermissionedDomain Set 3";
189 makeEnv(features),
190 {{"permissioned domain credentials aren't sorted"}},
191 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
192 // create PD
193 auto slePd = createPermissionedDomain(ac, a1, a2);
194
195 // update PD
196 {
197 STArray credentials(sfAcceptedCredentials, 2);
198 for (std::size_t n = 0; n < 2; ++n)
199 {
200 auto cred = STObject::makeInnerObject(sfCredential);
201 cred.setAccountID(sfIssuer, a2);
202 auto credType = std::string("cred_type2") + std::to_string(9 - n);
203 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
204 credentials.pushBack(std::move(cred));
205 }
206
207 slePd->setFieldArray(sfAcceptedCredentials, credentials);
208 ac.view().update(slePd);
209 }
210
211 return true;
212 },
213 XRPAmount{},
214 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
215 fixEnabled ? failTers : badTers);
216
217 testcase << "PermissionedDomain Set 4";
219 makeEnv(features),
220 {{"permissioned domain credentials aren't unique"}},
221 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
222 // create PD
223 auto slePd = createPermissionedDomain(ac, a1, a2);
224
225 // update PD
226 {
227 STArray credentials(sfAcceptedCredentials, 2);
228 for (std::size_t n = 0; n < 2; ++n)
229 {
230 auto cred = STObject::makeInnerObject(sfCredential);
231 cred.setAccountID(sfIssuer, a2);
232 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
233 credentials.pushBack(std::move(cred));
234 }
235 slePd->setFieldArray(sfAcceptedCredentials, credentials);
236 ac.view().update(slePd);
237 }
238
239 return true;
240 },
241 XRPAmount{},
242 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
243 fixEnabled ? failTers : badTers);
244
246
247 std::vector<std::string> const badMoreThan1{
248 {"transaction affected more than 1 permissioned domain entry."}};
249 std::vector<std::string> const emptyV;
250 std::vector<std::string> const badNoDomains{{"no domain objects affected by"}};
251 std::vector<std::string> const badNotDeleted{
252 {"domain object modified, but not deleted by "}};
253 std::vector<std::string> const badDeleted{{"domain object deleted by"}};
254 std::vector<std::string> const badTx{
255 {"domain object(s) affected by an unauthorized transaction."}};
256
257 {
258 testcase << "PermissionedDomain set 2 domains ";
260 makeEnv(features),
261 fixEnabled ? badMoreThan1 : emptyV,
262 [](Account const& a1, Account const& a2, ApplyContext& ac) {
263 createPermissionedDomain(ac, a1, a2);
264 createPermissionedDomain(ac, a1, a2, 2, 11);
265 return true;
266 },
267 XRPAmount{},
268 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
269 fixEnabled ? failTers : goodTers);
270 }
271
272 {
273 testcase << "PermissionedDomain del 2 domains";
274
275 Env env1(*this, features);
276
277 Account const a1{"A1"};
278 Account const a2{"A2"};
279 env1.fund(XRP(1000), a1, a2);
280 env1.close();
281
282 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
283 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
284 env1.close();
285
287 std::move(env1),
288 a1,
289 a2,
290 fixEnabled ? badMoreThan1 : emptyV,
291 [&pd1, &pd2](Account const&, Account const&, ApplyContext& ac) {
292 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
293 auto sle2 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd2});
294 ac.view().erase(sle1);
295 ac.view().erase(sle2);
296 return true;
297 },
298 XRPAmount{},
299 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
300 fixEnabled ? failTers : goodTers);
301 }
302
303 {
304 testcase << "PermissionedDomain set 0 domains ";
306 makeEnv(features),
307 fixEnabled ? badNoDomains : emptyV,
308 [](Account const&, Account const&, ApplyContext&) { return true; },
309 XRPAmount{},
310 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
311 fixEnabled ? badTers : goodTers);
312 }
313
314 {
315 testcase << "PermissionedDomain del 0 domains";
316
317 Env env1(*this, features);
318
319 Account const a1{"A1"};
320 Account const a2{"A2"};
321 env1.fund(XRP(1000), a1, a2);
322 env1.close();
323
324 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
325 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
326 env1.close();
327
329 std::move(env1),
330 a1,
331 a2,
332 fixEnabled ? badNoDomains : emptyV,
333 [](Account const&, Account const&, ApplyContext&) { return true; },
334 XRPAmount{},
335 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
336 fixEnabled ? badTers : goodTers);
337 }
338
339 {
340 testcase << "PermissionedDomain set, delete domain";
341
342 Env env1(*this, features);
343
344 Account const a1{"A1"};
345 Account const a2{"A2"};
346 env1.fund(XRP(1000), a1, a2);
347 env1.close();
348
349 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
350 env1.close();
351
353 std::move(env1),
354 a1,
355 a2,
356 fixEnabled ? badDeleted : emptyV,
357 [&pd1](Account const&, Account const&, ApplyContext& ac) {
358 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
359 ac.view().erase(sle1);
360 return true;
361 },
362 XRPAmount{},
363 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
364 fixEnabled ? failTers : goodTers);
365 }
366
367 {
368 testcase << "PermissionedDomain del, create domain ";
370 makeEnv(features),
371 fixEnabled ? badNotDeleted : emptyV,
372 [](Account const& a1, Account const& a2, ApplyContext& ac) {
373 createPermissionedDomain(ac, a1, a2);
374 return true;
375 },
376 XRPAmount{},
377 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
378 fixEnabled ? failTers : goodTers);
379 }
380
381 {
382 testcase << "PermissionedDomain invalid tx";
383
385 fixEnabled ? badTx : emptyV,
386 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
387 createPermissionedDomain(ac, a1, a2);
388 return true;
389 },
390 XRPAmount{},
391 STTx{ttPAYMENT, [](STObject&) {}},
392 failTers);
393 }
394 }
395
396 void
398 {
399 using namespace test::jtx;
400
401 bool const fixEnabled = features[fixCleanup3_1_3];
402
403 testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
404
406 makeEnv(features),
407 {{"domain doesn't exist"}},
408 [](Account const& a1, Account const&, ApplyContext& ac) {
409 Keylet const offerKey = keylet::offer(a1.id(), SeqProxy::rawSequence(10));
410 auto sleOffer = std::make_shared<SLE>(offerKey);
411 sleOffer->setAccountID(sfAccount, a1);
412 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
413 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
414 ac.view().insert(sleOffer);
415 return true;
416 },
417 XRPAmount{},
418 STTx{
419 ttOFFER_CREATE,
420 [](STObject& tx) {
421 tx.setFieldH256(
422 sfDomainID,
423 UInt256{"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33"
424 "70F3649CE134E5"});
425 Account const a1{"A1"};
426 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
427 tx.setFieldAmount(sfTakerGets, XRP(1));
428 }},
430
431 // missing domain ID in offer object
433 makeEnv(features),
434 {{"hybrid offer is malformed"}},
435 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
436 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
437 auto sleOffer = std::make_shared<SLE>(offerKey);
438 sleOffer->setAccountID(sfAccount, a2);
439 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
440 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
441 sleOffer->setFlag(lsfHybrid);
442
443 STArray bookArr;
444 bookArr.pushBack(STObject::makeInnerObject(sfBook));
445 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
446 ac.view().insert(sleOffer);
447 return true;
448 },
449 XRPAmount{},
450 STTx{ttOFFER_CREATE, [&](STObject&) {}},
452
453 // more than one entry in sfAdditionalBooks
454 {
455 Env env1(*this, features);
456
457 Account const a1{"A1"};
458 Account const a2{"A2"};
459 env1.fund(XRP(1000), a1, a2);
460 env1.close();
461
462 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
463 env1.close();
464
466 std::move(env1),
467 a1,
468 a2,
469 {{"hybrid offer is malformed"}},
470 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
471 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
472 auto sleOffer = std::make_shared<SLE>(offerKey);
473 sleOffer->setAccountID(sfAccount, a2);
474 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
475 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
476 sleOffer->setFlag(lsfHybrid);
477 sleOffer->setFieldH256(sfDomainID, pd1);
478
479 STArray bookArr;
480 bookArr.pushBack(STObject::makeInnerObject(sfBook));
481 bookArr.pushBack(STObject::makeInnerObject(sfBook));
482 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
483 ac.view().insert(sleOffer);
484 return true;
485 },
486 XRPAmount{},
487 STTx{ttOFFER_CREATE, [&](STObject&) {}},
489 }
490
491 // empty sfAdditionalBooks (size 0)
492 {
493 Env env1(*this, features);
494
495 Account const a1{"A1"};
496 Account const a2{"A2"};
497 env1.fund(XRP(1000), a1, a2);
498 env1.close();
499
500 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
501 env1.close();
502
504 std::move(env1),
505 a1,
506 a2,
507 fixEnabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
509 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
510 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
511 auto sleOffer = std::make_shared<SLE>(offerKey);
512 sleOffer->setAccountID(sfAccount, a2);
513 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
514 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
515 sleOffer->setFlag(lsfHybrid);
516 sleOffer->setFieldH256(sfDomainID, pd1);
517
518 STArray const bookArr; // empty array, size 0
519 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
520 ac.view().insert(sleOffer);
521 return true;
522 },
523 XRPAmount{},
524 STTx{ttOFFER_CREATE, [&](STObject&) {}},
527 }
528
529 // hybrid offer missing sfAdditionalBooks
530 {
531 Env env1(*this, features);
532
533 Account const a1{"A1"};
534 Account const a2{"A2"};
535 env1.fund(XRP(1000), a1, a2);
536 env1.close();
537
538 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
539 env1.close();
540
542 std::move(env1),
543 a1,
544 a2,
545 {{"hybrid offer is malformed"}},
546 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
547 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
548 auto sleOffer = std::make_shared<SLE>(offerKey);
549 sleOffer->setAccountID(sfAccount, a2);
550 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
551 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
552 sleOffer->setFlag(lsfHybrid);
553 sleOffer->setFieldH256(sfDomainID, pd1);
554 ac.view().insert(sleOffer);
555 return true;
556 },
557 XRPAmount{},
558 STTx{ttOFFER_CREATE, [&](STObject&) {}},
560 }
561
562 {
563 Env env1(*this, features);
564
565 Account const a1{"A1"};
566 Account const a2{"A2"};
567 env1.fund(XRP(1000), a1, a2);
568 env1.close();
569
570 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
571 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
572 env1.close();
573
575 std::move(env1),
576 a1,
577 a2,
578 {{"transaction consumed wrong domains"}},
579 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
580 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
581 auto sleOffer = std::make_shared<SLE>(offerKey);
582 sleOffer->setAccountID(sfAccount, a2);
583 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
584 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
585 sleOffer->setFieldH256(sfDomainID, pd1);
586 ac.view().insert(sleOffer);
587 return true;
588 },
589 XRPAmount{},
590 STTx{
591 ttOFFER_CREATE,
592 [&pd2, &a1](STObject& tx) {
593 tx.setFieldH256(sfDomainID, pd2);
594 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
595 tx.setFieldAmount(sfTakerGets, XRP(1));
596 }},
598 }
599
600 {
601 Env env1(*this, features);
602
603 Account const a1{"A1"};
604 Account const a2{"A2"};
605 env1.fund(XRP(1000), a1, a2);
606 env1.close();
607
608 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
609 env1.close();
610
612 std::move(env1),
613 a1,
614 a2,
615 {{"domain transaction affected regular offers"}},
616 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
617 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
618 auto sleOffer = std::make_shared<SLE>(offerKey);
619 sleOffer->setAccountID(sfAccount, a2);
620 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
621 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
622 ac.view().insert(sleOffer);
623 return true;
624 },
625 XRPAmount{},
626 STTx{
627 ttOFFER_CREATE,
628 [&](STObject& tx) {
629 Account const a1{"A1"};
630 tx.setFieldH256(sfDomainID, pd1);
631 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
632 tx.setFieldAmount(sfTakerGets, XRP(1));
633 }},
635 }
636 }
637
638 void
640 {
641 using namespace test::jtx;
642
643 testcase << "PermissionedDEX null after";
644
645 // Tx is OfferCreate on pd2. Tracking pd1 fails the invariant iff that
646 // domain lands in the set finalize consults. after == null is never
647 // tracked (pre-340: after-only; post-340: early return) — same result,
648 // both sides are coverage/regression that we do not fall back to before.
649 auto const check = [this](
650 FeatureBitset features,
651 bool const afterIsNull,
652 bool const isDelete,
653 bool const expectInvariantFailure) {
654 Env env(*this, features);
655
656 Account const a1{"A1"};
657 Account const a2{"A2"};
658 env.fund(XRP(1000), a1, a2);
659 env.close();
660
661 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env, a1, a2);
662 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env, a1, a2);
663 env.close();
664
665 auto sleOffer =
667 sleOffer->setAccountID(sfAccount, a2);
668 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
669 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
670 sleOffer->setFieldH256(sfDomainID, pd1);
671
672 CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
673
674 ValidPermissionedDEX invariant;
675 if (afterIsNull)
676 {
677 // Defensive path: after is null. Must not fall back to before.
678 invariant.visitEntry(isDelete, sleOffer, nullptr);
679 }
680 else
681 {
682 // Normal / real-erase path: after is the offer on pd1.
683 invariant.visitEntry(isDelete, nullptr, sleOffer);
684 }
685
686 STTx const tx{ttOFFER_CREATE, [&pd2, &a1](STObject& tx) {
687 tx.setFieldH256(sfDomainID, pd2);
688 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
689 tx.setFieldAmount(sfTakerGets, XRP(1));
690 }};
691
693 beast::Journal const jlog{sink};
694 bool const passed =
695 invariant.finalize(tx, tesSUCCESS, XRPAmount{}, *env.current(), jlog);
696 BEAST_EXPECT(passed != expectInvariantFailure);
697 if (expectInvariantFailure)
698 {
699 BEAST_EXPECT(sink.messages().str().contains("transaction consumed wrong domains"));
700 }
701 else
702 {
703 BEAST_EXPECT(sink.messages().str().empty());
704 }
705 };
706
707 auto const pre = all_ - fixCleanup3_4_0;
708 auto const post = all_;
709
710 // after == null: not tracked
711 check(pre, true, true, false);
712 check(post, true, true, false);
713
714 // after == offer on pd1
715 // pre-340: domainsOld_ (delete still inserted) → fail
716 check(pre, false, true, true);
717 // post-340: isDelete → only domainsOld_ → pass; !isDelete → domains_ → fail
718 check(post, false, true, false);
719 check(post, false, false, true);
720 }
721
722 void
724 {
725 using namespace test::jtx;
726 testcase << "book directory exchange rate";
727
728 auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
729 Book const book{xrpIssue(), account["USD"], std::nullopt};
730 return keylet::quality(keylet::book(book), quality);
731 };
732
733 // Root book-directory pages carry exchange-rate metadata that must
734 // match the quality encoded in the directory key.
735 auto const makeRootPage = [](Keylet const& dir, std::uint64_t exchangeRate) {
736 auto sleDir = std::make_shared<SLE>(dir);
737 sleDir->setFieldH256(sfRootIndex, dir.key);
738 STVector256 indexes;
739 indexes.pushBack(UInt256{1});
740 sleDir->setFieldV256(sfIndexes, indexes);
741 sleDir->setFieldU64(sfExchangeRate, exchangeRate);
742 return sleDir;
743 };
744
745 // Child pages do not carry quality metadata; they only point back to
746 // the root directory.
747 auto const makeChildPage = [](Keylet const& rootDir) {
748 auto sleDir = std::make_shared<SLE>(keylet::page(rootDir, 1));
749 sleDir->setFieldH256(sfRootIndex, rootDir.key);
750 STVector256 indexes;
751 indexes.pushBack(UInt256{2});
752 sleDir->setFieldV256(sfIndexes, indexes);
753 return sleDir;
754 };
755
756 auto const makeOfferCreateTx = [] {
757 return STTx{ttOFFER_CREATE, [](STObject& tx) {
758 Account const account{"A1"};
759 tx.setFieldAmount(sfTakerPays, XRP(1));
760 tx.setFieldAmount(sfTakerGets, account["USD"](1));
761 }};
762 };
764
765 // Creating a root book directory with mismatched exchange-rate
766 // metadata violates the invariant.
768 {{"book directory exchange rate does not match directory quality"}},
769 [&](Account const& a1, Account const&, ApplyContext& ac) {
770 auto const directoryQuality = STAmount::kURateOne;
771 auto const dir = getBookRootKey(a1, directoryQuality);
772 ac.view().insert(makeRootPage(dir, directoryQuality + 1));
773 return true;
774 },
775 XRPAmount{},
776 makeOfferCreateTx(),
777 failTers);
778
779 // A new child page must point to an existing root page.
781 {{"book directory root missing"}},
782 [&](Account const& a1, Account const&, ApplyContext& ac) {
783 auto const directoryQuality = STAmount::kURateOne;
784 auto const rootDir = getBookRootKey(a1, directoryQuality);
785 // Insert only the child page. It points at rootDir, but the
786 // corresponding root page is intentionally missing.
787 ac.view().insert(makeChildPage(rootDir));
788 return true;
789 },
790 XRPAmount{},
791 makeOfferCreateTx(),
792 failTers);
793
794 // Legacy bad-root tolerance:
795 // - The view contains a pre-existing root page with bad sfExchangeRate
796 // metadata.
797 // - The simulated transaction only creates a child page pointing to
798 // that root.
799 // - The invariant must pass because this transaction did not create
800 // the bad root, only adding a child page.
801 {
802 Env env{*this, all_};
803 Account const a1{"A1"};
804 env.fund(XRP(1000), a1);
805 env.close();
806
807 OpenView view{*env.current()};
808 auto const directoryQuality = STAmount::kURateOne;
809 auto const rootDir = getBookRootKey(a1, directoryQuality);
810 view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
811
812 ValidBookDirectory invariant;
813 invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
814
816 beast::Journal const jlog{sink};
817 BEAST_EXPECT(
818 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
819 }
820
821 // A bad root is rejected when added, ignored when a legacy bad root is
822 // modified without changing sfRootIndex or deleted, and checked when a
823 // modified directory changes sfRootIndex.
824 {
825 Env env{*this, all_};
826 Account const a1{"A1"};
827 env.fund(XRP(1000), a1);
828 env.close();
829
830 OpenView view{*env.current()};
831 auto const directoryQuality = STAmount::kURateOne;
832 auto const rootDir = getBookRootKey(a1, directoryQuality);
833 auto const missingRootDir = getBookRootKey(a1, directoryQuality + 1);
834 auto const badRoot = makeRootPage(rootDir, directoryQuality + 1);
835 view.rawInsert(badRoot);
836
838 beast::Journal const jlog{sink};
839
840 {
841 // add
842 ValidBookDirectory invariant;
843 invariant.visitEntry(false, nullptr, badRoot);
844
845 BEAST_EXPECT(
846 !invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
847 }
848 {
849 // modify (without changing the sfRootIndex)
850 ValidBookDirectory invariant;
851 invariant.visitEntry(false, badRoot, badRoot);
852
853 BEAST_EXPECT(
854 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
855 }
856 {
857 // modify (changing sfRootIndex to a missing root)
858 auto const childBefore = makeChildPage(rootDir);
859 auto const childAfter = std::make_shared<SLE>(*childBefore, childBefore->key());
860 childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
861
862 ValidBookDirectory invariant;
863 invariant.visitEntry(false, childBefore, childAfter);
864
866 beast::Journal const missingRootJlog{missingRootSink};
867 BEAST_EXPECT(!invariant.finalize(
868 makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, missingRootJlog));
869 BEAST_EXPECT(
870 missingRootSink.messages().str().contains("book directory root missing"));
871 }
872 {
873 // delete
874 view.rawErase(badRoot);
875 BEAST_EXPECT(!view.exists(rootDir));
876
877 ValidBookDirectory invariant;
878 invariant.visitEntry(true, badRoot, badRoot);
879 BEAST_EXPECT(
880 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
881 }
882 }
883 }
884
885 static SLE::pointer
887 ApplyContext& ac,
888 test::jtx::Account const& a1,
889 test::jtx::Account const& a2,
890 std::uint32_t numCreds = 2,
891 std::uint32_t seq = 10)
892 {
893 Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), SeqProxy::rawSequence(seq));
894 auto sle = std::make_shared<SLE>(pdKeylet);
895
896 sle->setAccountID(sfOwner, a1);
897 sle->setFieldU32(sfSequence, seq);
898
899 if (numCreds != 0u)
900 {
901 // This array is sorted naturally, but if you are going to change
902 // this behavior, don't forget to use credentials::makeSorted
903 STArray credentials(sfAcceptedCredentials, numCreds);
904 for (std::size_t n = 0; n < numCreds; ++n)
905 {
906 auto cred = STObject::makeInnerObject(sfCredential);
907 cred.setAccountID(sfIssuer, a2);
908 auto credType = "cred_type" + std::to_string(n);
909 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
910 credentials.pushBack(std::move(cred));
911 }
912 sle->setFieldArray(sfAcceptedCredentials, credentials);
913 }
914
915 ac.view().insert(sle);
916 return sle;
917 }
918
921 test::jtx::Env& env,
922 test::jtx::Account const& a1,
923 test::jtx::Account const& a2,
924 std::uint32_t numCreds = 2)
925 {
926 using namespace test::jtx;
927
929
930 for (std::size_t n = 0; n < numCreds; ++n)
931 {
932 auto credType = "cred_type" + std::to_string(n);
933 credentials.push_back({.issuer = a2, .credType = credType});
934 }
935
936 std::uint32_t const seq = env.seq(a1);
937 env(pdomain::setTx(a1, credentials));
938 UInt256 const key = pdomain::getNewDomain(env.meta());
939
940 return {seq, key};
941 }
942
943 void
953};
954
955BEAST_DEFINE_TESTSUITE(InvariantsPermissioned, app, xrpl);
956
957} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
State information when applying a tx.
ApplyView & view()
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void insert(SLE::Ref sle)=0
Insert a new state SLE.
virtual void erase(SLE::Ref sle)=0
Remove a peeked SLE.
Specifies an order book.
Definition Book.h:28
RAII class to set and restore the current transaction rules.
Definition Rules.h:113
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawInsert(SLE::Ref sle) override
Unconditionally insert a state item.
Definition OpenView.cpp:238
bool exists(Keylet const &k) const override
Determine if a state item exists.
Definition OpenView.cpp:155
void rawErase(SLE::Ref sle) override
Delete an existing state item.
Definition OpenView.cpp:232
static std::uint64_t const kURateOne
Definition STAmount.h:78
void pushBack(STObject const &object)
Definition STArray.h:212
std::shared_ptr< STLedgerEntry > pointer
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:803
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:79
void setFieldH256(SField const &field, UInt256 const &)
Definition STObject.cpp:767
void pushBack(UInt256 const &v)
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
An immutable linear range of bytes.
Definition Slice.h:28
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
test::jtx::Env makeEnv(FeatureBitset features)
void doInvariantCheck(std::vector< std::string > const &expectLogs, Precheck const &precheck, XRPAmount fee=XRPAmount{}, STTx tx=STTx{ttACCOUNT_SET, [](STObject &) {}}, std::initializer_list< TER > ters={tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const &preclose={}, TxAccount setTxAccount=TxAccount::None, std::source_location const &loc=std::source_location::current(), TER initialResult=tesSUCCESS)
Run a specific test case to put the ledger into a state that will be detected by an invariant.
static std::pair< std::uint32_t, UInt256 > createPermissionedDomainEnv(test::jtx::Env &env, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2)
static SLE::pointer createPermissionedDomain(ApplyContext &ac, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2, std::uint32_t seq=10)
void testPermissionedDomainInvariants(FeatureBitset features)
std::stringstream const & messages() const
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
T get(T... args)
T make_shared(T... args)
Keylet quality(Keylet const &k, std::uint64_t const q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:304
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:298
Keylet book(Book const &b)
The beginning of an order book.
Definition Indexes.cpp:269
Keylet permissionedDomain(AccountID const &account, SeqProxy const &seq) noexcept
Definition Indexes.cpp:609
Keylet page(UInt256 const &root, std::uint64_t const index=0) noexcept
A page in a directory.
Definition Indexes.cpp:409
Check operations.
Definition check.h:18
std::vector< Credential > Credentials
UInt256 getNewDomain(std::shared_ptr< STObject const > const &meta)
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< UInt256 > domain)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:92
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
@ tefINVARIANT_FAILED
Definition TER.h:178
BaseUInt< 256 > UInt256
Definition base_uint.h:580
constexpr std::size_t kMaxPermissionedDomainCredentialsArraySize
The maximum number of credentials can be passed in array for permissioned domain.
Definition Protocol.h:287
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tesSUCCESS
Definition TER.h:250
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
T to_string(T... args)