java-topology/defects/sparrow-0002/patch/sparrow-0002.patch
russell@unturf.com 830b54936d sparrow-0001/sparrow-0002: Sparrow Wallet CWE-407 scan, 2 defects
sparrow-0001: WalletUtxosEntry.updateUtxos() ArrayList.removeAll O(N^2)
  UTXO diff uses List.removeAll which is O(current * previous).
  Fix: Set-based diff via Sets.difference (same pattern already used
  in WalletTransactionsEntry). MEDIUM, 250x at N=1000 UTXOs.

sparrow-0002: UtxoEntry.recountMixesDone stream().anyMatch O(M*I*T)
  Whirlpool mix chain walk streams all wallet TXOs per input per mix.
  Fix: HashMap<Sha256Hash, Set<Long>> index for O(1) lookup.
  MEDIUM-HIGH, 50x at M=200 mixes, T=1000 TXOs.

MOAD-0002 (Intertangle): EventManager is a thin Guava EventBus singleton,
  not a god object. Wallet/network/UI coupling is event-driven, acceptable.
MOAD-0003 (Leaked Context): CLEAN, no ThreadLocal usage found.
MOAD-0004 (Logged Secret): CLEAN, no private keys/mnemonics/passphrases
  logged. SecureChannelSession has commented-out secret logging.
MOAD-0005 (Thundering Herd): CLEAN, no unsynchronized cache patterns.

4/4 unit tests PASS. UNDF-2026-000000931 through UNDF-2026-000000932.
2026-03-31 09:41:11 -04:00

32 lines
2.2 KiB
Diff

# UNDF: UNDF-2026-000000932
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java
@@ -149,16 +149,23 @@ public class UtxoEntry extends HashIndexEntry {
public int recountMixesDone(Wallet postmixWallet, BlockTransactionHashIndex postmixUtxo) {
int mixesDone = 0;
- Set<BlockTransactionHashIndex> walletTxos = postmixWallet.getWalletTxos().entrySet().stream()
- .filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
+ // Build a lookup index: txHash -> set of output indices for O(1) membership test
+ // instead of streaming the entire walletTxos set per input per mix iteration
+ Map<Sha256Hash, Set<Long>> txoIndex = new HashMap<>();
+ for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : postmixWallet.getWalletTxos().entrySet()) {
+ if(entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE) {
+ BlockTransactionHashIndex txo = entry.getKey();
+ txoIndex.computeIfAbsent(txo.getHash(), k -> new HashSet<>()).add(txo.getIndex());
+ }
+ }
BlockTransaction blkTx = postmixWallet.getTransactions().get(postmixUtxo.getHash());
while(blkTx != null) {
mixesDone++;
List<TransactionInput> inputs = blkTx.getTransaction().getInputs();
blkTx = null;
for(TransactionInput txInput : inputs) {
BlockTransaction inputTx = postmixWallet.getTransactions().get(txInput.getOutpoint().getHash());
- if(inputTx != null && walletTxos.stream().anyMatch(txo -> txo.getHash().equals(inputTx.getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()) && inputTx.getTransaction() != null) {
+ Set<Long> indices = inputTx == null ? null : txoIndex.get(inputTx.getHash());
+ if(inputTx != null && indices != null && indices.contains(txInput.getOutpoint().getIndex()) && inputTx.getTransaction() != null) {
blkTx = inputTx;
}
}