mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
optimize and reduce electrum server rpc calls #3
This commit is contained in:
parent
474f3a4e91
commit
df0c4310ca
5 changed files with 94 additions and 26 deletions
|
|
@ -2,14 +2,15 @@ package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
|
||||||
class BlockHeaderTip {
|
public class BlockHeaderTip {
|
||||||
public int height;
|
public int height;
|
||||||
public String hex;
|
public String hex;
|
||||||
|
|
||||||
public BlockHeader getBlockHeader() {
|
public BlockHeader getBlockHeader() {
|
||||||
if(hex == null) {
|
if(hex == null) {
|
||||||
return null;
|
return new BlockHeader(0, Sha256Hash.ZERO_HASH, Sha256Hash.ZERO_HASH, Sha256Hash.ZERO_HASH, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] blockHeaderBytes = Utils.hexToBytes(hex);
|
byte[] blockHeaderBytes = Utils.hexToBytes(hex);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.util.Duration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -61,17 +62,19 @@ public class ElectrumServer {
|
||||||
|
|
||||||
private static CloseableTransport transport;
|
private static CloseableTransport transport;
|
||||||
|
|
||||||
private static final Map<String, List<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
private static final Map<String, List<String>> subscribedScriptHashes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static Server previousServer;
|
private static Server previousServer;
|
||||||
|
|
||||||
private static final Map<String, String> retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
private static final Map<String, String> retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
private static final Map<Sha256Hash, BlockTransaction> retrievedTransactions = Collections.synchronizedMap(new HashMap<>());
|
private static final Map<Sha256Hash, BlockTransaction> retrievedTransactions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final Map<Integer, BlockHeader> retrievedBlockHeaders = Collections.synchronizedMap(new HashMap<>());
|
private static final Map<Integer, BlockHeader> retrievedBlockHeaders = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final Set<String> sameHeightTxioScriptHashes = Collections.synchronizedSet(new HashSet<>());
|
private static final Map<Sha256Hash, BlockTransaction> broadcastedTransactions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Set<String> sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||||
|
|
||||||
|
|
@ -420,27 +423,47 @@ public class ElectrumServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Optimistic optimization for confirming transactions by matching against the script hash status should all mempool transactions confirm at the current block height
|
//Optimistic optimizations from guessing the script hash status based on known information
|
||||||
for(Map.Entry<WalletNode, ScriptHashTx[]> entry : nodeHashHistory.entrySet()) {
|
for(Map.Entry<WalletNode, ScriptHashTx[]> entry : nodeHashHistory.entrySet()) {
|
||||||
WalletNode node = entry.getKey();
|
WalletNode node = entry.getKey();
|
||||||
String scriptHash = pathScriptHashes.get(node.getDerivationPath());
|
String scriptHash = pathScriptHashes.get(node.getDerivationPath());
|
||||||
List<String> statuses = subscribedScriptHashes.get(scriptHash);
|
List<String> statuses = subscribedScriptHashes.get(scriptHash);
|
||||||
|
|
||||||
if(statuses != null && !statuses.isEmpty() && AppServices.getCurrentBlockHeight() != null &&
|
if(statuses != null && !statuses.isEmpty()) {
|
||||||
node.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
|
//Optimize for new transactions that have been recently broadcasted
|
||||||
.anyMatch(txo -> txo.getHeight() <= 0)) {
|
for(Sha256Hash txid : broadcastedTransactions.keySet()) {
|
||||||
List<ScriptHashTx> scriptHashTxes = getScriptHashes(scriptHash, node);
|
BlockTransaction blkTx = broadcastedTransactions.get(txid);
|
||||||
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
|
if(blkTx.getTransaction().getOutputs().stream().map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals) ||
|
||||||
if(scriptHashTx.height <= 0) {
|
blkTx.getTransaction().getInputs().stream().map(txInput -> getPrevOutput(wallet, txInput))
|
||||||
scriptHashTx.height = AppServices.getCurrentBlockHeight();
|
.filter(Objects::nonNull).map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals)) {
|
||||||
scriptHashTx.fee = 0;
|
List<ScriptHashTx> scriptHashTxes = new ArrayList<>(getScriptHashes(scriptHash, node));
|
||||||
|
scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee()));
|
||||||
|
|
||||||
|
String status = getScriptHashStatus(scriptHashTxes);
|
||||||
|
if(Objects.equals(status, statuses.getLast())) {
|
||||||
|
entry.setValue(scriptHashTxes.toArray(new ScriptHashTx[0]));
|
||||||
|
pathScriptHashes.remove(node.getDerivationPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String status = getScriptHashStatus(scriptHashTxes);
|
//Optimize for new confirmations should all pending transactions confirm at the current block height
|
||||||
if(Objects.equals(status, statuses.getLast())) {
|
if(entry.getValue() == null && AppServices.getCurrentBlockHeight() != null &&
|
||||||
entry.setValue(scriptHashTxes.toArray(new ScriptHashTx[0]));
|
node.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
|
||||||
pathScriptHashes.remove(node.getDerivationPath());
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String status = getScriptHashStatus(scriptHashTxes);
|
||||||
|
if(Objects.equals(status, statuses.getLast())) {
|
||||||
|
entry.setValue(scriptHashTxes.toArray(new ScriptHashTx[0]));
|
||||||
|
pathScriptHashes.remove(node.getDerivationPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -623,6 +646,8 @@ public class ElectrumServer {
|
||||||
} else {
|
} else {
|
||||||
entry.setValue(blockTransaction.getTransaction());
|
entry.setValue(blockTransaction.getTransaction());
|
||||||
}
|
}
|
||||||
|
} else if(broadcastedTransactions.containsKey(reference.getHash())) {
|
||||||
|
entry.setValue(broadcastedTransactions.get(reference.getHash()).getTransaction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -634,6 +659,8 @@ public class ElectrumServer {
|
||||||
|
|
||||||
if(!transactionMap.equals(wallet.getTransactions())) {
|
if(!transactionMap.equals(wallet.getTransactions())) {
|
||||||
wallet.updateTransactions(transactionMap);
|
wallet.updateTransactions(transactionMap);
|
||||||
|
broadcastedTransactions.keySet().removeAll(transactionMap.entrySet().stream().filter(entry -> entry.getValue().getHeight() > 0)
|
||||||
|
.map(Map.Entry::getKey).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -643,7 +670,7 @@ public class ElectrumServer {
|
||||||
Set<Integer> blockHeights = new TreeSet<>();
|
Set<Integer> blockHeights = new TreeSet<>();
|
||||||
for(BlockTransactionHash reference : references) {
|
for(BlockTransactionHash reference : references) {
|
||||||
if(reference.getHeight() > 0) {
|
if(reference.getHeight() > 0) {
|
||||||
if(retrievedBlockHeaders.get(reference.getHeight()) != null) {
|
if(retrievedBlockHeaders.containsKey(reference.getHeight())) {
|
||||||
blockHeaderMap.put(reference.getHeight(), retrievedBlockHeaders.get(reference.getHeight()));
|
blockHeaderMap.put(reference.getHeight(), retrievedBlockHeaders.get(reference.getHeight()));
|
||||||
} else {
|
} else {
|
||||||
blockHeights.add(reference.getHeight());
|
blockHeights.add(reference.getHeight());
|
||||||
|
|
@ -1014,6 +1041,16 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Sha256Hash broadcastTransaction(Transaction transaction, Long fee) throws ServerException {
|
||||||
|
Sha256Hash txid = broadcastTransactionPrivately(transaction);
|
||||||
|
if(txid != null) {
|
||||||
|
BlockTransaction blkTx = new BlockTransaction(txid, 0, null, fee, transaction);
|
||||||
|
broadcastedTransactions.put(txid, blkTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return txid;
|
||||||
|
}
|
||||||
|
|
||||||
public Sha256Hash broadcastTransactionPrivately(Transaction transaction) throws ServerException {
|
public Sha256Hash broadcastTransactionPrivately(Transaction transaction) throws ServerException {
|
||||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||||
if(AppServices.isUsingProxy()) {
|
if(AppServices.isUsingProxy()) {
|
||||||
|
|
@ -1126,6 +1163,14 @@ public class ElectrumServer {
|
||||||
return scriptHashes;
|
return scriptHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TransactionOutput getPrevOutput(Wallet wallet, TransactionInput txInput) {
|
||||||
|
try {
|
||||||
|
return wallet.getWalletTransaction(txInput.getOutpoint().getHash()).getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||||
|
} catch(Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getScriptHash(WalletNode node) {
|
public static String getScriptHash(WalletNode node) {
|
||||||
byte[] hash = Sha256Hash.hash(node.getOutputScript().getProgram());
|
byte[] hash = Sha256Hash.hash(node.getOutputScript().getProgram());
|
||||||
byte[] reversed = Utils.reverseBytes(hash);
|
byte[] reversed = Utils.reverseBytes(hash);
|
||||||
|
|
@ -1729,7 +1774,7 @@ public class ElectrumServer {
|
||||||
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
|
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
|
||||||
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
||||||
for(Sha256Hash ref : references) {
|
for(Sha256Hash ref : references) {
|
||||||
if(retrievedTransactions.get(ref) != null) {
|
if(retrievedTransactions.containsKey(ref)) {
|
||||||
transactionMap.put(ref, retrievedTransactions.get(ref));
|
transactionMap.put(ref, retrievedTransactions.get(ref));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1848,9 +1893,11 @@ public class ElectrumServer {
|
||||||
|
|
||||||
public static class BroadcastTransactionService extends Service<Sha256Hash> {
|
public static class BroadcastTransactionService extends Service<Sha256Hash> {
|
||||||
private final Transaction transaction;
|
private final Transaction transaction;
|
||||||
|
private final Long fee;
|
||||||
|
|
||||||
public BroadcastTransactionService(Transaction transaction) {
|
public BroadcastTransactionService(Transaction transaction, Long fee) {
|
||||||
this.transaction = transaction;
|
this.transaction = transaction;
|
||||||
|
this.fee = fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1858,7 +1905,7 @@ public class ElectrumServer {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected Sha256Hash call() throws ServerException {
|
protected Sha256Hash call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
return electrumServer.broadcastTransactionPrivately(transaction);
|
return electrumServer.broadcastTransaction(transaction, fee);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1953,6 +2000,26 @@ public class ElectrumServer {
|
||||||
log.debug("Error subscribing to recent mempool transactions", e);
|
log.debug("Error subscribing to recent mempool transactions", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScheduledService<Void> broadcastService = new ScheduledService<>() {
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() throws Exception {
|
||||||
|
for(BlockTransaction blkTx : recentTransactions) {
|
||||||
|
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
broadcastService.setDelay(Duration.seconds(Math.random() * 60 * 10));
|
||||||
|
broadcastService.setPeriod(Duration.hours(1));
|
||||||
|
broadcastService.setOnSucceeded(_ -> broadcastService.cancel());
|
||||||
|
broadcastService.setOnFailed(_ -> broadcastService.cancel());
|
||||||
|
broadcastService.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@ public class PayNymController {
|
||||||
decryptedWallet.finalise(psbt);
|
decryptedWallet.finalise(psbt);
|
||||||
Transaction transaction = psbt.extractTransaction();
|
Transaction transaction = psbt.extractTransaction();
|
||||||
|
|
||||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction);
|
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction, psbt.getFee());
|
||||||
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
||||||
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
||||||
transactionMempoolService.setDelay(Duration.seconds(2));
|
transactionMempoolService.setDelay(Duration.seconds(2));
|
||||||
|
|
|
||||||
|
|
@ -1158,7 +1158,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
historyService.start();
|
historyService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction());
|
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction(), fee.getValue());
|
||||||
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
||||||
//Although we wait for WalletNodeHistoryChangedEvent to indicate tx is in mempool, start a scheduled service to check the script hashes should notifications fail
|
//Although we wait for WalletNodeHistoryChangedEvent to indicate tx is in mempool, start a scheduled service to check the script hashes should notifications fail
|
||||||
if(headersForm.getSigningWallet() != null) {
|
if(headersForm.getSigningWallet() != null) {
|
||||||
|
|
|
||||||
|
|
@ -1213,7 +1213,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
Transaction transaction = psbt.extractTransaction();
|
Transaction transaction = psbt.extractTransaction();
|
||||||
|
|
||||||
ServiceProgressDialog.ProxyWorker proxyWorker = new ServiceProgressDialog.ProxyWorker();
|
ServiceProgressDialog.ProxyWorker proxyWorker = new ServiceProgressDialog.ProxyWorker();
|
||||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction);
|
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(transaction, psbt.getFee());
|
||||||
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
broadcastTransactionService.setOnSucceeded(successEvent -> {
|
||||||
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
ElectrumServer.TransactionMempoolService transactionMempoolService = new ElectrumServer.TransactionMempoolService(walletTransaction.getWallet(), transaction.getTxId(), new HashSet<>(walletTransaction.getSelectedUtxos().values()));
|
||||||
transactionMempoolService.setDelay(Duration.seconds(2));
|
transactionMempoolService.setDelay(Duration.seconds(2));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue