Skip to content

Commit

Permalink
Ensure v5 staged txs are linked to trade in Transactions view
Browse files Browse the repository at this point in the history
Make sure 'TransactionAwareTrade::isRelatedToTransaction' returns true
for warning, redirect & claim txs belonging to the given trade. Also
optimise the method somewhat by short circuiting on a wider class of txs
than those with zero locktime, when ruling out that the tx is a delayed
payout or warning tx. The previous short circuit test was inadequate due
to the fact that a lot of wallets, such as Sparrow, set a nonzero
locktime on all txs by default, to prevent fee sniping.

Also modify 'TransactionAwareTradable::bucketIndex' to place the new
staged txs in the (global) delayed payout tx bucket, so that they get
past the related transactions filter, used to speed up the pairing of
txs with tradables.
  • Loading branch information
stejbac committed Sep 6, 2024
1 parent e7b7e41 commit 95aaca9
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@

import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;

import java.util.stream.IntStream;

import javax.annotation.Nullable;

interface TransactionAwareTradable {
int TX_FILTER_SIZE = 64;
// Delayed payout, warning, redirect and claim txs all go into one bucket (as there shouldn't be too many of them).
int DELAYED_PAYOUT_TX_BUCKET_INDEX = TX_FILTER_SIZE - 1;

boolean isRelatedToTransaction(Transaction transaction);
Expand All @@ -38,16 +40,28 @@ interface TransactionAwareTradable {
IntStream getRelatedTransactionFilter();

static int bucketIndex(Transaction tx) {
return tx.getLockTime() == 0 ? bucketIndex(tx.getTxId()) : DELAYED_PAYOUT_TX_BUCKET_INDEX;
return tx.getInputs().size() == 1 && (tx.getLockTime() != 0 || isPossibleRedirectOrClaimTx(tx)) &&
isPossibleEscrowSpend(tx.getInput(0)) ? DELAYED_PAYOUT_TX_BUCKET_INDEX : bucketIndex(tx.getTxId());
}

static int bucketIndex(Sha256Hash hash) {
int i = hash.getBytes()[31] & 255;
return i % TX_FILTER_SIZE != DELAYED_PAYOUT_TX_BUCKET_INDEX ?
i % TX_FILTER_SIZE : i / TX_FILTER_SIZE;
return i % TX_FILTER_SIZE != DELAYED_PAYOUT_TX_BUCKET_INDEX ? i % TX_FILTER_SIZE : i / TX_FILTER_SIZE;
}

static int bucketIndex(@Nullable String txId) {
return txId != null ? bucketIndex(Sha256Hash.wrap(txId)) : -1;
}

static boolean isPossibleRedirectOrClaimTx(Transaction tx) {
return tx.getInput(0).getWitness().getPushCount() == 5 || tx.hasRelativeLockTime();
}

static boolean isPossibleEscrowSpend(TransactionInput input) {
// The maximum ScriptSig length of a (canonically signed) P2PKH or P2SH-P2WH input is 107 bytes, whereas
// multisig P2SH will always be longer than that. P2PKH, P2SH-P2WPKH and P2WPKH have a witness push count less
// than 3, but all Segwit trade escrow spends have a witness push count of at least 3. So we catch all escrow
// spends this way, without too many false positives.
return input.getScriptBytes().length > 107 || input.getWitness().getPushCount() > 2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@
import javafx.collections.ObservableList;

import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.IntStream;

import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;

import static bisq.desktop.main.funds.transactions.TransactionAwareTradable.bucketIndex;
import static com.google.common.base.Preconditions.checkNotNull;

Expand Down Expand Up @@ -88,28 +91,26 @@ public boolean isRelatedToTransaction(Transaction transaction) {
boolean isTakerOfferFeeTx = txId.equals(trade.getTakerFeeTxId());
boolean isOfferFeeTx = isOfferFeeTx(txId);
boolean isDepositTx = isDepositTx(txId);
boolean isPayoutTx = isPayoutTx(txId);
boolean isPayoutTx = isPayoutTx(trade, txId);
boolean isDisputedPayoutTx = isDisputedPayoutTx(txId);
boolean isDelayedPayoutTx = transaction.getLockTime() != 0 && isDelayedPayoutTx(txId);
boolean isDelayedPayoutOrWarningTx = isDelayedPayoutOrWarningTx(transaction, txId);
boolean isRedirectOrClaimTx = isRedirectOrClaimTx(transaction, txId);
boolean isRefundPayoutTx = isRefundPayoutTx(trade, txId);
tradeRelated = isTakerOfferFeeTx ||
isOfferFeeTx ||
isDepositTx ||
isPayoutTx ||
isDisputedPayoutTx ||
isDelayedPayoutTx ||
isDelayedPayoutOrWarningTx ||
isRedirectOrClaimTx ||
isRefundPayoutTx;
}
boolean isBsqSwapTrade = isBsqSwapTrade(txId);

return tradeRelated || isBsqSwapTrade;
}

private boolean isPayoutTx(String txId) {
if (isBsqSwapTrade())
return false;

Trade trade = (Trade) tradeModel;
private boolean isPayoutTx(Trade trade, String txId) {
return txId.equals(trade.getPayoutTxId());
}

Expand All @@ -122,17 +123,11 @@ private boolean isDepositTx(String txId) {
}

private boolean isOfferFeeTx(String txId) {
if (isBsqSwapTrade())
return false;

Offer offer = tradeModel.getOffer();
return offer != null && txId.equals(offer.getOfferFeePaymentTxId());
}

private boolean isDisputedPayoutTx(String txId) {
if (isBsqSwapTrade())
return false;

String delegateId = tradeModel.getId();
ObservableList<Dispute> disputes = arbitrationManager.getDisputesAsObservableList();

Expand All @@ -151,37 +146,58 @@ private boolean isDisputedPayoutTx(String txId) {
}

boolean isDelayedPayoutTx(String txId) {
if (isBsqSwapTrade())
return false;
return isDelayedPayoutOrWarningTx(txId) && !((Trade) tradeModel).hasV5Protocol();
}

Transaction transaction = btcWalletService.getTransaction(txId);
if (transaction == null)
return false;
private boolean isWarningTx(String txId) {
return isDelayedPayoutOrWarningTx(txId) && ((Trade) tradeModel).hasV5Protocol();
}

if (transaction.getLockTime() == 0)
private boolean isDelayedPayoutOrWarningTx(String txId) {
if (isBsqSwapTrade()) {
return false;
}
Transaction transaction = btcWalletService.getTransaction(txId);
return transaction != null && isDelayedPayoutOrWarningTx(transaction, null);
}

if (transaction.getInputs() == null || transaction.getInputs().size() != 1)
private boolean isDelayedPayoutOrWarningTx(Transaction transaction, @Nullable String txId) {
if (transaction.getLockTime() == 0 || transaction.getInputs().size() != 1) {
return false;
}
if (!TransactionAwareTradable.isPossibleEscrowSpend(transaction.getInput(0))) {
return false;
}
return firstParent(this::isDepositTx, transaction, txId);
}

return transaction.getInputs().stream()
.anyMatch(input -> {
TransactionOutput connectedOutput = input.getConnectedOutput();
if (connectedOutput == null) {
return false;
}
Transaction parentTransaction = connectedOutput.getParentTransaction();
if (parentTransaction == null) {
return false;
}
return isDepositTx(parentTransaction.getTxId().toString());
});
private boolean isRedirectOrClaimTx(Transaction transaction, @Nullable String txId) {
if (transaction.getInputs().size() != 1) {
return false;
}
if (!TransactionAwareTradable.isPossibleRedirectOrClaimTx(transaction)) {
return false;
}
return firstParent(this::isWarningTx, transaction, txId);
}

private boolean isRefundPayoutTx(Trade trade, String txId) {
if (isBsqSwapTrade())
private boolean firstParent(Predicate<String> parentPredicate, Transaction transaction, @Nullable String txId) {
Transaction walletTransaction = txId != null ? btcWalletService.getTransaction(txId) : transaction;
if (walletTransaction == null) {
return false;
}
TransactionOutput connectedOutput = walletTransaction.getInput(0).getConnectedOutput();
if (connectedOutput == null) {
return false;
}
Transaction parentTransaction = connectedOutput.getParentTransaction();
if (parentTransaction == null) {
return false;
}
return parentPredicate.test(parentTransaction.getTxId().toString());
}

private boolean isRefundPayoutTx(Trade trade, String txId) {
String tradeId = tradeModel.getId();
boolean isAnyDisputeRelatedToThis = refundManager.getDisputedTradeIds().contains(tradeId);

Expand Down

0 comments on commit 95aaca9

Please sign in to comment.