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 since old actions stored in the kvdb implementation
only have the last 4 bytes of the mac root key persisted, we will only
ever persist the last 4 byte of the mac root key ID for kvdb actions.
When the actions are later read back from the kvdb store, the first 4
bytes of the mac root key ID will be padded with zeroes to make up the
full 8 bytes.
As no call site currently utilizes the full 8 bytes of the mac root key
ID, this is okay for now. When we later deprecate and remove the kvdb
implementation, we can then update the rest of `litd` to also use the
full mac root key ID.
This commit is contained in:
Viktor Torstensson 2025-09-15 11:26:36 +02:00
parent 580ce701a2
commit 651cc678fc
No known key found for this signature in database
GPG key ID: 961CC8259AE675D4
6 changed files with 84 additions and 8 deletions

View file

@ -7,6 +7,7 @@ import (
"sync"
"github.com/lightninglabs/lightning-terminal/firewalldb"
litmac "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 := litmac.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,
}

View file

@ -39,6 +39,15 @@ 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.
//
// NOTE: for our BoltDB impl, only the lower 32 bits / last 4 bytes of
// this uint64 are stored. When read back, the upper 32 bits / first 4
// bytes are zeroed.
MacaroonRootKeyID fn.Option[uint64]
// SessionID holds the optional session ID of the session that this
// action was performed with.
//

View file

@ -596,7 +596,13 @@ func DeserializeAction(r io.Reader, sessionID session.ID) (*Action, error) {
return nil, err
}
// Since the kvdb only persists 4 bytes for the macaroon root key ID, we
// first cast it to a uint32, and then to a uint64, effectively padding
// the first 4 bytes with zeroes.
rootKeyID := uint64(binary.BigEndian.Uint32(sessionID[:]))
action.MacaroonIdentifier = fn.Some([4]byte(sessionID))
action.MacaroonRootKeyID = fn.Some(rootKeyID)
action.SessionID = fn.Some(sessionID)
action.ActorName = string(actor)
action.FeatureName = string(featureName)

View file

@ -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,14 +397,24 @@ func unmarshalAction(ctx context.Context, db SQLActionQueries,
legacyAcctID = fn.Some(acctID)
}
// Note that we export the full 8 byte macaroon root key ID in the sql
// actions DB, while the kvdb version persists and exports stored the
// last 4 bytes only.
var macID fn.Option[[4]byte]
if len(dbAction.MacaroonIdentifier) > 0 {
macID = fn.Some([4]byte(dbAction.MacaroonIdentifier))
var macRootKeyID fn.Option[uint64]
if len(dbAction.MacaroonIdentifier) >= 4 {
dbMacID := dbAction.MacaroonIdentifier
macID = fn.Some([4]byte(dbMacID[len(dbMacID)-4:]))
if len(dbAction.MacaroonIdentifier) >= 8 {
macRootKeyID = fn.Some(binary.BigEndian.Uint64(dbMacID))
}
}
return &Action{
AddActionReq: AddActionReq{
MacaroonIdentifier: macID,
MacaroonRootKeyID: macRootKeyID,
AccountID: legacyAcctID,
SessionID: legacySessID,
ActorName: dbAction.ActorName.String,

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/lightninglabs/lightning-terminal/accounts"
litmac "github.com/lightninglabs/lightning-terminal/macaroons"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn"
@ -60,10 +61,13 @@ func TestActionStorage(t *testing.T) {
acct1, err := accountsDB.NewAccount(ctx, 0, time.Time{}, "foo")
require.NoError(t, err)
sess1RootKeyID := litmac.NewSuperMacaroonRootKeyID(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 := litmac.NewSuperMacaroonRootKeyID(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 := litmac.NewSuperMacaroonRootKeyID(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 := litmac.NewSuperMacaroonRootKeyID(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 := litmac.NewSuperMacaroonRootKeyID(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",

View file

@ -3,10 +3,12 @@
package firewalldb
import (
"encoding/binary"
"testing"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn"
"github.com/stretchr/testify/require"
)
@ -59,7 +61,23 @@ 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)
// As the kvdb implementation only stores the last 4 bytes Macaroon Root
// Key ID, we pad it with 4 zero bytes when comparing.
expectedMacRootKey := expected.MacaroonRootKeyID
expectedMacRootKey.WhenSome(func(rootID uint64) {
// Remove the 4 byte prefix of the actual Macaroon Root Key ID.
sessID := session.IDFromMacRootKeyID(rootID)
// Recreate the full 8 byte Macaroon Root Key ID (represented as
// a uint64) by padding the first 4 bytes with zeroes.
expected.MacaroonRootKeyID = fn.Some(
uint64(binary.BigEndian.Uint32(sessID[:])),
)
})
require.Equal(t, expected, got)
got.AccountID = actualAccountID
expected.MacaroonRootKeyID = expectedMacRootKey
}