mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
1002 lines
29 KiB
Go
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.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})
|
|
|
|
// 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)
|
|
}
|