From 0e42c657b335f8a33f96718d72e5bdb1374782e8 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 18 May 2021 09:37:59 +0200 Subject: [PATCH] broadcast transactions over tor to a broadcasting service where tor proxy available --- .../sparrowwallet/sparrow/AppServices.java | 30 ++-- .../sparrow/net/BroadcastSource.java | 137 ++++++++++++++++++ .../sparrow/net/ElectrumServer.java | 13 ++ 3 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 580fea57..2472c569 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -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) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java new file mode 100644 index 00000000..a376ed46 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java @@ -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); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 75be18c2..11e86b98 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1249,6 +1249,19 @@ public class ElectrumServer { protected Task 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 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); }