Skip to content

Commit 9f58b16

Browse files
committed
itest: add private taproot V2 migration integration test
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
1 parent 9df3451 commit 9f58b16

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ var allTestCases = []*lntest.TestCase{
515515
Name: "simple taproot channel activation",
516516
TestFunc: testSimpleTaprootChannelActivation,
517517
},
518+
{
519+
Name: "private taproot v2 migration",
520+
TestFunc: testPrivateTaprootV2Migration,
521+
},
518522
{
519523
Name: "wallet import pubkey",
520524
TestFunc: testWalletImportPubKey,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package itest
2+
3+
import (
4+
"github.com/lightningnetwork/lnd/lnrpc"
5+
"github.com/lightningnetwork/lnd/lntest"
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
// testPrivateTaprootV2Migration tests that a private taproot channel survives
10+
// the V1→V2 SQL data migration. The test:
11+
//
12+
// 1. Starts Alice with --dev.skip-taproot-v2-migration so the migration
13+
// does not run on first startup.
14+
// 2. Opens a private taproot channel between Alice and Bob.
15+
// 3. Sends a payment from Alice to Bob to verify the channel works.
16+
// 4. Restarts Alice WITHOUT the skip flag so the migration runs.
17+
// 5. Verifies the channel is still active and payments still work.
18+
// 6. Bob updates his channel policy and Alice correctly receives and
19+
// persists the V1 update against the now-V2-stored channel.
20+
func testPrivateTaprootV2Migration(ht *lntest.HarnessTest) {
21+
// Args for taproot channel support.
22+
taprootArgs := lntest.NodeArgsForCommitType(
23+
lnrpc.CommitmentType_SIMPLE_TAPROOT,
24+
)
25+
26+
// Start Alice with the skip flag so the migration doesn't run yet.
27+
skipMigArgs := append(
28+
taprootArgs, "--dev.skip-taproot-v2-migration",
29+
)
30+
alice := ht.NewNodeWithCoins("Alice", skipMigArgs)
31+
bob := ht.NewNodeWithCoins("Bob", taprootArgs)
32+
33+
ht.EnsureConnected(alice, bob)
34+
35+
// Open a private taproot channel.
36+
const chanAmt = 1_000_000
37+
params := lntest.OpenChannelParams{
38+
Amt: chanAmt,
39+
CommitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
40+
Private: true,
41+
}
42+
pendingChan := ht.OpenChannelAssertPending(alice, bob, params)
43+
chanPoint := lntest.ChanPointFromPendingUpdate(pendingChan)
44+
45+
// Mine blocks to confirm the channel.
46+
ht.MineBlocksAndAssertNumTxes(6, 1)
47+
ht.AssertChannelActive(alice, chanPoint)
48+
ht.AssertChannelActive(bob, chanPoint)
49+
50+
// Send a payment pre-migration to verify the channel works.
51+
const paymentAmt = 10_000
52+
invoice := bob.RPC.AddInvoice(&lnrpc.Invoice{
53+
Value: paymentAmt,
54+
})
55+
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
56+
57+
// Restart Alice WITHOUT the skip flag. This allows the taproot V2
58+
// migration to run, converting the private taproot channel from V1
59+
// workaround storage to canonical V2.
60+
alice.SetExtraArgs(taprootArgs)
61+
ht.RestartNode(alice)
62+
63+
// Ensure reconnection.
64+
ht.EnsureConnected(alice, bob)
65+
66+
// Verify the channel is still active after migration.
67+
ht.AssertChannelActive(alice, chanPoint)
68+
69+
// Send another payment post-migration to verify the channel still
70+
// works for pathfinding and forwarding.
71+
invoice2 := bob.RPC.AddInvoice(&lnrpc.Invoice{
72+
Value: paymentAmt,
73+
})
74+
ht.CompletePaymentRequests(alice, []string{invoice2.PaymentRequest})
75+
76+
// Also send a payment in the other direction to verify both policy
77+
// directions survived the migration.
78+
invoice3 := alice.RPC.AddInvoice(&lnrpc.Invoice{
79+
Value: paymentAmt,
80+
})
81+
ht.CompletePaymentRequests(bob, []string{invoice3.PaymentRequest})
82+
83+
// Now test that Bob can update his channel policy and Alice
84+
// correctly receives the V1 ChannelUpdate and persists it against
85+
// the now-V2-stored channel. This exercises the UpdateEdgePolicy
86+
// shim that converts incoming V1 policy updates to V2 for migrated
87+
// private taproot channels.
88+
const (
89+
newBaseFee = 5000
90+
newFeeRate = 500
91+
newTimeLockDelta = 80
92+
)
93+
94+
expectedPolicy := &lnrpc.RoutingPolicy{
95+
FeeBaseMsat: newBaseFee,
96+
FeeRateMilliMsat: newFeeRate,
97+
TimeLockDelta: newTimeLockDelta,
98+
MinHtlc: 1000,
99+
MaxHtlcMsat: 990_000_000,
100+
}
101+
102+
req := &lnrpc.PolicyUpdateRequest{
103+
BaseFeeMsat: newBaseFee,
104+
FeeRate: float64(newFeeRate) / 1_000_000,
105+
TimeLockDelta: newTimeLockDelta,
106+
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
107+
ChanPoint: chanPoint,
108+
},
109+
}
110+
updateResp := bob.RPC.UpdateChannelPolicy(req)
111+
require.Empty(ht, updateResp.FailedUpdates)
112+
113+
// Wait for Alice to receive Bob's policy update. Use
114+
// includeUnannounced=true since this is a private channel.
115+
ht.AssertChannelPolicyUpdate(
116+
alice, bob, expectedPolicy, chanPoint, true,
117+
)
118+
119+
// Verify Alice can still route a payment to Bob using the
120+
// updated policy. This confirms the policy was correctly
121+
// persisted as V2 in Alice's graph.
122+
invoice4 := bob.RPC.AddInvoice(&lnrpc.Invoice{
123+
Value: paymentAmt,
124+
})
125+
ht.CompletePaymentRequests(alice, []string{invoice4.PaymentRequest})
126+
127+
// Now test the reverse: Alice (migrated, channel stored as V2)
128+
// updates her own policy. This exercises the path where Alice's
129+
// gossiper reads the V2 channel via the shim (projected as V1),
130+
// builds and signs a V1 ChannelUpdate, persists it as V2 via
131+
// the UpdateEdgePolicy shim, and sends the V1 update to Bob
132+
// (the legacy peer). Bob must correctly receive and apply it.
133+
const (
134+
aliceBaseFee = 3000
135+
aliceFeeRate = 300
136+
aliceTimeLockDelta = 60
137+
)
138+
139+
aliceExpectedPolicy := &lnrpc.RoutingPolicy{
140+
FeeBaseMsat: aliceBaseFee,
141+
FeeRateMilliMsat: aliceFeeRate,
142+
TimeLockDelta: aliceTimeLockDelta,
143+
MinHtlc: 1000,
144+
MaxHtlcMsat: 990_000_000,
145+
}
146+
147+
aliceReq := &lnrpc.PolicyUpdateRequest{
148+
BaseFeeMsat: aliceBaseFee,
149+
FeeRate: float64(aliceFeeRate) / 1_000_000,
150+
TimeLockDelta: aliceTimeLockDelta,
151+
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
152+
ChanPoint: chanPoint,
153+
},
154+
}
155+
aliceUpdateResp := alice.RPC.UpdateChannelPolicy(aliceReq)
156+
require.Empty(ht, aliceUpdateResp.FailedUpdates)
157+
158+
// Wait for Bob (legacy peer, no migration) to receive Alice's
159+
// V1 policy update.
160+
ht.AssertChannelPolicyUpdate(
161+
bob, alice, aliceExpectedPolicy, chanPoint, true,
162+
)
163+
164+
// Verify Bob can route a payment to Alice using the updated
165+
// policy. This confirms Alice's V1 ChannelUpdate was correctly
166+
// constructed from V2 storage and received by the legacy peer.
167+
invoice5 := alice.RPC.AddInvoice(&lnrpc.Invoice{
168+
Value: paymentAmt,
169+
})
170+
ht.CompletePaymentRequests(bob, []string{invoice5.PaymentRequest})
171+
172+
// Clean up.
173+
ht.CloseChannel(alice, chanPoint)
174+
}

0 commit comments

Comments
 (0)