package itest import ( "fmt" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/aliasmgr" "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/node" "github.com/lightningnetwork/lnd/lntest/rpc" "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(ht *lntest.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", } bob := ht.Bob carol := ht.NewNode("Carol", scidAliasArgs) ht.EnsureConnected(bob, carol) // We'll open a regular public channel between Bob and Carol here. chanAmt := btcutil.Amount(1_000_000) p := lntest.OpenChannelParams{ Amt: chanAmt, } chanPoint := ht.OpenChannel(bob, carol, p) // Spin-up Dave so Carol can open a zero-conf channel to him. dave := ht.NewNode("Dave", scidAliasArgs) // We'll give Carol some coins in order to fund the channel. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Ensure that both Carol and Dave are connected. ht.EnsureConnected(carol, dave) // Setup a ChannelAcceptor for Dave. acceptStream, cancel := dave.RPC.ChannelAcceptor() go acceptChannel(ht.T, true, acceptStream) // Open a private zero-conf anchors channel of 1M satoshis. params := lntest.OpenChannelParams{ Amt: chanAmt, Private: true, CommitmentType: lnrpc.CommitmentType_ANCHORS, ZeroConf: true, } stream := ht.OpenChannelAssertStream(carol, dave, params) // Remove the ChannelAcceptor. cancel() // We should receive the OpenStatusUpdate_ChanOpen update without // having to mine any blocks. fundingPoint2 := ht.WaitForChannelOpenEvent(stream) ht.AssertTopologyChannelOpen(carol, fundingPoint2) ht.AssertTopologyChannelOpen(dave, fundingPoint2) // Attempt to send a 10K satoshi payment from Carol to Dave. daveInvoiceParams := &lnrpc.Invoice{ Value: int64(10_000), Private: true, } daveInvoiceResp := dave.RPC.AddInvoice(daveInvoiceParams) ht.CompletePaymentRequests( carol, []string{daveInvoiceResp.PaymentRequest}, ) // 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 := dave.RPC.AddInvoice(daveInvoiceParams) ht.CompletePaymentRequests( bob, []string{daveInvoiceResp2.PaymentRequest}, ) // Check that Dave has a zero-conf alias SCID in the graph. descReq := &lnrpc.ChannelGraphRequest{ IncludeUnannounced: true, } err := waitForZeroConfGraphChange(dave, descReq, true) require.NoError(ht, err) // We'll now confirm the zero-conf channel between Carol and Dave and // assert that sending is still possible. block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] // Dave should still have the alias edge in his db. err = waitForZeroConfGraphChange(dave, descReq, true) require.NoError(ht, err) fundingTxID := ht.GetChanPointFundingTxid(fundingPoint2) ht.Miner.AssertTxInBlock(block, fundingTxID) daveInvoiceResp3 := dave.RPC.AddInvoice(daveInvoiceParams) ht.CompletePaymentRequests( bob, []string{daveInvoiceResp3.PaymentRequest}, ) // 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 := ht.NewNode("Eve", scidAliasArgs) ht.EnsureConnected(eve, carol) // Give Eve some coins to fund the channel. ht.FundCoins(btcutil.SatoshiPerBitcoin, eve) // Setup a ChannelAcceptor. acceptStream, cancel = carol.RPC.ChannelAcceptor() go acceptChannel(ht.T, true, acceptStream) // We'll open a public zero-conf anchors channel of 1M satoshis. params.Private = false stream = ht.OpenChannelAssertStream(eve, carol, params) // Remove the ChannelAcceptor. cancel() // Wait to receive the OpenStatusUpdate_ChanOpen update. fundingPoint3 := ht.WaitForChannelOpenEvent(stream) ht.AssertTopologyChannelOpen(eve, fundingPoint3) ht.AssertTopologyChannelOpen(carol, fundingPoint3) // Attempt to send a 20K satoshi payment from Eve to Dave. daveInvoiceParams.Value = int64(20_000) daveInvoiceResp4 := dave.RPC.AddInvoice(daveInvoiceParams) ht.CompletePaymentRequests( eve, []string{daveInvoiceResp4.PaymentRequest}, ) // Assert that Eve has stored the zero-conf alias in her graph. err = waitForZeroConfGraphChange(eve, descReq, true) require.NoError(ht, err, "expected to not receive error") // We'll confirm the zero-conf channel between Eve and Carol and assert // that sending is still possible. block = ht.MineBlocksAndAssertNumTxes(6, 1)[0] fundingTxID = ht.GetChanPointFundingTxid(fundingPoint3) ht.Miner.AssertTxInBlock(block, fundingTxID) // Wait until Eve's ZeroConf channel is replaced by the confirmed SCID // in her graph. err = waitForZeroConfGraphChange(eve, descReq, false) require.NoError(ht, 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 := eve.RPC.AddInvoice(eveInvoiceParams) // Assert that route hints is empty since the channel is public. payReq := eve.RPC.DecodePayReq(eveInvoiceResp.PaymentRequest) require.Len(ht, payReq.RouteHints, 0) // Make sure Dave is aware of this channel and send the payment. ht.AssertTopologyChannelOpen(dave, fundingPoint3) ht.CompletePaymentRequests( dave, []string{eveInvoiceResp.PaymentRequest}, ) // Close standby node's channels. ht.CloseChannel(bob, chanPoint) } // testOptionScidAlias checks that opening an option_scid_alias channel-type // channel or w/o the channel-type works properly. func testOptionScidAlias(ht *lntest.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 := ht.Run(testCase.name, func(t *testing.T) { st := ht.Subtest(t) optionScidAliasScenario( st, testCase.chantype, testCase.private, ) }) if !success { break } } } func optionScidAliasScenario(ht *lntest.HarnessTest, chantype, private bool) { // Option-scid-alias is opt-in, as is anchors. scidAliasArgs := []string{ "--protocol.option-scid-alias", "--protocol.anchors", } bob := ht.Bob carol := ht.NewNode("Carol", scidAliasArgs) dave := ht.NewNode("Dave", scidAliasArgs) // Ensure Bob, Carol are connected. ht.EnsureConnected(bob, carol) // Ensure Carol, Dave are connected. ht.EnsureConnected(carol, dave) // Give Carol some coins so she can open the channel. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) chanAmt := btcutil.Amount(1_000_000) params := lntest.OpenChannelParams{ Amt: chanAmt, Private: private, CommitmentType: lnrpc.CommitmentType_ANCHORS, ScidAlias: chantype, } fundingPoint := ht.OpenChannel(carol, dave, params) // Make sure Bob knows this channel if it's public. if !private { ht.AssertTopologyChannelOpen(bob, fundingPoint) } // Assert that a payment from Carol to Dave works as expected. daveInvoiceParams := &lnrpc.Invoice{ Value: int64(10_000), Private: true, } daveInvoiceResp := dave.RPC.AddInvoice(daveInvoiceParams) ht.CompletePaymentRequests( carol, []string{daveInvoiceResp.PaymentRequest}, ) // 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. p := lntest.OpenChannelParams{ Amt: chanAmt, } fundingPoint2 := ht.OpenChannel(bob, carol, p) defer func() { // TODO(yy): remove the sleep once the following bug is fixed. // When the payment is reported as settled by Bob, it's // expected the commitment dance is finished and all subsequent // states have been updated. Yet we'd receive the error `cannot // co-op close channel with active htlcs` or `link failed to // shutdown` if we close the channel. We need to investigate // the order of settling the payments and updating commitments // to understand and fix. time.Sleep(2 * time.Second) // Close standby node's channels. ht.CloseChannel(bob, fundingPoint2) }() // Wait until Dave receives the Bob<->Carol channel. ht.AssertTopologyChannelOpen(dave, fundingPoint2) daveInvoiceResp2 := dave.RPC.AddInvoice(daveInvoiceParams) decodedReq := dave.RPC.DecodePayReq(daveInvoiceResp2.PaymentRequest) if !private { require.Len(ht, decodedReq.RouteHints, 0) payReq := daveInvoiceResp2.PaymentRequest ht.CompletePaymentRequests(bob, []string{payReq}) return } require.Len(ht, decodedReq.RouteHints, 1) require.Len(ht, decodedReq.RouteHints[0].HopHints, 1) startingAlias := lnwire.ShortChannelID{ BlockHeight: 16_000_000, TxIndex: 0, TxPosition: 0, } daveHopHint := decodedReq.RouteHints[0].HopHints[0].ChanId require.Equal(ht, startingAlias.ToUint64(), daveHopHint) ht.CompletePaymentRequests( bob, []string{daveInvoiceResp2.PaymentRequest}, ) } // 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(hn *node.HarnessNode, req *lnrpc.ChannelGraphRequest, expect bool) error { return wait.NoError(func() error { graph := hn.RPC.DescribeGraph(req) 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 == hn.PubKeyStr || e.Node2Pub == hn.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 == hn.PubKeyStr || e.Node2Pub == hn.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(ht *lntest.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 := ht.Run(test.name, func(t *testing.T) { st := ht.Subtest(t) testPrivateUpdateAlias( st, test.zeroConf, test.scidAliasType, test.private, ) }) if !success { return } } } func testPrivateUpdateAlias(ht *lntest.HarnessTest, zeroConf, scidAliasType, private bool) { // We'll create a new node Eve that will not have option-scid-alias // channels. eve := ht.NewNode("Eve", nil) ht.FundCoins(btcutil.SatoshiPerBitcoin, 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 := ht.NewNode("Carol", scidAliasArgs) // Spin-up Dave who will have an option-scid-alias feature-bit-only or // channel-type channel with Carol. dave := ht.NewNode("Dave", scidAliasArgs) // We'll give Carol some coins in order to fund the channel. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Ensure that Carol and Dave are connected. ht.EnsureConnected(carol, dave) // We'll open a regular public channel between Eve and Carol here. Eve // will be the one receiving the onion-encrypted ChannelUpdate. ht.EnsureConnected(eve, carol) chanAmt := btcutil.Amount(1_000_000) p := lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, } fundingPoint := ht.OpenChannel(eve, carol, p) // Make sure Dave has seen this public channel. ht.AssertTopologyChannelOpen(dave, fundingPoint) // Setup a ChannelAcceptor for Dave. acceptStream, cancel := dave.RPC.ChannelAcceptor() go acceptChannel(ht.T, zeroConf, acceptStream) // 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, } fundingPoint2 := ht.OpenChannelNoAnnounce(carol, dave, params) // Remove the ChannelAcceptor. cancel() // 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, }, } carol.RPC.UpdateChannelPolicy(updateFeeReq) expectedPolicy := &lnrpc.RoutingPolicy{ FeeBaseMsat: int64(baseFeeMSat), FeeRateMilliMsat: testFeeBase * feeRate, TimeLockDelta: timeLockDelta, MinHtlc: 1000, // default value MaxHtlcMsat: lntest.CalculateMaxHtlc(chanAmt), } // Assert that Dave receives Carol's policy update. ht.AssertChannelPolicyUpdate( dave, carol, 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, }, } dave.RPC.UpdateChannelPolicy(updateFeeReq) expectedPolicy = &lnrpc.RoutingPolicy{ FeeBaseMsat: int64(baseFeeMSat), FeeRateMilliMsat: testFeeBase * feeRate, TimeLockDelta: timeLockDelta, MinHtlc: 1000, MaxHtlcMsat: lntest.CalculateMaxHtlc(chanAmt), } // Assert that Carol receives Dave's policy update. ht.AssertChannelPolicyUpdate( carol, dave, expectedPolicy, fundingPoint2, true, ) // Assert that if Dave disables the channel, Carol sees it. disableReq := &routerrpc.UpdateChanStatusRequest{ ChanPoint: fundingPoint2, Action: routerrpc.ChanStatusAction_DISABLE, } dave.RPC.UpdateChanStatus(disableReq) expectedPolicy.Disabled = true ht.AssertChannelPolicyUpdate( carol, dave, expectedPolicy, fundingPoint2, true, ) // Assert that if Dave enables the channel, Carol sees it. enableReq := &routerrpc.UpdateChanStatusRequest{ ChanPoint: fundingPoint2, Action: routerrpc.ChanStatusAction_ENABLE, } dave.RPC.UpdateChanStatus(enableReq) expectedPolicy.Disabled = false ht.AssertChannelPolicyUpdate( carol, dave, expectedPolicy, fundingPoint2, true, ) // Create an invoice for Carol to pay. invoiceParams := &lnrpc.Invoice{ Value: int64(10_000), Private: true, } daveInvoiceResp := dave.RPC.AddInvoice(invoiceParams) // Carol will attempt to send Dave an HTLC. payReqs := []string{daveInvoiceResp.PaymentRequest} ht.CompletePaymentRequests(carol, payReqs) // Now Eve will create an invoice that Dave will pay. eveInvoiceResp := eve.RPC.AddInvoice(invoiceParams) payReqs = []string{eveInvoiceResp.PaymentRequest} ht.CompletePaymentRequests(dave, payReqs) // 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 } ht.MineBlocksAndAssertNumTxes(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. // // TODO(yy): further investigate this sleep. time.Sleep(time.Second * 5) } // Dave creates an invoice that Eve will pay. daveInvoiceResp2 := dave.RPC.AddInvoice(invoiceParams) // 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, }, } carol.RPC.UpdateChannelPolicy(updateFeeReq) expectedPolicy = &lnrpc.RoutingPolicy{ FeeBaseMsat: int64(baseFeeMSat), FeeRateMilliMsat: testFeeBase * feeRate, TimeLockDelta: timeLockDelta, MinHtlc: 1000, MaxHtlcMsat: lntest.CalculateMaxHtlc(chanAmt), } // Assert Dave receives Carol's policy update. ht.AssertChannelPolicyUpdate( dave, carol, expectedPolicy, fundingPoint2, true, ) // If the channel is public, check that Eve receives Carol's policy // update. if !private { ht.AssertChannelPolicyUpdate( eve, carol, expectedPolicy, fundingPoint2, true, ) } // Eve will pay Dave's invoice and should use the updated base fee. payReqs = []string{daveInvoiceResp2.PaymentRequest} ht.CompletePaymentRequests(eve, payReqs) // Eve will issue an invoice that Dave will pay. eveInvoiceResp2 := eve.RPC.AddInvoice(invoiceParams) payReqs = []string{eveInvoiceResp2.PaymentRequest} ht.CompletePaymentRequests(dave, payReqs) // 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 } ht.MineBlocksAndAssertNumTxes(6, expectTx) } // Dave will issue an invoice and Eve will pay it. daveInvoiceResp3 := dave.RPC.AddInvoice(invoiceParams) payReqs = []string{daveInvoiceResp3.PaymentRequest} ht.CompletePaymentRequests(eve, payReqs) // Carol will disable the channel, assert that Dave sees it and Eve as // well if the channel is public. carol.RPC.UpdateChanStatus(disableReq) expectedPolicy.Disabled = true ht.AssertChannelPolicyUpdate( dave, carol, expectedPolicy, fundingPoint2, true, ) if !private { ht.AssertChannelPolicyUpdate( eve, carol, expectedPolicy, fundingPoint2, true, ) } // Carol will enable the channel, assert the same as above. carol.RPC.UpdateChanStatus(enableReq) expectedPolicy.Disabled = false ht.AssertChannelPolicyUpdate( dave, carol, expectedPolicy, fundingPoint2, true, ) if !private { ht.AssertChannelPolicyUpdate( eve, carol, expectedPolicy, fundingPoint2, true, ) } // Dave will issue an invoice and Eve should pay it after Carol updates // her channel policy. daveInvoiceResp4 := dave.RPC.AddInvoice(invoiceParams) feeRate = int64(3) updateFeeReq = &lnrpc.PolicyUpdateRequest{ BaseFeeMsat: int64(baseFeeMSat), FeeRate: float64(feeRate), TimeLockDelta: timeLockDelta, Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ ChanPoint: fundingPoint2, }, } carol.RPC.UpdateChannelPolicy(updateFeeReq) expectedPolicy = &lnrpc.RoutingPolicy{ FeeBaseMsat: int64(baseFeeMSat), FeeRateMilliMsat: testFeeBase * feeRate, TimeLockDelta: timeLockDelta, MinHtlc: 1000, MaxHtlcMsat: lntest.CalculateMaxHtlc(chanAmt), } // Assert Dave and optionally Eve receives Carol's update. ht.AssertChannelPolicyUpdate( dave, carol, expectedPolicy, fundingPoint2, true, ) if !private { ht.AssertChannelPolicyUpdate( eve, carol, expectedPolicy, fundingPoint2, true, ) } payReqs = []string{daveInvoiceResp4.PaymentRequest} ht.CompletePaymentRequests(eve, payReqs) } // testOptionScidUpgrade tests that toggling the option-scid-alias feature bit // correctly upgrades existing channels. func testOptionScidUpgrade(ht *lntest.HarnessTest) { bob := ht.Bob // Start carol with anchors only. carolArgs := []string{ "--protocol.anchors", } carol := ht.NewNode("carol", carolArgs) // Start dave with anchors + scid-alias. daveArgs := []string{ "--protocol.anchors", "--protocol.option-scid-alias", } dave := ht.NewNode("dave", daveArgs) // Give carol some coins. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Ensure carol and are connected. ht.EnsureConnected(carol, dave) chanAmt := btcutil.Amount(1_000_000) p := lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, Private: true, } ht.OpenChannel(carol, dave, p) // Bob will open a channel to Carol now. ht.EnsureConnected(bob, carol) p = lntest.OpenChannelParams{ Amt: chanAmt, } fundingPoint2 := ht.OpenChannel(bob, carol, p) // Make sure Dave knows this channel. ht.AssertTopologyChannelOpen(dave, fundingPoint2) // Carol will now set the option-scid-alias feature bit and restart. carolArgs = append(carolArgs, "--protocol.option-scid-alias") ht.RestartNodeWithExtraArgs(carol, carolArgs) // 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 // TODO(yy): Carol and Dave will attempt to connect to each other // during restart. However, due to the race condition in peer // connection, they may both fail. Thus we need to ensure the // connection here. Once the race is fixed, we can remove this line. ht.EnsureConnected(dave, carol) err := wait.NoError(func() error { invoiceResp := dave.RPC.AddInvoice(daveParams) decodedReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest) if len(decodedReq.RouteHints) != 1 { return fmt.Errorf("expected 1 route hint, got %v", decodedReq.RouteHints) } if len(decodedReq.RouteHints[0].HopHints) != 1 { return fmt.Errorf("expected 1 hop hint, got %v", len(decodedReq.RouteHints[0].HopHints)) } hopHint := decodedReq.RouteHints[0].HopHints[0].ChanId if startingAlias.ToUint64() == hopHint { daveInvoice = invoiceResp return nil } return fmt.Errorf("unmatched alias, expected %v, got %v", startingAlias.ToUint64(), hopHint) }, defaultTimeout) require.NoError(ht, err) // Carol should be able to pay it. ht.CompletePaymentRequests(carol, []string{daveInvoice.PaymentRequest}) // TODO(yy): remove this connection once the following bug is fixed. // When Carol restarts, she will try to make a persistent connection to // Bob. Meanwhile, Bob will also make a conn request as he notices the // connection is broken. If they make these conn requests at the same // time, they both have an outbound conn request, and will close the // inbound conn they receives, which ends up in no conn. ht.EnsureConnected(bob, carol) daveInvoice2 := dave.RPC.AddInvoice(daveParams) ht.CompletePaymentRequests(bob, []string{daveInvoice2.PaymentRequest}) // TODO(yy): remove the sleep once the following bug is fixed. When // the payment is reported as settled by Bob, it's expected the // commitment dance is finished and all subsequent states have been // updated. Yet we'd receive the error `cannot co-op close channel with // active htlcs` or `link failed to shutdown` if we close the channel. // We need to investigate the order of settling the payments and // updating commitments to understand and fix. time.Sleep(2 * time.Second) // Close standby node's channels. ht.CloseChannel(bob, fundingPoint2) } // acceptChannel is used to accept a single channel that comes across. This // should be run in a goroutine and is used to test nodes with the zero-conf // feature bit. func acceptChannel(t *testing.T, zeroConf bool, stream rpc.AcceptorClient) { t.Helper() req, err := stream.Recv() require.NoError(t, err) resp := &lnrpc.ChannelAcceptResponse{ Accept: true, PendingChanId: req.PendingChanId, ZeroConf: zeroConf, } err = stream.Send(resp) require.NoError(t, err) } // testZeroConfReorg tests that a reorg does not cause a zero-conf channel to // be deleted from the channel graph. This was previously the case due to logic // in the function DisconnectBlockAtHeight. func testZeroConfReorg(ht *lntest.HarnessTest) { if ht.IsNeutrinoBackend() { ht.Skipf("skipping zero-conf reorg test for neutrino backend") } // Since zero-conf is opt in, the harness nodes provided won't be able // to open zero-conf channels. In that case, we just spin up new nodes. zeroConfArgs := []string{ "--protocol.option-scid-alias", "--protocol.zero-conf", "--protocol.anchors", } carol := ht.NewNode("Carol", zeroConfArgs) // Spin-up Dave so Carol can open a zero-conf channel to him. dave := ht.NewNode("Dave", zeroConfArgs) // We'll give Carol some coins in order to fund the channel. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Ensure that both Carol and Dave are connected. ht.EnsureConnected(carol, dave) // Setup a ChannelAcceptor for Dave. acceptStream, cancel := dave.RPC.ChannelAcceptor() go acceptChannel(ht.T, true, acceptStream) // Open a private zero-conf anchors channel of 1M satoshis. params := lntest.OpenChannelParams{ Amt: btcutil.Amount(1_000_000), CommitmentType: lnrpc.CommitmentType_ANCHORS, ZeroConf: true, } _ = ht.OpenChannelNoAnnounce(carol, dave, params) // Remove the ChannelAcceptor. cancel() // Attempt to send a 10K satoshi payment from Carol to Dave. This // requires that the edge exists in the graph. daveInvoiceParams := &lnrpc.Invoice{ Value: int64(10_000), } daveInvoiceResp := dave.RPC.AddInvoice(daveInvoiceParams) payReqs := []string{daveInvoiceResp.PaymentRequest} ht.CompletePaymentRequests(carol, payReqs) // We will now attempt to query for the alias SCID in Carol's graph. // We will query for the starting alias, which is exported by the // aliasmgr package. carol.RPC.GetChanInfo(&lnrpc.ChanInfoRequest{ ChanId: aliasmgr.StartingAlias.ToUint64(), }) // Now we will trigger a reorg and we'll assert that the edge still // exists in the graph. // // First, we'll setup a new miner that we can use to cause a reorg. tempMiner := ht.Miner.SpawnTempMiner() // We now cause a fork, by letting our original miner mine 1 block and // our new miner will mine 2. We also expect the funding transition to // be mined. ht.MineBlocksAndAssertNumTxes(1, 1) tempMiner.MineEmptyBlocks(2) // Ensure the temp miner is one block ahead. ht.Miner.AssertMinerBlockHeightDelta(tempMiner, 1) // Wait for Carol to sync to the original miner's chain. _, minerHeight := ht.Miner.GetBestBlock() ht.WaitForNodeBlockHeight(carol, minerHeight) // Now we'll disconnect Carol's chain backend from the original miner // so that we can connect the two miners together and let the original // miner sync to the temp miner's chain. ht.DisconnectMiner() // Connecting to the temporary miner should cause the original miner to // reorg to the longer chain. ht.Miner.ConnectMiner(tempMiner) // They should now be on the same chain. ht.Miner.AssertMinerBlockHeightDelta(tempMiner, 0) // Now we disconnect the two miners and reconnect our original chain // backend. ht.Miner.DisconnectMiner(tempMiner) ht.ConnectMiner() // This should have caused a reorg and Carol should sync to the new // chain. _, tempMinerHeight := tempMiner.GetBestBlock() ht.WaitForNodeBlockHeight(carol, tempMinerHeight) // Make sure all active nodes are synced. ht.AssertActiveNodesSynced() // Carol should have the channel once synced. carol.RPC.GetChanInfo(&lnrpc.ChanInfoRequest{ ChanId: aliasmgr.StartingAlias.ToUint64(), }) // Mine the zero-conf funding transaction so the test doesn't fail. ht.MineBlocksAndAssertNumTxes(1, 1) }