From 3e671c45bbfae03dba97a663fe314158851e6b46 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 2 Dec 2025 19:19:18 +0100 Subject: [PATCH 1/4] aliasmgr: allow persisting manually added aliases Previously we were able to manually add scid aliases for channels but that would slightly misbehave especially around persistence and restarts. By default when a channel gets confirmed it won't get any of its aliases reloaded in memory. We change the existing base-lookup flag to also signal that an alias may be forcefully persisted and reloaded to the in-memory maps when the alias manager starts up. We achieve this by appending an extra byte to all the alias storage entries. Previously we'd have 8 bytes, and now we extend the entry to have 9 bytes. For old entries, the x00 byte is assumed which signals non-persistency. Any new entries will always have 9 bytes stored with the last byte serving as the signal for persistence. --- aliasmgr/aliasmgr.go | 113 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/aliasmgr/aliasmgr.go b/aliasmgr/aliasmgr.go index a8cd24878d5..9360dcbe1ce 100644 --- a/aliasmgr/aliasmgr.go +++ b/aliasmgr/aliasmgr.go @@ -59,6 +59,16 @@ var ( // operations. byteOrder = binary.BigEndian + // AliasFlagNone indicates standard alias behavior where the alias + // will not be added to aliasToBase after the channel is confirmed. + AliasFlagNone byte = 0x00 + + // AliasFlagPersistent indicates the alias should persist in the + // aliasToBase map even after the base SCID is confirmed. This is + // useful for manually added aliases that need to remain accessible + // via FindBaseSCID after 6 confirmations. + AliasFlagPersistent byte = 0x01 + // AliasStartBlockHeight is the starting block height of the alias // range. AliasStartBlockHeight uint32 = 16_000_000 @@ -146,6 +156,12 @@ func (m *Manager) populateMaps() error { // populate the Manager's actual maps. aliasMap := make(map[lnwire.ShortChannelID]lnwire.ShortChannelID) + // This map tracks aliases that have the persistent flag set and should + // remain in aliasToBase even after confirmation. + persistentAliasMap := make( + map[lnwire.ShortChannelID]lnwire.ShortChannelID, + ) + // This map caches the ChannelID/alias SCIDs stored in the database and // is used to populate the Manager's cache. peerAliasMap := make(map[lnwire.ChannelID]lnwire.ShortChannelID) @@ -177,14 +193,23 @@ func (m *Manager) populateMaps() error { err = aliasToBaseBucket.ForEach(func(k, v []byte) error { // The key will be the alias SCID and the value will be - // the base SCID. + // the base SCID (8 bytes) optionally followed by flags + // (1 byte). aliasScid := lnwire.NewShortChanIDFromInt( byteOrder.Uint64(k), ) baseScid := lnwire.NewShortChanIDFromInt( - byteOrder.Uint64(v), + byteOrder.Uint64(v[:8]), ) aliasMap[aliasScid] = baseScid + + // Check if the persistent flag is set. Backward + // compatible: old entries have len(v) == 8, new entries + // have len(v) == 9. + if len(v) > 8 && (v[8]&AliasFlagPersistent) != 0 { + persistentAliasMap[aliasScid] = baseScid + } + return nil }) if err != nil { @@ -214,6 +239,9 @@ func (m *Manager) populateMaps() error { }, func() { baseConfMap = make(map[lnwire.ShortChannelID]struct{}) aliasMap = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID) + persistentAliasMap = make( + map[lnwire.ShortChannelID]lnwire.ShortChannelID, + ) peerAliasMap = make(map[lnwire.ChannelID]lnwire.ShortChannelID) }) if err != nil { @@ -233,6 +261,13 @@ func (m *Manager) populateMaps() error { m.aliasToBase[aliasSCID] = baseSCID } + // Add persistent aliases to aliasToBase even if they're confirmed. + // This allows FindBaseSCID to work for manually-added aliases that + // should survive confirmation. + for aliasSCID, baseSCID := range persistentAliasMap { + m.aliasToBase[aliasSCID] = baseSCID + } + // Populate the peer alias cache. m.peerAlias = peerAliasMap @@ -252,7 +287,8 @@ type addAliasCfg struct { type AddLocalAliasOption func(cfg *addAliasCfg) // WithBaseLookup is a functional option that controls whether a reverse lookup -// will be stored from the alias to the base scid. +// will be stored from the alias to the base scid. This reverse lookup is +// persisted and will not be wiped when the channel is confirmed. func WithBaseLookup() AddLocalAliasOption { return func(cfg *addAliasCfg) { cfg.baseLookup = true @@ -268,7 +304,8 @@ func WithBaseLookup() AddLocalAliasOption { // // NOTE: The following aliases will not be persisted (will be lost on restart): // - Aliases that were created without gossip flag. -// - Aliases that correspond to confirmed channels. +// - Aliases that correspond to confirmed channels (unless WithBaseLookup +// option is used). func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID, gossip, linkUpdate bool, opts ...AddLocalAliasOption) error { @@ -315,14 +352,24 @@ func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID, return err } - var ( - aliasBytes [8]byte - baseBytes [8]byte - ) - + var aliasBytes [8]byte byteOrder.PutUint64(aliasBytes[:], alias.ToUint64()) - byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64()) - return aliasToBaseBucket.Put(aliasBytes[:], baseBytes[:]) + + // Write base SCID (8 bytes) + flags (1 byte). + // Always write 9 bytes for consistency and future extensibility. + var valueBytes [9]byte + byteOrder.PutUint64(valueBytes[:8], baseScid.ToUint64()) + + // Set the persistent flag if baseLookup is requested. + // The baseLookup option indicates this is a manually-added + // alias that should persist even after confirmation. + if cfg.baseLookup { + valueBytes[8] = AliasFlagPersistent + } else { + valueBytes[8] = AliasFlagNone + } + + return aliasToBaseBucket.Put(aliasBytes[:], valueBytes[:]) }, func() {}) if err != nil { return err @@ -396,6 +443,9 @@ func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error { m.Lock() defer m.Unlock() + // Track which aliases are persistent so we don't delete them. + persistentAliases := make(map[lnwire.ShortChannelID]struct{}) + err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error { baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket) if err != nil { @@ -404,16 +454,51 @@ func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error { var baseBytes [8]byte byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64()) - return baseConfBucket.Put(baseBytes[:], []byte{}) - }, func() {}) + err = baseConfBucket.Put(baseBytes[:], []byte{}) + if err != nil { + return err + } + + // Check which aliases for this base are marked as persistent. + aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket) + if err != nil { + return err + } + + return aliasToBaseBucket.ForEach(func(k, v []byte) error { + // Check if this entry maps to our baseScid. + entryBase := lnwire.NewShortChanIDFromInt( + byteOrder.Uint64(v[:8]), + ) + if entryBase.ToUint64() != baseScid.ToUint64() { + return nil + } + + // Check if persistent flag is set. + if len(v) > 8 && (v[8]&AliasFlagPersistent) != 0 { + aliasScid := lnwire.NewShortChanIDFromInt( + byteOrder.Uint64(k), + ) + persistentAliases[aliasScid] = struct{}{} + } + + return nil + }) + }, func() { + persistentAliases = make(map[lnwire.ShortChannelID]struct{}) + }) if err != nil { return err } // Now that the database state has been updated, we'll delete all of - // the aliasToBase mappings for this SCID. + // the aliasToBase mappings for this SCID, except persistent ones. for alias, base := range m.aliasToBase { if base.ToUint64() == baseScid.ToUint64() { + _, isPersistent := persistentAliases[alias] + if isPersistent { + continue + } delete(m.aliasToBase, alias) } } From 08ee8274f3694075ae119f3aaf22d7545823b675 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 2 Dec 2025 19:20:22 +0100 Subject: [PATCH 2/4] aliasmgr: add test coverage for alias persistency --- aliasmgr/aliasmgr_test.go | 234 +++++++++++++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 1 deletion(-) diff --git a/aliasmgr/aliasmgr_test.go b/aliasmgr/aliasmgr_test.go index 3237e5bbb19..beb0a208c93 100644 --- a/aliasmgr/aliasmgr_test.go +++ b/aliasmgr/aliasmgr_test.go @@ -180,7 +180,8 @@ func TestAliasLifecycle(t *testing.T) { // We now manually add the next alias from the range as a custom alias. // This time we also use the base lookup option, in order to be able to - // go from alias back to the base scid. + // go from alias back to the base scid. The WithBaseLookup option also + // marks this alias as persistent. secondAlias := getNextScid(firstRequested) err = aliasStore.AddLocalAlias( secondAlias, baseScid, false, true, WithBaseLookup(), @@ -266,3 +267,234 @@ func TestGetNextScid(t *testing.T) { }) } } + +// TestPersistentAlias tests that aliases marked as persistent remain in the +// aliasToBase map even after the base SCID is confirmed. +func TestPersistentAlias(t *testing.T) { + t.Parallel() + + // Create the backend database and use this to create the aliasStore. + dbPath := filepath.Join(t.TempDir(), "testdb") + db, err := kvdb.Create( + kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout, + false, + ) + require.NoError(t, err) + defer db.Close() + + linkUpdater := func(shortID lnwire.ShortChannelID) error { + return nil + } + + aliasStore, err := NewManager(db, linkUpdater) + require.NoError(t, err) + + const ( + base = uint64(123123123) + nonPersistentAlias = uint64(456456456) + persistentAlias = uint64(789789789) + ) + + baseScid := lnwire.NewShortChanIDFromInt(base) + nonPersistentScid := lnwire.NewShortChanIDFromInt(nonPersistentAlias) + persistentScid := lnwire.NewShortChanIDFromInt(persistentAlias) + + // Add a non-persistent alias (without WithBaseLookup option). + err = aliasStore.AddLocalAlias( + nonPersistentScid, baseScid, true, false, + ) + require.NoError(t, err) + + // Add a persistent alias (with WithBaseLookup option). + err = aliasStore.AddLocalAlias( + persistentScid, baseScid, true, false, WithBaseLookup(), + ) + require.NoError(t, err) + + // Both aliases should be in aliasToBase before confirmation. + _, err = aliasStore.FindBaseSCID(nonPersistentScid) + require.NoError(t, err) + + _, err = aliasStore.FindBaseSCID(persistentScid) + require.NoError(t, err) + + // Mark the base as confirmed (simulating 6 confirmations). + err = aliasStore.DeleteSixConfs(baseScid) + require.NoError(t, err) + + // Non-persistent alias should no longer be findable. + _, err = aliasStore.FindBaseSCID(nonPersistentScid) + require.Error(t, err) + + // Persistent alias should still be findable. + foundBase, err := aliasStore.FindBaseSCID(persistentScid) + require.NoError(t, err) + require.Equal(t, baseScid, foundBase) + + // Restart: create a new manager with the same database. + aliasStore2, err := NewManager(db, linkUpdater) + require.NoError(t, err) + + // After restart, non-persistent alias should still not be findable. + _, err = aliasStore2.FindBaseSCID(nonPersistentScid) + require.Error(t, err) + + // Persistent alias should still be findable after restart. + foundBase, err = aliasStore2.FindBaseSCID(persistentScid) + require.NoError(t, err) + require.Equal(t, baseScid, foundBase) + + // Both aliases should still be in baseToSet. + aliases := aliasStore2.GetAliases(baseScid) + require.Len(t, aliases, 2) + require.Contains(t, aliases, nonPersistentScid) + require.Contains(t, aliases, persistentScid) +} + +// TestBackwardCompatibility tests that old aliases (without flags) are still +// loaded correctly. +func TestBackwardCompatibility(t *testing.T) { + t.Parallel() + + // Create the backend database and use this to create the aliasStore. + dbPath := filepath.Join(t.TempDir(), "testdb") + db, err := kvdb.Create( + kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout, + false, + ) + require.NoError(t, err) + defer db.Close() + + linkUpdater := func(shortID lnwire.ShortChannelID) error { + return nil + } + + // Manually write an old-format alias (8 bytes, no flags). + const ( + base = uint64(111111111) + alias = uint64(222222222) + ) + + err = kvdb.Update(db, func(tx kvdb.RwTx) error { + bucket, err := tx.CreateTopLevelBucket(aliasBucket) + if err != nil { + return err + } + + var aliasBytes [8]byte + var baseBytes [8]byte + byteOrder.PutUint64(aliasBytes[:], alias) + byteOrder.PutUint64(baseBytes[:], base) + + // Write only 8 bytes (old format, no flags). + return bucket.Put(aliasBytes[:], baseBytes[:]) + }, func() {}) + require.NoError(t, err) + + // Create the manager - it should load the old entry. + aliasStore, err := NewManager(db, linkUpdater) + require.NoError(t, err) + + baseScid := lnwire.NewShortChanIDFromInt(base) + aliasScid := lnwire.NewShortChanIDFromInt(alias) + + // The alias should be in baseToSet. + aliases := aliasStore.GetAliases(baseScid) + require.Len(t, aliases, 1) + require.Contains(t, aliases, aliasScid) + + // The alias should be findable (since it's not confirmed). + foundBase, err := aliasStore.FindBaseSCID(aliasScid) + require.NoError(t, err) + require.Equal(t, baseScid, foundBase) + + // Mark as confirmed - old aliases should not persist. + err = aliasStore.DeleteSixConfs(baseScid) + require.NoError(t, err) + + // Should no longer be findable after confirmation. + _, err = aliasStore.FindBaseSCID(aliasScid) + require.Error(t, err) +} + +// TestDeletePersistentAlias tests that persistent aliases can be manually +// deleted via DeleteLocalAlias. +func TestDeletePersistentAlias(t *testing.T) { + t.Parallel() + + // Create the backend database and use this to create the aliasStore. + dbPath := filepath.Join(t.TempDir(), "testdb") + db, err := kvdb.Create( + kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout, + false, + ) + require.NoError(t, err) + defer db.Close() + + updateChan := make(chan struct{}, 1) + linkUpdater := func(shortID lnwire.ShortChannelID) error { + updateChan <- struct{}{} + return nil + } + + aliasStore, err := NewManager(db, linkUpdater) + require.NoError(t, err) + + const ( + base = uint64(123123123) + persistentAlias = uint64(789789789) + ) + + baseScid := lnwire.NewShortChanIDFromInt(base) + persistentScid := lnwire.NewShortChanIDFromInt(persistentAlias) + + // Add a persistent alias (with WithBaseLookup option). + err = aliasStore.AddLocalAlias( + persistentScid, baseScid, true, true, WithBaseLookup(), + ) + require.NoError(t, err) + + // Link updater should be called. + <-updateChan + + // Alias should be findable. + foundBase, err := aliasStore.FindBaseSCID(persistentScid) + require.NoError(t, err) + require.Equal(t, baseScid, foundBase) + + // Mark as confirmed - persistent alias should survive. + err = aliasStore.DeleteSixConfs(baseScid) + require.NoError(t, err) + + // Persistent alias should still be findable after confirmation. + foundBase, err = aliasStore.FindBaseSCID(persistentScid) + require.NoError(t, err) + require.Equal(t, baseScid, foundBase) + + // Now manually delete the persistent alias. + err = aliasStore.DeleteLocalAlias(persistentScid, baseScid) + require.NoError(t, err) + + // Link updater should be called. + <-updateChan + + // Alias should no longer be findable. + _, err = aliasStore.FindBaseSCID(persistentScid) + require.Error(t, err) + + // Alias should not be in baseToSet. + aliases := aliasStore.GetAliases(baseScid) + require.Len(t, aliases, 0) + + // Verify it's deleted from database by restarting. + aliasStore2, err := NewManager(db, linkUpdater) + require.NoError(t, err) + + // Should still not be findable after restart. + _, err = aliasStore2.FindBaseSCID(persistentScid) + require.Error(t, err) + + // Should not be in baseToSet after restart. + aliases = aliasStore2.GetAliases(baseScid) + require.Len(t, aliases, 0) +} From 4abd69be28acdc430da99ee469378eaf7506667c Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 2 Dec 2025 19:21:45 +0100 Subject: [PATCH 3/4] lnrpc: update comment on alias base lookup option --- lnrpc/routerrpc/router_server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 1dbc19e47f9..0b193d12bde 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -1716,7 +1716,10 @@ func (s *Server) XAddLocalChanAliases(_ context.Context, // We set the baseLookup flag as we want the alias // manager to keep a mapping from the alias back to its // base scid, in order to be able to provide it via the - // FindBaseLocalChanAlias RPC. + // FindBaseLocalChanAlias RPC. The baseLookup flag also + // marks these manually-added aliases as persistent so + // they'll survive through channel confirmation and + // restarts. err = s.cfg.AliasMgr.AddLocalAlias( aliasScid, baseScid, false, true, aliasmgr.WithBaseLookup(), From f66aa647d83f9937712f080c00dd0c444880024f Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Tue, 2 Dec 2025 19:37:59 +0100 Subject: [PATCH 4/4] docs: add release note for scid alias persistence --- docs/release-notes/release-notes-0.20.1.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/release-notes-0.20.1.md b/docs/release-notes/release-notes-0.20.1.md index 4cc4556fb0a..4d612398e7a 100644 --- a/docs/release-notes/release-notes-0.20.1.md +++ b/docs/release-notes/release-notes-0.20.1.md @@ -36,6 +36,11 @@ ## Functional Enhancements +- Aliases that are added via the `XAddLocalChanAliases` RPC will now be +persisted on restart and will be reloaded even for confirmed channels. The only +way to delete an alias added via the RPC is by calling the +`XDeleteLocalChanAliases` RPC endpoint. See more on the +[Github PR](https://github.com/lightningnetwork/lnd/pull/10411). ## RPC Additions ## lncli Additions