diff --git a/config_dev.go b/config_dev.go index ae7d1897..4ab17bd7 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 5ea897fc..c13d6696 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 c5f58217..7a8ae3e1 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 8ace2711..0f93ed2f 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 7da9cf5b..5ee729e9 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 edef36a1..e3256e89 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 2c0ad66c..65929270 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() {