bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.62k stars 2.08k forks source link

How to effectively calculate and update fees? #1867

Closed Nijinsha closed 1 year ago

Nijinsha commented 1 year ago

I could get the size with this and estimated s/vb from an API psbt.extractTransaction().virtualSize()

How to update the psbt to reduce the output value by the calculated fees?

trendy0413 commented 1 year ago

hi @Nijinsha Have you found a solution?

motorina0 commented 1 year ago

Changing the amount in any of the outputs will not alter the transaction size. So you can just subtract the fee from the output you desire.

trendy0413 commented 1 year ago

Thanks @Nijinsha I was considering creating a temporary Psbt object and recreating it after reducing the fee based on the estimated fee rate. Here's an example:

const psbt1 = new bitcoin.Psbt({ network: NETWORK });
psbt1.addInput({
  hash: txHash,
  index: 0,
  nonWitnessUtxo: Buffer.from(
    rawTransaction.hex,
    'hex'
  )
})
psbt1.addOutput({
  address: mainAddress,
  value: transactionAmount // use whole amount
});
const feeRate = ... // get feeRate from an API
const vSize = psbt1.extractTransaction().virtualSize();
const fee = feeRate * vSize;
const changedValue = transactionAmount - fee;
// Create real psbt
const psbt = new bitcoin.Psbt({ network: NETWORK });
psbt.addInput({
  hash: txHash,
  index: 0,
  nonWitnessUtxo: Buffer.from(
    rawTransaction.hex,
    'hex'
  )
})

psbt.addOutput({
  address: mainAddress,
  value: Math.floor(changedValue) // use changedValue here
});
// do the rest...

However, I think this might not be the best approach. Instead, can I use the setFee method directly on the Psbt object, like this?

// Add the fee to your PSBT object
psbt.setFee(fee);

Please let me know if this is a recommended approach or if there's a better way to do it.

And can I use nonWitnessUtxo in all cases like below?

psbt.addInput({
  hash: txHash,
  index: 0,
  nonWitnessUtxo: Buffer.from(
    rawTransaction.hex,
    'hex'
  )
})

I found this at https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions.spec.ts

// // If this input was segwit, instead of nonWitnessUtxo, you would add
      // // a witnessUtxo as follows. The scriptPubkey and the value only are needed.
      // witnessUtxo: {
      //   script: Buffer.from(
      //     '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac',
      //     'hex',
      //   ),
      //   value: 90000,
      // },

Thank you!

trendy0413 commented 1 year ago

Changing the amount in any of the outputs will not alter the transaction size. So you can just subtract the fee from the output you desire.

Thanks @motorina0 Could you provide me an example code?

junderw commented 1 year ago

............. there is no setFee method on the Psbt class...

There's no setFee function anywhere near this repo.

Please don't use Chat-GPT to answer issues...


One way to do it: Create a 0 fee tx and use that to measure.

// Imagine a function called
// function createPsbt(feeSats: number | null): [Psbt, number] {}
// This function will set a 0 fee if feeSats is null, and it will return the finished,
// fully signed, fully finalized Psbt along with the number of sats in the change output

const [psbt, changeAmount] = createPsbt(null);
const feeRate = 15; // sats per vByte (get from some fee rate API like mempool.space)
const vSize = psbt.extractTransaction().virtualSize(); // vBytes
const feeSats = feeRate * vSize;

// Minimum amount an output can send (it's actually closer to 546 sats, but depends on other factors)
const dustLimit = 600;

// Since we need to subtract the fee from changeAmount, and it needs at least 600 sats left over to be a valid output
// We run this check... in reality you might want to loop back, add another input, then try again.
if (changeAmount < feeSats + dustLimit) {
  throw new Error('Need to add another input so your change amount can accommodate more fees');
}

// Now it will lower the amount
const [psbt2, ] = createPsbt(feeSats);

Another way: Use knowledge of input and output types to calculate.

https://gist.github.com/junderw/b43af3253ea5865ed52cb51c200ac19c

trendy0413 commented 1 year ago

Thanks @junderw! I really appreciate your help. And can I use nonWitnessUtxo in all cases like below? I am not sure when I can use nonWitnessUtxo or witnessUtxo.

psbt.addInput({
  hash: txHash,
  index: 0,
  nonWitnessUtxo: Buffer.from(
    rawTransaction.hex,
    'hex'
  )
})
junderw commented 1 year ago

Check the examples.

witnessUtxo is for segwit, nonWitnessUtxo is for non-segwit.

Some wallets include both for each input.

masterEye-07 commented 1 year ago

