Skip to content

Commit 88a2bf0

Browse files
authored
Merge pull request #985 from ellemouton/stateShift
sessions: strict state shifts
2 parents ad38fcb + ec8d136 commit 88a2bf0

File tree

5 files changed

+167
-32
lines changed

5 files changed

+167
-32
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Release Notes
2+
3+
- [Lightning Terminal](#lightning-terminal)
4+
- [Bug Fixes](#bug-fixes)
5+
- [Functional Changes/Additions](#functional-changesadditions)
6+
- [Technical and Architectural Updates](#technical-and-architectural-updates)
7+
- [Integrated Binary Updates](#integrated-binary-updates)
8+
- [LND](#lnd)
9+
- [Loop](#loop)
10+
- [Pool](#pool)
11+
- [Faraday](#faraday)
12+
- [Taproot Assets](#taproot-assets)
13+
- [Contributors](#contributors-alphabetical-order)
14+
15+
## Lightning Terminal
16+
17+
### Bug Fixes
18+
19+
### Functional Changes/Additions
20+
21+
### Technical and Architectural Updates
22+
23+
* Correctly [move a session to the Expired
24+
state](https://github.com/lightninglabs/lightning-terminal/pull/985) instead
25+
of the Revoked state if it is in fact being revoked due to the session expiry
26+
being reached.
27+
28+
29+
## RPC Updates
30+
31+
## Integrated Binary Updates
32+
33+
### LND
34+
35+
### Loop
36+
37+
### Pool
38+
39+
### Faraday
40+
41+
### Taproot Assets
42+
43+
# Contributors (Alphabetical Order)
44+
45+
* Elle Mouton

session/interface.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ const (
2727
type State uint8
2828

2929
/*
30-
/---> StateExpired
30+
/---> StateExpired (terminal)
3131
StateCreated ---
32-
\---> StateRevoked
32+
\---> StateRevoked (terminal)
3333
*/
3434

3535
const (
@@ -59,6 +59,24 @@ const (
5959
StateReserved State = 4
6060
)
6161

62+
// Terminal returns true if the state is a terminal state.
63+
func (s State) Terminal() bool {
64+
return s == StateExpired || s == StateRevoked
65+
}
66+
67+
// legalStateShifts is a map that defines the legal State transitions that a
68+
// Session can be put through.
69+
var legalStateShifts = map[State]map[State]bool{
70+
StateCreated: {
71+
StateExpired: true,
72+
StateRevoked: true,
73+
},
74+
StateInUse: {
75+
StateRevoked: true,
76+
StateExpired: true,
77+
},
78+
}
79+
6280
// MacaroonRecipe defines the permissions and caveats that should be used
6381
// to bake a macaroon.
6482
type MacaroonRecipe struct {
@@ -197,10 +215,6 @@ type Store interface {
197215
// that are in the given states.
198216
ListSessionsByState(...State) ([]*Session, error)
199217

200-
// RevokeSession updates the state of the session with the given local
201-
// public key to be revoked.
202-
RevokeSession(*btcec.PublicKey) error
203-
204218
// UpdateSessionRemotePubKey can be used to add the given remote pub key
205219
// to the session with the given local pub key.
206220
UpdateSessionRemotePubKey(localPubKey,
@@ -225,5 +239,9 @@ type Store interface {
225239
// StateReserved state.
226240
DeleteReservedSessions() error
227241

242+
// ShiftState updates the state of the session with the given ID to the
243+
// "dest" state.
244+
ShiftState(id ID, dest State) error
245+
228246
IDToGroupIndex
229247
}

session/kvdb_store.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -514,30 +514,42 @@ func (db *BoltStore) DeleteReservedSessions() error {
514514
})
515515
}
516516

517-
// RevokeSession updates the state of the session with the given local
518-
// public key to be revoked.
517+
// ShiftState updates the state of the session with the given ID to the "dest"
518+
// state.
519519
//
520520
// NOTE: this is part of the Store interface.
521-
func (db *BoltStore) RevokeSession(key *btcec.PublicKey) error {
522-
var session *Session
521+
func (db *BoltStore) ShiftState(id ID, dest State) error {
523522
return db.Update(func(tx *bbolt.Tx) error {
524523
sessionBucket, err := getBucket(tx, sessionBucketKey)
525524
if err != nil {
526525
return err
527526
}
528527

529-
sessionBytes := sessionBucket.Get(key.SerializeCompressed())
530-
if len(sessionBytes) == 0 {
531-
return ErrSessionNotFound
532-
}
533-
534-
session, err = DeserializeSession(bytes.NewReader(sessionBytes))
528+
session, err := getSessionByID(sessionBucket, id)
535529
if err != nil {
536530
return err
537531
}
538532

539-
session.State = StateRevoked
540-
session.RevokedAt = db.clock.Now().UTC()
533+
// If the session is already in the desired state, we return
534+
// with no error to maintain idempotency.
535+
if session.State == dest {
536+
return nil
537+
}
538+
539+
// Ensure that the wanted state change is allowed.
540+
allowedDestinations, ok := legalStateShifts[session.State]
541+
if !ok || !allowedDestinations[dest] {
542+
return fmt.Errorf("illegal session state transition "+
543+
"from %d to %d", session.State, dest)
544+
}
545+
546+
session.State = dest
547+
548+
// If the session is terminal, we set the revoked at time to the
549+
// current time.
550+
if dest.Terminal() {
551+
session.RevokedAt = db.clock.Now().UTC()
552+
}
541553

542554
return putSession(sessionBucket, session)
543555
})

session/store_test.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func TestBasicSessionStore(t *testing.T) {
106106
require.Equal(t, session1.State, StateCreated)
107107

108108
// Now revoke the session and assert that the state is revoked.
109-
require.NoError(t, db.RevokeSession(s1.LocalPublicKey))
109+
require.NoError(t, db.ShiftState(s1.ID, StateRevoked))
110110
s1, err = db.GetSession(s1.LocalPublicKey)
111111
require.NoError(t, err)
112112
require.Equal(t, s1.State, StateRevoked)
@@ -225,7 +225,7 @@ func TestLinkingSessions(t *testing.T) {
225225
require.ErrorContains(t, db.CreateSession(s2), "is still active")
226226

227227
// Revoke the first session.
228-
require.NoError(t, db.RevokeSession(s1.LocalPublicKey))
228+
require.NoError(t, db.ShiftState(s1.ID, StateRevoked))
229229

230230
// Persisting the second linked session should now work.
231231
require.NoError(t, db.CreateSession(s2))
@@ -248,16 +248,20 @@ func TestLinkedSessions(t *testing.T) {
248248
// the same group. The group ID is equivalent to the session ID of the
249249
// first session.
250250
s1 := newSession(t, db, clock, "session 1")
251-
s2 := newSession(t, db, clock, "session 2", withLinkedGroupID(&s1.GroupID))
252-
s3 := newSession(t, db, clock, "session 3", withLinkedGroupID(&s2.GroupID))
251+
s2 := newSession(
252+
t, db, clock, "session 2", withLinkedGroupID(&s1.GroupID),
253+
)
254+
s3 := newSession(
255+
t, db, clock, "session 3", withLinkedGroupID(&s2.GroupID),
256+
)
253257

254258
// Persist the sessions.
255259
require.NoError(t, db.CreateSession(s1))
256260

257-
require.NoError(t, db.RevokeSession(s1.LocalPublicKey))
261+
require.NoError(t, db.ShiftState(s1.ID, StateRevoked))
258262
require.NoError(t, db.CreateSession(s2))
259263

260-
require.NoError(t, db.RevokeSession(s2.LocalPublicKey))
264+
require.NoError(t, db.ShiftState(s2.ID, StateRevoked))
261265
require.NoError(t, db.CreateSession(s3))
262266

263267
// Assert that the session ID to group ID index works as expected.
@@ -282,7 +286,7 @@ func TestLinkedSessions(t *testing.T) {
282286

283287
// Persist the sessions.
284288
require.NoError(t, db.CreateSession(s4))
285-
require.NoError(t, db.RevokeSession(s4.LocalPublicKey))
289+
require.NoError(t, db.ShiftState(s4.ID, StateRevoked))
286290

287291
require.NoError(t, db.CreateSession(s5))
288292

@@ -337,7 +341,7 @@ func TestCheckSessionGroupPredicate(t *testing.T) {
337341
require.False(t, ok)
338342

339343
// Revoke the first session.
340-
require.NoError(t, db.RevokeSession(s1.LocalPublicKey))
344+
require.NoError(t, db.ShiftState(s1.ID, StateRevoked))
341345

342346
// Add a new session to the same group as the first one.
343347
s2 := newSession(t, db, clock, "label 2", withLinkedGroupID(&s1.GroupID))
@@ -392,6 +396,53 @@ func TestCheckSessionGroupPredicate(t *testing.T) {
392396
require.True(t, ok)
393397
}
394398

399+
// TestStateShift tests that the ShiftState method works as expected.
400+
func TestStateShift(t *testing.T) {
401+
// Set up a new DB.
402+
clock := clock.NewTestClock(testTime)
403+
db, err := NewDB(t.TempDir(), "test.db", clock)
404+
require.NoError(t, err)
405+
t.Cleanup(func() {
406+
_ = db.Close()
407+
})
408+
409+
// Add a new session to the DB.
410+
s1 := newSession(t, db, clock, "label 1")
411+
require.NoError(t, db.CreateSession(s1))
412+
413+
// Check that the session is in the StateCreated state. Also check that
414+
// the "RevokedAt" time has not yet been set.
415+
s1, err = db.GetSession(s1.LocalPublicKey)
416+
require.NoError(t, err)
417+
require.Equal(t, StateCreated, s1.State)
418+
require.Equal(t, time.Time{}, s1.RevokedAt)
419+
420+
// Shift the state of the session to StateRevoked.
421+
err = db.ShiftState(s1.ID, StateRevoked)
422+
require.NoError(t, err)
423+
424+
// This should have worked. Since it is now in a terminal state, the
425+
// "RevokedAt" time should be set.
426+
s1, err = db.GetSession(s1.LocalPublicKey)
427+
require.NoError(t, err)
428+
require.Equal(t, StateRevoked, s1.State)
429+
require.True(t, clock.Now().Equal(s1.RevokedAt))
430+
431+
// Trying to do the same state shift again should succeed since the
432+
// session is already in the expected "dest" state. The revoked-at time
433+
// should not have changed though.
434+
prevTime := clock.Now()
435+
clock.SetTime(prevTime.Add(time.Second))
436+
err = db.ShiftState(s1.ID, StateRevoked)
437+
require.NoError(t, err)
438+
require.True(t, prevTime.Equal(s1.RevokedAt))
439+
440+
// Trying to shift the state from a terminal state back to StateCreated
441+
// should also fail since this is not a legal state transition.
442+
err = db.ShiftState(s1.ID, StateCreated)
443+
require.ErrorContains(t, err, "illegal session state transition")
444+
}
445+
395446
// testSessionModifier is a functional option that can be used to modify the
396447
// default test session created by newSession.
397448
type testSessionModifier func(*Session)

session_rpcserver.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ func (s *sessionRpcServer) start(ctx context.Context) error {
154154
err)
155155

156156
if perm {
157-
err := s.cfg.db.RevokeSession(
158-
sess.LocalPublicKey,
157+
err := s.cfg.db.ShiftState(
158+
sess.ID, session.StateRevoked,
159159
)
160160
if err != nil {
161161
log.Errorf("error revoking "+
@@ -360,7 +360,8 @@ func (s *sessionRpcServer) resumeSession(ctx context.Context,
360360
log.Debugf("Not resuming session %x with expiry %s",
361361
pubKeyBytes, sess.Expiry)
362362

363-
if err := s.cfg.db.RevokeSession(pubKey); err != nil {
363+
err := s.cfg.db.ShiftState(sess.ID, session.StateExpired)
364+
if err != nil {
364365
return fmt.Errorf("error revoking session: %v", err)
365366
}
366367

@@ -436,7 +437,9 @@ func (s *sessionRpcServer) resumeSession(ctx context.Context,
436437
log.Debugf("Deadline for session %x has already "+
437438
"passed. Revoking session", pubKeyBytes)
438439

439-
return s.cfg.db.RevokeSession(pubKey)
440+
return s.cfg.db.ShiftState(
441+
sess.ID, session.StateRevoked,
442+
)
440443
}
441444

442445
// Start the deadline timer.
@@ -515,7 +518,7 @@ func (s *sessionRpcServer) resumeSession(ctx context.Context,
515518
log.Debugf("Error stopping session: %v", err)
516519
}
517520

518-
err = s.cfg.db.RevokeSession(pubKey)
521+
err = s.cfg.db.ShiftState(sess.ID, session.StateRevoked)
519522
if err != nil {
520523
log.Debugf("error revoking session: %v", err)
521524
}
@@ -557,7 +560,13 @@ func (s *sessionRpcServer) RevokeSession(ctx context.Context,
557560
return nil, fmt.Errorf("error parsing public key: %v", err)
558561
}
559562

560-
if err := s.cfg.db.RevokeSession(pubKey); err != nil {
563+
sess, err := s.cfg.db.GetSession(pubKey)
564+
if err != nil {
565+
return nil, fmt.Errorf("error fetching session: %v", err)
566+
}
567+
568+
err = s.cfg.db.ShiftState(sess.ID, session.StateRevoked)
569+
if err != nil {
561570
return nil, fmt.Errorf("error revoking session: %v", err)
562571
}
563572

0 commit comments

Comments
 (0)