From 4a5259e0a147751ff8cdec8d3bf684b22951ea70 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 13 May 2025 09:54:20 +0200 Subject: [PATCH 1/4] firewalldb: best effort tight coupling of actions and sessions In this commit, we do our best to ensure that at least at the time of action creation, if the session ID is set, then our bbolt actions store impl will at least first check that the session really does exist. This also forces us to update our tests in preparation for the SQL store which will tightly couple the actions and sessions. --- firewalldb/actions_kvdb.go | 16 ++- firewalldb/actions_test.go | 206 ++++++++++++++++++++++-------------- firewalldb/kvstores_test.go | 7 -- 3 files changed, 142 insertions(+), 87 deletions(-) diff --git a/firewalldb/actions_kvdb.go b/firewalldb/actions_kvdb.go index f2b20465f..c5f582173 100644 --- a/firewalldb/actions_kvdb.go +++ b/firewalldb/actions_kvdb.go @@ -54,9 +54,21 @@ var ( ) // AddAction serialises and adds an Action to the DB under the given sessionID. -func (db *BoltDB) AddAction(_ context.Context, +func (db *BoltDB) AddAction(ctx context.Context, req *AddActionReq) (ActionLocator, error) { + // If the new action links to a session, the session must exist. + // For the bbolt impl of the store, this is our best effort attempt + // at ensuring each action links to a session. If the session is + // deleted later on, however, then the action will still exist. + var err error + req.SessionID.WhenSome(func(id session.ID) { + _, err = db.sessionIDIndex.GetSession(ctx, id) + }) + if err != nil { + return nil, err + } + action := &Action{ AddActionReq: *req, AttemptedAt: db.clock.Now().UTC(), @@ -69,7 +81,7 @@ func (db *BoltDB) AddAction(_ context.Context, } var locator kvdbActionLocator - err := db.DB.Update(func(tx *bbolt.Tx) error { + err = db.DB.Update(func(tx *bbolt.Tx) error { mainActionsBucket, err := getBucket(tx, actionsBucketKey) if err != nil { return err diff --git a/firewalldb/actions_test.go b/firewalldb/actions_test.go index 12824ff39..8ace27111 100644 --- a/firewalldb/actions_test.go +++ b/firewalldb/actions_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" "github.com/stretchr/testify/require" @@ -22,17 +23,37 @@ func TestActionStorage(t *testing.T) { ctx := context.Background() clock := clock.NewTestClock(testTime1) + sessDB := session.NewTestDB(t, clock) - db, err := NewBoltDB(t.TempDir(), "test.db", nil, clock) + db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) - sessionID1 := intToSessionID(1) + // Assert that attempting to add an action for a session that does not + // exist returns an error. + _, err = db.AddAction(ctx, &AddActionReq{ + SessionID: fn.Some(session.ID{1, 2, 3, 4}), + }) + require.ErrorIs(t, err, session.ErrSessionNotFound) + + // Add two sessions to the session DB so that we can reference them. + sess1, err := sessDB.NewSession( + ctx, "sess 1", session.TypeAutopilot, time.Unix(1000, 0), + "something", + ) + require.NoError(t, err) + + sess2, err := sessDB.NewSession( + ctx, "sess 2", session.TypeAutopilot, time.Unix(1000, 0), + "something", + ) + require.NoError(t, err) + action1Req := &AddActionReq{ - SessionID: fn.Some(sessionID1), - MacaroonIdentifier: sessionID1, + SessionID: fn.Some(sess1.ID), + MacaroonIdentifier: sess1.ID, ActorName: "Autopilot", FeatureName: "auto-fees", Trigger: "fee too low", @@ -48,10 +69,9 @@ func TestActionStorage(t *testing.T) { State: ActionStateDone, } - sessionID2 := intToSessionID(2) action2Req := &AddActionReq{ - SessionID: fn.Some(sessionID2), - MacaroonIdentifier: sessionID2, + SessionID: fn.Some(sess2.ID), + MacaroonIdentifier: sess2.ID, ActorName: "Autopilot", FeatureName: "rebalancer", Trigger: "channels not balanced", @@ -68,7 +88,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err := db.ListActions( ctx, nil, - WithActionSessionID(sessionID1), + WithActionSessionID(sess1.ID), WithActionState(ActionStateDone), ) require.NoError(t, err) @@ -76,7 +96,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err = db.ListActions( ctx, nil, - WithActionSessionID(sessionID2), + WithActionSessionID(sess2.ID), WithActionState(ActionStateDone), ) require.NoError(t, err) @@ -94,7 +114,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err = db.ListActions( ctx, nil, - WithActionSessionID(sessionID1), + WithActionSessionID(sess1.ID), WithActionState(ActionStateDone), ) require.NoError(t, err) @@ -103,7 +123,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err = db.ListActions( ctx, nil, - WithActionSessionID(sessionID2), + WithActionSessionID(sess2.ID), WithActionState(ActionStateDone), ) require.NoError(t, err) @@ -114,7 +134,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err = db.ListActions( ctx, nil, - WithActionSessionID(sessionID2), + WithActionSessionID(sess2.ID), WithActionState(ActionStateDone), ) require.NoError(t, err) @@ -145,7 +165,7 @@ func TestActionStorage(t *testing.T) { actions, _, _, err = db.ListActions( ctx, nil, - WithActionSessionID(sessionID2), + WithActionSessionID(sess2.ID), WithActionState(ActionStateError), ) require.NoError(t, err) @@ -162,15 +182,27 @@ func TestListActions(t *testing.T) { tmpDir := t.TempDir() ctx := context.Background() + clock := clock.NewDefaultClock() + sessDB := session.NewTestDB(t, clock) - db, err := NewBoltDB(tmpDir, "test.db", nil, clock.NewDefaultClock()) + db, err := NewBoltDB(tmpDir, "test.db", sessDB, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) - sessionID1 := [4]byte{1, 1, 1, 1} - sessionID2 := [4]byte{2, 2, 2, 2} + // Add 2 sessions that we can reference. + sess1, err := sessDB.NewSession( + ctx, "sess 1", session.TypeAutopilot, time.Unix(1000, 0), + "something", + ) + require.NoError(t, err) + + sess2, err := sessDB.NewSession( + ctx, "sess 2", session.TypeAutopilot, time.Unix(1000, 0), + "nothing", + ) + require.NoError(t, err) actionIds := 0 addAction := func(sessionID [4]byte) { @@ -206,11 +238,11 @@ func TestListActions(t *testing.T) { } } - addAction(sessionID1) - addAction(sessionID1) - addAction(sessionID1) - addAction(sessionID1) - addAction(sessionID2) + addAction(sess1.ID) + addAction(sess1.ID) + addAction(sess1.ID) + addAction(sess1.ID) + addAction(sess2.ID) actions, lastIndex, totalCount, err := db.ListActions(ctx, nil) require.NoError(t, err) @@ -218,11 +250,11 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 5, lastIndex) require.EqualValues(t, 0, totalCount) assertActions(actions, []*action{ - {sessionID1, "1"}, - {sessionID1, "2"}, - {sessionID1, "3"}, - {sessionID1, "4"}, - {sessionID2, "5"}, + {sess1.ID, "1"}, + {sess1.ID, "2"}, + {sess1.ID, "3"}, + {sess1.ID, "4"}, + {sess2.ID, "5"}, }) query := &ListActionsQuery{ @@ -235,11 +267,11 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 1, lastIndex) require.EqualValues(t, 0, totalCount) assertActions(actions, []*action{ - {sessionID2, "5"}, - {sessionID1, "4"}, - {sessionID1, "3"}, - {sessionID1, "2"}, - {sessionID1, "1"}, + {sess2.ID, "5"}, + {sess1.ID, "4"}, + {sess1.ID, "3"}, + {sess1.ID, "2"}, + {sess1.ID, "1"}, }) actions, lastIndex, totalCount, err = db.ListActions( @@ -252,11 +284,11 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 5, lastIndex) require.EqualValues(t, 5, totalCount) assertActions(actions, []*action{ - {sessionID1, "1"}, - {sessionID1, "2"}, - {sessionID1, "3"}, - {sessionID1, "4"}, - {sessionID2, "5"}, + {sess1.ID, "1"}, + {sess1.ID, "2"}, + {sess1.ID, "3"}, + {sess1.ID, "4"}, + {sess2.ID, "5"}, }) actions, lastIndex, totalCount, err = db.ListActions( @@ -270,18 +302,18 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 1, lastIndex) require.EqualValues(t, 5, totalCount) assertActions(actions, []*action{ - {sessionID2, "5"}, - {sessionID1, "4"}, - {sessionID1, "3"}, - {sessionID1, "2"}, - {sessionID1, "1"}, + {sess2.ID, "5"}, + {sess1.ID, "4"}, + {sess1.ID, "3"}, + {sess1.ID, "2"}, + {sess1.ID, "1"}, }) - addAction(sessionID2) - addAction(sessionID2) - addAction(sessionID1) - addAction(sessionID1) - addAction(sessionID2) + addAction(sess2.ID) + addAction(sess2.ID) + addAction(sess1.ID) + addAction(sess1.ID) + addAction(sess2.ID) actions, lastIndex, totalCount, err = db.ListActions(ctx, nil) require.NoError(t, err) @@ -289,16 +321,16 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 10, lastIndex) require.EqualValues(t, 0, totalCount) assertActions(actions, []*action{ - {sessionID1, "1"}, - {sessionID1, "2"}, - {sessionID1, "3"}, - {sessionID1, "4"}, - {sessionID2, "5"}, - {sessionID2, "6"}, - {sessionID2, "7"}, - {sessionID1, "8"}, - {sessionID1, "9"}, - {sessionID2, "10"}, + {sess1.ID, "1"}, + {sess1.ID, "2"}, + {sess1.ID, "3"}, + {sess1.ID, "4"}, + {sess2.ID, "5"}, + {sess2.ID, "6"}, + {sess2.ID, "7"}, + {sess1.ID, "8"}, + {sess1.ID, "9"}, + {sess2.ID, "10"}, }) actions, lastIndex, totalCount, err = db.ListActions( @@ -312,9 +344,9 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 3, lastIndex) require.EqualValues(t, 10, totalCount) assertActions(actions, []*action{ - {sessionID1, "1"}, - {sessionID1, "2"}, - {sessionID1, "3"}, + {sess1.ID, "1"}, + {sess1.ID, "2"}, + {sess1.ID, "3"}, }) actions, lastIndex, totalCount, err = db.ListActions( @@ -328,9 +360,9 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 6, lastIndex) require.EqualValues(t, 0, totalCount) assertActions(actions, []*action{ - {sessionID1, "4"}, - {sessionID2, "5"}, - {sessionID2, "6"}, + {sess1.ID, "4"}, + {sess2.ID, "5"}, + {sess2.ID, "6"}, }) actions, lastIndex, totalCount, err = db.ListActions( @@ -345,9 +377,9 @@ func TestListActions(t *testing.T) { require.EqualValues(t, 6, lastIndex) require.EqualValues(t, 10, totalCount) assertActions(actions, []*action{ - {sessionID1, "4"}, - {sessionID2, "5"}, - {sessionID2, "6"}, + {sess1.ID, "4"}, + {sess2.ID, "5"}, + {sess2.ID, "6"}, }) } @@ -358,12 +390,36 @@ func TestListGroupActions(t *testing.T) { ctx := context.Background() clock := clock.NewTestClock(testTime1) - group1 := intToSessionID(0) + sessDB := session.NewTestDB(t, clock) + + // Create two sessions both linked to session 1's group. + sess1, err := sessDB.NewSession( + ctx, "sess 1", session.TypeAutopilot, time.Unix(1000, 0), + "something", + ) + require.NoError(t, err) + + // We'll first need to revoke session 1 before we can link another + // session to the group. + require.NoError( + t, sessDB.ShiftState(ctx, sess1.ID, session.StateCreated), + ) + require.NoError( + t, sessDB.ShiftState(ctx, sess1.ID, session.StateRevoked), + ) + + group1 := sess1.GroupID + + // Create session 2 and link it to the same group as session 1. + sess2, err := sessDB.NewSession( + ctx, "sess 2", session.TypeAutopilot, time.Unix(1000, 0), + "something", session.WithLinkedGroupID(&group1), + ) + require.NoError(t, err) - sessionID1 := intToSessionID(1) action1Req := &AddActionReq{ - SessionID: fn.Some(sessionID1), - MacaroonIdentifier: sessionID1, + SessionID: fn.Some(sess1.ID), + MacaroonIdentifier: sess1.ID, ActorName: "Autopilot", FeatureName: "auto-fees", Trigger: "fee too low", @@ -379,10 +435,9 @@ func TestListGroupActions(t *testing.T) { State: ActionStateDone, } - sessionID2 := intToSessionID(2) action2Req := &AddActionReq{ - SessionID: fn.Some(sessionID2), - MacaroonIdentifier: sessionID2, + SessionID: fn.Some(sess2.ID), + MacaroonIdentifier: sess2.ID, ActorName: "Autopilot", FeatureName: "rebalancer", Trigger: "channels not balanced", @@ -397,12 +452,7 @@ func TestListGroupActions(t *testing.T) { State: ActionStateInit, } - // Link session 1 and session 2 to group 1. - index := NewMockSessionDB() - index.AddPair(sessionID1, group1) - index.AddPair(sessionID2, group1) - - db, err := NewBoltDB(t.TempDir(), "test.db", index, clock) + db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() diff --git a/firewalldb/kvstores_test.go b/firewalldb/kvstores_test.go index 20f6ec0c7..26d2d7c95 100644 --- a/firewalldb/kvstores_test.go +++ b/firewalldb/kvstores_test.go @@ -469,10 +469,3 @@ func TestKVStoreSessionCoupling(t *testing.T) { }) require.NoError(t, err) } - -func intToSessionID(i uint32) session.ID { - var id session.ID - byteOrder.PutUint32(id[:], i) - - return id -} From 85279690317115fe021d3b323daaa9159fe2280e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 13 May 2025 13:29:47 +0200 Subject: [PATCH 2/4] accounts: remove accountFromMacaroon helper We only use it in one place and we can just handle the unwrap of the option there. --- accounts/interceptor.go | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/accounts/interceptor.go b/accounts/interceptor.go index 56e9908e9..5ca331b24 100644 --- a/accounts/interceptor.go +++ b/accounts/interceptor.go @@ -81,7 +81,7 @@ func (s *InterceptorService) Intercept(ctx context.Context, return mid.RPCErrString(req, "error parsing macaroon: %v", err) } - acctID, err := accountFromMacaroon(mac) + acctID, err := IDFromCaveats(mac.Caveats()) if err != nil { return mid.RPCErrString( req, "error parsing account from macaroon: %v", err, @@ -91,15 +91,17 @@ func (s *InterceptorService) Intercept(ctx context.Context, // No account lock in the macaroon, something's weird. The interceptor // wouldn't have been triggered if there was no caveat, so we do expect // a macaroon here. - if acctID == nil { - return mid.RPCErrString(req, "expected account ID in "+ - "macaroon caveat") + accountID, err := acctID.UnwrapOrErr( + fmt.Errorf("expected account ID in macaroon caveat"), + ) + if err != nil { + return mid.RPCErr(req, err) } - acct, err := s.Account(ctx, *acctID) + acct, err := s.Account(ctx, accountID) if err != nil { return mid.RPCErrString( - req, "error getting account %x: %v", acctID[:], err, + req, "error getting account %x: %v", accountID[:], err, ) } @@ -208,27 +210,6 @@ func parseRPCMessage(msg *lnrpc.RPCMessage) (proto.Message, error) { return parsedMsg, nil } -// accountFromMacaroon attempts to extract an account ID from the custom account -// caveat in the macaroon. -func accountFromMacaroon(mac *macaroon.Macaroon) (*AccountID, error) { - if mac == nil { - return nil, nil - } - - // Extract the account caveat from the macaroon. - accountID, err := IDFromCaveats(mac.Caveats()) - if err != nil { - return nil, err - } - - var id *AccountID - accountID.WhenSome(func(aID AccountID) { - id = &aID - }) - - return id, nil -} - // CaveatFromID creates a custom caveat that can be used to bind a macaroon to // a certain account. func CaveatFromID(id AccountID) macaroon.Caveat { From 26d028f4a5f4fe260b28c0f6f6304f097b9b06b1 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 13 May 2025 13:34:50 +0200 Subject: [PATCH 3/4] firewall+firewalldb: extract Account ID and pass to AddActionReq In this commit we add an optional AccountID to the RequestInfo type. Then, we populate it if the caveat of the macaroon being used contains an accounts caveat. We also add an unused AccountID type to the AddActionReq and pass in the value from the RequestLogger. --- firewall/request_info.go | 8 ++++++++ firewall/request_logger.go | 1 + firewalldb/actions.go | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/firewall/request_info.go b/firewall/request_info.go index fd312b71f..3036071da 100644 --- a/firewall/request_info.go +++ b/firewall/request_info.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnrpc" @@ -29,6 +30,7 @@ const ( // request. type RequestInfo struct { SessionID fn.Option[session.ID] + AccountID fn.Option[accounts.AccountID] MsgID uint64 RequestID uint64 MWRequestType string @@ -140,6 +142,12 @@ func NewInfoFromRequest(req *lnrpc.RPCMiddlewareRequest) (*RequestInfo, error) { } } + ri.AccountID, err = accounts.IDFromCaveats(ri.Macaroon.Caveats()) + if err != nil { + return nil, fmt.Errorf("error extracting account ID "+ + "from macaroon: %v", err) + } + return ri, nil } diff --git a/firewall/request_logger.go b/firewall/request_logger.go index 0a98e6458..8b038a6b6 100644 --- a/firewall/request_logger.go +++ b/firewall/request_logger.go @@ -195,6 +195,7 @@ func (r *RequestLogger) addNewAction(ctx context.Context, ri *RequestInfo, actionReq := &firewalldb.AddActionReq{ SessionID: ri.SessionID, + AccountID: ri.AccountID, MacaroonIdentifier: macaroonID, RPCMethod: ri.URI, } diff --git a/firewalldb/actions.go b/firewalldb/actions.go index 129913a19..88172e31b 100644 --- a/firewalldb/actions.go +++ b/firewalldb/actions.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/fn" ) @@ -45,6 +46,13 @@ type AddActionReq struct { // guaranteed to be linked to an existing session. SessionID fn.Option[session.ID] + // AccountID holds the optional account ID of the account that this + // action was performed on. + // + // NOTE: for our BoltDB impl, this is not persisted in any way, and we + // do not populate it on reading from disk. + AccountID fn.Option[accounts.AccountID] + // ActorName is the name of the entity who performed the Action. ActorName string From 642a69f1c2ec16529226d679d0495e08fb22cde0 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 13 May 2025 13:54:05 +0200 Subject: [PATCH 4/4] firewalldb: best effort link from actions to accounts In this commit, we do our best to ensure that at least at the time of action creation, if the account ID is set, then our bbolt actions store impl will at least first check that the account really does exist. This also forces us to update our tests in preparation for the SQL store which will tightly couple the actions and accounts. --- config_dev.go | 3 ++- config_prod.go | 3 ++- firewalldb/actions_kvdb.go | 12 ++++++++++++ firewalldb/actions_test.go | 25 ++++++++++++++++++++++--- firewalldb/interface.go | 10 ++++++++++ firewalldb/kvdb_store.go | 4 +++- firewalldb/test_kvdb.go | 11 +++++------ 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/config_dev.go b/config_dev.go index ae7d18977..4ab17bd77 100644 --- a/config_dev.go +++ b/config_dev.go @@ -154,7 +154,8 @@ func NewStores(cfg *Config, clock clock.Clock) (*stores, error) { } firewallBoltDB, err := firewalldb.NewBoltDB( - networkDir, firewalldb.DBFilename, stores.sessions, clock, + networkDir, firewalldb.DBFilename, stores.sessions, + stores.accounts, clock, ) if err != nil { return stores, fmt.Errorf("error creating firewall BoltDB: %v", diff --git a/config_prod.go b/config_prod.go index 5ea897fc8..c13d66960 100644 --- a/config_prod.go +++ b/config_prod.go @@ -56,7 +56,8 @@ func NewStores(cfg *Config, clock clock.Clock) (*stores, error) { stores.closeFns["sessions"] = sessStore.Close firewallDB, err := firewalldb.NewBoltDB( - networkDir, firewalldb.DBFilename, sessStore, clock, + networkDir, firewalldb.DBFilename, stores.sessions, + stores.accounts, clock, ) if err != nil { return stores, fmt.Errorf("error creating firewall DB: %v", err) diff --git a/firewalldb/actions_kvdb.go b/firewalldb/actions_kvdb.go index c5f582173..7a8ae3e15 100644 --- a/firewalldb/actions_kvdb.go +++ b/firewalldb/actions_kvdb.go @@ -9,6 +9,7 @@ import ( "io" "time" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/tlv" @@ -69,6 +70,17 @@ func (db *BoltDB) AddAction(ctx context.Context, return nil, err } + // If the new action links to an account, the account must exist. + // For the bbolt impl of the store, this is our best effort attempt + // at ensuring each action links to an account. If the account is + // deleted later on, however, then the action will still exist. + req.AccountID.WhenSome(func(id accounts.AccountID) { + _, err = db.accountsDB.Account(ctx, id) + }) + if err != nil { + return nil, err + } + action := &Action{ AddActionReq: *req, AttemptedAt: db.clock.Now().UTC(), diff --git a/firewalldb/actions_test.go b/firewalldb/actions_test.go index 8ace27111..0f93ed2fe 100644 --- a/firewalldb/actions_test.go +++ b/firewalldb/actions_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" @@ -24,8 +25,9 @@ func TestActionStorage(t *testing.T) { ctx := context.Background() clock := clock.NewTestClock(testTime1) sessDB := session.NewTestDB(t, clock) + accountsDB := accounts.NewTestDB(t, clock) - db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, clock) + db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, accountsDB, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -38,6 +40,13 @@ func TestActionStorage(t *testing.T) { }) require.ErrorIs(t, err, session.ErrSessionNotFound) + // Assert that attempting to add an action that links to an account + // that does not exist returns an error. + _, err = db.AddAction(ctx, &AddActionReq{ + AccountID: fn.Some(accounts.AccountID{1, 2, 3, 4}), + }) + require.ErrorIs(t, err, accounts.ErrAccNotFound) + // Add two sessions to the session DB so that we can reference them. sess1, err := sessDB.NewSession( ctx, "sess 1", session.TypeAutopilot, time.Unix(1000, 0), @@ -51,8 +60,13 @@ func TestActionStorage(t *testing.T) { ) require.NoError(t, err) + // Add an account that we can link to as well. + acct1, err := accountsDB.NewAccount(ctx, 0, time.Time{}, "foo") + require.NoError(t, err) + action1Req := &AddActionReq{ SessionID: fn.Some(sess1.ID), + AccountID: fn.Some(acct1.ID), MacaroonIdentifier: sess1.ID, ActorName: "Autopilot", FeatureName: "auto-fees", @@ -185,7 +199,7 @@ func TestListActions(t *testing.T) { clock := clock.NewDefaultClock() sessDB := session.NewTestDB(t, clock) - db, err := NewBoltDB(tmpDir, "test.db", sessDB, clock) + db, err := NewBoltDB(tmpDir, "test.db", sessDB, nil, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -452,7 +466,7 @@ func TestListGroupActions(t *testing.T) { State: ActionStateInit, } - db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, clock) + db, err := NewBoltDB(t.TempDir(), "test.db", sessDB, nil, clock) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() @@ -490,6 +504,9 @@ func TestListGroupActions(t *testing.T) { } func assertEqualActions(t *testing.T, expected, got *Action) { + // Accounts are not explicitly linked in our bbolt DB implementation. + got.AccountID = expected.AccountID + expectedAttemptedAt := expected.AttemptedAt actualAttemptedAt := got.AttemptedAt @@ -501,4 +518,6 @@ func assertEqualActions(t *testing.T, expected, got *Action) { expected.AttemptedAt = expectedAttemptedAt got.AttemptedAt = actualAttemptedAt + + got.AccountID = fn.None[accounts.AccountID]() } diff --git a/firewalldb/interface.go b/firewalldb/interface.go index 7da9cf5b0..5ee729e91 100644 --- a/firewalldb/interface.go +++ b/firewalldb/interface.go @@ -3,6 +3,7 @@ package firewalldb import ( "context" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" ) @@ -15,6 +16,15 @@ type SessionDB interface { GetSession(context.Context, session.ID) (*session.Session, error) } +// AccountsDB is an interface that abstracts the database operations needed +// firewalldb to be able to query the accounts database. +type AccountsDB interface { + // Account fetches the Account with the given id from the accounts + // database. + Account(ctx context.Context, + id accounts.AccountID) (*accounts.OffChainBalanceAccount, error) +} + // DBExecutor provides an Update and View method that will allow the caller // to perform atomic read and write transactions defined by PrivacyMapTx on the // underlying BoltDB. diff --git a/firewalldb/kvdb_store.go b/firewalldb/kvdb_store.go index edef36a11..e3256e899 100644 --- a/firewalldb/kvdb_store.go +++ b/firewalldb/kvdb_store.go @@ -41,12 +41,13 @@ type BoltDB struct { clock clock.Clock sessionIDIndex SessionDB + accountsDB AccountsDB } // NewBoltDB creates a new bolt database that can be found at the given // directory. func NewBoltDB(dir, fileName string, sessionIDIndex SessionDB, - clock clock.Clock) (*BoltDB, error) { + accountsDB AccountsDB, clock clock.Clock) (*BoltDB, error) { firstInit := false path := filepath.Join(dir, fileName) @@ -73,6 +74,7 @@ func NewBoltDB(dir, fileName string, sessionIDIndex SessionDB, return &BoltDB{ DB: db, sessionIDIndex: sessionIDIndex, + accountsDB: accountsDB, clock: clock, }, nil } diff --git a/firewalldb/test_kvdb.go b/firewalldb/test_kvdb.go index 2c0ad66c9..659292702 100644 --- a/firewalldb/test_kvdb.go +++ b/firewalldb/test_kvdb.go @@ -5,7 +5,6 @@ package firewalldb import ( "testing" - "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" "github.com/stretchr/testify/require" ) @@ -18,21 +17,21 @@ func NewTestDB(t *testing.T, clock clock.Clock) *BoltDB { // NewTestDBFromPath is a helper function that creates a new BoltStore with a // connection to an existing BBolt database for testing. func NewTestDBFromPath(t *testing.T, dbPath string, clock clock.Clock) *BoltDB { - return newDBFromPathWithSessions(t, dbPath, nil, clock) + return newDBFromPathWithSessions(t, dbPath, nil, nil, clock) } // NewTestDBWithSessions creates a new test BoltDB Store with access to an // existing sessions DB. -func NewTestDBWithSessions(t *testing.T, sessStore session.Store, +func NewTestDBWithSessions(t *testing.T, sessStore SessionDB, clock clock.Clock) *BoltDB { - return newDBFromPathWithSessions(t, t.TempDir(), sessStore, clock) + return newDBFromPathWithSessions(t, t.TempDir(), sessStore, nil, clock) } func newDBFromPathWithSessions(t *testing.T, dbPath string, - sessStore session.Store, clock clock.Clock) *BoltDB { + sessStore SessionDB, acctStore AccountsDB, clock clock.Clock) *BoltDB { - store, err := NewBoltDB(dbPath, DBFilename, sessStore, clock) + store, err := NewBoltDB(dbPath, DBFilename, sessStore, acctStore, clock) require.NoError(t, err) t.Cleanup(func() {