From 27951da62391cd0979f070715be9adadf07c49a1 Mon Sep 17 00:00:00 2001 From: Timur Ialymov Date: Wed, 19 Aug 2026 15:40:29 +0100 Subject: [PATCH] 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. --- src/test/app/vault/VaultBugs_test.cpp | 117 ++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/src/test/app/vault/VaultBugs_test.cpp b/src/test/app/vault/VaultBugs_test.cpp index 2dbd20f855..e2c216c764 100644 --- a/src/test/app/vault/VaultBugs_test.cpp +++ b/src/test/app/vault/VaultBugs_test.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +25,7 @@ #include #include +#include #include #include #include @@ -792,6 +796,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 @@ -804,6 +919,8 @@ public: testVaultWithdrawCanonicalizeToZero(); testBugVaultDustDebitCanonicalizesToNoOp(); testVaultDepositNegativeBalanceFromOppositeLimit(); + testCredentialPinsPseudoAccount(); + testCredentialPinOverflow(); testBug6LimitBypassWithShares(); } };