From 140738fdb1859b46e1bce895d98f15b1ebffb900 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 4 Sep 2023 13:59:35 +0200 Subject: [PATCH] lsps2: fee params timeout based on current fees --- interceptor/intercept_handler.go | 24 ++----- lsps2/intercept_handler.go | 116 ++++++++++++++++--------------- lsps2/intercept_test.go | 24 +++++-- lsps2/mocks.go | 14 +++- main.go | 6 +- shared/opening_service.go | 17 +++++ 6 files changed, 114 insertions(+), 87 deletions(-) diff --git a/interceptor/intercept_handler.go b/interceptor/intercept_handler.go index dbf943af..c635887c 100644 --- a/interceptor/intercept_handler.go +++ b/interceptor/intercept_handler.go @@ -23,7 +23,7 @@ type Interceptor struct { client lightning.Client config *config.NodeConfig store InterceptStore - openingStore shared.OpeningStore + openingService shared.OpeningService feeEstimator chain.FeeEstimator feeStrategy chain.FeeStrategy payHashGroup singleflight.Group @@ -34,7 +34,7 @@ func NewInterceptHandler( client lightning.Client, config *config.NodeConfig, store InterceptStore, - openingStore shared.OpeningStore, + openingService shared.OpeningService, feeEstimator chain.FeeEstimator, feeStrategy chain.FeeStrategy, notificationService *notifications.NotificationService, @@ -43,7 +43,7 @@ func NewInterceptHandler( client: client, config: config, store: store, - openingStore: openingStore, + openingService: openingService, feeEstimator: feeEstimator, feeStrategy: feeStrategy, notificationService: notificationService, @@ -182,7 +182,7 @@ func (i *Interceptor) Intercept(req shared.InterceptRequest) shared.InterceptRes // Make sure the opening_fee_params are not expired. // If they are expired, but the current chain fee is fine, open channel anyway. if time.Now().UTC().After(validUntil) { - if !i.isCurrentChainFeeCheaper(token, params) { + if !i.openingService.IsCurrentChainFeeCheaper(token, params) { log.Printf("Intercepted expired payment registration. Failing payment. payment hash: %x, valid until: %s", paymentHash, params.ValidUntil) return shared.InterceptResult{ Action: shared.INTERCEPT_FAIL_HTLC_WITH_CODE, @@ -330,22 +330,6 @@ func (i *Interceptor) notify(reqPaymentHashStr string, nextHop []byte, isRegiste return nil } -func (i *Interceptor) isCurrentChainFeeCheaper(token string, params *shared.OpeningFeeParams) bool { - settings, err := i.openingStore.GetFeeParamsSettings(token) - if err != nil { - log.Printf("Failed to get fee params settings: %v", err) - return false - } - - for _, setting := range settings { - if setting.Params.MinFeeMsat <= params.MinFeeMsat { - return true - } - } - - return false -} - func (i *Interceptor) openChannel(paymentHash, destination []byte, incomingAmountMsat int64, tag *string) (*wire.OutPoint, error) { capacity := incomingAmountMsat/1000 + i.config.AdditionalChannelCapacity if capacity == i.config.PublicChannelAmount { diff --git a/lsps2/intercept_handler.go b/lsps2/intercept_handler.go index a2a87b9e..3e239996 100644 --- a/lsps2/intercept_handler.go +++ b/lsps2/intercept_handler.go @@ -29,6 +29,7 @@ type InterceptorConfig struct { type Interceptor struct { store Lsps2Store + openingService shared.OpeningService client lightning.Client feeEstimator chain.FeeEstimator config *InterceptorConfig @@ -38,7 +39,6 @@ type Interceptor struct { notRegistered chan string paymentReady chan string paymentTimeout chan string - feeParamsTimeout chan string paymentFailure chan *paymentFailureEvent paymentChanOpened chan *paymentChanOpenedEvent inflightPayments map[string]*paymentState @@ -46,6 +46,7 @@ type Interceptor struct { func NewInterceptHandler( store Lsps2Store, + openingService shared.OpeningService, client lightning.Client, feeEstimator chain.FeeEstimator, config *InterceptorConfig, @@ -55,10 +56,11 @@ func NewInterceptHandler( } return &Interceptor{ - store: store, - client: client, - feeEstimator: feeEstimator, - config: config, + store: store, + openingService: openingService, + client: client, + feeEstimator: feeEstimator, + config: config, // TODO: make sure the chan sizes do not lead to deadlocks. newPart: make(chan *partState, 1000), partAwaitingRegistration: make(chan *awaitingRegistrationEvent, 1000), @@ -66,7 +68,6 @@ func NewInterceptHandler( notRegistered: make(chan string, 1000), paymentReady: make(chan string, 1000), paymentTimeout: make(chan string, 1000), - feeParamsTimeout: make(chan string, 1000), paymentFailure: make(chan *paymentFailureEvent, 1000), paymentChanOpened: make(chan *paymentChanOpenedEvent, 1000), inflightPayments: make(map[string]*paymentState), @@ -74,18 +75,17 @@ func NewInterceptHandler( } type paymentState struct { - id string - fakeScid lightning.ShortChannelID - incomingSumMsat uint64 - outgoingSumMsat uint64 - paymentSizeMsat uint64 - feeMsat uint64 - registration *BuyRegistration - parts map[string]*partState - isFinal bool - timoutChanClosed bool - timeoutChan chan struct{} - isRunningTimeoutListener bool + id string + fakeScid lightning.ShortChannelID + incomingSumMsat uint64 + outgoingSumMsat uint64 + paymentSizeMsat uint64 + feeMsat uint64 + registration *BuyRegistration + parts map[string]*partState + isFinal bool + timoutChanClosed bool + timeoutChan chan struct{} } func (p *paymentState) closeTimeoutChan() { @@ -149,8 +149,6 @@ func (i *Interceptor) Start(ctx context.Context) { i.handlePaymentReady(paymentId) case paymentId := <-i.paymentTimeout: i.handlePaymentTimeout(paymentId) - case paymentId := <-i.feeParamsTimeout: - i.handleFeeParamsTimeout(paymentId) case ev := <-i.paymentFailure: i.handlePaymentFailure(ev.paymentId, ev.code) case ev := <-i.paymentChanOpened: @@ -264,6 +262,7 @@ func (i *Interceptor) handlePartAwaitingRegistration(ev *awaitingRegistrationEve } if part.isFinalized { + // This part is already handled. return } @@ -337,40 +336,6 @@ func (i *Interceptor) handlePartAwaitingRegistration(ev *awaitingRegistrationEve } } - validUntil, err := time.Parse( - lsps0.TIME_FORMAT, - payment.registration.OpeningFeeParams.ValidUntil, - ) - if err != nil { - log.Printf( - "Failed parse validUntil '%s' for %s: %v. Failing part.", - payment.registration.OpeningFeeParams.ValidUntil, - part.req.Scid.ToString(), - err, - ) - i.failPart(payment, part, shared.FAILURE_UNKNOWN_NEXT_PEER) - return - } - - // Expired opening_fee_params are failed back immediately. - if time.Now().After(validUntil) { - i.failPart(payment, part, shared.FAILURE_UNKNOWN_NEXT_PEER) - return - } - - if !payment.isRunningTimeoutListener { - payment.isRunningTimeoutListener = true - go func() { - select { - case <-time.After(time.Until(validUntil)): - // Handle timeout of the opening_fee_params. - i.feeParamsTimeout <- part.req.PaymentId() - case <-payment.timeoutChan: - // Stop listening for timeouts when the payment is ready. - } - }() - } - // Make sure the cltv delta is enough. if int64(part.req.IncomingExpiry)-int64(part.req.OutgoingExpiry) < int64(i.config.TimeLockDelta)+2 { @@ -437,17 +402,56 @@ func (i *Interceptor) handlePaymentReady(paymentId string) { // TODO: Handle notifications. // Stops the timeout listeners payment.closeTimeoutChan() - go i.openChannel(payment) + + go i.ensureChannelOpen(payment) } // Opens a channel to the destination and waits for the channel to become // active. When the channel is active, sends an openChanEvent. Should be run in // a goroutine. -func (i *Interceptor) openChannel(payment *paymentState) { +func (i *Interceptor) ensureChannelOpen(payment *paymentState) { destination, _ := hex.DecodeString(payment.registration.PeerId) if payment.registration.ChannelPoint == nil { + validUntil, err := time.Parse( + lsps0.TIME_FORMAT, + payment.registration.OpeningFeeParams.ValidUntil, + ) + if err != nil { + log.Printf( + "Failed parse validUntil '%s' for paymentId %s: %v", + payment.registration.OpeningFeeParams.ValidUntil, + payment.id, + err, + ) + i.paymentFailure <- &paymentFailureEvent{ + paymentId: payment.id, + code: shared.FAILURE_UNKNOWN_NEXT_PEER, + } + return + } + + // With expired fee params, the current chainfees are checked. If + // they're not cheaper now, fail the payment. + if time.Now().After(validUntil) && + !i.openingService.IsCurrentChainFeeCheaper( + payment.registration.Token, + &payment.registration.OpeningFeeParams, + ) { + log.Printf("LSPS2: Intercepted expired payment registration. "+ + "Failing payment. scid: %s, valid until: %s, destination: %s", + payment.fakeScid.ToString(), + payment.registration.OpeningFeeParams.ValidUntil, + payment.registration.PeerId, + ) + i.paymentFailure <- &paymentFailureEvent{ + paymentId: payment.id, + code: shared.FAILURE_UNKNOWN_NEXT_PEER, + } + return + } + var targetConf *uint32 confStr := "" var feeEstimation *float64 diff --git a/lsps2/intercept_test.go b/lsps2/intercept_test.go index 2f0be019..9c50cdbe 100644 --- a/lsps2/intercept_test.go +++ b/lsps2/intercept_test.go @@ -83,6 +83,12 @@ func defaultFeeEstimator() *mockFeeEstimator { return nil } +func defaultopeningService() *mockOpeningService { + return &mockOpeningService{ + isCurrentChainFeeCheaper: false, + } +} + func defaultConfig() *InterceptorConfig { var minConfs uint32 = 1 return &InterceptorConfig{ @@ -98,10 +104,11 @@ func defaultConfig() *InterceptorConfig { } type interceptP struct { - store *mockLsps2Store - client *mockLightningClient - feeEstimator *mockFeeEstimator - config *InterceptorConfig + store *mockLsps2Store + openingService *mockOpeningService + client *mockLightningClient + feeEstimator *mockFeeEstimator + config *InterceptorConfig } func setupInterceptor( @@ -136,7 +143,14 @@ func setupInterceptor( config = defaultConfig() } - i := NewInterceptHandler(store, client, f, config) + var openingService *mockOpeningService + if p != nil && p.openingService != nil { + openingService = p.openingService + } else { + openingService = defaultopeningService() + } + + i := NewInterceptHandler(store, openingService, client, f, config) go i.Start(ctx) return i } diff --git a/lsps2/mocks.go b/lsps2/mocks.go index 05c039d0..acdac219 100644 --- a/lsps2/mocks.go +++ b/lsps2/mocks.go @@ -29,9 +29,10 @@ func (m *mockNodesService) GetNodes() []*shared.Node { } type mockOpeningService struct { - menu []*shared.OpeningFeeParams - err error - invalid bool + menu []*shared.OpeningFeeParams + err error + invalid bool + isCurrentChainFeeCheaper bool } func (m *mockOpeningService) GetFeeParamsMenu( @@ -48,6 +49,13 @@ func (m *mockOpeningService) ValidateOpeningFeeParams( return !m.invalid } +func (m *mockOpeningService) IsCurrentChainFeeCheaper( + token string, + params *shared.OpeningFeeParams, +) bool { + return m.isCurrentChainFeeCheaper +} + type mockLsps2Store struct { err error req *RegisterBuy diff --git a/main.go b/main.go index 7917265a..d844077a 100644 --- a/main.go +++ b/main.go @@ -113,7 +113,7 @@ func main() { client.StartListeners() fwsync := lnd.NewForwardingHistorySync(client, interceptStore, forwardingStore) - interceptor := interceptor.NewInterceptHandler(client, node.NodeConfig, interceptStore, openingStore, feeEstimator, feeStrategy, notificationService) + interceptor := interceptor.NewInterceptHandler(client, node.NodeConfig, interceptStore, openingService, feeEstimator, feeStrategy, notificationService) htlcInterceptor, err = lnd.NewLndHtlcInterceptor(node.NodeConfig, client, fwsync, interceptor) if err != nil { log.Fatalf("failed to initialize LND interceptor: %v", err) @@ -126,8 +126,8 @@ func main() { log.Fatalf("failed to initialize CLN client: %v", err) } - legacyHandler := interceptor.NewInterceptHandler(client, node.NodeConfig, interceptStore, openingStore, feeEstimator, feeStrategy, notificationService) - lsps2Handler := lsps2.NewInterceptHandler(lsps2Store, client, feeEstimator, &lsps2.InterceptorConfig{ + legacyHandler := interceptor.NewInterceptHandler(client, node.NodeConfig, interceptStore, openingService, feeEstimator, feeStrategy, notificationService) + lsps2Handler := lsps2.NewInterceptHandler(lsps2Store, openingService, client, feeEstimator, &lsps2.InterceptorConfig{ AdditionalChannelCapacitySat: uint64(node.NodeConfig.AdditionalChannelCapacity), MinConfs: node.NodeConfig.MinConfs, TargetConf: node.NodeConfig.TargetConf, diff --git a/shared/opening_service.go b/shared/opening_service.go index 52e71329..f5dd6f87 100644 --- a/shared/opening_service.go +++ b/shared/opening_service.go @@ -17,6 +17,7 @@ import ( type OpeningService interface { GetFeeParamsMenu(token string, privateKey *btcec.PrivateKey) ([]*OpeningFeeParams, error) ValidateOpeningFeeParams(params *OpeningFeeParams, publicKey *btcec.PublicKey) bool + IsCurrentChainFeeCheaper(token string, params *OpeningFeeParams) bool } type openingService struct { @@ -96,6 +97,22 @@ func (s *openingService) ValidateOpeningFeeParams(params *OpeningFeeParams, publ return true } +func (s *openingService) IsCurrentChainFeeCheaper(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.Params.MinFeeMsat <= params.MinFeeMsat { + return true + } + } + + return false +} + func createPromise(lspPrivateKey *btcec.PrivateKey, params *OpeningFeeParams) (*string, error) { hash, err := paramsHash(params) if err != nil {