lnd/itest/lnd_zero_conf_test.go
2024-07-23 21:30:08 +08:00

1002 lines
29 KiB
Go

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.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.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})
// 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.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.AssertMinerBlockHeightDelta(tempMiner, 1)
// Wait for Carol to sync to the original miner's chain.
minerHeight := int32(ht.CurrentHeight())
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.ConnectToMiner(tempMiner)
// They should now be on the same chain.
ht.AssertMinerBlockHeightDelta(tempMiner, 0)
// Now we disconnect the two miners and reconnect our original chain
// backend.
ht.DisconnectFromMiner(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)
}