aperturedb: implement lnc sessions store

This commit is contained in:
positiveblue
2023-06-12 19:32:35 -07:00
parent ea2fb836b7
commit 0e02476dd2
2 changed files with 315 additions and 0 deletions

218
aperturedb/lnc_sessions.go Normal file
View File

@@ -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
}

View File

@@ -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)
}