Skip to content

Commit d184bed

Browse files
committed
Fix warning/redirect/claim tx fee calculations
Also make claim delay 5 blocks on regtest, and add missing TradeMessage constructors to provide a default argument for the P2P message version, for consistency with the other trade messages.
1 parent 854bfd8 commit d184bed

14 files changed

+112
-48
lines changed

core/src/main/java/bisq/core/dao/burningman/DelayedPayoutTxReceiverService.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ public static boolean isProposal412Activated() {
8888
// spike when opening arbitration.
8989
private static final long DPT_MIN_TX_FEE_RATE = 10;
9090

91+
// The DPT weight (= 4 * size) without any outputs. This should actually be higher (426 wu, once the
92+
// witness data is taken into account), but must be kept at this value for compatibility with the peer.
93+
private static final long DPT_MIN_WEIGHT = 204;
94+
9195
private final DaoStateService daoStateService;
9296
private final BurningManService burningManService;
9397
private int currentChainHeight;
@@ -131,12 +135,20 @@ public int getBurningManSelectionHeight() {
131135
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
132136
long inputAmount,
133137
long tradeTxFee) {
134-
return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, isBugfix6699Activated());
138+
return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, DPT_MIN_WEIGHT, isBugfix6699Activated());
139+
}
140+
141+
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
142+
long inputAmount,
143+
long tradeTxFee,
144+
boolean isBugfix6699Activated) {
145+
return getReceivers(burningManSelectionHeight, inputAmount, tradeTxFee, DPT_MIN_WEIGHT, isBugfix6699Activated);
135146
}
136147

137148
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
138149
long inputAmount,
139150
long tradeTxFee,
151+
long minTxWeight,
140152
boolean isBugfix6699Activated) {
141153
checkArgument(burningManSelectionHeight >= MIN_SNAPSHOT_HEIGHT, "Selection height must be >= " + MIN_SNAPSHOT_HEIGHT);
142154
Collection<BurningManCandidate> burningManCandidates = burningManService.getActiveBurningManCandidates(burningManSelectionHeight);
@@ -157,13 +169,15 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
157169

158170
if (burningManCandidates.isEmpty()) {
159171
// If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM
160-
long spendableAmount = getSpendableAmount(1, inputAmount, txFeePerVbyte);
161-
return List.of(new Tuple2<>(spendableAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight)));
172+
long spendableAmount = getSpendableAmount(1, inputAmount, txFeePerVbyte, minTxWeight);
173+
return spendableAmount > DPT_MIN_REMAINDER_TO_LEGACY_BM
174+
? List.of(new Tuple2<>(spendableAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight)))
175+
: List.of();
162176
}
163177

164-
long spendableAmount = getSpendableAmount(burningManCandidates.size(), inputAmount, txFeePerVbyte);
165-
// We only use outputs > 1000 sat or at least 2 times the cost for the output (32 bytes).
166-
// If we remove outputs it will be spent as miner fee.
178+
long spendableAmount = getSpendableAmount(burningManCandidates.size(), inputAmount, txFeePerVbyte, minTxWeight);
179+
// We only use outputs >= 1000 sat or at least 2 times the cost for the output (32 bytes).
180+
// If we remove outputs it will be distributed to the remaining receivers.
167181
long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2);
168182
// Sanity check that max share of a non-legacy BM is 20% over MAX_BURN_SHARE (taking into account potential increase due adjustment)
169183
long maxOutputAmount = Math.round(spendableAmount * (BurningManService.MAX_BURN_SHARE * 1.2));
@@ -178,6 +192,9 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
178192
})
179193
.sum();
180194

