Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fee estimation #118

Merged
merged 11 commits into from
Nov 28, 2022
Merged

feat: add fee estimation #118

merged 11 commits into from
Nov 28, 2022

Conversation

gitferry
Copy link
Collaborator

This PR is to add fee estimation. In particular, this PR changes the way it gets tx fee to via EstimateFee RPC call rather than via config.

The EstimateFee RPC call returns tx fee rate (BTC/byte). To estimate tx fee, this PR adds getTxSize to calculate tx size with simulated tx construction without adding tx fee.

Note that EstimateFee is deprecated, so it should be replaced by EstimateSmartFee after btcd upgrade to v0.23. See #117.

Copy link
Member

@vitsalis vitsalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Some comments

func (c *Client) GetTxFee(txSize uint64) uint64 {
defaultFee := uint64(c.Cfg.TxFeeDefault.ToUnit(btcutil.AmountSatoshi))
if txSize == 0 {
return defaultFee
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a panic condition? Someone calling with an empty transaction size.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be called when finding utxos that have sufficient funds

}
feeRateAmount, err := btcutil.NewAmount(feeRate)
if err != nil {
return defaultFee
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this having an error mean that EstimateFee returned something that is not a proper fee rate? Maybe we should panic here as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this err would mean feeRate is very wrong. We should panic here. Thanks for pointing out!

return defaultFee
}
fee := uint64(feeRateAmount.ToUnit(btcutil.AmountSatoshi)) * txSize
if fee > uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe create a helper method i.e. feeApplicable(fee, c.Cfg.TxFeeMax, c.Cfg.TxFeeMin) for this check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Thanks!

}
fee := uint64(feeRateAmount.ToUnit(btcutil.AmountSatoshi)) * txSize
if fee > uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) ||
fee < uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you use c.Cfg.TxFeeMax for both checks.

@@ -17,7 +17,10 @@ type BTCConfig struct {
WalletName string `mapstructure:"wallet-name"`
WalletCAFile string `mapstructure:"wallet-ca-file"`
WalletLockTime int64 `mapstructure:"wallet-lock-time"` // time duration in which the wallet remains unlocked, in seconds
TxFee btcutil.Amount `mapstructure:"tx-fee"` // BTC tx fee, in BTC
TxFeeDefault btcutil.Amount `mapstructure:"tx-fee-default"` // default BTC tx fee, in BTC
TxFeeMin btcutil.Amount `mapstructure:"tx-fee-min"` // minimum tx fee, in BTC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we want a minimum transaction fee? Personally I would always set this to 0

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having 0 might cause an error saying the tx has insufficient priority. I'm not sure of the exact min fee we should set tho.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove this attribute alltogether. Why would someone want to set the minimum fee into anything other than the minimum value possible (e.g. 1)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove this attribute alltogether. Why would someone want to set the minimum fee into anything other than the minimum value possible (e.g. 1)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The estimation might return us an extremely low tx fee which is not reasonable. We set a reasonable tx fee here to prevent this case. It could be 1, 10, or 100.

TxFeeDefault btcutil.Amount `mapstructure:"tx-fee-default"` // default BTC tx fee, in BTC
TxFeeMin btcutil.Amount `mapstructure:"tx-fee-min"` // minimum tx fee, in BTC
TxFeeMax btcutil.Amount `mapstructure:"tx-fee-max"` // maximum tx fee, in BTC
TargetBlockNum int64 `mapstructure:"target-block-num"` // for tx fee estimation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add in the comment that this specifies a parameter into how soon we want the transaction to be included (e.g. 1 specifying next block).

@@ -195,7 +197,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) {
}

// TODO: consider dust, reference: https://www.oreilly.com/library/view/mastering-bitcoin/9781491902639/ch08.html#tx_verification
if uint64(amount.ToUnit(btcutil.AmountSatoshi)) < rl.GetTxFee()*2 {
if uint64(amount.ToUnit(btcutil.AmountSatoshi)) < rl.GetTxFee(0)*2 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will return the default fee, without considering the transaction size.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is to ensure that we have sufficient balance in this utxo as we are considering max fee as default

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then why don't we use the max fee config directly? This adds further complexity to the GetTxFee function by adding a special case with a 0 input which is not clear.

@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) {
func (rl *Relayer) buildTxWithData(
utxo *types.UTXO,
data []byte,
forTxSize bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add this parameter? We should always try to estimate the fee.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used when calculating tx size. See here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we always calculate the transaction size?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see what's happening. I suggest that we don't use this function again with the dubious flag, but just create a copy of the transaction we have already built and add a dummy change output. Then calculate the transaction size based on that. A cyclic dependency between the functions based on a flag is going to come bite us in the future (I had to spend some time reading the code to understand what was happening).

}
txFee := rl.GetTxFee(txSize)
change -= txFee
log.Logger.Debugf("balance of input: %v satoshi, tx fee: %v satoshi, output value: %v",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log appeared before but now it will only appear if forTxSize is set to false.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. The log shouldn't appear when calculating tx size

change -= txFee
log.Logger.Debugf("balance of input: %v satoshi, tx fee: %v satoshi, output value: %v",
int64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)), txFee, change)
}
tx.AddTxOut(wire.NewTxOut(int64(change), changeScript))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above estimation uses a transaction without the change output and therefore it will have a lesser size leading to an estimation based on a lower-sized transaction than the one that is actually sent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the output is included. Only that the change will minus tx fee (we don't know the tx fee yet) when calculating tx size

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see tx.AddTxOut with an empty change (or a dummy change etc) for adding the second output in the transaction. I only see a single tx.AddTxOut for the OP_RETURN. Am I missing something?

@gitferry
Copy link
Collaborator Author

Hi @vitsalis, thanks for your reviews which I have replied/addressed. In particular, I removed tx-fee-default and use tx-fee-max as default tx fee. Please see the updates.

@@ -195,7 +197,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) {
}

