fix(litebe): make HD t-address derivation atomic under scan

Deriving the next HD transparent address read the key count under a
read lock, released it, derived the address, and only then took a
write lock to append it. Between reading the position and appending,
another caller - for instance a scan deriving an address at the same
time - could read the same position, derive the same address, and
append it too, leaving a duplicate transparent key in the wallet.

- The whole read-modify-write now runs under a single write guard, so
  the position a caller derives from reflects every key already
  added. The slow seed stretch is computed once before the guard is
  taken, so holding the lock does not block on it.
- As a further guard, derivation skips a position whose address is
  already present and advances to the next, so no path can produce a
  duplicate even if positions were ever to overlap.
- The starting index is computed exactly as before (it still counts
  imported keys too), so a wallet with no contention derives the same
  addresses in the same order.

Uncontended derivation produces the same addresses in the same order,
verified against the differential harness. The wallet file format is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 08:50:12 -05:00
co-authored by Claude Opus 4.8
parent b49f85bcbe
commit 6d4ed96f6d
@@ -659,15 +659,17 @@ impl LightWallet {
return "Error: Can't add key while wallet is encrypted".to_string();
}
let pos = self.tkeys.read().unwrap().len() as u32;
// PBKDF2 (the slow part) outside the lock; hold ONE write guard across read-modify-write.
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
let address = self.address_from_sk(&sk);
self.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone()));
address
let mut tkeys = self.tkeys.write().unwrap();
let mut pos = tkeys.len() as u32; // same start index as today (counts imported keys too - unchanged)
loop {
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
let address = self.address_from_sk(&sk);
if tkeys.iter().any(|t| t.address == address) { pos += 1; continue; }
tkeys.push(WalletTKey::new_hdkey(sk, address.clone()));
return address;
}
}
pub fn import_taddr(&mut self, sk: String, birthday: u64) -> String {