mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
13 Commits
a1q123456/
...
FN-36-cred
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea93252e1 | ||
|
|
cb0be1e631 | ||
|
|
55d7e389d0 | ||
|
|
27951da623 | ||
|
|
76d9db1f1d | ||
|
|
0107671846 | ||
|
|
ddfbaf6f71 | ||
|
|
e9d9f1f8e9 | ||
|
|
0ab294ed96 | ||
|
|
c7c42559cc | ||
|
|
b604857da6 | ||
|
|
9925144643 | ||
|
|
fa6a96e6d8 |
@@ -366,6 +366,7 @@ words:
|
||||
- venv
|
||||
- vfalco
|
||||
- vinnie
|
||||
- vkeylet
|
||||
- wasmi
|
||||
- wextra
|
||||
- wptr
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <xrpl/protocol/STVector256.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
@@ -33,6 +34,32 @@ checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
|
||||
[[nodiscard]] TER
|
||||
deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Remove credentials pinned to a pseudo-account's owner directory.
|
||||
*
|
||||
* Cleans up credentials that were linked to a pseudo-account (Vault, LoanBroker,
|
||||
* AMM), which such an account can neither accept nor delete. Only credentials
|
||||
* are removed; every other object is left in place. The walk visits at most
|
||||
* @p maxNodesToDelete directory entries and charges the ones it leaves alone
|
||||
* against that budget too, so a directory holding other objects yields fewer
|
||||
* than @p maxNodesToDelete deletions. On reaching the bound the result is
|
||||
* `tecINCOMPLETE` and the caller must propagate it so a later transaction
|
||||
* resumes.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param pseudoAcct The pseudo-account whose directory is cleaned.
|
||||
* @param maxNodesToDelete Upper bound on directory entries processed in one call.
|
||||
* @param j Journal for diagnostics.
|
||||
* @return tesSUCCESS once no credentials remain, tecINCOMPLETE if the bound was
|
||||
* reached, or a deletion error.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deletePseudoAccountCredentials(
|
||||
ApplyView& view,
|
||||
AccountID const& pseudoAcct,
|
||||
std::uint16_t maxNodesToDelete,
|
||||
beast::Journal j);
|
||||
|
||||
// Amendment and parameters checks for sfCredentialIDs field
|
||||
NotTEC
|
||||
checkFields(STTx const& tx, Rules const& rules, beast::Journal j);
|
||||
|
||||
@@ -396,6 +396,16 @@ using TxID = uint256;
|
||||
*/
|
||||
constexpr std::uint16_t kMaxDeletableAmmTrustLines = 512;
|
||||
|
||||
/**
|
||||
* The maximum number of owner-directory entries to walk when clearing
|
||||
* credentials pinned to a pseudo-account, in a single transaction.
|
||||
*
|
||||
* The walk stops after this many entries whether or not each one turns out to
|
||||
* be a credential, so a directory that also holds other objects yields fewer
|
||||
* deletions per transaction.
|
||||
*/
|
||||
constexpr std::uint16_t kMaxDeletablePseudoAccountCredentials = 512;
|
||||
|
||||
/**
|
||||
* The maximum length of a URI inside an Oracle
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -690,6 +691,12 @@ deleteAMMTrustLines(
|
||||
|
||||
return {deleteAMMTrustLine(sb, sleItem, ammAccountID, j), SkipEntry::No};
|
||||
}
|
||||
// A credential naming the pseudo-account as subject can't be
|
||||
// accepted or deleted by it and would otherwise permanently pin the
|
||||
// AMM. Clean it up here, inside the same bounded walk, so the
|
||||
// pinned AMM can still be deleted.
|
||||
if (sb.rules().enabled(fixCleanup3_4_0) && nodeType == ltCREDENTIAL)
|
||||
return {credentials::deleteSLE(sb, sleItem, j), SkipEntry::No};
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "deleteAMMObjects: deleting non-trustline or non-MPT " << nodeType;
|
||||
return {tecINTERNAL, SkipEntry::No};
|
||||
@@ -767,6 +774,8 @@ deleteAMMAccount(Sandbox& sb, Asset const& asset, Asset const& asset2, beast::Jo
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// deleteAMMTrustLines also removes any credentials pinned to the AMM
|
||||
// pseudo-account, within its bounded walk.
|
||||
if (auto const ter = deleteAMMTrustLines(sb, ammAccountID, kMaxDeletableAmmTrustLines, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -908,6 +917,11 @@ isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID c
|
||||
++nMPT;
|
||||
continue;
|
||||
}
|
||||
// A credential naming the pseudo-account as subject can be pinned
|
||||
// to its owner directory. Ignore it here; deleteAMMTrustLines
|
||||
// removes it when the AMM is deleted.
|
||||
if (view.rules().enabled(fixCleanup3_4_0) && entryType == ltCREDENTIAL)
|
||||
continue;
|
||||
if (entryType != ltRIPPLE_STATE)
|
||||
return std::unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
auto const lowLimit = sle->getFieldAmount(sfLowLimit);
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -127,6 +129,36 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
deletePseudoAccountCredentials(
|
||||
ApplyView& view,
|
||||
AccountID const& pseudoAcct,
|
||||
std::uint16_t maxNodesToDelete,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
isPseudoAccount(view.read(keylet::account(pseudoAcct))),
|
||||
"xrpl::credentials::deletePseudoAccountCredentials : is a pseudo-account");
|
||||
|
||||
// Delete the credentials linked into the pseudo-account's owner directory,
|
||||
// visiting at most maxNodesToDelete entries. Any other object is left in
|
||||
// place; the caller's own checks decide whether the remaining directory
|
||||
// blocks deletion. If the bound is reached, cleanupOnAccountDelete returns
|
||||
// tecINCOMPLETE and the caller propagates it so a later transaction resumes.
|
||||
return cleanupOnAccountDelete(
|
||||
view,
|
||||
keylet::ownerDir(pseudoAcct),
|
||||
[&view, &j](LedgerEntryType nodeType, uint256 const&, SLE::pointer& sleItem)
|
||||
-> std::pair<TER, SkipEntry> {
|
||||
if (nodeType == ltCREDENTIAL)
|
||||
return {deleteSLE(view, sleItem, j), SkipEntry::No};
|
||||
|
||||
return {tesSUCCESS, SkipEntry::Yes};
|
||||
},
|
||||
j,
|
||||
maxNodesToDelete);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
checkFields(STTx const& tx, Rules const& rules, beast::Journal j)
|
||||
{
|
||||
|
||||
@@ -1246,7 +1246,7 @@ removeExpiredNFTokenOffers(
|
||||
}
|
||||
|
||||
static void
|
||||
removeExpiredCredentials(ApplyView& view, std::vector<uint256> const& creds, beast::Journal viewJ)
|
||||
removeDeletedCredentials(ApplyView& view, std::vector<uint256> const& creds, beast::Journal viewJ)
|
||||
{
|
||||
for (auto const& index : creds)
|
||||
{
|
||||
@@ -1255,7 +1255,7 @@ removeExpiredCredentials(ApplyView& view, std::vector<uint256> const& creds, bea
|
||||
if (auto const ter = credentials::deleteSLE(view, sle, viewJ); !isTesSuccess(ter))
|
||||
{
|
||||
JLOG(viewJ.error())
|
||||
<< "removeExpiredCredentials: failed to delete expired credential. Err: "
|
||||
<< "removeDeletedCredentials: failed to delete credential. Err: "
|
||||
<< transToken(ter);
|
||||
}
|
||||
}
|
||||
@@ -1437,7 +1437,8 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
// should be used, making it possible to do more useful work
|
||||
// when transactions fail with a `tec` code.
|
||||
|
||||
auto typesForResult = [](TER const ter) {
|
||||
auto typesForResult = [credentialCleanup =
|
||||
view().rules().enabled(fixCleanup3_4_0)](TER const ter) {
|
||||
std::unordered_set<LedgerEntryType> types;
|
||||
if ((ter == tecOVERSIZE) || (ter == tecKILLED))
|
||||
{
|
||||
@@ -1446,6 +1447,11 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
else if (ter == tecINCOMPLETE)
|
||||
{
|
||||
types.insert(ltRIPPLE_STATE);
|
||||
// A bounded pseudo-account credential cleanup (VaultDelete /
|
||||
// LoanBrokerDelete) persists its partial credential deletions so a
|
||||
// later transaction can resume.
|
||||
if (credentialCleanup)
|
||||
types.insert(ltCREDENTIAL);
|
||||
}
|
||||
else if (ter == tecEXPIRED)
|
||||
{
|
||||
@@ -1523,7 +1529,7 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
|
||||
removeDeletedTrustLines(view(), ids, viewJ);
|
||||
break;
|
||||
case ltCREDENTIAL:
|
||||
removeExpiredCredentials(view(), ids, viewJ);
|
||||
removeDeletedCredentials(view(), ids, viewJ);
|
||||
break;
|
||||
// LCOV_EXCL_START
|
||||
default:
|
||||
|
||||
@@ -234,6 +234,14 @@ ValidMPTIssuance::finalize(
|
||||
|
||||
if (hasPrivilege(tx, Privilege::DestroyMptIssuance))
|
||||
{
|
||||
// A VaultDelete that is still cleaning up credentials pinned to its
|
||||
// pseudo-account returns tecINCOMPLETE and has not yet reached the
|
||||
// share issuance. Don't require the issuance to be removed until
|
||||
// the deletion completes (a later transaction).
|
||||
if (rules.enabled(fixCleanup3_4_0) && txnType == ttVAULT_DELETE &&
|
||||
result == tecINCOMPLETE)
|
||||
return mptIssuancesDeleted_ == 0 && mptIssuancesCreated_ == 0;
|
||||
|
||||
if (mptIssuancesDeleted_ == 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -140,6 +142,19 @@ LoanBrokerDelete::doApply()
|
||||
|
||||
auto const brokerPseudoID = broker->at(sfAccount);
|
||||
|
||||
// Remove any credentials pinned to the broker pseudo-account before anything
|
||||
// else. They would otherwise keep its owner directory alive and block
|
||||
// deletion with tecHAS_OBLIGATIONS. Doing it first means a bounded,
|
||||
// tecINCOMPLETE cleanup can be resumed by a later transaction without having
|
||||
// already torn down the broker.
|
||||
if (view().rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter = credentials::deletePseudoAccountCredentials(
|
||||
view(), brokerPseudoID, kMaxDeletablePseudoAccountCredentials, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (!view().dirRemove(
|
||||
keylet::ownerDir(accountID_), broker->at(sfOwnerNode), broker->key(), false))
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -100,6 +101,19 @@ VaultDelete::doApply()
|
||||
if (!vault)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Remove any credentials pinned to the vault pseudo-account before anything
|
||||
// else. They would otherwise keep its owner directory alive and block
|
||||
// deletion with tecHAS_OBLIGATIONS. Doing it first means a bounded,
|
||||
// tecINCOMPLETE cleanup can be resumed by a later transaction without having
|
||||
// already torn down the vault.
|
||||
if (view().rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter = credentials::deletePseudoAccountCredentials(
|
||||
view(), vault->at(sfAccount), kMaxDeletablePseudoAccountCredentials, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
// Destroy the asset holding.
|
||||
auto asset = vault->at(sfAsset);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
@@ -5192,6 +5193,51 @@ private:
|
||||
{features});
|
||||
}
|
||||
|
||||
void
|
||||
testCredentialPinsPseudoAccount()
|
||||
{
|
||||
testcase("Credential pins AMM pseudo-account");
|
||||
|
||||
using namespace jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
|
||||
// A credential issued to an AMM pseudo-account can't be accepted or
|
||||
// deleted by it. A pin created before the cure activates stays pinned
|
||||
// in the pseudo-account's owner directory and makes AMM deletion fail
|
||||
// with tecINTERNAL (deleteAMMTrustLines rejects the unexpected
|
||||
// directory entry).
|
||||
Account const attacker{"attacker"};
|
||||
char const credType[] = "FN36";
|
||||
|
||||
Env env(*this, all - fixCleanup3_3_0 - fixCleanup3_4_0);
|
||||
fund(env, gw_, {alice_}, XRP(20'000), {USD(10'000)});
|
||||
env.fund(XRP(1'000), attacker);
|
||||
env.close();
|
||||
|
||||
AMM amm(env, alice_, XRP(10'000), USD(10'000));
|
||||
Account const ammAcct{"amm pseudo-account", amm.ammAccount()};
|
||||
env.memoize(ammAcct);
|
||||
|
||||
env(credentials::create(ammAcct, attacker, credType));
|
||||
env.close();
|
||||
auto const credKey = credentials::keylet(ammAcct, attacker, credType);
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
|
||||
// Emptying the AMM would auto-delete it, but the pinned credential makes
|
||||
// deleteAMMAccount fail; the withdraw is rolled back and the AMM stays.
|
||||
amm.withdrawAll(alice_, std::nullopt, Ter(tecINTERNAL));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
|
||||
env.enableFeature(fixCleanup3_4_0);
|
||||
env.close();
|
||||
|
||||
// The pre-existing pin is cleaned up and the AMM deletes.
|
||||
amm.withdrawAll(alice_);
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
BEAST_EXPECT(!env.le(credKey));
|
||||
BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
|
||||
}
|
||||
|
||||
void
|
||||
testAutoDelete()
|
||||
{
|
||||
@@ -7459,6 +7505,7 @@ private:
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testInvalidInstance();
|
||||
testInstanceCreate();
|
||||
testCredentialPinsPseudoAccount();
|
||||
for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}))
|
||||
testInvalidDeposit(f);
|
||||
testDeposit();
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
@@ -2871,6 +2872,126 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
runTestCases(all_ - fixCleanup3_2_0);
|
||||
}
|
||||
|
||||
void
|
||||
testCredentialPinsPseudoAccount()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loan_broker;
|
||||
|
||||
// A credential issued to a LoanBroker pseudo-account can't be accepted
|
||||
// or deleted by it, so it stays pinned in the pseudo-account's owner
|
||||
// directory and blocks LoanBrokerDelete with tecHAS_OBLIGATIONS. A pin
|
||||
// created before the cure activates is removed by LoanBrokerDelete once
|
||||
// it does.
|
||||
Account const alice{"alice"}; // vault & broker owner
|
||||
Account const attacker{"attacker"};
|
||||
char const credType[] = "FN36";
|
||||
|
||||
Env env{*this, all_ - fixCleanup3_3_0 - fixCleanup3_4_0};
|
||||
env.fund(XRP(1'000'000), alice, attacker);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [vtx, vkeylet] = vault.create({.owner = alice, .asset = xrpIssue()});
|
||||
env(vtx);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(vkeylet));
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice.id(), vkeylet.key));
|
||||
env.close();
|
||||
|
||||
auto const broker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(broker);
|
||||
Account const pseudo{"broker pseudo-account", broker->at(sfAccount)};
|
||||
env.memoize(pseudo);
|
||||
|
||||
testcase("Credential pins broker pseudo-account");
|
||||
env(credentials::create(pseudo, attacker, credType));
|
||||
env.close();
|
||||
|
||||
auto const credKey = credentials::keylet(pseudo, attacker, credType);
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 1);
|
||||
|
||||
env(del(alice.id(), brokerKeylet.key), Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
env.enableFeature(fixCleanup3_4_0);
|
||||
env.close();
|
||||
|
||||
// The pre-existing pin no longer blocks deletion; the credential is
|
||||
// cleaned up and the issuer's owner count is restored.
|
||||
testcase("LoanBrokerDelete removes pinned credential");
|
||||
env(del(alice.id(), brokerKeylet.key));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(credKey));
|
||||
BEAST_EXPECT(!env.le(brokerKeylet));
|
||||
BEAST_EXPECT(!env.le(keylet::account(pseudo.id())));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testCredentialPinOverflow()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loan_broker;
|
||||
testcase("Credential pin cleanup is bounded (tecINCOMPLETE)");
|
||||
|
||||
// A pseudo-account can be pinned with more credentials than one
|
||||
// transaction is allowed to clean up. LoanBrokerDelete then removes
|
||||
// them a bounded batch at a time, returning tecINCOMPLETE until the
|
||||
// last batch.
|
||||
Account const alice{"alice"};
|
||||
Account const attacker{"attacker"};
|
||||
|
||||
Env env{*this, all_ - fixCleanup3_3_0 - fixCleanup3_4_0};
|
||||
env.fund(XRP(10'000'000), alice, attacker);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [vtx, vkeylet] = vault.create({.owner = alice, .asset = xrpIssue()});
|
||||
env(vtx);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(vkeylet));
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
|
||||
env(set(alice.id(), vkeylet.key));
|
||||
env.close();
|
||||
|
||||
auto const broker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(broker);
|
||||
Account const pseudo{"broker pseudo-account", broker->at(sfAccount)};
|
||||
env.memoize(pseudo);
|
||||
|
||||
// Pin more than one cleanup batch's worth of credentials.
|
||||
std::uint16_t const count = kMaxDeletablePseudoAccountCredentials + 3;
|
||||
for (std::uint16_t i = 0; i < count; ++i)
|
||||
env(credentials::create(pseudo, attacker, std::to_string(i)));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == count);
|
||||
|
||||
env.enableFeature(fixCleanup3_4_0);
|
||||
env.close();
|
||||
|
||||
// First delete removes one bounded batch and reports it isn't finished.
|
||||
env(del(alice.id(), brokerKeylet.key), Ter(tecINCOMPLETE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(brokerKeylet)); // broker still exists
|
||||
auto const remaining = ownerCount(env, attacker);
|
||||
BEAST_EXPECT(remaining > 0 && remaining < count);
|
||||
|
||||
// Second delete finishes the cleanup and removes the broker.
|
||||
env(del(alice.id(), brokerKeylet.key));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(brokerKeylet));
|
||||
BEAST_EXPECT(!env.le(keylet::account(pseudo.id())));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 0);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -2889,6 +3010,8 @@ public:
|
||||
|
||||
testDisabled();
|
||||
testLifecycle();
|
||||
testCredentialPinsPseudoAccount();
|
||||
testCredentialPinOverflow();
|
||||
testInvalidLoanBrokerDelete();
|
||||
testInvalidLoanBrokerSet();
|
||||
testRequireAuth();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/pay.h>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -972,6 +974,117 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCredentialPinsPseudoAccount()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
// A credential issued to a vault pseudo-account can't be accepted or
|
||||
// deleted by it (pseudo-accounts can't sign), so it stays pinned in the
|
||||
// pseudo-account's owner directory and blocks VaultDelete with
|
||||
// tecHAS_OBLIGATIONS. A pin created before the cure activates is removed
|
||||
// by VaultDelete once it does.
|
||||
Account const owner{"owner"};
|
||||
Account const attacker{"attacker"};
|
||||
char const credType[] = "FN36";
|
||||
|
||||
Env env{*this, all_ - fixCleanup3_3_0 - fixCleanup3_4_0};
|
||||
env.fund(XRP(1'000'000), owner, attacker);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
PrettyAsset const asset = xrpIssue();
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const vaultSle = env.le(keylet);
|
||||
BEAST_EXPECT(vaultSle);
|
||||
Account const pseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
|
||||
env.memoize(pseudo);
|
||||
|
||||
// The pseudo-account owns the share issuance; the pin must not change
|
||||
// its owner count (an unaccepted credential is owned by the issuer).
|
||||
auto const pseudoOwnerCount = ownerCount(env, pseudo);
|
||||
|
||||
testcase("Credential pins vault pseudo-account");
|
||||
env(credentials::create(pseudo, attacker, credType));
|
||||
env.close();
|
||||
|
||||
auto const credKey = credentials::keylet(pseudo, attacker, credType);
|
||||
BEAST_EXPECT(env.le(credKey));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, pseudo) == pseudoOwnerCount);
|
||||
|
||||
// The pin blocks deletion of an otherwise-empty vault.
|
||||
env(vault.del({.owner = owner, .id = keylet.key}), Ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
env.enableFeature(fixCleanup3_4_0);
|
||||
env.close();
|
||||
|
||||
// The pre-existing pin no longer blocks deletion; the credential is
|
||||
// cleaned up and the issuer's owner count is restored.
|
||||
testcase("VaultDelete removes pinned credential");
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(credKey));
|
||||
BEAST_EXPECT(!env.le(keylet));
|
||||
BEAST_EXPECT(!env.le(::xrpl::keylet::account(pseudo.id())));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testCredentialPinOverflow()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase("Credential pin cleanup is bounded (tecINCOMPLETE)");
|
||||
|
||||
// A pseudo-account can be pinned with more credentials than one
|
||||
// transaction is allowed to clean up. VaultDelete then removes them a
|
||||
// bounded batch at a time, returning tecINCOMPLETE until the last batch.
|
||||
Account const owner{"owner"};
|
||||
Account const attacker{"attacker"};
|
||||
|
||||
Env env{*this, all_ - fixCleanup3_3_0 - fixCleanup3_4_0};
|
||||
env.fund(XRP(10'000'000), owner, attacker);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
|
||||
env(tx);
|
||||
env.close();
|
||||
auto const vaultSle = env.le(keylet);
|
||||
BEAST_EXPECT(vaultSle);
|
||||
Account const pseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
|
||||
env.memoize(pseudo);
|
||||
|
||||
// Pin more than one cleanup batch's worth of credentials.
|
||||
std::uint16_t const count = kMaxDeletablePseudoAccountCredentials + 3;
|
||||
for (std::uint16_t i = 0; i < count; ++i)
|
||||
env(credentials::create(pseudo, attacker, std::to_string(i)));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == count);
|
||||
|
||||
env.enableFeature(fixCleanup3_4_0);
|
||||
env.close();
|
||||
|
||||
// First delete removes one bounded batch and reports it isn't finished.
|
||||
env(vault.del({.owner = owner, .id = keylet.key}), Ter(tecINCOMPLETE));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet)); // vault still exists
|
||||
auto const remaining = ownerCount(env, attacker);
|
||||
BEAST_EXPECT(remaining > 0 && remaining < count);
|
||||
|
||||
// Second delete finishes the cleanup and removes the vault.
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet));
|
||||
BEAST_EXPECT(!env.le(::xrpl::keylet::account(pseudo.id())));
|
||||
BEAST_EXPECT(ownerCount(env, attacker) == 0);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -985,6 +1098,8 @@ public:
|
||||
testVaultWithdrawCanonicalizeToZero();
|
||||
testBugVaultDustDebitCanonicalizesToNoOp();
|
||||
testVaultDepositNegativeBalanceFromOppositeLimit();
|
||||
testCredentialPinsPseudoAccount();
|
||||
testCredentialPinOverflow();
|
||||
testBug6LimitBypassWithShares();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user