From 0388c7c88a84386815c8c22518c832cb63b2fa39 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:03:55 -0500 Subject: [PATCH 01/10] Add block explorer tx as hex request using completeable future api Signed-off-by: HenrikJannsen --- .../bisq/core/provider/MempoolHttpClient.java | 16 ++++++++++++++++ .../core/provider/mempool/MempoolRequest.java | 8 ++++++++ .../core/provider/mempool/MempoolService.java | 8 +++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/provider/MempoolHttpClient.java b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java index 18d40016647..5a36f023da4 100644 --- a/core/src/main/java/bisq/core/provider/MempoolHttpClient.java +++ b/core/src/main/java/bisq/core/provider/MempoolHttpClient.java @@ -27,6 +27,8 @@ import java.io.IOException; +import java.util.concurrent.CompletableFuture; + import javax.annotation.Nullable; @Singleton @@ -42,4 +44,18 @@ public String getTxDetails(String txId) throws IOException { String api = "/" + txId; return get(api, "User-Agent", "bisq/" + Version.VERSION); } + + + public CompletableFuture 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); + } + }); + } } diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java index fcfdeb4a453..6141d2ed225 100644 --- a/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolRequest.java @@ -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; @@ -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 requestTxAsHex(String txId) { + mempoolHttpClient.setBaseUrl(getRandomServiceAddress(txBroadcastServices)); + return mempoolHttpClient.requestTxAsHex(txId); + } + public boolean switchToAnotherProvider() { txBroadcastServices.remove(mempoolHttpClient.getBaseUrl()); return txBroadcastServices.size() > 0; diff --git a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java index 02e079eba5f..d0dcf806e36 100644 --- a/core/src/main/java/bisq/core/provider/mempool/MempoolService.java +++ b/core/src/main/java/bisq/core/provider/mempool/MempoolService.java @@ -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; @@ -129,7 +130,12 @@ public void checkTxIsConfirmed(String txId, Consumer resultHandler) mempoolRequest.getTxStatus(future, txId); } - // /////////////////////////// + public CompletableFuture requestTxAsHex(String txId) { + outstandingRequests++; + return new MempoolRequest(preferences, socks5ProxyProvider) + .requestTxAsHex(txId) + .whenComplete((result, throwable) -> outstandingRequests--); + } private void validateOfferMakerTx(MempoolRequest mempoolRequest, TxValidator txValidator, From 0553a0f9d4eba8a74ccd78bb4971c49e35202a11 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:15:11 -0500 Subject: [PATCH 02/10] Add cachedDepositTx field to Dispute Signed-off-by: HenrikJannsen --- .../java/bisq/core/support/dispute/Dispute.java | 17 ++++++++++++++++- .../core/support/dispute/DisputeValidation.java | 3 +-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index 887f355521c..ba4067b0860 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -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; @@ -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; @@ -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 @@ -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 cachedDepositTx = Optional.empty(); public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, @@ -181,6 +185,7 @@ public FileTransferSender createFileTransferSender(NetworkNode networkNode, return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback); } + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -501,6 +506,16 @@ public String getRoleStringForLogFile() { + (disputeOpenerIsMaker ? "MAKER" : "TAKER"); } + public Optional 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{" + diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java index 87741bda40c..809cd0eb883 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java @@ -81,8 +81,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); From 60cd8b1e16309d20a9b0cfd5dabebceb52a3468d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:15:41 -0500 Subject: [PATCH 03/10] Add requestBlockchainTransactions and verifyTradeTxChain methods to RefundManager Signed-off-by: HenrikJannsen --- .../support/dispute/refund/RefundManager.java | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index d81a9cf0754..8d46e28a60c 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -24,6 +24,7 @@ import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.provider.mempool.MempoolService; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; @@ -48,22 +49,32 @@ import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; +import bisq.common.util.Hex; + +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @Singleton public final class RefundManager extends DisputeManager { - + private final MempoolService mempoolService; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -81,9 +92,12 @@ public RefundManager(P2PService p2PService, KeyRing keyRing, RefundDisputeListService refundDisputeListService, Config config, - PriceFeedService priceFeedService) { + PriceFeedService priceFeedService, + MempoolService mempoolService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService); + + this.mempoolService = mempoolService; } @@ -238,4 +252,63 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { public NodeAddress getAgentNodeAddress(Dispute dispute) { return dispute.getContract().getRefundAgentNodeAddress(); } + + + public CompletableFuture> requestBlockchainTransactions(String makerFeeTxId, + String takerFeeTxId, + String depositTxId, + String delayedPayoutTxId) { + NetworkParameters params = btcWalletService.getParams(); + List txs = new ArrayList<>(); + return mempoolService.requestTxAsHex(makerFeeTxId) + .thenCompose(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return mempoolService.requestTxAsHex(takerFeeTxId); + }).thenCompose(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return mempoolService.requestTxAsHex(depositTxId); + }).thenCompose(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return mempoolService.requestTxAsHex(delayedPayoutTxId); + }) + .thenApply(txAsHex -> { + txs.add(new Transaction(params, Hex.decode(txAsHex))); + return txs; + }); + } + + public void verifyTradeTxChain(List txs) { + Transaction makerFeeTx = txs.get(0); + Transaction takerFeeTx = txs.get(1); + Transaction depositTx = txs.get(2); + Transaction delayedPayoutTx = txs.get(3); + + // The order and number of buyer and seller inputs are not part of the trade protocol consensus. + // In the current implementation buyer inputs come before seller inputs at depositTx and there is + // only 1 input per trader, but we do not want to rely on that. + // So we just check that both fee txs are found in the inputs. + boolean makerFeeTxFoundAtInputs = false; + boolean takerFeeTxFoundAtInputs = false; + for (TransactionInput transactionInput : depositTx.getInputs()) { + String fundingTxId = transactionInput.getOutpoint().getHash().toString(); + if (!makerFeeTxFoundAtInputs) { + makerFeeTxFoundAtInputs = fundingTxId.equals(makerFeeTx.getTxId().toString()); + } + if (!takerFeeTxFoundAtInputs) { + takerFeeTxFoundAtInputs = fundingTxId.equals(takerFeeTx.getTxId().toString()); + } + } + checkArgument(makerFeeTxFoundAtInputs, "makerFeeTx not found at depositTx inputs"); + checkArgument(takerFeeTxFoundAtInputs, "takerFeeTx not found at depositTx inputs"); + checkArgument(depositTx.getInputs().size() >= 2, + "DepositTx must have at least 2 inputs"); + checkArgument(depositTx.getOutputs().size() == 1, + "DepositTx must have 1 input"); + checkArgument(delayedPayoutTx.getInputs().size() == 1, + "DelayedPayoutTx must have 1 input"); + TransactionOutPoint delayedPayoutTxInputOutpoint = delayedPayoutTx.getInputs().get(0).getOutpoint(); + String fundingTxId = delayedPayoutTxInputOutpoint.getHash().toString(); + checkArgument(fundingTxId.equals(depositTx.getTxId().toString()), + "First input at delayedPayoutTx does not connect to depositTx"); + } } From c49b811da313d1e087e186ca36e8c46ab4c08319 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:24:05 -0500 Subject: [PATCH 04/10] Add verification of chain of transactions at DisputeSummaryWindow Signed-off-by: HenrikJannsen --- .../resources/i18n/displayStrings.properties | 5 ++ .../windows/DisputeSummaryWindow.java | 50 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ee16fa6c5c0..8ec4f81e156 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2736,6 +2736,11 @@ disputeSummaryWindow.reason=Reason of dispute disputeSummaryWindow.tradePeriodEnd=Trade period end disputeSummaryWindow.extraInfo=Extra information disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status +disputeSummaryWindow.requestingTxs=Requesting blockchain transactions from block explorer... +disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\ + Please verify the transactions manually before closing the dispute. +disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout transaction failed. Error message: {0}.\n\n\ + Please do not make the payout but get in touch with developers to clearify the case. # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index adea7f53f07..5d59792537d 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -55,6 +55,7 @@ import bisq.common.UserThread; import bisq.common.app.DevEnv; +import bisq.common.config.Config; import bisq.common.handlers.ResultHandler; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; @@ -692,9 +693,9 @@ private void addButtons(Contract contract) { !peersDisputeOptional.get().isClosed()) { showPayoutTxConfirmation(contract, disputeResult, - () -> doCloseIfValid(closeTicketButton)); + () -> doCloseAfterTxsVerified(closeTicketButton)); } else { - doCloseIfValid(closeTicketButton); + doCloseAfterTxsVerified(closeTicketButton); } }); @@ -811,6 +812,51 @@ public void onFailure(TxBroadcastException exception) { } } + private void doCloseAfterTxsVerified(Button closeTicketButton) { + var disputeManager = getDisputeManager(dispute); + // Only RefundAgent need to verify transactions to ensure payout is safe + if (disputeManager instanceof RefundManager && Config.baseCurrencyNetwork().isMainnet()) { + RefundManager refundManager = (RefundManager) disputeManager; + Contract contract = dispute.getContract(); + String makerFeeTxId = contract.getOfferPayload().getOfferFeePaymentTxId(); + String takerFeeTxId = contract.getTakerFeeTxID(); + String depositTxId = dispute.getDepositTxId(); + String delayedPayoutTxId = dispute.getDelayedPayoutTxId(); + Popup requestingTxsPopup = new Popup().feedback(Res.get("disputeSummaryWindow.requestingTxs")); + requestingTxsPopup.show(); + refundManager.requestBlockchainTransactions(makerFeeTxId, + takerFeeTxId, + depositTxId, + delayedPayoutTxId + ).whenComplete((txList, throwable) -> { + UserThread.execute(() -> { + if (throwable == null) { + try { + requestingTxsPopup.hide(); + refundManager.verifyTradeTxChain(txList); + doCloseIfValid(closeTicketButton); + } catch (Throwable error) { + UserThread.runAfter(() -> + new Popup().warning(Res.get("disputeSummaryWindow.delayedPayoutTxVerificationFailed", error.getMessage())) + .show(), + 100, + TimeUnit.MILLISECONDS); + } + } else { + UserThread.runAfter(() -> + new Popup().warning(Res.get("disputeSummaryWindow.requestTransactionsError", throwable.getMessage())) + .onAction(() -> doCloseIfValid(closeTicketButton)) + .show(), + 100, + TimeUnit.MILLISECONDS); + } + }); + }); + } else { + doCloseIfValid(closeTicketButton); + } + } + private void doCloseIfValid(Button closeTicketButton) { var disputeManager = checkNotNull(getDisputeManager(dispute)); try { From 04e501d450d6ba6b60a35a52ad3922868ac7552c Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:55:52 -0500 Subject: [PATCH 05/10] We had not updated the contract hash after setting the signature. We verify later the contract hash in disputes and that would fail otherwise. --- .../buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java | 2 +- .../seller/SellerProcessShareBuyerPaymentAccountMessage.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java index 0318f615546..f5dfba5de76 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java @@ -103,7 +103,7 @@ protected void run() { trade.setTakerContractSignature(signature); } - byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); + byte[] contractHash = Hash.getSha256Hash(checkNotNull(contractAsJson)); trade.setContractHash(contractHash); } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerProcessShareBuyerPaymentAccountMessage.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerProcessShareBuyerPaymentAccountMessage.java index 1659f1fde6b..286f4c43ffd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerProcessShareBuyerPaymentAccountMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/SellerProcessShareBuyerPaymentAccountMessage.java @@ -42,6 +42,7 @@ import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; import bisq.core.util.JsonUtil; +import bisq.common.crypto.Hash; import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; @@ -91,6 +92,9 @@ protected void run() { trade.setMakerContractSignature(signature); } + byte[] contractHash = Hash.getSha256Hash(checkNotNull(contractAsJson)); + trade.setContractHash(contractHash); + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); processModel.getTradeManager().requestPersistence(); From 14312f08afe4e12542f497ba8bc206baef39226e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 2 Nov 2022 23:59:12 -0500 Subject: [PATCH 06/10] Add validateDisputeData, validateTradeAndDispute and validateSenderNodeAddress methods Signed-off-by: HenrikJannsen --- .../core/support/dispute/DisputeManager.java | 11 ++- .../support/dispute/DisputeValidation.java | 84 ++++++++++++++++++- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 7b61d5e4dcf..6163c700398 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -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); } @@ -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(), diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java index 809cd0eb883..d29bd404f2e 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java @@ -17,13 +17,20 @@ 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; @@ -31,10 +38,13 @@ 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; @@ -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 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"); + checkArgument(depositTx.get().getOutputs().size() == 1, "DepositTx must have exactly 1 output"); + } + + try { + 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 { @@ -224,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); } } From 8d8cdc14b416b1eec9e8799137328eae8299db15 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 3 Nov 2022 10:35:36 -0500 Subject: [PATCH 07/10] Cleanup --- core/src/main/java/bisq/core/support/dispute/Dispute.java | 1 + .../main/java/bisq/core/support/dispute/DisputeValidation.java | 1 + .../java/bisq/core/support/dispute/refund/RefundManager.java | 1 - .../trade/protocol/bisq_v1/tasks/taker/CreateTakerFeeTx.java | 3 +-- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index ba4067b0860..451c966c2df 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -549,6 +549,7 @@ public String toString() { ",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + ",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' + + ",\n cachedDepositTx='" + cachedDepositTx + '\'' + "\n}"; } } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java index d29bd404f2e..6fa5d9a5e29 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java @@ -73,6 +73,7 @@ public static void validateDisputeData(Dispute dispute, } try { + // Only the dispute opener has set the signature String makerContractSignature = dispute.getMakerContractSignature(); if (makerContractSignature != null) { Sig.verify(contract.getMakerPubKeyRing().getSignaturePubKey(), diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 8d46e28a60c..63f271a7a01 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -253,7 +253,6 @@ public NodeAddress getAgentNodeAddress(Dispute dispute) { return dispute.getContract().getRefundAgentNodeAddress(); } - public CompletableFuture> requestBlockchainTransactions(String makerFeeTxId, String takerFeeTxId, String depositTxId, diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/taker/CreateTakerFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/taker/CreateTakerFeeTx.java index 327177aae28..1371df86163 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/taker/CreateTakerFeeTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/taker/CreateTakerFeeTx.java @@ -65,9 +65,8 @@ protected void run() { TradeWalletService tradeWalletService = processModel.getTradeWalletService(); Transaction transaction; - String feeReceiver = FeeReceiverSelector.getAddress(processModel.getFilterManager()); - if (trade.isCurrencyForTakerFeeBtc()) { + String feeReceiver = FeeReceiverSelector.getAddress(processModel.getFilterManager()); transaction = tradeWalletService.createBtcTradingFeeTx( fundingAddress, reservedForTradeAddress, From cba35b63427fed1e44e95f5928b611bc04baca49 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 3 Nov 2022 12:21:47 -0500 Subject: [PATCH 08/10] Remove deposit tx output size check. It is allowed that there are change outputs, for instance if taker does not take full trade amount, there is a change output for maker. Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/support/dispute/DisputeValidation.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java index 6fa5d9a5e29..f8161f94e17 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeValidation.java @@ -69,7 +69,6 @@ public static void validateDisputeData(Dispute dispute, 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"); - checkArgument(depositTx.get().getOutputs().size() == 1, "DepositTx must have exactly 1 output"); } try { From 81be778df37df61607399ce9748e17037f2157cf Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 3 Nov 2022 14:14:32 -0500 Subject: [PATCH 09/10] Remove deposit tx output size check. It is allowed that there are change outputs, for instance if taker does not take full trade amount, there is a change output for maker. Signed-off-by: HenrikJannsen --- .../java/bisq/core/support/dispute/refund/RefundManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 63f271a7a01..4e52c51398d 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -301,8 +301,6 @@ public void verifyTradeTxChain(List txs) { checkArgument(takerFeeTxFoundAtInputs, "takerFeeTx not found at depositTx inputs"); checkArgument(depositTx.getInputs().size() >= 2, "DepositTx must have at least 2 inputs"); - checkArgument(depositTx.getOutputs().size() == 1, - "DepositTx must have 1 input"); checkArgument(delayedPayoutTx.getInputs().size() == 1, "DelayedPayoutTx must have 1 input"); TransactionOutPoint delayedPayoutTxInputOutpoint = delayedPayoutTx.getInputs().get(0).getOutpoint(); From b8637d067c6c5bd6e1e6ebc087d124369ea6f85e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 3 Nov 2022 17:16:13 -0500 Subject: [PATCH 10/10] Move requestingTxsPopup.hide() before if/else case Signed-off-by: HenrikJannsen --- .../desktop/main/overlays/windows/DisputeSummaryWindow.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 5d59792537d..8313e27f589 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -830,9 +830,10 @@ private void doCloseAfterTxsVerified(Button closeTicketButton) { delayedPayoutTxId ).whenComplete((txList, throwable) -> { UserThread.execute(() -> { + requestingTxsPopup.hide(); + if (throwable == null) { try { - requestingTxsPopup.hide(); refundManager.verifyTradeTxChain(txList); doCloseIfValid(closeTicketButton); } catch (Throwable error) {