Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Use LNC to connect to the LND node #98

Merged
merged 9 commits into from
Jul 5, 2023
99 changes: 80 additions & 19 deletions aperture.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import (
flags "github.com/jessevdk/go-flags"
"github.com/lightninglabs/aperture/aperturedb"
"github.com/lightninglabs/aperture/auth"
"github.com/lightninglabs/aperture/challenger"
"github.com/lightninglabs/aperture/lnc"
"github.com/lightninglabs/aperture/mint"
"github.com/lightninglabs/aperture/proxy"
"github.com/lightninglabs/lightning-node-connect/hashmailrpc"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/cert"
Expand Down Expand Up @@ -75,6 +78,10 @@ const (
// hashMailRESTPrefix is the prefix a REST request URI has when it is
// meant for the hashmailrpc server to be handled.
hashMailRESTPrefix = "/v1/lightning-node-connect/hashmail"

// invoiceMacaroonName is the name of the invoice macaroon belonging
// to the target lnd node.
invoiceMacaroonName = "invoice.macaroon"
)

var (
Expand Down Expand Up @@ -162,7 +169,7 @@ type Aperture struct {

etcdClient *clientv3.Client
db *sql.DB
challenger *LndChallenger
challenger challenger.Challenger
httpsServer *http.Server
torHTTPServer *http.Server
proxy *proxy.Proxy
Expand Down Expand Up @@ -213,6 +220,7 @@ func (a *Aperture) Start(errChan chan error) error {
var (
secretStore mint.SecretStore
onionStore tor.OnionStore
lncStore lnc.Store
)

// Connect to the chosen database backend.
Expand Down Expand Up @@ -254,6 +262,13 @@ func (a *Aperture) Start(errChan chan error) error {
)
onionStore = aperturedb.NewOnionStore(dbOnionTxer)

dbLNCTxer := aperturedb.NewTransactionExecutor(db,
func(tx *sql.Tx) aperturedb.LNCSessionsDB {
return db.WithTx(tx)
},
)
lncStore = aperturedb.NewLNCSessionsStore(dbLNCTxer)

case "sqlite":
db, err := aperturedb.NewSqliteStore(a.cfg.Sqlite)
if err != nil {
Expand All @@ -276,32 +291,78 @@ func (a *Aperture) Start(errChan chan error) error {
)
onionStore = aperturedb.NewOnionStore(dbOnionTxer)

dbLNCTxer := aperturedb.NewTransactionExecutor(db,
func(tx *sql.Tx) aperturedb.LNCSessionsDB {
return db.WithTx(tx)
},
)
lncStore = aperturedb.NewLNCSessionsStore(dbLNCTxer)

default:
return fmt.Errorf("unknown database backend: %s",
a.cfg.DatabaseBackend)
}

log.Infof("Using %v as database backend", a.cfg.DatabaseBackend)

// Create our challenger that uses our backing lnd node to create
// invoices and check their settlement status.
genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) {
return &lnrpc.Invoice{
Memo: "LSAT",
Value: price,
}, nil
}

if !a.cfg.Authenticator.Disable {
a.challenger, err = NewLndChallenger(
a.cfg.Authenticator, genInvoiceReq, errChan,
)
if err != nil {
return err
authCfg := a.cfg.Authenticator
genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) {
return &lnrpc.Invoice{
Memo: "LSAT",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but in the future we can insert relevant endpoint/context information here so someone can build a UI/dashboard on top of lnd/aperture to do things like track the amount earned from a given endpoint, etc.

To do this, we'd need to expose the memo as part of the challenger interface, as only much later in the pipeline do we have the full request context information.

Value: price,
}, nil
}
err = a.challenger.Start()
if err != nil {
return err

switch {
case authCfg.Passphrase != "":
log.Infof("Using lnc's authenticator config")

if a.cfg.DatabaseBackend == "etcd" {
return fmt.Errorf("etcd is not supported as " +
"a database backend for lnc " +
"connections")
}

session, err := lnc.NewSession(
authCfg.Passphrase, authCfg.MailboxAddress,
authCfg.DevServer,
)
if err != nil {
return fmt.Errorf("unable to create lnc "+
"session: %w", err)
}

a.challenger, err = challenger.NewLNCChallenger(
session, lncStore, genInvoiceReq, errChan,
)
if err != nil {
return fmt.Errorf("unable to start lnc "+
"challenger: %w", err)
}

case authCfg.LndHost != "":
log.Infof("Using lnd's authenticator config")

authCfg := a.cfg.Authenticator
client, err := lndclient.NewBasicClient(
authCfg.LndHost, authCfg.TLSPath,
authCfg.MacDir, authCfg.Network,
lndclient.MacFilename(
invoiceMacaroonName,
),
)
if err != nil {
return err
}

a.challenger, err = challenger.NewLndChallenger(
client, genInvoiceReq, context.Background,
errChan,
)
if err != nil {
return err
}
}
}

Expand Down Expand Up @@ -738,7 +799,7 @@ func initTorListener(cfg *Config, store tor.OnionStore) (*tor.Controller,
}

// createProxy creates the proxy with all the services it needs.
func createProxy(cfg *Config, challenger *LndChallenger,
func createProxy(cfg *Config, challenger challenger.Challenger,
store mint.SecretStore) (*proxy.Proxy, func(), error) {

minter := mint.New(&mint.Config{
Expand Down
218 changes: 218 additions & 0 deletions aperturedb/lnc_sessions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package aperturedb

import (
"context"
"database/sql"
"fmt"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/aperture/aperturedb/sqlc"
"github.com/lightninglabs/aperture/lnc"
"github.com/lightningnetwork/lnd/clock"
)

type (
NewLNCSession = sqlc.InsertSessionParams

SetRemoteParams = sqlc.SetRemotePubKeyParams

SetExpiryParams = sqlc.SetExpiryParams
)

// LNCSessionsDB is an interface that defines the set of operations that can be
// executed agaist the lnc sessions database.
type LNCSessionsDB interface {
// InsertLNCSession inserts a new session into the database.
InsertSession(ctx context.Context, arg NewLNCSession) error

// GetLNCSession returns the session tagged with the given passphrase
// entropy.
GetSession(ctx context.Context,
passphraseEntropy []byte) (sqlc.LncSession, error)

// SetRemotePubKey sets the remote public key for the session.
SetRemotePubKey(ctx context.Context,
arg SetRemoteParams) error

// SetExpiry sets the expiry for the session.
SetExpiry(ctx context.Context, arg SetExpiryParams) error
}

// LNCSessionsDBTxOptions defines the set of db txn options the LNCSessionsDB
// understands.
type LNCSessionsDBTxOptions struct {
// readOnly governs if a read only transaction is needed or not.
readOnly bool
}

// ReadOnly returns true if the transaction should be read only.
//
// NOTE: This implements the TxOptions
func (a *LNCSessionsDBTxOptions) ReadOnly() bool {
return a.readOnly
}

// NewLNCSessionsDBReadTx creates a new read transaction option set.
func NewLNCSessionsDBReadTx() LNCSessionsDBTxOptions {
return LNCSessionsDBTxOptions{
readOnly: true,
}
}

// BatchedLNCSessionsDB is a version of the LNCSecretsDB that's capable of
// batched database operations.
type BatchedLNCSessionsDB interface {
LNCSessionsDB

BatchedTx[LNCSessionsDB]
}

// LNCSessionsStore represents a storage backend.
type LNCSessionsStore struct {
db BatchedLNCSessionsDB
clock clock.Clock
}

// NewSecretsStore creates a new SecretsStore instance given a open
// BatchedSecretsDB storage backend.
func NewLNCSessionsStore(db BatchedLNCSessionsDB) *LNCSessionsStore {
return &LNCSessionsStore{
db: db,
clock: clock.NewDefaultClock(),
}
}

// AddSession adds a new session to the database.
func (l *LNCSessionsStore) AddSession(ctx context.Context,
session *lnc.Session) error {

if session.LocalStaticPrivKey == nil {
return fmt.Errorf("local static private key is required")
}

localPrivKey := session.LocalStaticPrivKey.Serialize()
createdAt := l.clock.Now().UTC().Truncate(time.Microsecond)

var writeTxOpts LNCSessionsDBTxOptions
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
params := sqlc.InsertSessionParams{
PassphraseWords: session.PassphraseWords,
PassphraseEntropy: session.PassphraseEntropy,
LocalStaticPrivKey: localPrivKey,
MailboxAddr: session.MailboxAddr,
CreatedAt: createdAt,
DevServer: session.DevServer,
}

return tx.InsertSession(ctx, params)
})
if err != nil {
return fmt.Errorf("failed to insert new session: %v", err)
}

session.CreatedAt = createdAt

return nil
}

// GetSession returns the session tagged with the given label.
func (l *LNCSessionsStore) GetSession(ctx context.Context,
passphraseEntropy []byte) (*lnc.Session, error) {

var session *lnc.Session

readTx := NewLNCSessionsDBReadTx()
err := l.db.ExecTx(ctx, &readTx, func(tx LNCSessionsDB) error {
dbSession, err := tx.GetSession(ctx, passphraseEntropy)
switch {
case err == sql.ErrNoRows:
return lnc.ErrSessionNotFound

case err != nil:
return err

}

privKey, _ := btcec.PrivKeyFromBytes(
dbSession.LocalStaticPrivKey,
)
session = &lnc.Session{
PassphraseWords: dbSession.PassphraseWords,
PassphraseEntropy: dbSession.PassphraseEntropy,
LocalStaticPrivKey: privKey,
MailboxAddr: dbSession.MailboxAddr,
CreatedAt: dbSession.CreatedAt,
DevServer: dbSession.DevServer,
}

if dbSession.RemoteStaticPubKey != nil {
pubKey, err := btcec.ParsePubKey(
dbSession.RemoteStaticPubKey,
)
if err != nil {
return fmt.Errorf("failed to parse remote "+
"public key for session(%x): %w",
dbSession.PassphraseEntropy, err)
}

session.RemoteStaticPubKey = pubKey
}

if dbSession.Expiry.Valid {
expiry := dbSession.Expiry.Time
session.Expiry = &expiry
}

return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get session: %w", err)
}

return session, nil
}

// SetRemotePubKey sets the remote public key for a session.
func (l *LNCSessionsStore) SetRemotePubKey(ctx context.Context,
passphraseEntropy, remotePubKey []byte) error {

var writeTxOpts LNCSessionsDBTxOptions
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
params := SetRemoteParams{
PassphraseEntropy: passphraseEntropy,
RemoteStaticPubKey: remotePubKey,
}
return tx.SetRemotePubKey(ctx, params)
})
if err != nil {
return fmt.Errorf("failed to set remote pub key to "+
"session(%x): %w", passphraseEntropy, err)
}

return nil
}

// SetExpiry sets the expiry time for a session.
func (l *LNCSessionsStore) SetExpiry(ctx context.Context,
passphraseEntropy []byte, expiry time.Time) error {

var writeTxOpts LNCSessionsDBTxOptions
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
params := SetExpiryParams{
PassphraseEntropy: passphraseEntropy,
Expiry: sql.NullTime{
Time: expiry,
Valid: true,
},
}

return tx.SetExpiry(ctx, params)
})
if err != nil {
return fmt.Errorf("failed to set expiry time to session(%x): "+
"%w", passphraseEntropy, err)
}

return nil
}
Loading