Skip to content

Commit

Permalink
Refactor replay detection logic in DisputeValidation
Browse files Browse the repository at this point in the history
Replace the large tuple of 'Map<String, Set<String>>' objects, built by
'DisputeValidation.getTestReplayHashMaps' to detect triplicate trade &
tx IDs across all the disputes, with a map from dispute field refs to
multimaps of all the corresponding fieldValue-disputeUid mappings. This
eliminates a lot of the repetition building the individual hash maps of
the tuple and consuming them, as a map was needed for each ID field of
'Dispute' with triplicate detection, namely the five fields:

  tradeId, delayedPayoutTxId, warningTxId, redirectTxId, depositTxId.

For this purpose, create a private 'DisputeIdField' enum of field refs
encapsulating the field name (for log & error messages) and getter.

(Triplicated rather than duplicated IDs are being detected because the
dispute DTOs come in pairs: one for the buyer and one for the seller.)
  • Loading branch information
stejbac committed Oct 3, 2024
1 parent 4230a38 commit 4b318a0
Showing 1 changed file with 58 additions and 104 deletions.
162 changes: 58 additions & 104 deletions core/src/main/java/bisq/core/support/dispute/DisputeValidation.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,25 @@
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.Sig;
import bisq.common.util.Tuple5;

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

import com.google.common.base.CaseFormat;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.EnumMap;
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;
import java.util.function.Function;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -203,153 +206,104 @@ public static void validateDonationAddress(Dispute dispute,
"; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx());
}

// TODO: Refactor:
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>> disputesPerWarningId = tuple.third;
Map<String, Set<String>> disputesPerRedirectTxId = tuple.fourth;
Map<String, Set<String>> disputesPerDepositTxId = tuple.fifth;

var map = getTestReplayMultimaps(disputeList);
disputeList.forEach(disputeToTest -> {
try {
testIfDisputeTriesReplay(disputeToTest,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerWarningId,
disputesPerRedirectTxId,
disputesPerDepositTxId);

testIfDisputeTriesReplay(disputeToTest, map);
} catch (DisputeReplayException e) {
exceptionHandler.accept(e);
}
});
}

// TODO: Refactor:
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>> disputesPerWarningTxId = tuple.third;
Map<String, Set<String>> disputesPerRedirectTxId = tuple.fourth;
Map<String, Set<String>> disputesPerDepositTxId = tuple.fifth;

testIfDisputeTriesReplay(dispute,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerWarningTxId,
disputesPerRedirectTxId,
disputesPerDepositTxId);
testIfDisputeTriesReplay(dispute, getTestReplayMultimaps(disputeList));
}

// TODO: Refactor:
private static Tuple5<Map<String, Set<String>>, Map<String, Set<String>>, 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>> disputesPerWarningTxId = new HashMap<>();
Map<String, Set<String>> disputesPerRedirectTxId = new HashMap<>();
Map<String, Set<String>> disputesPerDepositTxId = new HashMap<>();
private static Map<DisputeIdField, SetMultimap<String, String>> getTestReplayMultimaps(List<Dispute> disputeList) {
Map<DisputeIdField, SetMultimap<String, String>> disputesPerIdMap = new EnumMap<>(DisputeIdField.class);
disputeList.forEach(dispute -> {
String uid = dispute.getUid();
String disputeUid = dispute.getUid();

String tradeId = dispute.getTradeId();
disputesPerTradeId.computeIfAbsent(tradeId, id -> new HashSet<>()).add(uid);

String delayedPayoutTxId = dispute.getDelayedPayoutTxId();
if (delayedPayoutTxId != null) {
disputesPerDelayedPayoutTxId.computeIfAbsent(delayedPayoutTxId, id -> new HashSet<>()).add(uid);
}
String warningTxId = dispute.getWarningTxId();
if (warningTxId != null) {
disputesPerWarningTxId.computeIfAbsent(warningTxId, id -> new HashSet<>()).add(uid);
}
String redirectTxId = dispute.getRedirectTxId();
if (redirectTxId != null) {
disputesPerRedirectTxId.computeIfAbsent(redirectTxId, id -> new HashSet<>()).add(uid);
}
String depositTxId = dispute.getDepositTxId();
if (depositTxId != null) {
disputesPerDepositTxId.computeIfAbsent(depositTxId, id -> new HashSet<>()).add(uid);
for (var field : DisputeIdField.values()) {
String id = field.apply(dispute);
if (id != null) {
disputesPerIdMap.computeIfAbsent(field, k -> HashMultimap.create()).put(id, disputeUid);
}
}
});

return new Tuple5<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerWarningTxId,
disputesPerRedirectTxId, disputesPerDepositTxId);
return disputesPerIdMap;
}

