Skip to content

Commit 0e02476

Browse files
committed
aperturedb: implement lnc sessions store
1 parent ea2fb83 commit 0e02476

File tree

2 files changed

+315
-0
lines changed

2 files changed

+315
-0
lines changed

aperturedb/lnc_sessions.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package aperturedb
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"time"
8+
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/lightninglabs/aperture/aperturedb/sqlc"
11+
"github.com/lightninglabs/aperture/lnc"
12+
"github.com/lightningnetwork/lnd/clock"
13+
)
14+
15+
type (
16+
NewLNCSession = sqlc.InsertSessionParams
17+
18+
SetRemoteParams = sqlc.SetRemotePubKeyParams
19+
20+
SetExpiryParams = sqlc.SetExpiryParams
21+
)
22+
23+
// LNCSessionsDB is an interface that defines the set of operations that can be
24+
// executed agaist the lnc sessions database.
25+
type LNCSessionsDB interface {
26+
// InsertLNCSession inserts a new session into the database.
27+
InsertSession(ctx context.Context, arg NewLNCSession) error
28+
29+
// GetLNCSession returns the session tagged with the given passphrase
30+
// entropy.
31+
GetSession(ctx context.Context,
32+
passphraseEntropy []byte) (sqlc.LncSession, error)
33+
34+
// SetRemotePubKey sets the remote public key for the session.
35+
SetRemotePubKey(ctx context.Context,
36+
arg SetRemoteParams) error
37+
38+
// SetExpiry sets the expiry for the session.
39+
SetExpiry(ctx context.Context, arg SetExpiryParams) error
40+
}
41+
42+
// LNCSessionsDBTxOptions defines the set of db txn options the LNCSessionsDB
43+
// understands.
44+
type LNCSessionsDBTxOptions struct {
45+
// readOnly governs if a read only transaction is needed or not.
46+
readOnly bool
47+
}
48+
49+
// ReadOnly returns true if the transaction should be read only.
50+
//
51+
// NOTE: This implements the TxOptions
52+
func (a *LNCSessionsDBTxOptions) ReadOnly() bool {
53+
return a.readOnly
54+
}
55+
56+
// NewLNCSessionsDBReadTx creates a new read transaction option set.
57+
func NewLNCSessionsDBReadTx() LNCSessionsDBTxOptions {
58+
return LNCSessionsDBTxOptions{
59+
readOnly: true,
60+
}
61+
}
62+
63+
// BatchedLNCSessionsDB is a version of the LNCSecretsDB that's capable of
64+
// batched database operations.
65+
type BatchedLNCSessionsDB interface {
66+
LNCSessionsDB
67+
68+
BatchedTx[LNCSessionsDB]
69+
}
70+
71+
// LNCSessionsStore represents a storage backend.
72+
type LNCSessionsStore struct {
73+
db BatchedLNCSessionsDB
74+
clock clock.Clock
75+
}
76+
77+
// NewSecretsStore creates a new SecretsStore instance given a open
78+
// BatchedSecretsDB storage backend.
79+
func NewLNCSessionsStore(db BatchedLNCSessionsDB) *LNCSessionsStore {
80+
return &LNCSessionsStore{
81+
db: db,
82+
clock: clock.NewDefaultClock(),
83+
}
84+
}
85+
86+
// AddSession adds a new session to the database.
87+
func (l *LNCSessionsStore) AddSession(ctx context.Context,
88+
session *lnc.Session) error {
89+
90+
if session.LocalStaticPrivKey == nil {
91+
return fmt.Errorf("local static private key is required")
92+
}
93+
94+
localPrivKey := session.LocalStaticPrivKey.Serialize()
95+
createdAt := l.clock.Now().UTC().Truncate(time.Microsecond)
96+
97+
var writeTxOpts LNCSessionsDBTxOptions
98+
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
99+
params := sqlc.InsertSessionParams{
100+
PassphraseWords: session.PassphraseWords,
101+
PassphraseEntropy: session.PassphraseEntropy,
102+
LocalStaticPrivKey: localPrivKey,
103+
MailboxAddr: session.MailboxAddr,
104+
CreatedAt: createdAt,
105+
DevServer: session.DevServer,
106+
}
107+
108+
return tx.InsertSession(ctx, params)
109+
})
110+
if err != nil {
111+
return fmt.Errorf("failed to insert new session: %v", err)
112+
}
113+
114+
session.CreatedAt = createdAt
115+
116+
return nil
117+
}
118+
119+
// GetSession returns the session tagged with the given label.
120+
func (l *LNCSessionsStore) GetSession(ctx context.Context,
121+
passphraseEntropy []byte) (*lnc.Session, error) {
122+
123+
var session *lnc.Session
124+
125+
readTx := NewLNCSessionsDBReadTx()
126+
err := l.db.ExecTx(ctx, &readTx, func(tx LNCSessionsDB) error {
127+
dbSession, err := tx.GetSession(ctx, passphraseEntropy)
128+
switch {
129+
case err == sql.ErrNoRows:
130+
return lnc.ErrSessionNotFound
131+
132+
case err != nil:
133+
return err
134+
135+
}
136+
137+
privKey, _ := btcec.PrivKeyFromBytes(
138+
dbSession.LocalStaticPrivKey,
139+
)
140+
session = &lnc.Session{
141+
PassphraseWords: dbSession.PassphraseWords,
142+
PassphraseEntropy: dbSession.PassphraseEntropy,
143+
LocalStaticPrivKey: privKey,
144+
MailboxAddr: dbSession.MailboxAddr,
145+
CreatedAt: dbSession.CreatedAt,
146+
DevServer: dbSession.DevServer,
147+
}
148+
149+
if dbSession.RemoteStaticPubKey != nil {
150+
pubKey, err := btcec.ParsePubKey(
151+
dbSession.RemoteStaticPubKey,
152+
)
153+
if err != nil {
154+
return fmt.Errorf("failed to parse remote "+
155+
"public key for session(%x): %w",
156+
dbSession.PassphraseEntropy, err)
157+
}
158+
159+
session.RemoteStaticPubKey = pubKey
160+
}
161+
162+
if dbSession.Expiry.Valid {
163+
expiry := dbSession.Expiry.Time
164+
session.Expiry = &expiry
165+
}
166+
167+
return nil
168+
})
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to get session: %w", err)
171+
}
172+
173+
return session, nil
174+
}
175+
176+
// SetRemotePubKey sets the remote public key for a session.
177+
func (l *LNCSessionsStore) SetRemotePubKey(ctx context.Context,
178+
passphraseEntropy, remotePubKey []byte) error {
179+
180+
var writeTxOpts LNCSessionsDBTxOptions
181+
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
182+
params := SetRemoteParams{
183+
PassphraseEntropy: passphraseEntropy,
184+
RemoteStaticPubKey: remotePubKey,
185+
}
186+
return tx.SetRemotePubKey(ctx, params)
187+
})
188+
if err != nil {
189+
return fmt.Errorf("failed to set remote pub key to "+
190+
"session(%x): %w", passphraseEntropy, err)
191+
}
192+
193+
return nil
194+
}
195+
196+
// SetExpiry sets the expiry time for a session.
197+
func (l *LNCSessionsStore) SetExpiry(ctx context.Context,
198+
passphraseEntropy []byte, expiry time.Time) error {
199+
200+
var writeTxOpts LNCSessionsDBTxOptions
201+
err := l.db.ExecTx(ctx, &writeTxOpts, func(tx LNCSessionsDB) error {
202+
params := SetExpiryParams{
203+
PassphraseEntropy: passphraseEntropy,
204+
Expiry: sql.NullTime{
205+
Time: expiry,
206+
Valid: true,
207+
},
208+
}
209+
210+
return tx.SetExpiry(ctx, params)
211+
})
212+
if err != nil {
213+
return fmt.Errorf("failed to set expiry time to session(%x): "+
214+
"%w", passphraseEntropy, err)
215+
}
216+
217+
return nil
218+
}

