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.
This commit is contained in:
Elle Mouton 2025-05-13 13:54:05 +02:00
parent 26d028f4a5
commit 642a69f1c2
No known key found for this signature in database
GPG key ID: D7D916376026F177
7 changed files with 56 additions and 12 deletions

View file

@ -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",

View file

@ -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)

View file

@ -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(),

View file

@ -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]()
}

View file

@ -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.

View file

@ -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
}

View file

@ -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() {