From 4f7c6fcbe58ac069bfb9e5dbfb26c457f9b69e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 10 Sep 2025 11:53:43 +0200 Subject: [PATCH 1/2] firewalldb: update kvdb `assertEqualActions` The upcoming commit will update the `AddActionReq` struct to include an extra field which the `kvdb` actions store will ignore. Therefore the `assertEqualActions` for the `kvdb` version will need to be update to ignore this field. In preparation for that change, we also do another optimization of the `assertEqualActions` function under kvdb builds, to not mutate the passed action references. --- firewalldb/test_kvdb.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/firewalldb/test_kvdb.go b/firewalldb/test_kvdb.go index c3cd4533a..c6255b05f 100644 --- a/firewalldb/test_kvdb.go +++ b/firewalldb/test_kvdb.go @@ -5,10 +5,8 @@ package firewalldb import ( "testing" - "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" - "github.com/lightningnetwork/lnd/fn" "github.com/stretchr/testify/require" ) @@ -59,8 +57,9 @@ func newDBFromPathWithSessions(t *testing.T, dbPath string, func assertEqualActions(t *testing.T, expected, got *Action) { // Accounts are not explicitly linked in our bbolt DB implementation. + actualAccountID := got.AccountID got.AccountID = expected.AccountID require.Equal(t, expected, got) - got.AccountID = fn.None[accounts.AccountID]() + got.AccountID = actualAccountID } From 87cf67e8c56e41c3b4e7196182338601cda8e01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Mon, 15 Sep 2025 11:26:36 +0200 Subject: [PATCH 2/2] multi: persist full mac root key in sql actions db When migrating the actions store from kvdb to sql, we will update the existing actions to include the full mac root key, instead of just the last 4 bytes (currently called `MacaroonIdentifier`). In order to do so, we change the sql implementation of the `actions` store to persist the full mac root key, instead of just the last 4 bytes. As no production data in the sql actions store exists for users yet, it's fine for us to change this without having to address old sql actions which only stored the last 4 bytes. Note though that we do not update the kvdb implementation, and the full macaroon root key will be ignored by the kvdb store even if set. Therefore, the rest of `litd` will still have to just expect the last 4 bytes of the mac root key when accessing an `Action`'s MacaroonIdentifier. Therefore, we we currently never expose the rest of the mac root key outside of the sql actions store. Once the kvdb store has been fully deprecated and removed, we can then update the rest of `litd` to also use the full mac root key, and change the `Action` struct's field to reflect this. --- firewall/request_logger.go | 19 ++++++++++++++++--- firewalldb/actions.go | 5 +++++ firewalldb/actions_sql.go | 20 ++++++++++++++++---- firewalldb/actions_test.go | 27 +++++++++++++++++++++++++++ firewalldb/test_kvdb.go | 8 ++++++++ firewalldb/test_sql.go | 10 ++++++++++ 6 files changed, 82 insertions(+), 7 deletions(-) diff --git a/firewall/request_logger.go b/firewall/request_logger.go index b28b1092a..6663de339 100644 --- a/firewall/request_logger.go +++ b/firewall/request_logger.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/lightninglabs/lightning-terminal/firewalldb" + litd_macaroons "github.com/lightninglabs/lightning-terminal/macaroons" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/fn" @@ -182,14 +183,25 @@ func (r *RequestLogger) Intercept(ctx context.Context, func (r *RequestLogger) addNewAction(ctx context.Context, ri *RequestInfo, withPayloadData bool) error { - var macaroonID fn.Option[[4]byte] + var ( + rootKeyID fn.Option[uint64] + macaroonID fn.Option[[4]byte] + ) + if ri.Macaroon != nil { var err error - macID, err := session.IDFromMacaroon(ri.Macaroon) + + fullRootKeyID, err := litd_macaroons.RootKeyIDFromMacaroon( + ri.Macaroon, + ) if err != nil { - return fmt.Errorf("could not extract ID from macaroon") + return fmt.Errorf("could not extract root key ID from "+ + "macaroon: %w", err) } + macID := session.IDFromMacRootKeyID(fullRootKeyID) + + rootKeyID = fn.Some(fullRootKeyID) macaroonID = fn.Some([4]byte(macID)) } @@ -197,6 +209,7 @@ func (r *RequestLogger) addNewAction(ctx context.Context, ri *RequestInfo, SessionID: ri.SessionID, AccountID: ri.AccountID, MacaroonIdentifier: macaroonID, + MacaroonRootKeyID: rootKeyID, RPCMethod: ri.URI, } diff --git a/firewalldb/actions.go b/firewalldb/actions.go index 1d0c8c36f..c405787fe 100644 --- a/firewalldb/actions.go +++ b/firewalldb/actions.go @@ -39,6 +39,11 @@ type AddActionReq struct { // If no macaroon was used for the action, then this will not be set. MacaroonIdentifier fn.Option[[4]byte] + // MacaroonRootKeyID is the uint64 / full 8 bytes of the root key ID of + // the macaroon used to perform the action. + // If no macaroon was used for the action, then this will not be set. + MacaroonRootKeyID fn.Option[uint64] + // SessionID holds the optional session ID of the session that this // action was performed with. // diff --git a/firewalldb/actions_sql.go b/firewalldb/actions_sql.go index 75c9d0a6d..9c7a6df1c 100644 --- a/firewalldb/actions_sql.go +++ b/firewalldb/actions_sql.go @@ -3,6 +3,7 @@ package firewalldb import ( "context" "database/sql" + "encoding/binary" "errors" "fmt" "math" @@ -140,8 +141,11 @@ func (s *SQLDB) AddAction(ctx context.Context, } var macID []byte - req.MacaroonIdentifier.WhenSome(func(id [4]byte) { - macID = id[:] + req.MacaroonRootKeyID.WhenSome(func(rootKeyID uint64) { + rootKeyBytes := make([]byte, 8) + binary.BigEndian.PutUint64(rootKeyBytes[:], rootKeyID) + + macID = rootKeyBytes }) id, err := db.InsertAction(ctx, sqlc.InsertActionParams{ @@ -393,9 +397,17 @@ func unmarshalAction(ctx context.Context, db SQLActionQueries, legacyAcctID = fn.Some(acctID) } + // While we store the full 8 byte macaroon root key ID in the sql + // actions DB, the kvdb version only stored the last 4 bytes. So + // we'll only return that here to maintain compatibility with any + // existing callers. + // + // TODO(viktor): Remove this when we no longer need to be compatible + // with the kvdb version. var macID fn.Option[[4]byte] - if len(dbAction.MacaroonIdentifier) > 0 { - macID = fn.Some([4]byte(dbAction.MacaroonIdentifier)) + if len(dbAction.MacaroonIdentifier) >= 4 { + dbMacID := dbAction.MacaroonIdentifier + macID = fn.Some([4]byte(dbMacID[len(dbMacID)-4:])) } return &Action{ diff --git a/firewalldb/actions_test.go b/firewalldb/actions_test.go index 69990c1da..54f2a892c 100644 --- a/firewalldb/actions_test.go +++ b/firewalldb/actions_test.go @@ -2,6 +2,7 @@ package firewalldb import ( "context" + "encoding/binary" "fmt" "testing" "time" @@ -60,10 +61,13 @@ func TestActionStorage(t *testing.T) { acct1, err := accountsDB.NewAccount(ctx, 0, time.Time{}, "foo") require.NoError(t, err) + sess1RootKeyID := macIDToRootKeyID(sess1.ID) + action1Req := &AddActionReq{ SessionID: fn.Some(sess1.ID), AccountID: fn.Some(acct1.ID), MacaroonIdentifier: fn.Some([4]byte(sess1.ID)), + MacaroonRootKeyID: fn.Some(sess1RootKeyID), ActorName: "Autopilot", FeatureName: "auto-fees", Trigger: "fee too low", @@ -79,9 +83,12 @@ func TestActionStorage(t *testing.T) { State: ActionStateDone, } + sess2RootKeyID := macIDToRootKeyID(sess2.ID) + action2Req := &AddActionReq{ SessionID: fn.Some(sess2.ID), MacaroonIdentifier: fn.Some([4]byte(sess2.ID)), + MacaroonRootKeyID: fn.Some(sess2RootKeyID), ActorName: "Autopilot", FeatureName: "rebalancer", Trigger: "channels not balanced", @@ -213,8 +220,11 @@ func TestListActions(t *testing.T) { addAction := func(sessionID [4]byte) { actionIds++ + sessRootKeyID := macIDToRootKeyID(sessionID) + actionReq := &AddActionReq{ MacaroonIdentifier: fn.Some(sessionID), + MacaroonRootKeyID: fn.Some(sessRootKeyID), ActorName: "Autopilot", FeatureName: fmt.Sprintf("%d", actionIds), Trigger: "fee too low", @@ -424,9 +434,12 @@ func TestListGroupActions(t *testing.T) { ) require.NoError(t, err) + sess1RootKeyID := macIDToRootKeyID(sess1.ID) + action1Req := &AddActionReq{ SessionID: fn.Some(sess1.ID), MacaroonIdentifier: fn.Some([4]byte(sess1.ID)), + MacaroonRootKeyID: fn.Some(sess1RootKeyID), ActorName: "Autopilot", FeatureName: "auto-fees", Trigger: "fee too low", @@ -442,9 +455,12 @@ func TestListGroupActions(t *testing.T) { State: ActionStateDone, } + sess2RootKeyID := macIDToRootKeyID(sess2.ID) + action2Req := &AddActionReq{ SessionID: fn.Some(sess2.ID), MacaroonIdentifier: fn.Some([4]byte(sess2.ID)), + MacaroonRootKeyID: fn.Some(sess2RootKeyID), ActorName: "Autopilot", FeatureName: "rebalancer", Trigger: "channels not balanced", @@ -501,3 +517,14 @@ func TestListGroupActions(t *testing.T) { assertEqualActions(t, action2, al[0]) assertEqualActions(t, action1, al[1]) } + +// macIDToRootKeyID is a helper function for tests that converts a 4 byte +// macaroon ID to a full macaroon root key ID by padding it to 8 bytes. +// Note that the first 4 bytes of the returned root key ID will be zeros, +// followed by the 4 bytes passed in. +func macIDToRootKeyID(macID [4]byte) uint64 { + rootKeyIDBytes := make([]byte, 8) + copy(rootKeyIDBytes[4:], macID[:]) + + return binary.BigEndian.Uint64(rootKeyIDBytes) +} diff --git a/firewalldb/test_kvdb.go b/firewalldb/test_kvdb.go index c6255b05f..c330e0563 100644 --- a/firewalldb/test_kvdb.go +++ b/firewalldb/test_kvdb.go @@ -7,6 +7,7 @@ import ( "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/stretchr/testify/require" ) @@ -59,7 +60,14 @@ func assertEqualActions(t *testing.T, expected, got *Action) { // Accounts are not explicitly linked in our bbolt DB implementation. actualAccountID := got.AccountID got.AccountID = expected.AccountID + + // As the kvdb implementation doesn't store the Macaroon Root Key ID, + // we clear the expected value before comparison, and restore it after. + expectedMacRootKey := expected.MacaroonRootKeyID + expected.MacaroonRootKeyID = fn.None[uint64]() + require.Equal(t, expected, got) got.AccountID = actualAccountID + expected.MacaroonRootKeyID = expectedMacRootKey } diff --git a/firewalldb/test_sql.go b/firewalldb/test_sql.go index a412441f8..97c8434b7 100644 --- a/firewalldb/test_sql.go +++ b/firewalldb/test_sql.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/lightning-terminal/db" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/stretchr/testify/require" ) @@ -46,11 +47,20 @@ func assertEqualActions(t *testing.T, expected, got *Action) { expected.AttemptedAt = time.Time{} got.AttemptedAt = time.Time{} + // As the kvdb implementation doesn't store the Macaroon Root Key ID, + // we don't yet expose this for the sql version, until the kvdb version + // has been deprecated and removed. Therefore, we ignore this field in + // our comparison here, and clear the expected value before comparison, + // and restore it after. + expectedMacRootKey := expected.MacaroonRootKeyID + expected.MacaroonRootKeyID = fn.None[uint64]() + require.Equal(t, expected, got) require.Equal(t, expectedAttemptedAt.Unix(), actualAttemptedAt.Unix()) expected.AttemptedAt = expectedAttemptedAt got.AttemptedAt = actualAttemptedAt + expected.MacaroonRootKeyID = expectedMacRootKey } // createStore is a helper function that creates a new SQLDB and ensure that