Use segwit tx sizes

This commit is contained in:
Oscar Guindzberg 2020-10-16 17:00:56 -03:00
parent 7a58bfbafa
commit 29f23fe50c
No known key found for this signature in database
GPG Key ID: 209796BF2E1D4F75
3 changed files with 53 additions and 38 deletions

View File

@ -41,9 +41,24 @@ import static com.google.common.base.Preconditions.checkArgument;
*/
@Slf4j
public class TxFeeEstimationService {
public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 260;
private static int DEPOSIT_TX_SIZE = 320;
private static int PAYOUT_TX_SIZE = 380;
// Size/vsize of typical trade txs
// Real txs size/vsize may vary in 1 or 2 bytes from the estimated values.
// Values calculated with https://gist.github.com/oscarguindzberg/3d1349cb65d9fd9af9de0feaa3fd27ac
// legacy fee tx with 1 input, maker/taker fee paid in btc size/vsize = 258
// legacy deposit tx without change size/vsize = 381
// legacy deposit tx with change size/vsize = 414
// legacy payout tx size/vsize = 337
// legacy delayed payout tx size/vsize = 302
// segwit fee tx with 1 input, maker/taker fee paid in btc vsize = 173
// segwit deposit tx without change vsize = 232
// segwit deposit tx with change vsize = 263
// segwit payout tx vsize = 169
// segwit delayed payout tx vsize = 139
public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 175;
private static int DEPOSIT_TX_SIZE = 233;
private static int PAYOUT_TX_SIZE = 169;
private static int BSQ_INPUT_INCREASE = 150;
private static int MAX_ITERATIONS = 10;
@ -87,14 +102,14 @@ public class TxFeeEstimationService {
BtcWalletService btcWalletService,
Preferences preferences) {
Coin txFeePerByte = feeService.getTxFeePerByte();
// We start with min taker fee size of 260
// We start with min taker fee size of 175
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
try {
estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, amount), estimatedTxSize, txFeePerByte, btcWalletService);
} catch (InsufficientMoneyException e) {
if (isTaker) {
// if we cannot do the estimation we use the payout tx size
estimatedTxSize = PAYOUT_TX_SIZE;
// if we cannot do the estimation we use the deposit tx size
estimatedTxSize = DEPOSIT_TX_SIZE;
}
log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
"if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize);
@ -109,13 +124,13 @@ public class TxFeeEstimationService {
Coin txFee;
int size;
if (isTaker) {
int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 320 bytes
// We use at least the size of the payout tx to not underpay at payout.
size = Math.max(PAYOUT_TX_SIZE, averageSize);
int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 233 bytes
// We use at least the size of the deposit tx to not underpay it.
size = Math.max(DEPOSIT_TX_SIZE, averageSize);
txFee = txFeePerByte.multiply(size);
log.info("Fee estimation resulted in a tx size of {} bytes.\n" +
"We use an average between the taker fee tx and the deposit tx (320 bytes) which results in {} bytes.\n" +
"The payout tx has 380 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" +
"We use an average between the taker fee tx and the deposit tx (233 bytes) which results in {} bytes.\n" +
"The deposit tx has 233 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" +
"The tx fee of {} Sat", estimatedTxSize, averageSize, size, txFee.value);
} else {
size = estimatedTxSize;
@ -130,7 +145,7 @@ public class TxFeeEstimationService {
FeeService feeService,
BtcWalletService btcWalletService) {
Coin txFeePerByte = feeService.getTxFeePerByte();
// We start with min taker fee size of 260
// We start with min taker fee size of 175
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
try {
estimatedTxSize = getEstimatedTxSize(List.of(amount), estimatedTxSize, txFeePerByte, btcWalletService);
@ -145,7 +160,7 @@ public class TxFeeEstimationService {
return new Tuple2<>(txFee, estimatedTxSize);
}
// We start with the initialEstimatedTxSize for a tx with 1 input (260) bytes and get from BitcoinJ a tx back which
// We start with the initialEstimatedTxSize for a tx with 1 input (175) bytes and get from BitcoinJ a tx back which
// contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on
// the assumption that we only need 1 input. Once we receive back the real tx size from the tx BitcoinJ has created
// with the required inputs we compare if the size is not more then 20% different to our assumed tx size. If we are inside

View File

@ -45,14 +45,14 @@ public class TxFeeEstimationServiceTest {
int realTxSize;
Coin txFee;
initialEstimatedTxSize = 260;
initialEstimatedTxSize = 175;
txFeePerByte = Coin.valueOf(10);
realTxSize = 260;
realTxSize = 175;
txFee = txFeePerByte.multiply(initialEstimatedTxSize);
when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize);
result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService);
assertEquals(260, result);
assertEquals(175, result);
}
// FIXME @Bernard could you have a look?
@ -67,16 +67,16 @@ public class TxFeeEstimationServiceTest {
int realTxSize;
Coin txFee;
initialEstimatedTxSize = 260;
initialEstimatedTxSize = 175;
txFeePerByte = Coin.valueOf(10);
realTxSize = 2600;
realTxSize = 1750;
txFee = txFeePerByte.multiply(initialEstimatedTxSize);
when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize);
// repeated calls to getEstimatedFeeTxSize do not work (returns 0 at second call in loop which cause test to fail)
result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService);
assertEquals(2600, result);
assertEquals(1750, result);
}
// FIXME @Bernard could you have a look?
@ -91,14 +91,14 @@ public class TxFeeEstimationServiceTest {
int realTxSize;
Coin txFee;
initialEstimatedTxSize = 2600;
initialEstimatedTxSize = 1750;
txFeePerByte = Coin.valueOf(10);
realTxSize = 260;
realTxSize = 175;
txFee = txFeePerByte.multiply(initialEstimatedTxSize);
when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize);
result = TxFeeEstimationService.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService);
assertEquals(260, result);
assertEquals(175, result);
}
@Test

