Skip to content
26 changes: 23 additions & 3 deletions include/xrpl/tx/invariants/InvariantCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,35 @@ class NoModifiedUnmodifiableFields
*/
class ValidAmounts
{
std::vector<std::shared_ptr<SLE const>> afterEntries_;
std::vector<SLE::const_pointer> afterEntries_;

public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, SLE::const_ref, SLE::const_ref);

[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};

/*
* Verify that when an object with an associated pseudo-account is deleted,
* its pseudo-account is also deleted.
*
* The reverse (pseudo-account deleted → object deleted) is enforced by
* AccountRootsDeletedClean via getPseudoAccountFields().
*/
Comment thread
Tapanito marked this conversation as resolved.
class ObjectHasPseudoAccount
{
public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;

private:
std::vector<SLE::const_pointer> deletedObjSles_;
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
Expand Down Expand Up @@ -415,7 +434,8 @@ using InvariantChecks = std::tuple<
ValidVault,
ValidMPTPayment,
ValidAmounts,
ValidMPTTransfer>;
ValidMPTTransfer,
ObjectHasPseudoAccount>;
Comment thread
bthomee marked this conversation as resolved.

/**
* @brief get a tuple of all invariant checks
Expand Down
71 changes: 71 additions & 0 deletions src/libxrpl/tx/invariants/InvariantCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1069,4 +1069,75 @@
return true;
}

void
ObjectHasPseudoAccount::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (!isDelete)
return;

// Before should never be null when isDelete = true
if (!before)
{
XRPL_ASSERT(
before,
"xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state");
return; // LCOV_EXCL_LINE
}

switch (before->getType())
{
case ltAMM:
case ltVAULT:
case ltLOAN_BROKER:
deletedObjSles_.push_back(before);
break;
default:
return;
}
}

[[nodiscard]] bool
ObjectHasPseudoAccount::finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j) const
{
if (!view.rules().enabled(fixCleanup3_3_0))
return true;

if (deletedObjSles_.empty())
return true;

auto const typeName = [](SLE const& sle) {
if (auto item = LedgerFormats::getInstance().findByType(sle.getType()))
return item->getName();
return std::to_string(sle.getType());

Check warning on line 1116 in src/libxrpl/tx/invariants/InvariantCheck.cpp

View check run for this annotation

Codecov / codecov/patch

src/libxrpl/tx/invariants/InvariantCheck.cpp#L1116

Added line #L1116 was not covered by tests
};

bool failed = false;
for (auto const& sle : deletedObjSles_)
{
if (!sle->isFieldPresent(sfAccount))
{
JLOG(j.fatal()) << "Invariant failed: deleted " << typeName(*sle)
<< " is missing pseudo-account field";
failed = true;
Comment thread
Tapanito marked this conversation as resolved.
continue;
}

// The pseudo-account must NOT exist on the ledger after the object is deleted.
bool const exists = view.read(keylet::account(sle->getAccountID(sfAccount))) != nullptr;
if (exists)
{
JLOG(j.fatal()) << "Invariant failed: deleted " << typeName(*sle)
<< " without deleting its pseudo-account";
failed = true;
}
}

return !failed;
}

} // namespace xrpl
142 changes: 134 additions & 8 deletions src/test/app/Invariants_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2741,7 +2741,8 @@ class Invariants_test : public beast::unit_test::Suite
});

doInvariantCheck(
{"vault updated by a wrong transaction type"},
{"vault updated by a wrong transaction type",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
Expand All @@ -2752,7 +2753,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttPAYMENT, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers, ValidVault::finalize:290 returns true when !isTesSuccess(ret), so it only fires on the first pass. On the second pass (with tecINVARIANT_FAILED as input), ValidVault passes, but now ObjectHasPseudoAccount also fires because it doesn't gate on the result. This causes tefINVARIANT_FAILED on the second pass.

[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
Expand Down Expand Up @@ -2789,6 +2790,7 @@ class Invariants_test : public beast::unit_test::Suite
auto const vaultPage = ac.view().dirInsert(
keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
sleVault->setFieldU64(sfOwnerNode, *vaultPage);
sleVault->setAccountID(sfAccount, a1.id());
ac.view().insert(sleVault);
return true;
},
Expand All @@ -2797,7 +2799,8 @@ class Invariants_test : public beast::unit_test::Suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});

doInvariantCheck(
{"vault deleted by a wrong transaction type"},
{"vault deleted by a wrong transaction type",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
Expand All @@ -2808,7 +2811,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
Expand All @@ -2817,7 +2820,8 @@ class Invariants_test : public beast::unit_test::Suite
});

doInvariantCheck(
{"vault operation updated more than single vault"},
{"vault operation updated more than single vault",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
{
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
Expand All @@ -2837,7 +2841,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
{
Expand All @@ -2861,6 +2865,7 @@ class Invariants_test : public beast::unit_test::Suite
auto const vaultPage = ac.view().dirInsert(
keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
sleVault->setFieldU64(sfOwnerNode, *vaultPage);
sleVault->setAccountID(sfAccount, a.id());
ac.view().insert(sleVault);
};
insertVault(a1);
Expand All @@ -2872,7 +2877,8 @@ class Invariants_test : public beast::unit_test::Suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});

doInvariantCheck(
{"deleted vault must also delete shares"},
{"deleted vault must also delete shares",
"deleted Vault without deleting its pseudo-account"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
auto sleVault = ac.view().peek(keylet);
Expand All @@ -2883,7 +2889,7 @@ class Invariants_test : public beast::unit_test::Suite
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
Expand Down Expand Up @@ -4883,6 +4889,125 @@ class Invariants_test : public beast::unit_test::Suite
}
}

void
testObjectHasPseudoAccount()
{
testcase << "object has pseudo-account";
using namespace jtx;

auto const amendments = defaultAmendments() | fixCleanup3_3_0;

// Vault: object deleted without its pseudo-account
{
Keylet vaultKeylet = keylet::amendments();
doInvariantCheck(
Env{*this, amendments},
{{"deleted Vault without deleting its pseudo-account"}},
[&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(vaultKeylet);
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttVAULT_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&vaultKeylet](Account const& a1, Account const&, Env& env) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
vaultKeylet = keylet;
return true;
});
}

// AMM: object deleted without its pseudo-account
{
uint256 ammID{};
Account const gw{"gw"};
doInvariantCheck(
Env{*this, amendments},
{{"deleted AMM without deleting its pseudo-account"}},
[&ammID](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(keylet::amm(ammID));
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttAMM_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&ammID, &gw](Account const&, Account const&, Env& env) {
env.fund(XRP(1'000), gw);
AMM const amm(env, gw, XRP(100), gw["USD"](100));
ammID = amm.ammID();
return true;
});
}

// LoanBroker: object deleted without its pseudo-account
{
Keylet loanBrokerKeylet = keylet::amendments();
doInvariantCheck(
Env{*this, amendments},
{{"deleted LoanBroker without deleting its pseudo-account"}},
[&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(loanBrokerKeylet);
if (!sle)
return false;
ac.view().erase(sle);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
});
}

// Deleted object missing sfAccount field (defensive check).
// Manually construct the view to place a vault SLE without
// sfAccount into the base ledger, then erase it.
{
Env env{*this, amendments};
Account const a1{"A1"};
Account const a2{"A2"};
env.fund(XRP(1000), a1, a2);
env.close();

OpenView ov{*env.current()};

auto const vaultKeylet = keylet::vault(a1.id(), ov.seq());
auto sleVault = std::make_shared<SLE>(vaultKeylet);
sleVault->makeFieldAbsent(sfAccount);
ov.rawInsert(sleVault);

STTx tx{ttVAULT_DELETE, [](STObject&) {}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{
env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());

auto sle = ac.view().peek(vaultKeylet);
if (!BEAST_EXPECT(sle))
return;
ac.view().erase(sle);

auto transactor = makeTransactor(ac);
if (!BEAST_EXPECT(transactor))
return;
TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
BEAST_EXPECT(result == tecINVARIANT_FAILED);
BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
}
}

public:
void
run() override
Expand Down Expand Up @@ -4914,6 +5039,7 @@ class Invariants_test : public beast::unit_test::Suite
testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
testVaultComputeCoarsestScale();
testAMM();
testObjectHasPseudoAccount();
}
};

Expand Down
Loading