multi: preserve macaroon recipe order in SQL

Store explicit positions for session macaroon caveats and permissions
in the SQL schema and read them back in position order. Also remove the
migration-time sorting workaround in session comparison, so migration
validation now checks the actual persisted order instead of masking it.

This is needed because session caveat order is not just presentation
data. LiT adds caveats to the baked macaroon in slice order, and the
macaroon library updates the signature hash chain for each added caveat.
Reordering caveats can therefore change the resulting macaroon bytes and
signature.

The previous schema split caveats and permissions into child tables
without any position column, and the SQL reads had no ORDER BY. The KV
store preserves slice order, but SQL had no explicit way to reproduce
that order after migration or on later reads. The migration code’s old
sorting step was only making validation deterministic; it did not
preserve the original recipe order.

Permissions are canonicalized by lnd when baking, so their order is less
semantically important for the final macaroon. They still get positions
here so the stored recipe remains faithful to the original session data
and both child tables behave consistently.

Why it was needed:

- caveats needed explicit order preservation because they are appended
  and signed in order.
- The old SQL schema did not store order, and the read queries did not
  request one.
- Adding position makes the SQL representation faithful to the KV/TLV
  recipe instead of relying on incidental row order.

- Adding it to permissions too keeps the stored recipe lossless and
  consistent, even though lnd. canonicalizes permissions before baking.

NOTE: This commit explicitly edits the previous migration instead of
adding a new one. This is ok as SQL dbs are not yet supported in
production, so there are no live deployments to worry about.
This commit is contained in:
Viktor Torstensson 2026-06-04 12:55:47 +02:00
parent e955dc825b
commit b61f62d7d9
No known key found for this signature in database
GPG key ID: 961CC8259AE675D4
8 changed files with 76 additions and 85 deletions

View file

