From c77f52f7f6996108d0fd13a75336d03423311215 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 29 Apr 2025 12:49:58 +0200 Subject: [PATCH] optimize and reduce electrum server rpc calls #2 --- .../sparrow/net/ElectrumServer.java | 130 +++++++++++++----- .../sparrow/net/ScriptHashTx.java | 8 ++ .../transaction/HeadersController.java | 48 ++++--- .../TransactionFormController.java | 6 +- 4 files changed, 135 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index b03080b3..4850d2a9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -37,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; public class ElectrumServer { private static final Logger log = LoggerFactory.getLogger(ElectrumServer.class); @@ -224,6 +225,11 @@ public class ElectrumServer { } private static String getScriptHashStatus(String scriptHash, WalletNode walletNode) { + List scriptHashTxes = getScriptHashes(scriptHash, walletNode); + return getScriptHashStatus(scriptHashTxes); + } + + private static List getScriptHashes(String scriptHash, WalletNode walletNode) { List txos = new ArrayList<>(walletNode.getTransactionOutputs()); txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList())); Set unique = new HashSet<>(txos.size()); @@ -246,10 +252,15 @@ public class ElectrumServer { sameHeightTxioScriptHashes.add(scriptHash); return 0; }); - if(!txos.isEmpty()) { + + return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList(); + } + + private static String getScriptHashStatus(List scriptHashTxes) { + if(!scriptHashTxes.isEmpty()) { StringBuilder scriptHashStatus = new StringBuilder(); - for(BlockTransactionHashIndex txo : txos) { - scriptHashStatus.append(txo.getHash().toString()).append(":").append(txo.getHeight()).append(":"); + for(ScriptHashTx scriptHashTx : scriptHashTxes) { + scriptHashStatus.append(scriptHashTx.tx_hash).append(":").append(scriptHashTx.height).append(":"); } return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8))); @@ -393,10 +404,12 @@ public class ElectrumServer { public void getReferences(Wallet wallet, Collection nodes, Map> nodeTransactionMap, int startIndex) throws ServerException { try { + Map nodeHashHistory = new LinkedHashMap<>(nodes.size()); Map pathScriptHashes = new LinkedHashMap<>(nodes.size()); for(WalletNode node : nodes) { if(node.getIndex() >= startIndex) { pathScriptHashes.put(node.getDerivationPath(), getScriptHash(node)); + nodeHashHistory.put(node, null); } } @@ -404,43 +417,75 @@ public class ElectrumServer { return; } - //Even if we have some successes, failure to retrieve all references will result in an incomplete wallet history. Don't proceed if that's the case. - Map result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true); + //Optimistic optimization for confirming transactions by matching against the script hash status should all mempool transactions confirm at the current block height + for(Map.Entry entry : nodeHashHistory.entrySet()) { + WalletNode node = entry.getKey(); + String scriptHash = pathScriptHashes.get(node.getDerivationPath()); + List statuses = subscribedScriptHashes.get(scriptHash); - for(String path : result.keySet()) { - ScriptHashTx[] txes = result.get(path); + if(statuses != null && !statuses.isEmpty() && AppServices.getCurrentBlockHeight() != null && + node.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)) + .anyMatch(txo -> txo.getHeight() <= 0)) { + List scriptHashTxes = getScriptHashes(scriptHash, node); + for(ScriptHashTx scriptHashTx : scriptHashTxes) { + if(scriptHashTx.height <= 0) { + scriptHashTx.height = AppServices.getCurrentBlockHeight(); + scriptHashTx.fee = 0; + } + } - Optional optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst(); - if(optionalNode.isPresent()) { - WalletNode node = optionalNode.get(); + String status = getScriptHashStatus(scriptHashTxes); + if(Objects.equals(status, statuses.getLast())) { + entry.setValue(scriptHashTxes.toArray(new ScriptHashTx[0])); + pathScriptHashes.remove(node.getDerivationPath()); + } + } + } - //Some servers can return the same tx as multiple ScriptHashTx entries with different heights. Take the highest height only - Set references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash) - .collect(TreeSet::new, (set, ref) -> { - Optional optExisting = set.stream().filter(prev -> prev.getHash().equals(ref.getHash())).findFirst(); - if(optExisting.isPresent()) { - if(optExisting.get().getHeight() < ref.getHeight()) { - set.remove(optExisting.get()); - set.add(ref); - } - } else { + if(!pathScriptHashes.isEmpty()) { + //Even if we have some successes, failure to retrieve all references will result in an incomplete wallet history. Don't proceed if that's the case. + Map result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true); + + for(String path : result.keySet()) { + ScriptHashTx[] txes = result.get(path); + + Optional optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst(); + if(optionalNode.isPresent()) { + WalletNode node = optionalNode.get(); + nodeHashHistory.put(node, txes); + } + } + } + + for(WalletNode node : nodeHashHistory.keySet()) { + ScriptHashTx[] txes = nodeHashHistory.get(node); + + //Some servers can return the same tx as multiple ScriptHashTx entries with different heights. Take the highest height only + Set references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash) + .collect(TreeSet::new, (set, ref) -> { + Optional optExisting = set.stream().filter(prev -> prev.getHash().equals(ref.getHash())).findFirst(); + if(optExisting.isPresent()) { + if(optExisting.get().getHeight() < ref.getHeight()) { + set.remove(optExisting.get()); set.add(ref); } - }, TreeSet::addAll); - Set existingReferences = nodeTransactionMap.get(node); + } else { + set.add(ref); + } + }, TreeSet::addAll); + Set existingReferences = nodeTransactionMap.get(node); - if(existingReferences == null) { - nodeTransactionMap.put(node, references); - } else { - for(BlockTransactionHash reference : references) { - if(!existingReferences.add(reference)) { - Optional optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst(); - if(optionalReference.isPresent()) { - BlockTransactionHash existingReference = optionalReference.get(); - if(existingReference.getHeight() < reference.getHeight()) { - existingReferences.remove(existingReference); - existingReferences.add(reference); - } + if(existingReferences == null) { + nodeTransactionMap.put(node, references); + } else { + for(BlockTransactionHash reference : references) { + if(!existingReferences.add(reference)) { + Optional optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst(); + if(optionalReference.isPresent()) { + BlockTransactionHash existingReference = optionalReference.get(); + if(existingReference.getHeight() < reference.getHeight()) { + existingReferences.remove(existingReference); + existingReferences.add(reference); } } } @@ -1539,6 +1584,7 @@ public class ElectrumServer { private final Sha256Hash txId; private final Set nodes; private final IntegerProperty iterationCount = new SimpleIntegerProperty(0); + private boolean cancelled; public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set nodes) { this.wallet = wallet; @@ -1554,6 +1600,22 @@ public class ElectrumServer { return iterationCount; } + public boolean isCancelled() { + return cancelled; + } + + @Override + public void start() { + this.cancelled = false; + super.start(); + } + + @Override + public boolean cancel() { + this.cancelled = true; + return super.cancel(); + } + @Override protected Task> createTask() { return new Task<>() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java index f1c0281e..cc1b8d81 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java @@ -16,6 +16,14 @@ class ScriptHashTx { public String tx_hash; public long fee; + public ScriptHashTx() {} + + public ScriptHashTx(int height, String tx_hash, long fee) { + this.height = height; + this.tx_hash = tx_hash; + this.fee = fee; + } + public BlockTransactionHash getBlockchainTransactionHash() { Sha256Hash hash = Sha256Hash.wrap(tx_hash); return new BlockTransaction(hash, height, null, fee, null); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 84bcee49..a8db33b0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -1176,7 +1176,7 @@ public class HeadersController extends TransactionFormController implements Init Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next()))); } - if(transactionMempoolService.getIterationCount() > 3) { + if(transactionMempoolService.getIterationCount() > 3 && !transactionMempoolService.isCancelled()) { transactionMempoolService.cancel(); broadcastProgressBar.setProgress(0); log.error("Timeout searching for broadcasted transaction"); @@ -1185,11 +1185,13 @@ public class HeadersController extends TransactionFormController implements Init } }); transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> { - transactionMempoolService.cancel(); - broadcastProgressBar.setProgress(0); - log.error("Timeout searching for broadcasted transaction"); - AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not indicate it had entered the mempool. It is safe to try broadcasting again."); - broadcastButton.setDisable(false); + if(!transactionMempoolService.isCancelled()) { + transactionMempoolService.cancel(); + broadcastProgressBar.setProgress(0); + log.error("Timeout searching for broadcasted transaction"); + AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not indicate it had entered the mempool. It is safe to try broadcasting again."); + broadcastButton.setDisable(false); + } }); transactionMempoolService.start(); } else { @@ -1332,6 +1334,14 @@ public class HeadersController extends TransactionFormController implements Init } } + @Override + public void close() { + super.close(); + if(transactionMempoolService != null) { + transactionMempoolService.cancel(); + } + } + @Subscribe public void transactionChanged(TransactionChangedEvent event) { if(headersForm.getTransaction().equals(event.getTransaction())) { @@ -1577,24 +1587,18 @@ public class HeadersController extends TransactionFormController implements Init if(transactionMempoolService != null) { transactionMempoolService.cancel(); } + } + } + @Subscribe + public void walletHistoryFinished(WalletHistoryFinishedEvent event) { + if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().equals(event.getWallet()) && headersForm.isTransactionFinalized()) { Sha256Hash txid = headersForm.getTransaction().getTxId(); - ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash()); - transactionReferenceService.setOnSucceeded(successEvent -> { - Map transactionMap = transactionReferenceService.getValue(); - BlockTransaction blockTransaction = transactionMap.get(txid); - if(blockTransaction != null) { - headersForm.setBlockTransaction(blockTransaction); - updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight()); - } - EventManager.get().post(new TransactionReferencesFinishedEvent(headersForm.getTransaction(), blockTransaction)); - }); - transactionReferenceService.setOnFailed(failEvent -> { - log.error("Could not update block transaction", failEvent.getSource().getException()); - EventManager.get().post(new TransactionReferencesFailedEvent(headersForm.getTransaction(), failEvent.getSource().getException())); - }); - EventManager.get().post(new TransactionReferencesStartedEvent(headersForm.getTransaction())); - transactionReferenceService.start(); + BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid); + if(blockTransaction != null && !blockTransaction.equals(headersForm.getBlockTransaction())) { + headersForm.setBlockTransaction(blockTransaction); + updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight()); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java index ab90c0df..71a418da 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java @@ -76,11 +76,15 @@ public abstract class TransactionFormController extends BaseController { }); } + public void close() { + EventManager.get().unregister(this); + } + @Subscribe public void transactionTabsClosed(TransactionTabsClosedEvent event) { for(TransactionTabData tabData : event.getClosedTransactionTabData()) { if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) { - EventManager.get().unregister(this); + close(); } } }