From 7d06bf3803bca342cd35463725ee329725c60373 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 11:23:51 -0500 Subject: [PATCH 1/9] Add mempool nodes to Preferences --- .../main/java/bisq/core/user/Preferences.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index ac67776970d..4dd43b85d2a 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -131,6 +131,19 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid "devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck )); + + private static final ArrayList TX_BROADCAST_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( + "https://mempool.space/api/tx", // @wiz + "https://mempool.emzy.de/api/tx", // @emzy + "https://mempool.bisq.services/api/tx" // @devinbileck + )); + + private static final ArrayList TX_BROADCAST_SERVICES = new ArrayList<>(Arrays.asList( + "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/tx", // @wiz + "http://mempool4t6mypeemozyterviq3i5de4kpoua65r3qkn5i3kknu5l2cad.onion/api/tx", // @emzy + "http://mempoolusb2f67qi7mz2it7n5e77a6komdzx6wftobcduxszkdfun2yd.onion/api/tx" // @devinbileck + )); + public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; @@ -912,6 +925,14 @@ public List getDefaultXmrTxProofServices() { } } + public List getDefaultTxBroadcastServices() { + if (config.useLocalhostForP2P) { + return TX_BROADCAST_SERVICES_CLEAR_NET; + } else { + return TX_BROADCAST_SERVICES; + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private From 664ebc13b5ec0c68d3563de269dfdb909fe61331 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 11:24:07 -0500 Subject: [PATCH 2/9] Add constructor with responseCode --- p2p/src/main/java/bisq/network/http/HttpException.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/p2p/src/main/java/bisq/network/http/HttpException.java b/p2p/src/main/java/bisq/network/http/HttpException.java index 9432a953933..c078a274422 100644 --- a/p2p/src/main/java/bisq/network/http/HttpException.java +++ b/p2p/src/main/java/bisq/network/http/HttpException.java @@ -17,8 +17,18 @@ package bisq.network.http; +import lombok.Getter; + public class HttpException extends Exception { + @Getter + private int responseCode; + public HttpException(String message) { super(message); } + + public HttpException(String message, int responseCode) { + super(message); + this.responseCode = responseCode; + } } From 7f73fa10afa1ee8bf1eef3743bdb99c27faeba48 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 11:24:49 -0500 Subject: [PATCH 3/9] Add support for data post --- .../bisq/network/http/HttpClientImpl.java | 103 +++++++++++++----- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/p2p/src/main/java/bisq/network/http/HttpClientImpl.java b/p2p/src/main/java/bisq/network/http/HttpClientImpl.java index 3f5a6ee4299..e8e644830c5 100644 --- a/p2p/src/main/java/bisq/network/http/HttpClientImpl.java +++ b/p2p/src/main/java/bisq/network/http/HttpClientImpl.java @@ -22,6 +22,7 @@ import bisq.common.app.Version; import bisq.common.util.Utilities; +import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; @@ -30,6 +31,7 @@ import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -43,10 +45,13 @@ import java.net.InetSocketAddress; import java.net.URL; +import java.nio.charset.StandardCharsets; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -145,9 +150,9 @@ private String requestWithoutProxy(String baseUrl, @Nullable String headerKey, @Nullable String headerValue) throws IOException { long ts = System.currentTimeMillis(); - String spec = baseUrl + param; - log.info("requestWithoutProxy: URL={}, httpMethod={}", spec, httpMethod); + log.info("requestWithoutProxy: URL={}, param={}, httpMethod={}", baseUrl, param, httpMethod); try { + String spec = httpMethod == HttpMethod.GET ? baseUrl + param : baseUrl; URL url = new URL(spec); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(httpMethod.name()); @@ -158,23 +163,46 @@ private String requestWithoutProxy(String baseUrl, connection.setRequestProperty(headerKey, headerValue); } - if (connection.getResponseCode() == 200) { + if (httpMethod == HttpMethod.POST) { + connection.setDoOutput(true); + connection.getOutputStream().write(param.getBytes(StandardCharsets.UTF_8)); + } + + int responseCode = connection.getResponseCode(); + if (responseCode == 200) { String response = convertInputStreamToString(connection.getInputStream()); - log.info("Response for {} took {} ms. Data size:{}, response: {}", - spec, + log.info("Response from {} with param {} took {} ms. Data size:{}, response: {}", + baseUrl, + param, System.currentTimeMillis() - ts, Utilities.readableFileSize(response.getBytes().length), Utilities.toTruncatedString(response)); return response; } else { - String error = convertInputStreamToString(connection.getErrorStream()); - connection.getErrorStream().close(); - throw new HttpException(error); + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + String error = convertInputStreamToString(errorStream); + errorStream.close(); + log.info("Received errorMsg '{}' with responseCode {} from {}. Response took: {} ms. param: {}", + error, + responseCode, + baseUrl, + System.currentTimeMillis() - ts, + param); + throw new HttpException(error, responseCode); + } else { + log.info("Response with responseCode {} from {}. Response took: {} ms. param: {}", + responseCode, + baseUrl, + System.currentTimeMillis() - ts, + param); + throw new HttpException("Request failed", responseCode); + } } } catch (Throwable t) { - String message = "Error at requestWithoutProxy with URL: " + spec + ". Throwable=" + t.getMessage(); - log.error(message); - throw new IOException(message); + String message = "Error at requestWithoutProxy with url " + baseUrl + " and param " + param + + ". Throwable=" + t.getMessage(); + throw new IOException(message, t); } finally { try { if (connection != null) { @@ -195,8 +223,7 @@ private String doRequestWithProxy(String baseUrl, @Nullable String headerKey, @Nullable String headerValue) throws IOException { long ts = System.currentTimeMillis(); - String uri = baseUrl + param; - log.info("requestWithoutProxy: uri={}, httpMethod={}", uri, httpMethod); + log.info("requestWithoutProxy: baseUrl={}, param={}, httpMethod={}", baseUrl, param, httpMethod); // This code is adapted from: // http://stackoverflow.com/a/25203021/5616248 @@ -212,7 +239,7 @@ private String doRequestWithProxy(String baseUrl, new PoolingHttpClientConnectionManager(reg) : new PoolingHttpClientConnectionManager(reg, new FakeDnsResolver()); try { - closeableHttpClient = HttpClients.custom().setConnectionManager(cm).build(); + closeableHttpClient = checkNotNull(HttpClients.custom().setConnectionManager(cm).build()); InetSocketAddress socksAddress = new InetSocketAddress(socks5Proxy.getInetAddress(), socks5Proxy.getPort()); // remove me: Use this to test with system-wide Tor proxy, or change port for another proxy. @@ -221,23 +248,36 @@ private String doRequestWithProxy(String baseUrl, HttpClientContext context = HttpClientContext.create(); context.setAttribute("socks.address", socksAddress); - HttpUriRequest request = getHttpUriRequest(httpMethod, uri); - if (headerKey != null && headerValue != null) + HttpUriRequest request = getHttpUriRequest(httpMethod, baseUrl, param); + if (headerKey != null && headerValue != null) { request.setHeader(headerKey, headerValue); + } - try (CloseableHttpResponse httpResponse = checkNotNull(closeableHttpClient).execute(request, context)) { + try (CloseableHttpResponse httpResponse = closeableHttpClient.execute(request, context)) { String response = convertInputStreamToString(httpResponse.getEntity().getContent()); - log.info("Response for {} took {} ms. Data size:{}, response: {}", - uri, - System.currentTimeMillis() - ts, - Utilities.readableFileSize(response.getBytes().length), - Utilities.toTruncatedString(response)); - return response; + int statusCode = httpResponse.getStatusLine().getStatusCode(); + if (statusCode == 200) { + log.info("Response from {} took {} ms. Data size:{}, response: {}, param: {}", + baseUrl, + System.currentTimeMillis() - ts, + Utilities.readableFileSize(response.getBytes().length), + Utilities.toTruncatedString(response), + param); + return response; + } else { + log.info("Received errorMsg '{}' with statusCode {} from {}. Response took: {} ms. param: {}", + response, + statusCode, + baseUrl, + System.currentTimeMillis() - ts, + param); + throw new HttpException(response, statusCode); + } } } catch (Throwable t) { - String message = "Error at doRequestWithProxy with URL: " + uri + ". Throwable=" + t.getMessage(); - log.error(message); - throw new IOException(message); + String message = "Error at doRequestWithProxy with url " + baseUrl + " and param " + param + + ". Throwable=" + t.getMessage(); + throw new IOException(message, t); } finally { if (closeableHttpClient != null) { closeableHttpClient.close(); @@ -247,12 +287,17 @@ private String doRequestWithProxy(String baseUrl, } } - private HttpUriRequest getHttpUriRequest(HttpMethod httpMethod, String uri) { + private HttpUriRequest getHttpUriRequest(HttpMethod httpMethod, String baseUrl, String param) + throws UnsupportedEncodingException { switch (httpMethod) { case GET: - return new HttpGet(uri); + return new HttpGet(baseUrl + param); case POST: - return new HttpPost(uri); + HttpPost httpPost = new HttpPost(baseUrl); + HttpEntity httpEntity = new StringEntity(param); + httpPost.setEntity(httpEntity); + return httpPost; + default: throw new IllegalArgumentException("HttpMethod not supported: " + httpMethod); } From f0eefe794312bd94b675726c744242b9cbaf121c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 11:35:24 -0500 Subject: [PATCH 4/9] Add MemPoolSpaceTxBroadcaster --- .../main/java/bisq/core/app/BisqSetup.java | 7 +- .../bisq/core/btc/wallet/TxBroadcaster.java | 5 + .../http/MemPoolSpaceTxBroadcaster.java | 138 ++++++++++++++++++ .../wallet/http/TxBroadcastHttpClient.java | 32 ++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java create mode 100644 core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index f6ef065f2f6..6e9e32b8a6b 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -27,6 +27,7 @@ import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; import bisq.core.dao.governance.voteresult.VoteResultException; import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; import bisq.core.locale.Res; @@ -41,6 +42,7 @@ import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; +import bisq.network.Socks5ProxyProvider; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; @@ -210,7 +212,8 @@ public BisqSetup(DomainInitialisation domainInitialisation, TorSetup torSetup, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, LocalBitcoinNode localBitcoinNode, - AppStartupState appStartupState) { + AppStartupState appStartupState, + Socks5ProxyProvider socks5ProxyProvider) { this.domainInitialisation = domainInitialisation; this.p2PNetworkSetup = p2PNetworkSetup; this.walletAppSetup = walletAppSetup; @@ -230,6 +233,8 @@ public BisqSetup(DomainInitialisation domainInitialisation, this.formatter = formatter; this.localBitcoinNode = localBitcoinNode; this.appStartupState = appStartupState; + + MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java index ce5600d55e4..3f9d2cfd0e9 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java @@ -19,6 +19,7 @@ import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.exceptions.TxBroadcastTimeoutException; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; import bisq.common.Timer; import bisq.common.UserThread; @@ -135,6 +136,10 @@ public void onFailure(@NotNull Throwable throwable) { "the peerGroup.broadcastTransaction callback.", throwable))); } }, MoreExecutors.directExecutor()); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(tx); } private static void stopAndRemoveTimer(String txId) { diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java new file mode 100644 index 00000000000..10588a02230 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -0,0 +1,138 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.btc.wallet.http; + +import bisq.core.user.Preferences; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpException; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.List; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class MemPoolSpaceTxBroadcaster { + private static Socks5ProxyProvider socks5ProxyProvider; + private static Preferences preferences; + private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService( + "MemPoolSpaceTxBroadcaster", 3, 5, 10 * 60); + + public static void init(Socks5ProxyProvider socks5ProxyProvider, + Preferences preferences) { + MemPoolSpaceTxBroadcaster.socks5ProxyProvider = socks5ProxyProvider; + MemPoolSpaceTxBroadcaster.preferences = preferences; + } + + public static void broadcastTx(Transaction tx) { + if (socks5ProxyProvider == null) { + log.warn("We got broadcastTx called before init was called."); + return; + } + + String txIdToSend = tx.getTxId().toString(); + String rawTx = Utils.HEX.encode(tx.bitcoinSerialize(true)); + + List txBroadcastServices = preferences.getDefaultTxBroadcastServices(); + // Broadcast to first service + String serviceAddress = broadcastTx(txIdToSend, rawTx, txBroadcastServices); + if (serviceAddress != null) { + // Broadcast to second service + txBroadcastServices.remove(serviceAddress); + broadcastTx(txIdToSend, rawTx, txBroadcastServices); + } + } + + @Nullable + private static String broadcastTx(String txIdToSend, String rawTx, List txBroadcastServices) { + String serviceAddress = getRandomServiceAddress(txBroadcastServices); + if (serviceAddress == null) { + log.warn("We don't have a serviceAddress available. txBroadcastServices={}", txBroadcastServices); + return null; + } + broadcastTx(serviceAddress, txIdToSend, rawTx); + return serviceAddress; + } + + private static void broadcastTx(String serviceAddress, String txIdToSend, String rawTx) { + TxBroadcastHttpClient httpClient = new TxBroadcastHttpClient(socks5ProxyProvider); + httpClient.setBaseUrl(serviceAddress); + httpClient.setIgnoreSocks5Proxy(false); + + log.info("We broadcast rawTx {} to {}", rawTx, serviceAddress); + ListenableFuture future = executorService.submit(() -> { + Thread.currentThread().setName("MemPoolSpaceTxBroadcaster @ " + serviceAddress); + return httpClient.post(rawTx, "User-Agent", "bisq/" + Version.VERSION); + }); + + Futures.addCallback(future, new FutureCallback<>() { + public void onSuccess(String txId) { + if (txId.equals(txIdToSend)) { + log.info("Broadcast of raw tx with txId {} to {} was successful. rawTx={}", + txId, serviceAddress, rawTx); + } else { + log.error("The txId we got returned from the service does not match " + + "out tx of the sending tx. txId={}; txIdToSend={}", + txId, txIdToSend); + } + } + + public void onFailure(@NotNull Throwable throwable) { + Throwable cause = throwable.getCause(); + if (cause instanceof HttpException) { + int responseCode = ((HttpException) cause).getResponseCode(); + String message = cause.getMessage(); + if (responseCode == 400 && message.equals("sendrawtransaction RPC error: {\"code\":-27,\"message\":\"Transaction already in block chain\"}")) { + log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed", + serviceAddress, txIdToSend); + } else { + log.info("Broadcast of raw tx to {} failed for transaction {}. responseCode={}, error={}", + serviceAddress, txIdToSend, responseCode, message); + } + } else { + log.warn("Broadcast of raw tx with txId {} to {} failed. Error={}", + txIdToSend, serviceAddress, throwable.toString()); + } + } + }, MoreExecutors.directExecutor()); + } + + @Nullable + private static String getRandomServiceAddress(List txBroadcastServices) { + List list = checkNotNull(txBroadcastServices); + return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null; + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java b/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java new file mode 100644 index 00000000000..58dfad3f8b7 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/http/TxBroadcastHttpClient.java @@ -0,0 +1,32 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.btc.wallet.http; + +import bisq.core.trade.txproof.AssetTxProofHttpClient; + +import bisq.network.Socks5ProxyProvider; +import bisq.network.http.HttpClientImpl; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class TxBroadcastHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { + TxBroadcastHttpClient(Socks5ProxyProvider socks5ProxyProvider) { + super(socks5ProxyProvider); + } +} From 910441125e44b37a3695be3b0dcf92967da6efc5 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 12:11:24 -0500 Subject: [PATCH 5/9] Return if not mainnet --- .../core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java index 10588a02230..f5ef15525c1 100644 --- a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -23,6 +23,7 @@ import bisq.network.http.HttpException; import bisq.common.app.Version; +import bisq.common.config.Config; import bisq.common.util.Utilities; import org.bitcoinj.core.Transaction; @@ -58,6 +59,11 @@ public static void init(Socks5ProxyProvider socks5ProxyProvider, } public static void broadcastTx(Transaction tx) { + if (!Config.baseCurrencyNetwork().isMainnet()) { + log.info("MemPoolSpaceTxBroadcaster only supports mainnet"); + return; + } + if (socks5ProxyProvider == null) { log.warn("We got broadcastTx called before init was called."); return; From f26e76c908cae37fef6e7dcd37376caef32f1276 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 12:30:37 -0500 Subject: [PATCH 6/9] Only check for code --- .../bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java index f5ef15525c1..69d20000a19 100644 --- a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -121,7 +121,7 @@ public void onFailure(@NotNull Throwable throwable) { if (cause instanceof HttpException) { int responseCode = ((HttpException) cause).getResponseCode(); String message = cause.getMessage(); - if (responseCode == 400 && message.equals("sendrawtransaction RPC error: {\"code\":-27,\"message\":\"Transaction already in block chain\"}")) { + if (responseCode == 400 && message.contains("code\":-27")) { log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed", serviceAddress, txIdToSend); } else { From 478e1224e8c727f63c13cb3f8266ed6701fe8ea8 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 12:52:32 -0500 Subject: [PATCH 7/9] Use MemPoolSpaceTxBroadcaster for withdrawing funds and for empty wallet call. --- .../bisq/core/btc/wallet/BtcWalletService.java | 16 +++++++++++++++- .../java/bisq/core/btc/wallet/WalletService.java | 8 +++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 449ee734c6c..dfc14188091 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -24,6 +24,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.AddressEntryList; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; @@ -957,6 +958,10 @@ public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMes try { sendResult = wallet.sendCoins(sendRequest); printTx("FeeEstimationTransaction", newTransaction); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); } catch (InsufficientMoneyException e2) { errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. " + (e2.missing != null ? e2.missing.toFriendlyString() : "")); } @@ -1139,7 +1144,11 @@ public String sendFunds(String fromAddress, if (memo != null) { sendResult.tx.setMemo(memo); } - printTx("sendFunds", sendResult.tx); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + return sendResult.tx.getTxId().toString(); } @@ -1160,6 +1169,11 @@ public Transaction sendFundsForMultipleAddresses(Set fromAddresses, sendResult.tx.setMemo(memo); } printTx("sendFunds", sendResult.tx); + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + return sendResult.tx; } diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index d9e17f0f3a9..f2e662de566 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -23,6 +23,7 @@ import bisq.core.btc.listeners.BalanceListener; import bisq.core.btc.listeners.TxConfidenceListener; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; @@ -535,7 +536,12 @@ public void emptyBtcWallet(String toAddress, sendRequest.aesKey = aesKey; Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); printTx("empty btc wallet", sendResult.tx); - Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() { + + // For better redundancy in case the broadcast via BitcoinJ fails we also + // publish the tx via mempool nodes. + MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx); + + Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() { @Override public void onSuccess(Transaction result) { log.info("emptyBtcWallet onSuccess Transaction=" + result); From 33cf657d1dc7722739a3068872833fe66cfce8de Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 13:02:15 -0500 Subject: [PATCH 8/9] Clone txBroadcastServices so we do not manipulate source list --- .../bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java index 69d20000a19..6cbc7360648 100644 --- a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -35,6 +35,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -72,7 +73,7 @@ public static void broadcastTx(Transaction tx) { String txIdToSend = tx.getTxId().toString(); String rawTx = Utils.HEX.encode(tx.bitcoinSerialize(true)); - List txBroadcastServices = preferences.getDefaultTxBroadcastServices(); + List txBroadcastServices = new ArrayList<>(preferences.getDefaultTxBroadcastServices()); // Broadcast to first service String serviceAddress = broadcastTx(txIdToSend, rawTx, txBroadcastServices); if (serviceAddress != null) { From cb289845b65a62550ab2984c16aaaf068285d8b2 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 14 Dec 2020 13:08:12 -0500 Subject: [PATCH 9/9] Add reference to bitcoin RPC error codes --- .../bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java index 6cbc7360648..921b7718903 100644 --- a/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/http/MemPoolSpaceTxBroadcaster.java @@ -122,6 +122,7 @@ public void onFailure(@NotNull Throwable throwable) { if (cause instanceof HttpException) { int responseCode = ((HttpException) cause).getResponseCode(); String message = cause.getMessage(); + // See all error codes at: https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h if (responseCode == 400 && message.contains("code\":-27")) { log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed", serviceAddress, txIdToSend);