From d0a54d11595bbd39c40208fed72a9c4c3bdae70f Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:33:52 +0200 Subject: [PATCH] feat: Move VaultCreate invariants from ValidVault to VaultCreate Introduce VaultInvariantData to collect vault and MPTokenIssuance snapshots during visitInvariantEntry, then implement the six ttVAULT_CREATE-specific post-conditions in VaultCreate::finalizeInvariants (empty vault, pseudo-account linkage). ValidVault now passes through ttVAULT_CREATE in its switch, keeping only the universal vault checks and the cross-transaction sentinel that guards against other tx types creating vaults. --- .../xrpl/tx/invariants/VaultInvariantData.h | 76 +++++++++++++ .../xrpl/tx/transactors/vault/VaultCreate.h | 4 + src/libxrpl/tx/invariants/VaultInvariant.cpp | 69 ++---------- .../tx/invariants/VaultInvariantData.cpp | 80 ++++++++++++++ .../tx/transactors/vault/VaultCreate.cpp | 102 +++++++++++++++++- 5 files changed, 265 insertions(+), 66 deletions(-) create mode 100644 include/xrpl/tx/invariants/VaultInvariantData.h create mode 100644 src/libxrpl/tx/invariants/VaultInvariantData.cpp diff --git a/include/xrpl/tx/invariants/VaultInvariantData.h b/include/xrpl/tx/invariants/VaultInvariantData.h new file mode 100644 index 0000000000..e20456f74b --- /dev/null +++ b/include/xrpl/tx/invariants/VaultInvariantData.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Collects vault and share-issuance snapshots from ledger entry visits. + * + * Used by per-transaction invariant checks (e.g. VaultCreate) that need + * vault and MPTokenIssuance state without the full balance-delta tracking + * that ValidVault maintains. + */ +class VaultInvariantData +{ +public: + struct Vault + { + uint256 key = beast::kZero; + Asset asset; + AccountID pseudoId; + AccountID owner; + uint192 shareMPTID = beast::kZero; + Number assetsTotal = 0; + Number assetsAvailable = 0; + Number assetsMaximum = 0; + Number lossUnrealized = 0; + + static Vault + make(SLE const&); + }; + + struct Shares + { + MPTIssue share; + std::uint64_t sharesTotal = 0; + std::uint64_t sharesMaximum = 0; + + static Shares + make(SLE const&); + }; + + void + visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after); + + [[nodiscard]] std::vector const& + afterVaults() const + { + return afterVault_; + } + + [[nodiscard]] std::vector const& + beforeVaults() const + { + return beforeVault_; + } + + /** Find shares in afterMPTs_ whose mptID matches. */ + [[nodiscard]] std::optional + findShares(uint192 const& mptID) const; + +private: + std::vector afterVault_; + std::vector beforeVault_; + std::vector afterMPTs_; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/vault/VaultCreate.h b/include/xrpl/tx/transactors/vault/VaultCreate.h index 9b11f97957..0199202afb 100644 --- a/include/xrpl/tx/transactors/vault/VaultCreate.h +++ b/include/xrpl/tx/transactors/vault/VaultCreate.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace xrpl { @@ -38,6 +39,9 @@ public: XRPAmount fee, ReadView const& view, beast::Journal const& j) override; + +private: + VaultInvariantData data_; }; } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index 80b8f36bd9..af1c722254 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -544,61 +544,13 @@ ValidVault::finalize( result &= [&]() { switch (txnType) { - case ttVAULT_CREATE: { - bool result = true; - - if (!beforeVault_.empty()) - { - JLOG(j.fatal()) // - << "Invariant failed: create operation must not have " - "updated a vault"; - result = false; - } - - if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero || - afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0) - { - JLOG(j.fatal()) // - << "Invariant failed: created vault must be empty"; - result = false; - } - - if (afterVault.pseudoId != updatedShares->share.getIssuer()) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer and vault " - "pseudo-account must be the same"; - result = false; - } - - auto const sleSharesIssuer = - view.read(keylet::account(updatedShares->share.getIssuer())); - if (!sleSharesIssuer) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must exist"; - return false; - } - - if (!isPseudoAccount(sleSharesIssuer)) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer must be a " - "pseudo-account"; - result = false; - } - - if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; - !vaultId || *vaultId != afterVault.key) - { - JLOG(j.fatal()) // - << "Invariant failed: shares issuer pseudo-account " - "must point back to the vault"; - result = false; - } - - return result; - } + case ttVAULT_CREATE: + case ttLOAN_SET: + case ttLOAN_MANAGE: + case ttLOAN_PAY: + // Create-specific checks live in VaultCreate::finalizeInvariants. + // Loan checks are TBD. + return true; case ttVAULT_SET: { bool result = true; @@ -1042,13 +994,6 @@ ValidVault::finalize( return result; } - case ttLOAN_SET: - case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD - return true; - } - default: // LCOV_EXCL_START UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type"); diff --git a/src/libxrpl/tx/invariants/VaultInvariantData.cpp b/src/libxrpl/tx/invariants/VaultInvariantData.cpp new file mode 100644 index 0000000000..ddc2807579 --- /dev/null +++ b/src/libxrpl/tx/invariants/VaultInvariantData.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include +#include +#include +#include +#include // IWYU pragma: keep + +namespace xrpl { + +VaultInvariantData::Vault +VaultInvariantData::Vault::make(SLE const& from) +{ + XRPL_ASSERT(from.getType() == ltVAULT, "VaultInvariantData::Vault::make : from Vault object"); + + Vault self; + self.key = from.key(); + self.asset = from.at(sfAsset); + self.pseudoId = from.getAccountID(sfAccount); + self.owner = from.at(sfOwner); + self.shareMPTID = from.getFieldH192(sfShareMPTID); + self.assetsTotal = from.at(sfAssetsTotal); + self.assetsAvailable = from.at(sfAssetsAvailable); + self.assetsMaximum = from.at(sfAssetsMaximum); + self.lossUnrealized = from.at(sfLossUnrealized); + return self; +} + +VaultInvariantData::Shares +VaultInvariantData::Shares::make(SLE const& from) +{ + XRPL_ASSERT( + from.getType() == ltMPTOKEN_ISSUANCE, + "VaultInvariantData::Shares::make : from MPTokenIssuance object"); + + Shares self; + self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); + self.sharesTotal = from.getFieldU64(sfOutstandingAmount); + self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount); + return self; +} + +void +VaultInvariantData::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) +{ + XRPL_ASSERT( + after != nullptr && (before != nullptr || !isDelete), + "xrpl::VaultInvariantData::visitEntry : some object is available"); + + if (before && before->getType() == ltVAULT) + beforeVault_.push_back(Vault::make(*before)); + + if (!isDelete && after) + { + switch (after->getType()) + { + case ltVAULT: + afterVault_.push_back(Vault::make(*after)); + break; + case ltMPTOKEN_ISSUANCE: + afterMPTs_.push_back(Shares::make(*after)); + break; + default:; + } + } +} + +std::optional +VaultInvariantData::findShares(uint192 const& mptID) const +{ + for (auto const& s : afterMPTs_) + { + if (s.share.getMptID() == mptID) + return s; + } + return std::nullopt; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index 4163753014..8083bda777 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -1,8 +1,10 @@ #include +#include #include #include #include +#include #include #include #include @@ -263,15 +265,107 @@ VaultCreate::doApply() } void -VaultCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +VaultCreate::visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { - // No transaction-specific invariants yet (future work). + data_.visitEntry(isDelete, before, after); } bool -VaultCreate::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) +VaultCreate::finalizeInvariants( + STTx const&, + TER result, + XRPAmount, + ReadView const& view, + beast::Journal const& j) { - // No transaction-specific invariants yet (future work). + bool const enforce = view.rules().enabled(featureSingleAssetVault); + + if (!isTesSuccess(result)) + return true; + + auto const& afterVaults = data_.afterVaults(); + if (afterVaults.empty()) + return true; + + auto const& afterVault = afterVaults[0]; + bool checkResult = true; + + if (!data_.beforeVaults().empty()) + { + JLOG(j.fatal()) << "Invariant failed: create operation must not have updated a vault"; + checkResult = false; + } + + // The MPTokenIssuance may not be in the modified set (e.g. only the vault + // was touched in the test), so fall back to a view read if needed. + auto const updatedShares = [&]() -> std::optional { + if (auto found = data_.findShares(afterVault.shareMPTID)) + return found; + auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID)); + return sleShares ? std::optional( + VaultInvariantData::Shares::make(*sleShares)) + : std::nullopt; + }(); + + static constexpr Number kZero{}; + + if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero || + afterVault.lossUnrealized != kZero || (updatedShares && updatedShares->sharesTotal != 0)) + { + JLOG(j.fatal()) << "Invariant failed: created vault must be empty"; + checkResult = false; + } + + if (!updatedShares) + { + JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; + XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault has shares invariant"); + if (!checkResult) + { + XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants"); + } + return !enforce; + } + + if (afterVault.pseudoId != updatedShares->share.getIssuer()) + { + JLOG(j.fatal()) + << "Invariant failed: shares issuer and vault pseudo-account must be the same"; + checkResult = false; + } + + auto const sleSharesIssuer = view.read(keylet::account(updatedShares->share.getIssuer())); + if (!sleSharesIssuer) + { + JLOG(j.fatal()) << "Invariant failed: shares issuer must exist"; + XRPL_ASSERT( + enforce, "xrpl::VaultCreate::finalizeInvariants : shares issuer exists invariant"); + if (!checkResult) + { + XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants"); + } + return !enforce; + } + + if (!isPseudoAccount(sleSharesIssuer)) + { + JLOG(j.fatal()) << "Invariant failed: shares issuer must be a pseudo-account"; + checkResult = false; + } + + if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; !vaultId || *vaultId != afterVault.key) + { + JLOG(j.fatal()) + << "Invariant failed: shares issuer pseudo-account must point back to the vault"; + checkResult = false; + } + + if (!checkResult) + { + XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault create invariants"); + return !enforce; + } + return true; }