Skip to content

Commit 95aaca9

Browse files
committed
Ensure v5 staged txs are linked to trade in Transactions view
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.
1 parent e7b7e41 commit 95aaca9

File tree

2 files changed

+68
-38
lines changed

2 files changed

+68
-38
lines changed

desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradable.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121

2222
import org.bitcoinj.core.Sha256Hash;
2323
import org.bitcoinj.core.Transaction;
24+
import org.bitcoinj.core.TransactionInput;
2425

2526
import java.util.stream.IntStream;
2627

2728
import javax.annotation.Nullable;
2829

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

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

4042
static int bucketIndex(Transaction tx) {
41-
return tx.getLockTime() == 0 ? bucketIndex(tx.getTxId()) : DELAYED_PAYOUT_TX_BUCKET_INDEX;
43+
return tx.getInputs().size() == 1 && (tx.getLockTime() != 0 || isPossibleRedirectOrClaimTx(tx)) &&
44+
isPossibleEscrowSpend(tx.getInput(0)) ? DELAYED_PAYOUT_TX_BUCKET_INDEX : bucketIndex(tx.getTxId());
4245
}
4346

4447
static int bucketIndex(Sha256Hash hash) {
4548
int i = hash.getBytes()[31] & 255;
46-
return i % TX_FILTER_SIZE != DELAYED_PAYOUT_TX_BUCKET_INDEX ?
47-
i % TX_FILTER_SIZE : i / TX_FILTER_SIZE;
49+
return i % TX_FILTER_SIZE != DELAYED_PAYOUT_TX_BUCKET_INDEX ? i % TX_FILTER_SIZE : i / TX_FILTER_SIZE;
4850
}
4951

5052
static int bucketIndex(@Nullable String txId) {
5153
return txId != null ? bucketIndex(Sha256Hash.wrap(txId)) : -1;
5254
}
55+
56+
static boolean isPossibleRedirectOrClaimTx(Transaction tx) {
57+
return tx.getInput(0).getWitness().getPushCount() == 5 || tx.hasRelativeLockTime();
58+
}
59+
60+
static boolean isPossibleEscrowSpend(TransactionInput input) {
61+
// The maximum ScriptSig length of a (canonically signed) P2PKH or P2SH-P2WH input is 107 bytes, whereas
62+
// multisig P2SH will always be longer than that. P2PKH, P2SH-P2WPKH and P2WPKH have a witness push count less
63+
// than 3, but all Segwit trade escrow spends have a witness push count of at least 3. So we catch all escrow
64+
// spends this way, without too many false positives.
65+
return input.getScriptBytes().length > 107 || input.getWitness().getPushCount() > 2;
66+
}
5367
}

desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,13 @@
4141
import javafx.collections.ObservableList;
4242

4343
import java.util.Set;
44+
import java.util.function.Predicate;
4445
import java.util.stream.IntStream;
4546

4647
import lombok.extern.slf4j.Slf4j;
4748

49+
import javax.annotation.Nullable;
50+
4851
import static bisq.desktop.main.funds.transactions.TransactionAwareTradable.bucketIndex;
4952
import static com.google.common.base.Preconditions.checkNotNull;
5053

@@ -88,28 +91,26 @@ public boolean isRelatedToTransaction(Transaction transaction) {
8891
boolean isTakerOfferFeeTx = txId.equals(trade.getTakerFeeTxId());
8992
boolean isOfferFeeTx = isOfferFeeTx(txId);
9093
boolean isDepositTx = isDepositTx(txId);
91-
boolean isPayoutTx = isPayoutTx(txId);
94+
boolean isPayoutTx = isPayoutTx(trade, txId);
9295
boolean isDisputedPayoutTx = isDisputedPayoutTx(txId);
93-
boolean isDelayedPayoutTx = transaction.getLockTime() != 0 && isDelayedPayoutTx(txId);
96+
boolean isDelayedPayoutOrWarningTx = isDelayedPayoutOrWarningTx(transaction, txId);
97+
boolean isRedirectOrClaimTx = isRedirectOrClaimTx(transaction, txId);
9498
boolean isRefundPayoutTx = isRefundPayoutTx(trade, txId);
9599
tradeRelated = isTakerOfferFeeTx ||
96100
isOfferFeeTx ||
97101
isDepositTx ||
98102
isPayoutTx ||
99103
isDisputedPayoutTx ||
100-
isDelayedPayoutTx ||
104+
isDelayedPayoutOrWarningTx ||
105+
isRedirectOrClaimTx ||
101106
isRefundPayoutTx;
102107
}
103108
boolean isBsqSwapTrade = isBsqSwapTrade(txId);
104109