@junderw I have 2 questions about the solution you shared above and will be very grateful to you if you can help me understand these

1- Is it necessary to set "dustLimit"? how it can be advantageous and what if we skip this?

2- if changeAmount < feeSats + dustLimit, we have to add another input to meet the transaction requirement, but adding another input means we have to calculate feeSats again by signing a 0 fee transaction with additional input, doesn't this let the signer straight into a loophole? as the new fee may require additional input and this loop cost us a hectic computation and signing cost.

masterEye-07 commented 1 year ago

Also, if Bitcoin fees are implicit, then how do the Bitcoin wallets show the transaction fee to the end user while performing the transaction? Referencing the auto-calculated fees in the screenshots attached in https://www.alfa.cash/guides/how-to-set-miners-fee-bitcoin-transaction-popular-crypto-wallets-blockchaininfo-electrum

junderw commented 1 year ago
  1. The dust limit is another topic all its own, but basically "the dust limit" is "the minimum amount allowed on an output." The reason why we subtract it is, we don't want our "change" output to be worth less than the dust limit, otherwise our transaction will not broadcast. When your change output is less than dust, there are two options, add another input (so your change is greater than dust), or just throw away the dust by adding it to the fee.
  2. One way to prevent this is to calculate the "fee cost of the input" and divide it by the amount of the input, and refuse to consider any input that is greater than some threshold. ie. 1 P2PKH input is 148 vBytes to add, and if the current fee rate is 15 sats/vByte, that means 1 P2PKH input costs 2220 sats. If you are considering adding an input worth 1000 sats, you will lose money by adding this input, so don't even consider it until the fee rate is 1 sat/vByte etc which will make the cost 148 sats... etc. etc. Usually a wallet will say "ok, since 2221 sat input will only "add" 1 sat to the transaction, we will ignore (for the current fee rate) all inputs worth less than 3x the fee it would add. so we need at least 6660 sats input before we consider it at all. However, this logic varies depending on wallet.

Fees and dust are highly complicated topics, and a lot of wallets try to simplify things wherever they can, (ie. dust calculation is actually pretty complicated, but "600 or more is definitely fine" is just guessing high just to be safe. iirc 99.999% of the time it's 564 and sometimes it's less, but people don't like trying to calculate it to save a couple of sats.

junderw commented 1 year ago

coin selection (deciding which utxo to add to the transaction as an input), dust processing, and fee calculation are 3 very complicated topics that mix with each other, and are hard to bake into a library...

If we offered an "easy" way to calculate it that cost you 10% extra fees for the rest of your lifetime, as soon as you found out, you might get very angry with us.

Coin selection also affects privacy of your wallet greatly, as well.

masterEye-07 commented 1 year ago

Thank you @junderw for such a nice explanation. One thing, as you said 1 P2PKH input is 148 vBytes, please correct me if I'm wrong that 1 P2WPKH (non-segwit) input is 41 vBytes?

masterEye-07 commented 1 year ago

I have 1 P2WPKH (non-segwit) input, 1 P2WPKH output and another 1 P2PKH output and my virtual Bytes of hex after signature are 144, which is only possible when input is 68 bytes P2WPKH output is 31 bytes P2PKH output is 34 bytes and extra 11 bytes (don't know why and how)

Please enlighten me with your thoughts on this

masterEye-07 commented 1 year ago

Also, @junderw Sir, Is it the best approach to use the highest value UTXO first and then add (if needed) the next UTXO/s (In descending order as per their values)?

junderw commented 1 year ago

There is no best approach.

Each approach has it's own trade offs.

That approach tends to create a lot of tiny dust outputs. The longer you use that strategy, the more dust outputs you have to ignore.

masterEye-07 commented 1 year ago

@junderw From the calculation you've mentioned with "fee cost of the input", why did you skip "output/s fee"? as it also will be paid through value in input/s. So in this regard, I assume the calculations will be: 1 P2PKH input = 148 vBytes 1 P2PKH output (receiver address) = 34 vBytes 1 P2PKH output (change address) = 34 vBytes if the current fee rate is 15 then we should consider UTXO to add worth 15(148+34+34) = 3240 satoshis, also, all inputs worth less than 3x the fee would be ignored so 3 15 (148+34+34) = 9720. for 2 inputs it'll be 2 9720, for 3 inputs, 3 9720. if it's all correct then the dust amount you've mentioned is 600, such dust UTXOs can never be considered even fee rate is 1 as 3 1 * (148+34+34) = 648 which is less than 600

Please let me know if my calculations are wrong, where I'm mistaken, and how can be tackled, Thank you