From 04f18aad88994773522159538593ff6459a19fdc Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 26 Feb 2024 14:13:14 +0100 Subject: [PATCH] min/max payment size in fee params in lsps2 This change splits the logic for fee params generation between lsps2 and the grpc protocol, because it adds two fields to the opening fee params. These fields would otherwise not be able to be added without breaking existing clients, or having to trust clients to pass the right parameters. Making these parameters available only in lsps2 gives a clear upgrade path. --- cmd/lspd/main.go | 7 +- common/opening_service.go | 19 +- common/opening_store.go | 18 +- config/config.go | 8 - itest/lspd_node.go | 28 ++- itest/lsps2_buy_test.go | 6 +- itest/lsps2_get_info_test.go | 6 +- lsps2/intercept_handler.go | 11 +- lsps2/intercept_test.go | 12 +- lsps2/mocks.go | 13 +- lsps2/opening_service.go | 176 ++++++++++++++++++ lsps2/server.go | 43 ++--- lsps2/server_test.go | 45 +++-- lsps2/store.go | 18 +- postgresql/intercept_store.go | 5 + postgresql/lsps2_store.go | 74 +++++++- ...000017_lsps2_min_max_payment_size.down.sql | 26 +++ .../000017_lsps2_min_max_payment_size.up.sql | 35 ++++ postgresql/opening_store.go | 48 +++-- 19 files changed, 468 insertions(+), 130 deletions(-) create mode 100644 lsps2/opening_service.go create mode 100644 postgresql/migrations/000017_lsps2_min_max_payment_size.down.sql create mode 100644 postgresql/migrations/000017_lsps2_min_max_payment_size.up.sql diff --git a/cmd/lspd/main.go b/cmd/lspd/main.go index ac44ed16..029bb0d1 100644 --- a/cmd/lspd/main.go +++ b/cmd/lspd/main.go @@ -106,6 +106,7 @@ func main() { notificationService := notifications.NewNotificationService(notificationsStore) go notificationService.Start(ctx) openingService := common.NewOpeningService(openingStore, nodesService) + lsps2OpeningService := lsps2.NewOpeningService(openingStore) lsps2CleanupService := lsps2.NewCleanupService(lsps2Store) go lsps2CleanupService.Start(ctx) notificationCleanupService := notifications.NewCleanupService(notificationsStore) @@ -149,14 +150,12 @@ func main() { forwardSync := cln.NewForwardSync(node.NodeId, client, historyStore) go forwardSync.ForwardsSynchronize(ctx) legacyHandler := interceptor.NewInterceptHandler(client, node.NodeConfig, interceptStore, historyStore, openingService, feeEstimator, feeStrategy, notificationService) - lsps2Handler := lsps2.NewInterceptHandler(lsps2Store, historyStore, openingService, client, feeEstimator, &lsps2.InterceptorConfig{ + lsps2Handler := lsps2.NewInterceptHandler(lsps2Store, historyStore, lsps2OpeningService, client, feeEstimator, &lsps2.InterceptorConfig{ NodeId: node.NodeId, AdditionalChannelCapacitySat: uint64(node.NodeConfig.AdditionalChannelCapacity), MinConfs: node.NodeConfig.MinConfs, TargetConf: node.NodeConfig.TargetConf, FeeStrategy: feeStrategy, - MinPaymentSizeMsat: node.NodeConfig.MinPaymentSizeMsat, - MaxPaymentSizeMsat: node.NodeConfig.MaxPaymentSizeMsat, TimeLockDelta: node.NodeConfig.TimeLockDelta, HtlcMinimumMsat: node.NodeConfig.MinHtlcMsat, MppTimeout: time.Second * 90, @@ -172,7 +171,7 @@ func main() { go msgClient.Start() msgServer := lsps0.NewServer() protocolServer := lsps0.NewProtocolServer([]uint32{2}) - lsps2Server := lsps2.NewLsps2Server(openingService, nodesService, node, lsps2Store) + lsps2Server := lsps2.NewLsps2Server(lsps2OpeningService, nodesService, node, lsps2Store) lsps0.RegisterProtocolServer(msgServer, protocolServer) lsps2.RegisterLsps2Server(msgServer, lsps2Server) msgClient.WaitStarted() diff --git a/common/opening_service.go b/common/opening_service.go index 4c3da162..30963af6 100644 --- a/common/opening_service.go +++ b/common/opening_service.go @@ -14,6 +14,15 @@ import ( "github.com/btcsuite/btcd/btcec/v2/ecdsa" ) +type OpeningFeeParams struct { + MinFeeMsat uint64 `json:"min_msat,string"` + Proportional uint32 `json:"proportional"` + ValidUntil string `json:"valid_until"` + MinLifetime uint32 `json:"max_idle_time"` + MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` + Promise string `json:"promise"` +} + type OpeningService interface { GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool @@ -50,11 +59,11 @@ func (s *openingService) GetFeeParamsMenu(token string, privateKey *btcec.Privat for _, setting := range settings { validUntil := time.Now().UTC().Add(setting.Validity) params := &OpeningFeeParams{ - MinFeeMsat: setting.Params.MinFeeMsat, - Proportional: setting.Params.Proportional, + MinFeeMsat: setting.MinFeeMsat, + Proportional: setting.Proportional, ValidUntil: validUntil.Format(lsps0.TIME_FORMAT), - MinLifetime: setting.Params.MinLifetime, - MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay, + MinLifetime: setting.MinLifetime, + MaxClientToSelfDelay: setting.MaxClientToSelfDelay, } promise, err := createPromise(privateKey, params) @@ -109,7 +118,7 @@ func (s *openingService) IsCurrentChainFeeCheaper(token string, params *OpeningF } for _, setting := range settings { - if setting.Params.MinFeeMsat <= params.MinFeeMsat { + if setting.MinFeeMsat <= params.MinFeeMsat { return true } } diff --git a/common/opening_store.go b/common/opening_store.go index 175fad02..ac1e225f 100644 --- a/common/opening_store.go +++ b/common/opening_store.go @@ -3,17 +3,13 @@ package common import "time" type OpeningFeeParamsSetting struct { - Validity time.Duration - Params *OpeningFeeParams -} - -type OpeningFeeParams struct { - MinFeeMsat uint64 `json:"min_msat,string"` - Proportional uint32 `json:"proportional"` - ValidUntil string `json:"valid_until"` - MinLifetime uint32 `json:"max_idle_time"` - MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` - Promise string `json:"promise"` + Validity time.Duration + MinFeeMsat uint64 + Proportional uint32 + MinLifetime uint32 + MaxClientToSelfDelay uint32 + MinPaymentSizeMsat uint64 + MaxPaymentSizeMsat uint64 } type OpeningStore interface { diff --git a/config/config.go b/config/config.go index 19157413..d13aa38b 100644 --- a/config/config.go +++ b/config/config.go @@ -84,14 +84,6 @@ type NodeConfig struct { // peer is offline. Defaults to 1m. NotificationTimeout string `json:"notificationTimeout,string"` - // The minimum payment size accepted in LSPS2 forwards that need a channel - // open. - MinPaymentSizeMsat uint64 `json:"minPaymentSizeMsat,string"` - - // The maximum payment size accepted in LSPS2 forwards that need a channel - // open. - MaxPaymentSizeMsat uint64 `json:"maxPaymentSizeMsat,string"` - // Set this field to connect to an LND node. Lnd *LndConfig `json:"lnd,omitempty"` diff --git a/itest/lspd_node.go b/itest/lspd_node.go index 36492ad1..7a66955f 100644 --- a/itest/lspd_node.go +++ b/itest/lspd_node.go @@ -116,8 +116,6 @@ func newLspd(h *lntest.TestHarness, mem *mempoolApi, name string, nodeConfig *co ChannelMinimumFeeMsat: 2000000, AdditionalChannelCapacity: 100000, MaxInactiveDuration: 3888000, - MinPaymentSizeMsat: 600, - MaxPaymentSizeMsat: 4000000000, Lnd: lnd, Cln: cln, } @@ -206,10 +204,18 @@ func (l *lspBase) Initialize() error { _, err = l.postgresBackend.Pool().Exec( l.harness.Ctx, - `INSERT INTO new_channel_params (validity, params, token) + `INSERT INTO new_channel_params ( + token + , validity + , min_fee_msat + , proportional + , min_lifetime + , max_client_to_self_delay + , min_payment_size_msat + , max_payment_size_msat) VALUES - (3600, '{"min_msat": "1000000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello'), - (259200, '{"min_msat": "1100000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello');`, + ('hello', 3600, 1000000, 7500, 4320, 432, 1000, 4000000000), + ('hello', 259200, 1100000, 7500, 4320, 432, 1000, 4000000000);`, ) if err != nil { lntest.PerformCleanup(cleanups) @@ -337,7 +343,15 @@ func SetFeeParams(l LspNode, settings []*FeeParamSetting) error { return nil } - query := `INSERT INTO new_channel_params (validity, params, token) VALUES ` + query := `INSERT INTO new_channel_params ( + token + , validity + , min_fee_msat + , proportional + , min_lifetime + , max_client_to_self_delay + , min_payment_size_msat + , max_payment_size_msat) VALUES ` first := true for _, setting := range settings { if !first { @@ -345,7 +359,7 @@ func SetFeeParams(l LspNode, settings []*FeeParamSetting) error { } query += fmt.Sprintf( - `(%d, '{"min_msat": "%d", "proportional": %d, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello')`, + `('hello', %d, %d, %d, 4320, 432, 1000, 4000000000)`, int64(setting.Validity.Seconds()), setting.MinMsat, setting.Proportional, diff --git a/itest/lsps2_buy_test.go b/itest/lsps2_buy_test.go index e328e41c..00a19143 100644 --- a/itest/lsps2_buy_test.go +++ b/itest/lsps2_buy_test.go @@ -48,13 +48,13 @@ func testLsps2Buy(p *testParams) { ValidUntil string `json:"valid_until"` MinLifetime uint32 `json:"min_lifetime"` MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` + MinPaymentSizeMsat uint64 `json:"min_payment_size_msat,string"` + MaxPaymentSizeMsat uint64 `json:"max_payment_size_msat,string"` Promise string `json:"promise"` } data := new(struct { Result struct { - Menu []params `json:"opening_fee_params_menu"` - MinPayment uint64 `json:"min_payment_size_msat,string"` - MaxPayment uint64 `json:"max_payment_size_msat,string"` + Menu []params `json:"opening_fee_params_menu"` } `json:"result"` }) err := json.Unmarshal(resp.Data[:], &data) diff --git a/itest/lsps2_get_info_test.go b/itest/lsps2_get_info_test.go index f0d70b50..5a6110f9 100644 --- a/itest/lsps2_get_info_test.go +++ b/itest/lsps2_get_info_test.go @@ -62,12 +62,16 @@ func testLsps2GetInfo(p *testParams) { ValidUntil string `json:"valid_until"` MinLifetime uint32 `json:"min_lifetime"` MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` + MinPaymentSizeMsat uint64 `json:"min_payment_size_msat,string"` + MaxPaymentSizeMsat uint64 `json:"max_payment_size_msat,string"` Promise string `json:"promise"` }{} err = json.Unmarshal(result["opening_fee_params_menu"], &menu) lntest.CheckError(p.t, err) - assert.Len(p.t, menu, 2) + if !assert.Len(p.t, menu, 2) { + return + } assert.Equal(p.t, uint64(2000000), menu[0].MinFeeMsat) assert.Equal(p.t, uint64(3000000), menu[1].MinFeeMsat) } diff --git a/lsps2/intercept_handler.go b/lsps2/intercept_handler.go index 14aa4c19..12b6023a 100644 --- a/lsps2/intercept_handler.go +++ b/lsps2/intercept_handler.go @@ -22,8 +22,6 @@ type InterceptorConfig struct { MinConfs *uint32 TargetConf uint32 FeeStrategy chain.FeeStrategy - MinPaymentSizeMsat uint64 - MaxPaymentSizeMsat uint64 TimeLockDelta uint32 HtlcMinimumMsat uint64 MppTimeout time.Duration @@ -32,7 +30,7 @@ type InterceptorConfig struct { type Interceptor struct { store Lsps2Store historyStore history.Store - openingService common.OpeningService + openingService OpeningService client lightning.Client feeEstimator chain.FeeEstimator config *InterceptorConfig @@ -47,7 +45,7 @@ type Interceptor struct { func NewInterceptHandler( store Lsps2Store, historyStore history.Store, - openingService common.OpeningService, + openingService OpeningService, client lightning.Client, feeEstimator chain.FeeEstimator, config *InterceptorConfig, @@ -239,8 +237,8 @@ func (i *Interceptor) processPart(payment *paymentState, part *partState) { payment.paymentSizeMsat = part.req.OutgoingAmountMsat // Make sure the minimum and maximum are not exceeded. - if payment.paymentSizeMsat > i.config.MaxPaymentSizeMsat || - payment.paymentSizeMsat < i.config.MinPaymentSizeMsat { + if payment.paymentSizeMsat > payment.registration.OpeningFeeParams.MaxPaymentSizeMsat || + payment.paymentSizeMsat < payment.registration.OpeningFeeParams.MinPaymentSizeMsat { i.failPart(payment, part, common.FAILURE_UNKNOWN_NEXT_PEER) return } @@ -402,6 +400,7 @@ func (i *Interceptor) ensureChannelOpen(payment *paymentState) { // they're not cheaper now, fail the payment. if time.Now().After(validUntil) && !i.openingService.IsCurrentChainFeeCheaper( + context.TODO(), payment.registration.Token, &payment.registration.OpeningFeeParams, ) { diff --git a/lsps2/intercept_test.go b/lsps2/intercept_test.go index b745aae2..f9488f13 100644 --- a/lsps2/intercept_test.go +++ b/lsps2/intercept_test.go @@ -36,13 +36,15 @@ var defaultChanResult = &lightning.GetChannelResult{ ConfirmedScid: (*lightning.ShortChannelID)(&defaultChannelScid), } -func defaultOpeningFeeParams() common.OpeningFeeParams { - return common.OpeningFeeParams{ +func defaultOpeningFeeParams() OpeningFeeParams { + return OpeningFeeParams{ MinFeeMsat: 1000, Proportional: 1000, ValidUntil: time.Now().UTC().Add(5 * time.Hour).Format(lsps0.TIME_FORMAT), MinLifetime: 1000, MaxClientToSelfDelay: 2016, + MinPaymentSizeMsat: 1000, + MaxPaymentSizeMsat: 4_000_000_000, Promise: "fake", } } @@ -96,8 +98,6 @@ func defaultConfig() *InterceptorConfig { MinConfs: &minConfs, TargetConf: 6, FeeStrategy: chain.FeeStrategyEconomy, - MinPaymentSizeMsat: 1_000, - MaxPaymentSizeMsat: 4_000_000_000, TimeLockDelta: 144, HtlcMinimumMsat: 100, } @@ -295,7 +295,7 @@ func Test_NoMpp_AmtAtMaximum(t *testing.T) { defer cancel() i := setupInterceptor(ctx, nil) - res := i.Intercept(createPart(&part{amt: defaultConfig().MaxPaymentSizeMsat})) + res := i.Intercept(createPart(&part{amt: defaultOpeningFeeParams().MaxPaymentSizeMsat})) assert.Equal(t, common.INTERCEPT_RESUME_WITH_ONION, res.Action) assertEmpty(t, i) } @@ -307,7 +307,7 @@ func Test_NoMpp_AmtAboveMaximum(t *testing.T) { defer cancel() i := setupInterceptor(ctx, nil) - res := i.Intercept(createPart(&part{amt: defaultConfig().MaxPaymentSizeMsat + 1})) + res := i.Intercept(createPart(&part{amt: defaultOpeningFeeParams().MaxPaymentSizeMsat + 1})) assert.Equal(t, common.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) assert.Equal(t, common.FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) assertEmpty(t, i) diff --git a/lsps2/mocks.go b/lsps2/mocks.go index 0736a32b..38cb0e66 100644 --- a/lsps2/mocks.go +++ b/lsps2/mocks.go @@ -31,29 +31,31 @@ func (m *mockNodesService) GetNodes() []*common.Node { } type mockOpeningService struct { - menu []*common.OpeningFeeParams + menu []*OpeningFeeParams err error invalid bool isCurrentChainFeeCheaper bool } func (m *mockOpeningService) GetFeeParamsMenu( + ctx context.Context, token string, privateKey *btcec.PrivateKey, -) ([]*common.OpeningFeeParams, error) { +) ([]*OpeningFeeParams, error) { return m.menu, m.err } func (m *mockOpeningService) ValidateOpeningFeeParams( - params *common.OpeningFeeParams, + params *OpeningFeeParams, publicKey *btcec.PublicKey, ) bool { return !m.invalid } func (m *mockOpeningService) IsCurrentChainFeeCheaper( + ctx context.Context, token string, - params *common.OpeningFeeParams, + params *OpeningFeeParams, ) bool { return m.isCurrentChainFeeCheaper } @@ -96,6 +98,9 @@ func (s *mockLsps2Store) SavePromises(ctx context.Context, req *SavePromises) er func (s *mockLsps2Store) RemoveUnusedExpired(ctx context.Context, before time.Time) error { return nil } +func (s *mockLsps2Store) GetFeeParamsSettings(ctx context.Context, token string) ([]*OpeningFeeParamsSetting, error) { + return nil, ErrNotImplemented +} type mockHistoryStore struct{} diff --git a/lsps2/opening_service.go b/lsps2/opening_service.go new file mode 100644 index 00000000..2f0d7a2f --- /dev/null +++ b/lsps2/opening_service.go @@ -0,0 +1,176 @@ +package lsps2 + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "sort" + "time" + + "github.com/breez/lspd/common" + "github.com/breez/lspd/lsps0" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" +) + +type OpeningService interface { + GetFeeParamsMenu(ctx context.Context, token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) + ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool + IsCurrentChainFeeCheaper(ctx context.Context, token string, params *OpeningFeeParams) bool +} + +type openingService struct { + store common.OpeningStore +} + +func NewOpeningService( + store common.OpeningStore, +) OpeningService { + return &openingService{ + store: store, + } +} + +func (s *openingService) GetFeeParamsMenu(ctx context.Context, 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 settings found in the db [token=%v]", token) + } + + for _, setting := range settings { + validUntil := time.Now().UTC().Add(setting.Validity) + params := &OpeningFeeParams{ + MinFeeMsat: setting.MinFeeMsat, + Proportional: setting.Proportional, + ValidUntil: validUntil.Format(lsps0.TIME_FORMAT), + MinLifetime: setting.MinLifetime, + MaxClientToSelfDelay: setting.MaxClientToSelfDelay, + MinPaymentSizeMsat: setting.MinPaymentSizeMsat, + MaxPaymentSizeMsat: setting.MaxPaymentSizeMsat, + } + + 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(lsps0.TIME_FORMAT, params.ValidUntil) + if err != nil { + log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", lsps0.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 *openingService) IsCurrentChainFeeCheaper(ctx context.Context, token string, params *OpeningFeeParams) bool { + settings, err := s.store.GetFeeParamsSettings(token) + if err != nil { + log.Printf("Failed to get fee params settings: %v", err) + return false + } + + for _, setting := range settings { + if setting.MinFeeMsat <= params.MinFeeMsat { + return true + } + } + + return false +} + +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, + params.MinPaymentSizeMsat, + params.MaxPaymentSizeMsat, + } + 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 +} diff --git a/lsps2/server.go b/lsps2/server.go index 9d0d38fe..2a3939b6 100644 --- a/lsps2/server.go +++ b/lsps2/server.go @@ -26,8 +26,6 @@ type GetInfoRequest struct { type GetInfoResponse struct { OpeningFeeParamsMenu []*OpeningFeeParams `json:"opening_fee_params_menu"` - MinPaymentSizeMsat uint64 `json:"min_payment_size_msat,string"` - MaxPaymentSizeMsat uint64 `json:"max_payment_size_msat,string"` } type OpeningFeeParams struct { @@ -36,6 +34,8 @@ type OpeningFeeParams struct { ValidUntil string `json:"valid_until"` MinLifetime uint32 `json:"min_lifetime"` MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"` + MinPaymentSizeMsat uint64 `json:"min_payment_size_msat,string"` + MaxPaymentSizeMsat uint64 `json:"max_payment_size_msat,string"` Promise string `json:"promise"` } @@ -57,7 +57,7 @@ type Lsps2Server interface { Buy(ctx context.Context, request *BuyRequest) (*BuyResponse, error) } type server struct { - openingService common.OpeningService + openingService OpeningService nodesService common.NodesService node *common.Node store Lsps2Store @@ -71,7 +71,7 @@ const ( ) func NewLsps2Server( - openingService common.OpeningService, + openingService OpeningService, nodesService common.NodesService, node *common.Node, store Lsps2Store, @@ -123,7 +123,7 @@ func (s *server) GetInfo( return nil, status.New(codes.Code(2), "unrecognized_or_stale_token").Err() } - m, err := s.openingService.GetFeeParamsMenu(*request.Token, node.PrivateKey) + menu, err := s.openingService.GetFeeParamsMenu(ctx, *request.Token, node.PrivateKey) if err == common.ErrNodeNotFound { return nil, status.New(codes.Code(2), "unrecognized_or_stale_token").Err() } @@ -133,29 +133,16 @@ func (s *server) GetInfo( } err = s.store.SavePromises(ctx, &SavePromises{ - Menu: m, + Menu: menu, Token: *request.Token, }) if err != nil { - log.Printf("Lsps2Server.GetInfo: store.SavePromises(%+v, %s) err: %v", m, *request.Token, err) + log.Printf("Lsps2Server.GetInfo: store.SavePromises(%+v, %s) err: %v", menu, *request.Token, err) return nil, status.New(codes.InternalError, "internal error").Err() } - menu := []*OpeningFeeParams{} - for _, p := range m { - menu = append(menu, &OpeningFeeParams{ - MinFeeMsat: p.MinFeeMsat, - Proportional: p.Proportional, - ValidUntil: p.ValidUntil, - MinLifetime: p.MinLifetime, - MaxClientToSelfDelay: p.MaxClientToSelfDelay, - Promise: p.Promise, - }) - } return &GetInfoResponse{ OpeningFeeParamsMenu: menu, - MinPaymentSizeMsat: node.NodeConfig.MinPaymentSizeMsat, - MaxPaymentSizeMsat: node.NodeConfig.MaxPaymentSizeMsat, }, nil } @@ -167,16 +154,8 @@ func (s *server) Buy( return nil, status.New(codes.Code(1), "unsupported_version").Err() } - params := &common.OpeningFeeParams{ - MinFeeMsat: request.OpeningFeeParams.MinFeeMsat, - Proportional: request.OpeningFeeParams.Proportional, - ValidUntil: request.OpeningFeeParams.ValidUntil, - MinLifetime: request.OpeningFeeParams.MinLifetime, - MaxClientToSelfDelay: request.OpeningFeeParams.MaxClientToSelfDelay, - Promise: request.OpeningFeeParams.Promise, - } paramsValid := s.openingService.ValidateOpeningFeeParams( - params, + &request.OpeningFeeParams, s.node.PublicKey, ) if !paramsValid { @@ -188,10 +167,10 @@ func (s *server) Buy( mode = OpeningMode_NoMppVarInvoice } else { mode = OpeningMode_MppFixedInvoice - if *request.PaymentSizeMsat < s.node.NodeConfig.MinPaymentSizeMsat { + if *request.PaymentSizeMsat < request.OpeningFeeParams.MinPaymentSizeMsat { return nil, status.New(codes.Code(3), "payment_size_too_small").Err() } - if *request.PaymentSizeMsat > s.node.NodeConfig.MaxPaymentSizeMsat { + if *request.PaymentSizeMsat > request.OpeningFeeParams.MaxPaymentSizeMsat { return nil, status.New(codes.Code(4), "payment_size_too_large").Err() } @@ -245,7 +224,7 @@ func (s *server) Buy( LspId: s.node.NodeConfig.NodePubkey, PeerId: peerId, Scid: *scid, - OpeningFeeParams: *params, + OpeningFeeParams: request.OpeningFeeParams, PaymentSizeMsat: request.PaymentSizeMsat, Mode: mode, }) diff --git a/lsps2/server_test.go b/lsps2/server_test.go index be60f60c..2c7b6716 100644 --- a/lsps2/server_test.go +++ b/lsps2/server_test.go @@ -16,9 +16,7 @@ var token = "blah" var node = func() *common.Node { return &common.Node{ NodeConfig: &config.NodeConfig{ - MinPaymentSizeMsat: 1000, - MaxPaymentSizeMsat: 10000, - TimeLockDelta: 143, + TimeLockDelta: 143, }, } } @@ -58,7 +56,7 @@ func Test_GetInfo_InvalidToken(t *testing.T) { func Test_GetInfo_EmptyMenu(t *testing.T) { node := node() n := &mockNodesService{node: node} - o := &mockOpeningService{menu: []*common.OpeningFeeParams{}} + o := &mockOpeningService{menu: []*OpeningFeeParams{}} st := &mockLsps2Store{} s := NewLsps2Server(o, n, node, st) resp, err := s.GetInfo(context.Background(), &GetInfoRequest{ @@ -68,28 +66,30 @@ func Test_GetInfo_EmptyMenu(t *testing.T) { assert.Nil(t, err) assert.Equal(t, []*OpeningFeeParams{}, resp.OpeningFeeParamsMenu) - assert.Equal(t, node.NodeConfig.MinPaymentSizeMsat, resp.MinPaymentSizeMsat) - assert.Equal(t, node.NodeConfig.MaxPaymentSizeMsat, resp.MaxPaymentSizeMsat) } func Test_GetInfo_PopulatedMenu_Ordered(t *testing.T) { node := node() n := &mockNodesService{node: node} - o := &mockOpeningService{menu: []*common.OpeningFeeParams{ + o := &mockOpeningService{menu: []*OpeningFeeParams{ { MinFeeMsat: 1, Proportional: 2, ValidUntil: "a", MinLifetime: 3, MaxClientToSelfDelay: 4, + MinPaymentSizeMsat: 5, + MaxPaymentSizeMsat: 6, Promise: "b", }, { - MinFeeMsat: 5, - Proportional: 6, + MinFeeMsat: 7, + Proportional: 8, ValidUntil: "c", - MinLifetime: 7, - MaxClientToSelfDelay: 8, + MinLifetime: 9, + MaxClientToSelfDelay: 10, + MinPaymentSizeMsat: 11, + MaxPaymentSizeMsat: 12, Promise: "d", }, }} @@ -108,17 +108,18 @@ func Test_GetInfo_PopulatedMenu_Ordered(t *testing.T) { assert.Equal(t, "a", resp.OpeningFeeParamsMenu[0].ValidUntil) assert.Equal(t, uint32(3), resp.OpeningFeeParamsMenu[0].MinLifetime) assert.Equal(t, uint32(4), resp.OpeningFeeParamsMenu[0].MaxClientToSelfDelay) + assert.Equal(t, uint64(5), resp.OpeningFeeParamsMenu[0].MinPaymentSizeMsat) + assert.Equal(t, uint64(6), resp.OpeningFeeParamsMenu[0].MaxPaymentSizeMsat) assert.Equal(t, "b", resp.OpeningFeeParamsMenu[0].Promise) - assert.Equal(t, uint64(5), resp.OpeningFeeParamsMenu[1].MinFeeMsat) - assert.Equal(t, uint32(6), resp.OpeningFeeParamsMenu[1].Proportional) + assert.Equal(t, uint64(7), resp.OpeningFeeParamsMenu[1].MinFeeMsat) + assert.Equal(t, uint32(8), resp.OpeningFeeParamsMenu[1].Proportional) assert.Equal(t, "c", resp.OpeningFeeParamsMenu[1].ValidUntil) - assert.Equal(t, uint32(7), resp.OpeningFeeParamsMenu[1].MinLifetime) - assert.Equal(t, uint32(8), resp.OpeningFeeParamsMenu[1].MaxClientToSelfDelay) + assert.Equal(t, uint32(9), resp.OpeningFeeParamsMenu[1].MinLifetime) + assert.Equal(t, uint32(10), resp.OpeningFeeParamsMenu[1].MaxClientToSelfDelay) + assert.Equal(t, uint64(11), resp.OpeningFeeParamsMenu[1].MinPaymentSizeMsat) + assert.Equal(t, uint64(12), resp.OpeningFeeParamsMenu[1].MaxPaymentSizeMsat) assert.Equal(t, "d", resp.OpeningFeeParamsMenu[1].Promise) - - assert.Equal(t, node.NodeConfig.MinPaymentSizeMsat, resp.MinPaymentSizeMsat) - assert.Equal(t, node.NodeConfig.MaxPaymentSizeMsat, resp.MaxPaymentSizeMsat) } func Test_Buy_UnsupportedVersion(t *testing.T) { @@ -151,6 +152,8 @@ func Test_Buy_InvalidFeeParams(t *testing.T) { ValidUntil: "2023-08-18T13:39:00.000Z", MinLifetime: 3, MaxClientToSelfDelay: 4, + MinPaymentSizeMsat: 5, + MaxPaymentSizeMsat: 6, Promise: "fake", }, }) @@ -241,6 +244,8 @@ func Test_Buy_PaymentSize(t *testing.T) { ValidUntil: "2023-08-18T13:39:00.000Z", MinLifetime: 3, MaxClientToSelfDelay: 4, + MinPaymentSizeMsat: 5, + MaxPaymentSizeMsat: 6, Promise: "fake", }, PaymentSizeMsat: &c.paymentSize, @@ -276,6 +281,8 @@ func Test_Buy_Registered(t *testing.T) { ValidUntil: "2023-08-18T13:39:00.000Z", MinLifetime: 3, MaxClientToSelfDelay: 4, + MinPaymentSizeMsat: 5, + MaxPaymentSizeMsat: 6, Promise: "fake", }, PaymentSizeMsat: &paymentSize, @@ -292,6 +299,8 @@ func Test_Buy_Registered(t *testing.T) { assert.Equal(t, "2023-08-18T13:39:00.000Z", st.req.OpeningFeeParams.ValidUntil) assert.Equal(t, uint32(3), st.req.OpeningFeeParams.MinLifetime) assert.Equal(t, uint32(4), st.req.OpeningFeeParams.MaxClientToSelfDelay) + assert.Equal(t, uint64(5), st.req.OpeningFeeParams.MinPaymentSizeMsat) + assert.Equal(t, uint64(6), st.req.OpeningFeeParams.MaxPaymentSizeMsat) assert.Equal(t, "fake", st.req.OpeningFeeParams.Promise) assert.Equal(t, st.req.Scid.ToString(), resp.JitChannelScid) diff --git a/lsps2/store.go b/lsps2/store.go index d202f736..4685248d 100644 --- a/lsps2/store.go +++ b/lsps2/store.go @@ -7,14 +7,13 @@ import ( "time" "github.com/GoWebProd/uuid7" - "github.com/breez/lspd/common" "github.com/breez/lspd/lightning" "github.com/breez/lspd/lsps0" "github.com/btcsuite/btcd/wire" ) type SavePromises struct { - Menu []*common.OpeningFeeParams + Menu []*OpeningFeeParams Token string } @@ -22,7 +21,7 @@ type RegisterBuy struct { LspId string PeerId string Scid lightning.ShortChannelID - OpeningFeeParams common.OpeningFeeParams + OpeningFeeParams OpeningFeeParams PaymentSizeMsat *uint64 Mode OpeningMode } @@ -33,13 +32,23 @@ type BuyRegistration struct { PeerId string // TODO: Make peerId in the registration a byte array. Token string Scid lightning.ShortChannelID - OpeningFeeParams common.OpeningFeeParams + OpeningFeeParams OpeningFeeParams PaymentSizeMsat *uint64 Mode OpeningMode ChannelPoint *wire.OutPoint IsComplete bool } +type OpeningFeeParamsSetting struct { + Validity time.Duration + MinFeeMsat uint64 + Proportional uint32 + MinLifetime uint32 + MaxClientToSelfDelay uint32 + MinPaymentSizeMsat uint64 + MaxPaymentSizeMsat uint64 +} + func (b *BuyRegistration) IsExpired() bool { t, err := time.Parse(lsps0.TIME_FORMAT, b.OpeningFeeParams.ValidUntil) if err != nil { @@ -65,6 +74,7 @@ var ErrScidExists = errors.New("scid exists") var ErrNotFound = errors.New("not found") type Lsps2Store interface { + GetFeeParamsSettings(ctx context.Context, token string) ([]*OpeningFeeParamsSetting, error) SavePromises(ctx context.Context, req *SavePromises) error RegisterBuy(ctx context.Context, req *RegisterBuy) error GetBuyRegistration(ctx context.Context, scid lightning.ShortChannelID) (*BuyRegistration, error) diff --git a/postgresql/intercept_store.go b/postgresql/intercept_store.go index cac0e53d..4cf73ca8 100644 --- a/postgresql/intercept_store.go +++ b/postgresql/intercept_store.go @@ -14,6 +14,11 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) +type extendedParams struct { + Token string `json:"token"` + Params common.OpeningFeeParams `json:"fees_params"` +} + type PostgresInterceptStore struct { pool *pgxpool.Pool } diff --git a/postgresql/lsps2_store.go b/postgresql/lsps2_store.go index 204fb52e..d5a1254d 100644 --- a/postgresql/lsps2_store.go +++ b/postgresql/lsps2_store.go @@ -8,7 +8,6 @@ import ( "time" "github.com/GoWebProd/uuid7" - "github.com/breez/lspd/common" "github.com/breez/lspd/lightning" "github.com/breez/lspd/lsps0" "github.com/breez/lspd/lsps2" @@ -59,10 +58,12 @@ func (s *Lsps2Store) RegisterBuy( , params_valid_until , params_min_lifetime , params_max_client_to_self_delay + , params_min_payment_size_msat + , params_max_payment_size_msat , params_promise , token ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`, uuid, req.LspId, req.PeerId, @@ -74,6 +75,8 @@ func (s *Lsps2Store) RegisterBuy( req.OpeningFeeParams.ValidUntil, req.OpeningFeeParams.MinLifetime, req.OpeningFeeParams.MaxClientToSelfDelay, + int64(req.OpeningFeeParams.MinPaymentSizeMsat), + int64(req.OpeningFeeParams.MaxPaymentSizeMsat), req.OpeningFeeParams.Promise, token, ) @@ -102,6 +105,8 @@ func (s *Lsps2Store) GetBuyRegistration(ctx context.Context, scid lightning.Shor , r.params_valid_until , r.params_min_lifetime , r.params_max_client_to_self_delay + , r.params_min_payment_size_msat + , r.params_max_payment_size_msat , r.params_promise , r.token , c.funding_tx_id @@ -123,6 +128,8 @@ func (s *Lsps2Store) GetBuyRegistration(ctx context.Context, scid lightning.Shor var db_params_valid_until string var db_params_min_lifetime uint32 var db_params_max_client_to_self_delay uint32 + var db_params_min_payment_size_msat int64 + var db_params_max_payment_size_msat int64 var db_params_promise string var db_token string var db_funding_tx_id *[]byte @@ -140,6 +147,8 @@ func (s *Lsps2Store) GetBuyRegistration(ctx context.Context, scid lightning.Shor &db_params_valid_until, &db_params_min_lifetime, &db_params_max_client_to_self_delay, + &db_params_min_payment_size_msat, + &db_params_max_payment_size_msat, &db_params_promise, &db_token, &db_funding_tx_id, @@ -177,12 +186,14 @@ func (s *Lsps2Store) GetBuyRegistration(ctx context.Context, scid lightning.Shor LspId: db_lsp_id, PeerId: db_peer_id, Scid: lightning.ShortChannelID(uint64(db_scid)), - OpeningFeeParams: common.OpeningFeeParams{ + OpeningFeeParams: lsps2.OpeningFeeParams{ MinFeeMsat: uint64(db_params_min_fee_msat), Proportional: db_params_proportional, ValidUntil: db_params_valid_until, MinLifetime: db_params_min_lifetime, MaxClientToSelfDelay: db_params_max_client_to_self_delay, + MinPaymentSizeMsat: uint64(db_params_min_payment_size_msat), + MaxPaymentSizeMsat: uint64(db_params_max_payment_size_msat), Promise: db_params_promise, }, PaymentSizeMsat: paymentSizeMsat, @@ -307,3 +318,60 @@ func (s *Lsps2Store) RemoveUnusedExpired( return tx.Commit(ctx) } + +func (s *Lsps2Store) GetFeeParamsSettings( + ctx context.Context, + token string, +) ([]*lsps2.OpeningFeeParamsSetting, error) { + rows, err := s.pool.Query(context.Background(), + `SELECT validity + , min_fee_msat + , proportional + , min_lifetime + , max_client_to_self_delay + , min_payment_size_msat + , max_payment_size_msat + FROM lsps2.opening_settings + WHERE token=$1 AND validity>0`, token) + if err != nil { + log.Printf("GetFeeParamsSettings(%v) error: %v", token, err) + return nil, err + } + defer rows.Close() + + var settings []*lsps2.OpeningFeeParamsSetting + for rows.Next() { + var validity int64 + var min_fee_msat int64 + var proportional uint32 + var min_lifetime uint32 + var max_client_to_self_delay uint32 + var min_payment_size_msat int64 + var max_payment_size_msat int64 + err = rows.Scan( + &validity, + &min_fee_msat, + &proportional, + &min_lifetime, + &max_client_to_self_delay, + &min_payment_size_msat, + &max_payment_size_msat, + ) + if err != nil { + return nil, err + } + + duration := time.Second * time.Duration(validity) + settings = append(settings, &lsps2.OpeningFeeParamsSetting{ + Validity: duration, + MinFeeMsat: uint64(min_fee_msat), + Proportional: proportional, + MinLifetime: min_lifetime, + MaxClientToSelfDelay: max_client_to_self_delay, + MinPaymentSizeMsat: uint64(min_payment_size_msat), + MaxPaymentSizeMsat: uint64(max_payment_size_msat), + }) + } + + return settings, nil +} diff --git a/postgresql/migrations/000017_lsps2_min_max_payment_size.down.sql b/postgresql/migrations/000017_lsps2_min_max_payment_size.down.sql new file mode 100644 index 00000000..179c1e2f --- /dev/null +++ b/postgresql/migrations/000017_lsps2_min_max_payment_size.down.sql @@ -0,0 +1,26 @@ +ALTER TABLE lsps2.buy_registrations +DROP COLUMN params_min_payment_size_msat, +DROP COLUMN params_max_payment_size_msat; + +ALTER TABLE public.new_channel_params +ADD COLUMN params jsonb NULL; + +UPDATE public.new_channel_params a +SET params = to_jsonb(c) +FROM ( + SELECT b.min_fee_msat::varchar AS min_msat + , b.proportional + , b.min_lifetime AS max_idle_time + , b.max_client_to_self_delay + FROM public.new_channel_params b + WHERE a.token = b.token AND a.validity = b.validity +) c; + +ALTER TABLE public.new_channel_params +DROP COLUMN min_fee_msat, +DROP COLUMN proportional, +DROP COLUMN min_lifetime, +DROP COLUMN max_client_to_self_delay, +DROP COLUMN min_payment_size_msat, +DROP COLUMN max_payment_size_msat +ALTER COLUMN params SET NOT NULL; \ No newline at end of file diff --git a/postgresql/migrations/000017_lsps2_min_max_payment_size.up.sql b/postgresql/migrations/000017_lsps2_min_max_payment_size.up.sql new file mode 100644 index 00000000..d3d49bb5 --- /dev/null +++ b/postgresql/migrations/000017_lsps2_min_max_payment_size.up.sql @@ -0,0 +1,35 @@ +ALTER TABLE lsps2.buy_registrations +ADD COLUMN params_min_payment_size_msat bigint NULL, +ADD COLUMN params_max_payment_size_msat bigint NULL; + +UPDATE lsps2.buy_registrations +SET params_min_payment_size_msat = 0, params_max_payment_size_msat = 0; + +ALTER TABLE lsps2.buy_registrations +ALTER COLUMN params_min_payment_size_msat SET NOT NULL, +ALTER COLUMN params_max_payment_size_msat SET NOT NULL; + +ALTER TABLE public.new_channel_params +ADD COLUMN min_fee_msat bigint NULL, +ADD COLUMN proportional bigint NULL, +ADD COLUMN min_lifetime bigint NULL, +ADD COLUMN max_client_to_self_delay bigint NULL, +ADD COLUMN min_payment_size_msat bigint NULL, +ADD COLUMN max_payment_size_msat bigint NULL; + +UPDATE public.new_channel_params +SET min_fee_msat = (params::json->>'min_msat')::bigint +, proportional = (params::json->>'proportional')::bigint +, min_lifetime = (params::json->>'max_idle_time')::bigint +, max_client_to_self_delay = (params::json->>'max_client_to_self_delay')::bigint +, min_payment_size_msat = 1000 +, max_payment_size_msat = 4000000000; + +ALTER TABLE public.new_channel_params +ALTER COLUMN min_fee_msat SET NOT NULL, +ALTER COLUMN proportional SET NOT NULL, +ALTER COLUMN min_lifetime SET NOT NULL, +ALTER COLUMN max_client_to_self_delay SET NOT NULL, +ALTER COLUMN min_payment_size_msat SET NOT NULL, +ALTER COLUMN max_payment_size_msat SET NOT NULL, +DROP COLUMN params; diff --git a/postgresql/opening_store.go b/postgresql/opening_store.go index 4a93f6c5..6fd1706f 100644 --- a/postgresql/opening_store.go +++ b/postgresql/opening_store.go @@ -2,7 +2,6 @@ package postgresql import ( "context" - "encoding/json" "log" "time" @@ -10,11 +9,6 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -type extendedParams struct { - Token string `json:"token"` - Params common.OpeningFeeParams `json:"fees_params"` -} - type PostgresOpeningStore struct { pool *pgxpool.Pool } @@ -25,7 +19,14 @@ func NewPostgresOpeningStore(pool *pgxpool.Pool) *PostgresOpeningStore { func (s *PostgresOpeningStore) GetFeeParamsSettings(token string) ([]*common.OpeningFeeParamsSetting, error) { rows, err := s.pool.Query(context.Background(), - `SELECT validity, params FROM new_channel_params WHERE token=$1 AND validity>0`, token) + `SELECT validity + , min_fee_msat + , proportional + , min_lifetime + , max_client_to_self_delay + , min_payment_size_msat + , max_payment_size_msat + FROM new_channel_params WHERE token=$1 AND validity>0`, token) if err != nil { log.Printf("GetFeeParamsSettings(%v) error: %v", token, err) return nil, err @@ -35,23 +36,34 @@ func (s *PostgresOpeningStore) GetFeeParamsSettings(token string) ([]*common.Ope var settings []*common.OpeningFeeParamsSetting for rows.Next() { var validity int64 - var param string - err = rows.Scan(&validity, ¶m) - if err != nil { - return nil, err - } - - var params *common.OpeningFeeParams - err := json.Unmarshal([]byte(param), ¶ms) + var min_fee_msat int64 + var proportional uint32 + var min_lifetime uint32 + var max_client_to_self_delay uint32 + var min_payment_size_msat int64 + var max_payment_size_msat int64 + err = rows.Scan( + &validity, + &min_fee_msat, + &proportional, + &min_lifetime, + &max_client_to_self_delay, + &min_payment_size_msat, + &max_payment_size_msat, + ) 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, &common.OpeningFeeParamsSetting{ - Validity: duration, - Params: params, + Validity: duration, + MinFeeMsat: uint64(min_fee_msat), + Proportional: proportional, + MinLifetime: min_lifetime, + MaxClientToSelfDelay: max_client_to_self_delay, + MinPaymentSizeMsat: uint64(min_payment_size_msat), + MaxPaymentSizeMsat: uint64(max_payment_size_msat), }) }