Browse Source

broadcast transactions over tor to a broadcasting service where tor proxy available

terminal
Craig Raw 4 years ago
parent
commit
0e42c657b3
  1. 30
      src/main/java/com/sparrowwallet/sparrow/AppServices.java
  2. 137
      src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java
  3. 13
      src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java

30
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);
}
//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 null;
return proxy;
}
static void initialize(MainApp application) {

137
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);
}
}
}

13
src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java

@ -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…
Cancel
Save