View File

@ -109,8 +109,8 @@ class TakeOfferDataModel extends OfferDataModel {
private PaymentAccount paymentAccount;
private boolean isTabSelected;
Price tradePrice;
// 260 kb is size of typical trade fee tx with 1 input but trade tx (deposit and payout) are larger so we adjust to 320
private int feeTxSize = 320;
// Use an average of a typical trade fee tx with 1 input, deposit tx and payout tx.
private int feeTxSize = 192; // (175+233+169)/3
private boolean freezeFee;
private Coin txFeePerByteFromFeeService;
@ -213,13 +213,13 @@ class TakeOfferDataModel extends OfferDataModel {
// multiple batch-signed payout tx with different fees might be an option but RBF is not supported yet in BitcoinJ
// and batched txs would add more complexity to the trade protocol.
// A typical trade fee tx has about 260 bytes (if one input). The trade txs has about 336-414 bytes.
// We use 320 as a average value.
// A typical trade fee tx has about 175 bytes (if one input). The trade txs has about 169-263 bytes.
// We use 192 as average value.
// trade fee tx: 260 bytes (1 input)
// deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount)
// payout tx: 371 bytes
// disputed payout tx: 408 bytes
// trade fee tx: 175 bytes (1 input)
// deposit tx: 233 bytes (1 MS output+ OP_RETURN) - 263 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount)
// payout tx: 169 bytes
// disputed payout tx: 139 bytes
// Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values)
// But the "take offer" happens usually after that so we should have already the value from the estimation service.
@ -351,22 +351,22 @@ class TakeOfferDataModel extends OfferDataModel {
// there more deterministic.
// The trade fee tx can be in the worst case very large if there are many inputs so if we take that tx alone
// for the fee estimation we would overpay a lot.
// On the other side if we have the best case of a 1 input tx fee tx then it is only 260 bytes but the
// other 2 txs are larger (320 and 380 bytes) and would get a lower fee/byte as intended.
// On the other side if we have the best case of a 1 input tx fee tx then it is only 175 bytes but the
// other 2 txs are different (233 and 169 bytes) and may get a lower fee/byte as intended.
// We apply following model to not overpay too much but be on the safe side as well.
// We sum the taker fee tx and the deposit tx together as it can be assumed that both be in the same block and
// as they are dependent txs the miner will pick both if the fee in total is good enough.
// We make sure that the fee is sufficient to meet our intended fee/byte for the larger payout tx with 380 bytes.
// We make sure that the fee is sufficient to meet our intended fee/byte for the larger deposit tx with 233 bytes.
Tuple2<Coin, Integer> estimatedFeeAndTxSize = txFeeEstimationService.getEstimatedFeeAndTxSizeForTaker(fundsNeededForTrade,
getTakerFee());
txFeeFromFeeService = estimatedFeeAndTxSize.first;
feeTxSize = estimatedFeeAndTxSize.second;
} else {
feeTxSize = 380;
feeTxSize = 233;
txFeeFromFeeService = txFeePerByteFromFeeService.multiply(feeTxSize);
log.info("We cannot do the fee estimation because there are no funds in the wallet.\nThis is expected " +
"if the user has not funded their wallet yet.\n" +
"In that case we use an estimated tx size of 380 bytes.\n" +
"In that case we use an estimated tx size of 233 bytes.\n" +
"txFee based on estimated size of {} bytes. feeTxSize = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)",
feeTxSize, feeTxSize, txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte());
}
@ -531,7 +531,7 @@ class TakeOfferDataModel extends OfferDataModel {
// With that we avoid that we overpay in case that the trade fee has many inputs and we would apply that fee for the
// other 2 txs as well. We still might overpay a bit for the payout tx.
private int getAverageSize(int txSize) {
return (txSize + 320) / 2;
return (txSize + 233) / 2;
}
private Coin getTxFeeBySize(int sizeInBytes) {
@ -606,7 +606,7 @@ class TakeOfferDataModel extends OfferDataModel {
// Unfortunately we cannot change that to the correct fees as it would break backward compatibility
// We still might find a way with offer version or app version checks so lets keep that commented out
// code as that shows how it should be.
return txFeeFromFeeService; //feeService.getTxFee(320);
return txFeeFromFeeService; //feeService.getTxFee(233);
}
private Coin getTxFeeForPayoutTx() {
@ -614,7 +614,7 @@ class TakeOfferDataModel extends OfferDataModel {
// Unfortunately we cannot change that to the correct fees as it would break backward compatibility
// We still might find a way with offer version or app version checks so lets keep that commented out
// code as that shows how it should be.
return txFeeFromFeeService; //feeService.getTxFee(380);
return txFeeFromFeeService; //feeService.getTxFee(169);
}
public AddressEntry getAddressEntry() {