@@ -510,6 +510,121 @@ class Delegate_test : public beast::unit_test::suite
510510 !hasKey (xrpl::Dir (*env.closed (), keylet::ownerDir (alice.id ())), delegateKey.key ));
511511 BEAST_EXPECT (env.balance (carol) == carolBalance + bobBalance - deleteFee);
512512 }
513+
514+ // Multiple delegators -> same delegatee: when the delegatee (bob)
515+ // deletes his account, ALL Delegate objects (from alice and carol)
516+ // must be cleaned up from every delegator's directory.
517+ {
518+ Env env (*this );
519+ Account alice{" alice" };
520+ Account bob{" bob" };
521+ Account carol{" carol" };
522+ Account dave{" dave" };
523+ env.fund (XRP (100000 ), alice, bob, carol, dave);
524+ env.close ();
525+
526+ // Both alice and carol delegate to bob
527+ env (delegate::set (alice, bob, {" Payment" }));
528+ env (delegate::set (carol, bob, {" EscrowCreate" }));
529+ env.close ();
530+
531+ auto const aliceBobKey = keylet::delegate (alice.id (), bob.id ());
532+ auto const carolBobKey = keylet::delegate (carol.id (), bob.id ());
533+
534+ auto hasKey = [](xrpl::Dir const & dir, uint256 const & key) {
535+ return std::find_if (dir.begin (), dir.end (), [&](auto const & sle) {
536+ return sle->key () == key;
537+ }) != dir.end ();
538+ };
539+
540+ // Both Delegate objects exist and are in bob's directory
541+ BEAST_EXPECT (env.closed ()->exists (aliceBobKey));
542+ BEAST_EXPECT (env.closed ()->exists (carolBobKey));
543+ BEAST_EXPECT (
544+ hasKey (xrpl::Dir (*env.closed (), keylet::ownerDir (bob.id ())), aliceBobKey.key ));
545+ BEAST_EXPECT (
546+ hasKey (xrpl::Dir (*env.closed (), keylet::ownerDir (bob.id ())), carolBobKey.key ));
547+
548+ for (std::uint32_t i = 0 ; i < 256 ; ++i)
549+ env.close ();
550+
551+ auto const bobBalance = env.balance (bob);
552+ auto const daveBalance = env.balance (dave);
553+
554+ auto const deleteFee = drops (env.current ()->fees ().increment );
555+ env (acctdelete (bob, dave), fee (deleteFee));
556+ env.close ();
557+
558+ // bob's account and directory are gone
559+ BEAST_EXPECT (!env.closed ()->exists (keylet::account (bob.id ())));
560+ BEAST_EXPECT (!env.closed ()->exists (keylet::ownerDir (bob.id ())));
561+
562+ // Both Delegate objects are erased
563+ BEAST_EXPECT (!env.closed ()->exists (aliceBobKey));
564+ BEAST_EXPECT (!env.closed ()->exists (carolBobKey));
565+
566+ // alice's and carol's directories no longer reference the objects
567+ BEAST_EXPECT (
568+ !hasKey (xrpl::Dir (*env.closed (), keylet::ownerDir (alice.id ())), aliceBobKey.key ));
569+ BEAST_EXPECT (
570+ !hasKey (xrpl::Dir (*env.closed (), keylet::ownerDir (carol.id ())), carolBobKey.key ));
571+
572+ BEAST_EXPECT (env.balance (dave) == daveBalance + bobBalance - deleteFee);
573+ }
574+ }
575+
576+ void
577+ testAuthorizedAccountDeleteLocksReserve ()
578+ {
579+ testcase (" test authorized account deletion locks delegator reserve" );
580+ using namespace jtx ;
581+
582+ // Scenario: Alice delegates to Bob. Bob deletes his account.
583+ // Without the bidirectional fix, the Delegate entry would remain in
584+ // Alice's directory, locking her reserve and preventing her from
585+ // deleting her own account. With the fix, Bob's AccountDelete cleans
586+ // up both sides so Alice can freely delete her account afterwards.
587+ Env env (*this );
588+ Account alice{" alice" };
589+ Account bob{" bob" };
590+ Account carol{" carol" };
591+ env.fund (XRP (100000 ), alice, bob, carol);
592+ env.close ();
593+
594+ // Alice creates a delegate entry authorizing Bob
595+ env (delegate::set (alice, bob, {" Payment" }));
596+ env.close ();
597+
598+ BEAST_EXPECT (env.closed ()->exists (keylet::delegate (alice.id (), bob.id ())));
599+
600+ auto const sleAlice = env.closed ()->read (keylet::account (alice.id ()));
601+ BEAST_EXPECT (sleAlice);
602+ BEAST_EXPECT (sleAlice->getFieldU32 (sfOwnerCount) == 1 );
603+
604+ for (std::uint32_t i = 0 ; i < 256 ; ++i)
605+ env.close ();
606+
607+ // Bob deletes his account
608+ auto const bobDeleteFee = drops (env.current ()->fees ().increment );
609+ env (acctdelete (bob, carol), fee (bobDeleteFee));
610+ env.close ();
611+
612+ BEAST_EXPECT (!env.closed ()->exists (keylet::account (bob.id ())));
613+
614+ // Bob's AccountDelete cleans up the Delegate entry from both sides.
615+ BEAST_EXPECT (!env.closed ()->exists (keylet::delegate (alice.id (), bob.id ())));
616+
617+ auto const sleAlice2 = env.closed ()->read (keylet::account (alice.id ()));
618+ BEAST_EXPECT (sleAlice2);
619+ BEAST_EXPECT (sleAlice2->getFieldU32 (sfOwnerCount) == 0 );
620+
621+ // Alice can now delete her own account — the Delegate entry no longer
622+ // blocks her ownerCount.
623+ auto const aliceDeleteFee = drops (env.current ()->fees ().increment );
624+ env (acctdelete (alice, carol), fee (aliceDeleteFee));
625+ env.close ();
626+
627+ BEAST_EXPECT (!env.closed ()->exists (keylet::account (alice.id ())));
513628 }
514629
515630 void
@@ -1817,6 +1932,7 @@ class Delegate_test : public beast::unit_test::suite
18171932 testFee ();
18181933 testSequence ();
18191934 testAccountDelete ();
1935+ testAuthorizedAccountDeleteLocksReserve ();
18201936 testDelegateTransaction ();
18211937 testPaymentGranular (all);
18221938 testTrustSetGranular ();
0 commit comments