diff --git a/aperture.go b/aperture.go index a78c5d9..cdcd2e0 100644 --- a/aperture.go +++ b/aperture.go @@ -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" @@ -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 ( @@ -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 @@ -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. @@ -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 { @@ -276,6 +291,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) + default: return fmt.Errorf("unknown database backend: %s", a.cfg.DatabaseBackend) @@ -283,25 +305,64 @@ func (a *Aperture) Start(errChan chan error) error { 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", + 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 + } } } @@ -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{ diff --git a/aperturedb/lnc_sessions.go b/aperturedb/lnc_sessions.go new file mode 100644 index 0000000..4f4c749 --- /dev/null +++ b/aperturedb/lnc_sessions.go @@ -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 +} diff --git a/aperturedb/lnc_sessions_test.go b/aperturedb/lnc_sessions_test.go new file mode 100644 index 0000000..5d7e20f --- /dev/null +++ b/aperturedb/lnc_sessions_test.go @@ -0,0 +1,97 @@ +package aperturedb + +import ( + "context" + "database/sql" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/aperture/lnc" + "github.com/lightninglabs/lightning-node-connect/mailbox" + "github.com/stretchr/testify/require" +) + +func newLNCSessionsStoreWithDB(db *BaseDB) *LNCSessionsStore { + dbTxer := NewTransactionExecutor(db, + func(tx *sql.Tx) LNCSessionsDB { + return db.WithTx(tx) + }, + ) + + return NewLNCSessionsStore(dbTxer) +} + +func TestLNCSessionsDB(t *testing.T) { + t.Parallel() + + ctxt, cancel := context.WithTimeout( + context.Background(), defaultTestTimeout, + ) + defer cancel() + + // First, create a new test database. + db := NewTestDB(t) + store := newLNCSessionsStoreWithDB(db.BaseDB) + + words, passphraseEntropy, err := mailbox.NewPassphraseEntropy() + require.NoError(t, err, "error creating passphrase") + + passphrase := strings.Join(words[:], " ") + mailboxAddr := "test-mailbox" + devServer := true + + session, err := lnc.NewSession(passphrase, mailboxAddr, devServer) + require.NoError(t, err, "error creating session") + + // A session needs to have a local static key set to be stored in the + // database. + err = store.AddSession(ctxt, session) + require.Error(t, err) + + localStatic, err := btcec.NewPrivateKey() + require.NoError(t, err, "error creating local static key") + session.LocalStaticPrivKey = localStatic + + // The db has a precision of microseconds, so we need to truncate the + // timestamp so we are able to capture that it was created AFTER this + // timestamp. + timestampBeforeCreation := time.Now().UTC().Truncate(time.Millisecond) + + err = store.AddSession(ctxt, session) + require.NoError(t, err, "error adding session") + require.True(t, session.CreatedAt.After(timestampBeforeCreation)) + + // Get the session from the database. + dbSession, err := store.GetSession(ctxt, passphraseEntropy[:]) + require.NoError(t, err, "error getting session") + require.Equal(t, session, dbSession, "sessions do not match") + + // Set the remote static key. + remoteStatic := localStatic.PubKey() + session.RemoteStaticPubKey = remoteStatic + + err = store.SetRemotePubKey( + ctxt, passphraseEntropy[:], remoteStatic.SerializeCompressed(), + ) + require.NoError(t, err, "error setting remote static key") + + // Set expiration date. + expiry := session.CreatedAt.Add(time.Hour).Truncate(time.Millisecond) + session.Expiry = &expiry + + err = store.SetExpiry(ctxt, passphraseEntropy[:], expiry) + require.NoError(t, err, "error setting expiry") + + // Next time we fetch the session, it should have the remote static key + // and the expiry set. + dbSession, err = store.GetSession(ctxt, passphraseEntropy[:]) + require.NoError(t, err, "error getting session") + require.Equal(t, session, dbSession, "sessions do not match") + + // Trying to get a session that does not exist should return a specific + // error. + _, err = store.GetSession(ctxt, []byte("non-existent")) + require.ErrorIs(t, err, lnc.ErrSessionNotFound) +} diff --git a/aperturedb/sqlc/lnc_sessions.sql.go b/aperturedb/sqlc/lnc_sessions.sql.go new file mode 100644 index 0000000..fa6b479 --- /dev/null +++ b/aperturedb/sqlc/lnc_sessions.sql.go @@ -0,0 +1,99 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 +// source: lnc_sessions.sql + +package sqlc + +import ( + "context" + "database/sql" + "time" +) + +const getSession = `-- name: GetSession :one +SELECT id, passphrase_words, passphrase_entropy, remote_static_pub_key, local_static_priv_key, mailbox_addr, created_at, expiry, dev_server +FROM lnc_sessions +WHERE passphrase_entropy = $1 +` + +func (q *Queries) GetSession(ctx context.Context, passphraseEntropy []byte) (LncSession, error) { + row := q.db.QueryRowContext(ctx, getSession, passphraseEntropy) + var i LncSession + err := row.Scan( + &i.ID, + &i.PassphraseWords, + &i.PassphraseEntropy, + &i.RemoteStaticPubKey, + &i.LocalStaticPrivKey, + &i.MailboxAddr, + &i.CreatedAt, + &i.Expiry, + &i.DevServer, + ) + return i, err +} + +const insertSession = `-- name: InsertSession :exec +INSERT INTO lnc_sessions ( + passphrase_words, passphrase_entropy, local_static_priv_key, mailbox_addr, + created_at, expiry, dev_server +) VALUES ( + $1, $2, $3, $4, $5, $6, $7 +) +` + +type InsertSessionParams struct { + PassphraseWords string + PassphraseEntropy []byte + LocalStaticPrivKey []byte + MailboxAddr string + CreatedAt time.Time + Expiry sql.NullTime + DevServer bool +} + +func (q *Queries) InsertSession(ctx context.Context, arg InsertSessionParams) error { + _, err := q.db.ExecContext(ctx, insertSession, + arg.PassphraseWords, + arg.PassphraseEntropy, + arg.LocalStaticPrivKey, + arg.MailboxAddr, + arg.CreatedAt, + arg.Expiry, + arg.DevServer, + ) + return err +} + +const setExpiry = `-- name: SetExpiry :exec +UPDATE lnc_sessions +SET expiry=$1 +WHERE passphrase_entropy=$2 +` + +type SetExpiryParams struct { + Expiry sql.NullTime + PassphraseEntropy []byte +} + +func (q *Queries) SetExpiry(ctx context.Context, arg SetExpiryParams) error { + _, err := q.db.ExecContext(ctx, setExpiry, arg.Expiry, arg.PassphraseEntropy) + return err +} + +const setRemotePubKey = `-- name: SetRemotePubKey :exec +UPDATE lnc_sessions +SET remote_static_pub_key=$1 +WHERE passphrase_entropy=$2 +` + +type SetRemotePubKeyParams struct { + RemoteStaticPubKey []byte + PassphraseEntropy []byte +} + +func (q *Queries) SetRemotePubKey(ctx context.Context, arg SetRemotePubKeyParams) error { + _, err := q.db.ExecContext(ctx, setRemotePubKey, arg.RemoteStaticPubKey, arg.PassphraseEntropy) + return err +} diff --git a/aperturedb/sqlc/migrations/000003_lnc_sessions.down.sql b/aperturedb/sqlc/migrations/000003_lnc_sessions.down.sql new file mode 100644 index 0000000..0eef9c1 --- /dev/null +++ b/aperturedb/sqlc/migrations/000003_lnc_sessions.down.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS lnc_sessions_passphrase_entropy_idx; +DROP INDEX IF EXISTS lnc_sessions_label_idx; +DROP TABLE IF EXISTS lnc_sessions; diff --git a/aperturedb/sqlc/migrations/000003_lnc_sessions.up.sql b/aperturedb/sqlc/migrations/000003_lnc_sessions.up.sql new file mode 100644 index 0000000..e2d5ac0 --- /dev/null +++ b/aperturedb/sqlc/migrations/000003_lnc_sessions.up.sql @@ -0,0 +1,32 @@ +-- lnc_sessions is table used to store data about LNC sesssions. +CREATE TABLE IF NOT EXISTS lnc_sessions ( + id INTEGER PRIMARY KEY, + + -- The passphrase words used to derive the passphrase entropy. + passphrase_words TEXT NOT NULL UNIQUE, + + -- The entropy bytes to be used for mask the local ephemeral key during the + -- first step of the Noise XX handshake. + passphrase_entropy BLOB NOT NULL UNIQUE, + + -- The remote static key being used for the connection. + remote_static_pub_key BLOB UNIQUE, + + -- The local static key being used for the connection. + local_static_priv_key BLOB NOT NULL, + + -- mailbox_addr is the address of the mailbox used for the session. + mailbox_addr TEXT NOT NULL, + + -- created_at is the time the session was created. + created_at TIMESTAMP NOT NULL, + + -- expiry is the time the session will expire. + expiry TIMESTAMP, + + -- dev_server signals if we need to skip the verification of the server's + -- tls certificate. + dev_server BOOL NOT NULL +); + +CREATE INDEX IF NOT EXISTS lnc_sessions_passphrase_entropy_idx ON lnc_sessions(passphrase_entropy); diff --git a/aperturedb/sqlc/models.go b/aperturedb/sqlc/models.go index 8d70fdd..651eb47 100644 --- a/aperturedb/sqlc/models.go +++ b/aperturedb/sqlc/models.go @@ -5,9 +5,22 @@ package sqlc import ( + "database/sql" "time" ) +type LncSession struct { + ID int32 + PassphraseWords string + PassphraseEntropy []byte + RemoteStaticPubKey []byte + LocalStaticPrivKey []byte + MailboxAddr string + CreatedAt time.Time + Expiry sql.NullTime + DevServer bool +} + type Onion struct { PrivateKey []byte CreatedAt time.Time diff --git a/aperturedb/sqlc/querier.go b/aperturedb/sqlc/querier.go index 50ffea5..b457178 100644 --- a/aperturedb/sqlc/querier.go +++ b/aperturedb/sqlc/querier.go @@ -12,8 +12,12 @@ type Querier interface { DeleteOnionPrivateKey(ctx context.Context) error DeleteSecretByHash(ctx context.Context, hash []byte) (int64, error) GetSecretByHash(ctx context.Context, hash []byte) ([]byte, error) + GetSession(ctx context.Context, passphraseEntropy []byte) (LncSession, error) InsertSecret(ctx context.Context, arg InsertSecretParams) (int32, error) + InsertSession(ctx context.Context, arg InsertSessionParams) error SelectOnionPrivateKey(ctx context.Context) ([]byte, error) + SetExpiry(ctx context.Context, arg SetExpiryParams) error + SetRemotePubKey(ctx context.Context, arg SetRemotePubKeyParams) error UpsertOnion(ctx context.Context, arg UpsertOnionParams) error } diff --git a/aperturedb/sqlc/queries/lnc_sessions.sql b/aperturedb/sqlc/queries/lnc_sessions.sql new file mode 100644 index 0000000..d9be1ec --- /dev/null +++ b/aperturedb/sqlc/queries/lnc_sessions.sql @@ -0,0 +1,22 @@ +-- name: InsertSession :exec +INSERT INTO lnc_sessions ( + passphrase_words, passphrase_entropy, local_static_priv_key, mailbox_addr, + created_at, expiry, dev_server +) VALUES ( + $1, $2, $3, $4, $5, $6, $7 +); + +-- name: GetSession :one +SELECT * +FROM lnc_sessions +WHERE passphrase_entropy = $1; + +-- name: SetRemotePubKey :exec +UPDATE lnc_sessions +SET remote_static_pub_key=$1 +WHERE passphrase_entropy=$2; + +-- name: SetExpiry :exec +UPDATE lnc_sessions +SET expiry=$1 +WHERE passphrase_entropy=$2; diff --git a/challenger/interface.go b/challenger/interface.go new file mode 100644 index 0000000..207a2c1 --- /dev/null +++ b/challenger/interface.go @@ -0,0 +1,38 @@ +package challenger + +import ( + "context" + + "github.com/lightninglabs/aperture/auth" + "github.com/lightninglabs/aperture/mint" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc" +) + +// InvoiceRequestGenerator is a function type that returns a new request for the +// lnrpc.AddInvoice call. +type InvoiceRequestGenerator func(price int64) (*lnrpc.Invoice, error) + +// InvoiceClient is an interface that only implements part of a full lnd client, +// namely the part around the invoices we need for the challenger to work. +type InvoiceClient interface { + // ListInvoices returns a paginated list of all invoices known to lnd. + ListInvoices(ctx context.Context, in *lnrpc.ListInvoiceRequest, + opts ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error) + + // SubscribeInvoices subscribes to updates on invoices. + SubscribeInvoices(ctx context.Context, in *lnrpc.InvoiceSubscription, + opts ...grpc.CallOption) ( + lnrpc.Lightning_SubscribeInvoicesClient, error) + + // AddInvoice adds a new invoice to lnd. + AddInvoice(ctx context.Context, in *lnrpc.Invoice, + opts ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) +} + +// Challenger is an interface that combines the mint.Challenger and the +// auth.InvoiceChecker interfaces. +type Challenger interface { + mint.Challenger + auth.InvoiceChecker +} diff --git a/challenger/lnc.go b/challenger/lnc.go new file mode 100644 index 0000000..d30891f --- /dev/null +++ b/challenger/lnc.go @@ -0,0 +1,84 @@ +package challenger + +import ( + "fmt" + "time" + + "github.com/lightninglabs/aperture/lnc" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntypes" +) + +// LNCChallenger is a challenger that uses LNC to connect to an lnd backend to +// create new LSAT payment challenges. +type LNCChallenger struct { + lndChallenger *LndChallenger + nodeConn *lnc.NodeConn +} + +// NewLNCChallenger creates a new challenger that uses the given LNC session to +// connect to an lnd backend to create payment challenges. +func NewLNCChallenger(session *lnc.Session, lncStore lnc.Store, + genInvoiceReq InvoiceRequestGenerator, + errChan chan<- error) (*LNCChallenger, error) { + + nodeConn, err := lnc.NewNodeConn(session, lncStore) + if err != nil { + return nil, fmt.Errorf("unable to connect to lnd using lnc: %w", + err) + } + + client, err := nodeConn.Client() + if err != nil { + return nil, err + } + + lndChallenger, err := NewLndChallenger( + client, genInvoiceReq, nodeConn.CtxFunc, errChan, + ) + if err != nil { + return nil, err + } + + err = lndChallenger.Start() + if err != nil { + return nil, err + } + + return &LNCChallenger{ + lndChallenger: lndChallenger, + nodeConn: nodeConn, + }, nil +} + +// Stop stops the challenger. +func (l *LNCChallenger) Stop() { + err := l.nodeConn.Stop() + if err != nil { + log.Errorf("unable to stop lnc node conn: %v", err) + } + + l.lndChallenger.Stop() +} + +// NewChallenge creates a new LSAT payment challenge, returning a payment +// request (invoice) and the corresponding payment hash. +// +// NOTE: This is part of the mint.Challenger interface. +func (l *LNCChallenger) NewChallenge(price int64) (string, lntypes.Hash, + error) { + + return l.lndChallenger.NewChallenge(price) +} + +// VerifyInvoiceStatus checks that an invoice identified by a payment +// hash has the desired status. To make sure we don't fail while the +// invoice update is still on its way, we try several times until either +// the desired status is set or the given timeout is reached. +// +// NOTE: This is part of the auth.InvoiceChecker interface. +func (l *LNCChallenger) VerifyInvoiceStatus(hash lntypes.Hash, + state lnrpc.Invoice_InvoiceState, timeout time.Duration) error { + + return l.lndChallenger.VerifyInvoiceStatus(hash, state, timeout) +} diff --git a/challenger.go b/challenger/lnd.go similarity index 82% rename from challenger.go rename to challenger/lnd.go index b1690eb..9e10a02 100644 --- a/challenger.go +++ b/challenger/lnd.go @@ -1,4 +1,4 @@ -package aperture +package challenger import ( "context" @@ -9,39 +9,15 @@ import ( "sync" "time" - "github.com/lightninglabs/aperture/auth" - "github.com/lightninglabs/aperture/mint" - "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" - "google.golang.org/grpc" ) -// InvoiceRequestGenerator is a function type that returns a new request for the -// lnrpc.AddInvoice call. -type InvoiceRequestGenerator func(price int64) (*lnrpc.Invoice, error) - -// InvoiceClient is an interface that only implements part of a full lnd client, -// namely the part around the invoices we need for the challenger to work. -type InvoiceClient interface { - // ListInvoices returns a paginated list of all invoices known to lnd. - ListInvoices(ctx context.Context, in *lnrpc.ListInvoiceRequest, - opts ...grpc.CallOption) (*lnrpc.ListInvoiceResponse, error) - - // SubscribeInvoices subscribes to updates on invoices. - SubscribeInvoices(ctx context.Context, in *lnrpc.InvoiceSubscription, - opts ...grpc.CallOption) ( - lnrpc.Lightning_SubscribeInvoicesClient, error) - - // AddInvoice adds a new invoice to lnd. - AddInvoice(ctx context.Context, in *lnrpc.Invoice, - opts ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) -} - // LndChallenger is a challenger that uses an lnd backend to create new LSAT // payment challenges. type LndChallenger struct { client InvoiceClient + clientCtx func() context.Context genInvoiceReq InvoiceRequestGenerator invoiceStates map[lntypes.Hash]lnrpc.Invoice_InvoiceState @@ -55,44 +31,45 @@ type LndChallenger struct { wg sync.WaitGroup } -// A compile time flag to ensure the LndChallenger satisfies the -// mint.Challenger and auth.InvoiceChecker interface. -var _ mint.Challenger = (*LndChallenger)(nil) -var _ auth.InvoiceChecker = (*LndChallenger)(nil) +// A compile time flag to ensure the LndChallenger satisfies the Challenger +// interface. +var _ Challenger = (*LndChallenger)(nil) -const ( - // invoiceMacaroonName is the name of the invoice macaroon belonging - // to the target lnd node. - invoiceMacaroonName = "invoice.macaroon" -) - -// NewLndChallenger creates a new challenger that uses the given connection -// details to connect to an lnd backend to create payment challenges. -func NewLndChallenger(cfg *AuthConfig, genInvoiceReq InvoiceRequestGenerator, +// NewLndChallenger creates a new challenger that uses the given connection to +// an lnd backend to create payment challenges. +func NewLndChallenger(client InvoiceClient, + genInvoiceReq InvoiceRequestGenerator, + ctxFunc func() context.Context, errChan chan<- error) (*LndChallenger, error) { - if genInvoiceReq == nil { - return nil, fmt.Errorf("genInvoiceReq cannot be nil") + // Make sure we have a valid context function. This will be called to + // create a new context for each call to the lnd client. + if ctxFunc == nil { + ctxFunc = context.Background } - client, err := lndclient.NewBasicClient( - cfg.LndHost, cfg.TLSPath, cfg.MacDir, cfg.Network, - lndclient.MacFilename(invoiceMacaroonName), - ) - if err != nil { - return nil, err + if genInvoiceReq == nil { + return nil, fmt.Errorf("genInvoiceReq cannot be nil") } invoicesMtx := &sync.Mutex{} - return &LndChallenger{ + challenger := &LndChallenger{ client: client, + clientCtx: ctxFunc, genInvoiceReq: genInvoiceReq, invoiceStates: make(map[lntypes.Hash]lnrpc.Invoice_InvoiceState), invoicesMtx: invoicesMtx, invoicesCond: sync.NewCond(invoicesMtx), quit: make(chan struct{}), errChan: errChan, - }, nil + } + + err := challenger.Start() + if err != nil { + return nil, fmt.Errorf("unable to start challenger: %w", err) + } + + return challenger, nil } // Start starts the challenger's main work which is to keep track of all @@ -111,8 +88,9 @@ func (l *LndChallenger) Start() error { // cache. We need to keep track of all invoices, even quite old ones to // make sure tokens are valid. But to save space we only keep track of // an invoice's state. + ctx := l.clientCtx() invoiceResp, err := l.client.ListInvoices( - context.Background(), &lnrpc.ListInvoiceRequest{ + ctx, &lnrpc.ListInvoiceRequest{ NumMaxInvoices: math.MaxUint64, }, ) @@ -145,7 +123,7 @@ func (l *LndChallenger) Start() error { l.invoicesMtx.Unlock() // We need to be able to cancel any subscription we make. - ctxc, cancel := context.WithCancel(context.Background()) + ctxc, cancel := context.WithCancel(l.clientCtx()) l.invoicesCancel = cancel subscriptionResp, err := l.client.SubscribeInvoices( @@ -261,7 +239,9 @@ func (l *LndChallenger) Stop() { // request (invoice) and the corresponding payment hash. // // NOTE: This is part of the mint.Challenger interface. -func (l *LndChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) { +func (l *LndChallenger) NewChallenge(price int64) (string, lntypes.Hash, + error) { + // Obtain a new invoice from lnd first. We need to know the payment hash // so we can add it as a caveat to the macaroon. invoice, err := l.genInvoiceReq(price) @@ -269,12 +249,14 @@ func (l *LndChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) log.Errorf("Error generating invoice request: %v", err) return "", lntypes.ZeroHash, err } - ctx := context.Background() + + ctx := l.clientCtx() response, err := l.client.AddInvoice(ctx, invoice) if err != nil { log.Errorf("Error adding invoice: %v", err) return "", lntypes.ZeroHash, err } + paymentHash, err := lntypes.MakeHash(response.RHash) if err != nil { log.Errorf("Error parsing payment hash: %v", err) diff --git a/challenger_test.go b/challenger/lnd_test.go similarity index 98% rename from challenger_test.go rename to challenger/lnd_test.go index f0bf7e0..82f7620 100644 --- a/challenger_test.go +++ b/challenger/lnd_test.go @@ -1,4 +1,4 @@ -package aperture +package challenger import ( "context" @@ -102,6 +102,7 @@ func newChallenger() (*LndChallenger, *mockInvoiceClient, chan error) { mainErrChan := make(chan error) return &LndChallenger{ client: mockClient, + clientCtx: context.Background, genInvoiceReq: genInvoiceReq, invoiceStates: make(map[lntypes.Hash]lnrpc.Invoice_InvoiceState), quit: make(chan struct{}), @@ -130,7 +131,7 @@ func TestLndChallenger(t *testing.T) { // First of all, test that the NewLndChallenger doesn't allow a nil // invoice generator function. errChan := make(chan error) - _, err := NewLndChallenger(nil, nil, errChan) + _, err := NewLndChallenger(nil, nil, nil, errChan) require.Error(t, err) // Now mock the lnd backend and create a challenger instance that we can diff --git a/challenger/log.go b/challenger/log.go new file mode 100644 index 0000000..99033f0 --- /dev/null +++ b/challenger/log.go @@ -0,0 +1,26 @@ +package challenger + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "CHLL" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/config.go b/config.go index eaab3f5..c607d21 100644 --- a/config.go +++ b/config.go @@ -37,6 +37,10 @@ type EtcdConfig struct { } type AuthConfig struct { + Network string `long:"network" description:"The network LND is connected to." choice:"regtest" choice:"simnet" choice:"testnet" choice:"mainnet"` + + Disable bool `long:"disable" description:"Whether to disable auth."` + // LndHost is the hostname of the LND instance to connect to. LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"` @@ -44,9 +48,17 @@ type AuthConfig struct { MacDir string `long:"macdir" description:"Directory containing LND instance's macaroons"` - Network string `long:"network" description:"The network LND is connected to." choice:"regtest" choice:"simnet" choice:"testnet" choice:"mainnet"` + // The one-time-use passphrase used to set up the connection. This field + // identifies the connection that will be used. + Passphrase string `long:"passphrase" description:"the lnc passphrase"` + + // MailboxAddress is the address of the mailbox that the client will + // use for the LNC connection. + MailboxAddress string `long:"mailboxaddress" description:"the host:port of the mailbox server to be used"` - Disable bool `long:"disable" description:"Whether to disable LND auth."` + // DevServer set to true to skip verification of the mailbox server's + // tls cert. + DevServer bool `long:"devserver" description:"set to true to skip verification of the server's tls cert."` } func (a *AuthConfig) validate() error { @@ -55,6 +67,30 @@ func (a *AuthConfig) validate() error { return nil } + switch { + // If LndHost is set we connect directly to the LND node. + case a.LndHost != "": + log.Info("Validating lnd configuration") + + if a.Passphrase != "" { + return errors.New("passphrase field cannot be set " + + "when connecting directly to the lnd node") + } + + return a.validateLNDAuth() + + // If Passphrase is set we connect to the LND node through LNC. + case a.Passphrase != "": + log.Info("Validating lnc configuration") + return a.validateLNCAuth() + + default: + return errors.New("invalid authenticator configuration") + } +} + +// validateLNDAuth validates the direct LND auth configuration. +func (a *AuthConfig) validateLNDAuth() error { if a.LndHost == "" { return errors.New("lnd host required") } @@ -70,6 +106,22 @@ func (a *AuthConfig) validate() error { return nil } +// validateLNCAuth validates the LNC auth configuration. +func (a *AuthConfig) validateLNCAuth() error { + switch { + case a.Passphrase == "": + return errors.New("lnc passphrase required") + + case a.MailboxAddress == "": + return errors.New("lnc mailbox address required") + + case a.Network == "": + return errors.New("lnc network required") + } + + return nil +} + type HashMailConfig struct { Enabled bool `long:"enabled"` MessageRate time.Duration `long:"messagerate" description:"The average minimum time that should pass between each message."` @@ -120,6 +172,8 @@ type Config struct { // Etcd is the configuration section for the Etcd database backend. Etcd *EtcdConfig `group:"etcd" namespace:"etcd"` + // Authenticator is the configuration section for connecting directly + // to the LND node. Authenticator *AuthConfig `group:"authenticator" namespace:"authenticator"` Tor *TorConfig `group:"tor" namespace:"tor"` @@ -151,8 +205,10 @@ type Config struct { } func (c *Config) validate() error { - if err := c.Authenticator.validate(); err != nil { - return err + if !c.Authenticator.Disable { + if err := c.Authenticator.validate(); err != nil { + return err + } } if c.ListenAddr == "" { diff --git a/go.mod b/go.mod index ef9716f..aba8e93 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jessevdk/go-flags v1.4.0 github.com/lib/pq v1.10.7 + github.com/lightninglabs/lightning-node-connect v0.2.5-alpha github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 github.com/lightninglabs/lndclient v0.16.0-10 github.com/lightningnetwork/lnd v0.16.0-beta @@ -25,6 +26,7 @@ require ( github.com/lightningnetwork/lnd/clock v1.1.0 github.com/lightningnetwork/lnd/tlv v1.1.0 github.com/lightningnetwork/lnd/tor v1.1.0 + github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 github.com/ory/dockertest/v3 v3.10.0 github.com/prometheus/client_golang v1.11.1 github.com/stretchr/testify v1.8.1 @@ -35,6 +37,7 @@ require ( golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 + gopkg.in/macaroon-bakery.v2 v2.1.0 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 modernc.org/sqlite v1.20.3 @@ -180,7 +183,6 @@ require ( golang.org/x/tools v0.9.1 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect gopkg.in/errgo.v1 v1.0.1 // indirect - gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect @@ -192,5 +194,6 @@ require ( modernc.org/opt v0.1.3 // indirect modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.0.1 // indirect + nhooyr.io/websocket v1.8.7 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 1e758c3..e131956 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,10 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -221,8 +225,21 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -262,6 +279,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -303,6 +321,7 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -387,6 +406,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -413,6 +433,7 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -434,6 +455,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -443,6 +466,8 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/lightning-node-connect v0.2.5-alpha h1:ZRVChwczFXK0CEbxOCWwUA6TIZvrkE0APd1T3WjFAwg= +github.com/lightninglabs/lightning-node-connect v0.2.5-alpha/go.mod h1:A9Pof9fETkH+F67BnOmrBDThPKstqp73wlImWOZvTXQ= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= github.com/lightninglabs/lndclient v0.16.0-10 h1:cMBJNfssBQtpgYIu23QLP/qw0ijiT5SBZffnXz8zjJk= @@ -509,6 +534,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 h1:62uLwA3l2JMH84liO4ZhnjTH5PjFyCYxbHLgXPaJMtI= +github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9/go.mod h1:MvMXoufZAtqExNexqi4cjrNYE9MefKddKylxjS+//n0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -618,6 +645,10 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= @@ -741,6 +772,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -791,6 +823,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -866,9 +900,13 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -957,6 +995,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= @@ -1021,6 +1060,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= @@ -1038,6 +1078,7 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= @@ -1071,8 +1112,8 @@ gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/macaroon-bakery.v2 v2.0.1 h1:0N1TlEdfLP4HXNCg7MQUMp5XwvOoxk+oe9Owr2cpvsc= -gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon-bakery.v2 v2.1.0 h1:9Jw/+9XHBSutkaeVpWhDx38IcSNLJwWUICkOK98DHls= +gopkg.in/macaroon-bakery.v2 v2.1.0/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= @@ -1101,6 +1142,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= @@ -1125,6 +1167,8 @@ modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/lnc/lnc.go b/lnc/lnc.go new file mode 100644 index 0000000..cdbf14b --- /dev/null +++ b/lnc/lnc.go @@ -0,0 +1,323 @@ +package lnc + +import ( + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-node-connect/mailbox" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/mwitkow/grpc-proxy/proxy" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + "gopkg.in/macaroon.v2" +) + +var ( + // DefaultConnectionTimetout is the default timeout for a connection + // attempt. + DefaultConnectionTimetout = time.Second * 10 + + // DefaultStoreTimetout is the default timeout for a db transaction. + DefaultStoreTimetout = time.Second * 10 +) + +// HeaderMacaroon is the HTTP header field name that is used to send +// the macaroon. +const HeaderMacaroon = "Macaroon" + +// conn is a connection to a remote LND node. +type conn struct { + client lnrpc.LightningClient + grpcClient *grpc.ClientConn + creds credentials.PerRPCCredentials + cancel func() + + stop sync.Once +} + +// Close closes the underlying gRPC connection. +func (c *conn) Close() error { + var err error + c.stop.Do(func() { + err = c.grpcClient.Close() + c.cancel() + }) + + return err +} + +// NodeConn handles all the connection logic to a remote LND node using LNC. +type NodeConn struct { + // store is the session store. + store Store + + // session is the session that is currently open. + session *Session + + // conn is the underlying connection to the remote node. + conn *conn + + // macStr is the macaroon is used to authenticate the connection encoded + // as a hex string. + macStr string +} + +// NewNodeConn creates a new NodeConn instance. +func NewNodeConn(session *Session, store Store) (*NodeConn, error) { + ctxt, cancel := context.WithTimeout( + context.Background(), DefaultStoreTimetout, + ) + defer cancel() + + dbSession, err := store.GetSession(ctxt, session.PassphraseEntropy) + switch { + case errors.Is(err, ErrSessionNotFound): + localStatic, err := btcec.NewPrivateKey() + if err != nil { + return nil, fmt.Errorf("unable to generate local "+ + "static key: %w", err) + } + + session.LocalStaticPrivKey = localStatic + + err = store.AddSession(ctxt, session) + if err != nil { + return nil, fmt.Errorf("unable to add new session: %w", + err) + } + + case err != nil: + return nil, fmt.Errorf("unable to get session(%v): %w", + session.PassphraseEntropy, err) + + default: + session = dbSession + } + + nodeConn := &NodeConn{ + store: store, + session: session, + } + + conn, err := nodeConn.newConn(session) + if err != nil { + return nil, err + } + + nodeConn.conn = conn + + return nodeConn, nil +} + +// CloseConn closes the connection with the remote node. +func (n *NodeConn) CloseConn() error { + if n.conn == nil { + return fmt.Errorf("connection not open") + } + + err := n.conn.Close() + if err != nil { + return fmt.Errorf("unable to close connection: %w", err) + } + + return nil +} + +// Stop closes the connection with the remote node if it is open. +func (n *NodeConn) Stop() error { + if n.conn != nil { + return n.CloseConn() + } + + return nil +} + +// Client returns the gRPC client to the remote node. +func (n *NodeConn) Client() (lnrpc.LightningClient, error) { + if n.conn == nil { + return nil, fmt.Errorf("connection not open") + } + + return n.conn.client, nil +} + +// CtxFunc returns the context that needs to be used whenever the internal +// Client is used. +func (n *NodeConn) CtxFunc() context.Context { + ctx := context.Background() + return metadata.AppendToOutgoingContext(ctx, HeaderMacaroon, n.macStr) +} + +// onRemoteStatic is called when the remote static key is received. +// +// NOTE: this function is a callback to be used by the mailbox package during +// the mailbox.NewConnData call. +func (n *NodeConn) onRemoteStatic(key *btcec.PublicKey) error { + ctxt, cancel := context.WithTimeout( + context.Background(), time.Second*10, + ) + defer cancel() + + remoteKey := key.SerializeCompressed() + + err := n.store.SetRemotePubKey( + ctxt, n.session.PassphraseEntropy, remoteKey, + ) + if err != nil { + log.Errorf("unable to set remote pub key for session(%x): %w", + n.session.PassphraseEntropy, err) + } + + return err +} + +// onAuthData is called when the auth data is received. +// +// NOTE: this function is a callback to be used by the mailbox package during +// the mailbox.NewConnData call. +func (n *NodeConn) onAuthData(data []byte) error { + mac, err := extractMacaroon(data) + if err != nil { + log.Errorf("unable to extract macaroon for session(%x): %w", + n.session.PassphraseEntropy, err) + + return err + } + + macBytes, err := mac.MarshalBinary() + if err != nil { + log.Errorf("unable to marshal macaroon for session(%x): %w", + n.session.PassphraseEntropy, err) + + return err + } + + // TODO(positiveblue): check that the macaroon has all the needed + // permissions. + n.macStr = hex.EncodeToString(macBytes) + + // If we already know the expiry time for this session there is no need + // to parse the macaroon to obtain it. + if n.session.Expiry != nil { + return nil + } + + // If the macaroon does not contain an expiry time there is nothing to + // do. + expiry, found := checkers.ExpiryTime(nil, mac.Caveats()) + if !found { + return nil + } + + // We always store time in the db in UTC. + expiry = expiry.UTC() + + // When we store the expiry time in the db we lose the precision to + // microseconds, but we can store the correct one here. + n.session.Expiry = &expiry + + ctxb := context.Background() + err = n.store.SetExpiry(ctxb, n.session.PassphraseEntropy, expiry) + if err != nil { + log.Errorf("unable to set expiry for session(%x): %w", + n.session.PassphraseEntropy, err) + } + + return nil +} + +// newConn creates an LNC connection. +func (n *NodeConn) newConn(session *Session, opts ...grpc.DialOption) (*conn, + error) { + + localKey := &keychain.PrivKeyECDH{PrivKey: session.LocalStaticPrivKey} + + // remoteKey can be nil if this is the first time the session is used. + remoteKey := session.RemoteStaticPubKey + entropy := session.PassphraseEntropy + + connData := mailbox.NewConnData( + localKey, remoteKey, entropy, nil, n.onRemoteStatic, + n.onAuthData, + ) + + noiseConn := mailbox.NewNoiseGrpcConn(connData) + + tlsConfig := &tls.Config{} + if session.DevServer { + tlsConfig = &tls.Config{InsecureSkipVerify: true} + } + + ctxc, cancel := context.WithCancel(context.Background()) + transportConn, err := mailbox.NewGrpcClient( + ctxc, session.MailboxAddr, connData, + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + ) + if err != nil { + cancel() + return nil, err + } + + dialOpts := []grpc.DialOption{ + // From the grpcProxy doc: This codec is *crucial* to the + // functioning of the proxy. + grpc.WithCodec(proxy.Codec()), // nolint + grpc.WithContextDialer(transportConn.Dial), + grpc.WithTransportCredentials(noiseConn), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(1024 * 1024 * 200), + ), + grpc.WithBlock(), + } + dialOpts = append(dialOpts, opts...) + + grpcClient, err := grpc.DialContext( + ctxc, session.MailboxAddr, dialOpts..., + ) + if err != nil { + cancel() + return nil, err + } + + return &conn{ + client: lnrpc.NewLightningClient(grpcClient), + grpcClient: grpcClient, + creds: noiseConn, + cancel: cancel, + }, nil +} + +// extractMacaroon is a helper function that extracts a macaroon from raw bytes. +func extractMacaroon(authData []byte) (*macaroon.Macaroon, error) { + // The format of the authData is "Macaroon: ". + parts := strings.Split(string(authData), ": ") + if len(parts) != 2 || parts[0] != HeaderMacaroon { + return nil, fmt.Errorf("authdata does not contain a macaroon") + } + + macBytes, err := hex.DecodeString(parts[1]) + if err != nil { + return nil, err + } + + if len(macBytes) == 0 { + return nil, fmt.Errorf("no macaroon received during connman") + } + + mac := &macaroon.Macaroon{} + if err = mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %v", err) + } + + return mac, nil +} diff --git a/lnc/log.go b/lnc/log.go new file mode 100644 index 0000000..3f5ad9e --- /dev/null +++ b/lnc/log.go @@ -0,0 +1,26 @@ +package lnc + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "LNCS" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/lnc/session.go b/lnc/session.go new file mode 100644 index 0000000..77da8bb --- /dev/null +++ b/lnc/session.go @@ -0,0 +1,70 @@ +package lnc + +import ( + "fmt" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lightning-node-connect/mailbox" +) + +// Session contains all the information needed for an LNC connection. +type Session struct { + // PassphraseWords is the list of words the PassphraseEntropy is derived + // from. + PassphraseWords string + + // PassphraseEntropy is the entropy. This field identifies the session. + PassphraseEntropy []byte + + // RemoteStaticPubKey is the public key of the remote peer. + RemoteStaticPubKey *btcec.PublicKey + + // LocalStaticPrivKey is the private key of the local peer. + LocalStaticPrivKey *btcec.PrivateKey + + // MailboxAddr is the address of the mailbox server. + MailboxAddr string + + // CreatedAt is the time the session was added to the database. + CreatedAt time.Time + + // Expiry is the time the session will expire. + Expiry *time.Time + + // DevServer signals if we need to skip the verification of the server's + // tls certificate. + DevServer bool +} + +// NewSession creates a new non-initialized session. +func NewSession(passphrase, mailboxAddr string, devServer bool) (*Session, + error) { + + switch { + case passphrase == "": + return nil, fmt.Errorf("passphrase cannot be empty") + + case mailboxAddr == "": + return nil, fmt.Errorf("mailbox address cannot be empty") + } + + words := strings.Split(passphrase, " ") + if len(words) != mailbox.NumPassphraseWords { + return nil, fmt.Errorf("invalid passphrase. Expected %d "+ + "words, got %d", mailbox.NumPassphraseWords, + len(words)) + } + + var mnemonicWords [mailbox.NumPassphraseWords]string + copy(mnemonicWords[:], words) + entropy := mailbox.PassphraseMnemonicToEntropy(mnemonicWords) + + return &Session{ + PassphraseWords: passphrase, + PassphraseEntropy: entropy[:], + MailboxAddr: mailboxAddr, + DevServer: devServer, + }, nil +} diff --git a/lnc/store.go b/lnc/store.go new file mode 100644 index 0000000..b9558f5 --- /dev/null +++ b/lnc/store.go @@ -0,0 +1,32 @@ +package lnc + +import ( + "context" + "errors" + "time" +) + +var ( + // ErrSessionNotFound is returned when a session is not found in the + // database. + ErrSessionNotFound = errors.New("session not found") +) + +// Store represents access to a persistent session store. +type Store interface { + // AddSession adds a record for a new session in the database. + AddSession(ctx context.Context, session *Session) error + + // GetSession retrieves the session record matching the passphrase + // entropy. + GetSession(ctx context.Context, + passphraseEntropy []byte) (*Session, error) + + // SetRemotePubKey sets the remote public key for a session. + SetRemotePubKey(ctx context.Context, passphraseEntropy, + remotePubKey []byte) error + + // SetExpiry sets the expiry time for a session. + SetExpiry(ctx context.Context, passphraseEntroy []byte, + expiry time.Time) error +} diff --git a/mint/mint.go b/mint/mint.go index 21b5dae..41bad66 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -29,6 +29,9 @@ type Challenger interface { // to avoid having to decode the payment request in order to retrieve // its payment hash. NewChallenge(price int64) (string, lntypes.Hash, error) + + // Stop shuts down the challenger. + Stop() } // SecretStore is the store responsible for storing LSAT secrets. These secrets @@ -36,12 +39,14 @@ type Challenger interface { type SecretStore interface { // NewSecret creates a new cryptographically random secret which is // keyed by the given hash. - NewSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error) + NewSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, + error) // GetSecret returns the cryptographically random secret that // corresponds to the given hash. If there is no secret, then // ErrSecretNotFound is returned. - GetSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error) + GetSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, + error) // RevokeSecret removes the cryptographically random secret that // corresponds to the given hash. This acts as a NOP if the secret does @@ -55,16 +60,19 @@ type ServiceLimiter interface { // ServiceCapabilities returns the capabilities caveats for each // service. This determines which capabilities of each service can be // accessed. - ServiceCapabilities(context.Context, ...lsat.Service) ([]lsat.Caveat, error) + ServiceCapabilities(context.Context, ...lsat.Service) ([]lsat.Caveat, + error) // ServiceConstraints returns the constraints for each service. This // enforces additional constraints on a particular service/service // capability. - ServiceConstraints(context.Context, ...lsat.Service) ([]lsat.Caveat, error) + ServiceConstraints(context.Context, ...lsat.Service) ([]lsat.Caveat, + error) // ServiceTimeouts returns the timeout caveat for each service. This // will determine if and when service access can expire. - ServiceTimeouts(context.Context, ...lsat.Service) ([]lsat.Caveat, error) + ServiceTimeouts(context.Context, ...lsat.Service) ([]lsat.Caveat, + error) } // Config packages all of the required dependencies to instantiate a new LSAT @@ -245,7 +253,9 @@ type VerificationParams struct { } // VerifyLSAT attempts to verify an LSAT with the given parameters. -func (m *Mint) VerifyLSAT(ctx context.Context, params *VerificationParams) error { +func (m *Mint) VerifyLSAT(ctx context.Context, + params *VerificationParams) error { + // We'll first perform a quick check to determine if a valid preimage // was provided. id, err := lsat.DecodeIdentifier(bytes.NewReader(params.Macaroon.Id())) diff --git a/mint/mock_test.go b/mint/mock_test.go index 6156540..e2a0b26 100644 --- a/mint/mock_test.go +++ b/mint/mock_test.go @@ -26,7 +26,17 @@ func newMockChallenger() *mockChallenger { return &mockChallenger{} } -func (d *mockChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) { +func (d *mockChallenger) Start() error { + return nil +} + +func (d *mockChallenger) Stop() { + // Nothing to do here. +} + +func (d *mockChallenger) NewChallenge(price int64) (string, lntypes.Hash, + error) { + return testPayReq, testHash, nil } diff --git a/sample-conf.yaml b/sample-conf.yaml index 48411d4..b01bca6 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -37,6 +37,18 @@ authenticator: # The chain network the lnd is active on. network: "simnet" + + # The LNC connection passphrase. + passphrase: "my-own-passphrase" + + # The host:port of the mailbox server to be used. + mailboxaddress: "mailbox.terminal.lightning.today:443" + + # Set to true to skip verification of the mailbox server's tls cert. + devserver: false + + # Set to true to disable any auth. + disable: false # The selected database backend. The current default backend is "sqlite". # Aperture also has support for postgres and etcd. @@ -65,7 +77,6 @@ postgres: # server. requireSSL: true - # Settings for the etcd instance which the proxy will use to reliably store and # retrieve token information. etcd: