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/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..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 @@ -136,10 +78,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 +140,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 @@ -324,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() { @@ -364,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 { @@ -548,7 +471,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,46 +481,29 @@ 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, 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, 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, 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, @@ -644,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() } @@ -784,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 7ffabaf..f7c27a8 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 } @@ -263,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 96f5f97..ceb8712 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 { @@ -38,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 a697866..e6e020c 100644 --- a/cmd/lnmuxd/run.go +++ b/cmd/lnmuxd/run.go @@ -81,6 +81,8 @@ 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 e063a71..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,21 +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, - AutoSettle: req.AutoSettle, - } - - err = s.registry.NewInvoice(creationData) - if err != nil { - return nil, err - } - return &lnmux_proto.AddInvoiceResponse{ PaymentRequest: invoice.PaymentRequest, Hash: hash[:], 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 -} 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/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 6820661..ca2b739 100644 --- a/invoiceregistry.go +++ b/invoiceregistry.go @@ -65,6 +65,12 @@ type RegistryConfig struct { Clock clock.Clock 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 } type InvoiceCallback func(update InvoiceUpdate) @@ -72,7 +78,7 @@ type InvoiceCallback func(update InvoiceUpdate) type invoiceState struct { invoice *types.InvoiceCreationData acceptedHtlcs map[types.CircuitKey]*InvoiceHTLC - autoSettle bool + expiry time.Time } func (i *invoiceState) totalSetAmt() int { @@ -83,6 +89,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 @@ -114,11 +124,10 @@ 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 + requestSettleChan chan *invoiceRequest + cancelChan chan *invoiceRequest autoReleaseHeap *queue.PriorityQueue logger *zap.SugaredLogger @@ -144,11 +153,10 @@ 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), - settleChan: make(chan *invoiceRequest), + requestSettleChan: make(chan *invoiceRequest), cancelChan: make(chan *invoiceRequest), quit: make(chan struct{}), @@ -161,45 +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), - autoSettle: invoice.AutoSettle, - } - - 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) @@ -244,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) @@ -263,7 +223,7 @@ func (i *InvoiceRegistry) RequestSettle(hash lntypes.Hash) error { } select { - case i.settleChan <- request: + case i.requestSettleChan <- request: case <-i.quit: return ErrShuttingDown @@ -345,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 @@ -368,32 +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), - autoSettle: invoice.AutoSettle, - } - - 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 @@ -402,7 +328,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) } @@ -418,7 +344,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 @@ -460,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) @@ -478,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 { @@ -528,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) @@ -546,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) @@ -556,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) @@ -568,53 +484,19 @@ func (i *InvoiceRegistry) failInvoice(ctx context.Context, return nil } - // Don't expire invoices that are already accepted. - setComplete := state.totalSetAmt() == int(state.invoice.Value) - 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{ @@ -633,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, @@ -668,8 +549,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 } @@ -684,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 @@ -721,7 +606,54 @@ 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 { + 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 { @@ -732,54 +664,75 @@ func (i *InvoiceRegistry) process(ctx context.Context, h *registryHtlc) error { return nil } - 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)) + // Don't accept zero-valued sets. + if mpp.TotalMsat() == 0 { + h.resolve(NewFailResolution( + ResultHtlcSetTotalTooLow, + )) - return nil + return nil + } - case err != nil: - return err - } + statelessData, err := decodeStatelessData( + i.cfg.PrivKey[:], mpp.PaymentAddr(), + ) + if err != nil { + return err + } - // Fail htlcs paying to unpayable invoices (expired or cancelled). - if dbInvoice.State != persistence.InvoiceStateSettleRequested && - dbInvoice.State != persistence.InvoiceStateSettled { + // 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)) + h.resolve(NewFailResolution(ResultInvoiceNotFound)) - return nil - } + return nil + } - i.logger.Debugw("Loaded settled invoice from db", "hash", h.rHash) + logger = logger.With( + "amtMsat", statelessData.amtMsat, "expiry", statelessData.expiry, + ) - // Handle replays to a settled invoice. - if len(htlcs) == 0 { - return errors.New("unexpected unsettled invoice") - } + // Check expiry. + if statelessData.expiry.Before(time.Now()) { + logger.Infow("Stateless invoice payment to expired 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, - )) + h.resolve(NewFailResolution(ResultInvoiceExpired)) - return nil + 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 { + 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, } - // Otherwise fail the htlc. - h.resolve(NewFailResolution(ResultInvoiceNotOpen)) + 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 { - return nil + 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 + } } if _, ok := state.acceptedHtlcs[h.circuitKey]; ok { @@ -802,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 { @@ -854,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(), @@ -875,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. @@ -890,7 +832,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 { @@ -906,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, @@ -965,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) @@ -972,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 529cdf9..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,19 +29,34 @@ type registryTestContext struct { logger *zap.SugaredLogger testAmt int64 + + creator *InvoiceCreator } -func newRegistryTestContext(t *testing.T) *registryTestContext { +func newRegistryTestContext(t *testing.T, autoSettle bool) *registryTestContext { logger, _ := zap.NewDevelopment() 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, } c := ®istryTestContext{ @@ -50,6 +66,7 @@ func newRegistryTestContext(t *testing.T) *registryTestContext { db: db, logger: cfg.Logger, testAmt: 10000, + creator: creator, } c.start() @@ -84,38 +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, autoSettle bool) { - 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), - AutoSettle: autoSettle, - })) + 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) @@ -126,72 +123,46 @@ func (r *registryTestContext) subscribe(id int) (chan InvoiceUpdate, func()) { func TestInvoiceExpiry(t *testing.T) { defer test.Timeout()() - c := newRegistryTestContext(t) - - // Subscribe to updates for invoice 1. - updateChan1, cancel1 := c.subscribe(1) + c := newRegistryTestContext(t, false) // Add invoice. - c.addInvoice(1, time.Second, false) - - // 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, false) - - // 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) { defer test.Timeout()() - c := newRegistryTestContext(t) - - // Subscribe to updates for invoice 1. - updateChan, cancelUpdates := c.subscribe(1) + c := newRegistryTestContext(t, true) // Add invoice. - c.addInvoice(1, time.Hour, true) + 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 67bb409..6b37235 100644 --- a/persistence/migrations/1_initial.up.sql +++ b/persistence/migrations/1_initial.up.sql @@ -1,37 +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 - - "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), + "settled" BOOLEAN NOT NULL, - "auto_settle" BOOLEAN NOT NULL + PRIMARY KEY (hash) ); CREATE TABLE htlcs diff --git a/persistence/pg.go b/persistence/pg.go index 0c6e046..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,68 +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 - AutoSettle bool } -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"` - AutoSettle bool `pg:"auto_settle,use_zero"` + + Settled bool `pg:"settled,use_zero"` + SettledAt time.Time `pg:"settled_at"` + SettleRequestedAt time.Time `pg:"settle_requested_at"` } type dbHtlc struct { @@ -121,85 +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, - AutoSettle: invoice.AutoSettle, }, - SettledAt: invoice.SettledAt, - State: unmarshallDbInvoiceState(invoice.State), - CancelledReason: unmarshallDbCancelledReason(invoice.CancelledReason), + SettledAt: invoice.SettledAt, + Settled: invoice.Settled, } } @@ -237,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, @@ -295,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) @@ -310,50 +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, - AutoSettle: invoice.AutoSettle, - } - - _, 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.