Skip to content

Commit

Permalink
Merge pull request #9 from bottlepay/stateless
Browse files Browse the repository at this point in the history
stateless invoices
  • Loading branch information
joostjager committed Jul 1, 2022
2 parents 586ce69 + 799c333 commit 26f2fab
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 841 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,14 +26,16 @@ 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:

`grpcurl -plaintext -v -d '{"amt_msat":20000, "expiry_secs":600}' localhost:19090 lnmux.Service.AddInvoice`.

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`.
```
{
Expand Down Expand Up @@ -110,13 +112,16 @@ 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

The minimal setup to test on regtest is to create three LND nodes A, B and C. Create channels between B and A and between B and C. Connect `lnmuxd` to the nodes A and C. Pay invoices from node B, while experimenting with online status and liquidity of A and C.

## 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.


3 changes: 3 additions & 0 deletions cmd/lnmuxd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
214 changes: 59 additions & 155 deletions cmd/lnmuxd/lnmux_proto/lnmux.pb.go

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions cmd/lnmuxd/lnmux_proto/lnmux.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 3 additions & 19 deletions cmd/lnmuxd/lnmux_proto/lnmux.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions cmd/lnmuxd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)

Expand Down
44 changes: 1 addition & 43 deletions cmd/lnmuxd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"time"

"github.com/bottlepay/lnmux"
Expand Down Expand Up @@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand All @@ -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[:],
Expand Down
7 changes: 0 additions & 7 deletions config.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
47 changes: 30 additions & 17 deletions invoice_creator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package lnmux

import (
"crypto/rand"
"encoding/binary"
"time"

"github.com/btcsuite/btcd/btcec/v2"
Expand All @@ -17,6 +17,8 @@ import (
"github.com/bottlepay/lnmux/types"
)

var byteOrder = binary.BigEndian

const virtualChannel = 12345

type InvoiceCreatorConfig struct {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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...,
)
Expand All @@ -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
}
Loading

0 comments on commit 26f2fab

Please sign in to comment.