use fee params from database

This commit is contained in:
Jesse de Wit
2023-05-17 12:03:03 +02:00
parent e2912be9be
commit d7aee62fd4
10 changed files with 146 additions and 159 deletions

View File

@@ -1,61 +0,0 @@
package chain
import (
"context"
"sync"
"time"
)
var cacheDuration time.Duration = time.Minute * 5
type feeCache struct {
time time.Time
estimation *FeeEstimation
}
type CachedFeeEstimator struct {
cache map[FeeStrategy]*feeCache
inner FeeEstimator
mtx sync.Mutex
}
func NewCachedFeeEstimator(inner FeeEstimator) *CachedFeeEstimator {
return &CachedFeeEstimator{
inner: inner,
cache: make(map[FeeStrategy]*feeCache),
}
}
func (e *CachedFeeEstimator) EstimateFeeRate(
ctx context.Context,
strategy FeeStrategy,
) (*FeeEstimation, error) {
// Make sure we're in a lock, because we're reading/writing a map.
e.mtx.Lock()
defer e.mtx.Unlock()
// See if there's a cached value first.
cached, ok := e.cache[strategy]
// If there is and it's still valid, return that.
if ok && cached.time.Add(cacheDuration).After(time.Now()) {
return cached.estimation, nil
}
// There was no valid cache.
// Fetch the new fee estimate.
now := time.Now()
estimation, err := e.inner.EstimateFeeRate(ctx, strategy)
if err != nil {
return nil, err
}
// Cache it.
e.cache[strategy] = &feeCache{
time: now,
estimation: estimation,
}
return estimation, nil
}

View File

