optimize and reduce electrum server rpc calls #2

This commit is contained in:
Craig Raw 2025-04-29 12:49:58 +02:00
parent e3138f3392
commit c77f52f7f6
4 changed files with 135 additions and 57 deletions

View file

@ -37,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ElectrumServer { public class ElectrumServer {
private static final Logger log = LoggerFactory.getLogger(ElectrumServer.class); private static final Logger log = LoggerFactory.getLogger(ElectrumServer.class);
@ -224,6 +225,11 @@ public class ElectrumServer {
} }
private static String getScriptHashStatus(String scriptHash, WalletNode walletNode) { private static String getScriptHashStatus(String scriptHash, WalletNode walletNode) {
List<ScriptHashTx> scriptHashTxes = getScriptHashes(scriptHash, walletNode);
return getScriptHashStatus(scriptHashTxes);
}
private static List<ScriptHashTx> getScriptHashes(String scriptHash, WalletNode walletNode) {
List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs()); List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs());
txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList())); txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList()));
Set<Sha256Hash> unique = new HashSet<>(txos.size()); Set<Sha256Hash> unique = new HashSet<>(txos.size());
@ -246,10 +252,15 @@ public class ElectrumServer {
sameHeightTxioScriptHashes.add(scriptHash); sameHeightTxioScriptHashes.add(scriptHash);
return 0; return 0;
}); });
if(!txos.isEmpty()) {
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList();
}
private static String getScriptHashStatus(List<ScriptHashTx> scriptHashTxes) {
if(!scriptHashTxes.isEmpty()) {
StringBuilder scriptHashStatus = new StringBuilder(); StringBuilder scriptHashStatus = new StringBuilder();
for(BlockTransactionHashIndex txo : txos) { for(ScriptHashTx scriptHashTx : scriptHashTxes) {
scriptHashStatus.append(txo.getHash().toString()).append(":").append(txo.getHeight()).append(":"); scriptHashStatus.append(scriptHashTx.tx_hash).append(":").append(scriptHashTx.height).append(":");
} }
return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8))); return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8)));
@ -393,10 +404,12 @@ public class ElectrumServer {
public void getReferences(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException { public void getReferences(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
try { try {
Map<WalletNode, ScriptHashTx[]> nodeHashHistory = new LinkedHashMap<>(nodes.size());
Map<String, String> pathScriptHashes = new LinkedHashMap<>(nodes.size()); Map<String, String> pathScriptHashes = new LinkedHashMap<>(nodes.size());
for(WalletNode node : nodes) { for(WalletNode node : nodes) {
if(node.getIndex() >= startIndex) { if(node.getIndex() >= startIndex) {
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(node)); pathScriptHashes.put(node.getDerivationPath(), getScriptHash(node));
nodeHashHistory.put(node, null);
} }
} }
@ -404,43 +417,75 @@ public class ElectrumServer {
return; 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. //Optimistic optimization for confirming transactions by matching against the script hash status should all mempool transactions confirm at the current block height
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true); for(Map.Entry<WalletNode, ScriptHashTx[]> entry : nodeHashHistory.entrySet()) {
WalletNode node = entry.getKey();
String scriptHash = pathScriptHashes.get(node.getDerivationPath());
List<String> statuses = subscribedScriptHashes.get(scriptHash);
for(String path : result.keySet()) { if(statuses != null && !statuses.isEmpty() && AppServices.getCurrentBlockHeight() != null &&
ScriptHashTx[] txes = result.get(path); node.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
.anyMatch(txo -> txo.getHeight() <= 0)) {
List<ScriptHashTx> scriptHashTxes = getScriptHashes(scriptHash, node);
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
if(scriptHashTx.height <= 0) {
scriptHashTx.height = AppServices.getCurrentBlockHeight();
scriptHashTx.fee = 0;
}
}
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst(); String status = getScriptHashStatus(scriptHashTxes);
if(optionalNode.isPresent()) { if(Objects.equals(status, statuses.getLast())) {
WalletNode node = optionalNode.get(); 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 if(!pathScriptHashes.isEmpty()) {
Set<BlockTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash) //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.
.collect(TreeSet::new, (set, ref) -> { Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
Optional<BlockTransactionHash> optExisting = set.stream().filter(prev -> prev.getHash().equals(ref.getHash())).findFirst();
if(optExisting.isPresent()) { for(String path : result.keySet()) {
if(optExisting.get().getHeight() < ref.getHeight()) { ScriptHashTx[] txes = result.get(path);
set.remove(optExisting.get());
set.add(ref); Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
} if(optionalNode.isPresent()) {
} else { 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<BlockTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash)
.collect(TreeSet::new, (set, ref) -> {
Optional<BlockTransactionHash> 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); set.add(ref);
} }
}, TreeSet::addAll); } else {
Set<BlockTransactionHash> existingReferences = nodeTransactionMap.get(node); set.add(ref);
}
}, TreeSet::addAll);
Set<BlockTransactionHash> existingReferences = nodeTransactionMap.get(node);
if(existingReferences == null) { if(existingReferences == null) {
nodeTransactionMap.put(node, references); nodeTransactionMap.put(node, references);
} else { } else {
for(BlockTransactionHash reference : references) { for(BlockTransactionHash reference : references) {
if(!existingReferences.add(reference)) { if(!existingReferences.add(reference)) {
Optional<BlockTransactionHash> optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst(); Optional<BlockTransactionHash> optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst();
if(optionalReference.isPresent()) { if(optionalReference.isPresent()) {
BlockTransactionHash existingReference = optionalReference.get(); BlockTransactionHash existingReference = optionalReference.get();
if(existingReference.getHeight() < reference.getHeight()) { if(existingReference.getHeight() < reference.getHeight()) {
existingReferences.remove(existingReference); existingReferences.remove(existingReference);
existingReferences.add(reference); existingReferences.add(reference);
}
} }
} }
} }
@ -1539,6 +1584,7 @@ public class ElectrumServer {
private final Sha256Hash txId; private final Sha256Hash txId;
private final Set<WalletNode> nodes; private final Set<WalletNode> nodes;
private final IntegerProperty iterationCount = new SimpleIntegerProperty(0); private final IntegerProperty iterationCount = new SimpleIntegerProperty(0);
private boolean cancelled;
public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) { public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) {
this.wallet = wallet; this.wallet = wallet;
@ -1554,6 +1600,22 @@ public class ElectrumServer {
return iterationCount; 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 @Override
protected Task<Set<String>> createTask() { protected Task<Set<String>> createTask() {
return new Task<>() { return new Task<>() {

View file

@ -16,6 +16,14 @@ class ScriptHashTx {
public String tx_hash; public String tx_hash;
public long fee; 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() { public BlockTransactionHash getBlockchainTransactionHash() {
Sha256Hash hash = Sha256Hash.wrap(tx_hash); Sha256Hash hash = Sha256Hash.wrap(tx_hash);
return new BlockTransaction(hash, height, null, fee, null); return new BlockTransaction(hash, height, null, fee, null);

View file

@ -1176,7 +1176,7 @@ public class HeadersController extends TransactionFormController implements Init
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next()))); Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
} }
if(transactionMempoolService.getIterationCount() > 3) { if(transactionMempoolService.getIterationCount() > 3 && !transactionMempoolService.isCancelled()) {
transactionMempoolService.cancel(); transactionMempoolService.cancel();
broadcastProgressBar.setProgress(0); broadcastProgressBar.setProgress(0);
log.error("Timeout searching for broadcasted transaction"); log.error("Timeout searching for broadcasted transaction");
@ -1185,11 +1185,13 @@ public class HeadersController extends TransactionFormController implements Init
} }
}); });
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> { transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
transactionMempoolService.cancel(); if(!transactionMempoolService.isCancelled()) {
broadcastProgressBar.setProgress(0); transactionMempoolService.cancel();
log.error("Timeout searching for broadcasted transaction"); broadcastProgressBar.setProgress(0);
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."); log.error("Timeout searching for broadcasted transaction");
broadcastButton.setDisable(false); 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(); transactionMempoolService.start();
} else { } else {
@ -1332,6 +1334,14 @@ public class HeadersController extends TransactionFormController implements Init
} }
} }
@Override
public void close() {
super.close();
if(transactionMempoolService != null) {
transactionMempoolService.cancel();
}
}
@Subscribe @Subscribe
public void transactionChanged(TransactionChangedEvent event) { public void transactionChanged(TransactionChangedEvent event) {
if(headersForm.getTransaction().equals(event.getTransaction())) { if(headersForm.getTransaction().equals(event.getTransaction())) {
@ -1577,24 +1587,18 @@ public class HeadersController extends TransactionFormController implements Init
if(transactionMempoolService != null) { if(transactionMempoolService != null) {
transactionMempoolService.cancel(); transactionMempoolService.cancel();
} }
}
}
@Subscribe
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().equals(event.getWallet()) && headersForm.isTransactionFinalized()) {
Sha256Hash txid = headersForm.getTransaction().getTxId(); Sha256Hash txid = headersForm.getTransaction().getTxId();
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash()); BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid);
transactionReferenceService.setOnSucceeded(successEvent -> { if(blockTransaction != null && !blockTransaction.equals(headersForm.getBlockTransaction())) {
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue(); headersForm.setBlockTransaction(blockTransaction);
BlockTransaction blockTransaction = transactionMap.get(txid); updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight());
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();
} }
} }

View file

@ -76,11 +76,15 @@ public abstract class TransactionFormController extends BaseController {
}); });
} }
public void close() {
EventManager.get().unregister(this);
}
@Subscribe @Subscribe
public void transactionTabsClosed(TransactionTabsClosedEvent event) { public void transactionTabsClosed(TransactionTabsClosedEvent event) {
for(TransactionTabData tabData : event.getClosedTransactionTabData()) { for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) { if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) {
EventManager.get().unregister(this); close();
} }
} }
} }