mirror of
https://github.com/aljazceru/lspd.git
synced 2025-12-24 09:14:21 +01:00
use fee params from database
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
3
main.go
3
main.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, ¶m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var params *interceptor.OpeningFeeParams
|
||||
err := json.Unmarshal([]byte(param), ¶ms)
|
||||
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
|
||||
}
|
||||
|
||||
36
server.go
36
server.go
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user