From bfb25ae4bb3ca193cf0329f696f102c12f653f0f Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 30 Jan 2023 13:47:27 +0100 Subject: [PATCH] use mempool client for fee estimation --- cln_client.go | 34 +++++++++++++++++++++++++--------- intercept.go | 42 +++++++++++++++++++++++++++++++++++++----- itest/lspd_node.go | 2 ++ lightning_client.go | 14 ++++++++------ lnd_client.go | 13 ++++++++++--- main.go | 12 ++++++++++++ sample.env | 2 ++ server.go | 2 +- 8 files changed, 97 insertions(+), 24 deletions(-) diff --git a/cln_client.go b/cln_client.go index 04886ae..f99ef11 100644 --- a/cln_client.go +++ b/cln_client.go @@ -76,25 +76,41 @@ func (c *ClnClient) IsConnected(destination []byte) (bool, error) { func (c *ClnClient) OpenChannel(req *OpenChannelRequest) (*wire.OutPoint, error) { pubkey := hex.EncodeToString(req.Destination) - minConf := uint16(req.TargetConf) - if req.IsZeroConf { - minConf = 0 - } - + minConfs := uint16(req.MinConfs) var minDepth *uint16 if req.IsZeroConf { var d uint16 = 0 minDepth = &d } + var rate *glightning.FeeRate + if req.FeeSatPerVByte != nil { + rate = &glightning.FeeRate{ + Rate: uint(*req.FeeSatPerVByte * 1000), + Style: glightning.PerKb, + } + } else if req.TargetConf != nil { + if *req.TargetConf < 3 { + rate = &glightning.FeeRate{ + Directive: glightning.Urgent, + } + } else if *req.TargetConf < 30 { + rate = &glightning.FeeRate{ + Directive: glightning.Normal, + } + } else { + rate = &glightning.FeeRate{ + Directive: glightning.Slow, + } + } + } + fundResult, err := c.client.FundChannelExt( pubkey, glightning.NewSat(int(req.CapacitySat)), - &glightning.FeeRate{ - Directive: glightning.Slow, - }, + rate, !req.IsPrivate, - &minConf, + &minConfs, glightning.NewMsat(0), minDepth, glightning.NewSat(0), diff --git a/intercept.go b/intercept.go index a8ecc75..1e9dbff 100644 --- a/intercept.go +++ b/intercept.go @@ -2,12 +2,14 @@ package main import ( "bytes" + "context" "encoding/hex" "fmt" "log" "math/big" "time" + "github.com/breez/lspd/chain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" sphinx "github.com/lightningnetwork/lightning-onion" @@ -34,6 +36,7 @@ var ( ) var payHashGroup singleflight.Group +var feeEstimator chain.FeeEstimator type interceptResult struct { action interceptAction @@ -236,12 +239,41 @@ func openChannel(client LightningClient, config *NodeConfig, paymentHash, destin if capacity == config.PublicChannelAmount { capacity++ } + + var targetConf *uint32 + confStr := "" + var feeEstimation *float64 + feeStr := "" + if feeEstimator != nil { + fee, err := feeEstimator.EstimateFeeRate( + context.Background(), + chain.FeeStrategyMinimum, + ) + if err == nil { + feeEstimation = &fee.SatPerVByte + feeStr = fmt.Sprintf("%.5f", *feeEstimation) + } else { + log.Printf("Error estimating chain fee, fallback to target conf: %v", err) + targetConf = &config.TargetConf + confStr = fmt.Sprintf("%v", *targetConf) + } + } + + log.Printf( + "Opening zero conf channel. Destination: %x, capacity: %v, fee: %s, targetConf: %s", + destination, + capacity, + feeStr, + confStr, + ) channelPoint, err := client.OpenChannel(&OpenChannelRequest{ - Destination: destination, - CapacitySat: uint64(capacity), - TargetConf: 6, - IsPrivate: true, - IsZeroConf: true, + Destination: destination, + CapacitySat: uint64(capacity), + MinConfs: 6, + IsPrivate: true, + IsZeroConf: true, + FeeSatPerVByte: feeEstimation, + TargetConf: targetConf, }) if err != nil { log.Printf("client.OpenChannelSync(%x, %v) error: %v", destination, capacity, err) diff --git a/itest/lspd_node.go b/itest/lspd_node.go index 0b47bd0..2749094 100644 --- a/itest/lspd_node.go +++ b/itest/lspd_node.go @@ -110,6 +110,8 @@ func newLspd(h *lntest.TestHarness, name string, lnd *string, cln *string, envEx nodes, fmt.Sprintf("DATABASE_URL=%s", postgresBackend.ConnectionString()), fmt.Sprintf("LISTEN_ADDRESS=%s", grpcAddress), + fmt.Sprintf("USE_MEMPOOL_FEE_ESTIMATION=true"), + fmt.Sprintf("MEMPOOL_API_BASE_URL=https://mempool.space/api/v1/"), } env = append(env, envExt...) diff --git a/lightning_client.go b/lightning_client.go index 7f10124..7260d85 100644 --- a/lightning_client.go +++ b/lightning_client.go @@ -16,12 +16,14 @@ type GetChannelResult struct { } type OpenChannelRequest struct { - Destination []byte - CapacitySat uint64 - MinHtlcMsat uint64 - TargetConf uint32 - IsPrivate bool - IsZeroConf bool + Destination []byte + CapacitySat uint64 + MinHtlcMsat uint64 + IsPrivate bool + IsZeroConf bool + MinConfs uint32 + FeeSatPerVByte *float64 + TargetConf *uint32 } type LightningClient interface { diff --git a/lnd_client.go b/lnd_client.go index 82e4869..d8f087e 100644 --- a/lnd_client.go +++ b/lnd_client.go @@ -96,16 +96,23 @@ func (c *LndClient) IsConnected(destination []byte) (bool, error) { } func (c *LndClient) OpenChannel(req *OpenChannelRequest) (*wire.OutPoint, error) { - channelPoint, err := c.client.OpenChannelSync(context.Background(), &lnrpc.OpenChannelRequest{ + lnReq := &lnrpc.OpenChannelRequest{ NodePubkey: req.Destination, LocalFundingAmount: int64(req.CapacitySat), - TargetConf: int32(req.TargetConf), PushSat: 0, Private: req.IsPrivate, CommitmentType: lnrpc.CommitmentType_ANCHORS, ZeroConf: req.IsZeroConf, - }) + MinConfs: int32(req.MinConfs), + } + if req.FeeSatPerVByte != nil { + lnReq.SatPerVbyte = uint64(*req.FeeSatPerVByte) + } else if req.TargetConf != nil { + lnReq.TargetConf = int32(*req.TargetConf) + } + + channelPoint, err := c.client.OpenChannelSync(context.Background(), lnReq) if err != nil { log.Printf("LND: client.OpenChannelSync(%x, %v) error: %v", req.Destination, req.CapacitySat, err) return nil, fmt.Errorf("LND: OpenChannel() error: %w", err) diff --git a/main.go b/main.go index 4ba23ae..e55c4f3 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "sync" "syscall" + "github.com/breez/lspd/mempool" "github.com/btcsuite/btcd/btcec/v2" ) @@ -33,6 +34,17 @@ func main() { log.Fatalf("need at least one node configured in NODES.") } + useMempool := os.Getenv("USE_MEMPOOL_FEE_ESTIMATION") == "true" + if useMempool { + mempoolUrl := os.Getenv("MEMPOOL_API_BASE_URL") + feeEstimator, err = mempool.NewMempoolClient(mempoolUrl) + if err != nil { + log.Fatalf("failed to initialize mempool client: %v", err) + } + + log.Printf("using mempool api for fee estimation: %v", mempoolUrl) + } + var interceptors []HtlcInterceptor for _, node := range nodes { var interceptor HtlcInterceptor diff --git a/sample.env b/sample.env index 1057b32..5df5352 100644 --- a/sample.env +++ b/sample.env @@ -17,4 +17,6 @@ CHANNELMISMATCH_NOTIFICATION_TO='["Name1 "]' CHANNELMISMATCH_NOTIFICATION_CC='["Name2 ","Name3 "]' CHANNELMISMATCH_NOTIFICATION_FROM="Name4 " +USE_MEMPOOL_FEE_ESTIMATION=true +MEMPOOL_API_BASE_URL=https://mempool.space/api/v1/ NODES='[ { "name": "", "nodePubkey": "", "lspdPrivateKey": "", "token": "", "host": "", "publicChannelAmount": "1000183", "channelAmount": "100000", "channelPrivate": false, "targetConf": "6", "minHtlcMsat": "600", "baseFeeMsat": "1000", "feeRate": "0.000001", "timeLockDelta": "144", "channelFeePermyriad": "40", "channelMinimumFeeMsat": "2000000", "additionalChannelCapacity": "100000", "maxInactiveDuration": "3888000", "lnd": { "address": "", "cert": "", "macaroon": "" } }, { "name": "", "nodePubkey": "", "lspdPrivateKey": "", "token": "", "host": "", "publicChannelAmount": "1000183", "channelAmount": "100000", "channelPrivate": false, "targetConf": "6", "minHtlcMsat": "600", "baseFeeMsat": "1000", "feeRate": "0.000001", "timeLockDelta": "144", "channelFeePermyriad": "40", "channelMinimumFeeMsat": "2000000", "additionalChannelCapacity": "100000", "maxInactiveDuration": "3888000", "cln": { "pluginAddress": "
", "socketPath": "" } } ]' diff --git a/server.go b/server.go index a02ab98..e777784 100644 --- a/server.go +++ b/server.go @@ -127,7 +127,7 @@ func (s *server) OpenChannel(ctx context.Context, in *lspdrpc.OpenChannelRequest outPoint, err = node.client.OpenChannel(&OpenChannelRequest{ CapacitySat: node.nodeConfig.ChannelAmount, Destination: pubkey, - TargetConf: node.nodeConfig.TargetConf, + TargetConf: &node.nodeConfig.TargetConf, MinHtlcMsat: node.nodeConfig.MinHtlcMsat, IsPrivate: node.nodeConfig.ChannelPrivate, })