@ -84,9 +84,13 @@ CREATE TABLE IF NOT EXISTS session_macaroon_permissions (
entity TEXT NOT NULL,
-- The action that this permission is for.
action TEXT NOT NULL
action TEXT NOT NULL,
-- The original position of the permission in the session recipe.
position INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS sessions_mac_perms_idx ON session_macaroon_permissions(session_id);
CREATE INDEX IF NOT EXISTS sessions_mac_perms_idx
ON session_macaroon_permissions(session_id, position);
-- The session_macaroon_caveats table contains the macaroon caveats that are
-- associated with a session.
@ -105,10 +109,14 @@ CREATE TABLE IF NOT EXISTS session_macaroon_caveats (
verification_id BLOB,
-- The location hint for third party caveats.
location TEXT
location TEXT,
-- The original position of the caveat in the session recipe.
position INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS sessions_mac_caveats_idx ON session_macaroon_caveats(session_id);
CREATE INDEX IF NOT EXISTS sessions_mac_caveats_idx
ON session_macaroon_caveats(session_id, position);
-- The session_feature_configs table contains the feature configs that are
-- associated with a session.

View file

@ -113,6 +113,7 @@ type SessionMacaroonCaveat struct {
CaveatID []byte
VerificationID []byte
Location sql.NullString
Position int32
}
type SessionMacaroonPermission struct {
@ -120,6 +121,7 @@ type SessionMacaroonPermission struct {
SessionID int64
Entity string
Action string
Position int32
}
type SessionPrivacyFlag struct {

View file

@ -81,25 +81,27 @@ WHERE id = $2;
-- name: InsertSessionMacaroonPermission :exec
INSERT INTO session_macaroon_permissions (
session_id, entity, action
) VALUES (
$1, $2, $3
);
-- name: GetSessionMacaroonPermissions :many
SELECT * FROM session_macaroon_permissions
WHERE session_id = $1;
-- name: InsertSessionMacaroonCaveat :exec
INSERT INTO session_macaroon_caveats (
session_id, caveat_id, verification_id, location
session_id, entity, action, position
) VALUES (
$1, $2, $3, $4
);
-- name: GetSessionMacaroonPermissions :many
SELECT * FROM session_macaroon_permissions
WHERE session_id = $1
ORDER BY position ASC;
-- name: InsertSessionMacaroonCaveat :exec
INSERT INTO session_macaroon_caveats (
session_id, caveat_id, verification_id, location, position
) VALUES (
$1, $2, $3, $4, $5
);
-- name: GetSessionMacaroonCaveats :many
SELECT * FROM session_macaroon_caveats
WHERE session_id = $1;
WHERE session_id = $1
ORDER BY position ASC;
-- name: InsertSessionFeatureConfig :exec
INSERT INTO session_feature_configs (
@ -121,4 +123,4 @@ INSERT INTO session_privacy_flags (
-- name: GetSessionPrivacyFlags :many
SELECT * FROM session_privacy_flags
WHERE session_id = $1;
WHERE session_id = $1;

View file

@ -205,8 +205,9 @@ func (q *Queries) GetSessionIDByAlias(ctx context.Context, alias []byte) (int64,
}
const getSessionMacaroonCaveats = `-- name: GetSessionMacaroonCaveats :many
SELECT id, session_id, caveat_id, verification_id, location FROM session_macaroon_caveats
SELECT id, session_id, caveat_id, verification_id, location, position FROM session_macaroon_caveats
WHERE session_id = $1
ORDER BY position ASC
`
func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64) ([]SessionMacaroonCaveat, error) {
@ -224,6 +225,7 @@ func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64
&i.CaveatID,
&i.VerificationID,
&i.Location,
&i.Position,
); err != nil {
return nil, err
}
@ -239,8 +241,9 @@ func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64
}
const getSessionMacaroonPermissions = `-- name: GetSessionMacaroonPermissions :many
SELECT id, session_id, entity, action FROM session_macaroon_permissions
SELECT id, session_id, entity, action, position FROM session_macaroon_permissions
WHERE session_id = $1
ORDER BY position ASC
`
func (q *Queries) GetSessionMacaroonPermissions(ctx context.Context, sessionID int64) ([]SessionMacaroonPermission, error) {
@ -257,6 +260,7 @@ func (q *Queries) GetSessionMacaroonPermissions(ctx context.Context, sessionID i
&i.SessionID,
&i.Entity,
&i.Action,
&i.Position,
); err != nil {
return nil, err
}
@ -422,9 +426,9 @@ func (q *Queries) InsertSessionFeatureConfig(ctx context.Context, arg InsertSess
const insertSessionMacaroonCaveat = `-- name: InsertSessionMacaroonCaveat :exec
INSERT INTO session_macaroon_caveats (
session_id, caveat_id, verification_id, location
session_id, caveat_id, verification_id, location, position
) VALUES (
$1, $2, $3, $4
$1, $2, $3, $4, $5
)
`
@ -433,6 +437,7 @@ type InsertSessionMacaroonCaveatParams struct {
CaveatID []byte
VerificationID []byte
Location sql.NullString
Position int32
}
func (q *Queries) InsertSessionMacaroonCaveat(ctx context.Context, arg InsertSessionMacaroonCaveatParams) error {
@ -441,15 +446,16 @@ func (q *Queries) InsertSessionMacaroonCaveat(ctx context.Context, arg InsertSes
arg.CaveatID,
arg.VerificationID,
arg.Location,
arg.Position,
)
return err
}
const insertSessionMacaroonPermission = `-- name: InsertSessionMacaroonPermission :exec
INSERT INTO session_macaroon_permissions (
session_id, entity, action
session_id, entity, action, position
) VALUES (
$1, $2, $3
$1, $2, $3, $4
)
`
@ -457,10 +463,16 @@ type InsertSessionMacaroonPermissionParams struct {
SessionID int64
Entity string
Action string
Position int32
}
func (q *Queries) InsertSessionMacaroonPermission(ctx context.Context, arg InsertSessionMacaroonPermissionParams) error {
_, err := q.db.ExecContext(ctx, insertSessionMacaroonPermission, arg.SessionID, arg.Entity, arg.Action)
_, err := q.db.ExecContext(ctx, insertSessionMacaroonPermission,
arg.SessionID,
arg.Entity,
arg.Action,
arg.Position,
)
return err
}

View file

@ -109,6 +109,7 @@ type SessionMacaroonCaveat struct {
CaveatID []byte
VerificationID []byte
Location sql.NullString
Position int64
}
type SessionMacaroonPermission struct {
@ -116,6 +117,7 @@ type SessionMacaroonPermission struct {
SessionID int64
Entity string
Action string
Position int64
}
type SessionPrivacyFlag struct {

View file

@ -200,8 +200,9 @@ func (q *Queries) GetSessionIDByAlias(ctx context.Context, alias []byte) (int64,
}
const getSessionMacaroonCaveats = `-- name: GetSessionMacaroonCaveats :many
SELECT id, session_id, caveat_id, verification_id, location FROM session_macaroon_caveats
SELECT id, session_id, caveat_id, verification_id, location, position FROM session_macaroon_caveats
WHERE session_id = $1
ORDER BY position ASC
`
func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64) ([]SessionMacaroonCaveat, error) {
@ -219,6 +220,7 @@ func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64
&i.CaveatID,
&i.VerificationID,
&i.Location,
&i.Position,
); err != nil {
return nil, err
}
@ -234,8 +236,9 @@ func (q *Queries) GetSessionMacaroonCaveats(ctx context.Context, sessionID int64
}
const getSessionMacaroonPermissions = `-- name: GetSessionMacaroonPermissions :many
SELECT id, session_id, entity, action FROM session_macaroon_permissions
SELECT id, session_id, entity, action, position FROM session_macaroon_permissions
WHERE session_id = $1
ORDER BY position ASC
`
func (q *Queries) GetSessionMacaroonPermissions(ctx context.Context, sessionID int64) ([]SessionMacaroonPermission, error) {
@ -252,6 +255,7 @@ func (q *Queries) GetSessionMacaroonPermissions(ctx context.Context, sessionID i
&i.SessionID,
&i.Entity,
&i.Action,
&i.Position,
); err != nil {
return nil, err
}
@ -417,9 +421,9 @@ func (q *Queries) InsertSessionFeatureConfig(ctx context.Context, arg InsertSess
const insertSessionMacaroonCaveat = `-- name: InsertSessionMacaroonCaveat :exec
INSERT INTO session_macaroon_caveats (
session_id, caveat_id, verification_id, location
session_id, caveat_id, verification_id, location, position
) VALUES (
$1, $2, $3, $4
$1, $2, $3, $4, $5
)
`
@ -428,6 +432,7 @@ type InsertSessionMacaroonCaveatParams struct {
CaveatID []byte
VerificationID []byte
Location sql.NullString
Position int64
}
func (q *Queries) InsertSessionMacaroonCaveat(ctx context.Context, arg InsertSessionMacaroonCaveatParams) error {
@ -436,15 +441,16 @@ func (q *Queries) InsertSessionMacaroonCaveat(ctx context.Context, arg InsertSes
arg.CaveatID,
arg.VerificationID,
arg.Location,
arg.Position,
)
return err
}
const insertSessionMacaroonPermission = `-- name: InsertSessionMacaroonPermission :exec
INSERT INTO session_macaroon_permissions (
session_id, entity, action
session_id, entity, action, position
) VALUES (
$1, $2, $3
$1, $2, $3, $4
)
`
@ -452,10 +458,13 @@ type InsertSessionMacaroonPermissionParams struct {
SessionID int64
Entity string
Action string
Position int64
}
func (q *Queries) InsertSessionMacaroonPermission(ctx context.Context, arg InsertSessionMacaroonPermissionParams) error {
_, err := q.db.ExecContext(ctx, insertSessionMacaroonPermission, arg.SessionID, arg.Entity, arg.Action)
_, err := q.db.ExecContext(ctx, insertSessionMacaroonPermission,
arg.SessionID, arg.Entity, arg.Action, arg.Position,
)
return err
}

View file

@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"reflect"
"sort"
"time"
"github.com/btcsuite/btcd/btcec/v2"
@ -547,12 +546,13 @@ func migrateSingleSessionToSQL(ctx context.Context, tx *s6.Queries,
// into the linked child tables.
if session.MacaroonRecipe != nil {
// We start by inserting the macaroon permissions.
for _, sessionPerm := range session.MacaroonRecipe.Permissions {
for i, sessionPerm := range session.MacaroonRecipe.Permissions {
err = tx.InsertSessionMacaroonPermission(
ctx, s6.InsertSessionMacaroonPermissionParams{
SessionID: sqlId,
Entity: sessionPerm.Entity,
Action: sessionPerm.Action,
Position: int64(i),
},
)
if err != nil {
@ -561,7 +561,7 @@ func migrateSingleSessionToSQL(ctx context.Context, tx *s6.Queries,
}
// Next we insert the macaroon caveats.
for _, caveat := range session.MacaroonRecipe.Caveats {
for i, caveat := range session.MacaroonRecipe.Caveats {
err = tx.InsertSessionMacaroonCaveat(
ctx, s6.InsertSessionMacaroonCaveatParams{
SessionID: sqlId,
@ -570,6 +570,7 @@ func migrateSingleSessionToSQL(ctx context.Context, tx *s6.Queries,
Location: sqldb.SQLStr(
caveat.Location,
),
Position: int64(i),
},
)
if err != nil {
@ -650,10 +651,6 @@ func overrideSessionTimeZone(session *Session) {
// as nil in the bbolt store. Therefore, we also override the permissions
// or caveats to nil for the migrated session in that scenario, so that the
// deep equals check does not fail in this scenario either.
//
// Additionally, we sort the caveats & permissions of both the kv and sql
// sessions by their ID, so that they are always comparable in a deterministic
// way with deep equals.
func overrideMacaroonRecipe(kvSession *Session, migratedSession *Session) {
if kvSession.MacaroonRecipe != nil {
kvPerms := kvSession.MacaroonRecipe.Permissions
@ -671,30 +668,10 @@ func overrideMacaroonRecipe(kvSession *Session, migratedSession *Session) {
}
sqlCaveats := migratedSession.MacaroonRecipe.Caveats
sqlPerms := migratedSession.MacaroonRecipe.Permissions
// If there have been caveats set for the MacaroonRecipe,
// the order of the postgres db caveats will in very rare cases
// differ from the kv store caveats. Therefore, we sort
// both the kv and sql caveats by their ID, so that we can
// compare them in a deterministic way.
if kvCaveats != nil {
sort.Slice(kvCaveats, func(i, j int) bool {
return bytes.Compare(
kvCaveats[i].Id, kvCaveats[j].Id,
) < 0
})
sort.Slice(sqlCaveats, func(i, j int) bool {
return bytes.Compare(
sqlCaveats[i].Id, sqlCaveats[j].Id,
) < 0
})
}
// Empty caveat verification IDs can be persisted as nil by SQL
// backends, while the KV store can retain them as empty slices.
// After sorting, we only normalize caveats that still line up
// We only normalize caveats that still line up
// by ID. If the lengths or IDs differ, we leave the slices
// as-is and let the subsequent DeepEqual report the migration
// mismatch, hence let the migration fail.
@ -718,29 +695,6 @@ func overrideMacaroonRecipe(kvSession *Session, migratedSession *Session) {
}
}
}
// Similarly, we sort the macaroon permissions for both the kv
// and sql sessions, so that we can compare them in a
// deterministic way.
if kvPerms != nil {
sort.Slice(kvPerms, func(i, j int) bool {
if kvPerms[i].Entity == kvPerms[j].Entity {
return kvPerms[i].Action <
kvPerms[j].Action
}
return kvPerms[i].Entity < kvPerms[j].Entity
})
sort.Slice(sqlPerms, func(i, j int) bool {
if sqlPerms[i].Entity == sqlPerms[j].Entity {
return sqlPerms[i].Action <
sqlPerms[j].Action
}
return sqlPerms[i].Entity < sqlPerms[j].Entity
})
}
}
}

View file

@ -240,13 +240,14 @@ func (s *SQLStore) NewSession(ctx context.Context, label string, typ Type,
// Write mac perms and caveats.
if sess.MacaroonRecipe != nil {
for _, perm := range sess.MacaroonRecipe.Permissions {
for i, perm := range sess.MacaroonRecipe.Permissions {
// nolint:ll
err := db.InsertSessionMacaroonPermission(
ctx, sqlc.InsertSessionMacaroonPermissionParams{
SessionID: dbID,
Entity: perm.Entity,
Action: perm.Action,
Position: int32(i),
},
)
if err != nil {
@ -255,7 +256,7 @@ func (s *SQLStore) NewSession(ctx context.Context, label string, typ Type,
}
}
for _, caveat := range sess.MacaroonRecipe.Caveats {
for i, caveat := range sess.MacaroonRecipe.Caveats {
// nolint:ll
err := db.InsertSessionMacaroonCaveat(
ctx, sqlc.InsertSessionMacaroonCaveatParams{
@ -268,6 +269,7 @@ func (s *SQLStore) NewSession(ctx context.Context, label string, typ Type,
Valid: caveat.
Location != "",
},
Position: int32(i),
},
)
if err != nil {