// TODO: Refactor:
private static void testIfDisputeTriesReplay(Dispute disputeToTest,
Map<String, Set<String>> disputesPerTradeId,
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
Map<String, Set<String>> disputesPerWarningTxId,
Map<String, Set<String>> disputesPerRedirectTxId,
Map<String, Set<String>> disputesPerDepositTxId)
Map<DisputeIdField, SetMultimap<String, String>> disputesPerIdMap)
throws DisputeReplayException {
try {
String disputeToTestTradeId = disputeToTest.getTradeId();
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
String disputeToTestWarningTxId = disputeToTest.getWarningTxId();
String disputeToTestRedirectTxId = disputeToTest.getRedirectTxId();
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.
// With 1.4.0 we send the delayed payout tx also in mediation cases. For v5 protocol trades, there is no DPT
// and it is unknown which staged txs will be published, if any, so they are only sent in refund agent cases.
if (disputeToTest.getSupportType() == SupportType.REFUND) {
if (disputeToTestWarningTxId == null) {
checkNotNull(disputeToTestDelayedPayoutTxId,
if (disputeToTest.getWarningTxId() == null) {
checkNotNull(disputeToTest.getDelayedPayoutTxId(),
"Delayed payout transaction ID is null. " +
"Trade ID: %s", disputeToTestTradeId);
} else {
checkNotNull(disputeToTestRedirectTxId,
checkNotNull(disputeToTest.getRedirectTxId(),
"Redirect transaction ID is null. " +
"Trade ID: %s", disputeToTestTradeId);
}
}
checkNotNull(disputeToTestDepositTxId,
checkNotNull(disputeToTest.getDepositTxId(),
"depositTxId must not be null. Trade ID: %s", disputeToTestTradeId);
checkNotNull(disputeToTestUid,
checkNotNull(disputeToTest.getUid(),
"agentsUid must not be null. Trade ID: %s", disputeToTestTradeId);

Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
checkArgument(disputesPerTradeIdItems == null || disputesPerTradeIdItems.size() <= 2,
"We found more than 2 disputes with the same trade ID. " +
"Trade ID: %s", disputeToTestTradeId);
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
checkArgument(disputesPerDelayedPayoutTxIdItems == null || disputesPerDelayedPayoutTxIdItems.size() <= 2,
"We found more than 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: %s", disputeToTestTradeId);
Set<String> disputesPerWarningTxIdItems = disputesPerWarningTxId.get(disputeToTestWarningTxId);
checkArgument(disputesPerWarningTxIdItems == null || disputesPerWarningTxIdItems.size() <= 2,
"We found more than 2 disputes with the same warningTxId. " +
"Trade ID: %s", disputeToTestTradeId);
Set<String> disputesPerRedirectTxIdItems = disputesPerRedirectTxId.get(disputeToTestRedirectTxId);
checkArgument(disputesPerRedirectTxIdItems == null || disputesPerRedirectTxIdItems.size() <= 2,
"We found more than 2 disputes with the same redirectTxId. " +
"Trade ID: %s", disputeToTestTradeId);
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
checkArgument(disputesPerDepositTxIdItems == null || disputesPerDepositTxIdItems.size() <= 2,
"We found more than 2 disputes with the same depositTxId. " +
"Trade ID: %s", disputeToTestTradeId);
for (DisputeIdField field : disputesPerIdMap.keySet()) {
String id = field.apply(disputeToTest);
int numDisputesPerId = disputesPerIdMap.get(field).keys().count(id);
checkArgument(numDisputesPerId <= 2,
"We found more than 2 disputes with the same %s. " +
"Trade ID: %s", field, disputeToTestTradeId);
}
} catch (IllegalArgumentException e) {
throw new DisputeReplayException(disputeToTest, e.getMessage());
} catch (NullPointerException e) {
log.error("NullPointerException at testIfDisputeTriesReplay: " +
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
"disputesPerWarningTxId={}, disputesPerRedirectTxId={}, " +
"disputesPerDepositTxId={}",
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerWarningTxId,
disputesPerRedirectTxId, disputesPerDepositTxId);
"disputeToTest={}, disputesPerIdMap={}", disputeToTest, disputesPerIdMap);
throw new DisputeReplayException(disputeToTest, e + " at dispute " + disputeToTest);
}
}

private enum DisputeIdField implements Function<Dispute, String> {
TRADE_ID(Dispute::getTradeId),
DELAYED_PAYOUT_TX_ID(Dispute::getDelayedPayoutTxId),
WARNING_TX_ID(Dispute::getWarningTxId),
REDIRECT_TX_ID(Dispute::getRedirectTxId),
DEPOSIT_TX_ID(Dispute::getDepositTxId);

private final Function<Dispute, String> getter;

DisputeIdField(Function<Dispute, String> getter) {
this.getter = getter;
}

@Override
public String apply(Dispute dispute) {
return getter.apply(dispute);
}

@Override
public String toString() {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name());
}
}


///////////////////////////////////////////////////////////////////////////////////////////
// Exceptions
Expand Down

0 comments on commit 4b318a0

Please sign in to comment.