@@ -66,16 +66,6 @@ type NodeConfig struct {
// The channel can be closed if not used this duration in seconds.
MaxInactiveDuration uint64 `json:"maxInactiveDuration,string"`
FeeParams []*FeeParamsSettings `json:"feeParams"`
// When a htlc comes in where a channel open is needed, and that payment
// was registered with a promise, but the promise has expired, lspd may
// open the channel anyway if the fee is low enough right now (the promise
// fee was higher than the current fee). ExpiredPromiseMultiplicationFactor
// is the multiplication factor to use on the mempool fee rate to check
// whether the min fee of the promise is lower than the current min fee.
ExpiredPromiseMultiplicationFactor uint64
// Set this field to connect to an LND node.
Lnd *LndConfig `json:"lnd,omitempty"`
@@ -83,33 +73,6 @@ type NodeConfig struct {
Cln *ClnConfig `json:"cln,omitempty"`
}
type FeeParamsSettings struct {
// The validity duration of an opening params promise in seconds.
ValidityDuration uint64 `json:"validityDuration,string"`
// Maximum number of blocks that the client is allowed to set its
// `to_self_delay` parameter.
MaxClientToSelfDelay uint64 `json:"maxClientToSelfDelay,string"`
// Multiplication factor to calculate the minimum fee for a JIT channel open.
// The resulting fee after multiplying sat/vbyte by the multiplication factor
// is denominated in millisat.
// e.g. if you expect to publish 500 bytes onchain with the given sat/vbyte
// fee rate, and take a margin of 20%, the fee multiplication factor should
// be 500 * 1.2 * 1000 = 600000. With 20 sat/vbyte, the resulting minimum fee
// would be 600000 * 20 = 12000000msat = 12000sat.
MultiplicationFactor uint64 `json:"multiplicationFactor,string"`
// The proportional fee to charge on channeel opens in ppm.
Proportional uint32 `json:"proportional,string"`
// The minimum fee to charge for a channel open.
MinimumFeeMsat uint64 `json:"minimumFeeMsat,string"`
// The maximum idle time in blocks
MaxIdleTime uint32 `json:"maxIdleTime,string"`
}
type LndConfig struct {
// Address to the grpc api.
Address string `json:"address"`

View File

@@ -123,8 +123,7 @@ func (i *Interceptor) Intercept(nextHop string, reqPaymentHash []byte, reqOutgoi
}
if time.Now().UTC().After(validUntil) {
feeEstimate, err := i.feeEstimator.EstimateFeeRate(context.Background(), i.feeStrategy)
if err != nil {
if !i.isCurrentChainFeeCheaper(params) {
log.Printf("Intercepted expired payment registration. Failing payment. payment hash: %x, valid until: %s", paymentHash, params.ValidUntil)
return InterceptResult{
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
@@ -132,16 +131,7 @@ func (i *Interceptor) Intercept(nextHop string, reqPaymentHash []byte, reqOutgoi
}, nil
}
minMsat := uint64(feeEstimate.SatPerVByte * float64(i.config.ExpiredPromiseMultiplicationFactor))
if minMsat >= params.MinMsat {
log.Printf("Intercepted expired payment registration. Failing payment. payment hash: %x, valid until: %s", paymentHash, params.ValidUntil)
return InterceptResult{
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
}, nil
}
log.Printf("Intercepted expired payment registration. Opening channel anyway, because it's cheaper at the current rate. feeEstimate: %v minMsat: %v, params: %+v", feeEstimate.SatPerVByte, minMsat, params)
log.Printf("Intercepted expired payment registration. Opening channel anyway, because it's cheaper at the current rate. %+v", params)
}
channelPoint, err = i.openChannel(reqPaymentHash, destination, incomingAmountMsat)
@@ -291,6 +281,22 @@ func (i *Interceptor) Intercept(nextHop string, reqPaymentHash []byte, reqOutgoi
return resp.(InterceptResult)
}
func (i *Interceptor) isCurrentChainFeeCheaper(params *OpeningFeeParams) bool {
settings, err := i.store.GetFeeParamsSettings()
if err != nil {
log.Printf("Failed to get fee params settings: %v", err)
return false
}
for _, setting := range settings {
if setting.Params.MinMsat <= params.MinMsat {
return true
}
}
return false
}
func (i *Interceptor) openChannel(paymentHash, destination []byte, incomingAmountMsat int64) (*wire.OutPoint, error) {
capacity := incomingAmountMsat/1000 + i.config.AdditionalChannelCapacity
if capacity == i.config.PublicChannelAmount {

View File

@@ -6,6 +6,10 @@ import (
"github.com/btcsuite/btcd/wire"
)
type OpeningFeeParamsSetting struct {
Validity time.Duration
Params *OpeningFeeParams
}
type OpeningFeeParams struct {
MinMsat uint64 `json:"min_msat,string"`
Proportional uint32 `json:"proportional"`
@@ -20,4 +24,5 @@ type InterceptStore interface {
SetFundingTx(paymentHash []byte, channelPoint *wire.OutPoint) error
RegisterPayment(params *OpeningFeeParams, destination, paymentHash, paymentSecret []byte, incomingAmountMsat, outgoingAmountMsat int64, tag string) error
InsertChannel(initialChanID, confirmedChanId uint64, channelPoint string, nodeID []byte, lastUpdate time.Time) error
GetFeeParamsSettings() ([]*OpeningFeeParamsSetting, error)
}

View File

@@ -22,18 +22,20 @@ func testDynamicFeeFlow(p *testParams) {
channelId := alice.WaitForChannelReady(channel)
log.Printf("Getting channel information")
p.Mempool().SetFees(&RecommendedFeesResponse{
FastestFee: 3,
HalfHourFee: 3,
HourFee: 3,
EconomyFee: 3,
MinimumFee: 3,
})
SetFeeParams(p.lsp, []*FeeParamSetting{
{
Validity: time.Second * 3600,
MinMsat: 3000000,
Proportional: 1000,
},
},
)
info := ChannelInformation(p.lsp)
assert.Len(p.t, info.OpeningFeeParamsMenu, 1)
params := info.OpeningFeeParamsMenu[0]
assert.Equal(p.t, uint64(3000000), params.MinMsat)
log.Printf("opening_fee_params: %+v", params)
log.Printf("Adding bob's invoices")
outerAmountMsat := uint64(4200000)
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, params)

View File

@@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path/filepath"
"time"
"github.com/breez/lntest"
"github.com/breez/lspd/config"
@@ -19,6 +20,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
ecies "github.com/ecies/go/v2"
"github.com/golang/protobuf/proto"
"github.com/jackc/pgx/v4/pgxpool"
)
var (
@@ -109,18 +111,8 @@ func newLspd(h *lntest.TestHarness, mem *mempoolApi, name string, nodeConfig *co
ChannelMinimumFeeMsat: 2000000,
AdditionalChannelCapacity: 100000,
MaxInactiveDuration: 3888000,
FeeParams: []*config.FeeParamsSettings{
{
ValidityDuration: 60 * 60 * 24,
MaxClientToSelfDelay: 2016,
MultiplicationFactor: 1000000,
Proportional: 4000,
MinimumFeeMsat: 2000000,
MaxIdleTime: 6480,
},
},
Lnd: lnd,
Cln: cln,
Lnd: lnd,
Cln: cln,
}
if nodeConfig != nil {
@@ -192,6 +184,25 @@ func (l *lspBase) Initialize() error {
return err
}
pgxPool, err := pgxpool.Connect(l.harness.Ctx, l.postgresBackend.ConnectionString())
if err != nil {
lntest.PerformCleanup(cleanups)
return fmt.Errorf("failed to connect to postgres: %w", err)
}
defer pgxPool.Close()
_, err = pgxPool.Exec(
l.harness.Ctx,
`INSERT INTO new_channel_params (validity, params)
VALUES
(3600, '{"min_msat": "1000000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}'),
(259200, '{"min_msat": "1100000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}');`,
)
if err != nil {
lntest.PerformCleanup(cleanups)
return fmt.Errorf("failed to insert new_channel_params: %w", err)
}
log.Printf("%s: Creating lspd startup script at %s", l.name, l.scriptFilePath)
scriptFile, err := os.OpenFile(l.scriptFilePath, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
@@ -264,6 +275,53 @@ func RegisterPayment(l LspNode, paymentInfo *lspd.PaymentInformation, continueOn
return err
}
type FeeParamSetting struct {
Validity time.Duration
MinMsat uint64
Proportional uint32
}
func SetFeeParams(l LspNode, settings []*FeeParamSetting) error {
pgxPool, err := pgxpool.Connect(l.Harness().Ctx, l.PostgresBackend().ConnectionString())
if err != nil {
return fmt.Errorf("failed to connect to postgres: %w", err)
}
defer pgxPool.Close()
_, err = pgxPool.Exec(l.Harness().Ctx, "DELETE FROM new_channel_params")
if err != nil {
return fmt.Errorf("failed to delete new_channel_params: %w", err)
}
if len(settings) == 0 {
return nil
}
query := `INSERT INTO new_channel_params (validity, params) VALUES `
first := true
for _, setting := range settings {
if !first {
query += `,`
}
query += fmt.Sprintf(
`(%d, '{"min_msat": "%d", "proportional": %d, "max_idle_time": 4320, "max_client_to_self_delay": 432}')`,
int64(setting.Validity.Seconds()),
setting.MinMsat,
setting.Proportional,
)
first = false
}
query += `;`
_, err = pgxPool.Exec(l.Harness().Ctx, query)
if err != nil {
return fmt.Errorf("failed to insert new_channel_params: %w", err)
}
return nil
}
func getLspdBinary() (string, error) {
if lspdExecutable != nil {
return *lspdExecutable, nil

View File

@@ -37,7 +37,7 @@ func calculateInnerAmountMsat(lsp LspNode, outerAmountMsat uint64, params *lspd.
fee = 2000000
}
} else {
fee = outerAmountMsat * 40 / 1_000_000 / 1_000 * 1_000
fee = outerAmountMsat * uint64(params.Proportional) / 1_000_000 / 1_000 * 1_000
if fee < params.MinMsat {
fee = params.MinMsat
}

View File

@@ -117,8 +117,7 @@ func main() {
address := os.Getenv("LISTEN_ADDRESS")
certMagicDomain := os.Getenv("CERTMAGIC_DOMAIN")
cachedEstimator := chain.NewCachedFeeEstimator(feeEstimator)
s, err := NewGrpcServer(nodes, address, certMagicDomain, interceptStore, feeStrategy, cachedEstimator)
s, err := NewGrpcServer(nodes, address, certMagicDomain, interceptStore)
if err != nil {
log.Fatalf("failed to initialize grpc server: %v", err)
}

View File

@@ -118,3 +118,36 @@ func (s *PostgresInterceptStore) InsertChannel(initialChanID, confirmedChanId ui
initialChanID, confirmedChanId, nodeID, c.String())
return nil
}
func (s *PostgresInterceptStore) GetFeeParamsSettings() ([]*interceptor.OpeningFeeParamsSetting, error) {
rows, err := s.pool.Query(context.Background(), `SELECT validity, params FROM new_channel_params`)
if err != nil {
log.Printf("GetFeeParamsSettings() error: %v", err)
return nil, err
}
var settings []*interceptor.OpeningFeeParamsSetting
for rows.Next() {
var validity int64
var param string
err = rows.Scan(&validity, &param)
if err != nil {
return nil, err
}
var params *interceptor.OpeningFeeParams
err := json.Unmarshal([]byte(param), &params)
if err != nil {
log.Printf("Failed to unmarshal fee param '%v': %v", param, err)
return nil, err
}
duration := time.Second * time.Duration(validity)
settings = append(settings, &interceptor.OpeningFeeParamsSetting{
Validity: duration,
Params: params,
})
}
return settings, nil
}

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"net"
"sort"
"strings"
@@ -16,7 +15,6 @@ import (
"github.com/breez/lspd/basetypes"
"github.com/breez/lspd/btceclegacy"
"github.com/breez/lspd/chain"
"github.com/breez/lspd/cln"
"github.com/breez/lspd/config"
"github.com/breez/lspd/interceptor"
@@ -48,8 +46,6 @@ type server struct {
s *grpc.Server
nodes map[string]*node
store interceptor.InterceptStore
feeStrategy chain.FeeStrategy
feeEstimator chain.FeeEstimator
}
type node struct {
@@ -97,30 +93,20 @@ func (s *server) createOpeningParamsMenu(
) ([]*lspdrpc.OpeningFeeParams, error) {
var menu []*lspdrpc.OpeningFeeParams
// Get a fee estimate.
estimate, err := s.feeEstimator.EstimateFeeRate(ctx, s.feeStrategy)
settings, err := s.store.GetFeeParamsSettings()
if err != nil {
log.Printf("Failed to get fee estimate: %v", err)
return nil, fmt.Errorf("failed to get fee estimate")
log.Printf("Failed to fetch fee params settings: %v", err)
return nil, fmt.Errorf("failed to get opening_fee_params")
}
for _, setting := range node.nodeConfig.FeeParams {
// Multiply the fee estiimate by the configured multiplication factor.
minFeeMsat := estimate.SatPerVByte *
float64(setting.MultiplicationFactor)
// Make sure the fee is not lower than the minimum fee.
minFeeMsat = math.Max(minFeeMsat, float64(setting.MinimumFeeMsat))
validUntil := time.Now().UTC().Add(
time.Second * time.Duration(setting.ValidityDuration),
)
for _, setting := range settings {
validUntil := time.Now().UTC().Add(setting.Validity)
params := &lspdrpc.OpeningFeeParams{
MinMsat: uint64(minFeeMsat),
Proportional: setting.Proportional,
MinMsat: setting.Params.MinMsat,
Proportional: setting.Params.Proportional,
ValidUntil: validUntil.Format(basetypes.TIME_FORMAT),
MaxIdleTime: setting.MaxIdleTime,
MaxClientToSelfDelay: uint32(setting.MaxClientToSelfDelay),
MaxIdleTime: setting.Params.MaxIdleTime,
MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay,
}
promise, err := createPromise(node, params)
@@ -416,8 +402,6 @@ func NewGrpcServer(
address string,
certmagicDomain string,
store interceptor.InterceptStore,
feeStrategy chain.FeeStrategy,
feeEstimator chain.FeeEstimator,
) (*server, error) {
if len(configs) == 0 {
return nil, fmt.Errorf("no nodes supplied")
@@ -477,8 +461,6 @@ func NewGrpcServer(
certmagicDomain: certmagicDomain,
nodes: nodes,
store: store,
feeStrategy: feeStrategy,
feeEstimator: feeEstimator,
}, nil
}