migrate to DisputeValidation

Co-authored-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
woodser 2023-02-10 11:10:51 -05:00
parent 6f16a5ee92
commit 190003b5ba
6 changed files with 369 additions and 190 deletions

View file

@ -104,7 +104,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
private final String depositTxId; private final String depositTxId;
@Nullable @Nullable
private final String payoutTxId; private final String payoutTxId;
private final String contractAsJson; private String contractAsJson;
@Nullable @Nullable
private final String makerContractSignature; private final String makerContractSignature;
@Nullable @Nullable
@ -351,6 +351,39 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION; return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
} }
public boolean removeAllChatMessages() {
if (chatMessages.size() > 1) {
// removes all chat except the initial guidelines message.
String firstMessageUid = chatMessages.get(0).getUid();
chatMessages.removeIf((msg) -> !msg.getUid().equals(firstMessageUid));
return true;
}
return false;
}
public void maybeClearSensitiveData() {
String change = "";
if (contract.maybeClearSensitiveData()) {
change += "contract;";
}
String edited = Contract.sanitizeContractAsJson(contractAsJson);
if (!edited.equals(contractAsJson)) {
contractAsJson = edited;
change += "contractAsJson;";
}
if (removeAllChatMessages()) {
change += "chat messages;";
}
if (change.length() > 0) {
log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId()));
}
}
// sanitizes a contract json string
public static String sanitizeContractAsJson(String contractAsJson) {
return contractAsJson;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Setters // Setters
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -40,7 +40,6 @@ import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.HavenoUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.protocol.TradePeer; import bisq.core.trade.protocol.TradePeer;
import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.BootstrapListener;
@ -67,6 +66,7 @@ import javafx.collections.ObservableList;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.KeyPair; import java.security.KeyPair;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@ -101,7 +101,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
private final PriceFeedService priceFeedService; private final PriceFeedService priceFeedService;
@Getter @Getter
protected final ObservableList<TradeDataValidation.ValidationException> validationExceptions = protected final ObservableList<DisputeValidation.ValidationException> validationExceptions =
FXCollections.observableArrayList(); FXCollections.observableArrayList();
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -272,21 +272,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
List<Dispute> disputes = getDisputeList().getList(); List<Dispute> disputes = getDisputeList().getList();
disputes.forEach(dispute -> { disputes.forEach(dispute -> {
try { try {
TradeDataValidation.validateDonationAddress(dispute, dispute.getDonationAddressOfDelayedPayoutTx()); DisputeValidation.validateNodeAddresses(dispute, config);
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); } catch (DisputeValidation.ValidationException e) {
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
} catch (TradeDataValidation.AddressException | TradeDataValidation.NodeAddressException e) {
log.error(e.toString()); log.error(e.toString());
validationExceptions.add(e); validationExceptions.add(e);
} }
}); });
// TODO (woodser): disabled for xmr, needed? maybeClearSensitiveData();
// TradeDataValidation.testIfAnyDisputeTriedReplay(disputes,
// disputeReplayException -> {
// log.error(disputeReplayException.toString());
// validationExceptions.add(disputeReplayException);
// });
} }
public boolean isTrader(Dispute dispute) { public boolean isTrader(Dispute dispute) {
@ -304,6 +297,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
} }
public void maybeClearSensitiveData() {
log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName());
Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing();
getDisputeList().getList().stream()
.filter(e -> e.isClosed())
.filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate))
.forEach(Dispute::maybeClearSensitiveData);
requestPersistence();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Dispute handling // Dispute handling
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -457,14 +460,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// validate dispute // validate dispute
try { try {
TradeDataValidation.validatePaymentAccountPayload(dispute); DisputeValidation.validateDisputeData(dispute);
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx()); DisputeValidation.validateNodeAddresses(dispute, config);
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed? DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress());
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config); DisputeValidation.validatePaymentAccountPayload(dispute);
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config); //DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
} catch (TradeDataValidation.AddressException | } catch (DisputeValidation.ValidationException e) {
TradeDataValidation.NodeAddressException |
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
validationExceptions.add(e); validationExceptions.add(e);
throw e; throw e;
} }

View file

