mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-01-27 10:51:09 +00:00
broadcast transactions over tor to a broadcasting service where tor proxy available
This commit is contained in:
parent
b17c15f702
commit
0e42c657b3
3 changed files with 171 additions and 9 deletions
|
@ -46,10 +46,7 @@ import java.awt.desktop.OpenFilesHandler;
|
|||
import java.awt.desktop.OpenURIHandler;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
@ -69,6 +66,7 @@ public class AppServices {
|
|||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
|
@ -375,17 +373,31 @@ public class AppServices {
|
|||
}
|
||||
|
||||
public static Proxy getProxy() {
|
||||
return getProxy(TOR_DEFAULT_PROXY_CIRCUIT_ID);
|
||||
}
|
||||
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
} else if(AppServices.isTorRunning()) {
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
|
||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
}
|
||||
|
||||
return null;
|
||||
//Setting new proxy authentication credentials will force a new Tor circuit to be created
|
||||
if(proxy != null) {
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return (new PasswordAuthentication("user", proxyCircuitId.toCharArray()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
static void initialize(MainApp application) {
|
||||
|
|
137
src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java
Normal file
137
src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java
Normal file
|
@ -0,0 +1,137 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public enum BroadcastSource {
|
||||
BLOCKSTREAM_INFO("blockstream.info", "https://blockstream.info", "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion") {
|
||||
@Override
|
||||
public Sha256Hash broadcastTransaction(Transaction transaction) throws BroadcastException {
|
||||
String data = Utils.bytesToHex(transaction.bitcoinSerialize());
|
||||
return postTransactionData(data);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
}
|
||||
},
|
||||
MEMPOOL_SPACE("mempool.space", "https://mempool.space", "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion") {
|
||||
public Sha256Hash broadcastTransaction(Transaction transaction) throws BroadcastException {
|
||||
String data = Utils.bytesToHex(transaction.bitcoinSerialize());
|
||||
return postTransactionData(data);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final String name;
|
||||
private final String tlsUrl;
|
||||
private final String onionUrl;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BroadcastSource.class);
|
||||
private static final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
BroadcastSource(String name, String tlsUrl, String onionUrl) {
|
||||
this.name = name;
|
||||
this.tlsUrl = tlsUrl;
|
||||
this.onionUrl = onionUrl;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getTlsUrl() {
|
||||
return tlsUrl;
|
||||
}
|
||||
|
||||
public String getOnionUrl() {
|
||||
return onionUrl;
|
||||
}
|
||||
|
||||
public String getBaseUrl(Proxy proxy) {
|
||||
return (proxy == null ? getTlsUrl() : getOnionUrl());
|
||||
}
|
||||
|
||||
public abstract Sha256Hash broadcastTransaction(Transaction transaction) throws BroadcastException;
|
||||
|
||||
protected abstract URL getURL(Proxy proxy) throws MalformedURLException;
|
||||
|
||||
public Sha256Hash postTransactionData(String data) throws BroadcastException {
|
||||
//If a Tor proxy is configured, ensure we use a new circuit by configuring a random proxy password
|
||||
Proxy proxy = AppServices.getProxy(Integer.toString(secureRandom.nextInt()));
|
||||
|
||||
try {
|
||||
URL url = getURL(proxy);
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection)url.openConnection() : (HttpURLConnection)url.openConnection(proxy);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "text/plain");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
|
||||
writer.write(data);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String responseLine;
|
||||
while((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
}
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if(statusCode < 200 || statusCode >= 300) {
|
||||
throw new BroadcastException("Could not broadcast transaction, server returned " + statusCode + ": " + response);
|
||||
}
|
||||
|
||||
try {
|
||||
return Sha256Hash.wrap(response.toString().trim());
|
||||
} catch(Exception e) {
|
||||
throw new BroadcastException("Could not retrieve txid from broadcast, server returned " + statusCode + ": " + response);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Could not post transaction via " + getName(), e);
|
||||
throw new BroadcastException("Could not broadcast transaction via " + getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class BroadcastException extends Exception {
|
||||
public BroadcastException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BroadcastException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1249,6 +1249,19 @@ public class ElectrumServer {
|
|||
protected Task<Sha256Hash> createTask() {
|
||||
return new Task<>() {
|
||||
protected Sha256Hash call() throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.getProxy() != null) {
|
||||
List<BroadcastSource> broadcastSources = new ArrayList<>(Arrays.asList(BroadcastSource.values()));
|
||||
while(!broadcastSources.isEmpty()) {
|
||||
try {
|
||||
BroadcastSource broadcastSource = broadcastSources.remove(new Random().nextInt(broadcastSources.size()));
|
||||
return broadcastSource.broadcastTransaction(transaction);
|
||||
} catch(BroadcastSource.BroadcastException e) {
|
||||
//ignore, already logged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.broadcastTransaction(transaction);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue