Skip to content

graph/db: private taproot V1→V2 channel migration#10676

Draft
ellemouton wants to merge 9 commits intolightningnetwork:masterfrom
ellemouton:migrate-tap-chans
Draft

graph/db: private taproot V1→V2 channel migration#10676
ellemouton wants to merge 9 commits intolightningnetwork:masterfrom
ellemouton:migrate-tap-chans

Conversation

@ellemouton
Copy link
Copy Markdown
Collaborator

Summary

Migrates private taproot channels from the V1 gossip workaround (V1 ChannelAnnouncement + SimpleTaprootChannelsRequiredStaging feature bit) to canonical V2 storage in the SQL graph backend. This is a DB-layer-only change — the gossiper, builder, server, and peer layers are unmodified.

Private taproot channels have always been stored as fake V1 gossip objects with a feature bit signalling taproot support. This was necessary because V2 gossip wasn't available when taproot channels were introduced. With V2 column support already in the SQL schema (from 000009_graph_v2_columns), we can now migrate these channels to their correct canonical representation ahead of full V2 gossip support.

Approach

SQL migration (version 17): Finds V1 channels with the taproot staging feature bit, computes the taproot funding script, creates V2 shell nodes, converts policies (timestamp → approximate block height), copies features/extras, and deletes the old V1 rows. Idempotent and SQL-only — no schema changes needed.

SQLStore shim: Since callers (gossiper, builder, etc.) still query with GossipVersion1, the SQL store transparently falls back to V2 for migrated private taproot channels and projects results back to V1 with the taproot feature bit re-added. This covers lookups (FetchChannelEdgesByID, FetchChannelEdgesByOutpoint, HasChannelEdge), iteration (ForEachChannel, ForEachNodeDirectedChannel, ChannelView, etc.), and writes (UpdateEdgePolicy converts incoming V1 policies to V2). All shim code is marked TODO(elle) for removal when the gossiper/builder gain full V2 support.

Policy update compatibility: When a legacy peer sends a V1 ChannelUpdate for a migrated channel, the shim converts it to V2 and persists it. When we update our own policy, the shim reads the V2 channel (projected as V1), the gossiper signs a V1 update, the shim persists it as V2, and the legacy peer receives a standard V1 wire message.

Key design decisions

  • DB-layer only: No changes to gossiper, builder, server, peer, or netann. The shim is entirely in sql_store.go and taproot_v2_shim.go.
  • Approximate timestamps: V2 policies use block heights; V1 uses unix timestamps. The conversion is approximate (height * 600 + genesis). Acceptable because these are our own private channels and policies get re-signed.
  • Private channels only: The shim guards (isPrivateTaprootV2, len(Signature) == 0) ensure only private channels are affected. No real V2 public channels exist until the gossiper gains V2 support.
  • KV backend unaffected: The KV store rejects V2 operations and is not touched by this migration.

Test plan

  • Unit tests: projection helpers (timestamp conversion, feature bit handling, policy field mapping)
  • Unit tests: migration happy path, idempotency, non-taproot channels untouched
  • Unit tests: full graph/db suite passes on both KV and SQLite backends
  • Integration test: open private taproot channel → pay → restart with migration → pay → Bob updates policy → Alice receives → Alice updates policy → Bob receives → pay both directions → close

🤖 Generated with Claude Code

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors how private taproot channels are stored in the database, moving them from an older V1 workaround to a canonical V2 representation. This change is crucial for improving data consistency and preparing the system for future full V2 gossip support. A transparent shim ensures backward compatibility, allowing existing V1-aware components to seamlessly interact with the newly migrated V2 channels without requiring immediate updates to those components.

Highlights

  • Private Taproot Channel Migration: Migrated private taproot channels from their V1 workaround storage format to canonical V2 storage within the SQL graph backend.
  • SQL Migration (Version 17): Introduced a new SQL migration that identifies V1 taproot channels, creates corresponding V2 shell nodes, computes and stores taproot funding scripts, converts policies from timestamp-based to block-height-based, copies features and extras, and deletes the old V1 rows. This migration is idempotent.
  • SQLStore Shim for V1 Compatibility: Implemented a transparent shim in the SQLStore to project migrated V2 private taproot channels back to a V1 representation. This ensures that existing V1-aware components (like the gossiper and builder) can continue to query and interact with these channels without modification.
  • Policy Update Compatibility: Ensured that legacy V1 ChannelUpdate messages for migrated channels are correctly converted to V2 and persisted. Conversely, when a migrated node updates its own policy, the V2 channel data is projected to V1 for signing, then persisted back as V2, and sent as a standard V1 wire message to legacy peers.
  • DB-Layer Only Changes: All modifications are confined to the database layer, specifically sql_store.go and new migration/shim files, leaving the gossiper, builder, server, and peer layers untouched.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ellemouton ellemouton marked this pull request as draft March 26, 2026 12:19
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a significant database migration for private Taproot channels, moving them from a V1 workaround storage format to a canonical V2 storage. The changes include defining a new migration version, implementing the core logic to convert channel and policy data, and introducing a compatibility shim to ensure existing V1-aware components can interact with the migrated V2 channels. The shim provides functions for projecting V2 data back to V1 and handling V1 policy updates for V2 channels. Comprehensive unit and integration tests have been added to validate the migration and shim behavior, alongside a development flag to skip the migration for testing. A review comment suggests an optimization to the migratePolicies function to improve efficiency by directly passing V2 node IDs, thereby avoiding redundant database queries.

Comment on lines +289 to +314
func migratePolicies(ctx context.Context, tx SQLQueries,
v1Chan sqlc.GraphChannel, c taprootMigrationCandidate,
v2ChanID int64) error {

// Map V1 node IDs to V2 node IDs via pubkey lookup. We already
// created the V2 shell nodes in migrateOneChannel.
v1NodeIDToV2 := make(map[int64]int64)
for _, entry := range []struct {
v1NodeID int64
pubKey []byte
}{
{v1Chan.NodeID1, c.node1PubKey},
{v1Chan.NodeID2, c.node2PubKey},
} {
v2ID, err := tx.GetNodeIDByPubKey(
ctx, sqlc.GetNodeIDByPubKeyParams{
Version: int16(gossipV2),
PubKey: entry.pubKey,
},
)
if err != nil {
return fmt.Errorf("fetching v2 node ID: %w", err)
}

v1NodeIDToV2[entry.v1NodeID] = v2ID
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The V2 node IDs (v2Node1ID and v2Node2ID) are already available in migrateOneChannel. To improve efficiency and avoid redundant database queries, you could pass these IDs directly to migratePolicies.

This would involve:

  1. Changing the signature of migratePolicies to accept v2Node1ID and v2Node2ID.
  2. Updating the call site in migrateOneChannel.
  3. Simplifying the logic inside migratePolicies to use the passed-in IDs instead of re-querying them.

For example, you could change migratePolicies to:

func migratePolicies(ctx context.Context, tx SQLQueries,
	v1Chan sqlc.GraphChannel, v2ChanID, v2Node1ID, v2Node2ID int64) error {

	// Map V1 node IDs to their V2 counterparts.
	v1NodeIDToV2 := map[int64]int64{
		v1Chan.NodeID1: v2Node1ID,
		v1Chan.NodeID2: v2Node2ID,
	}

	// ... rest of the function

And in migrateOneChannel:

	// ...
	if err := migratePolicies(
		ctx, tx, ch, v2ChanID, v2Node1ID, v2Node2ID,
	); err != nil {
		return fmt.Errorf("migrating policies: %w", err)
	}
	// ...

This refactoring would make the migration process more efficient, especially for nodes with a large number of channels to migrate.

func migratePolicies(ctx context.Context, tx SQLQueries,
	v1Chan sqlc.GraphChannel, v2ChanID, v2Node1ID, v2Node2ID int64) error {

	// Map V1 node IDs to their V2 counterparts.
	v1NodeIDToV2 := map[int64]int64{
		v1Chan.NodeID1: v2Node1ID,
		v1Chan.NodeID2: v2Node2ID,
	}

Add projection helpers for the private taproot V1→V2 migration shim.
These convert between V1 and V2 representations of channel edge info
and policies, enabling the SQL store to internally store migrated
private taproot channels as V2 while presenting them as V1 to callers
that are not yet V2-aware.

Includes helpers for:
- Approximate timestamp↔block-height conversion
- V2 edge → V1 edge projection (re-adds taproot staging feature bit)
- V2 policy → V1 policy projection (converts block height to timestamp)
- V1 policy → V2 policy projection (for write shim)
- Private taproot channel detection
Add a shim layer to the SQL store that makes migrated V2 private
taproot channels visible to V1-only callers. After the SQL migration
converts private taproot channels from V1 workaround storage to
canonical V2, callers (gossiper, builder, server) still query with
GossipVersion1. The shim transparently falls back to V2 and projects
results back to V1 with the taproot staging feature bit re-added.

Affected methods:
- FetchChannelEdgesByID: V2 fallback lookup on V1 miss
- FetchChannelEdgesByOutpoint: V2 fallback lookup on V1 miss
- HasChannelEdge: V2 fallback lookup on V1 miss
- UpdateEdgePolicy: V1→V2 policy translation when channel is V2
- ForEachChannel: includes V2 private taproot channels in V1 iteration
- ForEachChannelCacheable: includes V2 channels in cache for pathfinding
- ForEachNodeDirectedChannel: includes V2 channels per-node
- ForEachNodeChannel: includes V2 channels per-node
- ChannelView: includes V2 channels in startup chain filter
- FetchChanInfos: V2 fallback for missing V1 channel IDs

All shim code is marked with TODO(elle) for removal when the
gossiper/builder gain full V2 support.
Add the migration function that converts private taproot channels from
V1 workaround storage (V1 + SimpleTaprootChannelsStaging feature bit)
to canonical V2 storage. The migration:

- Finds V1 channels with the taproot staging feature bit (180/181)
- Creates V2 shell nodes for both channel endpoints
- Computes and stores the taproot funding script
- Converts policies (timestamp → approximate block height)
- Copies features (minus taproot staging bits), extras, policy extras
- Deletes old V1 rows (CASCADE handles cleanup)

The migration is idempotent and runs as a SQL-only data transformation
within the existing schema (no DDL changes needed). Also adds
RunTaprootV2Migration method on SQLStore for test use, and unit tests
covering the happy path, idempotency, and non-taproot channels being
untouched.
Register the private taproot migration as database version 17 in the
migration config and wire the migration function in config_builder.go.
The migration runs after all existing migrations (including KV→SQL
graph migration if applicable) and converts private taproot channels
from V1 workaround storage to canonical V2.

No schema version change is needed since V2 channel columns already
exist from the 000009_graph_v2_columns migration.
Add --dev.skip-taproot-v2-migration flag (integration builds only) to
allow itests to control when the private taproot V1→V2 migration runs.
The flag is always false in production builds.
Add an integration test that verifies a private taproot channel
survives the V1→V2 SQL data migration:

1. Start Alice with --dev.skip-taproot-v2-migration
2. Open a private taproot channel, send payment (V1 workaround storage)
3. Restart Alice without the skip flag (migration runs)
4. Verify channel is still active and payments work in both directions
@ellemouton ellemouton changed the base branch from elle-v2-shim-base to master March 27, 2026 15:49
@saubyk saubyk added this to lnd v0.22 Mar 30, 2026
@github-project-automation github-project-automation bot moved this to Backlog in lnd v0.22 Mar 30, 2026
@saubyk saubyk moved this from Backlog to In progress in lnd v0.22 Mar 30, 2026
@saubyk saubyk removed this from v0.21 Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants