Compare commits

...

13 Commits

Author SHA1 Message Date
Timur Ialymov
bea93252e1 Merge remote-tracking branch 'origin/develop' into FN-36-credential_pins_pseudo_account 2026-08-21 17:24:12 +01:00
Timur Ialymov
cb0be1e631 Merge remote-tracking branch 'origin/develop' into FN-36-credential_pins_pseudo_account
# Conflicts:
#	src/test/app/lending/LoanBroker_test.cpp
2026-08-20 17:55:48 +01:00
Timur Ialymov
55d7e389d0 test: Include the header the credential pin bound comes from
clang-tidy's include-cleaner wants the header that declares kMaxDeletablePseudoAccountCredentials to
be included directly, not reached through VaultTestBase.h.
2026-08-20 14:36:22 +01:00
Timur Ialymov
27951da623 test: Re-home the credential pin tests into the vault bug suite
develop split Vault_test.cpp into topical suites under src/test/app/vault/, which removed the file
these two regression tests lived in. They cover a bug, so they follow the neighbouring bug tests
into VaultBugs_test.cpp; the suite's includes grow to cover ownerCount, the credentials helpers,
keylet::account and std::uint16_t, none of which the file used before.
2026-08-19 15:40:29 +01:00
Timur Ialymov
76d9db1f1d Merge remote-tracking branch 'origin/develop' into FN-36-credential_pins_pseudo_account
# Conflicts:
#	src/test/app/Vault_test.cpp
2026-08-19 15:08:06 +01:00
Timur Ialymov
0107671846 fix: Clarify the cleanup bound and narrow the invariant relaxation
The bound passed to the pseudo-account credential cleanup limits how many
owner-directory entries the walk visits, not how many credentials it deletes:
entries the walk leaves alone are charged against the same budget. Both comments
claimed the latter, so say what the code actually does.

Narrow the tecINCOMPLETE relaxation in the MPT issuance invariant to VaultDelete.
That is the only transaction carrying the DestroyMptIssuance privilege that can
return tecINCOMPLETE, and the comment above the branch already said as much, so
the code now matches it and any future transaction with the same privilege stays
under the strict rule.

Rename removeExpiredCredentials to removeDeletedCredentials. It now also deletes
credentials removed by a bounded cleanup rather than by expiry, which is the same
reason removeDeletedTrustLines carries that name.
2026-08-18 18:51:03 +01:00
Timur Ialymov
ddfbaf6f71 Merge remote-tracking branch 'origin/develop' into FN-36-credential_pins_pseudo_account
# Conflicts:
#	src/libxrpl/ledger/helpers/AMMHelpers.cpp
2026-08-18 18:04:41 +01:00
Vito Tumas
e9d9f1f8e9 Merge branch 'develop' into FN-36-credential_pins_pseudo_account 2026-08-13 22:05:45 +02:00
Vito Tumas
0ab294ed96 Merge branch 'develop' into FN-36-credential_pins_pseudo_account 2026-08-13 10:25:12 +02:00
Timur Ialymov
c7c42559cc fix: Include <string> directly in LoanBroker_test.cpp
The new overflow test calls std::to_string but only relied on a transitive
include for it, which clang-tidy's include-cleaner check flagged in CI.
2026-08-10 14:29:18 +01:00
Timur Ialymov
b604857da6 fix: Remove duplicate pseudo-account rejection from CredentialCreate
CredentialCreate::preclaim already rejects a pseudo-account Subject via a
separate amendment merged into develop while this branch was in review.
Drop the duplicate check added here and keep only the credential cleanup
this branch is actually for.

Adjust the Vault, LoanBroker, and AMM tests to build the pre-existing pin
with that other amendment disabled instead of this one, and drop the
now-redundant assertions that a new pin gets rejected -- that is covered
elsewhere now.

Add an overflow test for LoanBroker mirroring the existing Vault one,
exercising the tecINCOMPLETE branch a reviewer flagged as uncovered.

Fix an unrelated SeqProxy/namespace-rename mismatch in LoanBroker_test.cpp
introduced by the same develop merge, and rewrite comments across the
changed files to describe the rule rather than name the amendment or
ticket.
2026-08-10 13:22:33 +01:00
Timur Ialymov
9925144643 Merge remote-tracking branch 'origin/develop' into FN-36-credential_pins_pseudo_account 2026-08-10 12:45:15 +01:00
Timur Ialymov
fa6a96e6d8 fix: Reject credentials on pseudo-accounts and unpin Vault/LoanBroker (FN-36)
CredentialCreate accepted any existing Subject, including Vault, LoanBroker, and
AMM pseudo-accounts. A pseudo-account can't sign, so it can never accept or
delete a credential issued to it; the unaccepted credential stays pinned in the
pseudo-account's owner directory and blocks deletion of the owning object,
locking it and its owner reserve indefinitely.

Behind fixCleanup3_4_0:
- CredentialCreate::preclaim rejects a pseudo-account Subject with
  tecPSEUDO_ACCOUNT.
- VaultDelete and LoanBrokerDelete remove any credentials pinned to the
  pseudo-account before deleting it.
- isOnlyLiquidityProvider ignores credential entries and deleteAMMAccount removes
  them, so an AMM pinned before the amendment can still be withdrawn and deleted.

This clears objects pinned before the amendment and prevents new pins after it.
2026-08-03 10:38:35 +01:00
12 changed files with 416 additions and 4 deletions

View File

@@ -366,6 +366,7 @@ words:
- venv
- vfalco
- vinnie
- vkeylet
- wasmi
- wextra
- wptr

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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:

View File

@@ -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 "

View File

@@ -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))
{

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
}
};