@@ -604,8 +604,15 @@ impl Wallet {
604604 Ok ( ( ) ) => Ok ( address) ,
605605 Err ( e) => {
606606 // The address was never handed out, so return it for the next caller rather
607- // than leaving its index revealed but unreachable.
608- self . address_pool . lock ( ) . expect ( "lock" ) . available . push_front ( ( index, address) ) ;
607+ // than leaving its index revealed but unreachable. Reinsert by index:
608+ // concurrent failed handouts complete in pop order, so pushing to the front
609+ // would reverse their segment and let the next successful handout skip past a
610+ // lower index, stranding it behind a used address in a from-seed restore's scan.
611+ {
612+ let mut locked_pool = self . address_pool . lock ( ) . expect ( "lock" ) ;
613+ let position = locked_pool. available . partition_point ( |( i, _) | * i < index) ;
614+ locked_pool. available . insert ( position, ( index, address) ) ;
615+ }
609616 // The refill may have failed after rewriting the record, which then durably
610617 // excludes the pushed-back index; rewrite it from the restored pool so a crash
611618 // before the next successful refill doesn't strand the index outside the pool.
@@ -2789,12 +2796,13 @@ mod tests {
27892796 }
27902797 }
27912798
2792- /// An in-memory store whose writes can be made to park until aborted, signalling when a
2793- /// write has entered the gate.
2799+ /// An in-memory store whose writes can be made to park until aborted or released,
2800+ /// signalling when a write has entered the gate, and whose writes can be made to fail .
27942801 #[ derive( Clone ) ]
27952802 struct GatedStore {
27962803 inner : Arc < InMemoryStore > ,
27972804 gate_writes : Arc < AtomicBool > ,
2805+ fail_writes : Arc < AtomicBool > ,
27982806 write_entered : Arc < tokio:: sync:: Notify > ,
27992807 release : Arc < tokio:: sync:: Notify > ,
28002808 }
@@ -2804,6 +2812,7 @@ mod tests {
28042812 Self {
28052813 inner : Arc :: new ( InMemoryStore :: new ( ) ) ,
28062814 gate_writes : Arc :: new ( AtomicBool :: new ( false ) ) ,
2815+ fail_writes : Arc :: new ( AtomicBool :: new ( false ) ) ,
28072816 write_entered : Arc :: new ( tokio:: sync:: Notify :: new ( ) ) ,
28082817 release : Arc :: new ( tokio:: sync:: Notify :: new ( ) ) ,
28092818 }
@@ -2822,6 +2831,7 @@ mod tests {
28222831 ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + Send {
28232832 let inner = Arc :: clone ( & self . inner ) ;
28242833 let gate_writes = Arc :: clone ( & self . gate_writes ) ;
2834+ let fail_writes = Arc :: clone ( & self . fail_writes ) ;
28252835 let write_entered = Arc :: clone ( & self . write_entered ) ;
28262836 let release = Arc :: clone ( & self . release ) ;
28272837 let primary_namespace = primary_namespace. to_string ( ) ;
@@ -2832,6 +2842,9 @@ mod tests {
28322842 write_entered. notify_one ( ) ;
28332843 release. notified ( ) . await ;
28342844 }
2845+ if fail_writes. load ( Ordering :: Acquire ) {
2846+ return Err ( io:: Error :: new ( io:: ErrorKind :: Other , "write failed" ) ) ;
2847+ }
28352848 KVStore :: write ( & * inner, & primary_namespace, & secondary_namespace, & key, buf) . await
28362849 }
28372850 }
@@ -3212,4 +3225,49 @@ mod tests {
32123225 indices
32133226 ) ;
32143227 }
3228+
3229+ #[ tokio:: test]
3230+ async fn oldest_address_still_leads_the_pool_after_concurrent_failed_handouts ( ) {
3231+ let gated_store = GatedStore :: new ( ) ;
3232+ let store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( gated_store. clone ( ) ) ) ;
3233+ let wallet = new_test_wallet ( Arc :: clone ( & store) , false ) . await ;
3234+ wallet. refill_address_pool ( ) . await . unwrap ( ) ;
3235+ let ( _, oldest_address) =
3236+ wallet. address_pool . lock ( ) . unwrap ( ) . available . front ( ) . cloned ( ) . unwrap ( ) ;
3237+
3238+ // First handout pops index 0 and parks inside its refill's record write, holding the
3239+ // refill lock.
3240+ gated_store. gate_writes . store ( true , Ordering :: Release ) ;
3241+ gated_store. fail_writes . store ( true , Ordering :: Release ) ;
3242+ let first_wallet = Arc :: clone ( & wallet) ;
3243+ let first_handout = tokio:: spawn ( async move { first_wallet. get_new_address ( ) . await } ) ;
3244+ gated_store. write_entered . notified ( ) . await ;
3245+
3246+ // Second handout pops index 1 while the first is parked, then queues on the refill lock.
3247+ let second_wallet = Arc :: clone ( & wallet) ;
3248+ let second_handout = tokio:: spawn ( async move { second_wallet. get_new_address ( ) . await } ) ;
3249+ while wallet. address_pool . lock ( ) . unwrap ( ) . available . len ( ) > ADDRESS_POOL_TARGET_SIZE - 2 {
3250+ tokio:: task:: yield_now ( ) . await ;
3251+ }
3252+
3253+ // Both handouts now fail and return their indices to the pool, completing out of pop
3254+ // order: index 0 first, index 1 second.
3255+ gated_store. gate_writes . store ( false , Ordering :: Release ) ;
3256+ gated_store. release . notify_one ( ) ;
3257+ assert ! ( first_handout. await . unwrap( ) . is_err( ) ) ;
3258+ assert ! ( second_handout. await . unwrap( ) . is_err( ) ) ;
3259+ gated_store. fail_writes . store ( false , Ordering :: Release ) ;
3260+
3261+ // The pushed-back indices must not swap the pool out of index order: the next handout
3262+ // has to serve the oldest revealed index, or a lower unused index would be left sitting
3263+ // behind a handed-out (potentially funded) one, where a from-seed restore's stop gap
3264+ // could strand it.
3265+ let handed_out = wallet. get_new_address ( ) . await . unwrap ( ) ;
3266+ assert_eq ! (
3267+ handed_out,
3268+ oldest_address,
3269+ "the oldest pooled address must be handed out first, pool: {:?}" ,
3270+ pooled_indices( & wallet)
3271+ ) ;
3272+ }
32153273}
0 commit comments