From c61741baa2e5ea8cb1c784246f3fcf4da682250b Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 14 Aug 2023 13:15:18 +0200 Subject: [PATCH] separate opening fee params logic --- channel_opener_server.go | 164 +++++++------------------------------ main.go | 3 +- shared/opening_service.go | 166 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 135 deletions(-) create mode 100644 shared/opening_service.go diff --git a/channel_opener_server.go b/channel_opener_server.go index 8048841..94e64d5 100644 --- a/channel_opener_server.go +++ b/channel_opener_server.go @@ -2,12 +2,10 @@ package main import ( "context" - "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "log" - "sort" "time" "github.com/breez/lspd/basetypes" @@ -22,7 +20,6 @@ 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/lightningnetwork/lnd/lnwire" @@ -30,14 +27,17 @@ import ( type channelOpenerServer struct { lspdrpc.ChannelOpenerServer - store interceptor.InterceptStore + store interceptor.InterceptStore + openingService shared.OpeningService } func NewChannelOpenerServer( store interceptor.InterceptStore, + openingService shared.OpeningService, ) *channelOpenerServer { return &channelOpenerServer{ - store: store, + store: store, + openingService: openingService, } } @@ -49,11 +49,23 @@ func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrp return nil, err } - params, err := s.createOpeningParamsMenu(ctx, node, token) + params, err := s.openingService.GetFeeParamsMenu(token, node.PrivateKey) if err != nil { return nil, err } + var menu []*lspdrpc.OpeningFeeParams + for _, p := range params { + menu = append(menu, &lspdrpc.OpeningFeeParams{ + MinMsat: p.MinFeeMsat, + Proportional: p.Proportional, + ValidUntil: p.ValidUntil, + MaxIdleTime: p.MinLifetime, + MaxClientToSelfDelay: p.MaxClientToSelfDelay, + Promise: p.Promise, + }) + } + return &lspdrpc.ChannelInformationReply{ Name: node.NodeConfig.Name, Pubkey: node.NodeConfig.NodePubkey, @@ -68,136 +80,10 @@ func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrp 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: params, + OpeningFeeParamsMenu: menu, }, nil } -func (s *channelOpenerServer) createOpeningParamsMenu( - ctx context.Context, - node *shared.Node, - token string, -) ([]*lspdrpc.OpeningFeeParams, error) { - var menu []*lspdrpc.OpeningFeeParams - - settings, err := s.store.GetFeeParamsSettings(token) - if err != nil { - log.Printf("Failed to fetch fee params settings: %v", err) - return nil, fmt.Errorf("failed to get opening_fee_params") - } - - if len(settings) == 0 { - log.Printf("No fee params setings found in the db [token=%v]", token) - } - - for _, setting := range settings { - validUntil := time.Now().UTC().Add(setting.Validity) - params := &lspdrpc.OpeningFeeParams{ - MinMsat: setting.Params.MinMsat, - Proportional: setting.Params.Proportional, - ValidUntil: validUntil.Format(basetypes.TIME_FORMAT), - MaxIdleTime: setting.Params.MaxIdleTime, - MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay, - } - - promise, err := createPromise(node, params) - if err != nil { - log.Printf("Failed to create promise: %v", err) - return nil, err - } - - params.Promise = *promise - menu = append(menu, params) - } - - sort.Slice(menu, func(i, j int) bool { - if menu[i].MinMsat == menu[j].MinMsat { - return menu[i].Proportional < menu[j].Proportional - } - - return menu[i].MinMsat < menu[j].MinMsat - }) - return menu, nil -} - -func paramsHash(params *lspdrpc.OpeningFeeParams) ([]byte, 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 { - log.Printf("paramsHash error: %v", err) - return nil, err - } - hash := sha256.Sum256(blob) - return hash[:], nil -} - -func createPromise(node *shared.Node, params *lspdrpc.OpeningFeeParams) (*string, error) { - hash, err := paramsHash(params) - if err != nil { - return nil, err - } - // Sign the hash with the private key of the LSP id. - sig, err := ecdsa.SignCompact(node.PrivateKey, hash[:], true) - if err != nil { - log.Printf("createPromise: SignCompact error: %v", err) - return nil, err - } - promise := hex.EncodeToString(sig) - return &promise, nil -} - -func verifyPromise(node *shared.Node, params *lspdrpc.OpeningFeeParams) error { - hash, err := paramsHash(params) - if err != nil { - return err - } - sig, err := hex.DecodeString(params.Promise) - if err != nil { - log.Printf("verifyPromise: hex.DecodeString error: %v", err) - return err - } - pub, _, err := ecdsa.RecoverCompact(sig, hash) - if err != nil { - log.Printf("verifyPromise: RecoverCompact(%x) error: %v", sig, err) - return err - } - if !node.PublicKey.IsEqual(pub) { - log.Print("verifyPromise: not signed by us", err) - return fmt.Errorf("invalid promise") - } - return nil -} - -func validateOpeningFeeParams(node *shared.Node, params *lspdrpc.OpeningFeeParams) bool { - if params == nil { - return false - } - - err := verifyPromise(node, params) - if err != nil { - return false - } - - t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil) - if err != nil { - log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", basetypes.TIME_FORMAT, params.ValidUntil, err) - return false - } - - if time.Now().UTC().After(t) { - log.Printf("validateOpeningFeeParams: promise not valid anymore: %v", t) - return false - } - - return true -} - func (s *channelOpenerServer) RegisterPayment( ctx context.Context, in *lspdrpc.RegisterPaymentRequest, @@ -241,7 +127,17 @@ func (s *channelOpenerServer) RegisterPayment( // TODO: Remove this nil check and the else cluase when we enforce all // clients to use opening_fee_params. if pi.OpeningFeeParams != nil { - valid := validateOpeningFeeParams(node, pi.OpeningFeeParams) + valid := s.openingService.ValidateOpeningFeeParams( + &shared.OpeningFeeParams{ + MinFeeMsat: pi.OpeningFeeParams.MinMsat, + Proportional: pi.OpeningFeeParams.Proportional, + ValidUntil: pi.OpeningFeeParams.ValidUntil, + MinLifetime: pi.OpeningFeeParams.MaxIdleTime, + MaxClientToSelfDelay: pi.OpeningFeeParams.MaxClientToSelfDelay, + Promise: pi.OpeningFeeParams.Promise, + }, + node.PublicKey, + ) if !valid { return nil, fmt.Errorf("invalid opening_fee_params") } diff --git a/main.go b/main.go index 5e40180..3e50a4b 100644 --- a/main.go +++ b/main.go @@ -89,6 +89,7 @@ func main() { notificationsStore := postgresql.NewNotificationsStore(pool) notificationService := notifications.NewNotificationService(notificationsStore) + openingService := shared.NewOpeningService(interceptStore, nodesService) var interceptors []interceptor.HtlcInterceptor nodes := nodesService.GetNodes() for _, node := range nodes { @@ -141,7 +142,7 @@ func main() { address := os.Getenv("LISTEN_ADDRESS") certMagicDomain := os.Getenv("CERTMAGIC_DOMAIN") - cs := NewChannelOpenerServer(interceptStore) + cs := NewChannelOpenerServer(interceptStore, openingService) ns := notifications.NewNotificationsServer(notificationsStore) s, err := NewGrpcServer(nodesService, address, certMagicDomain, cs, ns) if err != nil { diff --git a/shared/opening_service.go b/shared/opening_service.go new file mode 100644 index 0000000..fb12e99 --- /dev/null +++ b/shared/opening_service.go @@ -0,0 +1,166 @@ +package shared + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "sort" + "time" + + "github.com/breez/lspd/basetypes" + "github.com/breez/lspd/interceptor" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" +) + +type OpeningService interface { + GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) + ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool +} + +type openingService struct { + store interceptor.InterceptStore + nodesService NodesService +} + +func NewOpeningService( + store interceptor.InterceptStore, + nodesService NodesService, +) OpeningService { + return &openingService{ + store: store, + nodesService: nodesService, + } +} + +type OpeningFeeParams struct { + MinFeeMsat uint64 `json:"min_fee_msat,string"` + Proportional uint32 `json:"proportional"` + ValidUntil string `json:"valid_until"` + MinLifetime uint32 `json:"min_lifetime"` + MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` + Promise string `json:"promise"` +} + +func (s *openingService) GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) { + var menu []*OpeningFeeParams + settings, err := s.store.GetFeeParamsSettings(token) + if err != nil { + log.Printf("Failed to fetch fee params settings: %v", err) + return nil, fmt.Errorf("failed to get opening_fee_params") + } + + if len(settings) == 0 { + log.Printf("No fee params setings found in the db [token=%v]", token) + } + + for _, setting := range settings { + validUntil := time.Now().UTC().Add(setting.Validity) + params := &OpeningFeeParams{ + MinFeeMsat: setting.Params.MinMsat, + Proportional: setting.Params.Proportional, + ValidUntil: validUntil.Format(basetypes.TIME_FORMAT), + MinLifetime: setting.Params.MaxIdleTime, + MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay, + } + + promise, err := createPromise(privateKey, params) + if err != nil { + log.Printf("Failed to create promise: %v", err) + return nil, err + } + + params.Promise = *promise + menu = append(menu, params) + } + + sort.Slice(menu, func(i, j int) bool { + if menu[i].MinFeeMsat == menu[j].MinFeeMsat { + return menu[i].Proportional < menu[j].Proportional + } + + return menu[i].MinFeeMsat < menu[j].MinFeeMsat + }) + return menu, nil +} + +func (s *openingService) ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool { + if params == nil { + return false + } + + err := verifyPromise(publicKey, params) + if err != nil { + return false + } + + t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil) + if err != nil { + log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", basetypes.TIME_FORMAT, params.ValidUntil, err) + return false + } + + if time.Now().UTC().After(t) { + log.Printf("validateOpeningFeeParams: promise not valid anymore: %v", t) + return false + } + + return true +} + +func createPromise(lspPrivateKey *btcec.PrivateKey, params *OpeningFeeParams) (*string, error) { + hash, err := paramsHash(params) + if err != nil { + return nil, err + } + // Sign the hash with the private key of the LSP id. + sig, err := ecdsa.SignCompact(lspPrivateKey, hash[:], true) + if err != nil { + log.Printf("createPromise: SignCompact error: %v", err) + return nil, err + } + promise := hex.EncodeToString(sig) + return &promise, nil +} + +func paramsHash(params *OpeningFeeParams) ([]byte, error) { + // First hash all the values in the params in a fixed order. + items := []interface{}{ + params.MinFeeMsat, + params.Proportional, + params.ValidUntil, + params.MinLifetime, + params.MaxClientToSelfDelay, + } + blob, err := json.Marshal(items) + if err != nil { + log.Printf("paramsHash error: %v", err) + return nil, err + } + hash := sha256.Sum256(blob) + return hash[:], nil +} + +func verifyPromise(lspPublicKey *btcec.PublicKey, params *OpeningFeeParams) error { + hash, err := paramsHash(params) + if err != nil { + return err + } + sig, err := hex.DecodeString(params.Promise) + if err != nil { + log.Printf("verifyPromise: hex.DecodeString error: %v", err) + return err + } + pub, _, err := ecdsa.RecoverCompact(sig, hash) + if err != nil { + log.Printf("verifyPromise: RecoverCompact(%x) error: %v", sig, err) + return err + } + if !lspPublicKey.IsEqual(pub) { + log.Print("verifyPromise: not signed by us", err) + return fmt.Errorf("invalid promise") + } + return nil +}