-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
There was a problem hiding this 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
btcclient/client_wallet.go
Outdated
func (c *Client) GetTxFee(txSize uint64) uint64 { | ||
defaultFee := uint64(c.Cfg.TxFeeDefault.ToUnit(btcutil.AmountSatoshi)) | ||
if txSize == 0 { | ||
return defaultFee |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
btcclient/client_wallet.go
Outdated
} | ||
feeRateAmount, err := btcutil.NewAmount(feeRate) | ||
if err != nil { | ||
return defaultFee |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
btcclient/client_wallet.go
Outdated
return defaultFee | ||
} | ||
fee := uint64(feeRateAmount.ToUnit(btcutil.AmountSatoshi)) * txSize | ||
if fee > uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) || |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Thanks!
btcclient/client_wallet.go
Outdated
} | ||
fee := uint64(feeRateAmount.ToUnit(btcutil.AmountSatoshi)) * txSize | ||
if fee > uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) || | ||
fee < uint64(c.Cfg.TxFeeMax.ToUnit(btcutil.AmountSatoshi)) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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
.
config/bitcoin.go
Outdated
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 |
There was a problem hiding this comment.
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).
submitter/relayer/relayer.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
submitter/relayer/relayer.go
Outdated
@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) { | |||
func (rl *Relayer) buildTxWithData( | |||
utxo *types.UTXO, | |||
data []byte, | |||
forTxSize bool, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
submitter/relayer/relayer.go
Outdated
} | ||
txFee := rl.GetTxFee(txSize) | ||
change -= txFee | ||
log.Logger.Debugf("balance of input: %v satoshi, tx fee: %v satoshi, output value: %v", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
submitter/relayer/relayer.go
Outdated
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
dbbcf30
to
2d29aa8
Compare
Hi @vitsalis, thanks for your reviews which I have replied/addressed. In particular, I removed |
2d29aa8
to
09cdf8a
Compare
submitter/relayer/relayer.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
submitter/relayer/relayer.go
Outdated
@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) { | |||
func (rl *Relayer) buildTxWithData( | |||
utxo *types.UTXO, | |||
data []byte, | |||
forTxSize bool, |
There was a problem hiding this comment.
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?
submitter/relayer/relayer.go
Outdated
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)) |
There was a problem hiding this comment.
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?
submitter/relayer/relayer.go
Outdated
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)) |
There was a problem hiding this comment.
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
.
submitter/relayer/relayer.go
Outdated
@@ -219,6 +221,7 @@ func (rl *Relayer) PickHighUTXO() (*types.UTXO, error) { | |||
func (rl *Relayer) buildTxWithData( | |||
utxo *types.UTXO, | |||
data []byte, | |||
forTxSize bool, |
There was a problem hiding this comment.
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).
f47c932
to
56b0189
Compare
56b0189
to
58c9847
Compare
Thanks @vitsalis for your review. I have improved the way we calculate tx size and addressed other comments. Please see the updates. Thanks! |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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?
submitter/relayer/utils.go
Outdated
"github.com/btcsuite/btcutil" | ||
) | ||
|
||
const dummyChangeValue int64 = 2500000000 |
There was a problem hiding this comment.
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.
submitter/relayer/utils.go
Outdated
@@ -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)) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
138de58
to
bce3483
Compare
c39cf7e
to
fc82572
Compare
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 addsgetTxSize
to calculate tx size with simulated tx construction without adding tx fee.Note that
EstimateFee
is deprecated, so it should be replaced byEstimateSmartFee
after btcd upgrade tov0.23
. See #117.