aperturedb/lnc_sessions_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package aperturedb
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/lightninglabs/aperture/lnc"
12+
"github.com/lightninglabs/lightning-node-connect/mailbox"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func newLNCSessionsStoreWithDB(db *BaseDB) *LNCSessionsStore {
17+
dbTxer := NewTransactionExecutor(db,
18+
func(tx *sql.Tx) LNCSessionsDB {
19+
return db.WithTx(tx)
20+
},
21+
)
22+
23+
return NewLNCSessionsStore(dbTxer)
24+
}
25+
26+
func TestLNCSessionsDB(t *testing.T) {
27+
t.Parallel()
28+
29+
ctxt, cancel := context.WithTimeout(
30+
context.Background(), defaultTestTimeout,
31+
)
32+
defer cancel()
33+
34+
// First, create a new test database.
35+
db := NewTestDB(t)
36+
store := newLNCSessionsStoreWithDB(db.BaseDB)
37+
38+
words, passphraseEntropy, err := mailbox.NewPassphraseEntropy()
39+
require.NoError(t, err, "error creating passphrase")
40+
41+
passphrase := strings.Join(words[:], " ")
42+
mailboxAddr := "test-mailbox"
43+
devServer := true
44+
45+
session, err := lnc.NewSession(passphrase, mailboxAddr, devServer)
46+
require.NoError(t, err, "error creating session")
47+
48+
// A session needs to have a local static key set to be stored in the
49+
// database.
50+
err = store.AddSession(ctxt, session)
51+
require.Error(t, err)
52+
53+
localStatic, err := btcec.NewPrivateKey()
54+
require.NoError(t, err, "error creating local static key")
55+
session.LocalStaticPrivKey = localStatic
56+
57+
// The db has a precision of microseconds, so we need to truncate the
58+
// timestamp so we are able to capture that it was created AFTER this
59+
// timestamp.
60+
timestampBeforeCreation := time.Now().UTC().Truncate(time.Millisecond)
61+
62+
err = store.AddSession(ctxt, session)
63+
require.NoError(t, err, "error adding session")
64+
require.True(t, session.CreatedAt.After(timestampBeforeCreation))
65+
66+
// Get the session from the database.
67+
dbSession, err := store.GetSession(ctxt, passphraseEntropy[:])
68+
require.NoError(t, err, "error getting session")
69+
require.Equal(t, session, dbSession, "sessions do not match")
70+
71+
// Set the remote static key.
72+
remoteStatic := localStatic.PubKey()
73+
session.RemoteStaticPubKey = remoteStatic
74+
75+
err = store.SetRemotePubKey(
76+
ctxt, passphraseEntropy[:], remoteStatic.SerializeCompressed(),
77+
)
78+
require.NoError(t, err, "error setting remote static key")
79+
80+
// Set expiration date.
81+
expiry := session.CreatedAt.Add(time.Hour).Truncate(time.Millisecond)
82+
session.Expiry = &expiry
83+
84+
err = store.SetExpiry(ctxt, passphraseEntropy[:], expiry)
85+
require.NoError(t, err, "error setting expiry")
86+
87+
// Next time we fetch the session, it should have the remote static key
88+
// and the expiry set.
89+
dbSession, err = store.GetSession(ctxt, passphraseEntropy[:])
90+
require.NoError(t, err, "error getting session")
91+
require.Equal(t, session, dbSession, "sessions do not match")
92+
93+
// Trying to get a session that does not exist should return a specific
94+
// error.
95+
_, err = store.GetSession(ctxt, []byte("non-existent"))
96+
require.ErrorIs(t, err, lnc.ErrSessionNotFound)
97+
}

0 commit comments

Comments
 (0)