diff --git a/lnwallet/fee_estimator.go b/lnwallet/fee_estimator.go index 078e6db7..c0574c5d 100644 --- a/lnwallet/fee_estimator.go +++ b/lnwallet/fee_estimator.go @@ -8,38 +8,49 @@ import ( "github.com/btcsuite/btcutil" ) -// SatPerVByte represents a fee rate in satoshis per vbyte. -type SatPerVByte btcutil.Amount +const ( + // FeePerKwFloor is the lowest fee rate in sat/kw that we should use for + // determining transaction fees. + FeePerKwFloor SatPerKWeight = 253 +) -// FeeForVSize calculates the fee resulting from this fee rate and -// the given vsize in vbytes. -func (s SatPerVByte) FeeForVSize(vbytes int64) btcutil.Amount { - return btcutil.Amount(s) * btcutil.Amount(vbytes) +// SatPerKVByte represents a fee rate in sat/kb. +type SatPerKVByte btcutil.Amount + +// FeeForVSize calculates the fee resulting from this fee rate and the given +// vsize in vbytes. +func (s SatPerKVByte) FeeForVSize(vbytes int64) btcutil.Amount { + return btcutil.Amount(s) * btcutil.Amount(vbytes) / 1000 } -// FeePerKWeight converts the fee rate into SatPerKWeight. -func (s SatPerVByte) FeePerKWeight() SatPerKWeight { - return SatPerKWeight(s * 1000 / blockchain.WitnessScaleFactor) +// FeePerKWeight converts the current fee rate from sat/kb to sat/kw. +func (s SatPerKVByte) FeePerKWeight() SatPerKWeight { + return SatPerKWeight(s / blockchain.WitnessScaleFactor) } -// SatPerKWeight represents a fee rate in satoshis per kilo weight unit. +// SatPerKWeight represents a fee rate in sat/kw. type SatPerKWeight btcutil.Amount -// FeeForWeight calculates the fee resulting from this fee rate and the -// given weight in weight units (wu). +// FeeForWeight calculates the fee resulting from this fee rate and the given +// weight in weight units (wu). func (s SatPerKWeight) FeeForWeight(wu int64) btcutil.Amount { // The resulting fee is rounded down, as specified in BOLT#03. return btcutil.Amount(s) * btcutil.Amount(wu) / 1000 } +// FeePerKVByte converts the current fee rate from sat/kw to sat/kb. +func (s SatPerKWeight) FeePerKVByte() SatPerKVByte { + return SatPerKVByte(s * blockchain.WitnessScaleFactor) +} + // FeeEstimator provides the ability to estimate on-chain transaction fees for // various combinations of transaction sizes and desired confirmation time // (measured by number of blocks). type FeeEstimator interface { - // EstimateFeePerVSize takes in a target for the number of blocks until - // an initial confirmation and returns the estimated fee expressed in - // satoshis/vbyte. - EstimateFeePerVSize(numBlocks uint32) (SatPerVByte, error) + // EstimateFeePerKW takes in a target for the number of blocks until an + // initial confirmation and returns the estimated fee expressed in + // sat/kw. + EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) // Start signals the FeeEstimator to start any processes or goroutines // it needs to perform its duty. @@ -54,16 +65,16 @@ type FeeEstimator interface { // requests. It is designed to be replaced by a proper fee calculation // implementation. type StaticFeeEstimator struct { - // FeeRate is the static fee rate in satoshis-per-vbyte that will be + // FeePerKW is the static fee rate in satoshis-per-vbyte that will be // returned by this fee estimator. - FeeRate SatPerVByte + FeePerKW SatPerKWeight } -// EstimateFeePerVSize will return a static value for fee calculations. +// EstimateFeePerKW will return a static value for fee calculations. // // NOTE: This method is part of the FeeEstimator interface. -func (e StaticFeeEstimator) EstimateFeePerVSize(numBlocks uint32) (SatPerVByte, error) { - return e.FeeRate, nil +func (e StaticFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) { + return e.FeePerKW, nil } // Start signals the FeeEstimator to start any processes or goroutines @@ -90,16 +101,16 @@ var _ FeeEstimator = (*StaticFeeEstimator)(nil) // by the RPC interface of an active btcd node. This implementation will proxy // any fee estimation requests to btcd's RPC interface. type BtcdFeeEstimator struct { - // fallBackFeeRate is the fall back fee rate in satoshis per vbyte that - // is returned if the fee estimator does not yet have enough data to - // actually produce fee estimates. - fallBackFeeRate SatPerVByte + // fallbackFeePerKW is the fall back fee rate in sat/kw that is returned + // if the fee estimator does not yet have enough data to actually + // produce fee estimates. + fallbackFeePerKW SatPerKWeight - // minFeeRate is the minimum relay fee, in sat/vbyte, of the backend - // node. This will be used as the default fee rate of a transaction when - // the estimated fee rate is too low to allow the transaction to - // propagate through the network. - minFeeRate SatPerVByte + // minFeePerKW is the minimum fee, in sat/kw, that we should enforce. + // This will be used as the default fee rate for a transaction when the + // estimated fee rate is too low to allow the transaction to propagate + // through the network. + minFeePerKW SatPerKWeight btcdConn *rpcclient.Client } @@ -110,7 +121,7 @@ type BtcdFeeEstimator struct { // the occasion that the estimator has insufficient data, or returns zero for a // fee estimate. func NewBtcdFeeEstimator(rpcConfig rpcclient.ConnConfig, - fallBackFeeRate SatPerVByte) (*BtcdFeeEstimator, error) { + fallBackFeeRate SatPerKWeight) (*BtcdFeeEstimator, error) { rpcConfig.DisableConnectOnNew = true rpcConfig.DisableAutoReconnect = false @@ -120,8 +131,8 @@ func NewBtcdFeeEstimator(rpcConfig rpcclient.ConnConfig, } return &BtcdFeeEstimator{ - fallBackFeeRate: fallBackFeeRate, - btcdConn: chainConn, + fallbackFeePerKW: fallBackFeeRate, + btcdConn: chainConn, }, nil } @@ -146,9 +157,20 @@ func (b *BtcdFeeEstimator) Start() error { return err } - // The fee rate is expressed in sat/KB, so we'll manually convert it to - // our desired sat/vbyte rate. - b.minFeeRate = SatPerVByte(relayFee / 1000) + // The fee rate is expressed in sat/kb, so we'll manually convert it to + // our desired sat/kw rate. + minRelayFeePerKw := SatPerKVByte(relayFee).FeePerKWeight() + + // By default, we'll use the backend node's minimum relay fee as the + // minimum fee rate we'll propose for transacations. However, if this + // happens to be lower than our fee floor, we'll enforce that instead. + b.minFeePerKW = minRelayFeePerKw + if b.minFeePerKW < FeePerKwFloor { + b.minFeePerKW = FeePerKwFloor + } + + walletLog.Debugf("Using minimum fee rate of %v sat/kw", + int64(b.minFeePerKW)) return nil } @@ -163,13 +185,12 @@ func (b *BtcdFeeEstimator) Stop() error { return nil } -// EstimateFeePerVSize takes in a target for the number of blocks until an -// initial confirmation and returns the estimated fee expressed in -// satoshis/vbyte. +// EstimateFeePerKW takes in a target for the number of blocks until an initial +// confirmation and returns the estimated fee expressed in sat/kw. // // NOTE: This method is part of the FeeEstimator interface. -func (b *BtcdFeeEstimator) EstimateFeePerVSize(numBlocks uint32) (SatPerVByte, error) { - feeEstimate, err := b.fetchEstimatePerVSize(numBlocks) +func (b *BtcdFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) { + feeEstimate, err := b.fetchEstimate(numBlocks) switch { // If the estimator doesn't have enough data, or returns an error, then // to return a proper value, then we'll return the default fall back @@ -179,16 +200,15 @@ func (b *BtcdFeeEstimator) EstimateFeePerVSize(numBlocks uint32) (SatPerVByte, e fallthrough case feeEstimate == 0: - return b.fallBackFeeRate, nil + return b.fallbackFeePerKW, nil } return feeEstimate, nil } // fetchEstimate returns a fee estimate for a transaction to be confirmed in -// confTarget blocks. The estimate is returned in sat/vbyte. -func (b *BtcdFeeEstimator) fetchEstimatePerVSize( - confTarget uint32) (SatPerVByte, error) { +// confTarget blocks. The estimate is returned in sat/kw. +func (b *BtcdFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) { // First, we'll fetch the estimate for our confirmation target. btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget)) if err != nil { @@ -202,23 +222,21 @@ func (b *BtcdFeeEstimator) fetchEstimatePerVSize( return 0, err } - // The value returned is expressed in fees per KB, while we want - // fee-per-byte, so we'll divide by 1000 to map to satoshis-per-byte - // before returning the estimate. - satPerByte := SatPerVByte(satPerKB / 1000) + // Since we use fee rates in sat/kw internally, we'll convert the + // estimated fee rate from its sat/kb representation to sat/kw. + satPerKw := SatPerKVByte(satPerKB).FeePerKWeight() - // Before proceeding, we'll make sure that this fee rate respects the - // minimum relay fee set on the backend node. - if satPerByte < b.minFeeRate { - walletLog.Debugf("Using backend node's minimum relay fee rate "+ - "of %v sat/vbyte", b.minFeeRate) - satPerByte = b.minFeeRate + // Finally, we'll enforce our fee floor. + if satPerKw < b.minFeePerKW { + walletLog.Debugf("Estimated fee rate of %v sat/kw is too low, "+ + "using fee floor of %v sat/kw instead", b.minFeePerKW) + satPerKw = b.minFeePerKW } - walletLog.Debugf("Returning %v sat/vbyte for conf target of %v", - int64(satPerByte), confTarget) + walletLog.Debugf("Returning %v sat/kw for conf target of %v", + int64(satPerKw), confTarget) - return satPerByte, nil + return satPerKw, nil } // A compile-time assertion to ensure that BtcdFeeEstimator implements the @@ -229,16 +247,16 @@ var _ FeeEstimator = (*BtcdFeeEstimator)(nil) // backed by the RPC interface of an active bitcoind node. This implementation // will proxy any fee estimation requests to bitcoind's RPC interface. type BitcoindFeeEstimator struct { - // fallBackFeeRate is the fall back fee rate in satoshis per vbyte that - // is returned if the fee estimator does not yet have enough data to - // actually produce fee estimates. - fallBackFeeRate SatPerVByte + // fallbackFeePerKW is the fallback fee rate in sat/kw that is returned + // if the fee estimator does not yet have enough data to actually + // produce fee estimates. + fallbackFeePerKW SatPerKWeight - // minFeeRate is the minimum relay fee, in sat/vbyte, of the backend - // node. This will be used as the default fee rate of a transaction when - // the estimated fee rate is too low to allow the transaction to - // propagate through the network. - minFeeRate SatPerVByte + // minFeePerKW is the minimum fee, in sat/kw, that we should enforce. + // This will be used as the default fee rate for a transaction when the + // estimated fee rate is too low to allow the transaction to propagate + // through the network. + minFeePerKW SatPerKWeight bitcoindConn *rpcclient.Client } @@ -249,7 +267,7 @@ type BitcoindFeeEstimator struct { // is used in the occasion that the estimator has insufficient data, or returns // zero for a fee estimate. func NewBitcoindFeeEstimator(rpcConfig rpcclient.ConnConfig, - fallBackFeeRate SatPerVByte) (*BitcoindFeeEstimator, error) { + fallBackFeeRate SatPerKWeight) (*BitcoindFeeEstimator, error) { rpcConfig.DisableConnectOnNew = true rpcConfig.DisableAutoReconnect = false @@ -261,8 +279,8 @@ func NewBitcoindFeeEstimator(rpcConfig rpcclient.ConnConfig, } return &BitcoindFeeEstimator{ - fallBackFeeRate: fallBackFeeRate, - bitcoindConn: chainConn, + fallbackFeePerKW: fallBackFeeRate, + bitcoindConn: chainConn, }, nil } @@ -293,9 +311,20 @@ func (b *BitcoindFeeEstimator) Start() error { return err } - // The fee rate is expressed in sat/KB, so we'll manually convert it to - // our desired sat/vbyte rate. - b.minFeeRate = SatPerVByte(relayFee / 1000) + // The fee rate is expressed in sat/kb, so we'll manually convert it to + // our desired sat/kw rate. + minRelayFeePerKw := SatPerKVByte(relayFee).FeePerKWeight() + + // By default, we'll use the backend node's minimum relay fee as the + // minimum fee rate we'll propose for transacations. However, if this + // happens to be lower than our fee floor, we'll enforce that instead. + b.minFeePerKW = minRelayFeePerKw + if b.minFeePerKW < FeePerKwFloor { + b.minFeePerKW = FeePerKwFloor + } + + walletLog.Debugf("Using minimum fee rate of %v sat/kw", + int64(b.minFeePerKW)) return nil } @@ -308,13 +337,12 @@ func (b *BitcoindFeeEstimator) Stop() error { return nil } -// EstimateFeePerVSize takes in a target for the number of blocks until an -// initial confirmation and returns the estimated fee expressed in -// satoshis/vbyte. +// EstimateFeePerKW takes in a target for the number of blocks until an initial +// confirmation and returns the estimated fee expressed in sat/kw. // // NOTE: This method is part of the FeeEstimator interface. -func (b *BitcoindFeeEstimator) EstimateFeePerVSize(numBlocks uint32) (SatPerVByte, error) { - feeEstimate, err := b.fetchEstimatePerVSize(numBlocks) +func (b *BitcoindFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) { + feeEstimate, err := b.fetchEstimate(numBlocks) switch { // If the estimator doesn't have enough data, or returns an error, then // to return a proper value, then we'll return the default fall back @@ -324,16 +352,15 @@ func (b *BitcoindFeeEstimator) EstimateFeePerVSize(numBlocks uint32) (SatPerVByt fallthrough case feeEstimate == 0: - return b.fallBackFeeRate, nil + return b.fallbackFeePerKW, nil } return feeEstimate, nil } -// fetchEstimatePerVSize returns a fee estimate for a transaction to be confirmed in -// confTarget blocks. The estimate is returned in sat/vbyte. -func (b *BitcoindFeeEstimator) fetchEstimatePerVSize( - confTarget uint32) (SatPerVByte, error) { +// fetchEstimate returns a fee estimate for a transaction to be confirmed in +// confTarget blocks. The estimate is returned in sat/kw. +func (b *BitcoindFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) { // First, we'll send an "estimatesmartfee" command as a raw request, // since it isn't supported by btcd but is available in bitcoind. target, err := json.Marshal(uint64(confTarget)) @@ -341,45 +368,44 @@ func (b *BitcoindFeeEstimator) fetchEstimatePerVSize( return 0, err } // TODO: Allow selection of economical/conservative modifiers. - resp, err := b.bitcoindConn.RawRequest("estimatesmartfee", - []json.RawMessage{target}) + resp, err := b.bitcoindConn.RawRequest( + "estimatesmartfee", []json.RawMessage{target}, + ) if err != nil { return 0, err } // Next, we'll parse the response to get the BTC per KB. feeEstimate := struct { - Feerate float64 `json:"feerate"` + FeeRate float64 `json:"feerate"` }{} err = json.Unmarshal(resp, &feeEstimate) if err != nil { return 0, err } - // Next, we'll convert the returned value to satoshis, as it's - // currently returned in BTC. - satPerKB, err := btcutil.NewAmount(feeEstimate.Feerate) + // Next, we'll convert the returned value to satoshis, as it's currently + // returned in BTC. + satPerKB, err := btcutil.NewAmount(feeEstimate.FeeRate) if err != nil { return 0, err } - // The value returned is expressed in fees per KB, while we want - // fee-per-byte, so we'll divide by 1000 to map to satoshis-per-byte - // before returning the estimate. - satPerByte := SatPerVByte(satPerKB / 1000) + // Since we use fee rates in sat/kw internally, we'll convert the + // estimated fee rate from its sat/kb representation to sat/kw. + satPerKw := SatPerKVByte(satPerKB).FeePerKWeight() - // Before proceeding, we'll make sure that this fee rate respects the - // minimum relay fee set on the backend node. - if satPerByte < b.minFeeRate { - walletLog.Debugf("Using backend node's minimum relay fee rate "+ - "of %v sat/vbyte", b.minFeeRate) - satPerByte = b.minFeeRate + // Finally, we'll enforce our fee floor. + if satPerKw < b.minFeePerKW { + walletLog.Debugf("Estimated fee rate of %v sat/kw is too low, "+ + "using fee floor of %v sat/kw instead", b.minFeePerKW) + satPerKw = b.minFeePerKW } - walletLog.Debugf("Returning %v sat/vbyte for conf target of %v", - int64(satPerByte), confTarget) + walletLog.Debugf("Returning %v sat/kw for conf target of %v", + int64(satPerKw), confTarget) - return satPerByte, nil + return satPerKw, nil } // A compile-time assertion to ensure that BitcoindFeeEstimator implements the diff --git a/lnwallet/fee_estimator_test.go b/lnwallet/fee_estimator_test.go index 0fa97ead..d4608b9b 100644 --- a/lnwallet/fee_estimator_test.go +++ b/lnwallet/fee_estimator_test.go @@ -13,57 +13,56 @@ import ( func TestFeeRateTypes(t *testing.T) { t.Parallel() - // Let our fee rate be 100 sat/vbyte. - feePerVSize := lnwallet.SatPerVByte(100) + // We'll be calculating the transaction fees for the given measurements + // using different fee rates and expecting them to match. + const vsize = 300 + const weight = vsize * 4 - // It is also equivalent to 25000 sat/kw. - feePerKw := feePerVSize.FeePerKWeight() - if feePerKw != 25000 { - t.Fatalf("expected %d sat/kw, got %d sat/kw", 25000, - feePerKw) - } - - const txVSize = 300 - - // We'll now run through a set of values for the fee per vsize type, - // making sure the conversion to sat/kw and fee calculation is done - // correctly. - for f := lnwallet.SatPerVByte(0); f <= 40; f++ { - fPerKw := f.FeePerKWeight() - - // The kw is always 250*vsize. - if fPerKw != lnwallet.SatPerKWeight(f*250) { - t.Fatalf("expected %d sat/kw, got %d sat/kw, when "+ - "converting %d sat/vbyte", f*250, fPerKw, f) + // Test the conversion from sat/kw to sat/kb. + for feePerKw := lnwallet.SatPerKWeight(250); feePerKw < 10000; feePerKw += 50 { + feePerKB := feePerKw.FeePerKVByte() + if feePerKB != lnwallet.SatPerKVByte(feePerKw*4) { + t.Fatalf("expected %d sat/kb, got %d sat/kb when "+ + "converting from %d sat/kw", feePerKw*4, + feePerKB, feePerKw) } - // The tx fee should simply be f*txvsize. - fee := f.FeeForVSize(txVSize) - if fee != btcutil.Amount(f*txVSize) { - t.Fatalf("expected tx fee to be %d sat, was %d sat", - f*txVSize, fee) + // The resulting transaction fee should be the same when using + // both rates. + expectedFee := btcutil.Amount(feePerKw * weight / 1000) + fee1 := feePerKw.FeeForWeight(weight) + if fee1 != expectedFee { + t.Fatalf("expected fee of %d sats, got %d sats", + expectedFee, fee1) } - - // The weight is 4*vsize. Fee calculation from the fee/kw - // should result in the same fee. - fee2 := fPerKw.FeeForWeight(txVSize * 4) - if fee != fee2 { - t.Fatalf("fee calculated from vsize (%d) not equal "+ - "fee calculated from weight (%d)", fee, fee2) + fee2 := feePerKB.FeeForVSize(vsize) + if fee2 != expectedFee { + t.Fatalf("expected fee of %d sats, got %d sats", + expectedFee, fee2) } } - // Do the same for fee per kw. - for f := lnwallet.SatPerKWeight(0); f < 1500; f++ { - weight := int64(txVSize * 4) + // Test the conversion from sat/kb to sat/kw. + for feePerKB := lnwallet.SatPerKVByte(1000); feePerKB < 40000; feePerKB += 1000 { + feePerKw := feePerKB.FeePerKWeight() + if feePerKw != lnwallet.SatPerKWeight(feePerKB/4) { + t.Fatalf("expected %d sat/kw, got %d sat/kw when "+ + "converting from %d sat/kb", feePerKB/4, + feePerKw, feePerKB) + } - // The expected fee is weight*f / 1000, since the fee is - // denominated per 1000 wu. - expFee := btcutil.Amount(weight) * btcutil.Amount(f) / 1000 - fee := f.FeeForWeight(weight) - if fee != expFee { - t.Fatalf("expected fee to be %d sat, was %d", - fee, expFee) + // The resulting transaction fee should be the same when using + // both rates. + expectedFee := btcutil.Amount(feePerKB * vsize / 1000) + fee1 := feePerKB.FeeForVSize(vsize) + if fee1 != expectedFee { + t.Fatalf("expected fee of %d sats, got %d sats", + expectedFee, fee1) + } + fee2 := feePerKw.FeeForWeight(weight) + if fee2 != expectedFee { + t.Fatalf("expected fee of %d sats, got %d sats", + expectedFee, fee2) } } } @@ -73,22 +72,22 @@ func TestFeeRateTypes(t *testing.T) { func TestStaticFeeEstimator(t *testing.T) { t.Parallel() - const feePerVSize = 100 + const feePerKw = lnwallet.FeePerKwFloor feeEstimator := &lnwallet.StaticFeeEstimator{ - FeeRate: feePerVSize, + FeePerKW: feePerKw, } if err := feeEstimator.Start(); err != nil { t.Fatalf("unable to start fee estimator: %v", err) } defer feeEstimator.Stop() - feeRate, err := feeEstimator.EstimateFeePerVSize(6) + feeRate, err := feeEstimator.EstimateFeePerKW(6) if err != nil { t.Fatalf("unable to get fee rate: %v", err) } - if feeRate != feePerVSize { - t.Fatalf("expected fee rate %v, got %v", feePerVSize, feeRate) + if feeRate != feePerKw { + t.Fatalf("expected fee rate %v, got %v", feePerKw, feeRate) } }