Files
rippled/src/test/app/AccountDelete_test.cpp
Pratik Mankawde 9b332d88c1 refactor: Retire PayChanRecipientOwnerDir amendment (#5946)
Amendments activated for more than 2 years can be retired. This change retires the PayChanRecipientOwnerDir amendment.
2025-11-11 09:07:45 -05:00

1166 lines
42 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <test/jtx.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
class AccountDelete_test : public beast::unit_test::suite
{
private:
// Helper function that verifies the expected DeliveredAmount is present.
//
// NOTE: the function _infers_ the transaction to operate on by calling
// env.tx(), which returns the result from the most recent transaction.
void
verifyDeliveredAmount(jtx::Env& env, STAmount const& amount)
{
// Get the hash for the most recent transaction.
std::string const txHash{
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
// Verify DeliveredAmount and delivered_amount metadata are correct.
// We can't use env.meta() here, because meta() doesn't include
// delivered_amount.
env.close();
Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
// Expect there to be a DeliveredAmount field.
if (!BEAST_EXPECT(meta.isMember(sfDeliveredAmount.jsonName)))
return;
// DeliveredAmount and delivered_amount should both be present and
// equal amount.
Json::Value const jsonExpect{amount.getJson(JsonOptions::none)};
BEAST_EXPECT(meta[sfDeliveredAmount.jsonName] == jsonExpect);
BEAST_EXPECT(meta[jss::delivered_amount] == jsonExpect);
}
// Helper function to create a payment channel.
static Json::Value
payChanCreate(
jtx::Account const& account,
jtx::Account const& to,
STAmount const& amount,
NetClock::duration const& settleDelay,
NetClock::time_point const& cancelAfter,
PublicKey const& pk)
{
Json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = account.human();
jv[jss::Destination] = to.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[sfSettleDelay.jsonName] = settleDelay.count();
jv[sfCancelAfter.jsonName] = cancelAfter.time_since_epoch().count() + 2;
jv[sfPublicKey.jsonName] = strHex(pk.slice());
return jv;
};
public:
void
testBasics()
{
using namespace jtx;
testcase("Basics");
Env env{*this};
Account const alice("alice");
Account const becky("becky");
Account const carol("carol");
Account const gw("gw");
env.fund(XRP(10000), alice, becky, carol, gw);
env.close();
// Alice can't delete her account and then give herself the XRP.
env(acctdelete(alice, alice), ter(temDST_IS_SRC));
// alice can't delete her account with a negative fee.
env(acctdelete(alice, becky), fee(drops(-1)), ter(temBAD_FEE));
// Invalid flags.
env(acctdelete(alice, becky),
txflags(tfImmediateOrCancel),
ter(temINVALID_FLAG));
// Account deletion has a high fee. Make sure the fee requirement
// behaves as we expect.
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, becky), ter(telINSUF_FEE_P));
// Try a fee one drop less than the required amount.
env(acctdelete(alice, becky),
fee(acctDelFee - drops(1)),
ter(telINSUF_FEE_P));
// alice's account is created too recently to be deleted.
env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON));
// Give becky a trustline. She is no longer deletable.
env(trust(becky, gw["USD"](1000)));
env.close();
// Give carol a deposit preauthorization, an offer, a ticket,
// a signer list, and a DID. Even with all that she's still deletable.
env(deposit::auth(carol, becky));
std::uint32_t const carolOfferSeq{env.seq(carol)};
env(offer(carol, gw["USD"](51), XRP(51)));
std::uint32_t const carolTicketSeq{env.seq(carol) + 1};
env(ticket::create(carol, 1));
env(signers(carol, 1, {{alice, 1}, {becky, 1}}));
env(did::setValid(carol));
// Deleting should fail with TOO_SOON, which is a relatively
// cheap check compared to validating the contents of her directory.
env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON));
// Close enough ledgers to almost be able to delete alice's account.
incLgrSeqForAccDel(env, alice, 1);
// alice's account is still created too recently to be deleted.
env(acctdelete(alice, becky), fee(acctDelFee), ter(tecTOO_SOON));
// The most recent delete attempt advanced alice's sequence. So
// close two ledgers and her account should be deletable.
env.close();
env.close();
{
auto const aliceOldBalance{env.balance(alice)};
auto const beckyOldBalance{env.balance(becky)};
// Verify that alice's account exists but she has no directory.
BEAST_EXPECT(env.closed()->exists(keylet::account(alice.id())));
BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(alice.id())));
env(acctdelete(alice, becky), fee(acctDelFee));
verifyDeliveredAmount(env, aliceOldBalance - acctDelFee);
env.close();
// Verify that alice's account and directory are actually gone.
BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id())));
BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(alice.id())));
// Verify that alice's XRP, minus the fee, was transferred to becky.
BEAST_EXPECT(
env.balance(becky) ==
aliceOldBalance + beckyOldBalance - acctDelFee);
}
// Attempt to delete becky's account but get stopped by the trust line.
env(acctdelete(becky, carol), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
// Verify that becky's account is still there by giving her a regular
// key. This has the side effect of setting the lsfPasswordSpent bit
// on her account root.
Account const beck("beck");
env(regkey(becky, beck), fee(drops(0)));
env.close();
// Show that the lsfPasswordSpent bit is set by attempting to change
// becky's regular key for free again. That fails.
Account const reb("reb");
env(regkey(becky, reb), sig(becky), fee(drops(0)), ter(telINSUF_FEE_P));
// Close enough ledgers that becky's failing regkey transaction is
// no longer retried.
for (int i = 0; i < 8; ++i)
env.close();
{
auto const beckyOldBalance{env.balance(becky)};
auto const carolOldBalance{env.balance(carol)};
// Verify that Carol's account, directory, deposit
// preauthorization, offer, ticket, and signer list exist.
BEAST_EXPECT(env.closed()->exists(keylet::account(carol.id())));
BEAST_EXPECT(env.closed()->exists(keylet::ownerDir(carol.id())));
BEAST_EXPECT(env.closed()->exists(
keylet::depositPreauth(carol.id(), becky.id())));
BEAST_EXPECT(
env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq)));
BEAST_EXPECT(env.closed()->exists(
keylet::ticket(carol.id(), carolTicketSeq)));
BEAST_EXPECT(env.closed()->exists(keylet::signers(carol.id())));
// Delete carol's account even with stuff in her directory. Show
// that multisigning for the delete does not increase carol's fee.
env(acctdelete(carol, becky), fee(acctDelFee), msig(alice));
verifyDeliveredAmount(env, carolOldBalance - acctDelFee);
env.close();
// Verify that Carol's account, directory, and other stuff are gone.
BEAST_EXPECT(!env.closed()->exists(keylet::account(carol.id())));
BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(carol.id())));
BEAST_EXPECT(!env.closed()->exists(
keylet::depositPreauth(carol.id(), becky.id())));
BEAST_EXPECT(!env.closed()->exists(
keylet::offer(carol.id(), carolOfferSeq)));
BEAST_EXPECT(!env.closed()->exists(
keylet::ticket(carol.id(), carolTicketSeq)));
BEAST_EXPECT(!env.closed()->exists(keylet::signers(carol.id())));
// Verify that Carol's XRP, minus the fee, was transferred to becky.
BEAST_EXPECT(
env.balance(becky) ==
carolOldBalance + beckyOldBalance - acctDelFee);
// Since becky received an influx of XRP, her lsfPasswordSpent bit
// is cleared and she can change her regular key for free again.
env(regkey(becky, reb), sig(becky), fee(drops(0)));
}
}
void
testDirectories()
{
// The code that deletes consecutive directory entries uses a
// peculiarity of the implementation. Make sure that peculiarity
// behaves as expected across owner directory pages.
using namespace jtx;
testcase("Directories");
Env env{*this};
Account const alice("alice");
Account const gw("gw");
env.fund(XRP(10000), alice, gw);
env.close();
// Alice creates enough offers to require two owner directories.
for (int i{0}; i < 45; ++i)
{
env(offer(alice, gw["USD"](1), XRP(1)));
env.close();
}
env.require(offers(alice, 45));
// Close enough ledgers to be able to delete alice's account.
incLgrSeqForAccDel(env, alice);
// Verify that both directory nodes exist.
Keylet const aliceRootKey{keylet::ownerDir(alice.id())};
Keylet const alicePageKey{keylet::page(aliceRootKey, 1)};
BEAST_EXPECT(env.closed()->exists(aliceRootKey));
BEAST_EXPECT(env.closed()->exists(alicePageKey));
// Delete alice's account.
auto const acctDelFee{drops(env.current()->fees().increment)};
auto const aliceBalance{env.balance(alice)};
env(acctdelete(alice, gw), fee(acctDelFee));
verifyDeliveredAmount(env, aliceBalance - acctDelFee);
env.close();
// Both of alice's directory nodes should be gone.
BEAST_EXPECT(!env.closed()->exists(aliceRootKey));
BEAST_EXPECT(!env.closed()->exists(alicePageKey));
}
void
testOwnedTypes()
{
using namespace jtx;
testcase("Owned types");
// We want to test PayChannels with the backlink.
Env env{*this, testable_amendments()};
Account const alice("alice");
Account const becky("becky");
Account const gw("gw");
env.fund(XRP(100000), alice, becky, gw);
env.close();
// Give alice and becky a bunch of offers that we have to search
// through before we figure out that there's a non-deletable
// entry in their directory.
for (int i{0}; i < 200; ++i)
{
env(offer(alice, gw["USD"](1), XRP(1)));
env(offer(becky, gw["USD"](1), XRP(1)));
env.close();
}
env.require(offers(alice, 200));
env.require(offers(becky, 200));
// Close enough ledgers to be able to delete alice's and becky's
// accounts.
incLgrSeqForAccDel(env, alice);
incLgrSeqForAccDel(env, becky);
// alice writes a check to becky. Until that check is cashed or
// canceled it will prevent alice's and becky's accounts from being
// deleted.
uint256 const checkId = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, becky, XRP(1)));
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env(acctdelete(becky, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
// Cancel the check, but add an escrow. Again, with the escrow
// on board, alice and becky should not be able to delete their
// accounts.
env(check::cancel(becky, checkId));
env.close();
using namespace std::chrono_literals;
std::uint32_t const escrowSeq{env.seq(alice)};
env(escrow::create(alice, becky, XRP(333)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
// alice and becky should be unable to delete their accounts because
// of the escrow.
env(acctdelete(alice, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env(acctdelete(becky, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
// Now cancel the escrow, but create a payment channel between
// alice and becky.
bool const withTokenEscrow =
env.current()->rules().enabled(featureTokenEscrow);
if (withTokenEscrow)
{
Account const gw1("gw1");
Account const carol("carol");
auto const USD = gw1["USD"];
env.fund(XRP(100000), carol, gw1);
env(fset(gw1, asfAllowTrustLineLocking));
env.close();
env.trust(USD(10000), carol);
env.close();
env(pay(gw1, carol, USD(100)));
env.close();
std::uint32_t const escrowSeq{env.seq(carol)};
env(escrow::create(carol, becky, USD(1)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
incLgrSeqForAccDel(env, gw1);
env(acctdelete(gw1, becky),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
env.close();
env(escrow::cancel(becky, carol, escrowSeq));
env.close();
}
env(escrow::cancel(becky, alice, escrowSeq));
env.close();
Keylet const alicePayChanKey{
keylet::payChan(alice, becky, env.seq(alice))};
env(payChanCreate(
alice, becky, XRP(57), 4s, env.now() + 2s, alice.pk()));
env.close();
// With the PayChannel in place becky and alice should not be
// able to delete her account
auto const beckyBalance{env.balance(becky)};
env(acctdelete(alice, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env(acctdelete(becky, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
// Alice cancels her PayChannel, which will leave her with only offers
// in her directory.
// Lambda to close a PayChannel.
auto payChanClose = [](jtx::Account const& account,
Keylet const& payChanKeylet,
PublicKey const& pk) {
Json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Flags] = tfClose;
jv[jss::Account] = account.human();
jv[sfChannel.jsonName] = to_string(payChanKeylet.key);
jv[sfPublicKey.jsonName] = strHex(pk.slice());
return jv;
};
env(payChanClose(alice, alicePayChanKey, alice.pk()));
env.close();
// gw creates a PayChannel with alice as the destination, this should
// prevent alice from deleting her account.
Keylet const gwPayChanKey{keylet::payChan(gw, alice, env.seq(gw))};
env(payChanCreate(gw, alice, XRP(68), 4s, env.now() + 2s, alice.pk()));
env.close();
// alice can't delete her account because of the PayChannel.
env(acctdelete(alice, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
// alice closes the PayChannel which should (finally) allow her to
// delete her account.
env(payChanClose(alice, gwPayChanKey, alice.pk()));
env.close();
// Now alice can successfully delete her account.
auto const aliceBalance{env.balance(alice)};
env(acctdelete(alice, gw), fee(acctDelFee));
verifyDeliveredAmount(env, aliceBalance - acctDelFee);
env.close();
}
void
testAmendmentEnable()
{
// Start with the featureDeletableAccounts amendment disabled.
// Then enable the amendment and delete an account.
using namespace jtx;
testcase("Amendment enable");
Env env{*this, testable_amendments() - featureDeletableAccounts};
Account const alice("alice");
Account const becky("becky");
env.fund(XRP(10000), alice, becky);
env.close();
// Close enough ledgers to be able to delete alice's account.
incLgrSeqForAccDel(env, alice);
// Verify that alice's account root is present.
Keylet const aliceAcctKey{keylet::account(alice.id())};
BEAST_EXPECT(env.closed()->exists(aliceAcctKey));
auto const alicePreDelBal{env.balance(alice)};
auto const beckyPreDelBal{env.balance(becky)};
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, becky), fee(acctDelFee), ter(temDISABLED));
env.close();
// Verify that alice's account root is still present and alice and
// becky both have their XRP.
BEAST_EXPECT(env.current()->exists(aliceAcctKey));
BEAST_EXPECT(env.balance(alice) == alicePreDelBal);
BEAST_EXPECT(env.balance(becky) == beckyPreDelBal);
// When the amendment is enabled the previous transaction is
// retried into the new open ledger and succeeds.
env.enableFeature(featureDeletableAccounts);
env.close();
// alice's account is still in the most recently closed ledger.
BEAST_EXPECT(env.closed()->exists(aliceAcctKey));
// Verify that alice's account root is gone from the current ledger
// and becky has alice's XRP.
BEAST_EXPECT(!env.current()->exists(aliceAcctKey));
BEAST_EXPECT(
env.balance(becky) == alicePreDelBal + beckyPreDelBal - acctDelFee);
env.close();
BEAST_EXPECT(!env.closed()->exists(aliceAcctKey));
}
void
testTooManyOffers()
{
// Put enough offers in an account that we refuse to delete the account.
using namespace jtx;
testcase("Too many offers");
Env env{*this};
Account const alice("alice");
Account const gw("gw");
// Fund alice well so she can afford the reserve on the offers.
env.fund(XRP(10000000), alice, gw);
env.close();
// To increase the number of Books affected, change the currency of
// each offer.
std::string currency{"AAA"};
// Alice creates 1001 offers. This is one greater than the number of
// directory entries an AccountDelete will remove.
std::uint32_t const offerSeq0{env.seq(alice)};
constexpr int offerCount{1001};
for (int i{0}; i < offerCount; ++i)
{
env(offer(alice, gw[currency](1), XRP(1)));
env.close();
// Increment to next currency.
++currency[0];
if (currency[0] > 'Z')
{
currency[0] = 'A';
++currency[1];
}
if (currency[1] > 'Z')
{
currency[1] = 'A';
++currency[2];
}
if (currency[2] > 'Z')
{
currency[0] = 'A';
currency[1] = 'A';
currency[2] = 'A';
}
}
// Close enough ledgers to be able to delete alice's account.
incLgrSeqForAccDel(env, alice);
// Verify the existence of the expected ledger entries.
Keylet const aliceOwnerDirKey{keylet::ownerDir(alice.id())};
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(closed->exists(keylet::account(alice.id())));
BEAST_EXPECT(closed->exists(aliceOwnerDirKey));
// alice's directory nodes.
for (std::uint32_t i{0}; i < ((offerCount / 32) + 1); ++i)
BEAST_EXPECT(closed->exists(keylet::page(aliceOwnerDirKey, i)));
// alice's offers.
for (std::uint32_t i{0}; i < offerCount; ++i)
BEAST_EXPECT(
closed->exists(keylet::offer(alice.id(), offerSeq0 + i)));
}
// Delete alice's account. Should fail because she has too many
// offers in her directory.
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, gw), fee(acctDelFee), ter(tefTOO_BIG));
// Cancel one of alice's offers. Then the account delete can succeed.
env.require(offers(alice, offerCount));
env(offer_cancel(alice, offerSeq0));
env.close();
env.require(offers(alice, offerCount - 1));
// alice successfully deletes her account.
auto const alicePreDelBal{env.balance(alice)};
env(acctdelete(alice, gw), fee(acctDelFee));
verifyDeliveredAmount(env, alicePreDelBal - acctDelFee);
env.close();
// Verify that alice's account root is gone as well as her directory
// nodes and all of her offers.
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(!closed->exists(keylet::account(alice.id())));
BEAST_EXPECT(!closed->exists(aliceOwnerDirKey));
// alice's former directory nodes.
for (std::uint32_t i{0}; i < ((offerCount / 32) + 1); ++i)
BEAST_EXPECT(
!closed->exists(keylet::page(aliceOwnerDirKey, i)));
// alice's former offers.
for (std::uint32_t i{0}; i < offerCount; ++i)
BEAST_EXPECT(
!closed->exists(keylet::offer(alice.id(), offerSeq0 + i)));
}
}
void
testImplicitlyCreatedTrustline()
{
// Show that a trust line that is implicitly created by offer crossing
// prevents an account from being deleted.
using namespace jtx;
testcase("Implicitly created trust line");
Env env{*this};
Account const alice{"alice"};
Account const gw{"gw"};
auto const BUX{gw["BUX"]};
env.fund(XRP(10000), alice, gw);
env.close();
// alice creates an offer that, if crossed, will implicitly create
// a trust line.
env(offer(alice, BUX(30), XRP(30)));
env.close();
// gw crosses alice's offer. alice should end up with BUX(30).
env(offer(gw, XRP(30), BUX(30)));
env.close();
env.require(balance(alice, BUX(30)));
// Close enough ledgers to be able to delete alice's account.
incLgrSeqForAccDel(env, alice);
// alice and gw can't delete their accounts because of the implicitly
// created trust line.
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, gw), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
env(acctdelete(gw, alice), fee(acctDelFee), ter(tecHAS_OBLIGATIONS));
env.close();
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(closed->exists(keylet::account(alice.id())));
BEAST_EXPECT(closed->exists(keylet::account(gw.id())));
}
}
void
testBalanceTooSmallForFee()
{
// See what happens when an account with a balance less than the
// incremental reserve tries to delete itself.
using namespace jtx;
testcase("Balance too small for fee");
Env env{*this};
Account const alice("alice");
// Note that the fee structure for unit tests does not match the fees
// on the production network (October 2019). Unit tests have a base
// reserve of 200 XRP.
env.fund(env.current()->fees().reserve, noripple(alice));
env.close();
// Burn a chunk of alice's funds so she only has 1 XRP remaining in
// her account.
env(noop(alice), fee(env.balance(alice) - XRP(1)));
env.close();
auto const acctDelFee{drops(env.current()->fees().increment)};
BEAST_EXPECT(acctDelFee > env.balance(alice));
// alice attempts to delete her account even though she can't pay
// the full fee. She specifies a fee that is larger than her balance.
//
// The balance of env.master should not change.
auto const masterBalance{env.balance(env.master)};
env(acctdelete(alice, env.master),
fee(acctDelFee),
ter(terINSUF_FEE_B));
env.close();
{
std::shared_ptr<ReadView const> const closed{env.closed()};
BEAST_EXPECT(closed->exists(keylet::account(alice.id())));
BEAST_EXPECT(env.balance(env.master) == masterBalance);
}
// alice again attempts to delete her account. This time she specifies
// her current balance in XRP. Again the transaction fails.
BEAST_EXPECT(env.balance(alice) == XRP(1));
env(acctdelete(alice, env.master), fee(XRP(1)), ter(telINSUF_FEE_P));
env.close();
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(closed->exists(keylet::account(alice.id())));
BEAST_EXPECT(env.balance(env.master) == masterBalance);
}
}
void
testWithTickets()
{
testcase("With Tickets");
using namespace test::jtx;
Account const alice{"alice"};
Account const bob{"bob"};
Env env{*this};
env.fund(XRP(100000), alice, bob);
env.close();
// bob grabs as many tickets as he is allowed to have.
std::uint32_t const ticketSeq{env.seq(bob) + 1};
env(ticket::create(bob, 250));
env.close();
env.require(owners(bob, 250));
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(closed->exists(keylet::account(bob.id())));
for (std::uint32_t i = 0; i < 250; ++i)
{
BEAST_EXPECT(
closed->exists(keylet::ticket(bob.id(), ticketSeq + i)));
}
}
// Close enough ledgers to be able to delete bob's account.
incLgrSeqForAccDel(env, bob);
// bob deletes his account using a ticket. bob's account and all
// of his tickets should be removed from the ledger.
auto const acctDelFee{drops(env.current()->fees().increment)};
auto const bobOldBalance{env.balance(bob)};
env(acctdelete(bob, alice), ticket::use(ticketSeq), fee(acctDelFee));
verifyDeliveredAmount(env, bobOldBalance - acctDelFee);
env.close();
{
std::shared_ptr<ReadView const> closed{env.closed()};
BEAST_EXPECT(!closed->exists(keylet::account(bob.id())));
for (std::uint32_t i = 0; i < 250; ++i)
{
BEAST_EXPECT(
!closed->exists(keylet::ticket(bob.id(), ticketSeq + i)));
}
}
}
void
testDest()
{
testcase("Destination Constraints");
using namespace test::jtx;
Account const alice{"alice"};
Account const becky{"becky"};
Account const carol{"carol"};
Account const daria{"daria"};
Env env{*this};
env.fund(XRP(100000), alice, becky, carol);
env.close();
// alice sets the lsfDepositAuth flag on her account. This should
// prevent becky from deleting her account while using alice as the
// destination.
env(fset(alice, asfDepositAuth));
// carol requires a destination tag.
env(fset(carol, asfRequireDest));
env.close();
// Close enough ledgers to be able to delete becky's account.
incLgrSeqForAccDel(env, becky);
// becky attempts to delete her account using daria as the destination.
// Since daria is not in the ledger the delete attempt fails.
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(becky, daria), fee(acctDelFee), ter(tecNO_DST));
env.close();
// becky attempts to delete her account, but carol requires a
// destination tag which becky has omitted.
env(acctdelete(becky, carol), fee(acctDelFee), ter(tecDST_TAG_NEEDED));
env.close();
// becky attempts to delete her account, but alice won't take her XRP,
// so the delete is blocked.
env(acctdelete(becky, alice), fee(acctDelFee), ter(tecNO_PERMISSION));
env.close();
// alice preauthorizes deposits from becky. Now becky can delete her
// account and forward the leftovers to alice.
env(deposit::auth(alice, becky));
env.close();
auto const beckyOldBalance{env.balance(becky)};
env(acctdelete(becky, alice), fee(acctDelFee));
verifyDeliveredAmount(env, beckyOldBalance - acctDelFee);
env.close();
}
void
testDestinationDepositAuthCredentials()
{
{
testcase(
"Destination Constraints with DepositPreauth and Credentials");
using namespace test::jtx;
Account const alice{"alice"};
Account const becky{"becky"};
Account const carol{"carol"};
Account const daria{"daria"};
char const credType[] = "abcd";
Env env{*this};
env.fund(XRP(100000), alice, becky, carol, daria);
env.close();
// carol issue credentials for becky
env(credentials::create(becky, carol, credType));
env.close();
// get credentials index
auto const jv =
credentials::ledgerEntry(env, becky, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Close enough ledgers to be able to delete becky's account.
incLgrSeqForAccDel(env, becky);
auto const acctDelFee{drops(env.current()->fees().increment)};
// becky use credentials but they aren't accepted
env(acctdelete(becky, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(tecBAD_CREDENTIALS));
env.close();
{
// alice sets the lsfDepositAuth flag on her account. This
// should prevent becky from deleting her account while using
// alice as the destination.
env(fset(alice, asfDepositAuth));
env.close();
}
// Fail, credentials still not accepted
env(acctdelete(becky, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(tecBAD_CREDENTIALS));
env.close();
// becky accept the credentials
env(credentials::accept(becky, carol, credType));
env.close();
// Fail, credentials doesnt belong to carol
env(acctdelete(carol, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(tecBAD_CREDENTIALS));
// Fail, no depositPreauth for provided credentials
env(acctdelete(becky, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(tecNO_PERMISSION));
env.close();
// alice create DepositPreauth Object
env(deposit::authCredentials(alice, {{carol, credType}}));
env.close();
// becky attempts to delete her account, but alice won't take her
// XRP, so the delete is blocked.
env(acctdelete(becky, alice),
fee(acctDelFee),
ter(tecNO_PERMISSION));
// becky use empty credentials and can't delete account
env(acctdelete(becky, alice),
fee(acctDelFee),
credentials::ids({}),
ter(temMALFORMED));
// becky use bad credentials and can't delete account
env(acctdelete(becky, alice),
credentials::ids(
{"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6E"
"A288BE4"}),
fee(acctDelFee),
ter(tecBAD_CREDENTIALS));
env.close();
// becky use credentials and can delete account
env(acctdelete(becky, alice),
credentials::ids({credIdx}),
fee(acctDelFee));
env.close();
{
// check that credential object deleted too
auto const jNoCred =
credentials::ledgerEntry(env, becky, carol, credType);
BEAST_EXPECT(
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
jNoCred[jss::result].isMember(jss::error) &&
jNoCred[jss::result][jss::error] == "entryNotFound");
}
testcase("Credentials that aren't required");
{ // carol issue credentials for daria
env(credentials::create(daria, carol, credType));
env.close();
env(credentials::accept(daria, carol, credType));
env.close();
std::string const credDaria =
credentials::ledgerEntry(
env, daria, carol, credType)[jss::result][jss::index]
.asString();
// daria use valid credentials, which aren't required and can
// delete her account
env(acctdelete(daria, carol),
credentials::ids({credDaria}),
fee(acctDelFee));
env.close();
// check that credential object deleted too
auto const jNoCred =
credentials::ledgerEntry(env, daria, carol, credType);
BEAST_EXPECT(
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
jNoCred[jss::result].isMember(jss::error) &&
jNoCred[jss::result][jss::error] == "entryNotFound");
}
{
Account const eaton{"eaton"};
Account const fred{"fred"};
env.fund(XRP(5000), eaton, fred);
// carol issue credentials for eaton
env(credentials::create(eaton, carol, credType));
env.close();
env(credentials::accept(eaton, carol, credType));
env.close();
std::string const credEaton =
credentials::ledgerEntry(
env, eaton, carol, credType)[jss::result][jss::index]
.asString();
// fred make preauthorization through authorized account
env(fset(fred, asfDepositAuth));
env.close();
env(deposit::auth(fred, eaton));
env.close();
// Close enough ledgers to be able to delete becky's account.
incLgrSeqForAccDel(env, eaton);
auto const acctDelFee{drops(env.current()->fees().increment)};
// eaton use valid credentials, but he already authorized
// through "Authorized" field.
env(acctdelete(eaton, fred),
credentials::ids({credEaton}),
fee(acctDelFee));
env.close();
// check that credential object deleted too
auto const jNoCred =
credentials::ledgerEntry(env, eaton, carol, credType);
BEAST_EXPECT(
jNoCred.isObject() && jNoCred.isMember(jss::result) &&
jNoCred[jss::result].isMember(jss::error) &&
jNoCred[jss::result][jss::error] == "entryNotFound");
}
testcase("Expired credentials");
{
Account const john{"john"};
env.fund(XRP(10000), john);
env.close();
auto jv = credentials::create(john, carol, credType);
uint32_t const t = env.current()
->info()
.parentCloseTime.time_since_epoch()
.count() +
20;
jv[sfExpiration.jsonName] = t;
env(jv);
env.close();
env(credentials::accept(john, carol, credType));
env.close();
jv = credentials::ledgerEntry(env, john, carol, credType);
std::string const credIdx =
jv[jss::result][jss::index].asString();
incLgrSeqForAccDel(env, john);
// credentials are expired
// john use credentials but can't delete account
env(acctdelete(john, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(tecEXPIRED));
env.close();
{
// check that expired credential object deleted
auto jv =
credentials::ledgerEntry(env, john, carol, credType);
BEAST_EXPECT(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
jv[jss::result][jss::error] == "entryNotFound");
}
}
}
{
testcase("Credentials feature disabled");
using namespace test::jtx;
Account const alice{"alice"};
Account const becky{"becky"};
Account const carol{"carol"};
Env env{*this, testable_amendments() - featureCredentials};
env.fund(XRP(100000), alice, becky, carol);
env.close();
// alice sets the lsfDepositAuth flag on her account. This should
// prevent becky from deleting her account while using alice as the
// destination.
env(fset(alice, asfDepositAuth));
env.close();
// Close enough ledgers to be able to delete becky's account.
incLgrSeqForAccDel(env, becky);
auto const acctDelFee{drops(env.current()->fees().increment)};
std::string const credIdx =
"098B7F1B146470A1C5084DC7832C04A72939E3EBC58E68AB8B579BA072B0CE"
"CB";
// and can't delete even with old DepositPreauth
env(deposit::auth(alice, becky));
env.close();
env(acctdelete(becky, alice),
credentials::ids({credIdx}),
fee(acctDelFee),
ter(temDISABLED));
env.close();
}
}
void
testDeleteCredentialsOwner()
{
{
testcase("Deleting Issuer deletes issued credentials");
using namespace test::jtx;
Account const alice{"alice"};
Account const becky{"becky"};
Account const carol{"carol"};
char const credType[] = "abcd";
Env env{*this};
env.fund(XRP(100000), alice, becky, carol);
env.close();
// carol issue credentials for becky
env(credentials::create(becky, carol, credType));
env.close();
env(credentials::accept(becky, carol, credType));
env.close();
// get credentials index
auto const jv =
credentials::ledgerEntry(env, becky, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Close enough ledgers to be able to delete carol's account.
incLgrSeqForAccDel(env, carol);
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(carol, alice), fee(acctDelFee));
env.close();
{ // check that credential object deleted too
BEAST_EXPECT(!env.le(credIdx));
auto const jv =
credentials::ledgerEntry(env, becky, carol, credType);
BEAST_EXPECT(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
jv[jss::result][jss::error] == "entryNotFound");
}
}
{
testcase("Deleting Subject deletes issued credentials");
using namespace test::jtx;
Account const alice{"alice"};
Account const becky{"becky"};
Account const carol{"carol"};
char const credType[] = "abcd";
Env env{*this};
env.fund(XRP(100000), alice, becky, carol);
env.close();
// carol issue credentials for becky
env(credentials::create(becky, carol, credType));
env.close();
env(credentials::accept(becky, carol, credType));
env.close();
// get credentials index
auto const jv =
credentials::ledgerEntry(env, becky, carol, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// Close enough ledgers to be able to delete carol's account.
incLgrSeqForAccDel(env, becky);
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(becky, alice), fee(acctDelFee));
env.close();
{ // check that credential object deleted too
BEAST_EXPECT(!env.le(credIdx));
auto const jv =
credentials::ledgerEntry(env, becky, carol, credType);
BEAST_EXPECT(
jv.isObject() && jv.isMember(jss::result) &&
jv[jss::result].isMember(jss::error) &&
jv[jss::result][jss::error] == "entryNotFound");
}
}
}
void
run() override
{
testBasics();
testDirectories();
testOwnedTypes();
testAmendmentEnable();
testTooManyOffers();
testImplicitlyCreatedTrustline();
testBalanceTooSmallForFee();
testWithTickets();
testDest();
testDestinationDepositAuthCredentials();
testDeleteCredentialsOwner();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, ripple, 2);
} // namespace test
} // namespace ripple