From b26330313f5a42e77699a4b2883166b96f6795e4 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 22 Jun 2022 12:38:07 +0200 Subject: [PATCH 1/6] extract resolveViaDb --- invoiceregistry.go | 82 ++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/invoiceregistry.go b/invoiceregistry.go index 6820661..f65be96 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -721,6 +721,42 @@ type registryHtlc struct { resolve func(HtlcResolution) } +func (i *InvoiceRegistry) resolveViaDb(ctx context.Context, + h *registryHtlc) (bool, error) { + + dbInvoice, htlcs, err := i.cdb.Get(ctx, h.rHash) + switch { + case err == types.ErrInvoiceNotFound: + return false, nil + + case err != nil: + return false, err + } + + i.logger.Debugw("Loaded settled invoice from db", "hash", h.rHash) + + // Handle replays to a settled invoice. + if len(htlcs) == 0 { + return false, errors.New("unexpected unsettled invoice") + } + + // If this htlc was used for settling the invoice, + // resolve to settled again. + if _, ok := htlcs[h.circuitKey]; ok { + h.resolve(NewSettleResolution( + dbInvoice.PaymentPreimage, + ResultReplayToSettled, + )) + + return true, nil + } + + // Otherwise fail the htlc. + h.resolve(NewFailResolution(ResultInvoiceNotOpen)) + + return true, nil +} + func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { // Always require an mpp record. mpp := h.payload.MultiPath() @@ -735,50 +771,18 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { state, ok := i.invoices[h.rHash] if !ok { // Invoice is not present in memory. Do a db lookup to see if this - // happens to be a previously settled invoice. - dbInvoice, htlcs, err := i.cdb.Get(ctx, h.rHash) - switch { - case err == types.ErrInvoiceNotFound: - // If the invoice was not found, return a failure - // resolution with an invoice not found result. - h.resolve(NewFailResolution(ResultInvoiceNotFound)) - - return nil - - case err != nil: + // happens to be a previously settled invoice and resolve the htlc if + // possible. + resolved, err := i.resolveViaDb(ctx, h) + if err != nil { return err } - - // Fail htlcs paying to unpayable invoices (expired or cancelled). - if dbInvoice.State != persistence.InvoiceStateSettleRequested && - dbInvoice.State != persistence.InvoiceStateSettled { - + if !resolved { + // If the invoice was not found, return a failure + // resolution with an invoice not found result. h.resolve(NewFailResolution(ResultInvoiceNotFound)) - - return nil } - i.logger.Debugw("Loaded settled invoice from db", "hash", h.rHash) - - // Handle replays to a settled invoice. - if len(htlcs) == 0 { - return errors.New("unexpected unsettled invoice") - } - - // If this htlc was used for settling the invoice, - // resolve to settled again. - if _, ok := htlcs[h.circuitKey]; ok { - h.resolve(NewSettleResolution( - dbInvoice.PaymentPreimage, - ResultReplayToSettled, - )) - - return nil - } - - // Otherwise fail the htlc. - h.resolve(NewFailResolution(ResultInvoiceNotOpen)) - return nil } From 28732f1c3eb72ea7fe5035db8349fa75d4969865 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 24 Jun 2022 15:42:22 +0200 Subject: [PATCH 2/6] remove unused config struct --- config.go | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 config.go diff --git a/config.go b/config.go deleted file mode 100644 index a72d810..0000000 --- a/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package lnmux - -import "github.com/bottlepay/lnmux/lnd" - -type Config struct { - Lnd lnd.LndClient -} From e52da45a428d2033317f7b074ed890a884f368f5 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 24 Jun 2022 16:06:47 +0200 Subject: [PATCH 3/6] global auto settle flag For stateless invoices, there is no option anymore to store this value per invoice. --- cmd/lnmuxd/config.go | 3 + cmd/lnmuxd/lnmux_proto/lnmux.pb.go | 153 +++++++++----------- cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go | 2 - cmd/lnmuxd/lnmux_proto/lnmux.proto | 5 - cmd/lnmuxd/run.go | 1 + cmd/lnmuxd/server.go | 1 - go.mod | 2 +- invoiceregistry.go | 9 +- invoiceregistry_test.go | 16 +- persistence/migrations/1_initial.up.sql | 4 +- persistence/pg.go | 8 +- 11 files changed, 90 insertions(+), 114 deletions(-) diff --git a/cmd/lnmuxd/config.go b/cmd/lnmuxd/config.go index e225e0d..a92263e 100644 --- a/cmd/lnmuxd/config.go +++ b/cmd/lnmuxd/config.go @@ -19,6 +19,9 @@ type Config struct { // IdentityKey is the private key that is used to sign invoices. IdentityKey string `yaml:"identityKey"` + + // AutoSettle indicates that payments should be accepted automatically. + AutoSettle bool `yaml:"autoSettle"` } func (c *Config) GetIdentityKey() ([32]byte, error) { diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.pb.go b/cmd/lnmuxd/lnmux_proto/lnmux.pb.go index 6b7df9e..9b1d330 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.pb.go +++ b/cmd/lnmuxd/lnmux_proto/lnmux.pb.go @@ -136,10 +136,6 @@ type AddInvoiceRequest struct { Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` DescriptionHash []byte `protobuf:"bytes,3,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"` ExpirySecs int64 `protobuf:"varint,4,opt,name=expiry_secs,json=expirySecs,proto3" json:"expiry_secs,omitempty"` - // If set to true, invoices will be settled automatically when a payment to - // it comes in. It is not necessary or possible to request settlement - // manually. - AutoSettle bool `protobuf:"varint,5,opt,name=auto_settle,json=autoSettle,proto3" json:"auto_settle,omitempty"` } func (x *AddInvoiceRequest) Reset() { @@ -202,13 +198,6 @@ func (x *AddInvoiceRequest) GetExpirySecs() int64 { return 0 } -func (x *AddInvoiceRequest) GetAutoSettle() bool { - if x != nil { - return x.AutoSettle - } - return false -} - type AddInvoiceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -548,7 +537,7 @@ var File_lnmux_proto protoreflect.FileDescriptor var file_lnmux_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6c, - 0x6e, 0x6d, 0x75, 0x78, 0x22, 0xbd, 0x01, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, + 0x6e, 0x6d, 0x75, 0x78, 0x22, 0x9c, 0x01, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x6d, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x61, 0x6d, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, @@ -558,78 +547,76 @@ var file_lnmux_proto_rawDesc = []byte{ 0x0c, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x73, 0x65, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x53, - 0x65, 0x63, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x74, 0x74, - 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x6f, 0x53, 0x65, - 0x74, 0x74, 0x6c, 0x65, 0x22, 0x6d, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x22, 0x33, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xac, 0x03, 0x0a, 0x1e, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x6c, 0x6e, 0x6d, - 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, - 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x60, 0x0a, 0x10, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, - 0x65, 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x35, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, - 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x76, 0x0a, 0x0c, 0x49, 0x6e, 0x76, 0x6f, 0x69, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, - 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x22, - 0x66, 0x0a, 0x0f, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, - 0x45, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, - 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x45, 0x41, 0x53, 0x4f, - 0x4e, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, - 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x54, - 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x2a, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x74, 0x6c, - 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, - 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x14, - 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x32, 0xcd, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, - 0x0a, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x18, 0x2e, 0x6c, 0x6e, - 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, - 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x67, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, - 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x6c, 0x6e, 0x6d, - 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, - 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x25, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x0d, 0x53, 0x65, 0x74, - 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, - 0x75, 0x78, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, - 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, + 0x65, 0x63, 0x73, 0x22, 0x6d, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x22, 0x33, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, + 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xac, 0x03, 0x0a, 0x1e, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, + 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, + 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x60, 0x0a, 0x10, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, + 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x35, + 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x76, 0x0a, 0x0c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, + 0x53, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, + 0x54, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x22, 0x66, + 0x0a, 0x0f, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, + 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x50, + 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, + 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, + 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x54, 0x45, + 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x2a, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, + 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x14, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x5f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0xcd, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x0a, + 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x6d, + 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, + 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x67, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, + 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, + 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x25, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, + 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, + 0x78, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, + 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, + 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, + 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x5f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go b/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go index 7ffabaf..c890064 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go +++ b/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go @@ -49,8 +49,6 @@ func (m *AddInvoiceRequest) Validate() error { // no validation rules for ExpirySecs - // no validation rules for AutoSettle - return nil } diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.proto b/cmd/lnmuxd/lnmux_proto/lnmux.proto index 96f5f97..935bf3f 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.proto +++ b/cmd/lnmuxd/lnmux_proto/lnmux.proto @@ -19,11 +19,6 @@ message AddInvoiceRequest { string description = 2; bytes description_hash = 3; int64 expiry_secs = 4; - - // If set to true, invoices will be settled automatically when a payment to - // it comes in. It is not necessary or possible to request settlement - // manually. - bool auto_settle = 5; } message AddInvoiceResponse { diff --git a/cmd/lnmuxd/run.go b/cmd/lnmuxd/run.go index a697866..fac7c12 100644 --- a/cmd/lnmuxd/run.go +++ b/cmd/lnmuxd/run.go @@ -81,6 +81,7 @@ func runAction(c *cli.Context) error { HtlcHoldDuration: 30 * time.Second, AcceptTimeout: 60 * time.Second, Logger: log, + AutoSettle: cfg.AutoSettle, }, ) diff --git a/cmd/lnmuxd/server.go b/cmd/lnmuxd/server.go index e063a71..8e2c2cc 100644 --- a/cmd/lnmuxd/server.go +++ b/cmd/lnmuxd/server.go @@ -187,7 +187,6 @@ func (s *server) AddInvoice(ctx context.Context, ID: int64(rand.Int31()), PaymentRequest: invoice.PaymentRequest, ExpiresAt: expiryTime, - AutoSettle: req.AutoSettle, } err = s.registry.NewInvoice(creationData) diff --git a/go.mod b/go.mod index 619a1df..4e540fe 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/urfave/cli/v2 v2.2.0 go.uber.org/zap v1.17.0 google.golang.org/grpc v1.39.1 + google.golang.org/protobuf v1.26.0 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -143,7 +144,6 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect - google.golang.org/protobuf v1.26.0 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v2 v2.2.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/invoiceregistry.go b/invoiceregistry.go index f65be96..4628b8e 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -65,6 +65,8 @@ type RegistryConfig struct { Clock clock.Clock Logger *zap.SugaredLogger + + AutoSettle bool } type InvoiceCallback func(update InvoiceUpdate) @@ -72,7 +74,6 @@ type InvoiceCallback func(update InvoiceUpdate) type invoiceState struct { invoice *types.InvoiceCreationData acceptedHtlcs map[types.CircuitKey]*InvoiceHTLC - autoSettle bool } func (i *invoiceState) totalSetAmt() int { @@ -187,7 +188,6 @@ func (i *InvoiceRegistry) Run(ctx context.Context) error { state := &invoiceState{ invoice: &invoice.InvoiceCreationData.InvoiceCreationData, acceptedHtlcs: make(map[types.CircuitKey]*InvoiceHTLC), - autoSettle: invoice.AutoSettle, } hash := invoice.PaymentPreimage.Hash() @@ -382,7 +382,6 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { state := &invoiceState{ invoice: &invoice.InvoiceCreationData, acceptedHtlcs: make(map[types.CircuitKey]*InvoiceHTLC), - autoSettle: invoice.AutoSettle, } i.invoices[hash] = state @@ -418,7 +417,7 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { } // Don't allow external settles on auto-settling invoices. - if state.autoSettle { + if i.cfg.AutoSettle { err := sendResponse(errors.New("invoice is auto-settling")) if err != nil { return err @@ -894,7 +893,7 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { }) // Auto-settle invoice if specified. - if state.autoSettle { + if i.cfg.AutoSettle { i.logger.Debugw("Auto-settling", "hash", h.rHash) if err := i.markSettleRequested(ctx, state); err != nil { diff --git a/invoiceregistry_test.go b/invoiceregistry_test.go index 529cdf9..5d4efe3 100644 --- a/invoiceregistry_test.go +++ b/invoiceregistry_test.go @@ -30,7 +30,7 @@ type registryTestContext struct { testAmt int64 } -func newRegistryTestContext(t *testing.T) *registryTestContext { +func newRegistryTestContext(t *testing.T, autoSettle bool) *registryTestContext { logger, _ := zap.NewDevelopment() pg, db := setupTestDB(t) @@ -41,6 +41,7 @@ func newRegistryTestContext(t *testing.T) *registryTestContext { HtlcHoldDuration: time.Second, AcceptTimeout: time.Second * 2, Logger: logger.Sugar(), + AutoSettle: autoSettle, } c := ®istryTestContext{ @@ -92,7 +93,7 @@ func (r *registryTestContext) payAddr(id int) lntypes.Preimage { return [32]byte{0, byte(id)} } -func (r *registryTestContext) addInvoice(id int, expiry time.Duration, autoSettle bool) { +func (r *registryTestContext) addInvoice(id int, expiry time.Duration) { preimage := r.preimage(id) payAddr := r.payAddr(id) @@ -107,7 +108,6 @@ func (r *registryTestContext) addInvoice(id int, expiry time.Duration, autoSettl CreatedAt: time.Now(), PaymentRequest: "payreq", ID: int64(id), - AutoSettle: autoSettle, })) } @@ -126,13 +126,13 @@ func (r *registryTestContext) subscribe(id int) (chan InvoiceUpdate, func()) { func TestInvoiceExpiry(t *testing.T) { defer test.Timeout()() - c := newRegistryTestContext(t) + c := newRegistryTestContext(t, false) // Subscribe to updates for invoice 1. updateChan1, cancel1 := c.subscribe(1) // Add invoice. - c.addInvoice(1, time.Second, false) + c.addInvoice(1, time.Second) // Expect an open notification. update := <-updateChan1 @@ -146,7 +146,7 @@ func TestInvoiceExpiry(t *testing.T) { cancel1() // Add another invoice. - c.addInvoice(2, time.Second, false) + c.addInvoice(2, time.Second) // Expect the open update. updateChan2, cancel2 := c.subscribe(2) @@ -179,13 +179,13 @@ func TestInvoiceExpiry(t *testing.T) { func TestAutoSettle(t *testing.T) { defer test.Timeout()() - c := newRegistryTestContext(t) + c := newRegistryTestContext(t, true) // Subscribe to updates for invoice 1. updateChan, cancelUpdates := c.subscribe(1) // Add invoice. - c.addInvoice(1, time.Hour, true) + c.addInvoice(1, time.Hour) // Expect an open notification. update := <-updateChan diff --git a/persistence/migrations/1_initial.up.sql b/persistence/migrations/1_initial.up.sql index 67bb409..1b88679 100644 --- a/persistence/migrations/1_initial.up.sql +++ b/persistence/migrations/1_initial.up.sql @@ -29,9 +29,7 @@ CREATE TABLE invoices "final_cltv_delta" INTEGER NOT NULL CHECK (final_cltv_delta > 0), "payment_addr" BYTEA NOT NULL UNIQUE CHECK (LENGTH(payment_addr) = 32), - "payment_request" TEXT NOT NULL CHECK (LENGTH(payment_request) > 0), - - "auto_settle" BOOLEAN NOT NULL + "payment_request" TEXT NOT NULL CHECK (LENGTH(payment_request) > 0) ); CREATE TABLE htlcs diff --git a/persistence/pg.go b/persistence/pg.go index 0c6e046..04d4ef3 100644 --- a/persistence/pg.go +++ b/persistence/pg.go @@ -49,7 +49,6 @@ type InvoiceCreationData struct { ExpiresAt time.Time ID int64 PaymentRequest string - AutoSettle bool } type dbInvoiceState string @@ -87,7 +86,6 @@ type dbInvoice struct { FinalCltvDelta int32 `pg:"final_cltv_delta,use_zero"` PaymentAddr [32]byte `pg:"payment_addr"` PaymentRequest string `pg:"payment_request"` - AutoSettle bool `pg:"auto_settle,use_zero"` } type dbHtlc struct { @@ -193,9 +191,8 @@ func unmarshallDbInvoice(invoice *dbInvoice) *Invoice { Value: lnwire.MilliSatoshi(invoice.AmountMsat), PaymentAddr: invoice.PaymentAddr, }, - ID: invoice.ID, - ExpiresAt: invoice.ExpiresAt, - AutoSettle: invoice.AutoSettle, + ID: invoice.ID, + ExpiresAt: invoice.ExpiresAt, }, SettledAt: invoice.SettledAt, State: unmarshallDbInvoiceState(invoice.State), @@ -346,7 +343,6 @@ func (p *PostgresPersister) Add(ctx context.Context, invoice *InvoiceCreationDat PaymentRequest: invoice.PaymentRequest, ID: invoice.ID, State: dbInvoiceStateOpen, - AutoSettle: invoice.AutoSettle, } _, err := p.conn.ModelContext(ctx, dbInvoice).Insert() From e04e4789a2338ee3717b5beda93c5906cba3f165 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 24 Jun 2022 16:16:39 +0200 Subject: [PATCH 4/6] rename settle channel to request settle channel --- invoiceregistry.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invoiceregistry.go b/invoiceregistry.go index 4628b8e..a0ff76b 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -115,11 +115,11 @@ type InvoiceRegistry struct { // It is used for efficient notification of links. hodlSubscriptions map[types.CircuitKey][]func(HtlcResolution) - invoices map[lntypes.Hash]*invoiceState - htlcChan chan *registryHtlc - newInvoiceChan chan *persistence.InvoiceCreationData - settleChan chan *invoiceRequest - cancelChan chan *invoiceRequest + invoices map[lntypes.Hash]*invoiceState + htlcChan chan *registryHtlc + newInvoiceChan chan *persistence.InvoiceCreationData + requestSettleChan chan *invoiceRequest + cancelChan chan *invoiceRequest autoReleaseHeap *queue.PriorityQueue logger *zap.SugaredLogger @@ -149,7 +149,7 @@ func NewRegistry(cdb *persistence.PostgresPersister, newInvoiceSubscription: make(chan invoiceSubscription), cancelInvoiceSubscription: make(chan invoiceSubscriptionCancelRequest), subscriptionManager: newSubscriptionManager(cfg.Logger), - settleChan: make(chan *invoiceRequest), + requestSettleChan: make(chan *invoiceRequest), cancelChan: make(chan *invoiceRequest), quit: make(chan struct{}), @@ -263,7 +263,7 @@ func (i *InvoiceRegistry) RequestSettle(hash lntypes.Hash) error { } select { - case i.settleChan <- request: + case i.requestSettleChan <- request: case <-i.quit: return ErrShuttingDown @@ -401,7 +401,7 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { case request := <-i.cancelInvoiceSubscription: i.subscriptionManager.deleteSubscription(request.hash, request.id) - case req := <-i.settleChan: + case req := <-i.requestSettleChan: sendResponse := func(err error) error { return i.sendResponse(req.errChan, err) } From 23daad2f6116245cb871b9ee9f0d5d4d4417625a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 24 Jun 2022 16:21:26 +0200 Subject: [PATCH 5/6] add isSetComplete helper --- invoiceregistry.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/invoiceregistry.go b/invoiceregistry.go index a0ff76b..1f5ced6 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -84,6 +84,10 @@ func (i *invoiceState) totalSetAmt() int { return total } +func (i *invoiceState) isSetComplete() bool { + return i.totalSetAmt() == int(i.invoice.Value) +} + type invoiceRequest struct { hash lntypes.Hash errChan chan error @@ -568,7 +572,7 @@ func (i *InvoiceRegistry) failInvoice(ctx context.Context, } // Don't expire invoices that are already accepted. - setComplete := state.totalSetAmt() == int(state.invoice.Value) + setComplete := state.isSetComplete() if reason == persistence.CancelledReasonExpired && setComplete { return nil } @@ -667,8 +671,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash, } // Do nothing if the set is already complete. - setComplete := invoice.totalSetAmt() == int(invoice.invoice.Value) - if setComplete { + if invoice.isSetComplete() { return nil } From 799c3335d90e66ebf6cd48aba752366db975ff8a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 22 Jun 2022 15:43:09 +0200 Subject: [PATCH 6/6] use stateless invoices --- README.md | 13 +- cmd/lnmuxd/lnmux_proto/lnmux.pb.go | 229 +++++---------- cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go | 2 - cmd/lnmuxd/lnmux_proto/lnmux.proto | 17 +- cmd/lnmuxd/run.go | 1 + cmd/lnmuxd/server.go | 43 +-- invoice_creator.go | 47 +-- invoiceregistry.go | 298 ++++++++------------ invoiceregistry_test.go | 127 ++++----- mux_test.go | 56 ++-- persistence/migrations/1_initial.up.sql | 28 +- persistence/pg.go | 217 ++------------ persistence/pg_test.go | 36 +-- release_event.go | 4 - stateless_data.go | 83 ++++++ test/timeout.go | 2 +- types/types.go | 4 - 17 files changed, 430 insertions(+), 777 deletions(-) create mode 100644 stateless_data.go diff --git a/README.md b/README.md index e27d543..8626746 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For the setup, it is assumed that there are multiple LND nodes running with conn there are momentary disconnects. Not running with this option can lead to HTLCs failing after the invoice that they pay to has been marked as settled. -* Create a postgres database +* Create a postgres database. Note that `lnmux` uses stateless invoices. This means that the database only contains settled invoices. * Create a config file for `lnmuxd` named `lnmux.yml`. An [example](lnmux.yml.example) can be found in this repository. The config file contains the following elements: * LND nodes configuration: TLS certificate, macaroon, address and pubkey. Pubkey is configured as a protection against unintentionally connecting to the wrong node. @@ -26,7 +26,7 @@ For the setup, it is assumed that there are multiple LND nodes running with conn * Migrate the database to the latest version: `go run ./cmd/lnmuxd -c lnmux.yml migrate up` -* Run `lnmuxd`: `go run ./cmd/lnmuxd -c lnmux.yml run`. This opens connections to all LND nodes via the HTLC interceptor API. When HTLCs come in, they are matched against the invoice database. If there is a match, the invoice is marked as settled and a settle action is returned to the LND instance holding the HTLC. For multi-part payments, `lnmuxd` holds matching HTLCs until the full invoice amount is in. +* Run `lnmuxd`: `go run ./cmd/lnmuxd -c lnmux.yml run`. This opens connections to all LND nodes via the HTLC interceptor API. Incoming htlcs are collected and assembled into sets. When a set is complete, an external application connected to `lnmux` decides whether to settle the invoice. If the application sends a settle request, the invoice is marked as settled in the `lnmux` database and a settle action is returned to the LND instance(s) holding the HTLC(s). * Invoice generation is taken over by `lnmuxd`. It is no longer a responsibility of the LND nodes. To generate an invoice, run: @@ -34,6 +34,8 @@ For the setup, it is assumed that there are multiple LND nodes running with conn If you decode the invoice, you'll find route hints from each node in the cluster to the `lnmuxd` public key. `lnmuxd` acts as a virtual node without real channels. + The invoice is not stored in the `lnmux` database and does not take up any disk space. This makes `lnmux` particularly suitable for scenarios where large numbers of invoices are generated. + Below is an example invoice generated by `lnmuxd`. ``` { @@ -110,7 +112,9 @@ If you've set up `lnmuxd` correctly, output similar to what is shown below is ex ![](invoice_lifecycle.png) -Notice that the transition from `settle requested` to `settled` is marked as `[future]`. The transition is happening already, but not backed by an actual final settle event from lnd. See https://github.com/lightningnetwork/lnd/issues/6208. +Note that only the states `accepted`, `settle requested` and `settled` are published to callers of the `SubscribeSingleInvoice` rpc. + +The transition from `settle requested` to `settled` is marked as `[future]`. This transition is happening already, but not backed by an actual final settle event from lnd. See https://github.com/lightningnetwork/lnd/issues/6208. ## Regtest testing @@ -118,5 +122,6 @@ The minimal setup to test on regtest is to create three LND nodes A, B and C. Cr ## Experimental -This software is in an experimental state. Use at your own risk. +This software is in an experimental state. At this stage, breaking changes can happen and may not always include a database migration. Use at your own risk. + diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.pb.go b/cmd/lnmuxd/lnmux_proto/lnmux.pb.go index 9b1d330..c647898 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.pb.go +++ b/cmd/lnmuxd/lnmux_proto/lnmux.pb.go @@ -23,28 +23,22 @@ const ( type SubscribeSingleInvoiceResponse_InvoiceState int32 const ( - SubscribeSingleInvoiceResponse_STATE_OPEN SubscribeSingleInvoiceResponse_InvoiceState = 0 - SubscribeSingleInvoiceResponse_STATE_ACCEPTED SubscribeSingleInvoiceResponse_InvoiceState = 1 - SubscribeSingleInvoiceResponse_STATE_SETTLE_REQUESTED SubscribeSingleInvoiceResponse_InvoiceState = 2 - SubscribeSingleInvoiceResponse_STATE_SETTLED SubscribeSingleInvoiceResponse_InvoiceState = 3 - SubscribeSingleInvoiceResponse_STATE_CANCELLED SubscribeSingleInvoiceResponse_InvoiceState = 4 + SubscribeSingleInvoiceResponse_STATE_ACCEPTED SubscribeSingleInvoiceResponse_InvoiceState = 0 + SubscribeSingleInvoiceResponse_STATE_SETTLE_REQUESTED SubscribeSingleInvoiceResponse_InvoiceState = 1 + SubscribeSingleInvoiceResponse_STATE_SETTLED SubscribeSingleInvoiceResponse_InvoiceState = 2 ) // Enum value maps for SubscribeSingleInvoiceResponse_InvoiceState. var ( SubscribeSingleInvoiceResponse_InvoiceState_name = map[int32]string{ - 0: "STATE_OPEN", - 1: "STATE_ACCEPTED", - 2: "STATE_SETTLE_REQUESTED", - 3: "STATE_SETTLED", - 4: "STATE_CANCELLED", + 0: "STATE_ACCEPTED", + 1: "STATE_SETTLE_REQUESTED", + 2: "STATE_SETTLED", } SubscribeSingleInvoiceResponse_InvoiceState_value = map[string]int32{ - "STATE_OPEN": 0, - "STATE_ACCEPTED": 1, - "STATE_SETTLE_REQUESTED": 2, - "STATE_SETTLED": 3, - "STATE_CANCELLED": 4, + "STATE_ACCEPTED": 0, + "STATE_SETTLE_REQUESTED": 1, + "STATE_SETTLED": 2, } ) @@ -75,58 +69,6 @@ func (SubscribeSingleInvoiceResponse_InvoiceState) EnumDescriptor() ([]byte, []i return file_lnmux_proto_rawDescGZIP(), []int{3, 0} } -type SubscribeSingleInvoiceResponse_CancelledReason int32 - -const ( - SubscribeSingleInvoiceResponse_REASON_NONE SubscribeSingleInvoiceResponse_CancelledReason = 0 - SubscribeSingleInvoiceResponse_REASON_EXPIRED SubscribeSingleInvoiceResponse_CancelledReason = 1 - SubscribeSingleInvoiceResponse_REASON_ACCEPT_TIMEOUT SubscribeSingleInvoiceResponse_CancelledReason = 2 - SubscribeSingleInvoiceResponse_REASON_EXTERNAL SubscribeSingleInvoiceResponse_CancelledReason = 3 -) - -// Enum value maps for SubscribeSingleInvoiceResponse_CancelledReason. -var ( - SubscribeSingleInvoiceResponse_CancelledReason_name = map[int32]string{ - 0: "REASON_NONE", - 1: "REASON_EXPIRED", - 2: "REASON_ACCEPT_TIMEOUT", - 3: "REASON_EXTERNAL", - } - SubscribeSingleInvoiceResponse_CancelledReason_value = map[string]int32{ - "REASON_NONE": 0, - "REASON_EXPIRED": 1, - "REASON_ACCEPT_TIMEOUT": 2, - "REASON_EXTERNAL": 3, - } -) - -func (x SubscribeSingleInvoiceResponse_CancelledReason) Enum() *SubscribeSingleInvoiceResponse_CancelledReason { - p := new(SubscribeSingleInvoiceResponse_CancelledReason) - *p = x - return p -} - -func (x SubscribeSingleInvoiceResponse_CancelledReason) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SubscribeSingleInvoiceResponse_CancelledReason) Descriptor() protoreflect.EnumDescriptor { - return file_lnmux_proto_enumTypes[1].Descriptor() -} - -func (SubscribeSingleInvoiceResponse_CancelledReason) Type() protoreflect.EnumType { - return &file_lnmux_proto_enumTypes[1] -} - -func (x SubscribeSingleInvoiceResponse_CancelledReason) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SubscribeSingleInvoiceResponse_CancelledReason.Descriptor instead. -func (SubscribeSingleInvoiceResponse_CancelledReason) EnumDescriptor() ([]byte, []int) { - return file_lnmux_proto_rawDescGZIP(), []int{3, 1} -} - type AddInvoiceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -313,8 +255,7 @@ type SubscribeSingleInvoiceResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - State SubscribeSingleInvoiceResponse_InvoiceState `protobuf:"varint,1,opt,name=state,proto3,enum=lnmux.SubscribeSingleInvoiceResponse_InvoiceState" json:"state,omitempty"` - CancelledReason SubscribeSingleInvoiceResponse_CancelledReason `protobuf:"varint,2,opt,name=cancelled_reason,json=cancelledReason,proto3,enum=lnmux.SubscribeSingleInvoiceResponse_CancelledReason" json:"cancelled_reason,omitempty"` + State SubscribeSingleInvoiceResponse_InvoiceState `protobuf:"varint,1,opt,name=state,proto3,enum=lnmux.SubscribeSingleInvoiceResponse_InvoiceState" json:"state,omitempty"` } func (x *SubscribeSingleInvoiceResponse) Reset() { @@ -353,14 +294,7 @@ func (x *SubscribeSingleInvoiceResponse) GetState() SubscribeSingleInvoiceRespon if x != nil { return x.State } - return SubscribeSingleInvoiceResponse_STATE_OPEN -} - -func (x *SubscribeSingleInvoiceResponse) GetCancelledReason() SubscribeSingleInvoiceResponse_CancelledReason { - if x != nil { - return x.CancelledReason - } - return SubscribeSingleInvoiceResponse_REASON_NONE + return SubscribeSingleInvoiceResponse_STATE_ACCEPTED } type SettleInvoiceRequest struct { @@ -557,66 +491,51 @@ var file_lnmux_proto_rawDesc = []byte{ 0x73, 0x68, 0x22, 0x33, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xac, 0x03, 0x0a, 0x1e, 0x53, 0x75, 0x62, 0x73, + 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xbd, 0x01, 0x0a, 0x1e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x60, 0x0a, 0x10, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, - 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x35, - 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, 0x52, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x76, 0x0a, 0x0c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, - 0x53, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, - 0x54, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x22, 0x66, - 0x0a, 0x0f, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x6c, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, - 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x50, - 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, - 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, - 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x54, 0x45, - 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x03, 0x22, 0x2a, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, - 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, - 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x14, 0x43, + 0x74, 0x61, 0x74, 0x65, 0x22, 0x51, 0x0a, 0x0c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, + 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x45, + 0x54, 0x54, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22, 0x2a, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x74, 0x6c, + 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, + 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a, 0x14, + 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0xcd, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, + 0x0a, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x18, 0x2e, 0x6c, 0x6e, + 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, + 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x67, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, + 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x6c, 0x6e, 0x6d, + 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x0d, 0x53, 0x65, 0x74, + 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, + 0x75, 0x78, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, + 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0xcd, 0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x0a, - 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x6d, - 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x41, 0x64, 0x64, - 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x67, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, - 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, - 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, - 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x25, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, - 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, - 0x78, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x53, - 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, - 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, - 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, - 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x5f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x2e, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x6d, 0x75, 0x78, 0x5f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -631,36 +550,34 @@ func file_lnmux_proto_rawDescGZIP() []byte { return file_lnmux_proto_rawDescData } -var file_lnmux_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_lnmux_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_lnmux_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_lnmux_proto_goTypes = []interface{}{ - (SubscribeSingleInvoiceResponse_InvoiceState)(0), // 0: lnmux.SubscribeSingleInvoiceResponse.InvoiceState - (SubscribeSingleInvoiceResponse_CancelledReason)(0), // 1: lnmux.SubscribeSingleInvoiceResponse.CancelledReason - (*AddInvoiceRequest)(nil), // 2: lnmux.AddInvoiceRequest - (*AddInvoiceResponse)(nil), // 3: lnmux.AddInvoiceResponse - (*SubscribeSingleInvoiceRequest)(nil), // 4: lnmux.SubscribeSingleInvoiceRequest - (*SubscribeSingleInvoiceResponse)(nil), // 5: lnmux.SubscribeSingleInvoiceResponse - (*SettleInvoiceRequest)(nil), // 6: lnmux.SettleInvoiceRequest - (*SettleInvoiceResponse)(nil), // 7: lnmux.SettleInvoiceResponse - (*CancelInvoiceRequest)(nil), // 8: lnmux.CancelInvoiceRequest - (*CancelInvoiceResponse)(nil), // 9: lnmux.CancelInvoiceResponse + (SubscribeSingleInvoiceResponse_InvoiceState)(0), // 0: lnmux.SubscribeSingleInvoiceResponse.InvoiceState + (*AddInvoiceRequest)(nil), // 1: lnmux.AddInvoiceRequest + (*AddInvoiceResponse)(nil), // 2: lnmux.AddInvoiceResponse + (*SubscribeSingleInvoiceRequest)(nil), // 3: lnmux.SubscribeSingleInvoiceRequest + (*SubscribeSingleInvoiceResponse)(nil), // 4: lnmux.SubscribeSingleInvoiceResponse + (*SettleInvoiceRequest)(nil), // 5: lnmux.SettleInvoiceRequest + (*SettleInvoiceResponse)(nil), // 6: lnmux.SettleInvoiceResponse + (*CancelInvoiceRequest)(nil), // 7: lnmux.CancelInvoiceRequest + (*CancelInvoiceResponse)(nil), // 8: lnmux.CancelInvoiceResponse } var file_lnmux_proto_depIdxs = []int32{ 0, // 0: lnmux.SubscribeSingleInvoiceResponse.state:type_name -> lnmux.SubscribeSingleInvoiceResponse.InvoiceState - 1, // 1: lnmux.SubscribeSingleInvoiceResponse.cancelled_reason:type_name -> lnmux.SubscribeSingleInvoiceResponse.CancelledReason - 2, // 2: lnmux.Service.AddInvoice:input_type -> lnmux.AddInvoiceRequest - 4, // 3: lnmux.Service.SubscribeSingleInvoice:input_type -> lnmux.SubscribeSingleInvoiceRequest - 6, // 4: lnmux.Service.SettleInvoice:input_type -> lnmux.SettleInvoiceRequest - 8, // 5: lnmux.Service.CancelInvoice:input_type -> lnmux.CancelInvoiceRequest - 3, // 6: lnmux.Service.AddInvoice:output_type -> lnmux.AddInvoiceResponse - 5, // 7: lnmux.Service.SubscribeSingleInvoice:output_type -> lnmux.SubscribeSingleInvoiceResponse - 7, // 8: lnmux.Service.SettleInvoice:output_type -> lnmux.SettleInvoiceResponse - 9, // 9: lnmux.Service.CancelInvoice:output_type -> lnmux.CancelInvoiceResponse - 6, // [6:10] is the sub-list for method output_type - 2, // [2:6] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 1, // 1: lnmux.Service.AddInvoice:input_type -> lnmux.AddInvoiceRequest + 3, // 2: lnmux.Service.SubscribeSingleInvoice:input_type -> lnmux.SubscribeSingleInvoiceRequest + 5, // 3: lnmux.Service.SettleInvoice:input_type -> lnmux.SettleInvoiceRequest + 7, // 4: lnmux.Service.CancelInvoice:input_type -> lnmux.CancelInvoiceRequest + 2, // 5: lnmux.Service.AddInvoice:output_type -> lnmux.AddInvoiceResponse + 4, // 6: lnmux.Service.SubscribeSingleInvoice:output_type -> lnmux.SubscribeSingleInvoiceResponse + 6, // 7: lnmux.Service.SettleInvoice:output_type -> lnmux.SettleInvoiceResponse + 8, // 8: lnmux.Service.CancelInvoice:output_type -> lnmux.CancelInvoiceResponse + 5, // [5:9] is the sub-list for method output_type + 1, // [1:5] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_lnmux_proto_init() } @@ -771,7 +688,7 @@ func file_lnmux_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lnmux_proto_rawDesc, - NumEnums: 2, + NumEnums: 1, NumMessages: 8, NumExtensions: 0, NumServices: 1, diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go b/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go index c890064..f7c27a8 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go +++ b/cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go @@ -261,8 +261,6 @@ func (m *SubscribeSingleInvoiceResponse) Validate() error { // no validation rules for State - // no validation rules for CancelledReason - return nil } diff --git a/cmd/lnmuxd/lnmux_proto/lnmux.proto b/cmd/lnmuxd/lnmux_proto/lnmux.proto index 935bf3f..ceb8712 100644 --- a/cmd/lnmuxd/lnmux_proto/lnmux.proto +++ b/cmd/lnmuxd/lnmux_proto/lnmux.proto @@ -33,23 +33,12 @@ message SubscribeSingleInvoiceRequest { message SubscribeSingleInvoiceResponse { enum InvoiceState { - STATE_OPEN = 0; - STATE_ACCEPTED = 1; - STATE_SETTLE_REQUESTED = 2; - STATE_SETTLED = 3; - STATE_CANCELLED = 4; - } - - enum CancelledReason { - REASON_NONE = 0; - REASON_EXPIRED = 1; - REASON_ACCEPT_TIMEOUT = 2; - REASON_EXTERNAL = 3; + STATE_ACCEPTED = 0; + STATE_SETTLE_REQUESTED = 1; + STATE_SETTLED = 2; } InvoiceState state = 1; - - CancelledReason cancelled_reason = 2; } message SettleInvoiceRequest { diff --git a/cmd/lnmuxd/run.go b/cmd/lnmuxd/run.go index fac7c12..e6e020c 100644 --- a/cmd/lnmuxd/run.go +++ b/cmd/lnmuxd/run.go @@ -81,6 +81,7 @@ func runAction(c *cli.Context) error { HtlcHoldDuration: 30 * time.Second, AcceptTimeout: 60 * time.Second, Logger: log, + PrivKey: identityKey, AutoSettle: cfg.AutoSettle, }, ) diff --git a/cmd/lnmuxd/server.go b/cmd/lnmuxd/server.go index 8e2c2cc..9ff111d 100644 --- a/cmd/lnmuxd/server.go +++ b/cmd/lnmuxd/server.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math/rand" "time" "github.com/bottlepay/lnmux" @@ -39,8 +38,6 @@ func newServer(creator *lnmux.InvoiceCreator, registry *lnmux.InvoiceRegistry) ( func marshallInvoiceState(state persistence.InvoiceState) lnmux_proto.SubscribeSingleInvoiceResponse_InvoiceState { switch state { - case persistence.InvoiceStateOpen: - return lnmux_proto.SubscribeSingleInvoiceResponse_STATE_OPEN case persistence.InvoiceStateAccepted: return lnmux_proto.SubscribeSingleInvoiceResponse_STATE_ACCEPTED @@ -51,33 +48,11 @@ func marshallInvoiceState(state persistence.InvoiceState) lnmux_proto.SubscribeS case persistence.InvoiceStateSettled: return lnmux_proto.SubscribeSingleInvoiceResponse_STATE_SETTLED - case persistence.InvoiceStateCancelled: - return lnmux_proto.SubscribeSingleInvoiceResponse_STATE_CANCELLED - default: panic("unknown invoice state") } } -func marshallCancelledReason(reason persistence.CancelledReason) lnmux_proto.SubscribeSingleInvoiceResponse_CancelledReason { - switch reason { - case persistence.CancelledReasonNone: - return lnmux_proto.SubscribeSingleInvoiceResponse_REASON_NONE - - case persistence.CancelledReasonExpired: - return lnmux_proto.SubscribeSingleInvoiceResponse_REASON_EXPIRED - - case persistence.CancelledReasonAcceptTimeout: - return lnmux_proto.SubscribeSingleInvoiceResponse_REASON_ACCEPT_TIMEOUT - - case persistence.CancelledReasonExternal: - return lnmux_proto.SubscribeSingleInvoiceResponse_REASON_EXTERNAL - - default: - panic("unknown cancelled reason") - } -} - func (s *server) SubscribeSingleInvoice(req *lnmux_proto.SubscribeSingleInvoiceRequest, subscription lnmux_proto.Service_SubscribeSingleInvoiceServer) error { @@ -136,8 +111,7 @@ func (s *server) SubscribeSingleInvoice(req *lnmux_proto.SubscribeSingleInvoiceR } err := subscription.Send(&lnmux_proto.SubscribeSingleInvoiceResponse{ - State: marshallInvoiceState(update.State), - CancelledReason: marshallCancelledReason(update.CancelledReason), + State: marshallInvoiceState(update.State), }) if err != nil { return err @@ -170,7 +144,6 @@ func (s *server) AddInvoice(ctx context.Context, // Create the invoice. expiry := time.Duration(req.ExpirySecs) * time.Second - expiryTime := time.Now().Add(expiry) invoice, preimage, err := s.creator.Create( req.AmtMsat, expiry, req.Description, descHash, finalCltvExpiry, ) @@ -180,20 +153,6 @@ func (s *server) AddInvoice(ctx context.Context, hash := preimage.Hash() - // Store invoice. - creationData := &persistence.InvoiceCreationData{ - InvoiceCreationData: invoice.InvoiceCreationData, - CreatedAt: invoice.CreationDate, - ID: int64(rand.Int31()), - PaymentRequest: invoice.PaymentRequest, - ExpiresAt: expiryTime, - } - - err = s.registry.NewInvoice(creationData) - if err != nil { - return nil, err - } - return &lnmux_proto.AddInvoiceResponse{ PaymentRequest: invoice.PaymentRequest, Hash: hash[:], diff --git a/invoice_creator.go b/invoice_creator.go index efc0d6c..3e9f99c 100644 --- a/invoice_creator.go +++ b/invoice_creator.go @@ -1,7 +1,7 @@ package lnmux import ( - "crypto/rand" + "encoding/binary" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -17,6 +17,8 @@ import ( "github.com/bottlepay/lnmux/types" ) +var byteOrder = binary.BigEndian + const virtualChannel = 12345 type InvoiceCreatorConfig struct { @@ -65,6 +67,8 @@ func (c *InvoiceCreator) Create(amtMSat int64, expiry time.Duration, memo string, descHash *lntypes.Hash, cltvDelta uint64) ( *Invoice, lntypes.Preimage, error) { + creationDate := time.Now() + // Get features. featureMgr, err := feature.NewManager(feature.Config{}) if err != nil { @@ -77,11 +81,27 @@ func (c *InvoiceCreator) Create(amtMSat int64, expiry time.Duration, nodeSigner := netann.NewNodeSigner(nodeKeySigner) - paymentPreimage := &lntypes.Preimage{} - if _, err := rand.Read(paymentPreimage[:]); err != nil { + privKey, err := c.keyRing.DerivePrivKey(c.idKeyDesc) + if err != nil { return nil, lntypes.Preimage{}, err } - paymentHash := paymentPreimage.Hash() + + expiryTime := creationDate.Add(expiry) + statelessData, err := encodeStatelessData( + privKey.Serialize(), amtMSat, expiryTime, + ) + if err != nil { + return nil, lntypes.Preimage{}, err + } + + paymentHash := statelessData.preimage.Hash() + + // TODO: Optionally we could encrypt the payment metadata here, just like + // rust-lightning does: + // https://github.com/lightningdevkit/rust-lightning/blob/a600eee87c96ee8865402e86bb1865011bf2d2de/lightning/src/ln/inbound_payment.rs#L166 + // + // Background: + // https://github.com/lightningdevkit/rust-lightning/issues/1171#issuecomment-1162817360 // We also create an encoded payment request which allows the // caller to compactly send the invoice to the payer. We'll create a @@ -128,17 +148,11 @@ func (c *InvoiceCreator) Create(amtMSat int64, expiry time.Duration, invoiceFeatures := featureMgr.Get(feature.SetInvoice) options = append(options, zpay32.Features(invoiceFeatures)) - // Generate and set a random payment address for this invoice. If the - // sender understands payment addresses, this can be used to avoid - // intermediaries probing the receiver. - var paymentAddr [32]byte - if _, err := rand.Read(paymentAddr[:]); err != nil { - return nil, lntypes.Preimage{}, err - } - options = append(options, zpay32.PaymentAddr(paymentAddr)) + // Set the payment address. + options = append(options, zpay32.PaymentAddr(statelessData.paymentAddr)) // Create and encode the payment request as a bech32 (zpay32) string. - creationDate := time.Now() + payReq, err := zpay32.NewInvoice( c.activeNetParams, paymentHash, creationDate, options..., ) @@ -159,12 +173,11 @@ func (c *InvoiceCreator) Create(amtMSat int64, expiry time.Duration, CreationDate: creationDate, PaymentRequest: payReqString, InvoiceCreationData: types.InvoiceCreationData{ - FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()), Value: lnwire.MilliSatoshi(amtMSat), - PaymentPreimage: *paymentPreimage, - PaymentAddr: paymentAddr, + PaymentPreimage: statelessData.preimage, + PaymentAddr: statelessData.paymentAddr, }, } - return newInvoice, *paymentPreimage, nil + return newInvoice, statelessData.preimage, nil } diff --git a/invoiceregistry.go b/invoiceregistry.go index 1f5ced6..ca2b739 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -66,6 +66,10 @@ type RegistryConfig struct { Logger *zap.SugaredLogger + // PrivKey is the private key that is used for calculating payment metadata + // hmacs, which serve as the payment preimage. + PrivKey [32]byte + AutoSettle bool } @@ -74,6 +78,7 @@ type InvoiceCallback func(update InvoiceUpdate) type invoiceState struct { invoice *types.InvoiceCreationData acceptedHtlcs map[types.CircuitKey]*InvoiceHTLC + expiry time.Time } func (i *invoiceState) totalSetAmt() int { @@ -121,7 +126,6 @@ type InvoiceRegistry struct { invoices map[lntypes.Hash]*invoiceState htlcChan chan *registryHtlc - newInvoiceChan chan *persistence.InvoiceCreationData requestSettleChan chan *invoiceRequest cancelChan chan *invoiceRequest @@ -149,7 +153,6 @@ func NewRegistry(cdb *persistence.PostgresPersister, cfg: cfg, invoices: make(map[lntypes.Hash]*invoiceState), htlcChan: make(chan *registryHtlc), - newInvoiceChan: make(chan *persistence.InvoiceCreationData), newInvoiceSubscription: make(chan invoiceSubscription), cancelInvoiceSubscription: make(chan invoiceSubscriptionCancelRequest), subscriptionManager: newSubscriptionManager(cfg.Logger), @@ -166,44 +169,7 @@ func NewRegistry(cdb *persistence.PostgresPersister, func (i *InvoiceRegistry) Run(ctx context.Context) error { i.logger.Info("InvoiceRegistry starting") - pendingInvoices, err := i.cdb.GetOpen(ctx) - if err != nil { - return err - } - - i.logger.Infow("Open invoices", "count", len(pendingInvoices)) - - for _, invoice := range pendingInvoices { - // Immediately fail invoices that expired while we were not running. - if time.Now().After(invoice.ExpiresAt) { - hash := invoice.PaymentPreimage.Hash() - - i.logger.Debugw("Invoice expired", - "hash", hash, "expiresAt", invoice.ExpiresAt) - - err := i.cdb.Fail(ctx, hash, persistence.CancelledReasonExpired) - if err != nil { - return err - } - - continue - } - - state := &invoiceState{ - invoice: &invoice.InvoiceCreationData.InvoiceCreationData, - acceptedHtlcs: make(map[types.CircuitKey]*InvoiceHTLC), - } - - hash := invoice.PaymentPreimage.Hash() - - i.invoices[hash] = state - i.startInvoiceExpireTimer(hash, invoice.ExpiresAt) - - i.logger.Debugw("Pending invoice", - "hash", hash, "expiresAt", invoice.ExpiresAt) - } - - err = i.invoiceEventLoop(ctx) + err := i.invoiceEventLoop(ctx) if err != nil && !errors.Is(err, context.Canceled) { i.logger.Errorw("InvoiceRegistry error", "err", err) @@ -248,16 +214,6 @@ func (i *InvoiceRegistry) Subscribe(hash lntypes.Hash, }, nil } -func (i *InvoiceRegistry) NewInvoice(invoice *persistence.InvoiceCreationData) error { - select { - case i.newInvoiceChan <- invoice: - case <-i.quit: - return ErrShuttingDown - } - - return nil -} - func (i *InvoiceRegistry) RequestSettle(hash lntypes.Hash) error { i.logger.Debugw("New settle request received", "hash", hash) @@ -349,17 +305,9 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { i.logger.Errorf("HTLC timer: %v", err) } - case *invoiceExpiredEvent: - err := i.failInvoice( - ctx, event.hash, persistence.CancelledReasonExpired, - ) - if err != nil { - return err - } - case *acceptTimeoutEvent: err := i.failInvoice( - ctx, event.hash, persistence.CancelledReasonAcceptTimeout, + ctx, event.hash, ) if err != nil { return err @@ -372,31 +320,6 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { i.logger.Errorf("Process: %v", err) } - case invoice := <-i.newInvoiceChan: - err := i.cdb.Add(ctx, invoice) - if err != nil { - return err - } - - hash := invoice.PaymentPreimage.Hash() - - i.logger.Debugw("New invoice", - "hash", hash, "amt", invoice.Value) - - state := &invoiceState{ - invoice: &invoice.InvoiceCreationData, - acceptedHtlcs: make(map[types.CircuitKey]*InvoiceHTLC), - } - - i.invoices[hash] = state - - // Notify subscriber of new invoice. - i.subscriptionManager.notifySubscribers(hash, InvoiceUpdate{ - State: persistence.InvoiceStateOpen, - }) - - i.startInvoiceExpireTimer(hash, invoice.ExpiresAt) - case newSubscription := <-i.newInvoiceSubscription: if err := i.addSubscriber(ctx, newSubscription); err != nil { return err @@ -463,11 +386,6 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { break } - // Mark invoice as failed. - if err := i.cdb.Fail(ctx, req.hash, persistence.CancelledReasonExternal); err != nil { - return errors.New("cannot fail invoice in database") - } - // Delete in-memory record for this invoice. Only open invoices are // kept in memory. delete(i.invoices, req.hash) @@ -481,15 +399,6 @@ func (i *InvoiceRegistry) invoiceEventLoop(ctx context.Context) error { i.notifyHodlSubscribers(key, resolution) } - // Notify subscriber of settled invoice. - i.subscriptionManager.notifySubscribers( - req.hash, - InvoiceUpdate{ - State: persistence.InvoiceStateCancelled, - CancelledReason: persistence.CancelledReasonExternal, - }, - ) - // Send success response. err := i.sendResponse(req.errChan, nil) if err != nil { @@ -531,10 +440,12 @@ func (i *InvoiceRegistry) addSubscriber(ctx context.Context, invoiceState, ok := i.invoices[hash] if ok { - update.State = persistence.InvoiceStateOpen - if len(invoiceState.acceptedHtlcs) > 0 { - update.State = persistence.InvoiceStateAccepted + // No event for partially accepted invoices. + if !invoiceState.isSetComplete() { + return nil } + + update.State = persistence.InvoiceStateAccepted } else { // Send other states from database. invoice, _, err := i.cdb.Get(ctx, hash) @@ -549,8 +460,10 @@ func (i *InvoiceRegistry) addSubscriber(ctx context.Context, return err } - update.State = invoice.State - update.CancelledReason = invoice.CancelledReason + update.State = persistence.InvoiceStateSettleRequested + if invoice.Settled { + update.State = persistence.InvoiceStateSettled + } } newSubscription.callback(update) @@ -559,7 +472,7 @@ func (i *InvoiceRegistry) addSubscriber(ctx context.Context, } func (i *InvoiceRegistry) failInvoice(ctx context.Context, - hash lntypes.Hash, reason persistence.CancelledReason) error { + hash lntypes.Hash) error { logger := i.logger.With("hash", hash) @@ -571,53 +484,19 @@ func (i *InvoiceRegistry) failInvoice(ctx context.Context, return nil } - // Don't expire invoices that are already accepted. - setComplete := state.isSetComplete() - if reason == persistence.CancelledReasonExpired && setComplete { - return nil - } - // Cancel all accepted htlcs. for key := range state.acceptedHtlcs { i.notifyHodlSubscribers(key, NewFailResolution(ResultInvoiceExpired)) } - // Mark invoice as expired in the database. - err := i.cdb.Fail(ctx, hash, reason) - if err != nil { - return err - } - // Remove from memory because invoice is no longer open. delete(i.invoices, hash) - // Notify subscriber. - i.subscriptionManager.notifySubscribers(hash, InvoiceUpdate{ - State: persistence.InvoiceStateCancelled, - CancelledReason: reason, - }) - logger.Infow("Failed invoice") return nil } -func (i *InvoiceRegistry) startInvoiceExpireTimer(hash lntypes.Hash, - releaseTime time.Time) { - - event := &invoiceExpiredEvent{ - eventBase: eventBase{ - hash: hash, - releaseTime: releaseTime, - }, - } - - i.logger.Debugw("Scheduling auto-release for invoice", - "hash", hash, "releaseTime", releaseTime) - - i.autoReleaseHeap.Push(event) -} - func (i *InvoiceRegistry) startAcceptTimer(hash lntypes.Hash) { releaseTime := time.Now().Add(i.cfg.AcceptTimeout) event := &acceptTimeoutEvent{ @@ -636,9 +515,8 @@ func (i *InvoiceRegistry) startAcceptTimer(hash lntypes.Hash) { // startHtlcTimer starts a new timer via the invoice registry main loop that // cancels a single htlc on an invoice when the htlc hold duration has passed. func (i *InvoiceRegistry) startHtlcTimer(hash lntypes.Hash, - key types.CircuitKey, acceptTime time.Time) { + key types.CircuitKey, releaseTime time.Time) { - releaseTime := acceptTime.Add(i.cfg.HtlcHoldDuration) event := &htlcReleaseEvent{ eventBase: eventBase{ hash: hash, @@ -686,6 +564,11 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash, delete(invoice.acceptedHtlcs, key) + // If this was the last htlc, clean up the in-memory record. + if len(invoice.acceptedHtlcs) == 0 { + delete(i.invoices, hash) + } + i.notifyHodlSubscribers(key, NewFailResolution(result)) return nil @@ -760,6 +643,17 @@ func (i *InvoiceRegistry) resolveViaDb(ctx context.Context, } func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { + logger := i.logger.With("hash", h.rHash) + + // First try to resolve via the database, in case this is a replay. + resolved, err := i.resolveViaDb(ctx, h) + if err != nil { + return err + } + if resolved { + return nil + } + // Always require an mpp record. mpp := h.payload.MultiPath() if mpp == nil { @@ -770,22 +664,75 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { return nil } + // Don't accept zero-valued sets. + if mpp.TotalMsat() == 0 { + h.resolve(NewFailResolution( + ResultHtlcSetTotalTooLow, + )) + + return nil + } + + statelessData, err := decodeStatelessData( + i.cfg.PrivKey[:], mpp.PaymentAddr(), + ) + if err != nil { + return err + } + + // If the preimage doesn't match the payment hash, fail this htlc. Someone + // must have tampered with our payment parameters. + if statelessData.preimage.Hash() != h.rHash { + logger.Debugw("Hash mismatch") + + h.resolve(NewFailResolution(ResultInvoiceNotFound)) + + return nil + } + + logger = logger.With( + "amtMsat", statelessData.amtMsat, "expiry", statelessData.expiry, + ) + + // Check expiry. + if statelessData.expiry.Before(time.Now()) { + logger.Infow("Stateless invoice payment to expired invoice") + + h.resolve(NewFailResolution(ResultInvoiceExpired)) + + return nil + } + + logger.Infow("Stateless invoice payment received") + + // Look up this invoice in memory. If it is present, we have already + // received other shards of the payment. state, ok := i.invoices[h.rHash] if !ok { - // Invoice is not present in memory. Do a db lookup to see if this - // happens to be a previously settled invoice and resolve the htlc if - // possible. - resolved, err := i.resolveViaDb(ctx, h) - if err != nil { - return err + state = &invoiceState{ + invoice: &types.InvoiceCreationData{ + PaymentPreimage: statelessData.preimage, + Value: lnwire.MilliSatoshi(statelessData.amtMsat), + PaymentAddr: statelessData.paymentAddr, + }, + acceptedHtlcs: make(map[types.CircuitKey]*InvoiceHTLC), + expiry: statelessData.expiry, } - if !resolved { - // If the invoice was not found, return a failure - // resolution with an invoice not found result. + + i.invoices[h.rHash] = state + } else { + // Sanity check that the total amount and expiry time are identical. + if statelessData.amtMsat != int64(state.invoice.Value) || + statelessData.expiry != state.expiry { + + logger.Errorw("Stateless invoice sanity check failed", + "expectedAmtMsat", state.invoice.Value, "amtMsat", statelessData.amtMsat, + "expectedExpiry", state.expiry, "expiry", statelessData.expiry) + h.resolve(NewFailResolution(ResultInvoiceNotFound)) - } - return nil + return nil + } } if _, ok := state.acceptedHtlcs[h.circuitKey]; ok { @@ -808,15 +755,6 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { return nil } - // Don't accept zero-valued sets. - if mpp.TotalMsat() == 0 { - h.resolve(NewFailResolution( - ResultHtlcSetTotalTooLow, - )) - - return nil - } - // Check that the total amt of the htlc set is matching the invoice // amount. We don't accept overpayment. if mpp.TotalMsat() != inv.Value { @@ -860,14 +798,6 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { return nil } - if h.expiry < uint32(h.currentHeight+inv.FinalCltvDelta) { - h.resolve(NewFailResolution( - ResultExpiryTooSoon, - )) - - return nil - } - state.acceptedHtlcs[h.circuitKey] = &InvoiceHTLC{ Amt: h.amtPaid, MppTotalAmt: mpp.TotalMsat(), @@ -881,13 +811,19 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { // If the invoice cannot be settled yet, only record the htlc. setComplete := newSetTotal == mpp.TotalMsat() if !setComplete { - i.startHtlcTimer( - h.rHash, h.circuitKey, time.Now(), - ) + // Start a release timer for this htlc. We release either after the hold + // duration has passed or the invoice expires - whichever comes first. + releaseTime := time.Now().Add(i.cfg.HtlcHoldDuration) + if releaseTime.After(statelessData.expiry) { + releaseTime = statelessData.expiry + } + + i.startHtlcTimer(h.rHash, h.circuitKey, releaseTime) return nil } + // The set is complete and we start the accept timer. i.startAcceptTimer(h.rHash) // Notify subscriber of accepted invoice. @@ -912,8 +848,7 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { } type InvoiceUpdate struct { - State persistence.InvoiceState - CancelledReason persistence.CancelledReason + State persistence.InvoiceState } func (i *InvoiceRegistry) requestSettle(ctx context.Context, @@ -971,6 +906,9 @@ func (i *InvoiceRegistry) markSettleRequested(ctx context.Context, return errors.New("set no longer complete") } + i.logger.Infow("Stateless invoice JIT insertion", + "hash", hash) + // Store settle request in database. This is important to prevent partial // settles after a restart. htlcMap := make(map[types.CircuitKey]int64) @@ -978,11 +916,17 @@ func (i *InvoiceRegistry) markSettleRequested(ctx context.Context, htlcMap[key] = int64(htlc.Amt) } - err := i.cdb.RequestSettle( - ctx, hash, htlcMap, - ) + invoice := &persistence.InvoiceCreationData{ + InvoiceCreationData: types.InvoiceCreationData{ + Value: state.invoice.Value, + PaymentPreimage: state.invoice.PaymentPreimage, + PaymentAddr: state.invoice.PaymentAddr, + }, + } + + err := i.cdb.RequestSettle(ctx, invoice, htlcMap) if err != nil { - return errors.New("cannot settle invoice in database") + return fmt.Errorf("cannot request settle in database: %w", err) } // Notify subscriber of settle request. diff --git a/invoiceregistry_test.go b/invoiceregistry_test.go index 5d4efe3..adf6ad7 100644 --- a/invoiceregistry_test.go +++ b/invoiceregistry_test.go @@ -5,9 +5,10 @@ import ( "testing" "time" + "github.com/bottlepay/lnmux/common" "github.com/bottlepay/lnmux/persistence" "github.com/bottlepay/lnmux/test" - "github.com/bottlepay/lnmux/types" + "github.com/btcsuite/btcd/chaincfg" "github.com/go-pg/pg/v10" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" @@ -28,6 +29,8 @@ type registryTestContext struct { logger *zap.SugaredLogger testAmt int64 + + creator *InvoiceCreator } func newRegistryTestContext(t *testing.T, autoSettle bool) *registryTestContext { @@ -35,12 +38,24 @@ func newRegistryTestContext(t *testing.T, autoSettle bool) *registryTestContext pg, db := setupTestDB(t) + keyRing := NewKeyRing(testKey) + + creator, err := NewInvoiceCreator( + &InvoiceCreatorConfig{ + KeyRing: keyRing, + GwPubKeys: []common.PubKey{testPubKey1, testPubKey2}, + ActiveNetParams: &chaincfg.RegressionNetParams, + }, + ) + require.NoError(t, err) + cfg := &RegistryConfig{ Clock: clock.NewDefaultClock(), FinalCltvRejectDelta: 10, HtlcHoldDuration: time.Second, AcceptTimeout: time.Second * 2, Logger: logger.Sugar(), + PrivKey: testKey, AutoSettle: autoSettle, } @@ -51,6 +66,7 @@ func newRegistryTestContext(t *testing.T, autoSettle bool) *registryTestContext db: db, logger: cfg.Logger, testAmt: 10000, + creator: creator, } c.start() @@ -85,37 +101,18 @@ func (r *registryTestContext) close() { r.pg.Close() } -func (r *registryTestContext) preimage(id int) lntypes.Preimage { - return lntypes.Preimage{byte(id)} -} +func (r *registryTestContext) createInvoice(id int, expiry time.Duration) ( + *Invoice, lntypes.Preimage) { -func (r *registryTestContext) payAddr(id int) lntypes.Preimage { - return [32]byte{0, byte(id)} -} + invoice, preimage, err := r.creator.Create(r.testAmt, expiry, "test", nil, 40) + require.NoError(r.t, err) -func (r *registryTestContext) addInvoice(id int, expiry time.Duration) { - preimage := r.preimage(id) - payAddr := r.payAddr(id) - - require.NoError(r.t, r.registry.NewInvoice(&persistence.InvoiceCreationData{ - ExpiresAt: time.Now().Add(expiry), - InvoiceCreationData: types.InvoiceCreationData{ - FinalCltvDelta: 40, - PaymentPreimage: preimage, - Value: lnwire.MilliSatoshi(r.testAmt), - PaymentAddr: payAddr, - }, - CreatedAt: time.Now(), - PaymentRequest: "payreq", - ID: int64(id), - })) + return invoice, preimage } -func (r *registryTestContext) subscribe(id int) (chan InvoiceUpdate, func()) { - preimage := r.preimage(id) - +func (r *registryTestContext) subscribe(hash lntypes.Hash) (chan InvoiceUpdate, func()) { updateChan := make(chan InvoiceUpdate) - cancel, err := r.registry.Subscribe(preimage.Hash(), func(update InvoiceUpdate) { + cancel, err := r.registry.Subscribe(hash, func(update InvoiceUpdate) { updateChan <- update }) require.NoError(r.t, err) @@ -128,52 +125,31 @@ func TestInvoiceExpiry(t *testing.T) { c := newRegistryTestContext(t, false) - // Subscribe to updates for invoice 1. - updateChan1, cancel1 := c.subscribe(1) - // Add invoice. - c.addInvoice(1, time.Second) - - // Expect an open notification. - update := <-updateChan1 - require.Equal(t, persistence.InvoiceStateOpen, update.State) - - // Expected an expired notification. - update = <-updateChan1 - require.Equal(t, persistence.InvoiceStateCancelled, update.State) - require.Equal(t, persistence.CancelledReasonExpired, update.CancelledReason) - - cancel1() - - // Add another invoice. - c.addInvoice(2, time.Second) - - // Expect the open update. - updateChan2, cancel2 := c.subscribe(2) - update = <-updateChan2 - require.Equal(t, persistence.InvoiceStateOpen, update.State) - cancel2() - - // Stop the registry. - c.stop() + invoice, preimage := c.createInvoice(1, 100*time.Millisecond) // Wait for the invoice to expire. - time.Sleep(2 * time.Second) - - // Restart the registry. - c.start() + time.Sleep(200 * time.Millisecond) - // This should result in an immediate expiry of the invoice. - updateChan3, cancel3 := c.subscribe(2) - - select { - case update := <-updateChan3: - require.Equal(t, persistence.InvoiceStateCancelled, update.State) - require.Equal(t, persistence.CancelledReasonExpired, update.CancelledReason) + // Send htlc. + resolved := make(chan HtlcResolution) + c.registry.NotifyExitHopHtlc(®istryHtlc{ + rHash: preimage.Hash(), + amtPaid: lnwire.MilliSatoshi(c.testAmt), + expiry: 100, + currentHeight: 0, + resolve: func(r HtlcResolution) { + resolved <- r + }, + payload: &testPayload{ + amt: lnwire.MilliSatoshi(c.testAmt), + payAddr: invoice.PaymentAddr, + }, + }) - case <-time.After(200 * time.Millisecond): - } - cancel3() + resolution := <-resolved + require.IsType(t, &HtlcFailResolution{}, resolution) + require.Equal(t, ResultInvoiceExpired, resolution.(*HtlcFailResolution).Outcome) } func TestAutoSettle(t *testing.T) { @@ -181,17 +157,12 @@ func TestAutoSettle(t *testing.T) { c := newRegistryTestContext(t, true) - // Subscribe to updates for invoice 1. - updateChan, cancelUpdates := c.subscribe(1) - // Add invoice. - c.addInvoice(1, time.Hour) + invoice, preimage := c.createInvoice(1, time.Hour) - // Expect an open notification. - update := <-updateChan - require.Equal(t, persistence.InvoiceStateOpen, update.State) + // Subscribe to updates for invoice. + updateChan, cancelUpdates := c.subscribe(preimage.Hash()) - preimage := c.preimage(1) resolved := make(chan struct{}) c.registry.NotifyExitHopHtlc(®istryHtlc{ rHash: preimage.Hash(), @@ -203,11 +174,11 @@ func TestAutoSettle(t *testing.T) { }, payload: &testPayload{ amt: lnwire.MilliSatoshi(c.testAmt), - payAddr: c.payAddr(1), + payAddr: invoice.PaymentAddr, }, }) - update = <-updateChan + update := <-updateChan require.Equal(t, persistence.InvoiceStateAccepted, update.State) update = <-updateChan diff --git a/mux_test.go b/mux_test.go index 0e331ed..33029d4 100644 --- a/mux_test.go +++ b/mux_test.go @@ -23,11 +23,19 @@ import ( "github.com/bottlepay/lnmux/lnd" "github.com/bottlepay/lnmux/persistence" "github.com/bottlepay/lnmux/persistence/test" + test_common "github.com/bottlepay/lnmux/test" ) var ( testPubKey1, _ = common.NewPubKeyFromStr("02e1ce77dfdda9fd1cf5e9d796faf57d1cedef9803aec84a6d7f8487d32781341e") testPubKey2, _ = common.NewPubKeyFromStr("0314aaf9b2547682b81977b3ac0c5585c3521a0a5430fb410cb572d5c72364edf3") + + testKey = [32]byte{ + 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, + 0x68, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, + 0xd, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, + 0x1e, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9, + } ) func createTestLndClient(ctrl *gomock.Controller, pubKey common.PubKey) ( @@ -62,14 +70,9 @@ func createTestLndClient(ctrl *gomock.Controller, pubKey common.PubKey) ( } func TestMux(t *testing.T) { - logger, _ := zap.NewDevelopment() + defer test_common.Timeout()() - var testKey = [32]byte{ - 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, - 0x68, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, - 0xd, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, - 0x1e, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9, - } + logger, _ := zap.NewDevelopment() keyRing := NewKeyRing(testKey) @@ -109,6 +112,7 @@ func TestMux(t *testing.T) { HtlcHoldDuration: time.Second, AcceptTimeout: time.Second * 2, Logger: logger.Sugar(), + PrivKey: testKey, }, ) @@ -129,15 +133,6 @@ func TestMux(t *testing.T) { errChan <- mux.Run(ctx) }() - // Store invoice. - require.NoError(t, registry.NewInvoice(&persistence.InvoiceCreationData{ - ExpiresAt: time.Now().Add(time.Minute), - InvoiceCreationData: invoice.InvoiceCreationData, - CreatedAt: time.Now(), - PaymentRequest: "payreq", - ID: 1, - })) - var updateChan = make(chan InvoiceUpdate, 1) cancelSubscription, err := registry.Subscribe(testHash, func(update InvoiceUpdate) { logger.Sugar().Infow("Payment received", "state", update.State) @@ -152,8 +147,6 @@ func TestMux(t *testing.T) { require.Equal(t, state, update.State) } - expectUpdate(persistence.InvoiceStateOpen) - // Send initial block heights. blockChan1 <- &chainrpc.BlockEpoch{Height: 1000} blockChan2 <- &chainrpc.BlockEpoch{Height: 1000} @@ -198,10 +191,11 @@ func TestMux(t *testing.T) { } expectResponse := func(resp *routerrpc.ForwardHtlcInterceptResponse, - expectedAction routerrpc.ResolveHoldForwardAction) { + htlcID int, expectedAction routerrpc.ResolveHoldForwardAction) { t.Helper() + require.Equal(t, uint64(htlcID), resp.IncomingCircuitKey.HtlcId) require.Equal(t, expectedAction, resp.Action) } @@ -213,8 +207,8 @@ func TestMux(t *testing.T) { htlcChan1 <- receiveHtlc(0, 6000) // Let it time out. Expect two responses, one for each notified arrival. - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_FAIL) - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_FAIL) + expectResponse(<-responseChan1, 0, routerrpc.ResolveHoldForwardAction_FAIL) + expectResponse(<-responseChan1, 0, routerrpc.ResolveHoldForwardAction_FAIL) // Notify arrival of part 1. htlcChan1 <- receiveHtlc(1, 6000) @@ -227,8 +221,8 @@ func TestMux(t *testing.T) { expectUpdate(persistence.InvoiceStateSettleRequested) - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_SETTLE) - expectResponse(<-responseChan2, routerrpc.ResolveHoldForwardAction_SETTLE) + expectResponse(<-responseChan1, 1, routerrpc.ResolveHoldForwardAction_SETTLE) + expectResponse(<-responseChan2, 2, routerrpc.ResolveHoldForwardAction_SETTLE) expectUpdate(persistence.InvoiceStateSettled) @@ -238,11 +232,11 @@ func TestMux(t *testing.T) { // Replay settled htlc. htlcChan1 <- receiveHtlc(1, 6000) - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_SETTLE) + expectResponse(<-responseChan1, 1, routerrpc.ResolveHoldForwardAction_SETTLE) // New payment to settled invoice htlcChan1 <- receiveHtlc(10, 10000) - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_FAIL) + expectResponse(<-responseChan1, 10, routerrpc.ResolveHoldForwardAction_FAIL) cancelSubscription() @@ -262,16 +256,6 @@ func TestMux(t *testing.T) { }) require.NoError(t, err) - // Store invoice. - require.NoError(t, registry.NewInvoice(&persistence.InvoiceCreationData{ - InvoiceCreationData: invoice.InvoiceCreationData, - CreatedAt: time.Now(), - PaymentRequest: "payreq", - ID: 2, - ExpiresAt: time.Now().Add(time.Minute), - })) - expectUpdate(persistence.InvoiceStateOpen) - // Regenerate onion blob for new hash. onionBlob = genOnion() @@ -282,7 +266,7 @@ func TestMux(t *testing.T) { expectUpdate(persistence.InvoiceStateSettleRequested) - expectResponse(<-responseChan1, routerrpc.ResolveHoldForwardAction_SETTLE) + expectResponse(<-responseChan1, 20, routerrpc.ResolveHoldForwardAction_SETTLE) expectUpdate(persistence.InvoiceStateSettled) diff --git a/persistence/migrations/1_initial.up.sql b/persistence/migrations/1_initial.up.sql index 1b88679..6b37235 100644 --- a/persistence/migrations/1_initial.up.sql +++ b/persistence/migrations/1_initial.up.sql @@ -1,35 +1,15 @@ SET search_path TO lnmux; -CREATE TYPE invoice_state as ENUM ( - 'OPEN', - 'SETTLE_REQUESTED', - 'SETTLED', - 'CANCELLED' -); - -CREATE TYPE cancelled_reason as ENUM ( - 'EXPIRED', - 'ACCEPT_TIMEOUT', - 'EXTERNAL' -); - CREATE TABLE invoices ( - "created_at" TIMESTAMPTZ NOT NULL, - "settled_at" TIMESTAMPTZ, -- TODO: rename to completed_at + "settled_at" TIMESTAMPTZ, "settle_requested_at" TIMESTAMPTZ, - "hash" BYTEA NOT NULL PRIMARY KEY CHECK (LENGTH(hash) = 32), + "hash" BYTEA NOT NULL CHECK (LENGTH(hash) = 32), "preimage" BYTEA NOT NULL UNIQUE CHECK (LENGTH(preimage) = 32), "amount_msat" BIGINT NOT NULL CHECK (amount_msat > 0), - "id" BIGINT NOT NULL UNIQUE CHECK (id > 0), - "expires_at" TIMESTAMPTZ NOT NULL, - - "state" invoice_state NOT NULL, - "cancelled_reason" cancelled_reason, -- TODO: Add check for state CANCELLED + "settled" BOOLEAN NOT NULL, - "final_cltv_delta" INTEGER NOT NULL CHECK (final_cltv_delta > 0), - "payment_addr" BYTEA NOT NULL UNIQUE CHECK (LENGTH(payment_addr) = 32), - "payment_request" TEXT NOT NULL CHECK (LENGTH(payment_request) > 0) + PRIMARY KEY (hash) ); CREATE TABLE htlcs diff --git a/persistence/pg.go b/persistence/pg.go index 04d4ef3..2b89194 100644 --- a/persistence/pg.go +++ b/persistence/pg.go @@ -16,9 +16,7 @@ import ( type Invoice struct { InvoiceCreationData - State InvoiceState - CancelledReason CancelledReason - + Settled bool SettledAt time.Time SettleRequestedAt time.Time } @@ -26,66 +24,25 @@ type Invoice struct { type InvoiceState int const ( - InvoiceStateOpen InvoiceState = iota - InvoiceStateAccepted // This state is not persisted in the database. + InvoiceStateAccepted InvoiceState = iota InvoiceStateSettleRequested InvoiceStateSettled - InvoiceStateCancelled -) - -type CancelledReason int - -const ( - CancelledReasonNone CancelledReason = iota - CancelledReasonExpired - CancelledReasonAcceptTimeout - CancelledReasonExternal ) type InvoiceCreationData struct { types.InvoiceCreationData - - CreatedAt time.Time - ExpiresAt time.Time - ID int64 - PaymentRequest string } -type dbInvoiceState string - -const ( - dbInvoiceStateOpen = dbInvoiceState("OPEN") - dbInvoiceStateSettleRequested = dbInvoiceState("SETTLE_REQUESTED") - dbInvoiceStateSettled = dbInvoiceState("SETTLED") - dbInvoiceStateExpired = dbInvoiceState("EXPIRED") - dbInvoiceStateCancelled = dbInvoiceState("CANCELLED") -) - -type dbCancelledReason string - -const ( - dbCancelledReasonExpired = dbCancelledReason("EXPIRED") - dbCancelledReasonAcceptTimeout = dbCancelledReason("ACCEPT_TIMEOUT") - dbCancelledReasonExternal = dbCancelledReason("EXTERNAL") -) - type dbInvoice struct { tableName struct{} `pg:"lnmux.invoices,discard_unknown_columns"` // nolint Hash lntypes.Hash `pg:"hash"` Preimage lntypes.Preimage `pg:"preimage"` - CreatedAt time.Time `pg:"created_at"` - ExpiresAt time.Time `pg:"expires_at"` AmountMsat int64 `pg:"amount_msat,use_zero"` - ID int64 `pg:"id,use_zero"` - - State dbInvoiceState `pg:"state"` - CancelledReason *dbCancelledReason `pg:"cancelled_reason"` - SettledAt time.Time `pg:"settled_at"` - SettleRequestedAt time.Time `pg:"settle_requested_at"` - FinalCltvDelta int32 `pg:"final_cltv_delta,use_zero"` - PaymentAddr [32]byte `pg:"payment_addr"` - PaymentRequest string `pg:"payment_request"` + + Settled bool `pg:"settled,use_zero"` + SettledAt time.Time `pg:"settled_at"` + SettleRequestedAt time.Time `pg:"settle_requested_at"` } type dbHtlc struct { @@ -119,84 +76,16 @@ func (p *PostgresPersister) Delete(ctx context.Context, hash lntypes.Hash) error return nil } -func unmarshallDbInvoiceState(state dbInvoiceState) InvoiceState { - switch state { - case dbInvoiceStateOpen: - return InvoiceStateOpen - - case dbInvoiceStateSettleRequested: - return InvoiceStateSettleRequested - - case dbInvoiceStateSettled: - return InvoiceStateSettled - - case dbInvoiceStateCancelled: - return InvoiceStateCancelled - - default: - panic("unknown invoice state") - } -} - -func unmarshallDbCancelledReason(reason *dbCancelledReason) CancelledReason { - if reason == nil { - return CancelledReasonNone - } - - switch *reason { - case dbCancelledReasonExpired: - return CancelledReasonExpired - - case dbCancelledReasonAcceptTimeout: - return CancelledReasonAcceptTimeout - - case dbCancelledReasonExternal: - return CancelledReasonExternal - - default: - panic("unknown cancelled reason") - } -} - -func marshallCancelledReason(reason CancelledReason) *dbCancelledReason { - var dbReason dbCancelledReason - switch reason { - case CancelledReasonNone: - return nil - - case CancelledReasonExpired: - dbReason = dbCancelledReasonExpired - - case CancelledReasonAcceptTimeout: - dbReason = dbCancelledReasonAcceptTimeout - - case CancelledReasonExternal: - dbReason = dbCancelledReasonExternal - - default: - panic("unknown cancelled reason") - } - - return &dbReason -} - func unmarshallDbInvoice(invoice *dbInvoice) *Invoice { return &Invoice{ InvoiceCreationData: InvoiceCreationData{ - CreatedAt: invoice.CreatedAt, - PaymentRequest: invoice.PaymentRequest, InvoiceCreationData: types.InvoiceCreationData{ - FinalCltvDelta: invoice.FinalCltvDelta, PaymentPreimage: invoice.Preimage, Value: lnwire.MilliSatoshi(invoice.AmountMsat), - PaymentAddr: invoice.PaymentAddr, }, - ID: invoice.ID, - ExpiresAt: invoice.ExpiresAt, }, - SettledAt: invoice.SettledAt, - State: unmarshallDbInvoiceState(invoice.State), - CancelledReason: unmarshallDbCancelledReason(invoice.CancelledReason), + SettledAt: invoice.SettledAt, + Settled: invoice.Settled, } } @@ -234,46 +123,25 @@ func (p *PostgresPersister) Get(ctx context.Context, hash lntypes.Hash) (*Invoic return invoice, htlcs, nil } -func (p *PostgresPersister) GetOpen(ctx context.Context) ([]*Invoice, - error) { - - var dbInvoices []dbInvoice - err := p.conn.ModelContext(ctx, &dbInvoices). - Where("state=?", dbInvoiceStateOpen).Select() - - if err != nil { - return nil, err - } - - var invoices []*Invoice - for _, dbInvoice := range dbInvoices { - invoice := unmarshallDbInvoice(&dbInvoice) - invoices = append(invoices, invoice) - } - - return invoices, nil -} - func (p *PostgresPersister) RequestSettle(ctx context.Context, - hash lntypes.Hash, htlcs map[types.CircuitKey]int64) error { + invoice *InvoiceCreationData, htlcs map[types.CircuitKey]int64) error { return p.conn.RunInTransaction(ctx, func(tx *pg.Tx) error { - result, err := p.conn.ModelContext(ctx, &dbInvoice{}). - Set("state=?", dbInvoiceStateSettleRequested). - Set("settle_requested_at=?", time.Now().UTC()). - Where("hash=?", hash). - Where("state=?", dbInvoiceStateOpen). - Update() - if err != nil { - return fmt.Errorf("cannot request settle: %w", err) + dbInvoice := &dbInvoice{ + Hash: invoice.PaymentPreimage.Hash(), + Preimage: invoice.PaymentPreimage, + AmountMsat: int64(invoice.Value), + SettleRequestedAt: time.Now(), } - if result.RowsAffected() == 0 { - return errors.New("cannot request settle") + + _, err := p.conn.ModelContext(ctx, dbInvoice).Insert() + if err != nil { + return err } for key, amt := range htlcs { dbHtlc := dbHtlc{ - Hash: hash, + Hash: invoice.PaymentPreimage.Hash(), ChanID: key.ChanID, HtlcID: key.HtlcID, AmountMsat: amt, @@ -292,10 +160,10 @@ func (p *PostgresPersister) Settle(ctx context.Context, hash lntypes.Hash) error { result, err := p.conn.ModelContext(ctx, &dbInvoice{}). - Set("state=?", dbInvoiceStateSettled). + Set("settled=?", true). Set("settled_at=?", time.Now().UTC()). Where("hash=?", hash). - Where("state=?", dbInvoiceStateSettleRequested). + Where("settled=?", false). Update() if err != nil { return fmt.Errorf("cannot settle invoice: %w", err) @@ -307,49 +175,6 @@ func (p *PostgresPersister) Settle(ctx context.Context, return nil } -func (p *PostgresPersister) Fail(ctx context.Context, - hash lntypes.Hash, reason CancelledReason) error { - - if reason == CancelledReasonNone { - return errors.New("no cancelled reason specified") - } - - result, err := p.conn.ModelContext(ctx, &dbInvoice{}). - Set("state=?", dbInvoiceStateCancelled). - Set("settled_at=?", time.Now().UTC()). - Set("cancelled_reason=?", marshallCancelledReason(reason)). - Where("hash=?", hash). - Where("state=?", dbInvoiceStateOpen). - Update() - if err != nil { - return fmt.Errorf("cannot fail invoice: %w", err) - } - if result.RowsAffected() == 0 { - return errors.New("cannot fail invoice") - } - - return nil -} - -func (p *PostgresPersister) Add(ctx context.Context, invoice *InvoiceCreationData) error { - dbInvoice := &dbInvoice{ - CreatedAt: invoice.CreatedAt, - ExpiresAt: invoice.ExpiresAt, - Hash: invoice.PaymentPreimage.Hash(), - Preimage: invoice.PaymentPreimage, - AmountMsat: int64(invoice.Value), - FinalCltvDelta: invoice.FinalCltvDelta, - PaymentAddr: invoice.PaymentAddr, - PaymentRequest: invoice.PaymentRequest, - ID: invoice.ID, - State: dbInvoiceStateOpen, - } - - _, err := p.conn.ModelContext(ctx, dbInvoice).Insert() - - return err -} - // Ping pings the database connection to ensure it is available func (p *PostgresPersister) Ping(ctx context.Context) error { if p.conn != nil { diff --git a/persistence/pg_test.go b/persistence/pg_test.go index 3ed959e..ea2b520 100644 --- a/persistence/pg_test.go +++ b/persistence/pg_test.go @@ -3,7 +3,6 @@ package persistence import ( "context" "testing" - "time" "github.com/go-pg/pg/v10" "github.com/lightningnetwork/lnd/lntypes" @@ -37,26 +36,7 @@ func TestSettleInvoice(t *testing.T) { _, _, err := persister.Get(context.Background(), hash) require.ErrorIs(t, err, types.ErrInvoiceNotFound) - require.NoError(t, persister.Add(context.Background(), &InvoiceCreationData{ - CreatedAt: time.Unix(100, 0), - PaymentRequest: "ln...", - InvoiceCreationData: types.InvoiceCreationData{ - FinalCltvDelta: 40, - PaymentPreimage: preimage, - Value: 100, - PaymentAddr: [32]byte{2}, - }, - ID: 123, - ExpiresAt: time.Now().Add(time.Hour), - })) - - invoice, htlcs, err := persister.Get(context.Background(), hash) - require.NoError(t, err) - require.Empty(t, htlcs, 0) - require.Equal(t, invoice.PaymentRequest, "ln...") - require.Equal(t, InvoiceStateOpen, invoice.State) - - htlcs = map[types.CircuitKey]int64{ + htlcs := map[types.CircuitKey]int64{ { ChanID: 10, HtlcID: 11, @@ -66,6 +46,18 @@ func TestSettleInvoice(t *testing.T) { HtlcID: 12, }: 30, } - require.NoError(t, persister.RequestSettle(context.Background(), hash, htlcs)) + require.NoError(t, persister.RequestSettle(context.Background(), &InvoiceCreationData{ + InvoiceCreationData: types.InvoiceCreationData{ + PaymentPreimage: preimage, + Value: 100, + PaymentAddr: [32]byte{2}, + }, + }, htlcs)) + + invoice, htlcs, err := persister.Get(context.Background(), hash) + require.NoError(t, err) + require.Len(t, htlcs, 2) + require.False(t, invoice.Settled) + require.NoError(t, persister.Settle(context.Background(), hash)) } diff --git a/release_event.go b/release_event.go index 23be1be..0809cfb 100644 --- a/release_event.go +++ b/release_event.go @@ -39,10 +39,6 @@ func (r *eventBase) Less(other queue.PriorityQueueItem) bool { return r.getReleaseTime().Before(other.(releaseEvent).getReleaseTime()) } -type invoiceExpiredEvent struct { - eventBase -} - type acceptTimeoutEvent struct { eventBase } diff --git a/stateless_data.go b/stateless_data.go new file mode 100644 index 0000000..1f34148 --- /dev/null +++ b/stateless_data.go @@ -0,0 +1,83 @@ +package lnmux + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "time" + + "github.com/lightningnetwork/lnd/lntypes" +) + +type statelessData struct { + amtMsat int64 + expiry time.Time + preimage lntypes.Preimage + paymentAddr [32]byte +} + +func encodeStatelessData(privKey []byte, amtMsat int64, expiry time.Time) ( + *statelessData, error) { + + // We'll use part of the payment address to store the stateless invoice + // properties. + var addr [32]byte + + // Start payment address with 16 random bytes. The payment address was + // introduced to thwart probing attacks and 16 bytes still provide plenty of + // protection. + if _, err := rand.Read(addr[:16]); err != nil { + return nil, err + } + + // Store the expiry time in the payment address. + byteOrder.PutUint64(addr[16:24], uint64(expiry.Unix())) + + // Store amount in the payment address. + byteOrder.PutUint64(addr[24:32], uint64(amtMsat)) + + // Create hmac using private key. + mac := hmac.New(sha256.New, privKey) + mac.Write(addr[:]) + + // The hmac can only be derived with knowledge of the private key. We'll use + // it as the preimage for this invoice. When we later receive a payment, the + // preimage can be re-derived. + preimageBytes := mac.Sum(nil) + + paymentPreimage, err := lntypes.MakePreimage(preimageBytes) + if err != nil { + return nil, err + } + + return &statelessData{ + amtMsat: amtMsat, + expiry: expiry, + preimage: paymentPreimage, + paymentAddr: addr, + }, nil +} + +func decodeStatelessData(privKey []byte, paymentAddr [32]byte) ( + *statelessData, error) { + + // Re-derive preimage from payment address. + mac := hmac.New(sha256.New, privKey[:]) + + mac.Write(paymentAddr[:]) + paymentPreimage, err := lntypes.MakePreimage(mac.Sum(nil)) + if err != nil { + return nil, err + } + + // Extract payment parameters. + expiry := time.Unix(int64(byteOrder.Uint64(paymentAddr[16:24])), 0) + amtMsat := byteOrder.Uint64(paymentAddr[24:32]) + + return &statelessData{ + amtMsat: int64(amtMsat), + expiry: expiry, + paymentAddr: paymentAddr, + preimage: paymentPreimage, + }, nil +} diff --git a/test/timeout.go b/test/timeout.go index 92fb1c0..c0afc6f 100644 --- a/test/timeout.go +++ b/test/timeout.go @@ -6,7 +6,7 @@ import ( "time" ) -const testTimeout = 20 * time.Second +const testTimeout = 30 * time.Second // Timeout implements a test level timeout. func Timeout() func() { diff --git a/types/types.go b/types/types.go index f5c44c0..14d5570 100644 --- a/types/types.go +++ b/types/types.go @@ -14,10 +14,6 @@ var ( ) type InvoiceCreationData struct { - // FinalCltvDelta is the minimum required number of blocks before htlc - // expiry when the invoice is accepted. - FinalCltvDelta int32 - // PaymentPreimage is the preimage which is to be revealed in the // occasion that an HTLC paying to the hash of this preimage is // extended. Set to nil if the preimage isn't known yet.