|
| 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