Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tx broadcast to mempool explorer api #4943

Merged
7 changes: 6 additions & 1 deletion core/src/main/java/bisq/core/app/BisqSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -230,6 +233,8 @@ public BisqSetup(DomainInitialisation domainInitialisation,
this.formatter = formatter;
this.localBitcoinNode = localBitcoinNode;
this.appStartupState = appStartupState;

MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences);
}

///////////////////////////////////////////////////////////////////////////////////////////
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

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.config.Config;
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 (!Config.baseCurrencyNetwork().isMainnet()) {
log.info("MemPoolSpaceTxBroadcaster only supports mainnet");
return;
}

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<String> 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<String> 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<String> 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.contains("code\":-27")) {
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<String> txBroadcastServices) {
List<String> list = checkNotNull(txBroadcastServices);
return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null;
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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);
}
}
21 changes: 21 additions & 0 deletions core/src/main/java/bisq/core/user/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
"devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck
));


private static final ArrayList<String> 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<String> 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;


Expand Down Expand Up @@ -912,6 +925,14 @@ public List<String> getDefaultXmrTxProofServices() {
}
}

public List<String> getDefaultTxBroadcastServices() {
if (config.useLocalhostForP2P) {
return TX_BROADCAST_SERVICES_CLEAR_NET;
} else {
return TX_BROADCAST_SERVICES;
}
}


///////////////////////////////////////////////////////////////////////////////////////////
// Private
Expand Down
Loading