195+
// FIXME: The small outputs should be filtered out before adjustment, not afterwards. Otherwise, outputs of
196+
// amount just under 1000 sats or 64 * fee-rate could get erroneously included and lead to significant
197+
// underpaying of the DPT (by perhaps around 5-10% per erroneously included output).
181198
List<Tuple2<Long, String>> receivers = burningManCandidates.stream()
182199
.filter(candidate -> candidate.getReceiverAddress(isBugfix6699Activated).isPresent())
183200
.map(candidate -> {
@@ -191,6 +208,8 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
191208
.thenComparing(tuple -> tuple.second))
192209
.collect(Collectors.toList());
193210
long totalOutputValue = receivers.stream().mapToLong(e -> e.first).sum();
211+
// FIXME: The balance given to the LBM burning man needs to take into account the tx size increase due to
212+
// the extra output, to avoid underpaying the tx fee.
194213
if (totalOutputValue < spendableAmount) {
195214
long available = spendableAmount - totalOutputValue;
196215
// If the available is larger than DPT_MIN_REMAINDER_TO_LEGACY_BM we send it to legacy BM
@@ -202,14 +221,19 @@ public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
202221
return receivers;
203222
}
204223

205-
private static long getSpendableAmount(int numOutputs, long inputAmount, long txFeePerVbyte) {
224+
// TODO: For the v5 trade protocol, should we compute a more precise fee estimate taking into account the individual
225+
// receiver output script types? (P2SH costs 32 bytes per output, P2WPKH costs 31, etc.) This has the advantage of
226+
// avoiding the slight overestimate at present (32 vs 31) and could allow future support for P2TR receiver outputs,
227+
// which cost 43 bytes each. (Note that bitcoinj has already added partial taproot support upstream, and recognises
228+
// P2TR addresses & ScriptPubKey types.)
229+
private static long getSpendableAmount(int numOutputs, long inputAmount, long txFeePerVbyte, long minTxWeight) {
206230
// Output size: 32 bytes
207231
// Tx size without outputs: 51 bytes
208-
int txSize = 51 + numOutputs * 32; // Min value: txSize=83
209-
long minerFee = txFeePerVbyte * txSize; // Min value: minerFee=830
232+
long txWeight = minTxWeight + numOutputs * 128L; // Min value: txWeight=332 (for DPT with 1 output)
233+
long minerFee = (txFeePerVbyte * txWeight + 3) / 4; // Min value: minerFee=830
210234
// We need to make sure we have at least 1000 sat as defined in TradeWalletService
211235
minerFee = Math.max(TradeWalletService.MIN_DELAYED_PAYOUT_TX_FEE.value, minerFee);
212-
return inputAmount - minerFee;
236+
return Math.max(inputAmount - minerFee, 0);
213237
}
214238

215239
private static int getSnapshotHeight(int genesisHeight, int height, int grid) {

core/src/main/java/bisq/core/trade/protocol/bisq_v5/messages/PreparedTxBuyerSignaturesMessage.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import bisq.network.p2p.DirectMessage;
2323
import bisq.network.p2p.NodeAddress;
2424

25+
import bisq.common.app.Version;
2526
import bisq.common.util.Utilities;
2627

2728
import protobuf.NetworkEnvelope;
@@ -41,15 +42,37 @@ public class PreparedTxBuyerSignaturesMessage extends TradeMessage implements Di
4142
private final byte[] buyersRedirectTxBuyerSignature;
4243
private final byte[] sellersRedirectTxBuyerSignature;
4344

44-
public PreparedTxBuyerSignaturesMessage(int messageVersion,
45-
String tradeId,
45+
public PreparedTxBuyerSignaturesMessage(String tradeId,
4646
String uid,
4747
NodeAddress senderNodeAddress,
4848
byte[] depositTxWithBuyerWitnesses,
4949
byte[] buyersWarningTxBuyerSignature,
5050
byte[] sellersWarningTxBuyerSignature,
5151
byte[] buyersRedirectTxBuyerSignature,
5252
byte[] sellersRedirectTxBuyerSignature) {
53+
this(Version.getP2PMessageVersion(), tradeId, uid,
54+
senderNodeAddress,
55+
depositTxWithBuyerWitnesses,
56+
buyersWarningTxBuyerSignature,
57+
sellersWarningTxBuyerSignature,
58+
buyersRedirectTxBuyerSignature,
59+
sellersRedirectTxBuyerSignature);
60+
}
61+
62+
63+
///////////////////////////////////////////////////////////////////////////////////////////
64+
// PROTO BUFFER
65+
///////////////////////////////////////////////////////////////////////////////////////////
66+
67+
private PreparedTxBuyerSignaturesMessage(int messageVersion,
68+
String tradeId,
69+
String uid,
70+
NodeAddress senderNodeAddress,
71+
byte[] depositTxWithBuyerWitnesses,
72+
byte[] buyersWarningTxBuyerSignature,
73+
byte[] sellersWarningTxBuyerSignature,
74+
byte[] buyersRedirectTxBuyerSignature,
75+
byte[] sellersRedirectTxBuyerSignature) {
5376
super(messageVersion, tradeId, uid);
5477
this.senderNodeAddress = senderNodeAddress;
5578
this.depositTxWithBuyerWitnesses = depositTxWithBuyerWitnesses;
@@ -59,11 +82,6 @@ public PreparedTxBuyerSignaturesMessage(int messageVersion,
5982
this.sellersRedirectTxBuyerSignature = sellersRedirectTxBuyerSignature;
6083
}
6184

62-
63-
///////////////////////////////////////////////////////////////////////////////////////////
64-
// PROTO BUFFER
65-
///////////////////////////////////////////////////////////////////////////////////////////
66-
6785
@Override
6886
public NetworkEnvelope toProtoNetworkEnvelope() {
6987
protobuf.PreparedTxBuyerSignaturesMessage.Builder builder = protobuf.PreparedTxBuyerSignaturesMessage.newBuilder()

core/src/main/java/bisq/core/trade/protocol/bisq_v5/messages/PreparedTxBuyerSignaturesRequest.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import bisq.network.p2p.DirectMessage;
2323
import bisq.network.p2p.NodeAddress;
2424

25+
import bisq.common.app.Version;
2526
import bisq.common.util.Utilities;
2627

2728
import protobuf.NetworkEnvelope;
@@ -40,14 +41,34 @@ public class PreparedTxBuyerSignaturesRequest extends TradeMessage implements Di
4041
private final byte[] buyersRedirectTxSellerSignature;
4142
private final byte[] sellersRedirectTxSellerSignature;
4243

43-
public PreparedTxBuyerSignaturesRequest(int messageVersion,
44-
String tradeId,
44+
public PreparedTxBuyerSignaturesRequest(String tradeId,
4545
String uid,
4646
NodeAddress senderNodeAddress,
4747
byte[] buyersWarningTxSellerSignature,
4848
byte[] sellersWarningTxSellerSignature,
4949
byte[] buyersRedirectTxSellerSignature,
5050
byte[] sellersRedirectTxSellerSignature) {
51+
this(Version.getP2PMessageVersion(), tradeId, uid,
52+
senderNodeAddress,
53+
buyersWarningTxSellerSignature,
54+
sellersWarningTxSellerSignature,
55+
buyersRedirectTxSellerSignature,
56+
sellersRedirectTxSellerSignature);
57+
}
58+
59+
60+
///////////////////////////////////////////////////////////////////////////////////////////
61+
// PROTO BUFFER
62+
///////////////////////////////////////////////////////////////////////////////////////////
63+
64+
private PreparedTxBuyerSignaturesRequest(int messageVersion,
65+
String tradeId,
66+
String uid,
67+
NodeAddress senderNodeAddress,
68+
byte[] buyersWarningTxSellerSignature,
69+
byte[] sellersWarningTxSellerSignature,
70+
byte[] buyersRedirectTxSellerSignature,
71+
byte[] sellersRedirectTxSellerSignature) {
5172
super(messageVersion, tradeId, uid);
5273
this.senderNodeAddress = senderNodeAddress;
5374
this.buyersWarningTxSellerSignature = buyersWarningTxSellerSignature;
@@ -56,11 +77,6 @@ public PreparedTxBuyerSignaturesRequest(int messageVersion,
5677
this.sellersRedirectTxSellerSignature = sellersRedirectTxSellerSignature;
5778
}
5879

59-
60-
///////////////////////////////////////////////////////////////////////////////////////////
61-
// PROTO BUFFER
62-
///////////////////////////////////////////////////////////////////////////////////////////
63-
6480
@Override
6581
public NetworkEnvelope toProtoNetworkEnvelope() {
6682
protobuf.PreparedTxBuyerSignaturesRequest.Builder builder = protobuf.PreparedTxBuyerSignaturesRequest.newBuilder()

core/src/main/java/bisq/core/trade/protocol/bisq_v5/model/StagedPayoutTxParameters.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,34 @@
1717

1818
package bisq.core.trade.protocol.bisq_v5.model;
1919

20+
import bisq.common.config.Config;
21+
2022
public class StagedPayoutTxParameters {
2123
// 10 days
22-
public static final long CLAIM_DELAY = 144 * 10;
24+
private static final long CLAIM_DELAY = 144 * 10;
2325
//todo find what is min value (we filter dust values in the wallet, so better not go that low)
2426
public static final long WARNING_TX_FEE_BUMP_OUTPUT_VALUE = 2000;
2527
public static final long REDIRECT_TX_FEE_BUMP_OUTPUT_VALUE = 2000;
2628

27-
// todo find out size
28-
private static final long WARNING_TX_EXPECTED_SIZE = 1000; // todo find out size
29-
private static final long CLAIM_TX_EXPECTED_SIZE = 1000; // todo find out size
30-
// Min. fee rate for DPT. If fee rate used at take offer time was higher we use that.
31-
// We prefer a rather high fee rate to not risk that the DPT gets stuck if required fee rate would
29+
private static final long WARNING_TX_EXPECTED_WEIGHT = 722; // 125 tx bytes, 220-222 witness bytes
30+
private static final long CLAIM_TX_EXPECTED_WEIGHT = 520; // 82 tx bytes, 191-192 witness bytes
31+
public static final long REDIRECT_TX_MIN_WEIGHT = 595; // 82 tx bytes, 265-267 witness bytes
32+
33+
// Min. fee rate for staged payout txs. If fee rate used at take offer time was higher we use that.
34+
// We prefer a rather high fee rate to not risk that the tx gets stuck if required fee rate would
3235
// spike when opening arbitration.
3336
private static final long MIN_TX_FEE_RATE = 10;
3437

38+
public static long getClaimDelay() {
39+
return Config.baseCurrencyNetwork().isRegtest() ? 5 : CLAIM_DELAY;
40+
}
41+
3542
public static long getWarningTxMiningFee(long depositTxFeeRate) {
36-
return getFeePerVByte(depositTxFeeRate) * StagedPayoutTxParameters.WARNING_TX_EXPECTED_SIZE;
43+
return (getFeePerVByte(depositTxFeeRate) * StagedPayoutTxParameters.WARNING_TX_EXPECTED_WEIGHT + 3) / 4;
3744
}
3845

39-
public static long getClaimTxMiningFee(long depositTxFeeRate) {
40-
return getFeePerVByte(depositTxFeeRate) * StagedPayoutTxParameters.CLAIM_TX_EXPECTED_SIZE;
46+
public static long getClaimTxMiningFee(long txFeePerVByte) {
47+
return (txFeePerVByte * StagedPayoutTxParameters.CLAIM_TX_EXPECTED_WEIGHT + 3) / 4;
4148
}
4249

4350
private static long getFeePerVByte(long depositTxFeeRate) {

core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/CreateRedirectTxs.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ protected void run() {
5858
"Different warningTx output amounts. Ours: {}; Peer's: {}", warningTxOutput.getValue().value, inputAmount);
5959

6060
long depositTxFee = trade.getTradeTxFeeAsLong(); // Used for fee rate calculation inside getDelayedPayoutTxReceiverService
61-
long inputAmountMinusFeeForFeeBumpOutput = inputAmount - 32 * depositTxFee;
61+
long inputAmountMinusFeeBumpAmount = inputAmount - StagedPayoutTxParameters.REDIRECT_TX_FEE_BUMP_OUTPUT_VALUE;
6262
int selectionHeight = processModel.getBurningManSelectionHeight();
6363
List<Tuple2<Long, String>> burningMen = processModel.getDelayedPayoutTxReceiverService().getReceivers(
6464
selectionHeight,
65-
inputAmountMinusFeeForFeeBumpOutput,
66-
depositTxFee);
65+
inputAmountMinusFeeBumpAmount,
66+
depositTxFee,
67+
StagedPayoutTxParameters.REDIRECT_TX_MIN_WEIGHT,
68+
true);
6769

6870
log.info("Create redirectionTxs using selectionHeight {} and receivers {}", selectionHeight, burningMen);
6971

core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/CreateWarningTxs.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected void run() {
5454
long lockTime = trade.getLockTime();
5555
byte[] buyerPubKey = amBuyer ? processModel.getMyMultiSigPubKey() : tradingPeer.getMultiSigPubKey();
5656
byte[] sellerPubKey = amBuyer ? tradingPeer.getMultiSigPubKey() : processModel.getMyMultiSigPubKey();
57-
long claimDelay = StagedPayoutTxParameters.CLAIM_DELAY; // FIXME: Make sure this is a low value off mainnet
57+
long claimDelay = StagedPayoutTxParameters.getClaimDelay();
5858
long miningFee = StagedPayoutTxParameters.getWarningTxMiningFee(trade.getDepositTxFeeRate());
5959

6060
// Create our warning tx.

core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/FinalizeRedirectTxs.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected void run() {
4949
boolean amBuyer = trade instanceof BuyerTrade;
5050
byte[] buyerPubKey = amBuyer ? processModel.getMyMultiSigPubKey() : tradingPeer.getMultiSigPubKey();
5151
byte[] sellerPubKey = amBuyer ? tradingPeer.getMultiSigPubKey() : processModel.getMyMultiSigPubKey();
52-
long claimDelay = StagedPayoutTxParameters.CLAIM_DELAY; // FIXME: Make sure this is a low value off mainnet
52+
long claimDelay = StagedPayoutTxParameters.getClaimDelay();
5353

5454
// Finalize our redirect tx.
5555
TransactionOutput peersWarningTxOutput = tradingPeer.getWarningTx().getOutput(0);

core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/arbitration/CreateSignedClaimTx.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ protected void run() {
5656
TransactionOutput myWarningTxOutput = processModel.getWarningTx().getOutput(0);
5757
AddressEntry addressEntry = processModel.getBtcWalletService().getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT);
5858
Address payoutAddress = addressEntry.getAddress();
59-
// long miningFee = StagedPayoutTxParameters.getClaimTxMiningFee(trade.getDepositTxFeeRate());
6059
long miningFee = StagedPayoutTxParameters.getClaimTxMiningFee(feeService.getTxFeePerVbyte().value);
61-
long claimDelay = StagedPayoutTxParameters.CLAIM_DELAY; // FIXME: Make sure this is a low value off mainnet
60+
long claimDelay = StagedPayoutTxParameters.getClaimDelay();
6261
byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey();
6362
byte[] peersMultiSigPubKey = tradingPeer.getMultiSigPubKey();
6463
DeterministicKey myMultiSigKeyPair = btcWalletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey);

core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/buyer/BuyerSendsPreparedTxBuyerSignaturesMessage.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import bisq.network.p2p.NodeAddress;
2525
import bisq.network.p2p.SendDirectMessageListener;
2626

27-
import bisq.common.app.Version;
2827
import bisq.common.taskrunner.TaskRunner;
2928

3029
import java.util.UUID;
@@ -50,7 +49,7 @@ protected void run() {
5049
byte[] buyersRedirectTxBuyerSignature = processModel.getRedirectTxBuyerSignature();
5150
byte[] sellersRedirectTxBuyerSignature = processModel.getTradePeer().getRedirectTxBuyerSignature();
5251

53-
PreparedTxBuyerSignaturesMessage message = new PreparedTxBuyerSignaturesMessage(Version.getP2PMessageVersion(), // TODO: Add extra constructor
52+
PreparedTxBuyerSignaturesMessage message = new PreparedTxBuyerSignaturesMessage(
5453
processModel.getOfferId(),
5554
UUID.randomUUID().toString(),
5655
processModel.getMyNodeAddress(),

0 commit comments

Comments
 (0)