@ -0,0 +1,299 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute;
import bisq.core.support.SupportType;
import bisq.core.trade.Contract;
import bisq.core.trade.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.Set;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class DisputeValidation {
public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException {
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract");
}
public static void validateDisputeData(Dispute dispute) 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");
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");
} 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())
&& !senderNodeAddress.equals(dispute.getContract().getArbitratorNodeAddress())) {
throw new NodeAddressException(dispute, "senderNodeAddress not matching any of the traders node addresses");
}
}
public static void validateNodeAddresses(Dispute dispute, Config config)
throws NodeAddressException {
if (!config.useLocalhostForP2P) {
validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress());
validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress());
}
}
private static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress) throws NodeAddressException {
if (!RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) {
String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " +
dispute.getShortTradeId() + " is not a valid address";
log.error(msg);
throw new NodeAddressException(dispute, msg);
}
}
public static void validateDonationAddress(Dispute dispute,
Transaction delayedPayoutTx,
NetworkParameters params)
throws AddressException {
TransactionOutput output = delayedPayoutTx.getOutput(0);
Address address = output.getScriptPubKey().getToAddress(params);
if (address == null) {
String errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new DisputeValidation.AddressException(dispute, errorMsg);
}
// Verify that address in the dispute matches the one in the trade.
String delayedPayoutTxOutputAddress = address.toString();
checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()),
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx. " +
"delayedPayoutTxOutputAddress=" + delayedPayoutTxOutputAddress +
"; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx());
}
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
Consumer<DisputeReplayException> exceptionHandler) {
var tuple = getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
disputeList.forEach(disputeToTest -> {
try {
testIfDisputeTriesReplay(disputeToTest,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
} catch (DisputeReplayException e) {
exceptionHandler.accept(e);
}
});
}
public static void testIfDisputeTriesReplay(Dispute dispute,
List<Dispute> disputeList) throws DisputeReplayException {
var tuple = getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
testIfDisputeTriesReplay(dispute,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
}
private static Tuple3<Map<String, Set<String>>, Map<String, Set<String>>, Map<String, Set<String>>> getTestReplayHashMaps(
List<Dispute> disputeList) {
Map<String, Set<String>> disputesPerTradeId = new HashMap<>();
Map<String, Set<String>> disputesPerDelayedPayoutTxId = new HashMap<>();
Map<String, Set<String>> disputesPerDepositTxId = new HashMap<>();
disputeList.forEach(dispute -> {
String uid = dispute.getUid();
String tradeId = dispute.getTradeId();
disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>());
Set<String> set = disputesPerTradeId.get(tradeId);
set.add(uid);
String delayedPayoutTxId = dispute.getDelayedPayoutTxId();
if (delayedPayoutTxId != null) {
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
set.add(uid);
}
String depositTxId = dispute.getDepositTxId();
if (depositTxId != null) {
disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>());
set = disputesPerDepositTxId.get(depositTxId);
set.add(uid);
}
});
return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
}
private static void testIfDisputeTriesReplay(Dispute disputeToTest,
Map<String, Set<String>> disputesPerTradeId,
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
Map<String, Set<String>> disputesPerDepositTxId)
throws DisputeReplayException {
try {
String disputeToTestTradeId = disputeToTest.getTradeId();
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
String disputeToTestDepositTxId = disputeToTest.getDepositTxId();
String disputeToTestUid = disputeToTest.getUid();
// For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do.
// So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the
// delayed payout tx also in mediation cases and that if check can be removed.
if (disputeToTest.getSupportType() == SupportType.REFUND) {
checkNotNull(disputeToTestDelayedPayoutTxId,
"Delayed payout transaction ID is null. " +
"Trade ID: " + disputeToTestTradeId);
}
checkNotNull(disputeToTestDepositTxId,
"depositTxId must not be null. Trade ID: " + disputeToTestTradeId);
checkNotNull(disputeToTestUid,
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);
Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2,
"We found more then 2 disputes with the same trade ID. " +
"Trade ID: " + disputeToTestTradeId);
if (!disputesPerDelayedPayoutTxId.isEmpty()) {
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2,
"We found more then 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: " + disputeToTestTradeId);
}
if (!disputesPerDepositTxId.isEmpty()) {
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2,
"We found more then 2 disputes with the same depositTxId. " +
"Trade ID: " + disputeToTestTradeId);
}
} catch (IllegalArgumentException e) {
throw new DisputeReplayException(disputeToTest, e.getMessage());
} catch (NullPointerException e) {
log.error("NullPointerException at testIfDisputeTriesReplay: " +
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
"disputesPerDepositTxId={}",
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString());
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Exceptions
///////////////////////////////////////////////////////////////////////////////////////////
public static class ValidationException extends Exception {
@Getter
private final Dispute dispute;
ValidationException(Dispute dispute, String msg) {
super(msg);
this.dispute = dispute;
}
}
public static class NodeAddressException extends ValidationException {
NodeAddressException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
public static class AddressException extends ValidationException {
AddressException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
public static class DisputeReplayException extends ValidationException {
DisputeReplayException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
}

View file

@ -257,6 +257,15 @@ public final class Contract implements NetworkPayload {
return isBuyerMakerAndSellerTaker() == isMyRoleBuyer(myPubKeyRing); return isBuyerMakerAndSellerTaker() == isMyRoleBuyer(myPubKeyRing);
} }
public boolean maybeClearSensitiveData() {
return false; // TODO: anything to clear?
}
// edits a contract json string
public static String sanitizeContractAsJson(String contractAsJson) {
return contractAsJson; // TODO: anything to sanitize?
}
public void printDiff(@Nullable String peersContractAsJson) { public void printDiff(@Nullable String peersContractAsJson) {
String json = JsonUtil.objectToJson(this); String json = JsonUtil.objectToJson(this);
String diff = StringUtils.difference(json, peersContractAsJson); String diff = StringUtils.difference(json, peersContractAsJson);

View file

@ -19,14 +19,7 @@ package bisq.core.trade;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.util.validation.RegexValidatorFactory;
import bisq.network.p2p.NodeAddress;
import bisq.common.config.Config;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Address; import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
@ -35,12 +28,6 @@ import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput; 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.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.Getter; import lombok.Getter;
@ -54,153 +41,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
public class TradeDataValidation { public class TradeDataValidation {
public static void validatePaymentAccountPayload(Dispute dispute) throws InvalidPaymentAccountPayloadException {
if (dispute.getSellerPaymentAccountPayload() == null) throw new InvalidPaymentAccountPayloadException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new InvalidPaymentAccountPayloadException(dispute, "Hash of maker's payment account payload does not match contract");
}
public static void validateDonationAddress(String addressAsString)
throws AddressException {
validateDonationAddress(null, addressAsString);
}
public static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress, Config config)
throws NodeAddressException {
if (!config.useLocalhostForP2P && !RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) {
String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " +
dispute.getShortTradeId() + " is not a valid address";
log.error(msg);
throw new NodeAddressException(dispute, msg);
}
}
public static void validateDonationAddress(@Nullable Dispute dispute, String addressAsString)
throws AddressException {
if (addressAsString == null) {
log.debug("address is null at validateDonationAddress. This is expected in case of an not updated trader.");
return;
}
}
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
Consumer<DisputeReplayException> exceptionHandler) {
var tuple = getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
disputeList.forEach(disputeToTest -> {
try {
testIfDisputeTriesReplay(disputeToTest,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
} catch (DisputeReplayException e) {
exceptionHandler.accept(e);
}
});
}
public static void testIfDisputeTriesReplay(Dispute dispute,
List<Dispute> disputeList) throws DisputeReplayException {
var tuple = TradeDataValidation.getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
testIfDisputeTriesReplay(dispute,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
}
private static Tuple3<Map<String, Set<String>>, Map<String, Set<String>>, Map<String, Set<String>>> getTestReplayHashMaps(
List<Dispute> disputeList) {
Map<String, Set<String>> disputesPerTradeId = new HashMap<>();
Map<String, Set<String>> disputesPerDelayedPayoutTxId = new HashMap<>();
Map<String, Set<String>> disputesPerDepositTxId = new HashMap<>();
disputeList.forEach(dispute -> {
String uid = dispute.getUid();
String tradeId = dispute.getTradeId();
disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>());
Set<String> set = disputesPerTradeId.get(tradeId);
set.add(uid);
String delayedPayoutTxId = dispute.getDelayedPayoutTxId();
if (delayedPayoutTxId != null) {
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
set.add(uid);
}
String depositTxId = dispute.getDepositTxId();
if (depositTxId != null) {
disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>());
set = disputesPerDepositTxId.get(depositTxId);
set.add(uid);
}
});
return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
}
private static void testIfDisputeTriesReplay(Dispute disputeToTest,
Map<String, Set<String>> disputesPerTradeId,
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
Map<String, Set<String>> disputesPerDepositTxId)
throws DisputeReplayException {
try {
String disputeToTestTradeId = disputeToTest.getTradeId();
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
String disputeToTestDepositTxId = disputeToTest.getDepositTxId();
String disputeToTestUid = disputeToTest.getUid();
// For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do.
// So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the
// delayed payout tx also in mediation cases and that if check can be removed.
if (disputeToTest.getSupportType() == SupportType.REFUND) {
checkNotNull(disputeToTestDelayedPayoutTxId,
"Delayed payout transaction ID is null. " +
"Trade ID: " + disputeToTestTradeId);
}
checkNotNull(disputeToTestDepositTxId,
"depositTxId must not be null. Trade ID: " + disputeToTestTradeId);
checkNotNull(disputeToTestUid,
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);
Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2,
"We found more then 2 disputes with the same trade ID. " +
"Trade ID: " + disputeToTestTradeId);
if (!disputesPerDelayedPayoutTxId.isEmpty()) {
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2,
"We found more then 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: " + disputeToTestTradeId);
}
if (!disputesPerDepositTxId.isEmpty()) {
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2,
"We found more then 2 disputes with the same depositTxId. " +
"Trade ID: " + disputeToTestTradeId);
}
} catch (IllegalArgumentException e) {
throw new DisputeReplayException(disputeToTest, e.getMessage());
} catch (NullPointerException e) {
log.error("NullPointerException at testIfDisputeTriesReplay: " +
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
"disputesPerDepositTxId={}",
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString());
}
}
public static void validateDelayedPayoutTx(Trade trade, public static void validateDelayedPayoutTx(Trade trade,
Transaction delayedPayoutTx, Transaction delayedPayoutTx,
BtcWalletService btcWalletService) BtcWalletService btcWalletService)
@ -315,8 +155,6 @@ public class TradeDataValidation {
addressConsumer.accept(addressAsString); addressConsumer.accept(addressAsString);
} }
validateDonationAddress(addressAsString);
if (dispute != null) { if (dispute != null) {
// Verify that address in the dispute matches the one in the trade. // Verify that address in the dispute matches the one in the trade.
String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx(); String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx();

View file

@ -30,9 +30,9 @@ import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeValidation;
import bisq.core.support.dispute.agent.MultipleHolderNameDetection; import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
@ -60,13 +60,12 @@ import java.util.List;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import static bisq.core.trade.TradeDataValidation.ValidationException;
import static bisq.desktop.util.FormBuilder.getIconForLabel; import static bisq.desktop.util.FormBuilder.getIconForLabel;
public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener {
private final MultipleHolderNameDetection multipleHolderNameDetection; private final MultipleHolderNameDetection multipleHolderNameDetection;
private ListChangeListener<ValidationException> validationExceptionListener; private ListChangeListener<DisputeValidation.ValidationException> validationExceptionListener;
public DisputeAgentView(DisputeManager<? extends DisputeList<Dispute>> disputeManager, public DisputeAgentView(DisputeManager<? extends DisputeList<Dispute>> disputeManager,
KeyRing keyRing, KeyRing keyRing,
@ -126,7 +125,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
}; };
} }
protected void showWarningForValidationExceptions(List<? extends ValidationException> exceptions) { protected void showWarningForValidationExceptions(List<? extends DisputeValidation.ValidationException> exceptions) {
exceptions.stream() exceptions.stream()
.filter(ex -> ex.getDispute() != null) .filter(ex -> ex.getDispute() != null)
.filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases .filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases
@ -134,7 +133,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
.forEach(ex -> new Popup().width(900).warning(getValidationExceptionMessage(ex)).dontShowAgainId(getKey(ex)).show()); .forEach(ex -> new Popup().width(900).warning(getValidationExceptionMessage(ex)).dontShowAgainId(getKey(ex)).show());
} }
private String getKey(ValidationException exception) { private String getKey(DisputeValidation.ValidationException exception) {
Dispute dispute = exception.getDispute(); Dispute dispute = exception.getDispute();
if (dispute != null) { if (dispute != null) {
return "ValExcPopup-" + dispute.getTradeId() + "-" + dispute.getTraderId(); return "ValExcPopup-" + dispute.getTradeId() + "-" + dispute.getTraderId();
@ -142,9 +141,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
return "ValExcPopup-" + exception.toString(); return "ValExcPopup-" + exception.toString();
} }
private String getValidationExceptionMessage(ValidationException exception) { private String getValidationExceptionMessage(DisputeValidation.ValidationException exception) {
Dispute dispute = exception.getDispute(); Dispute dispute = exception.getDispute();
if (dispute != null && exception instanceof TradeDataValidation.AddressException) { if (dispute != null && exception instanceof DisputeValidation.AddressException) {
return getAddressExceptionMessage(dispute); return getAddressExceptionMessage(dispute);
} else if (exception.getMessage() != null && !exception.getMessage().isEmpty()) { } else if (exception.getMessage() != null && !exception.getMessage().isEmpty()) {
return exception.getMessage(); return exception.getMessage();