improve randomization for utxo selection, input and output shuffling, and anti-fee-sniping
What changed, and why it matters
This commit replaces ordinary shuffling with a cryptographically secure random number generator when selecting which coins (UTXOs) to use in Bitcoin transactions and when ordering transaction inputs and outputs. The change makes it harder for an observer to guess or reproduce the wallet's choices, which can reduce privacy risks and certain fee-sniping attacks. The actual security improvement depends on code inside the updated 'drongo' submodule that is not shown in the diff.
Review the drongo submodule diff at commit 23e092f556c4ae88576674965d806d49dfc11966 to confirm the scope of randomization changes. Verify that SecureRandom instances are not re-seeded predictably and that all transaction shuffling sites are covered. Consider whether the change warrants a release note or advisory for privacy-conscious users.
Security signals we found
Use of cryptographically secure random source for transaction-related shuffling
Replacement of default Collections.shuffle (java.util.Random) with SecureRandom
Anti-fee-sniping randomization mentioned in commit title
UTXO selection and input/output shuffling are privacy-sensitive operations
Submodule update likely contains related changes but is not visible in the diff
Evidence from the diff
The patch adds java.security.SecureRandom instances in EntryCell.java and SendController.java and passes them to Collections.shuffle(…) for UTXO selection, input/output ordering, and anti-fee-sniping. Previously these calls used Collections.shuffle(List), which relies on java.util.Random and is deterministic if an attacker can predict the seed. The drongo submodule is bumped from 097420f6ea702d0a62f0ba244aeb0a54be06b569 to 23e092f556c4ae88576674965d806d49dfc11966, presumably containing related randomization changes, but the submodule diff is not supplied.
Changed components
src/main/java/com/sparrowwallet/sparrow/control/EntryCell.javasrc/main/java/com/sparrowwallet/sparrow/wallet/SendController.javadrongo submoduleInspect captured patch +8 / −4
### drongo
@@ -1 +1 @@
-Subproject commit 097420f6ea702d0a62f0ba244aeb0a54be06b569
+Subproject commit 23e092f556c4ae88576674965d806d49dfc11966
### src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -29,6 +29,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
@@ -38,6 +39,7 @@
public class EntryCell extends TreeTableCell<Entry, Entry> implements ConfirmationsListener {
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*?)( \\(Replaced By Fee( #)?(\\d+)?\\)).*?");
@@ -258,7 +260,7 @@ private static void increaseFee(TransactionEntry transactionEntry, boolean cance
double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate();
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
- Collections.shuffle(outputGroups);
+ Collections.shuffle(outputGroups, SECURE_RANDOM);
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
//If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0);
@@ -382,7 +384,7 @@ private static void createCpfp(TransactionEntry transactionEntry) {
double feeRate = blockTransaction.getFeeRate() == null ? AppServices.getMinimumRelayFeeRate() : blockTransaction.getFeeRate();
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
- Collections.shuffle(outputGroups);
+ Collections.shuffle(outputGroups, SECURE_RANDOM);
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
utxos.add(cpfpUtxo);
### src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
@@ -54,6 +54,7 @@
import java.io.IOException;
import java.net.URL;
+import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.Pattern;
@@ -63,6 +64,7 @@
public class SendController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(SendController.class);
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
@FXML
private TabPane paymentTabs;
@@ -710,7 +712,7 @@ protected WalletTransaction call() throws InsufficientFundsException {
filters.add(presetUtxoSelector.asExcludeTxoFilter());
List<OutputGroup> outputGroups = wallet.getGroupedUtxos(filters, params.feeRate(), AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
- Collections.shuffle(outputGroups);
+ Collections.shuffle(outputGroups, SECURE_RANDOM);
while(!outputGroups.isEmpty() && presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum() < e.getTargetValue()) {
OutputGroup outputGroup = outputGroups.removeFirst();Why this scored 51/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.