avoid creating a node entry for every utxo table address cell update
What changed, and why it matters
This commit is a performance and resource-usage improvement, not a security fix. It changes the way a Bitcoin wallet's user interface builds right-click context menus for address cells in the UTXO table. Previously, every time an address cell was redrawn, the code created a full 'NodeEntry' object, which in turn created child entries for every transaction output belonging to that address. With many transactions or outputs, this caused unnecessary memory and CPU use. The patch delays creation of the full NodeEntry until the user actually selects a menu action, and passes the wallet and node directly where possible. There is no evidence in the commit of a vulnerability that can be exploited by an attacker.
No security action required. Treat as a routine performance/refactoring patch. If reviewing for release notes, classify under UI/performance improvements rather than security fixes.
Security signals we found
Performance/resource-usage optimization only
No change to authentication, authorization, cryptography, or network code
No input validation or parsing changes
No vendor disclosure of security relevance in commit or supplied references
Evidence from the diff
The diff refactors AddressCell and EntryCell to avoid eager NodeEntry instantiation. AddressCell now passes a Supplier
Changed components
src/main/java/com/sparrowwallet/sparrow/control/AddressCell.javasrc/main/java/com/sparrowwallet/sparrow/control/EntryCell.javasrc/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.javaInspect captured patch +34 / −15
### src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java
@@ -32,6 +32,7 @@ protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty)
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
EntryCell.applyRowStyles(this, utxoEntry);
+ setContextMenu(null);
if (empty) {
setText(null);
@@ -40,7 +41,9 @@ protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty)
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
- setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()), false, getTreeTableView()));
+ //Pass the wallet and node rather than a NodeEntry, which would eagerly create an entry for every transaction output of the address on each cell update
+ setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), utxoEntry.getWallet(), utxoEntry.getNode(),
+ () -> new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode(), false), false, getTreeTableView()));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
### src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -714,60 +714,69 @@ public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
- if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
+ this(address, outputDescriptor, nodeEntry == null ? null : nodeEntry.getWallet(), nodeEntry == null ? null : nodeEntry.getNode(), () -> nodeEntry, addUtxoItems, treetable);
+ }
+
+ /**
+ * Creates the menu from the wallet and address node only. The NodeEntry, which eagerly creates an entry for every transaction output of the address,
+ * is only created by the supplier when an action that requires it is selected.
+ */
+ public AddressContextMenu(Address address, String outputDescriptor, Wallet wallet, WalletNode node, java.util.function.Supplier<NodeEntry> nodeEntrySupplier, boolean addUtxoItems, TreeTableView<Entry> treetable) {
+ if(node == null || (!wallet.isBip47() && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
hide();
+ NodeEntry nodeEntry = nodeEntrySupplier.get();
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
getItems().add(receiveToAddress);
}
- if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
+ if(node != null && canSignMessage(node)) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
hide();
- MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
+ MessageSignDialog messageSignDialog = new MessageSignDialog(wallet, node);
messageSignDialog.initOwner(treetable.getScene().getWindow());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
- if(addUtxoItems && nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
- List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
+ if(addUtxoItems && node != null && !node.getUnspentTransactionOutputs().isEmpty()) {
+ List<BlockTransactionHashIndex> utxos = node.getUnspentTransactionOutputs().stream().collect(Collectors.toList());
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
spendUtxos.setGraphic(getSendGlyph());
spendUtxos.setOnAction(AE -> {
hide();
- EventManager.get().post(new SendActionEvent(nodeEntry.getWallet(), utxos));
- Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(nodeEntry.getWallet(), utxos)));
+ EventManager.get().post(new SendActionEvent(wallet, utxos));
+ Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(wallet, utxos)));
});
getItems().add(spendUtxos);
- List<BlockTransactionHashIndex> unfrozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
+ List<BlockTransactionHashIndex> unfrozenUtxos = node.getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
if(!unfrozenUtxos.isEmpty()) {
MenuItem freezeUtxos = new MenuItem("Freeze UTXOs");
freezeUtxos.setGraphic(getFreezeGlyph());
freezeUtxos.setOnAction(AE -> {
hide();
unfrozenUtxos.forEach(utxo -> utxo.setStatus(Status.FROZEN));
- EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), unfrozenUtxos));
+ EventManager.get().post(new WalletUtxoStatusChangedEvent(wallet, unfrozenUtxos));
});
getItems().add(freezeUtxos);
}
- List<BlockTransactionHashIndex> frozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
+ List<BlockTransactionHashIndex> frozenUtxos = node.getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
if(!frozenUtxos.isEmpty()) {
MenuItem unfreezeUtxos = new MenuItem("Unfreeze UTXOs");
unfreezeUtxos.setGraphic(getUnfreezeGlyph());
unfreezeUtxos.setOnAction(AE -> {
hide();
frozenUtxos.forEach(utxo -> utxo.setStatus(null));
- EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), frozenUtxos));
+ EventManager.get().post(new WalletUtxoStatusChangedEvent(wallet, frozenUtxos));
});
getItems().add(unfreezeUtxos);
}
@@ -791,11 +800,11 @@ public AddressContextMenu(Address address, String outputDescriptor, NodeEntry no
getItems().addAll(copyAddress, copyOutputDescriptor);
- if(nodeEntry != null) {
+ if(node != null) {
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
- Script outputScript = nodeEntry.getWallet().getOutputScript(nodeEntry.getNode());
+ Script outputScript = wallet.getOutputScript(node);
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(outputScript.getProgram()));
Clipboard.getSystemClipboard().setContent(content);
### src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java
@@ -16,7 +16,14 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
private final WalletNode node;
public NodeEntry(Wallet wallet, WalletNode node) {
- super(wallet, node.getLabel(), createChildren(wallet, node));
+ this(wallet, node, true);
+ }
+
+ /**
+ * @param createChildren false to skip creating an entry for every child node or transaction output, for uses that only need the node, address and label
+ */
+ public NodeEntry(Wallet wallet, WalletNode node, boolean createChildren) {
+ super(wallet, node.getLabel(), createChildren ? createChildren(wallet, node) : new ArrayList<>());
this.node = node;
labelProperty().addListener((observable, oldValue, newValue) -> {Why this scored 19/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.