From 0e02476dd2239df08b87ac58ef8a107a80399a91 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Mon, 12 Jun 2023 19:32:35 -0700 Subject: [PATCH] aperturedb: implement lnc sessions store --- aperturedb/lnc_sessions.go | 218 ++++++++++++++++++++++++++++++++ aperturedb/lnc_sessions_test.go | 97 ++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 aperturedb/lnc_sessions.go create mode 100644 aperturedb/lnc_sessions_test.go 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) +}