lnwallet: switch fee estimation to return sat/kw fee rates

In this commit, we modify our FeeEstimator interface to return an
estimated fee rate in sat/kw. Recently, due to low fees on the network,
users have been experiencing failures broadcasting transactions due to
not meeting specific fee requirements. This was happening more often
than not, as the estimated fee returned by backend nodes (bitcoind and
btcd) only takes into account vbytes, rather than weight. The fees
returned are also expressed in sat/kb, so we must take care that we do
not lose precision while converting to sat/kw. In the event that this
happens, a fee floor of 253 sat/kw has been added. This fee rate
originates from bitcoind rounding up the conversion from weight to
vbytes.
This commit is contained in:
Wilmer Paulino 2018-07-27 18:37:05 -07:00
parent 30b156c706
commit b3d5d7ab9c
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
2 changed files with 175 additions and 150 deletions

@ -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

@ -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)
}
}