From c01699853d36cf7c8ce1187ff4a7137dcadd90f8 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Fri, 25 Jun 2021 16:19:34 +0200 Subject: [PATCH] chainfee: add minFeeManager This commit adds a minFeeManager which holds a copy of minFeePerKW and updates this fee every few calls. --- lnwallet/chainfee/minfeemanager.go | 74 +++++++++++++++++++++++++ lnwallet/chainfee/minfeemanager_test.go | 54 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 lnwallet/chainfee/minfeemanager.go create mode 100644 lnwallet/chainfee/minfeemanager_test.go diff --git a/lnwallet/chainfee/minfeemanager.go b/lnwallet/chainfee/minfeemanager.go new file mode 100644 index 00000000..490b020e --- /dev/null +++ b/lnwallet/chainfee/minfeemanager.go @@ -0,0 +1,74 @@ +package chainfee + +import ( + "sync" + "time" +) + +// minFeeManager is used to store and update the minimum fee that is required +// by a transaction to be accepted to the mempool. The minFeeManager ensures +// that the backend used to fetch the fee is not queried too regularly. +type minFeeManager struct { + mu sync.Mutex + minFeePerKW SatPerKWeight + lastUpdatedTime time.Time + minUpdateInterval time.Duration + fetchFeeFunc fetchFee +} + +// fetchFee represents a function that can be used to fetch a fee. +type fetchFee func() (SatPerKWeight, error) + +// newMinFeeManager creates a new minFeeManager and uses the +// given fetchMinFee function to set the minFeePerKW of the minFeeManager. +// This function requires the fetchMinFee function to succeed. +func newMinFeeManager(minUpdateInterval time.Duration, + fetchMinFee fetchFee) (*minFeeManager, error) { + + minFee, err := fetchMinFee() + if err != nil { + return nil, err + } + + return &minFeeManager{ + minFeePerKW: minFee, + lastUpdatedTime: time.Now(), + minUpdateInterval: minUpdateInterval, + fetchFeeFunc: fetchMinFee, + }, nil +} + +// fetchMinFee returns the stored minFeePerKW if it has been updated recently +// or if the call to the chain backend fails. Otherwise, it sets the stored +// minFeePerKW to the fee returned from the backend and floors it based on +// our fee floor. +func (m *minFeeManager) fetchMinFee() SatPerKWeight { + m.mu.Lock() + defer m.mu.Unlock() + + if time.Since(m.lastUpdatedTime) < m.minUpdateInterval { + return m.minFeePerKW + } + + newMinFee, err := m.fetchFeeFunc() + if err != nil { + log.Errorf("Unable to fetch updated min fee from chain "+ + "backend. Using last known min fee instead: %v", err) + + return m.minFeePerKW + } + + // By default, we'll use the backend node's minimum fee as the + // minimum fee rate we'll propose for transactions. However, if this + // happens to be lower than our fee floor, we'll enforce that instead. + m.minFeePerKW = newMinFee + if m.minFeePerKW < FeePerKwFloor { + m.minFeePerKW = FeePerKwFloor + } + m.lastUpdatedTime = time.Now() + + log.Debugf("Using minimum fee rate of %v sat/kw", + int64(m.minFeePerKW)) + + return m.minFeePerKW +} diff --git a/lnwallet/chainfee/minfeemanager_test.go b/lnwallet/chainfee/minfeemanager_test.go new file mode 100644 index 00000000..0209f1a2 --- /dev/null +++ b/lnwallet/chainfee/minfeemanager_test.go @@ -0,0 +1,54 @@ +package chainfee + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type mockChainBackend struct { + minFee SatPerKWeight + callCount int +} + +func (m *mockChainBackend) fetchFee() (SatPerKWeight, error) { + m.callCount++ + return m.minFee, nil +} + +// TestMinFeeManager tests that the minFeeManager returns an up to date min fee +// by querying the chain backend and that it returns a cached fee if the chain +// backend was recently queried. +func TestMinFeeManager(t *testing.T) { + t.Parallel() + + chainBackend := &mockChainBackend{ + minFee: SatPerKWeight(1000), + } + + // Initialise the min fee manager. This should call the chain backend + // once. + feeManager, err := newMinFeeManager( + 100*time.Millisecond, + chainBackend.fetchFee, + ) + require.NoError(t, err) + require.Equal(t, 1, chainBackend.callCount) + + // If the fee is requested again, the stored fee should be returned + // and the chain backend should not be queried. + chainBackend.minFee = SatPerKWeight(2000) + minFee := feeManager.fetchMinFee() + require.Equal(t, minFee, SatPerKWeight(1000)) + require.Equal(t, 1, chainBackend.callCount) + + // Fake the passing of time. + feeManager.lastUpdatedTime = time.Now().Add(-200 * time.Millisecond) + + // If the fee is queried again after the backoff period has passed + // then the chain backend should be queried again for the min fee. + minFee = feeManager.fetchMinFee() + require.Equal(t, SatPerKWeight(2000), minFee) + require.Equal(t, 2, chainBackend.callCount) +}