diff --git a/basetypes/time.go b/basetypes/time.go new file mode 100644 index 0000000..6c28c37 --- /dev/null +++ b/basetypes/time.go @@ -0,0 +1,3 @@ +package basetypes + +var TIME_FORMAT string = "2006-01-02T15:04:05.999Z" diff --git a/config/config.go b/config/config.go index 4587f85..662f1dc 100644 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,22 @@ type NodeConfig struct { // The channel can be closed if not used this duration in seconds. MaxInactiveDuration uint64 `json:"maxInactiveDuration,string"` + // The validity duration of an opening params promise. + FeeValidityDuration uint64 `json:"feeValidityDuration,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. + FeeMultiplicationFactor uint64 `json:"feeMultiplicationFactor,string"` + // Set this field to connect to an LND node. Lnd *LndConfig `json:"lnd,omitempty"` diff --git a/main.go b/main.go index a2821a5..c82a25a 100644 --- a/main.go +++ b/main.go @@ -117,7 +117,8 @@ func main() { address := os.Getenv("LISTEN_ADDRESS") certMagicDomain := os.Getenv("CERTMAGIC_DOMAIN") - s, err := NewGrpcServer(nodes, address, certMagicDomain, interceptStore) + cachedEstimator := chain.NewCachedFeeEstimator(feeEstimator) + s, err := NewGrpcServer(nodes, address, certMagicDomain, interceptStore, feeStrategy, cachedEstimator) if err != nil { log.Fatalf("failed to initialize grpc server: %v", err) } diff --git a/server.go b/server.go index ed7e528..d3517e4 100644 --- a/server.go +++ b/server.go @@ -2,15 +2,20 @@ package main import ( "context" + "crypto/sha256" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "log" + "math" "net" "strings" + "time" + "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" @@ -27,12 +32,15 @@ import ( "google.golang.org/grpc/status" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/caddyserver/certmagic" "github.com/lightningnetwork/lnd/lnwire" ) +var TIME_FORMAT string = "2006-01-02T15:04:05.999Z" + type server struct { lspdrpc.ChannelOpenerServer address string @@ -41,6 +49,8 @@ type server struct { s *grpc.Server nodes map[string]*node store interceptor.InterceptStore + feeStrategy chain.FeeStrategy + feeEstimator chain.FeeEstimator } type node struct { @@ -59,6 +69,11 @@ func (s *server) ChannelInformation(ctx context.Context, in *lspdrpc.ChannelInfo return nil, err } + params, err := s.createOpeningParams(ctx, node) + if err != nil { + return nil, err + } + return &lspdrpc.ChannelInformationReply{ Name: node.nodeConfig.Name, Pubkey: node.nodeConfig.NodePubkey, @@ -73,9 +88,104 @@ func (s *server) ChannelInformation(ctx context.Context, in *lspdrpc.ChannelInfo ChannelMinimumFeeMsat: int64(node.nodeConfig.ChannelMinimumFeeMsat), LspPubkey: node.publicKey.SerializeCompressed(), // TODO: Is the publicKey different from the ecies public key? MaxInactiveDuration: int64(node.nodeConfig.MaxInactiveDuration), + OpeningFeeParamsMenu: []*lspdrpc.OpeningFeeParams{params}, }, nil } +func (s *server) createOpeningParams( + ctx context.Context, + node *node, +) (*lspdrpc.OpeningFeeParams, error) { + // Get a fee estimate. + estimate, err := s.feeEstimator.EstimateFeeRate(ctx, s.feeStrategy) + if err != nil { + log.Printf("Failed to get fee estimate: %v", err) + return nil, fmt.Errorf("failed to get fee estimate") + } + + // Multiply the fee estiimate by the configured multiplication factor. + minFeeMsat := estimate.SatPerVByte * + float64(node.nodeConfig.FeeMultiplicationFactor) + + // Make sure the fee is not lower than the minimum fee. + minFeeMsat = math.Max(minFeeMsat, float64(node.nodeConfig.ChannelMinimumFeeMsat)) + + validUntil := time.Now().UTC().Add( + time.Second * time.Duration(node.nodeConfig.FeeValidityDuration), + ) + params := &lspdrpc.OpeningFeeParams{ + MinMsat: uint64(minFeeMsat), + // Proportional is ppm, so divide by 100. + Proportional: uint32(node.nodeConfig.ChannelFeePermyriad / 100), + ValidUntil: validUntil.Format(basetypes.TIME_FORMAT), + // MaxInactiveDuration is in seconds, so divide by 600 for blocks. + MaxIdleTime: uint32(node.nodeConfig.MaxInactiveDuration / 600), + MaxClientToSelfDelay: uint32(node.nodeConfig.MaxClientToSelfDelay), + } + + promise, err := createPromise(node, params) + if err != nil { + log.Printf("Failed to create promise: %v", err) + } + + params.Promise = *promise + return params, nil +} + +func createPromise(node *node, params *lspdrpc.OpeningFeeParams) (*string, error) { + + // First hash all the values in the params in a fixed order. + items := []interface{}{ + params.MinMsat, + params.Proportional, + params.ValidUntil, + params.MaxIdleTime, + params.MaxClientToSelfDelay, + } + blob, err := json.Marshal(items) + if err != nil { + return nil, err + } + hash := sha256.Sum256(blob) + + // Sign the hash with the private key of the LSP id. + sig, err := ecdsa.SignCompact(node.privateKey, hash[:], true) + if err != nil { + return nil, err + } + + // The promise is the hex encoded hash of the signature. + result := sha256.Sum256(sig) + promise := hex.EncodeToString(result[:]) + return &promise, nil +} + +func validateOpeningFeeParams(node *node, params *lspdrpc.OpeningFeeParams) bool { + if params == nil { + return false + } + + promise, err := createPromise(node, params) + if err != nil { + return false + } + + if *promise != params.Promise { + return false + } + + t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil) + if err != nil { + return false + } + + if time.Now().UTC().After(t) { + return false + } + + return true +} + func (s *server) RegisterPayment(ctx context.Context, in *lspdrpc.RegisterPaymentRequest) (*lspdrpc.RegisterPaymentReply, error) { node, err := getNode(ctx) if err != nil { @@ -270,6 +380,8 @@ 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") @@ -329,6 +441,8 @@ func NewGrpcServer( certmagicDomain: certmagicDomain, nodes: nodes, store: store, + feeStrategy: feeStrategy, + feeEstimator: feeEstimator, }, nil }