Skip to content

Commit a7c52af

Browse files
jkczyzclaude
andcommitted
f - Reinsert failed handouts into the address pool by index
Concurrent failed handouts complete in pop order, so pushing each returned address back to the front of the pool reverses their segment. A subsequent successful handout would then serve a higher index while lower unused ones stay pooled behind it, where a from-seed restore's full-scan stop gap could strand them behind a used address. Reinsert returned addresses at their index position instead, keeping handouts consuming the oldest revealed index first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5fd08ef commit a7c52af

1 file changed

Lines changed: 62 additions & 4 deletions

File tree

src/wallet/mod.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)