#include #include #include 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 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 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 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 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 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 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 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 doesn’t 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