105110
return tradeRelated || isBsqSwapTrade;
106111
}
107112

108-
private boolean isPayoutTx(String txId) {
109-
if (isBsqSwapTrade())
110-
return false;
111-
112-
Trade trade = (Trade) tradeModel;
113+
private boolean isPayoutTx(Trade trade, String txId) {
113114
return txId.equals(trade.getPayoutTxId());
114115
}
115116

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

124125
private boolean isOfferFeeTx(String txId) {
125-
if (isBsqSwapTrade())
126-
return false;
127-
128126
Offer offer = tradeModel.getOffer();
129127
return offer != null && txId.equals(offer.getOfferFeePaymentTxId());
130128
}
131129

132130
private boolean isDisputedPayoutTx(String txId) {
133-
if (isBsqSwapTrade())
134-
return false;
135-
136131
String delegateId = tradeModel.getId();
137132
ObservableList<Dispute> disputes = arbitrationManager.getDisputesAsObservableList();
138133

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

153148
boolean isDelayedPayoutTx(String txId) {
154-
if (isBsqSwapTrade())
155-
return false;
149+
return isDelayedPayoutOrWarningTx(txId) && !((Trade) tradeModel).hasV5Protocol();
150+
}
156151

157-
Transaction transaction = btcWalletService.getTransaction(txId);
158-
if (transaction == null)
159-
return false;
152+
private boolean isWarningTx(String txId) {
153+
return isDelayedPayoutOrWarningTx(txId) && ((Trade) tradeModel).hasV5Protocol();
154+
}
160155

161-
if (transaction.getLockTime() == 0)
156+
private boolean isDelayedPayoutOrWarningTx(String txId) {
157+
if (isBsqSwapTrade()) {
162158
return false;
159+
}
160+
Transaction transaction = btcWalletService.getTransaction(txId);
161+
return transaction != null && isDelayedPayoutOrWarningTx(transaction, null);
162+
}
163163

164-
if (transaction.getInputs() == null || transaction.getInputs().size() != 1)
164+
private boolean isDelayedPayoutOrWarningTx(Transaction transaction, @Nullable String txId) {
165+
if (transaction.getLockTime() == 0 || transaction.getInputs().size() != 1) {
166+
return false;
167+
}
168+
if (!TransactionAwareTradable.isPossibleEscrowSpend(transaction.getInput(0))) {
165169
return false;
170+
}
171+
return firstParent(this::isDepositTx, transaction, txId);
172+
}
166173

167-
return transaction.getInputs().stream()
168-
.anyMatch(input -> {
169-
TransactionOutput connectedOutput = input.getConnectedOutput();
170-
if (connectedOutput == null) {
171-
return false;
172-
}
173-
Transaction parentTransaction = connectedOutput.getParentTransaction();
174-
if (parentTransaction == null) {
175-
return false;
176-
}
177-
return isDepositTx(parentTransaction.getTxId().toString());
178-
});
174+
private boolean isRedirectOrClaimTx(Transaction transaction, @Nullable String txId) {
175+
if (transaction.getInputs().size() != 1) {
176+
return false;
177+
}
178+
if (!TransactionAwareTradable.isPossibleRedirectOrClaimTx(transaction)) {
179+
return false;
180+
}
181+
return firstParent(this::isWarningTx, transaction, txId);
179182
}
180183

181-
private boolean isRefundPayoutTx(Trade trade, String txId) {
182-
if (isBsqSwapTrade())
184+
private boolean firstParent(Predicate<String> parentPredicate, Transaction transaction, @Nullable String txId) {
185+
Transaction walletTransaction = txId != null ? btcWalletService.getTransaction(txId) : transaction;
186+
if (walletTransaction == null) {
183187
return false;
188+
}
189+
TransactionOutput connectedOutput = walletTransaction.getInput(0).getConnectedOutput();
190+
if (connectedOutput == null) {
191+
return false;
192+
}
193+
Transaction parentTransaction = connectedOutput.getParentTransaction();
194+
if (parentTransaction == null) {
195+
return false;
196+
}
197+
return parentPredicate.test(parentTransaction.getTxId().toString());
198+
}
184199

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

0 commit comments

Comments
 (0)