From 5e0a2d3f92c7ba0a3e0eac36d0701cfe2715a576 Mon Sep 17 00:00:00 2001 From: eugene Date: Mon, 4 Apr 2022 17:01:07 -0400 Subject: [PATCH] itest: zero-conf, scid-alias channel-type integration tests --- lntest/itest/lnd_channel_backup_test.go | 118 +- .../lnd_multi-hop_htlc_aggregation_test.go | 5 +- ...d_multi-hop_htlc_local_chain_claim_test.go | 5 +- .../lnd_multi-hop_htlc_local_timeout_test.go | 5 +- ...ulti-hop_htlc_receiver_chain_claim_test.go | 5 +- ..._multi-hop_htlc_remote_chain_claim_test.go | 5 +- ..._force_close_on_chain_htlc_timeout_test.go | 5 +- ..._force_close_on_chain_htlc_timeout_test.go | 5 +- lntest/itest/lnd_multi-hop_test.go | 69 +- lntest/itest/lnd_test_list_on_test.go | 16 + lntest/itest/lnd_zero_conf_test.go | 1042 +++++++++++++++++ 11 files changed, 1248 insertions(+), 32 deletions(-) create mode 100644 lntest/itest/lnd_zero_conf_test.go diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index 91455ddcc..ade29582f 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -447,6 +447,97 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) { ) }, }, + + // Restore the backup from the on-disk file, using the RPC + // interface, for zero-conf anchor channels. + { + name: "restore from backup file for zero-conf " + + "anchors channel", + initiator: true, + private: false, + commitmentType: lnrpc.CommitmentType_ANCHORS, + zeroConf: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channels.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that we'll restore + // using the on-disk channels.backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + oldNode, + ) + }, + }, + + // Restore the backup from the on-disk file, using the RPC + // interface for a zero-conf script-enforced leased channel. + { + name: "restore from backup file zero-conf " + + "script-enforced leased channel", + initiator: true, + private: false, + commitmentType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, + zeroConf: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channel.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that we'll restore + // using the on-disk channel backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + oldNode, + ) + }, + }, + + // Restore a zero-conf anchors channel that was force closed by + // dave just before going offline. + { + name: "restore force closed from backup file " + + "anchors w/ zero-conf", + initiator: true, + private: false, + commitmentType: lnrpc.CommitmentType_ANCHORS, + localForceClose: true, + zeroConf: true, + restoreMethod: func(oldNode *lntest.HarnessNode, + backupFilePath string, + mnemonic []string) (nodeRestorer, error) { + + // Read the entire Multi backup stored within + // this node's channel.backup file. + multi, err := ioutil.ReadFile(backupFilePath) + if err != nil { + return nil, err + } + + // Now that we have Dave's backup file, we'll + // create a new nodeRestorer that we'll restore + // using the on-disk channel backup. + return chanRestoreViaRPC( + net, password, mnemonic, multi, + oldNode, + ) + }, + }, } // TODO(roasbeef): online vs offline close? @@ -861,6 +952,10 @@ type chanRestoreTestCase struct { restoreMethod func(oldNode *lntest.HarnessNode, backupFilePath string, mnemonic []string) (nodeRestorer, error) + + // zeroConf denotes whether the opened channel is a zero-conf channel + // or not. + zeroConf bool } // testChanRestoreScenario executes a chanRestoreTestCase from end to end, @@ -886,6 +981,13 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, nodeArgs = append(nodeArgs, args...) } + if testCase.zeroConf { + nodeArgs = append( + nodeArgs, "--protocol.option-scid-alias", + "--protocol.zero-conf", + ) + } + // First, we'll create a brand new node we'll use within the test. If // we have a custom backup file specified, then we'll also create that // for use. @@ -971,14 +1073,16 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, net, t, from, to, chanAmt, thawHeight, true, ) } + params := lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: pushAmt, + Private: testCase.private, + FundingShim: fundingShim, + CommitmentType: testCase.commitmentType, + ZeroConf: testCase.zeroConf, + } chanPoint = openChannelAndAssert( - t, net, from, to, lntest.OpenChannelParams{ - Amt: chanAmt, - PushAmt: pushAmt, - Private: testCase.private, - FundingShim: fundingShim, - CommitmentType: testCase.commitmentType, - }, + t, net, from, to, params, ) // Wait for both sides to see the opened channel. diff --git a/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go index bdce29711..ecca7efb2 100644 --- a/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go @@ -23,14 +23,15 @@ import ( // case of anchor channels, the second-level spends can also be aggregated and // properly feebumped, so we'll check that as well. func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest, - alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { const finalCltvDelta = 40 ctxb := context.Background() // First, we'll create a three hop network: Alice -> Bob -> Carol. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, false, c, + t, net, alice, bob, false, c, zeroConf, ) defer shutdownAndAssert(net, t, carol) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go index a7beae81d..f35c025da 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go @@ -20,7 +20,8 @@ import ( // preimage via the witness beacon, we properly settle the HTLC on-chain using // the HTLC success transaction in order to ensure we don't lose any funds. func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, - alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -28,7 +29,7 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, false, c, + t, net, alice, bob, false, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go index e68bca05e..9c5063075 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go @@ -20,7 +20,8 @@ import ( // canceled backwards. Once the timeout has been reached, then we should sweep // it on-chain, and cancel the HTLC backwards. func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest, - alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -28,7 +29,7 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest, // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, true, c, + t, net, alice, bob, true, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go index 8ce72a639..348361447 100644 --- a/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go @@ -22,7 +22,8 @@ import ( // extract the preimage from the sweep transaction, and finish settling the // HTLC backwards into the route. func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, - alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -30,7 +31,7 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, false, c, + t, net, alice, bob, false, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go index e1a835514..b354fdc3f 100644 --- a/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go @@ -20,7 +20,8 @@ import ( // HTLC directly on-chain using the preimage in order to ensure that we don't // lose any funds. func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest, - alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -28,7 +29,7 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, false, c, + t, net, alice, bob, false, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go index 5f7fb6a1b..ba57f0331 100644 --- a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go @@ -19,7 +19,8 @@ import ( // that's timed out. At this point, the node should timeout the HTLC using the // HTLC timeout transaction, then cancel it backwards as normal. func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, - t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -27,7 +28,7 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, true, c, + t, net, alice, bob, true, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go b/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go index 74b3ca367..e1fda55ee 100644 --- a/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go @@ -21,7 +21,8 @@ import ( // transaction once the timeout has expired. Once we sweep the transaction, we // should also cancel back the initial HTLC. func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, - t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) { + t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) { ctxb := context.Background() @@ -29,7 +30,7 @@ func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, // Carol refusing to actually settle or directly cancel any HTLC's // self. aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( - t, net, alice, bob, true, c, + t, net, alice, bob, true, c, zeroConf, ) // Clean up carol's node when the test finishes. diff --git a/lntest/itest/lnd_multi-hop_test.go b/lntest/itest/lnd_multi-hop_test.go index 79d3439d8..d69ba0110 100644 --- a/lntest/itest/lnd_multi-hop_test.go +++ b/lntest/itest/lnd_multi-hop_test.go @@ -19,7 +19,8 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) { type testCase struct { name string test func(net *lntest.NetworkHarness, t *harnessTest, alice, - bob *lntest.HarnessNode, c lnrpc.CommitmentType) + bob *lntest.HarnessNode, c lnrpc.CommitmentType, + zeroConf bool) } subTests := []testCase{ @@ -68,20 +69,51 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) { }, } - commitTypes := []lnrpc.CommitmentType{ - lnrpc.CommitmentType_LEGACY, - lnrpc.CommitmentType_ANCHORS, - lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, + commitWithZeroConf := []struct { + commitType lnrpc.CommitmentType + zeroConf bool + }{ + { + commitType: lnrpc.CommitmentType_LEGACY, + zeroConf: false, + }, + { + commitType: lnrpc.CommitmentType_ANCHORS, + zeroConf: false, + }, + { + commitType: lnrpc.CommitmentType_ANCHORS, + zeroConf: true, + }, + { + commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, + zeroConf: false, + }, + { + commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, + zeroConf: true, + }, } - for _, commitType := range commitTypes { - commitType := commitType - testName := fmt.Sprintf("committype=%v", commitType.String()) + for _, typeAndConf := range commitWithZeroConf { + typeAndConf := typeAndConf + testName := fmt.Sprintf( + "committype=%v zeroconf=%v", + typeAndConf.commitType.String(), typeAndConf.zeroConf, + ) success := t.t.Run(testName, func(t *testing.T) { ht := newHarnessTest(t, net) - args := nodeArgsForCommitType(commitType) + args := nodeArgsForCommitType(typeAndConf.commitType) + + if typeAndConf.zeroConf { + args = append( + args, "--protocol.option-scid-alias", + "--protocol.zero-conf", + ) + } + alice := net.NewNode(t, "Alice", args) defer shutdownAndAssert(net, ht, alice) @@ -107,7 +139,11 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) { // static fee estimate. net.SetFeeEstimate(12500) - subTest.test(net, ht, alice, bob, commitType) + subTest.test( + net, ht, alice, bob, + typeAndConf.commitType, + typeAndConf.zeroConf, + ) }) if !success { return @@ -204,7 +240,8 @@ func checkPaymentStatus(node *lntest.HarnessNode, preimage lntypes.Preimage, } func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, - alice, bob *lntest.HarnessNode, carolHodl bool, c lnrpc.CommitmentType) ( + alice, bob *lntest.HarnessNode, carolHodl bool, c lnrpc.CommitmentType, + zeroConf bool) ( *lnrpc.ChannelPoint, *lnrpc.ChannelPoint, *lntest.HarnessNode) { net.EnsureConnected(t.t, alice, bob) @@ -234,6 +271,7 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, Amt: chanAmt, CommitmentType: c, FundingShim: aliceFundingShim, + ZeroConf: zeroConf, }, ) @@ -254,6 +292,14 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, if carolHodl { carolFlags = append(carolFlags, "--hodl.exit-settle") } + + if zeroConf { + carolFlags = append( + carolFlags, "--protocol.option-scid-alias", + "--protocol.zero-conf", + ) + } + carol := net.NewNode(t.t, "Carol", carolFlags) net.ConnectNodes(t.t, bob, carol) @@ -280,6 +326,7 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, Amt: chanAmt, CommitmentType: c, FundingShim: bobFundingShim, + ZeroConf: zeroConf, }, ) err = bob.WaitForNetworkChannelOpen(bobChanPoint) diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 82b71e9f1..5585ae472 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -407,4 +407,20 @@ var allTestCases = []*testCase{ name: "resolution handoff", test: testResHandoff, }, + { + name: "zero conf channel open", + test: testZeroConfChannelOpen, + }, + { + name: "option scid alias", + test: testOptionScidAlias, + }, + { + name: "scid alias channel update", + test: testUpdateChannelPolicyScidAlias, + }, + { + name: "scid alias upgrade", + test: testOptionScidUpgrade, + }, } diff --git a/lntest/itest/lnd_zero_conf_test.go b/lntest/itest/lnd_zero_conf_test.go new file mode 100644 index 000000000..694038e4f --- /dev/null +++ b/lntest/itest/lnd_zero_conf_test.go @@ -0,0 +1,1042 @@ +package itest + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// testZeroConfChannelOpen tests that opening a zero-conf channel works and +// sending payments also works. +func testZeroConfChannelOpen(net *lntest.NetworkHarness, t *harnessTest) { + // Since option-scid-alias is opt-in, the provided harness nodes will + // not have the feature bit set. Also need to set anchors as those are + // default-off in itests. + scidAliasArgs := []string{ + "--protocol.option-scid-alias", + "--protocol.zero-conf", + "--protocol.anchors", + } + + carol := net.NewNode(t.t, "Carol", scidAliasArgs) + defer shutdownAndAssert(net, t, carol) + + // We'll open a regular public channel between Bob and Carol here. + net.EnsureConnected(t.t, net.Bob, carol) + + chanAmt := btcutil.Amount(1_000_000) + + fundingPoint := openChannelAndAssert( + t, net, net.Bob, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + // Wait for both Bob and Carol to view the channel as active. + ctxb := context.Background() + err := net.Bob.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "bob didn't report channel") + err = carol.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "carol didn't report channel") + + // Spin-up Dave so Carol can open a zero-conf channel to him. + dave := net.NewNode(t.t, "Dave", scidAliasArgs) + defer shutdownAndAssert(net, t, dave) + + // We'll give Carol some coins in order to fund the channel. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + + // Ensure that both Carol and Dave are connected. + net.EnsureConnected(t.t, carol, dave) + + // Open a private zero-conf anchors channel of 1M satoshis. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + Private: true, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + ZeroConf: true, + } + chanOpenUpdate := openChannelStream(t, net, carol, dave, params) + + // We should receive the OpenStatusUpdate_ChanOpen update without + // having to mine any blocks. + fundingPoint2, err := net.WaitForChannelOpen(chanOpenUpdate) + require.NoError(t.t, err, "error while waiting for channel open") + + err = carol.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err, "carol didn't report channel") + err = dave.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err, "dave didn't report channel") + + // Attempt to send a 10K satoshi payment from Carol to Dave. + daveInvoiceParams := &lnrpc.Invoice{ + Value: int64(10_000), + Private: true, + } + daveInvoiceResp, err := dave.AddInvoice( + ctxb, daveInvoiceParams, + ) + require.NoError(t.t, err, "unable to add invoice") + _ = sendAndAssertSuccess( + t, carol, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // Now attempt to send a multi-hop payment from Bob to Dave. This tests + // that Dave issues an invoice with an alias SCID that Carol knows and + // uses to forward to Dave. + daveInvoiceResp2, err := dave.AddInvoice(ctxb, daveInvoiceParams) + require.NoError(t.t, err, "unable to add invoice") + _ = sendAndAssertSuccess( + t, net.Bob, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp2.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // Check that Dave has a zero-conf alias SCID in the graph. + descReq := &lnrpc.ChannelGraphRequest{ + IncludeUnannounced: true, + } + + err = waitForZeroConfGraphChange(ctxb, dave, descReq, true) + require.NoError(t.t, err) + + // We'll now confirm the zero-conf channel between Carol and Dave and + // assert that sending is still possible. + block := mineBlocks(t, net, 6, 1)[0] + + // Dave should still have the alias edge in his db. + err = waitForZeroConfGraphChange(ctxb, dave, descReq, true) + require.NoError(t.t, err) + + fundingTxID, err := lnrpc.GetChanPointFundingTxid(fundingPoint2) + require.NoError(t.t, err, "unable to get txid") + + assertTxInBlock(t, block, fundingTxID) + + daveInvoiceResp3, err := dave.AddInvoice( + ctxb, daveInvoiceParams, + ) + require.NoError(t.t, err, "unable to add invoice") + _ = sendAndAssertSuccess( + t, net.Bob, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp3.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // Eve will now initiate a zero-conf channel with Carol. This tests + // that the ChannelUpdates sent are correct since they will be + // referring to different alias SCIDs. + eve := net.NewNode(t.t, "Eve", scidAliasArgs) + defer shutdownAndAssert(net, t, eve) + + net.EnsureConnected(t.t, eve, carol) + + // Give Eve some coins to fund the channel. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, eve) + + // We'll open a public zero-conf anchors channel of 1M satoshis. + params.Private = false + chanOpenUpdate2 := openChannelStream(t, net, eve, carol, params) + + // Wait to receive the OpenStatusUpdate_ChanOpen update. + fundingPoint3, err := net.WaitForChannelOpen(chanOpenUpdate2) + require.NoError(t.t, err, "error while waiting for channel open") + + err = eve.WaitForNetworkChannelOpen(fundingPoint3) + require.NoError(t.t, err, "eve didn't report channel") + err = carol.WaitForNetworkChannelOpen(fundingPoint3) + require.NoError(t.t, err, "carol didn't report channel") + + // Attempt to send a 20K satoshi payment from Eve to Dave. + daveInvoiceParams.Value = int64(20_000) + daveInvoiceResp4, err := dave.AddInvoice( + ctxb, daveInvoiceParams, + ) + require.NoError(t.t, err, "unable to add invoice") + _ = sendAndAssertSuccess( + t, eve, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp4.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // Assert that Eve has stored the zero-conf alias in her graph. + err = waitForZeroConfGraphChange(ctxb, eve, descReq, true) + require.NoError(t.t, err) + + // We'll confirm the zero-conf channel between Eve and Carol and assert + // that sending is still possible. + block = mineBlocks(t, net, 6, 1)[0] + + fundingTxID, err = lnrpc.GetChanPointFundingTxid(fundingPoint3) + require.NoError(t.t, err, "unable to get txid") + + assertTxInBlock(t, block, fundingTxID) + + // Wait until Eve's ZeroConf channel is replaced by the confirmed SCID + // in her graph. + err = waitForZeroConfGraphChange(ctxb, eve, descReq, false) + require.NoError(t.t, err, "expected to not receive error") + + // Attempt to send a 6K satoshi payment from Dave to Eve. + eveInvoiceParams := &lnrpc.Invoice{ + Value: int64(6_000), + Private: true, + } + eveInvoiceResp, err := eve.AddInvoice(ctxb, eveInvoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + // Assert that route hints is empty since the channel is public. + payReq, err := eve.DecodePayReq(ctxb, &lnrpc.PayReqString{ + PayReq: eveInvoiceResp.PaymentRequest, + }) + require.NoError(t.t, err) + require.True(t.t, len(payReq.RouteHints) == 0) + + _ = sendAndAssertSuccess( + t, dave, &routerrpc.SendPaymentRequest{ + PaymentRequest: eveInvoiceResp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) +} + +// testOptionScidAlias checks that opening an option_scid_alias channel-type +// channel or w/o the channel-type works properly. +func testOptionScidAlias(net *lntest.NetworkHarness, t *harnessTest) { + type scidTestCase struct { + name string + + // If this is false, then the channel will be a regular non + // channel-type option-scid-alias-feature-bit channel. + chantype bool + + private bool + } + + var testCases = []scidTestCase{ + { + name: "private chan-type", + chantype: true, + private: true, + }, + { + name: "public no chan-type", + chantype: false, + private: false, + }, + { + name: "private no chan-type", + chantype: false, + private: true, + }, + } + + for _, testCase := range testCases { + testCase := testCase + success := t.t.Run(testCase.name, func(t *testing.T) { + h := newHarnessTest(t, net) + optionScidAliasScenario( + net, h, testCase.chantype, testCase.private, + ) + }) + if !success { + break + } + } +} + +func optionScidAliasScenario(net *lntest.NetworkHarness, t *harnessTest, + chantype, private bool) { + + ctxb := context.Background() + + // Option-scid-alias is opt-in, as is anchors. + scidAliasArgs := []string{ + "--protocol.option-scid-alias", + "--protocol.anchors", + } + + carol := net.NewNode(t.t, "Carol", scidAliasArgs) + defer shutdownAndAssert(net, t, carol) + + dave := net.NewNode(t.t, "Dave", scidAliasArgs) + defer shutdownAndAssert(net, t, dave) + + // Ensure Bob, Carol are connected. + net.EnsureConnected(t.t, net.Bob, carol) + + // Ensure Carol, Dave are connected. + net.EnsureConnected(t.t, carol, dave) + + // Give Carol some coins so she can open the channel. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + + chanAmt := btcutil.Amount(1_000_000) + + params := lntest.OpenChannelParams{ + Amt: chanAmt, + Private: private, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + ScidAlias: chantype, + } + fundingPoint := openChannelAndAssert(t, net, carol, dave, params) + + if !private { + err := net.Bob.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "bob didn't report channel") + } + + err := carol.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "carol didn't report channel") + err = dave.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "dave didn't report channel") + + // Assert that a payment from Carol to Dave works as expected. + daveInvoiceParams := &lnrpc.Invoice{ + Value: int64(10_000), + Private: true, + } + daveInvoiceResp, err := dave.AddInvoice(ctxb, daveInvoiceParams) + require.NoError(t.t, err) + _ = sendAndAssertSuccess( + t, carol, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + // We'll now open a regular public channel between Bob and Carol and + // assert that Bob can pay Dave. We'll also assert that the invoice + // Dave issues has the startingAlias as a hop hint. + fundingPoint2 := openChannelAndAssert( + t, net, net.Bob, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + err = net.Bob.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + err = carol.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + + // Wait until Dave receives the Bob<->Carol channel. + err = dave.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + + daveInvoiceResp2, err := dave.AddInvoice(ctxb, daveInvoiceParams) + require.NoError(t.t, err) + davePayReq := &lnrpc.PayReqString{ + PayReq: daveInvoiceResp2.PaymentRequest, + } + + decodedReq, err := dave.DecodePayReq(ctxb, davePayReq) + require.NoError(t.t, err) + + if !private { + require.Equal(t.t, 0, len(decodedReq.RouteHints)) + payReq := daveInvoiceResp2.PaymentRequest + _ = sendAndAssertSuccess( + t, net.Bob, &routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + return + } + + require.Equal(t.t, 1, len(decodedReq.RouteHints)) + require.Equal(t.t, 1, len(decodedReq.RouteHints[0].HopHints)) + + startingAlias := lnwire.ShortChannelID{ + BlockHeight: 16_000_000, + TxIndex: 0, + TxPosition: 0, + } + + daveHopHint := decodedReq.RouteHints[0].HopHints[0].ChanId + require.Equal(t.t, startingAlias.ToUint64(), daveHopHint) + + _ = sendAndAssertSuccess( + t, net.Bob, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoiceResp2.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) +} + +// waitForZeroConfGraphChange waits for the zero-conf channel to be visible in +// the graph after confirmation or not. The expect argument denotes whether the +// zero-conf is expected in the graph or not. There should always be at least +// one channel of the passed HarnessNode, zero-conf or not. +func waitForZeroConfGraphChange(ctxb context.Context, n *lntest.HarnessNode, + req *lnrpc.ChannelGraphRequest, expect bool) error { + + return wait.NoError(func() error { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + graph, err := n.DescribeGraph(ctxt, req) + if err != nil { + return err + } + + if expect { + // If we expect a zero-conf channel, we'll assert that + // one exists, both policies exist, and we are party to + // the channel. + for _, e := range graph.Edges { + // The BlockHeight will be less than 16_000_000 + // if this is not a zero-conf channel. + scid := lnwire.NewShortChanIDFromInt( + e.ChannelId, + ) + if scid.BlockHeight < 16_000_000 { + continue + } + + // Both edge policies must exist in the zero + // conf case. + if e.Node1Policy == nil || + e.Node2Policy == nil { + + continue + } + + // Check if we are party to the zero-conf + // channel. + if e.Node1Pub == n.PubKeyStr || + e.Node2Pub == n.PubKeyStr { + + return nil + } + } + + return errors.New("failed to find zero-conf channel " + + "in graph") + } + + // If we don't expect a zero-conf channel, we'll assert that + // none exist, that we have a non-zero-conf channel with at + // both policies, and one of the policies in the database is + // ours. + for _, e := range graph.Edges { + scid := lnwire.NewShortChanIDFromInt(e.ChannelId) + if scid.BlockHeight == 16_000_000 { + return errors.New("found zero-conf channel") + } + + // One of the edge policies must exist. + if e.Node1Policy == nil || e.Node2Policy == nil { + continue + } + + // If we are part of this channel, exit gracefully. + if e.Node1Pub == n.PubKeyStr || + e.Node2Pub == n.PubKeyStr { + + return nil + } + } + + return errors.New( + "failed to find non-zero-conf channel in graph", + ) + }, defaultTimeout) +} + +// testUpdateChannelPolicyScidAlias checks that option-scid-alias, zero-conf +// channel-types, and option-scid-alias feature-bit-only channels have the +// expected graph and that payments work when updating the channel policy. +func testUpdateChannelPolicyScidAlias(net *lntest.NetworkHarness, + t *harnessTest) { + + tests := []struct { + name string + + // The option-scid-alias channel type. + scidAliasType bool + + // The zero-conf channel type. + zeroConf bool + + private bool + }{ + { + name: "private scid-alias chantype update", + scidAliasType: true, + private: true, + }, + { + name: "private zero-conf update", + zeroConf: true, + private: true, + }, + { + name: "public zero-conf update", + zeroConf: true, + }, + { + name: "public no-chan-type update", + }, + { + name: "private no-chan-type update", + private: true, + }, + } + + for _, test := range tests { + test := test + + success := t.t.Run(test.name, func(t *testing.T) { + ht := newHarnessTest(t, net) + testPrivateUpdateAlias( + net, ht, test.zeroConf, test.scidAliasType, + test.private, + ) + }) + if !success { + return + } + } +} + +func testPrivateUpdateAlias(net *lntest.NetworkHarness, t *harnessTest, + zeroConf, scidAliasType, private bool) { + + ctxb := context.Background() + defer ctxb.Done() + + // We'll create a new node Eve that will not have option-scid-alias + // channels. + eve := net.NewNode(t.t, "Eve", nil) + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, eve) + defer shutdownAndAssert(net, t, eve) + + // Since option-scid-alias is opt-in we'll need to specify the protocol + // arguments when creating a new node. + scidAliasArgs := []string{ + "--protocol.option-scid-alias", + "--protocol.zero-conf", + "--protocol.anchors", + } + + carol := net.NewNode(t.t, "Carol", scidAliasArgs) + defer shutdownAndAssert(net, t, carol) + + // Spin-up Dave who will have an option-scid-alias feature-bit-only or + // channel-type channel with Carol. + dave := net.NewNode(t.t, "Dave", scidAliasArgs) + defer shutdownAndAssert(net, t, dave) + + // We'll give Carol some coins in order to fund the channel. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + + // Ensure that Carol and Dave are connected. + net.EnsureConnected(t.t, carol, dave) + + // We'll open a regular public channel between Eve and Carol here. Eve + // will be the one receiving the onion-encrypted ChannelUpdate. + net.EnsureConnected(t.t, eve, carol) + + chanAmt := btcutil.Amount(1_000_000) + + fundingPoint := openChannelAndAssert( + t, net, eve, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: chanAmt / 2, + }, + ) + defer closeChannelAndAssert(t, net, eve, fundingPoint, false) + + // Wait for all to view the channel as active. + err := eve.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "eve didn't report channel") + err = carol.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "carol didn't report channel") + err = dave.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err, "dave didn't report channel") + + // Open a private channel, optionally specifying a channel-type. + params := lntest.OpenChannelParams{ + Amt: chanAmt, + Private: private, + CommitmentType: lnrpc.CommitmentType_ANCHORS, + ZeroConf: zeroConf, + ScidAlias: scidAliasType, + PushAmt: chanAmt / 2, + } + chanOpenUpdate := openChannelStream(t, net, carol, dave, params) + + if !zeroConf { + // If this is not a zero-conf channel, mine a single block to + // confirm the channel. + _ = mineBlocks(t, net, 1, 1) + } + + // Wait for both Carol and Dave to see the channel as open. + fundingPoint2, err := net.WaitForChannelOpen(chanOpenUpdate) + require.NoError(t.t, err, "error while waiting for channel open") + + err = carol.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err, "carol didn't report channel") + err = dave.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err, "dave didn't report channel") + + // Carol will now update the channel edge policy for her channel with + // Dave. + baseFeeMSat := 33000 + feeRate := int64(5) + timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta) + updateFeeReq := &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: int64(baseFeeMSat), + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: fundingPoint2, + }, + } + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + _, err = carol.UpdateChannelPolicy(ctxt, updateFeeReq) + require.NoError(t.t, err, "unable to update chan policy") + + expectedPolicy := &lnrpc.RoutingPolicy{ + FeeBaseMsat: int64(baseFeeMSat), + FeeRateMilliMsat: testFeeBase * feeRate, + TimeLockDelta: timeLockDelta, + MinHtlc: 1000, // default value + MaxHtlcMsat: calculateMaxHtlc(chanAmt), + } + + // Assert that Dave receives Carol's policy update. + assertChannelPolicyUpdate( + t.t, dave, carol.PubKeyStr, expectedPolicy, fundingPoint2, + true, + ) + + // Have Dave also update his policy. + baseFeeMSat = 15000 + feeRate = int64(4) + updateFeeReq = &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: int64(baseFeeMSat), + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: fundingPoint2, + }, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = dave.UpdateChannelPolicy(ctxt, updateFeeReq) + require.NoError(t.t, err, "unable to update chan policy") + + expectedPolicy = &lnrpc.RoutingPolicy{ + FeeBaseMsat: int64(baseFeeMSat), + FeeRateMilliMsat: testFeeBase * feeRate, + TimeLockDelta: timeLockDelta, + MinHtlc: 1000, + MaxHtlcMsat: calculateMaxHtlc(chanAmt), + } + + // Assert that Carol receives Dave's policy update. + assertChannelPolicyUpdate( + t.t, carol, dave.PubKeyStr, expectedPolicy, fundingPoint2, + true, + ) + + // Assert that if Dave disables the channel, Carol sees it. + disableReq := &routerrpc.UpdateChanStatusRequest{ + ChanPoint: fundingPoint2, + Action: routerrpc.ChanStatusAction_DISABLE, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = dave.RouterClient.UpdateChanStatus(ctxt, disableReq) + require.NoError(t.t, err) + + davePolicy := getChannelPolicies( + t, carol, dave.PubKeyStr, fundingPoint2, + )[0] + davePolicy.Disabled = true + assertChannelPolicyUpdate( + t.t, carol, dave.PubKeyStr, davePolicy, fundingPoint2, true, + ) + + // Assert that if Dave enables the channel, Carol sees it. + enableReq := &routerrpc.UpdateChanStatusRequest{ + ChanPoint: fundingPoint2, + Action: routerrpc.ChanStatusAction_ENABLE, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = dave.RouterClient.UpdateChanStatus(ctxt, enableReq) + require.NoError(t.t, err) + + davePolicy.Disabled = false + assertChannelPolicyUpdate( + t.t, carol, dave.PubKeyStr, davePolicy, fundingPoint2, true, + ) + + // Create an invoice for Carol to pay. + invoiceParams := &lnrpc.Invoice{ + Value: int64(10_000), + Private: true, + } + daveInvoiceResp, err := dave.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + // Carol will attempt to send Dave an HTLC. + payReqs := []string{daveInvoiceResp.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + carol, carol.RouterClient, payReqs, true, + ), "unable to send payment", + ) + + // Now Eve will create an invoice that Dave will pay. + eveInvoiceResp, err := eve.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + payReqs = []string{eveInvoiceResp.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + dave, dave.RouterClient, payReqs, true, + ), "unable to send payment", + ) + + // If this is a public channel, it won't be included in the hop hints, + // so we'll mine enough for 6 confs here. We only expect a tx in the + // mempool for the zero-conf case. + if !private { + var expectTx int + if zeroConf { + expectTx = 1 + } + _ = mineBlocks(t, net, 6, expectTx) + + // Sleep here so that the edge can be deleted and re-inserted. + // This is necessary since the edge may have a policy for the + // peer that is "correct" but has an invalid signature from the + // PoV of BOLT#7. + time.Sleep(time.Second * 5) + } + + // Dave creates an invoice that Eve will pay. + daveInvoiceResp2, err := dave.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + // Carol then updates the channel policy again. + feeRate = int64(2) + updateFeeReq = &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: int64(baseFeeMSat), + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: fundingPoint2, + }, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = carol.UpdateChannelPolicy(ctxt, updateFeeReq) + require.NoError(t.t, err, "unable to update chan policy") + + expectedPolicy = &lnrpc.RoutingPolicy{ + FeeBaseMsat: int64(baseFeeMSat), + FeeRateMilliMsat: testFeeBase * feeRate, + TimeLockDelta: timeLockDelta, + MinHtlc: 1000, + MaxHtlcMsat: calculateMaxHtlc(chanAmt), + } + + // Assert Dave receives Carol's policy update. + assertChannelPolicyUpdate( + t.t, dave, carol.PubKeyStr, expectedPolicy, fundingPoint2, + true, + ) + + // If the channel is public, check that Eve receives Carol's policy + // update. + if !private { + assertChannelPolicyUpdate( + t.t, eve, carol.PubKeyStr, expectedPolicy, + fundingPoint2, true, + ) + } + + // Eve will pay Dave's invoice and should use the updated base fee. + payReqs = []string{daveInvoiceResp2.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + eve, eve.RouterClient, payReqs, true, + ), "unable to send payment", + ) + + // Eve will issue an invoice that Dave will pay. + eveInvoiceResp2, err := eve.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + payReqs = []string{eveInvoiceResp2.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + dave, dave.RouterClient, payReqs, true, + ), "unable to send payment", + ) + + // If this is a private channel, we'll mine 6 blocks here to test the + // funding manager logic that deals with ChannelUpdates. If this is not + // a zero-conf channel, we don't expect a tx in the mempool. + if private { + var expectTx int + if zeroConf { + expectTx = 1 + } + _ = mineBlocks(t, net, 6, expectTx) + } + + // Dave will issue an invoice and Eve will pay it. + daveInvoiceResp3, err := dave.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + payReqs = []string{daveInvoiceResp3.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + eve, eve.RouterClient, payReqs, true, + ), "unable to send payment", + ) + + // Carol will disable the channel, assert that Dave sees it and Eve as + // well if the channel is public. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = carol.RouterClient.UpdateChanStatus(ctxt, disableReq) + require.NoError(t.t, err) + + carolPolicy := getChannelPolicies( + t, dave, carol.PubKeyStr, fundingPoint2, + )[0] + carolPolicy.Disabled = true + assertChannelPolicyUpdate( + t.t, dave, carol.PubKeyStr, carolPolicy, fundingPoint2, true, + ) + + if !private { + assertChannelPolicyUpdate( + t.t, eve, carol.PubKeyStr, carolPolicy, fundingPoint2, + true, + ) + } + + // Carol will enable the channel, assert the same as above. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = carol.RouterClient.UpdateChanStatus(ctxt, enableReq) + require.NoError(t.t, err) + + carolPolicy.Disabled = false + assertChannelPolicyUpdate( + t.t, dave, carol.PubKeyStr, carolPolicy, fundingPoint2, true, + ) + + if !private { + assertChannelPolicyUpdate( + t.t, eve, carol.PubKeyStr, carolPolicy, fundingPoint2, + true, + ) + } + + // Dave will issue an invoice and Eve should pay it after Carol updates + // her channel policy. + daveInvoiceResp4, err := dave.AddInvoice(ctxb, invoiceParams) + require.NoError(t.t, err, "unable to add invoice") + + feeRate = int64(3) + updateFeeReq = &lnrpc.PolicyUpdateRequest{ + BaseFeeMsat: int64(baseFeeMSat), + FeeRate: float64(feeRate), + TimeLockDelta: timeLockDelta, + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: fundingPoint2, + }, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = carol.UpdateChannelPolicy(ctxt, updateFeeReq) + require.NoError(t.t, err, "unable to update chan policy") + + expectedPolicy = &lnrpc.RoutingPolicy{ + FeeBaseMsat: int64(baseFeeMSat), + FeeRateMilliMsat: testFeeBase * feeRate, + TimeLockDelta: timeLockDelta, + MinHtlc: 1000, + MaxHtlcMsat: calculateMaxHtlc(chanAmt), + } + + // Assert Dave and optionally Eve receives Carol's update. + assertChannelPolicyUpdate( + t.t, dave, carol.PubKeyStr, expectedPolicy, fundingPoint2, + true, + ) + + if !private { + assertChannelPolicyUpdate( + t.t, eve, carol.PubKeyStr, expectedPolicy, + fundingPoint2, true, + ) + } + + payReqs = []string{daveInvoiceResp4.PaymentRequest} + require.NoError( + t.t, completePaymentRequests( + eve, eve.RouterClient, payReqs, true, + ), "unable to send payment", + ) +} + +// testOptionScidUpgrade tests that toggling the option-scid-alias feature bit +// correctly upgrades existing channels. +func testOptionScidUpgrade(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + // Start carol with anchors only. + carolArgs := []string{ + "--protocol.anchors", + } + carol := net.NewNode(t.t, "carol", carolArgs) + + // Start dave with anchors + scid-alias. + daveArgs := []string{ + "--protocol.anchors", + "--protocol.option-scid-alias", + } + dave := net.NewNode(t.t, "dave", daveArgs) + defer shutdownAndAssert(net, t, dave) + + // Give carol some coins. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + + // Ensure carol and are connected. + net.EnsureConnected(t.t, carol, dave) + + chanAmt := btcutil.Amount(1_000_000) + + fundingPoint := openChannelAndAssert( + t, net, carol, dave, + lntest.OpenChannelParams{ + Amt: chanAmt, + PushAmt: chanAmt / 2, + Private: true, + }, + ) + defer closeChannelAndAssert(t, net, carol, fundingPoint, false) + + err := carol.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err) + err = dave.WaitForNetworkChannelOpen(fundingPoint) + require.NoError(t.t, err) + + // Bob will open a channel to Carol now. + net.EnsureConnected(t.t, net.Bob, carol) + + fundingPoint2 := openChannelAndAssert( + t, net, net.Bob, carol, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + err = net.Bob.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + err = carol.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + err = dave.WaitForNetworkChannelOpen(fundingPoint2) + require.NoError(t.t, err) + + // Carol will now set the option-scid-alias feature bit and restart. + carolArgs = append(carolArgs, "--protocol.option-scid-alias") + carol.SetExtraArgs(carolArgs) + err = net.RestartNode(carol, nil) + require.NoError(t.t, err) + + // Dave will create an invoice for Carol to pay, it should contain an + // alias in the hop hints. + daveParams := &lnrpc.Invoice{ + Value: int64(10_000), + Private: true, + } + + var daveInvoice *lnrpc.AddInvoiceResponse + + var startingAlias lnwire.ShortChannelID + startingAlias.BlockHeight = 16_000_000 + + err = wait.Predicate(func() bool { + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + invoiceResp, err := dave.AddInvoice(ctxt, daveParams) + if err != nil { + return false + } + + payReq := &lnrpc.PayReqString{ + PayReq: invoiceResp.PaymentRequest, + } + + decodedReq, err := dave.DecodePayReq(ctxb, payReq) + if err != nil { + return false + } + + if len(decodedReq.RouteHints) != 1 { + return false + } + + if len(decodedReq.RouteHints[0].HopHints) != 1 { + return false + } + + hopHint := decodedReq.RouteHints[0].HopHints[0].ChanId + if startingAlias.ToUint64() == hopHint { + daveInvoice = invoiceResp + return true + } + + return false + }, defaultTimeout) + require.NoError(t.t, err) + + // Carol should be able to pay it. + _ = sendAndAssertSuccess( + t, carol, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) + + daveInvoice2, err := dave.AddInvoice(ctxb, daveParams) + require.NoError(t.t, err) + + _ = sendAndAssertSuccess( + t, net.Bob, &routerrpc.SendPaymentRequest{ + PaymentRequest: daveInvoice2.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + }, + ) +}