mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-23 03:59:36 +00:00
migrate to DisputeValidation
Co-authored-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
parent
6f16a5ee92
commit
190003b5ba
6 changed files with 369 additions and 190 deletions
|
@ -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
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue