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

Improve dispute validation #6404

Merged
16 changes: 16 additions & 0 deletions core/src/main/java/bisq/core/provider/MempoolHttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

import java.io.IOException;

import java.util.concurrent.CompletableFuture;

import javax.annotation.Nullable;

@Singleton
Expand All @@ -42,4 +44,18 @@ public String getTxDetails(String txId) throws IOException {
String api = "/" + txId;
return get(api, "User-Agent", "bisq/" + Version.VERSION);
}


public CompletableFuture<String> requestTxAsHex(String txId) {
super.shutDown(); // close any prior incomplete request

return CompletableFuture.supplyAsync(() -> {
String api = "/" + txId + "/hex";
try {
return get(api, "User-Agent", "bisq/" + Version.VERSION);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;

import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -66,12 +67,19 @@ public void onSuccess(String mempoolData) {
log.info("Received mempoolData of [{}] from provider", mempoolData);
mempoolServiceCallback.set(mempoolData);
}

public void onFailure(@NotNull Throwable throwable) {
mempoolServiceCallback.setException(throwable);
}
}, MoreExecutors.directExecutor());
}


public CompletableFuture<String> requestTxAsHex(String txId) {
mempoolHttpClient.setBaseUrl(getRandomServiceAddress(txBroadcastServices));
return mempoolHttpClient.requestTxAsHex(txId);
}

public boolean switchToAnotherProvider() {
txBroadcastServices.remove(mempoolHttpClient.getBaseUrl());
return txBroadcastServices.size() > 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import lombok.Getter;
Expand Down Expand Up @@ -129,7 +130,12 @@ public void checkTxIsConfirmed(String txId, Consumer<TxValidator> resultHandler)
mempoolRequest.getTxStatus(future, txId);
}

// ///////////////////////////
public CompletableFuture<String> requestTxAsHex(String txId) {
outstandingRequests++;
return new MempoolRequest(preferences, socks5ProxyProvider)
.requestTxAsHex(txId)
.whenComplete((result, throwable) -> outstandingRequests--);
}

private void validateOfferMakerTx(MempoolRequest mempoolRequest,
TxValidator txValidator,
Expand Down
18 changes: 17 additions & 1 deletion core/src/main/java/bisq/core/support/dispute/Dispute.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package bisq.core.support.dispute;

import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.Res;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
Expand All @@ -39,6 +40,8 @@

import com.google.protobuf.ByteString;

import org.bitcoinj.core.Transaction;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
Expand Down Expand Up @@ -100,7 +103,7 @@ public static protobuf.Dispute.State toProtoMessage(Dispute.State state) {
private final PubKeyRing traderPubKeyRing;
private final long tradeDate;
private final long tradePeriodEnd;
private Contract contract;
private final Contract contract;
@Nullable
private final byte[] contractHash;
@Nullable
Expand Down Expand Up @@ -163,6 +166,7 @@ public static protobuf.Dispute.State toProtoMessage(Dispute.State state) {
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();

private transient FileTransferReceiver fileTransferSession = null;
private transient Optional<Transaction> cachedDepositTx = Optional.empty();

public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode,
NodeAddress peerNodeAddress,
Expand All @@ -181,6 +185,7 @@ public FileTransferSender createFileTransferSender(NetworkNode networkNode,
return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback);
}


///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -501,6 +506,16 @@ public String getRoleStringForLogFile() {
+ (disputeOpenerIsMaker ? "MAKER" : "TAKER");
}

public Optional<Transaction> findDepositTx(BtcWalletService btcWalletService) {
if (cachedDepositTx.isPresent() || depositTxSerialized == null) {
return cachedDepositTx;
}

Transaction tx = new Transaction(btcWalletService.getParams(), depositTxSerialized);
cachedDepositTx = Optional.of(tx);
return cachedDepositTx;
}

@Override
public String toString() {
return "Dispute{" +
Expand Down Expand Up @@ -534,6 +549,7 @@ public String toString() {
",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' +
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
",\n cachedDepositTx='" + cachedDepositTx + '\'' +
"\n}";
}
}
11 changes: 7 additions & 4 deletions core/src/main/java/bisq/core/support/dispute/DisputeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,12 +368,12 @@ protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessa
addMediationResultMessage(dispute);

try {
DisputeValidation.validateDisputeData(dispute, btcWalletService);
DisputeValidation.validateNodeAddresses(dispute, config);
DisputeValidation.validateSenderNodeAddress(dispute, openNewDisputeMessage.getSenderNodeAddress());
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
DisputeValidation.validateNodeAddresses(dispute, config);
} catch (DisputeValidation.AddressException |
DisputeValidation.DisputeReplayException |
DisputeValidation.NodeAddressException e) {
} catch (DisputeValidation.ValidationException e) {
log.error(e.toString());
validationExceptions.add(e);
}
Expand All @@ -398,6 +398,9 @@ protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDis

Trade trade = optionalTrade.get();
try {
DisputeValidation.validateDisputeData(dispute, btcWalletService);
DisputeValidation.validateNodeAddresses(dispute, config);
DisputeValidation.validateTradeAndDispute(dispute, trade);
DisputeValidation.validateDonationAddress(dispute,
Objects.requireNonNull(trade.getDelayedPayoutTx()),
btcWalletService.getParams(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,34 @@

package bisq.core.support.dispute;

import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType;
import bisq.core.trade.model.bisq_v1.Contract;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.util.JsonUtil;
import bisq.core.util.validation.RegexValidatorFactory;

import bisq.network.p2p.NodeAddress;

import bisq.common.config.Config;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.Sig;
import bisq.common.util.Tuple3;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

Expand All @@ -46,6 +56,72 @@

@Slf4j
public class DisputeValidation {
public static void validateDisputeData(Dispute dispute,
BtcWalletService btcWalletService) throws ValidationException {
try {
Contract contract = dispute.getContract();
checkArgument(contract.getOfferPayload().getId().equals(dispute.getTradeId()), "Invalid tradeId");
checkArgument(dispute.getContractAsJson().equals(JsonUtil.objectToJson(contract)), "Invalid contractAsJson");
checkArgument(Arrays.equals(Objects.requireNonNull(dispute.getContractHash()), Hash.getSha256Hash(checkNotNull(dispute.getContractAsJson()))),
"Invalid contractHash");

Optional<Transaction> depositTx = dispute.findDepositTx(btcWalletService);
if (depositTx.isPresent()) {
checkArgument(depositTx.get().getTxId().toString().equals(dispute.getDepositTxId()), "Invalid depositTxId");
checkArgument(depositTx.get().getInputs().size() >= 2, "DepositTx must have at least 2 inputs");
}

try {
// Only the dispute opener has set the signature
String makerContractSignature = dispute.getMakerContractSignature();
if (makerContractSignature != null) {
Sig.verify(contract.getMakerPubKeyRing().getSignaturePubKey(),
dispute.getContractAsJson(),
makerContractSignature);
}
String takerContractSignature = dispute.getTakerContractSignature();
if (takerContractSignature != null) {
Sig.verify(contract.getTakerPubKeyRing().getSignaturePubKey(),
dispute.getContractAsJson(),
takerContractSignature);
}
} catch (CryptoException e) {
throw new ValidationException(dispute, e.getMessage());
}
} catch (Throwable t) {
throw new ValidationException(dispute, t.getMessage());
}
}

public static void validateTradeAndDispute(Dispute dispute, Trade trade)
throws ValidationException {
try {
checkArgument(dispute.getContract().equals(trade.getContract()),
"contract must match contract from trade");

checkNotNull(trade.getDelayedPayoutTx(), "trade.getDelayedPayoutTx() must not be null");
checkNotNull(dispute.getDelayedPayoutTxId(), "delayedPayoutTxId must not be null");
checkArgument(dispute.getDelayedPayoutTxId().equals(trade.getDelayedPayoutTx().getTxId().toString()),
"delayedPayoutTxId must match delayedPayoutTxId from trade");

checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null");
checkNotNull(dispute.getDepositTxId(), "depositTxId must not be null");
checkArgument(dispute.getDepositTxId().equals(trade.getDepositTx().getTxId().toString()),
"depositTx must match depositTx from trade");

checkNotNull(dispute.getDepositTxSerialized(), "depositTxSerialized must not be null");
} catch (Throwable t) {
throw new ValidationException(dispute, t.getMessage());
}
}

public static void validateSenderNodeAddress(Dispute dispute,
NodeAddress senderNodeAddress) throws NodeAddressException {
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
&& !senderNodeAddress.equals(dispute.getContract().getSellerNodeAddress())) {
throw new NodeAddressException(dispute, "senderNodeAddress not matching any of the traders node addresses");
}
}

public static void validateNodeAddresses(Dispute dispute, Config config)
throws NodeAddressException {
Expand Down Expand Up @@ -81,8 +157,7 @@ public static void validateDonationAddressMatchesAnyPastParamValues(Dispute disp
public static void validateDonationAddress(Dispute dispute,
Transaction delayedPayoutTx,
NetworkParameters params,
DaoFacade daoFacade
)
DaoFacade daoFacade)
throws AddressException {
TransactionOutput output = delayedPayoutTx.getOutput(0);
Address address = output.getScriptPubKey().getToAddress(params);
Expand Down Expand Up @@ -225,26 +300,26 @@ public static class ValidationException extends Exception {
@Getter
private final Dispute dispute;

public ValidationException(Dispute dispute, String msg) {
ValidationException(Dispute dispute, String msg) {
super(msg);
this.dispute = dispute;
}
}

public static class NodeAddressException extends ValidationException {
public NodeAddressException(Dispute dispute, String msg) {
NodeAddressException(Dispute dispute, String msg) {
super(dispute, msg);
}
}

public static class AddressException extends ValidationException {
public AddressException(Dispute dispute, String msg) {
AddressException(Dispute dispute, String msg) {
super(dispute, msg);
}
}

public static class DisputeReplayException extends ValidationException {
public DisputeReplayException(Dispute dispute, String msg) {
DisputeReplayException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
Expand Down
Loading