mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Start writing Loan Broker invariants and tests
- Specifically those mentioned for LoanBrokerDelete
This commit is contained in:
@@ -1301,6 +1301,33 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
Keylet
|
||||
createLoanBroker(
|
||||
jtx::Account const& a,
|
||||
jtx::Env& env,
|
||||
jtx::PrettyAsset const& asset)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
// Create vault
|
||||
uint256 vaultID;
|
||||
Vault vault{env};
|
||||
auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
|
||||
env(tx);
|
||||
BEAST_EXPECT(env.le(vKeylet));
|
||||
|
||||
vaultID = vKeylet.key;
|
||||
|
||||
// Create Loan Broker
|
||||
using namespace loanBroker;
|
||||
|
||||
auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a));
|
||||
// Create a Loan Broker with all default values.
|
||||
env(set(a, vaultID), fee(increment));
|
||||
|
||||
return loanBrokerKeylet;
|
||||
};
|
||||
|
||||
void
|
||||
testNoModifiedUnmodifiableFields()
|
||||
{
|
||||
@@ -1313,23 +1340,7 @@ class Invariants_test : public beast::unit_test::suite
|
||||
[&, this](Account const& a, Account const& b, Env& env) {
|
||||
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
|
||||
|
||||
// Create vault
|
||||
uint256 vaultID;
|
||||
Vault vault{env};
|
||||
auto [tx, vKeylet] =
|
||||
vault.create({.owner = a, .asset = xrpAsset});
|
||||
env(tx);
|
||||
BEAST_EXPECT(env.le(vKeylet));
|
||||
|
||||
vaultID = vKeylet.key;
|
||||
|
||||
// Create Loan Broker
|
||||
using namespace loanBroker;
|
||||
|
||||
loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a));
|
||||
// Create a Loan Broker with all default values.
|
||||
env(set(a, vaultID), fee(increment));
|
||||
|
||||
loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
|
||||
return BEAST_EXPECT(env.le(loanBrokerKeylet));
|
||||
};
|
||||
|
||||
@@ -1509,6 +1520,228 @@ class Invariants_test : public beast::unit_test::suite
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testValidLoanBroker()
|
||||
{
|
||||
testcase << "valid loan broker";
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
enum class Asset { XRP, IOU, MPT };
|
||||
auto const assetTypes =
|
||||
std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
|
||||
|
||||
for (auto const assetType : assetTypes)
|
||||
{
|
||||
// Initialize with a placeholder value because there's no default
|
||||
// ctor
|
||||
Keylet loanBrokerKeylet = keylet::amendments();
|
||||
Preclose createLoanBroker = [&, this](
|
||||
Account const& alice,
|
||||
Account const& issuer,
|
||||
Env& env) {
|
||||
PrettyAsset const asset = [&]() {
|
||||
switch (assetType)
|
||||
{
|
||||
case Asset::IOU: {
|
||||
PrettyAsset const iouAsset = issuer["IOU"];
|
||||
env(trust(alice, iouAsset(1000)));
|
||||
env(pay(issuer, alice, iouAsset(1000)));
|
||||
env.close();
|
||||
return iouAsset;
|
||||
}
|
||||
|
||||
case Asset::MPT: {
|
||||
MPTTester mptt{env, issuer, mptInitNoFund};
|
||||
mptt.create(
|
||||
{.flags = tfMPTCanClawback | tfMPTCanTransfer |
|
||||
tfMPTCanLock});
|
||||
PrettyAsset const mptAsset = mptt.issuanceID();
|
||||
mptt.authorize({.account = alice});
|
||||
env(pay(issuer, alice, mptAsset(1000)));
|
||||
env.close();
|
||||
return mptAsset;
|
||||
}
|
||||
|
||||
case Asset::XRP:
|
||||
default:
|
||||
return PrettyAsset{xrpIssue(), 1'000'000};
|
||||
}
|
||||
}();
|
||||
loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
|
||||
return BEAST_EXPECT(env.le(loanBrokerKeylet));
|
||||
};
|
||||
|
||||
// Ensure the test scenarios are set up completely. The test cases
|
||||
// will need to recompute any of these values it needs for itself
|
||||
// rather than trying to return a bunch of items
|
||||
auto setupTest =
|
||||
[&, this](Account const& A1, Account const&, ApplyContext& ac)
|
||||
-> std::optional<std::pair<SLE::pointer, SLE::pointer>> {
|
||||
if (loanBrokerKeylet.type != ltLOAN_BROKER)
|
||||
return {};
|
||||
auto sleBroker = ac.view().peek(loanBrokerKeylet);
|
||||
if (!sleBroker)
|
||||
return {};
|
||||
if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
|
||||
return {};
|
||||
// Need to touch sleBroker so that it is included in the
|
||||
// modified entries for the invariant to find
|
||||
ac.view().update(sleBroker);
|
||||
|
||||
// The pseudo-account holds the directory, so get it
|
||||
auto const pseudoAccountID = sleBroker->at(sfAccount);
|
||||
auto const pseudoAccountKeylet =
|
||||
keylet::account(pseudoAccountID);
|
||||
// Strictly speaking, we don't need to load the
|
||||
// ACCOUNT_ROOT, but check anyway
|
||||
auto slePseudo = ac.view().peek(pseudoAccountKeylet);
|
||||
if (!BEAST_EXPECT(slePseudo))
|
||||
return {};
|
||||
// Make sure the directory doesn't already exist
|
||||
auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
|
||||
auto sleDir = ac.view().peek(dirKeylet);
|
||||
auto const describe = describeOwnerDir(pseudoAccountID);
|
||||
if (!sleDir)
|
||||
{
|
||||
// Create the directory
|
||||
BEAST_EXPECT(
|
||||
directory::createRoot(
|
||||
ac.view(),
|
||||
dirKeylet,
|
||||
loanBrokerKeylet.key,
|
||||
describe) == 0);
|
||||
|
||||
sleDir = ac.view().peek(dirKeylet);
|
||||
}
|
||||
|
||||
return std::make_pair(slePseudo, sleDir);
|
||||
};
|
||||
|
||||
// JLOG(j.fatal()) << "Invariant failed:";
|
||||
// JLOG(j.fatal()) << "Invariant failed: ";
|
||||
// JLOG(j.fatal())
|
||||
// << "Invariant failed: ";
|
||||
// JLOG(j.fatal())
|
||||
// << "Invariant failed: ";
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Loan Broker with zero OwnerCount has multiple directory "
|
||||
"pages"}},
|
||||
[&loanBrokerKeylet, &setupTest, this](
|
||||
Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto test = setupTest(A1, A2, ac);
|
||||
if (!test || !test->first || !test->second)
|
||||
return false;
|
||||
|
||||
auto slePseudo = test->first;
|
||||
auto sleDir = test->second;
|
||||
auto const describe =
|
||||
describeOwnerDir(slePseudo->at(sfAccount));
|
||||
|
||||
BEAST_EXPECT(
|
||||
directory::insertPage(
|
||||
ac.view(),
|
||||
0,
|
||||
sleDir,
|
||||
0,
|
||||
sleDir,
|
||||
slePseudo->key(),
|
||||
keylet::page(sleDir->key(), 0),
|
||||
describe) == 1);
|
||||
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
createLoanBroker);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Loan Broker with zero OwnerCount has multiple indexes in "
|
||||
"the Directory root"}},
|
||||
[&loanBrokerKeylet, &setupTest, this](
|
||||
Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto test = setupTest(A1, A2, ac);
|
||||
if (!test || !test->first || !test->second)
|
||||
return false;
|
||||
|
||||
auto slePseudo = test->first;
|
||||
auto sleDir = test->second;
|
||||
auto indexes = sleDir->getFieldV256(sfIndexes);
|
||||
|
||||
// Put some extra garbage into the directory
|
||||
for (auto const& key : {slePseudo->key(), sleDir->key()})
|
||||
{
|
||||
directory::insertKey(
|
||||
ac.view(), sleDir, 0, false, indexes, key);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
createLoanBroker);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Loan Broker directory corrupt"}},
|
||||
[&loanBrokerKeylet, &setupTest, this](
|
||||
Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto test = setupTest(A1, A2, ac);
|
||||
if (!test || !test->first || !test->second)
|
||||
return false;
|
||||
|
||||
auto slePseudo = test->first;
|
||||
auto sleDir = test->second;
|
||||
auto const describe =
|
||||
describeOwnerDir(slePseudo->at(sfAccount));
|
||||
// Empty vector will overwrite the existing entry for the
|
||||
// holding, if any, avoiding the "has multiple indexes"
|
||||
// failure.
|
||||
STVector256 indexes;
|
||||
|
||||
// Put one meaningless key into the directory
|
||||
auto const key =
|
||||
keylet::account(Account("random").id()).key;
|
||||
directory::insertKey(
|
||||
ac.view(), sleDir, 0, false, indexes, key);
|
||||
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
createLoanBroker);
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Loan Broker with zero OwnerCount has an unexpected entry in "
|
||||
"the directory"}},
|
||||
[&loanBrokerKeylet, &setupTest, this](
|
||||
Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto test = setupTest(A1, A2, ac);
|
||||
if (!test || !test->first || !test->second)
|
||||
return false;
|
||||
|
||||
auto slePseudo = test->first;
|
||||
auto sleDir = test->second;
|
||||
// Empty vector will overwrite the existing entry for the
|
||||
// holding, if any, avoiding the "has multiple indexes"
|
||||
// failure.
|
||||
STVector256 indexes;
|
||||
|
||||
directory::insertKey(
|
||||
ac.view(), sleDir, 0, false, indexes, slePseudo->key());
|
||||
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
createLoanBroker);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1529,6 +1762,7 @@ public:
|
||||
testPermissionedDomainInvariants();
|
||||
testNoModifiedUnmodifiableFields();
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1824,4 +1824,98 @@ ValidPseudoAccounts::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
ValidLoanBroker::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
if (after && after->getType() == ltLOAN_BROKER)
|
||||
{
|
||||
brokers_.emplace_back(after);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidLoanBroker::goodZeroDirectory(
|
||||
ReadView const& view,
|
||||
SLE::const_ref dir,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
auto const next = dir->at(~sfIndexNext);
|
||||
auto const prev = dir->at(~sfIndexPrevious);
|
||||
if ((prev && *prev) || (next && *next))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
|
||||
"OwnerCount has multiple directory pages";
|
||||
return false;
|
||||
}
|
||||
auto indexes = dir->getFieldV256(sfIndexes);
|
||||
if (indexes.size() > 1)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Broker with zero "
|
||||
"OwnerCount has multiple indexes in the Directory root";
|
||||
return false;
|
||||
}
|
||||
if (indexes.size() == 1)
|
||||
{
|
||||
auto const index = indexes.value().front();
|
||||
auto const sle = view.read(keylet::unchecked(index));
|
||||
if (!sle)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Broker directory corrupt";
|
||||
return false;
|
||||
}
|
||||
if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Broker with zero "
|
||||
"OwnerCount has an unexpected entry in the directory";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidLoanBroker::finalize(
|
||||
STTx const& tx,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureLendingProtocol);
|
||||
|
||||
for (auto const& broker : brokers_)
|
||||
{
|
||||
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
|
||||
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
|
||||
// one node (the root), which will only hold entries for `RippleState`
|
||||
// or `MPToken` objects.
|
||||
if (broker->at(sfOwnerCount) == 0)
|
||||
{
|
||||
auto const dir = view.read(keylet::ownerDir(broker->at(sfAccount)));
|
||||
if (dir)
|
||||
{
|
||||
if (!goodZeroDirectory(view, dir, j))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
enforce,
|
||||
"ripple::ValidLoanBroker::finalize : Enforcing "
|
||||
"invariant");
|
||||
if (enforce)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -646,7 +646,7 @@ public:
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariants: Pseudo-accounts have
|
||||
* @brief Invariants: Pseudo-accounts have valid and consisent properties
|
||||
*
|
||||
* Pseudo-accounts have certain properties, and some of those properties are
|
||||
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
|
||||
@@ -673,6 +673,40 @@ public:
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariants: Loan brokers are internally consistent
|
||||
*
|
||||
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
|
||||
* node (the root), which will only hold entries for `RippleState` or
|
||||
* `MPToken` objects.
|
||||
*
|
||||
*/
|
||||
class ValidLoanBroker
|
||||
{
|
||||
std::vector<SLE::const_pointer> brokers_;
|
||||
|
||||
bool
|
||||
goodZeroDirectory(
|
||||
ReadView const& view,
|
||||
SLE::const_ref dir,
|
||||
beast::Journal const& j) const;
|
||||
|
||||
public:
|
||||
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&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
@@ -694,7 +728,8 @@ using InvariantChecks = std::tuple<
|
||||
ValidMPTIssuance,
|
||||
ValidPermissionedDomain,
|
||||
NoModifiedUnmodifiableFields,
|
||||
ValidPseudoAccounts>;
|
||||
ValidPseudoAccounts,
|
||||
ValidLoanBroker>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
Reference in New Issue
Block a user