xrpld
Loading...
Searching...
No Matches
InvariantCheck.cpp
1#include <xrpl/tx/invariants/InvariantCheck.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/beast/utility/instrumentation.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/helpers/AccountRootHelpers.h>
10#include <xrpl/ledger/helpers/TokenHelpers.h>
11#include <xrpl/protocol/AccountID.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/Indexes.h>
14#include <xrpl/protocol/Issue.h>
15#include <xrpl/protocol/Keylet.h>
16#include <xrpl/protocol/LedgerFormats.h>
17#include <xrpl/protocol/MPTIssue.h>
18#include <xrpl/protocol/Protocol.h>
19#include <xrpl/protocol/Rules.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/STLedgerEntry.h>
23#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
24#include <xrpl/protocol/STTx.h>
25#include <xrpl/protocol/SystemParameters.h>
26#include <xrpl/protocol/TER.h>
27#include <xrpl/protocol/TxFormats.h>
28#include <xrpl/protocol/TxSettings.h>
29#include <xrpl/protocol/UintTypes.h>
30#include <xrpl/protocol/XRPAmount.h>
31#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
32
33#include <algorithm>
34#include <cstdint>
35#include <functional>
36#include <memory>
37#include <optional>
38#include <sstream>
39#include <string>
40#include <vector>
41
42namespace xrpl {
43
44#pragma push_macro("UNWRAP")
45#undef UNWRAP
46#pragma push_macro("TRANSACTION")
47#undef TRANSACTION
48
49#define UNWRAP(...) __VA_ARGS__
50#define TRANSACTION(tag, value, name, settings, ...) \
51 case tag: { \
52 return ((TxSettings UNWRAP settings).privileges & priv) != Privilege::NoPriv; \
53 }
54
55bool
56hasPrivilege(STTx const& tx, Privilege priv)
57{
58 switch (tx.getTxnType())
59 {
60#include <xrpl/protocol/detail/transactions.macro>
61
62 // Deprecated types
63 default:
64 return false;
65 }
66};
67
68#undef TRANSACTION
69#pragma pop_macro("TRANSACTION")
70#undef UNWRAP
71#pragma pop_macro("UNWRAP")
72
73// Returns the human-readable name of a ledger entry's type, falling back to
74// the numeric type if the format is somehow unknown.
75static std::string
77{
78 auto const item = LedgerFormats::getInstance().findByType(sle.getType());
79
80 if (item == nullptr)
81 {
82 // LCOV_EXCL_START
83 UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format");
84 return std::to_string(sle.getType());
85 // LCOV_EXCL_STOP
86 }
87 return item->getName();
88}
89
90void
95
96bool
98 STTx const& tx,
99 TER const,
100 XRPAmount const fee,
101 ReadView const&,
102 beast::Journal const& j)
103{
104 // We should never charge a negative fee
105 if (fee.drops() < 0)
106 {
107 JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops();
108 return false;
109 }
110
111 // We should never charge a fee that's greater than or equal to the
112 // entire XRP supply.
113 if (fee >= kInitialXrp)
114 {
115 JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops();
116 return false;
117 }
118
119 // We should never charge more for a transaction than the transaction
120 // authorizes. It's possible to charge less in some circumstances.
121 if (fee > tx.getFieldAmount(sfFee).xrp())
122 {
123 JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
124 << " exceeds fee specified in transaction.";
125 return false;
126 }
127
128 return true;
129}
130
131//------------------------------------------------------------------------------
132
133void
135{
136 /* We go through all modified ledger entries, looking only at account roots,
137 * escrow payments, and payment channels. We remove from the total any
138 * previous XRP values and add to the total any new XRP values. The net
139 * balance of a payment channel is computed from two fields (amount and
140 * balance) and deletions are ignored for paychan and escrow because the
141 * amount fields have not been adjusted for those in the case of deletion.
142 */
143 if (before)
144 {
145 switch (before->getType())
146 {
147 case ltACCOUNT_ROOT:
148 drops_ -= (*before)[sfBalance].xrp().drops();
149 break;
150 case ltPAYCHAN:
151 drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
152 break;
153 case ltESCROW:
154 if (isXRP((*before)[sfAmount]))
155 drops_ -= (*before)[sfAmount].xrp().drops();
156 break;
157 case ltSPONSORSHIP:
158 if (before->isFieldPresent(sfFeeAmount))
159 {
160 XRPL_ASSERT(
161 isXRP((*before)[sfFeeAmount]),
162 "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP");
163 drops_ -= (*before)[sfFeeAmount].xrp().drops();
164 }
165 break;
166 default:
167 break;
168 }
169 }
170
171 if (!after)
172 {
173 // LCOV_EXCL_START
174 UNREACHABLE("xrpl::XRPNotCreated::visitEntry : after can't be null");
175 return;
176 // LCOV_EXCL_STOP
177 }
178 switch (after->getType())
179 {
180 case ltACCOUNT_ROOT:
181 drops_ += (*after)[sfBalance].xrp().drops();
182 break;
183 case ltPAYCHAN:
184 if (!isDelete)
185 drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
186 break;
187 case ltESCROW:
188 if (!isDelete && isXRP((*after)[sfAmount]))
189 drops_ += (*after)[sfAmount].xrp().drops();
190 break;
191 case ltSPONSORSHIP:
192 if (!isDelete && after->isFieldPresent(sfFeeAmount))
193 {
194 XRPL_ASSERT(
195 isXRP((*after)[sfFeeAmount]),
196 "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP");
197 drops_ += (*after)[sfFeeAmount].xrp().drops();
198 }
199 break;
200 default:
201 break;
202 }
203}
204
205bool
207 STTx const& tx,
208 TER const,
209 XRPAmount const fee,
210 ReadView const&,
211 beast::Journal const& j) const
212{
213 // The net change should never be positive, as this would mean that the
214 // transaction created XRP out of thin air. That's not possible.
215 if (drops_ > 0)
216 {
217 JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_;
218 return false;
219 }
220
221 // The negative of the net change should be equal to actual fee charged.
222 if (-drops_ != fee.drops())
223 {
224 JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee "
225 << fee.drops();
226 return false;
227 }
228
229 return true;
230}
231
232//------------------------------------------------------------------------------
233
234void
236{
237 auto isBad = [](STAmount const& balance) {
238 if (!balance.native())
239 return true;
240
241 auto const drops = balance.xrp();
242
243 // Can't have more than the number of drops instantiated
244 // in the genesis ledger.
245 if (drops > kInitialXrp)
246 return true;
247
248 // Can't have a negative balance (0 is OK)
249 if (drops < XRPAmount{0})
250 return true;
251
252 return false;
253 };
254
255 if (before && before->getType() == ltACCOUNT_ROOT)
256 bad_ |= isBad((*before)[sfBalance]);
257
258 if (after && after->getType() == ltACCOUNT_ROOT)
259 bad_ |= isBad((*after)[sfBalance]);
260}
261
262bool
264 STTx const&,
265 TER const,
266 XRPAmount const,
267 ReadView const&,
268 beast::Journal const& j) const
269{
270 if (bad_)
271 {
272 JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
273 return false;
274 }
275
276 return true;
277}
278
279//------------------------------------------------------------------------------
280
281void
283{
284 auto isBad = [](STAmount const& pays, STAmount const& gets) {
285 // An offer should never be negative
286 if (pays < beast::kZero)
287 return true;
288
289 if (gets < beast::kZero)
290 return true;
291
292 // Can't have an XRP to XRP offer:
293 return pays.native() && gets.native();
294 };
295
296 if (before && before->getType() == ltOFFER)
297 bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
298
299 if (after && after->getType() == ltOFFER)
300 bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
301}
302
303bool
305 STTx const&,
306 TER const,
307 XRPAmount const,
308 ReadView const&,
309 beast::Journal const& j) const
310{
311 if (bad_)
312 {
313 JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
314 return false;
315 }
316
317 return true;
318}
319
320//------------------------------------------------------------------------------
321
322void
324{
325 auto isBad = [](STAmount const& amount) {
326 // XRP case
327 if (amount.native())
328 {
329 if (amount.xrp() <= XRPAmount{0})
330 return true;
331
332 if (amount.xrp() >= kInitialXrp)
333 return true;
334 }
335 else
336 {
337 return amount.asset().visit(
338 [&](Issue const& issue) {
339 // IOU case
340 if (amount <= beast::kZero)
341 return true;
342
343 if (badCurrency() == issue.currency)
344 return true;
345
346 return false;
347 }
348
349 // MPT case
350 ,
351 [&](MPTIssue const&) {
352 if (amount <= beast::kZero)
353 return true;
354
355 if (amount.mpt() > MPTAmount{kMaxMpTokenAmount})
356 return true; // LCOV_EXCL_LINE
357
358 return false;
359 });
360 }
361 return false;
362 };
363
364 if (before && before->getType() == ltESCROW)
365 bad_ |= isBad((*before)[sfAmount]);
366
367 if (after && after->getType() == ltESCROW)
368 bad_ |= isBad((*after)[sfAmount]);
369
370 auto checkAmount = [this](std::int64_t amount) {
371 if (amount > kMaxMpTokenAmount || amount < 0)
372 bad_ |= true;
373 };
374
375 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
376
377 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
378 {
379 auto const outstanding = (*after)[sfOutstandingAmount];
380 checkAmount(outstanding);
381 if (auto const locked = (*after)[~sfLockedAmount])
382 {
383 checkAmount(*locked);
384 bool const isBad = outstanding < *locked;
385 if (overwriteFixEnabled)
386 {
387 bad_ |= isBad;
388 }
389 else
390 {
391 bad_ = isBad;
392 }
393 }
394 }
395
396 if (after && after->getType() == ltMPTOKEN)
397 {
398 auto const mptAmount = (*after)[sfMPTAmount];
399 checkAmount(mptAmount);
400 if (auto const locked = (*after)[~sfLockedAmount])
401 {
402 checkAmount(*locked);
403 }
404 }
405}
406
407bool
409 STTx const& txn,
410 TER const,
411 XRPAmount const,
412 ReadView const&,
413 beast::Journal const& j) const
414{
415 if (bad_)
416 {
417 JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
418 return false;
419 }
420
421 return true;
422}
423
424//------------------------------------------------------------------------------
425
426void
428{
429 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
431}
432
433bool
435 STTx const& tx,
436 TER const result,
437 XRPAmount const,
438 ReadView const&,
439 beast::Journal const& j) const
440{
441 // AMM account root can be deleted as the result of AMM withdraw/delete
442 // transaction when the total AMM LP Tokens balance goes to 0.
443 // A successful AccountDelete or AMMDelete MUST delete exactly
444 // one account root.
446 {
447 if (accountsDeleted_ == 1)
448 return true;
449
450 if (accountsDeleted_ == 0)
451 {
452 JLOG(j.fatal()) << "Invariant failed: account deletion "
453 "succeeded without deleting an account";
454 }
455 else
456 {
457 JLOG(j.fatal()) << "Invariant failed: account deletion "
458 "succeeded but deleted multiple accounts!";
459 }
460 return false;
461 }
462
463 // A successful AMMWithdraw/AMMClawback MAY delete one account root
464 // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
465 // deletes the AMM account, accountsDeleted_ is set if it is deleted.
467 return true;
468
469 if (accountsDeleted_ == 0)
470 return true;
471
472 JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
473 return false;
474}
475
476//------------------------------------------------------------------------------
477
478void
480{
481 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
482 accountsDeleted_.emplace_back(before, after);
483}
484
485bool
487 STTx const& tx,
488 TER const result,
489 XRPAmount const,
490 ReadView const& view,
491 beast::Journal const& j)
492{
493 // Always check for objects in the ledger, but to prevent differing
494 // transaction processing results, however unlikely, only fail if the
495 // feature is enabled. Enabled, or not, though, a fatal-level message will
496 // be logged
497 [[maybe_unused]] bool const enforce = view.rules().enabled(fixCleanup3_2_0) ||
498 view.rules().enabled(featureSponsor) || view.rules().enabled(featureSingleAssetVault) ||
499 view.rules().enabled(featureLendingProtocol);
500
501 auto const objectExists = [&view, enforce, &j](auto const& keylet) {
502 (void)enforce;
503 if (auto const sle = view.read(keylet))
504 {
505 // Finding the object is bad
506 JLOG(j.fatal()) << "Invariant failed: account deletion left behind a "
507 << ledgerEntryTypeName(*sle) << " object";
508 // The comment above starting with "assert(enforce)" explains this
509 // assert.
510 XRPL_ASSERT(
511 enforce,
512 "xrpl::AccountRootsDeletedClean::finalize::objectExists : "
513 "account deletion left no objects behind");
514 return true;
515 }
516 return false;
517 };
518
519 for (auto const& [before, after] : accountsDeleted_)
520 {
521 auto const accountID = before->getAccountID(sfAccount);
522 // An account should not be deleted with a balance
523 if (after->at(sfBalance) != beast::kZero)
524 {
525 JLOG(j.fatal()) << "Invariant failed: account deletion left "
526 "behind a non-zero balance";
527 XRPL_ASSERT(
528 enforce,
529 "xrpl::AccountRootsDeletedClean::finalize : "
530 "deleted account has zero balance");
531 if (enforce)
532 return false;
533 }
534 // An account should not be deleted with a non-zero owner count
535 if (after->at(sfOwnerCount) != 0)
536 {
537 JLOG(j.fatal()) << "Invariant failed: account deletion left "
538 "behind a non-zero owner count";
539 XRPL_ASSERT(
540 enforce,
541 "xrpl::AccountRootsDeletedClean::finalize : "
542 "deleted account has zero owner count");
543 if (enforce)
544 return false;
545 }
546 // An account should not be deleted with sponsorship fields
547 if (after->isFieldPresent(sfSponsoredOwnerCount) ||
548 after->isFieldPresent(sfSponsoringOwnerCount) ||
549 after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor))
550 {
551 JLOG(j.fatal()) << "Invariant failed: account deletion left "
552 "behind a sponsorship field";
553 XRPL_ASSERT(
554 enforce,
555 "xrpl::AccountRootsDeletedClean::finalize : "
556 "deleted account has no sponsorship fields");
557 if (enforce)
558 return false;
559 }
560 // Simple types
561 for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets)
562 {
563 // TODO: use '_' for both unused variables above once we are in C++26
564 if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
565 return false;
566 }
567
568 {
569 // NFT pages. nftpage_min and nftpage_max were already explicitly
570 // checked above as entries in directAccountKeylets. This uses
571 // view.succ() to check for any NFT pages in between the two
572 // endpoints.
573 Keylet const first = keylet::nftokenPageMin(accountID);
574 Keylet const last = keylet::nftokenPageMax(accountID);
575
576 std::optional<UInt256> key = view.succ(first.key, last.key.next());
577
578 // current page
579 if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
580 return false;
581 }
582
583 // If the account is a pseudo account, then the linked object must
584 // also be deleted. e.g. AMM, Vault, etc.
585 for (auto const& field : getPseudoAccountFields())
586 {
587 if (before->isFieldPresent(*field))
588 {
589 auto const key = before->getFieldH256(*field);
590 if (objectExists(keylet::unchecked(key)) && enforce)
591 return false;
592 }
593 }
594 }
595
596 return true;
597}
598
599//------------------------------------------------------------------------------
600
601void
603{
604 if (before && after && before->getType() != after->getType())
605 typeMismatch_ = true;
606
607 if (after)
608 {
609#pragma push_macro("LEDGER_ENTRY")
610#undef LEDGER_ENTRY
611
612#define LEDGER_ENTRY(tag, ...) case tag:
613
614 switch (after->getType())
615 {
616#include <xrpl/protocol/detail/ledger_entries.macro>
617
618 break;
619 default:
620 invalidTypeAdded_ = true;
621 break;
622 }
623
624#undef LEDGER_ENTRY
625#pragma pop_macro("LEDGER_ENTRY")
626 }
627}
628
629bool
631 STTx const&,
632 TER const,
633 XRPAmount const,
634 ReadView const&,
635 beast::Journal const& j) const
636{
637 if ((!typeMismatch_) && (!invalidTypeAdded_))
638 return true;
639
640 if (typeMismatch_)
641 {
642 JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
643 }
644
646 {
647 JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
648 }
649
650 return false;
651}
652
653//------------------------------------------------------------------------------
654
655void
657{
658 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
659
660 if (after && after->getType() == ltRIPPLE_STATE)
661 {
662 // checking the issue directly here instead of
663 // relying on .native() just in case native somehow
664 // were systematically incorrect
665 bool const isXrp = after->getFieldAmount(sfLowLimit).asset() == xrpIssue() ||
666 after->getFieldAmount(sfHighLimit).asset() == xrpIssue();
667 if (overwriteFixEnabled)
668 {
669 xrpTrustLine_ |= isXrp;
670 }
671 else
672 {
673 xrpTrustLine_ = isXrp;
674 }
675 }
676}
677
678bool
680 STTx const&,
681 TER const,
682 XRPAmount const,
683 ReadView const&,
684 beast::Journal const& j) const
685{
686 if (!xrpTrustLine_)
687 return true;
688
689 JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
690 return false;
691}
692
693//------------------------------------------------------------------------------
694
695void
697{
698 if (after && after->getType() == ltRIPPLE_STATE)
699 {
700 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
701
702 bool const lowFreeze = after->isFlag(lsfLowFreeze);
703 bool const lowDeepFreeze = after->isFlag(lsfLowDeepFreeze);
704
705 bool const highFreeze = after->isFlag(lsfHighFreeze);
706 bool const highDeepFreeze = after->isFlag(lsfHighDeepFreeze);
707
708 bool const bad = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
709 if (overwriteFixEnabled)
710 {
712 }
713 else
714 {
716 }
717 }
718}
719
720bool
722 STTx const&,
723 TER const,
724 XRPAmount const,
725 ReadView const&,
726 beast::Journal const& j) const
727{
729 return true;
730
731 JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
732 "without normal freeze was created";
733 return false;
734}
735
736//------------------------------------------------------------------------------
737
738void
740{
741 if (!before && after->getType() == ltACCOUNT_ROOT)
742 {
744 accountSeq_ = (*after)[sfSequence];
746 flags_ = after->getFlags();
747 }
748}
749
750bool
752 STTx const& tx,
753 TER const result,
754 XRPAmount const,
755 ReadView const& view,
756 beast::Journal const& j) const
757{
758 if (accountsCreated_ == 0)
759 return true;
760
761 if (accountsCreated_ > 1)
762 {
763 JLOG(j.fatal()) << "Invariant failed: multiple accounts "
764 "created in a single transaction";
765 return false;
766 }
767
768 // From this point on we know exactly one account was created.
770 isTesSuccess(result))
771 {
772 bool const pseudoAccount =
774 (view.rules().enabled(featureSingleAssetVault) ||
775 view.rules().enabled(featureLendingProtocol)));
776
777 if (pseudoAccount && !hasPrivilege(tx, Privilege::CreatePseudoAcct))
778 {
779 JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
780 "wrong transaction type";
781 return false;
782 }
783
784 std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq();
785
786 if (accountSeq_ != startingSeq)
787 {
788 JLOG(j.fatal()) << "Invariant failed: account created with "
789 "wrong starting sequence number";
790 return false;
791 }
792
793 if (pseudoAccount)
794 {
795 std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
796 if (flags_ != expected)
797 {
798 JLOG(j.fatal()) << "Invariant failed: pseudo-account created with "
799 "wrong flags";
800 return false;
801 }
802 }
803
804 return true;
805 }
806
807 JLOG(j.fatal()) << "Invariant failed: account root created illegally";
808 return false;
809} // namespace xrpl
810
811//------------------------------------------------------------------------------
812
815 SLE::const_pointer const& sle,
816 AccountID const& holder,
817 AccountID const& issuer,
818 Currency const& currency)
819{
820 if (!sle)
821 return STAmount{Issue{currency, issuer}};
822
823 if (sle->getType() != ltRIPPLE_STATE ||
824 sle->key() != keylet::trustLine(holder, issuer, currency).key)
825 {
826 return std::nullopt;
827 }
828
829 STAmount balance = sle->getFieldAmount(sfBalance);
830 if (holder > issuer)
831 balance.negate();
832 balance.get<Issue>().account = issuer;
833 return balance;
834}
835
836void
838{
839 if (before && before->getType() == ltRIPPLE_STATE)
840 {
842 iou_.before = before;
843 }
844
845 if (!isDelete && after && after->getType() == ltRIPPLE_STATE)
846 iou_.after = after;
847
848 if (before && before->getType() == ltMPTOKEN)
849 {
851 mpt_.before = before;
852 }
853
854 if (!isDelete && after && after->getType() == ltMPTOKEN)
855 mpt_.after = after;
856}
857
858bool
860 STTx const& tx,
861 TER const result,
862 XRPAmount const,
863 ReadView const& view,
864 beast::Journal const& j) const
865{
866 if (tx.getTxnType() != ttCLAWBACK)
867 return true;
868
869 if (isTesSuccess(result))
870 {
871 if (trustlinesChanged_ > 1)
872 {
873 JLOG(j.fatal()) << "Invariant failed: more than one trustline changed.";
874 return false;
875 }
876
877 if (mptokensChanged_ > 1)
878 {
879 JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed.";
880 return false;
881 }
882
883 bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2);
884 if (trustlinesChanged_ != 0 && mptokensChanged_ != 0)
885 {
886 JLOG(j.fatal()) << "Invariant failed: trustline and MPToken both changed.";
887 if (mptV2Enabled)
888 return false;
889 }
890
891 if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1))
892 {
893 STAmount const& amount = tx.getFieldAmount(sfAmount);
894
895 return amount.asset().visit(
896 [&](Issue const& issue) {
897 AccountID const issuer = tx.getAccountID(sfAccount);
898 AccountID const& holder = amount.getIssuer();
899 STAmount const holderBalance = accountHolds(
900 view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
901
902 if (holderBalance.signum() < 0)
903 {
904 JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
905 return false;
906 }
907
908 if (!iou_.before)
909 {
910 JLOG(j.fatal())
911 << "Invariant failed: trustline clawback changed the wrong line";
912 return !mptV2Enabled;
913 }
914
915 auto const beforeBalance = clawbackTrustLineBalanceInHolderTerms(
916 iou_.before, holder, issuer, issue.currency);
917 auto const afterBalance = clawbackTrustLineBalanceInHolderTerms(
918 iou_.after, holder, issuer, issue.currency);
919 if (!beforeBalance || !afterBalance)
920 {
921 JLOG(j.fatal())
922 << "Invariant failed: trustline clawback changed the wrong line";
923 return !mptV2Enabled;
924 }
925
926 STAmount clawAmount = amount;
927 clawAmount.get<Issue>().account = issuer;
928 if (clawAmount <= beast::kZero)
929 {
930 JLOG(j.fatal()) << "Invariant failed: trustline clawback amount is invalid";
931 return !mptV2Enabled;
932 }
933
934 if (*afterBalance > *beforeBalance ||
935 (*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
936 {
937 JLOG(j.fatal())
938 << "Invariant failed: trustline clawback balance change is invalid";
939 return !mptV2Enabled;
940 }
941
942 return true;
943 },
944 [&](MPTIssue const& issue) {
945 auto const holder = tx[~sfHolder];
946 if (!holder)
947 {
948 JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder";
949 return !mptV2Enabled;
950 }
951
952 if (!mpt_.before || !mpt_.after)
953 {
954 JLOG(j.fatal()) << "Invariant failed: MPT clawback token is missing";
955 return !mptV2Enabled;
956 }
957
958 if (mpt_.before->getAccountID(sfAccount) != *holder ||
959 mpt_.after->getAccountID(sfAccount) != *holder ||
960 (*mpt_.before)[sfMPTokenIssuanceID] != issue.getMptID() ||
961 (*mpt_.after)[sfMPTokenIssuanceID] != issue.getMptID())
962 {
963 JLOG(j.fatal()) << "Invariant failed: MPT clawback changed the wrong token";
964 return !mptV2Enabled;
965 }
966
967 auto const before = mpt_.before->getFieldU64(sfMPTAmount);
968 auto const after = mpt_.after->getFieldU64(sfMPTAmount);
969 if (amount.negative() || amount.mantissa() == 0)
970 {
971 JLOG(j.fatal()) << "Invariant failed: MPT clawback amount is invalid";
972 return !mptV2Enabled;
973 }
974 auto const clawAmount = amount.mantissa();
975
976 // MPT balances are unsigned, so validate the raw holder
977 // debit instead of routing through accountHolds().
978 if (after > before || (before - after) != std::min(before, clawAmount))
979 {
980 JLOG(j.fatal())
981 << "Invariant failed: MPT clawback balance change is invalid";
982 return !mptV2Enabled;
983 }
984
985 return true;
986 });
987 }
988 }
989 else
990 {
991 if (trustlinesChanged_ != 0)
992 {
993 JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
994 "despite failure of the transaction.";
995 return false;
996 }
997
998 if (mptokensChanged_ != 0)
999 {
1000 JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
1001 "despite failure of the transaction.";
1002 return false;
1003 }
1004 }
1005
1006 return true;
1007}
1008
1009//------------------------------------------------------------------------------
1010
1011void
1013{
1014 if (isDelete)
1015 {
1016 // Deletion is ignored
1017 return;
1018 }
1019
1020 if (after && after->getType() == ltACCOUNT_ROOT)
1021 {
1022 bool const isPseudo = [&]() {
1023 // isPseudoAccount checks that any of the pseudo-account fields are
1024 // set.
1026 return true;
1027 // Not all pseudo-accounts have a zero sequence, but all accounts
1028 // with a zero sequence had better be pseudo-accounts.
1029 if (after->at(sfSequence) == 0)
1030 return true;
1031
1032 return false;
1033 }();
1034 if (isPseudo)
1035 {
1036 // Pseudo accounts must have the following properties:
1037 // 1. Exactly one of the pseudo-account fields is set.
1038 // 2. The sequence number is not changed.
1039 // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
1040 // flags are set.
1041 // 4. The RegularKey is not set.
1042 // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor
1043 // fields are not set.
1044 {
1046
1047 auto const numFields = std::ranges::count_if(
1048 fields,
1049 [&after](SField const* sf) -> bool { return after->isFieldPresent(*sf); });
1050 if (numFields != 1)
1051 {
1052 std::stringstream error;
1053 error << "pseudo-account has " << numFields << " pseudo-account fields set";
1054 errors_.emplace_back(error.str());
1055 }
1056 }
1057 if (before && before->at(sfSequence) != after->at(sfSequence))
1058 {
1059 errors_.emplace_back("pseudo-account sequence changed");
1060 }
1061 if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
1062 {
1063 errors_.emplace_back("pseudo-account flags are not set");
1064 }
1065 if (after->isFieldPresent(sfRegularKey))
1066 {
1067 errors_.emplace_back("pseudo-account has a regular key");
1068 }
1069 if (after->isFieldPresent(sfSponsoredOwnerCount) ||
1070 after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) ||
1071 after->isFieldPresent(sfSponsoringAccountCount))
1072 {
1073 errors_.emplace_back("pseudo-account has a sponsorship field");
1074 }
1075 }
1076 }
1077}
1078
1079bool
1081 STTx const& tx,
1082 TER const,
1083 XRPAmount const,
1084 ReadView const& view,
1085 beast::Journal const& j)
1086{
1087 bool const enforce = view.rules().enabled(featureSingleAssetVault);
1088 XRPL_ASSERT(
1089 errors_.empty() || enforce,
1090 "xrpl::ValidPseudoAccounts::finalize : no bad "
1091 "changes or enforce invariant");
1092 if (!errors_.empty())
1093 {
1094 for (auto const& error : errors_)
1095 {
1096 JLOG(j.fatal()) << "Invariant failed: " << error;
1097 }
1098 if (enforce)
1099 return false;
1100 }
1101 return true;
1102}
1103
1104//------------------------------------------------------------------------------
1105
1106void
1108{
1109 if (isDelete || !before)
1110 {
1111 // Creation and deletion are ignored
1112 return;
1113 }
1114
1115 changedEntries_.emplace(before, after);
1116}
1117
1118bool
1120 STTx const& tx,
1121 TER const,
1122 XRPAmount const,
1123 ReadView const& view,
1124 beast::Journal const& j)
1125{
1126 auto const kFieldChanged = [&j, &tx](auto const& before, auto const& after, auto const& field) {
1127 bool const beforeField = before->isFieldPresent(field);
1128 bool const afterField = after->isFieldPresent(field);
1129 bool const changed =
1130 beforeField != afterField || (afterField && before->at(field) != after->at(field));
1131 if (changed)
1132 {
1133 JLOG(j.fatal()) << "Invariant failed: " << field.getName()
1134 << " changed on immutable ledger entry in " << tx.getTransactionID();
1135 }
1136 return changed;
1137 };
1138 for (auto const& slePair : changedEntries_)
1139 {
1140 auto const& before = slePair.first;
1141 auto const& after = slePair.second;
1142 auto const type = after->getType();
1143 // featureLendingProtocol gates enforcement, not detection: changes are
1144 // always logged, but the transaction is only failed once the amendment
1145 // is enabled. Type-specific field lists may add their own gates (see
1146 // ltVAULT).
1147 bool const enforce = view.rules().enabled(featureLendingProtocol);
1148 bool bad = kFieldChanged(before, after, sfLedgerEntryType) ||
1149 kFieldChanged(before, after, sfLedgerIndex);
1150 switch (type)
1151 {
1152 case ltLOAN_BROKER:
1153 bad = bad || kFieldChanged(before, after, sfSequence) ||
1154 kFieldChanged(before, after, sfOwnerNode) ||
1155 kFieldChanged(before, after, sfVaultNode) ||
1156 kFieldChanged(before, after, sfVaultID) ||
1157 kFieldChanged(before, after, sfAccount) ||
1158 kFieldChanged(before, after, sfOwner) ||
1159 kFieldChanged(before, after, sfManagementFeeRate) ||
1160 kFieldChanged(before, after, sfCoverRateMinimum) ||
1161 kFieldChanged(before, after, sfCoverRateLiquidation);
1162 break;
1163 case ltLOAN:
1164 bad = bad || kFieldChanged(before, after, sfSequence) ||
1165 kFieldChanged(before, after, sfOwnerNode) ||
1166 kFieldChanged(before, after, sfLoanBrokerNode) ||
1167 kFieldChanged(before, after, sfLoanBrokerID) ||
1168 kFieldChanged(before, after, sfBorrower) ||
1169 kFieldChanged(before, after, sfLoanOriginationFee) ||
1170 kFieldChanged(before, after, sfLoanServiceFee) ||
1171 kFieldChanged(before, after, sfLatePaymentFee) ||
1172 kFieldChanged(before, after, sfClosePaymentFee) ||
1173 kFieldChanged(before, after, sfOverpaymentFee) ||
1174 kFieldChanged(before, after, sfInterestRate) ||
1175 kFieldChanged(before, after, sfLateInterestRate) ||
1176 kFieldChanged(before, after, sfCloseInterestRate) ||
1177 kFieldChanged(before, after, sfOverpaymentInterestRate) ||
1178 kFieldChanged(before, after, sfStartDate) ||
1179 kFieldChanged(before, after, sfPaymentInterval) ||
1180 kFieldChanged(before, after, sfGracePeriod) ||
1181 kFieldChanged(before, after, sfLoanScale);
1182
1183 // lsfLoanOverpayment must never toggle. lsfLoanDefault may only
1184 // transition from unset to set, which combined with ValidLoan's rule that
1185 // only LoanManage may change it makes the flag write-once.
1186 if (view.rules().enabled(featureLendingProtocolV1_1))
1187 {
1188 std::uint32_t const beforeFlags = before->getFlags();
1189 std::uint32_t const afterFlags = after->getFlags();
1190 bool const overpaymentChanged =
1191 (beforeFlags & lsfLoanOverpayment) != (afterFlags & lsfLoanOverpayment);
1192 if (overpaymentChanged)
1193 {
1194 JLOG(j.fatal()) << "Invariant failed: lsfLoanOverpayment flag "
1195 "toggled on immutable ledger entry in "
1196 << tx.getTransactionID();
1197 }
1198 bad = bad || overpaymentChanged;
1199 bool const defaultCleared =
1200 (beforeFlags & lsfLoanDefault) != 0 && (afterFlags & lsfLoanDefault) == 0;
1201 if (defaultCleared)
1202 {
1203 JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault flag "
1204 "cleared on immutable ledger entry in "
1205 << tx.getTransactionID();
1206 }
1207 bad = bad || defaultCleared;
1208 }
1209 break;
1210 case ltVAULT:
1211 /*
1212 * All the fields below are only immutable from
1213 * featureLendingProtocolV1_1 onwards; some of them only exist on
1214 * V1_1 vaults. Before that amendment, sfAsset, sfAccount and
1215 * sfShareMPTID are checked by VaultInvariant instead.
1216 */
1217 if (view.rules().enabled(featureLendingProtocolV1_1))
1218 {
1219 bad = bad || kFieldChanged(before, after, sfVaultKind) ||
1220 kFieldChanged(before, after, sfSubscriptionDate) ||
1221 kFieldChanged(before, after, sfRedemptionDate) ||
1222 kFieldChanged(before, after, sfSequence) ||
1223 kFieldChanged(before, after, sfOwnerNode) ||
1224 kFieldChanged(before, after, sfOwner) ||
1225 kFieldChanged(before, after, sfWithdrawalPolicy) ||
1226 kFieldChanged(before, after, sfScale) ||
1227 kFieldChanged(before, after, sfLEVersion) ||
1228 kFieldChanged(before, after, sfAsset) ||
1229 kFieldChanged(before, after, sfAccount) ||
1230 kFieldChanged(before, after, sfShareMPTID);
1231 }
1232 break;
1233 default:
1234 break;
1235 }
1236 XRPL_ASSERT(
1237 !bad || enforce,
1238 "xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
1239 "changes or enforce invariant");
1240 if (bad)
1241 {
1242 JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for "
1243 << tx.getTransactionID();
1244 if (enforce)
1245 return false;
1246 }
1247 }
1248 return true;
1249}
1250
1251void
1253 bool isDelete,
1256{
1257 if (!isDelete && after)
1258 afterEntries_.push_back(after);
1259}
1260
1261bool
1263 STTx const&,
1264 TER const,
1265 XRPAmount const,
1266 ReadView const& view,
1267 beast::Journal const& j) const
1268{
1269 bool const badLedgerEntry = std::ranges::any_of(
1270 afterEntries_, [&](auto const& sle) { return hasInvalidAmount(*sle, j); });
1271
1272 if (badLedgerEntry)
1273 {
1274 JLOG(j.fatal())
1275 << "Invariant failed: ledger entry contains non-canonical MPT or XRP amount";
1276 return !view.rules().enabled(fixCleanup3_2_0);
1277 }
1278
1279 return true;
1280}
1281
1282void
1284{
1285 if (!isDelete)
1286 return;
1287
1288 // Before should never be null when isDelete = true
1289 if (!before)
1290 {
1291 // LCOV_EXCL_START
1292 UNREACHABLE(
1293 "xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state");
1294 return;
1295 // LCOV_EXCL_STOP
1296 }
1297
1298 switch (before->getType())
1299 {
1300 case ltAMM:
1301 case ltVAULT:
1302 case ltLOAN_BROKER:
1303 deletedObjSles_.push_back(before);
1304 break;
1305 default:
1306 return;
1307 }
1308}
1309
1310[[nodiscard]] bool
1312 STTx const&,
1313 TER const,
1314 XRPAmount const,
1315 ReadView const& view,
1316 beast::Journal const& j) const
1317{
1318 if (!view.rules().enabled(fixCleanup3_3_0))
1319 return true;
1320
1321 if (deletedObjSles_.empty())
1322 return true;
1323
1324 bool failed = false;
1325 for (auto const& sle : deletedObjSles_)
1326 {
1327 if (!sle->isFieldPresent(sfAccount))
1328 {
1329 JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
1330 << " is missing pseudo-account field";
1331 failed = true;
1332 continue;
1333 }
1334
1335 // The pseudo-account must NOT exist on the ledger after the object is deleted.
1336 if (view.exists(keylet::account(sle->getAccountID(sfAccount))))
1337 {
1338 JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
1339 << " without deleting its pseudo-account";
1340 failed = true;
1341 }
1342 }
1343
1344 return !failed;
1345}
1346
1347} // namespace xrpl
T any_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
std::vector< std::pair< SLE::const_pointer, SLE::const_pointer > > accountsDeleted_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
BaseUInt next() const
Definition base_uint.h:477
A currency issued by an account.
Definition Issue.h:18
Currency currency
Definition Issue.h:20
Item const * findByType(KeyType type) const
Retrieve a format based on its type.
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
static LedgerFormats const & getInstance()
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::set< std::pair< SLE::const_pointer, SLE::const_pointer > > changedEntries_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::vector< SLE::const_pointer > deletedObjSles_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
virtual std::optional< key_type > succ(key_type const &key, std::optional< key_type > const &last=std::nullopt) const =0
Return the key of the next state item.
bool enabled(UInt256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:182
Identifies fields.
Definition SField.h:132
constexpr TIss const & get() const
void negate()
Definition STAmount.h:586
std::uint64_t mantissa() const noexcept
Definition STAmount.h:490
int signum() const noexcept
Definition STAmount.h:522
bool negative() const noexcept
Definition STAmount.h:484
bool native() const noexcept
Definition STAmount.h:471
Asset const & asset() const
Definition STAmount.h:496
AccountID const & getIssuer() const
Definition STAmount.h:516
XRPAmount xrp() const
Definition STAmount.cpp:272
LedgerEntryType getType() const
std::shared_ptr< STLedgerEntry const > const & ConstRef
std::shared_ptr< STLedgerEntry const > const_pointer
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:657
TxType getTxnType() const
Definition STTx.h:250
UInt256 getTransactionID() const
Definition STTx.h:262
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
static bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::vector< SLE::const_pointer > afterEntries_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
std::uint32_t mptokensChanged_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::uint32_t trustlinesChanged_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
std::vector< std::string > errors_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
constexpr value_type drops() const
Returns the number of drops.
Definition XRPAmount.h:170
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
T count_if(T... args)
T invoke(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet computation functions.
Definition Indexes.h:40
Keylet nftokenPageMin(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:430
Keylet unchecked(UInt256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:397
Keylet nftokenPageMax(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:438
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
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
std::vector< SField const * > const & getPseudoAccountFields()
Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if set.
bool isXRP(AccountID const &c)
Definition AccountID.h:84
bool isFeatureEnabled(UInt256 const &feature, bool resultIfNoRules)
Check whether a feature is enabled in the current ledger rules.
Definition Rules.cpp:199
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:42
Privilege
Operations a transaction is permitted to perform, as a bitfield.
Definition TxSettings.h:19
STLedgerEntry SLE
bool hasPrivilege(STTx const &tx, Privilege priv)
static std::optional< STAmount > clawbackTrustLineBalanceInHolderTerms(SLE::const_pointer const &sle, AccountID const &holder, AccountID const &issuer, Currency const &currency)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
static std::string ledgerEntryTypeName(SLE const &sle)
bool hasInvalidAmount(STBase const &field, beast::Journal j)
bool isPseudoAccount(SLE::const_pointer sleAcct)
Returns true if and only if sleAcct is a pseudo-account of any kind (i.e.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
constexpr XRPAmount kInitialXrp
Configure the native currency.
std::array< KeyletDesc< AccountID const & >, 6 > const kDirectAccountKeylets
Definition Indexes.cpp:39
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)
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)