// TODO: consider dust, reference: https://www.oreilly.com/library/view/mastering-bitcoin/9781491902639/ch08.html#tx_verification
if uint64(amount.ToUnit(btcutil.AmountSatoshi)) < rl.GetTxFee()*2 {
if uint64(amount.ToUnit(btcutil.AmountSatoshi)) < rl.GetTxFee(0)*2 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then why don't we use the max fee config directly? This adds further complexity to the GetTxFee function by adding a special case with a 0 input which is not clear.

@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) {
func (rl *Relayer) buildTxWithData(
utxo *types.UTXO,
data []byte,
forTxSize bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we always calculate the transaction size?

change -= txFee
log.Logger.Debugf("balance of input: %v satoshi, tx fee: %v satoshi, output value: %v",
int64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)), txFee, change)
}
tx.AddTxOut(wire.NewTxOut(int64(change), changeScript))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see tx.AddTxOut with an empty change (or a dummy change etc) for adding the second output in the transaction. I only see a single tx.AddTxOut for the OP_RETURN. Am I missing something?

change := uint64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)) - rl.GetTxFee()
log.Logger.Debugf("balance of input: %v satoshi, tx fee: %v satoshi, output value: %v",
int64(utxo.Amount.ToUnit(btcutil.AmountSatoshi)), rl.GetTxFee(), change)
change := uint64(utxo.Amount.ToUnit(btcutil.AmountSatoshi))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If forTxSize is true, then the change will be all the funds of the UTXO.

@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) {
func (rl *Relayer) buildTxWithData(
utxo *types.UTXO,
data []byte,
forTxSize bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see what's happening. I suggest that we don't use this function again with the dubious flag, but just create a copy of the transaction we have already built and add a dummy change output. Then calculate the transaction size based on that. A cyclic dependency between the functions based on a flag is going to come bite us in the future (I had to spend some time reading the code to understand what was happening).

@gitferry gitferry force-pushed the submitter/fee-estimation branch 3 times, most recently from f47c932 to 56b0189 Compare November 28, 2022 13:51
@gitferry
Copy link
Collaborator Author

Thanks @vitsalis for your review. I have improved the way we calculate tx size and addressed other comments. Please see the updates. Thanks!

Copy link
Member

@vitsalis vitsalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Minor comments:


func calTxSize(tx *wire.MsgTx, utxo *types.UTXO, changeScript []byte, isSegWit bool, privkey *btcec.PrivateKey) (uint64, error) {
tx.AddTxOut(wire.NewTxOut(dummyChangeValue, changeScript))
if !isSegWit {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if/else based on isSegwit already happens in the buildTxWithData. Can it be moved to a util function to avoid code duplication?

"github.com/btcsuite/btcutil"
)

const dummyChangeValue int64 = 2500000000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change value can be the amount of tokens contained in the UTXO. This would be a good value to estimate something that is close to the actual change value.

@@ -15,3 +21,42 @@ func isSegWit(addr btcutil.Address) (bool, error) {
return false, errors.New("non-supported address type")
}
}

func calTxSize(tx *wire.MsgTx, utxo *types.UTXO, changeScript []byte, isSegWit bool, privkey *btcec.PrivateKey) (uint64, error) {
tx.AddTxOut(wire.NewTxOut(dummyChangeValue, changeScript))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of modifying the transaction directly, a copy could be made?

@@ -17,7 +17,9 @@ type BTCConfig struct {
WalletName string `mapstructure:"wallet-name"`
WalletCAFile string `mapstructure:"wallet-ca-file"`
WalletLockTime int64 `mapstructure:"wallet-lock-time"` // time duration in which the wallet remains unlocked, in seconds
TxFee btcutil.Amount `mapstructure:"tx-fee"` // BTC tx fee, in BTC
TxFeeMin btcutil.Amount `mapstructure:"tx-fee-min"` // minimum tx fee, in BTC
TxFeeMax btcutil.Amount `mapstructure:"tx-fee-max"` // maximum tx fee, in BTC
Copy link
Member

@vitsalis vitsalis Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe panic if TxFeeMax < TxFeeMin?

@gitferry gitferry merged commit 4a0a759 into dev Nov 28, 2022
@gitferry gitferry deleted the submitter/fee-estimation branch November 28, 2022 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants