multi: move itest out of lntest
This commit moves all the test cases living in `itest` out of `lntest`, further making `lntest` an independent package for general testing.
This commit is contained in:
59 files changed
+19
-19
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
//go:build !rpctest
|
||||
// +build !rpctest
|
||||
|
||||
package itest
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lntemp"
|
||||
|
||||
var allTestCases = []*lntemp.TestCase{}
|
||||
@@ -1,515 +0,0 @@
|
||||
//go:build rpctest
|
||||
// +build rpctest
|
||||
|
||||
package itest
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lntemp"
|
||||
|
||||
var allTestCases = []*lntemp.TestCase{
|
||||
{
|
||||
Name: "update channel status",
|
||||
TestFunc: testUpdateChanStatus,
|
||||
},
|
||||
{
|
||||
Name: "basic funding flow",
|
||||
TestFunc: testBasicChannelFunding,
|
||||
},
|
||||
{
|
||||
Name: "multi hop htlc local timeout",
|
||||
TestFunc: testMultiHopHtlcLocalTimeout,
|
||||
},
|
||||
{
|
||||
Name: "multi hop receiver chain claim",
|
||||
TestFunc: testMultiHopReceiverChainClaim,
|
||||
},
|
||||
{
|
||||
Name: "multi hop local force close on-chain htlc timeout",
|
||||
TestFunc: testMultiHopLocalForceCloseOnChainHtlcTimeout,
|
||||
},
|
||||
{
|
||||
Name: "multi hop remote force close on-chain htlc timeout",
|
||||
TestFunc: testMultiHopRemoteForceCloseOnChainHtlcTimeout,
|
||||
},
|
||||
{
|
||||
Name: "multi hop htlc local chain claim",
|
||||
TestFunc: testMultiHopHtlcLocalChainClaim,
|
||||
},
|
||||
{
|
||||
Name: "multi hop htlc remote chain claim",
|
||||
TestFunc: testMultiHopHtlcRemoteChainClaim,
|
||||
},
|
||||
{
|
||||
Name: "multi hop htlc aggregation",
|
||||
TestFunc: testMultiHopHtlcAggregation,
|
||||
},
|
||||
{
|
||||
Name: "external channel funding",
|
||||
TestFunc: testExternalFundingChanPoint,
|
||||
},
|
||||
{
|
||||
Name: "channel backup restore basic",
|
||||
TestFunc: testChannelBackupRestoreBasic,
|
||||
},
|
||||
{
|
||||
Name: "channel backup restore unconfirmed",
|
||||
TestFunc: testChannelBackupRestoreUnconfirmed,
|
||||
},
|
||||
{
|
||||
Name: "channel backup restore commit types",
|
||||
TestFunc: testChannelBackupRestoreCommitTypes,
|
||||
},
|
||||
{
|
||||
Name: "channel backup restore force close",
|
||||
TestFunc: testChannelBackupRestoreForceClose,
|
||||
},
|
||||
{
|
||||
Name: "channel backup restore legacy",
|
||||
TestFunc: testChannelBackupRestoreLegacy,
|
||||
},
|
||||
{
|
||||
Name: "data loss protection",
|
||||
TestFunc: testDataLossProtection,
|
||||
},
|
||||
{
|
||||
Name: "sweep coins",
|
||||
TestFunc: testSweepAllCoins,
|
||||
},
|
||||
{
|
||||
Name: "disconnecting target peer",
|
||||
TestFunc: testDisconnectingTargetPeer,
|
||||
},
|
||||
{
|
||||
Name: "sphinx replay persistence",
|
||||
TestFunc: testSphinxReplayPersistence,
|
||||
},
|
||||
{
|
||||
Name: "list channels",
|
||||
TestFunc: testListChannels,
|
||||
},
|
||||
{
|
||||
Name: "max pending channel",
|
||||
TestFunc: testMaxPendingChannels,
|
||||
},
|
||||
{
|
||||
Name: "garbage collect link nodes",
|
||||
TestFunc: testGarbageCollectLinkNodes,
|
||||
},
|
||||
{
|
||||
Name: "reject onward htlc",
|
||||
TestFunc: testRejectHTLC,
|
||||
},
|
||||
{
|
||||
Name: "node sign verify",
|
||||
TestFunc: testNodeSignVerify,
|
||||
},
|
||||
{
|
||||
Name: "list addresses",
|
||||
TestFunc: testListAddresses,
|
||||
},
|
||||
{
|
||||
Name: "abandonchannel",
|
||||
TestFunc: testAbandonChannel,
|
||||
},
|
||||
{
|
||||
Name: "recovery info",
|
||||
TestFunc: testGetRecoveryInfo,
|
||||
},
|
||||
{
|
||||
Name: "onchain fund recovery",
|
||||
TestFunc: testOnchainFundRecovery,
|
||||
},
|
||||
{
|
||||
Name: "wallet rescan address detection",
|
||||
TestFunc: testRescanAddressDetection,
|
||||
},
|
||||
{
|
||||
Name: "basic funding flow with all input types",
|
||||
TestFunc: testChannelFundingInputTypes,
|
||||
},
|
||||
{
|
||||
Name: "unconfirmed channel funding",
|
||||
TestFunc: testUnconfirmedChannelFunding,
|
||||
},
|
||||
{
|
||||
Name: "funding flow persistence",
|
||||
TestFunc: testChannelFundingPersistence,
|
||||
},
|
||||
{
|
||||
Name: "batch channel funding",
|
||||
TestFunc: testBatchChanFunding,
|
||||
},
|
||||
{
|
||||
Name: "update channel policy",
|
||||
TestFunc: testUpdateChannelPolicy,
|
||||
},
|
||||
{
|
||||
Name: "send update disable channel",
|
||||
TestFunc: testSendUpdateDisableChannel,
|
||||
},
|
||||
{
|
||||
Name: "private channel update policy",
|
||||
TestFunc: testUpdateChannelPolicyForPrivateChannel,
|
||||
},
|
||||
{
|
||||
Name: "update channel policy fee rate accuracy",
|
||||
TestFunc: testUpdateChannelPolicyFeeRateAccuracy,
|
||||
},
|
||||
{
|
||||
Name: "connection timeout",
|
||||
TestFunc: testNetworkConnectionTimeout,
|
||||
},
|
||||
{
|
||||
Name: "reconnect after ip change",
|
||||
TestFunc: testReconnectAfterIPChange,
|
||||
},
|
||||
{
|
||||
Name: "addpeer config",
|
||||
TestFunc: testAddPeerConfig,
|
||||
},
|
||||
{
|
||||
Name: "unannounced channels",
|
||||
TestFunc: testUnannouncedChannels,
|
||||
},
|
||||
{
|
||||
Name: "graph topology notifications",
|
||||
TestFunc: testGraphTopologyNotifications,
|
||||
},
|
||||
{
|
||||
Name: "node announcement",
|
||||
TestFunc: testNodeAnnouncement,
|
||||
},
|
||||
{
|
||||
Name: "update node announcement rpc",
|
||||
TestFunc: testUpdateNodeAnnouncement,
|
||||
},
|
||||
{
|
||||
Name: "list outgoing payments",
|
||||
TestFunc: testListPayments,
|
||||
},
|
||||
{
|
||||
Name: "immediate payment after channel opened",
|
||||
TestFunc: testPaymentFollowingChannelOpen,
|
||||
},
|
||||
{
|
||||
Name: "invoice update subscription",
|
||||
TestFunc: testInvoiceSubscriptions,
|
||||
},
|
||||
{
|
||||
Name: "streaming channel backup update",
|
||||
TestFunc: testChannelBackupUpdates,
|
||||
},
|
||||
{
|
||||
Name: "export channel backup",
|
||||
TestFunc: testExportChannelBackup,
|
||||
},
|
||||
{
|
||||
Name: "channel balance",
|
||||
TestFunc: testChannelBalance,
|
||||
},
|
||||
{
|
||||
Name: "channel unsettled balance",
|
||||
TestFunc: testChannelUnsettledBalance,
|
||||
},
|
||||
{
|
||||
Name: "commitment deadline",
|
||||
TestFunc: testCommitmentTransactionDeadline,
|
||||
},
|
||||
{
|
||||
Name: "channel force closure",
|
||||
TestFunc: testChannelForceClosure,
|
||||
},
|
||||
{
|
||||
Name: "failing link",
|
||||
TestFunc: testFailingChannel,
|
||||
},
|
||||
{
|
||||
Name: "chain kit",
|
||||
TestFunc: testChainKit,
|
||||
},
|
||||
{
|
||||
Name: "neutrino kit",
|
||||
TestFunc: testNeutrino,
|
||||
},
|
||||
{
|
||||
Name: "etcd failover",
|
||||
TestFunc: testEtcdFailover,
|
||||
},
|
||||
{
|
||||
Name: "hold invoice force close",
|
||||
TestFunc: testHoldInvoiceForceClose,
|
||||
},
|
||||
{
|
||||
Name: "hold invoice sender persistence",
|
||||
TestFunc: testHoldInvoicePersistence,
|
||||
},
|
||||
{
|
||||
Name: "maximum channel size",
|
||||
TestFunc: testMaxChannelSize,
|
||||
},
|
||||
{
|
||||
Name: "wumbo channels",
|
||||
TestFunc: testWumboChannels,
|
||||
},
|
||||
{
|
||||
Name: "max htlc pathfind",
|
||||
TestFunc: testMaxHtlcPathfind,
|
||||
},
|
||||
{
|
||||
Name: "multi-hop htlc error propagation",
|
||||
TestFunc: testHtlcErrorPropagation,
|
||||
},
|
||||
{
|
||||
Name: "multi-hop payments",
|
||||
TestFunc: testMultiHopPayments,
|
||||
},
|
||||
{
|
||||
Name: "anchors reserved value",
|
||||
TestFunc: testAnchorReservedValue,
|
||||
},
|
||||
{
|
||||
Name: "3rd party anchor spend",
|
||||
TestFunc: testAnchorThirdPartySpend,
|
||||
},
|
||||
{
|
||||
Name: "open channel reorg test",
|
||||
TestFunc: testOpenChannelAfterReorg,
|
||||
},
|
||||
{
|
||||
Name: "psbt channel funding",
|
||||
TestFunc: testPsbtChanFunding,
|
||||
},
|
||||
{
|
||||
Name: "psbt channel funding external",
|
||||
TestFunc: testPsbtChanFundingExternal,
|
||||
},
|
||||
{
|
||||
Name: "psbt channel funding single step",
|
||||
TestFunc: testPsbtChanFundingSingleStep,
|
||||
},
|
||||
{
|
||||
Name: "sign psbt",
|
||||
TestFunc: testSignPsbt,
|
||||
},
|
||||
{
|
||||
Name: "resolution handoff",
|
||||
TestFunc: testResHandoff,
|
||||
},
|
||||
{
|
||||
Name: "REST API",
|
||||
TestFunc: testRestAPI,
|
||||
},
|
||||
{
|
||||
Name: "revoked uncooperative close retribution",
|
||||
TestFunc: testRevokedCloseRetribution,
|
||||
},
|
||||
{
|
||||
Name: "revoked uncooperative close retribution zero value " +
|
||||
"remote output",
|
||||
TestFunc: testRevokedCloseRetributionZeroValueRemoteOutput,
|
||||
},
|
||||
{
|
||||
Name: "revoked uncooperative close retribution remote hodl",
|
||||
TestFunc: testRevokedCloseRetributionRemoteHodl,
|
||||
},
|
||||
{
|
||||
Name: "revoked uncooperative close retribution altruist " +
|
||||
"watchtower",
|
||||
TestFunc: testRevokedCloseRetributionAltruistWatchtower,
|
||||
},
|
||||
{
|
||||
Name: "single-hop send to route",
|
||||
TestFunc: testSingleHopSendToRoute,
|
||||
},
|
||||
{
|
||||
Name: "multi-hop send to route",
|
||||
TestFunc: testMultiHopSendToRoute,
|
||||
},
|
||||
{
|
||||
Name: "send to route error propagation",
|
||||
TestFunc: testSendToRouteErrorPropagation,
|
||||
},
|
||||
{
|
||||
Name: "private channels",
|
||||
TestFunc: testPrivateChannels,
|
||||
},
|
||||
{
|
||||
Name: "invoice routing hints",
|
||||
TestFunc: testInvoiceRoutingHints,
|
||||
},
|
||||
{
|
||||
Name: "multi-hop payments over private channels",
|
||||
TestFunc: testMultiHopOverPrivateChannels,
|
||||
},
|
||||
{
|
||||
Name: "query routes",
|
||||
TestFunc: testQueryRoutes,
|
||||
},
|
||||
{
|
||||
Name: "route fee cutoff",
|
||||
TestFunc: testRouteFeeCutoff,
|
||||
},
|
||||
{
|
||||
Name: "rpc middleware interceptor",
|
||||
TestFunc: testRPCMiddlewareInterceptor,
|
||||
},
|
||||
{
|
||||
Name: "macaroon authentication",
|
||||
TestFunc: testMacaroonAuthentication,
|
||||
},
|
||||
{
|
||||
Name: "bake macaroon",
|
||||
TestFunc: testBakeMacaroon,
|
||||
},
|
||||
{
|
||||
Name: "delete macaroon id",
|
||||
TestFunc: testDeleteMacaroonID,
|
||||
},
|
||||
{
|
||||
Name: "stateless init",
|
||||
TestFunc: testStatelessInit,
|
||||
},
|
||||
{
|
||||
Name: "single hop invoice",
|
||||
TestFunc: testSingleHopInvoice,
|
||||
},
|
||||
{
|
||||
Name: "wipe forwarding packages",
|
||||
TestFunc: testWipeForwardingPackages,
|
||||
},
|
||||
{
|
||||
Name: "switch circuit persistence",
|
||||
TestFunc: testSwitchCircuitPersistence,
|
||||
},
|
||||
{
|
||||
Name: "switch offline delivery",
|
||||
TestFunc: testSwitchOfflineDelivery,
|
||||
},
|
||||
{
|
||||
Name: "switch offline delivery persistence",
|
||||
TestFunc: testSwitchOfflineDeliveryPersistence,
|
||||
},
|
||||
{
|
||||
Name: "switch offline delivery outgoing offline",
|
||||
TestFunc: testSwitchOfflineDeliveryOutgoingOffline,
|
||||
},
|
||||
{
|
||||
Name: "sendtoroute multi path payment",
|
||||
TestFunc: testSendToRouteMultiPath,
|
||||
},
|
||||
{
|
||||
Name: "send multi path payment",
|
||||
TestFunc: testSendMultiPathPayment,
|
||||
},
|
||||
{
|
||||
Name: "sendpayment amp invoice",
|
||||
TestFunc: testSendPaymentAMPInvoice,
|
||||
},
|
||||
{
|
||||
Name: "sendpayment amp invoice repeat",
|
||||
TestFunc: testSendPaymentAMPInvoiceRepeat,
|
||||
},
|
||||
{
|
||||
Name: "send payment amp",
|
||||
TestFunc: testSendPaymentAMP,
|
||||
},
|
||||
{
|
||||
Name: "sendtoroute amp",
|
||||
TestFunc: testSendToRouteAMP,
|
||||
},
|
||||
{
|
||||
Name: "forward interceptor dedup htlcs",
|
||||
TestFunc: testForwardInterceptorDedupHtlc,
|
||||
},
|
||||
{
|
||||
Name: "forward interceptor",
|
||||
TestFunc: testForwardInterceptorBasic,
|
||||
},
|
||||
{
|
||||
Name: "zero conf channel open",
|
||||
TestFunc: testZeroConfChannelOpen,
|
||||
},
|
||||
{
|
||||
Name: "option scid alias",
|
||||
TestFunc: testOptionScidAlias,
|
||||
},
|
||||
{
|
||||
Name: "scid alias channel update",
|
||||
TestFunc: testUpdateChannelPolicyScidAlias,
|
||||
},
|
||||
{
|
||||
Name: "scid alias upgrade",
|
||||
TestFunc: testOptionScidUpgrade,
|
||||
},
|
||||
{
|
||||
Name: "nonstd sweep",
|
||||
TestFunc: testNonstdSweep,
|
||||
},
|
||||
{
|
||||
Name: "multiple channel creation and update subscription",
|
||||
TestFunc: testBasicChannelCreationAndUpdates,
|
||||
},
|
||||
{
|
||||
Name: "derive shared key",
|
||||
TestFunc: testDeriveSharedKey,
|
||||
},
|
||||
{
|
||||
Name: "sign output raw",
|
||||
TestFunc: testSignOutputRaw,
|
||||
},
|
||||
{
|
||||
Name: "sign verify message",
|
||||
TestFunc: testSignVerifyMessage,
|
||||
},
|
||||
{
|
||||
Name: "cpfp",
|
||||
TestFunc: testCPFP,
|
||||
},
|
||||
{
|
||||
Name: "taproot",
|
||||
TestFunc: testTaproot,
|
||||
},
|
||||
{
|
||||
Name: "wallet import account",
|
||||
TestFunc: testWalletImportAccount,
|
||||
},
|
||||
{
|
||||
Name: "wallet import pubkey",
|
||||
TestFunc: testWalletImportPubKey,
|
||||
},
|
||||
{
|
||||
Name: "async payments benchmark",
|
||||
TestFunc: testAsyncPayments,
|
||||
},
|
||||
{
|
||||
Name: "remote signer",
|
||||
TestFunc: testRemoteSigner,
|
||||
},
|
||||
{
|
||||
Name: "taproot coop close",
|
||||
TestFunc: testTaprootCoopClose,
|
||||
},
|
||||
{
|
||||
Name: "trackpayments",
|
||||
TestFunc: testTrackPayments,
|
||||
},
|
||||
{
|
||||
Name: "open channel fee policy",
|
||||
TestFunc: testOpenChannelUpdateFeePolicy,
|
||||
},
|
||||
{
|
||||
Name: "custom message",
|
||||
TestFunc: testCustomMessage,
|
||||
},
|
||||
{
|
||||
Name: "sign verify message with addr",
|
||||
TestFunc: testSignVerifyMessageWithAddr,
|
||||
},
|
||||
{
|
||||
Name: "zero conf reorg edge existence",
|
||||
TestFunc: testZeroConfReorg,
|
||||
},
|
||||
{
|
||||
Name: "async bidirectional payments",
|
||||
TestFunc: testBidirectionalAsyncPayments,
|
||||
},
|
||||
}
|
||||
@@ -1,660 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/amp"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testSendPaymentAMPInvoice tests that we can send an AMP payment to a
|
||||
// specified AMP invoice using SendPaymentV2.
|
||||
func testSendPaymentAMPInvoice(ht *lntemp.HarnessTest) {
|
||||
succeed := ht.Run("native payaddr", func(t *testing.T) {
|
||||
tt := ht.Subtest(t)
|
||||
testSendPaymentAMPInvoiceCase(tt, false)
|
||||
})
|
||||
|
||||
// Abort the test if failed.
|
||||
if !succeed {
|
||||
return
|
||||
}
|
||||
|
||||
ht.Run("external payaddr", func(t *testing.T) {
|
||||
tt := ht.Subtest(t)
|
||||
testSendPaymentAMPInvoiceCase(tt, true)
|
||||
})
|
||||
}
|
||||
|
||||
func testSendPaymentAMPInvoiceCase(ht *lntemp.HarnessTest,
|
||||
useExternalPayAddr bool) {
|
||||
|
||||
mts := newMppTestScenario(ht)
|
||||
|
||||
// Subscribe to bob's invoices. Do this early in the test to make sure
|
||||
// that the subscription has actually been completed when we add an
|
||||
// invoice. Otherwise the notification will be missed.
|
||||
req := &lnrpc.InvoiceSubscription{}
|
||||
bobInvoiceSubscription := mts.bob.RPC.SubscribeInvoices(req)
|
||||
|
||||
const paymentAmt = btcutil.Amount(300000)
|
||||
|
||||
// Set up a network with three different paths Alice <-> Bob. Channel
|
||||
// capacities are set such that the payment can only succeed if (at
|
||||
// least) three paths are used.
|
||||
//
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
//
|
||||
mppReq := &mppOpenChannelRequest{
|
||||
amtAliceCarol: 235000,
|
||||
amtAliceDave: 135000,
|
||||
amtCarolBob: 135000,
|
||||
amtCarolEve: 135000,
|
||||
amtDaveBob: 135000,
|
||||
amtEveBob: 135000,
|
||||
}
|
||||
mts.openChannels(mppReq)
|
||||
chanPointAliceDave := mts.channelPoints[1]
|
||||
chanPointDaveBob := mts.channelPoints[4]
|
||||
|
||||
invoice := &lnrpc.Invoice{
|
||||
Value: int64(paymentAmt),
|
||||
IsAmp: true,
|
||||
}
|
||||
addInvoiceResp := mts.bob.RPC.AddInvoice(invoice)
|
||||
|
||||
// Ensure we get a notification of the invoice being added by Bob.
|
||||
rpcInvoice := ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
|
||||
require.False(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_OPEN, rpcInvoice.State)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidMsat)
|
||||
require.Equal(ht, 0, len(rpcInvoice.Htlcs))
|
||||
|
||||
// Increase Dave's fee to make the test deterministic. Otherwise it
|
||||
// would be unpredictable whether pathfinding would go through Charlie
|
||||
// or Dave for the first shard.
|
||||
expectedPolicy := mts.updateDaveGlobalPolicy()
|
||||
|
||||
// Make sure Alice has heard it for both Dave's channels.
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.dave, expectedPolicy, chanPointAliceDave, false,
|
||||
)
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.dave, expectedPolicy, chanPointDaveBob, false,
|
||||
)
|
||||
|
||||
// Generate an external payment address when attempting to pseudo-reuse
|
||||
// an AMP invoice. When using an external payment address, we'll also
|
||||
// expect an extra invoice to appear in the ListInvoices response, since
|
||||
// a new invoice will be JIT inserted under a different payment address
|
||||
// than the one in the invoice.
|
||||
var (
|
||||
expNumInvoices = 1
|
||||
externalPayAddr []byte
|
||||
)
|
||||
if useExternalPayAddr {
|
||||
expNumInvoices = 2
|
||||
externalPayAddr = ht.Random32Bytes()
|
||||
}
|
||||
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: addInvoiceResp.PaymentRequest,
|
||||
PaymentAddr: externalPayAddr,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
payment := ht.SendPaymentAssertSettled(mts.alice, sendReq)
|
||||
|
||||
// Check that Alice split the payment in at least three shards. Because
|
||||
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
||||
// there is some non-determinism in the process. Depending on whether
|
||||
// the new pathfinding round is started before or after the htlc is
|
||||
// locked into the channel, different sharding may occur. Therefore we
|
||||
// can only check if the number of shards isn't below the theoretical
|
||||
// minimum.
|
||||
succeeded := 0
|
||||
for _, htlc := range payment.Htlcs {
|
||||
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
|
||||
const minExpectedShards = 3
|
||||
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
|
||||
"expected num of shards not reached")
|
||||
|
||||
// When an external payment address is supplied, we'll get an extra
|
||||
// notification for the JIT inserted invoice, since it differs from the
|
||||
// original.
|
||||
if useExternalPayAddr {
|
||||
ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
}
|
||||
|
||||
// There should now be a settle event for the invoice.
|
||||
rpcInvoice = ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
|
||||
// Also fetch Bob's invoice from ListInvoices and assert it is equal to
|
||||
// the one received via the subscription.
|
||||
invoices := ht.AssertNumInvoices(mts.bob, expNumInvoices)
|
||||
ht.AssertInvoiceEqual(rpcInvoice, invoices[expNumInvoices-1])
|
||||
|
||||
// Assert that the invoice is settled for the total payment amount and
|
||||
// has the correct payment address.
|
||||
require.True(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, rpcInvoice.State)
|
||||
require.Equal(ht, int64(paymentAmt), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(paymentAmt*1000), rpcInvoice.AmtPaidMsat)
|
||||
|
||||
// Finally, assert that the same set id is recorded for each htlc, and
|
||||
// that the preimage hash pair is valid.
|
||||
var setID []byte
|
||||
require.Equal(ht, succeeded, len(rpcInvoice.Htlcs))
|
||||
for _, htlc := range rpcInvoice.Htlcs {
|
||||
require.NotNil(ht, htlc.Amp)
|
||||
if setID == nil {
|
||||
setID = make([]byte, 32)
|
||||
copy(setID, htlc.Amp.SetId)
|
||||
}
|
||||
require.Equal(ht, setID, htlc.Amp.SetId)
|
||||
|
||||
// Parse the child hash and child preimage, and assert they are
|
||||
// well-formed.
|
||||
childHash, err := lntypes.MakeHash(htlc.Amp.Hash)
|
||||
require.NoError(ht, err)
|
||||
childPreimage, err := lntypes.MakePreimage(htlc.Amp.Preimage)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Assert that the preimage actually matches the hashes.
|
||||
validPreimage := childPreimage.Matches(childHash)
|
||||
require.True(ht, validPreimage)
|
||||
}
|
||||
|
||||
// The set ID we extract above should be shown in the final settled
|
||||
// state.
|
||||
ampState := rpcInvoice.AmpInvoiceState[hex.EncodeToString(setID)]
|
||||
require.Equal(ht, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
||||
|
||||
// Finally, close all channels.
|
||||
mts.closeChannels()
|
||||
}
|
||||
|
||||
// testSendPaymentAMPInvoiceRepeat tests that it's possible to pay an AMP
|
||||
// invoice multiple times by having the client generate a new setID each time.
|
||||
func testSendPaymentAMPInvoiceRepeat(ht *lntemp.HarnessTest) {
|
||||
// In this basic test, we'll only need two nodes as we want to
|
||||
// primarily test the recurring payment feature. So we'll re-use the
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Send Carol enough coins to be able to open a channel to Dave.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
dave := ht.NewNode("Dave", nil)
|
||||
|
||||
// Set up an invoice subscription so we can be notified when Dave
|
||||
// receives his repeated payments.
|
||||
req := &lnrpc.InvoiceSubscription{}
|
||||
invSubscription := dave.RPC.SubscribeInvoices(req)
|
||||
|
||||
// Before we start the test, we'll ensure both sides are connected to
|
||||
// the funding flow can properly be executed.
|
||||
ht.EnsureConnected(carol, dave)
|
||||
|
||||
// Establish a channel between Carol and Dave.
|
||||
chanAmt := btcutil.Amount(100_000)
|
||||
ht.OpenChannel(
|
||||
carol, dave, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Create an AMP invoice of a trivial amount, that we'll pay repeatedly
|
||||
// in this integration test.
|
||||
paymentAmt := 10000
|
||||
invoice := &lnrpc.Invoice{
|
||||
Value: int64(paymentAmt),
|
||||
IsAmp: true,
|
||||
}
|
||||
addInvoiceResp := dave.RPC.AddInvoice(invoice)
|
||||
|
||||
// We should get an initial notification that the HTLC has been added.
|
||||
rpcInvoice := ht.ReceiveInvoiceUpdate(invSubscription)
|
||||
require.False(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_OPEN, rpcInvoice.State)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidMsat)
|
||||
require.Equal(ht, 0, len(rpcInvoice.Htlcs))
|
||||
|
||||
// Now we'll use Carol to pay the invoice that Dave created.
|
||||
ht.CompletePaymentRequests(
|
||||
carol, []string{addInvoiceResp.PaymentRequest},
|
||||
)
|
||||
|
||||
// Dave should get a notification that the invoice has been settled.
|
||||
invoiceNtfn := ht.ReceiveInvoiceUpdate(invSubscription)
|
||||
|
||||
// The notification should signal that the invoice is now settled, and
|
||||
// should also include the set ID, and show the proper amount paid.
|
||||
require.True(ht, invoiceNtfn.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, invoiceNtfn.State)
|
||||
require.Equal(ht, paymentAmt, int(invoiceNtfn.AmtPaidSat))
|
||||
require.Equal(ht, 1, len(invoiceNtfn.AmpInvoiceState))
|
||||
var firstSetID []byte
|
||||
for setIDStr, ampState := range invoiceNtfn.AmpInvoiceState {
|
||||
firstSetID, _ = hex.DecodeString(setIDStr)
|
||||
require.Equal(ht, lnrpc.InvoiceHTLCState_SETTLED,
|
||||
ampState.State)
|
||||
}
|
||||
|
||||
// Pay the invoice again, we should get another notification that Dave
|
||||
// has received another payment.
|
||||
ht.CompletePaymentRequests(
|
||||
carol, []string{addInvoiceResp.PaymentRequest},
|
||||
)
|
||||
|
||||
// Dave should get another notification.
|
||||
invoiceNtfn = ht.ReceiveInvoiceUpdate(invSubscription)
|
||||
|
||||
// The invoice should still be shown as settled, and also include the
|
||||
// information about this newly generated setID, showing 2x the amount
|
||||
// paid.
|
||||
require.True(ht, invoiceNtfn.Settled)
|
||||
require.Equal(ht, paymentAmt*2, int(invoiceNtfn.AmtPaidSat))
|
||||
|
||||
var secondSetID []byte
|
||||
for setIDStr, ampState := range invoiceNtfn.AmpInvoiceState {
|
||||
secondSetID, _ = hex.DecodeString(setIDStr)
|
||||
require.Equal(ht, lnrpc.InvoiceHTLCState_SETTLED,
|
||||
ampState.State)
|
||||
}
|
||||
|
||||
// The returned invoice should only include a single HTLC since we
|
||||
// return the "projected" sub-invoice for a given setID.
|
||||
require.Equal(ht, 1, len(invoiceNtfn.Htlcs))
|
||||
|
||||
// However the AMP state index should show that there've been two
|
||||
// repeated payments to this invoice so far.
|
||||
require.Equal(ht, 2, len(invoiceNtfn.AmpInvoiceState))
|
||||
|
||||
// Now we'll look up the invoice using the new LookupInvoice2 RPC call
|
||||
// by the set ID of each of the invoices.
|
||||
msg := &invoicesrpc.LookupInvoiceMsg{
|
||||
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_SetId{
|
||||
SetId: firstSetID,
|
||||
},
|
||||
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_ONLY,
|
||||
}
|
||||
subInvoice1 := dave.RPC.LookupInvoiceV2(msg)
|
||||
msg = &invoicesrpc.LookupInvoiceMsg{
|
||||
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_SetId{
|
||||
SetId: secondSetID,
|
||||
},
|
||||
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_ONLY,
|
||||
}
|
||||
subInvoice2 := dave.RPC.LookupInvoiceV2(msg)
|
||||
|
||||
// Each invoice should only show a single HTLC present, as we passed
|
||||
// the HTLC set only modifier.
|
||||
require.Equal(ht, 1, len(subInvoice1.Htlcs))
|
||||
require.Equal(ht, 1, len(subInvoice2.Htlcs))
|
||||
|
||||
// If we look up the same invoice, by its payment address, but now with
|
||||
// the HTLC blank modifier, then none of them should be returned.
|
||||
msg = &invoicesrpc.LookupInvoiceMsg{
|
||||
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
||||
PaymentAddr: addInvoiceResp.PaymentAddr,
|
||||
},
|
||||
LookupModifier: invoicesrpc.LookupModifier_HTLC_SET_BLANK,
|
||||
}
|
||||
rootInvoice := dave.RPC.LookupInvoiceV2(msg)
|
||||
require.Equal(ht, 0, len(rootInvoice.Htlcs))
|
||||
|
||||
// If we look up the same invoice, by its payment address, but without
|
||||
// that modified, then we should get all the relevant HTLCs.
|
||||
msg = &invoicesrpc.LookupInvoiceMsg{
|
||||
InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
|
||||
PaymentAddr: addInvoiceResp.PaymentAddr,
|
||||
},
|
||||
}
|
||||
rootInvoice = dave.RPC.LookupInvoiceV2(msg)
|
||||
require.Equal(ht, 2, len(rootInvoice.Htlcs))
|
||||
|
||||
// Finally, we'll test that if we subscribe for notifications of
|
||||
// settled invoices, we get a backlog, which includes the invoice we
|
||||
// settled last (since you can only fetch from index 1 onwards), and
|
||||
// only the relevant set of HTLCs.
|
||||
req = &lnrpc.InvoiceSubscription{
|
||||
SettleIndex: 1,
|
||||
}
|
||||
invSub2 := dave.RPC.SubscribeInvoices(req)
|
||||
|
||||
// The first invoice we get back should match the state of the invoice
|
||||
// after our second payment: amt updated, but only a single HTLC shown
|
||||
// through.
|
||||
backlogInv := ht.ReceiveInvoiceUpdate(invSub2)
|
||||
require.Equal(ht, 1, len(backlogInv.Htlcs))
|
||||
require.Equal(ht, 2, len(backlogInv.AmpInvoiceState))
|
||||
require.True(ht, backlogInv.Settled)
|
||||
require.Equal(ht, paymentAmt*2, int(backlogInv.AmtPaidSat))
|
||||
}
|
||||
|
||||
// testSendPaymentAMP tests that we can send an AMP payment to a specified
|
||||
// destination using SendPaymentV2.
|
||||
func testSendPaymentAMP(ht *lntemp.HarnessTest) {
|
||||
mts := newMppTestScenario(ht)
|
||||
const paymentAmt = btcutil.Amount(300000)
|
||||
|
||||
// Set up a network with three different paths Alice <-> Bob. Channel
|
||||
// capacities are set such that the payment can only succeed if (at
|
||||
// least) three paths are used.
|
||||
//
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
//
|
||||
mppReq := &mppOpenChannelRequest{
|
||||
amtAliceCarol: 235000,
|
||||
amtAliceDave: 135000,
|
||||
amtCarolBob: 135000,
|
||||
amtCarolEve: 135000,
|
||||
amtDaveBob: 135000,
|
||||
amtEveBob: 135000,
|
||||
}
|
||||
mts.openChannels(mppReq)
|
||||
chanPointAliceDave := mts.channelPoints[1]
|
||||
|
||||
// Increase Dave's fee to make the test deterministic. Otherwise it
|
||||
// would be unpredictable whether pathfinding would go through Charlie
|
||||
// or Dave for the first shard.
|
||||
expectedPolicy := mts.updateDaveGlobalPolicy()
|
||||
|
||||
// Make sure Alice has heard it.
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.dave, expectedPolicy, chanPointAliceDave, false,
|
||||
)
|
||||
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
Dest: mts.bob.PubKey[:],
|
||||
Amt: int64(paymentAmt),
|
||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
Amp: true,
|
||||
}
|
||||
payment := ht.SendPaymentAssertSettled(mts.alice, sendReq)
|
||||
|
||||
// Check that Alice split the payment in at least three shards. Because
|
||||
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
||||
// there is some non-determinism in the process. Depending on whether
|
||||
// the new pathfinding round is started before or after the htlc is
|
||||
// locked into the channel, different sharding may occur. Therefore we
|
||||
// can only check if the number of shards isn't below the theoretical
|
||||
// minimum.
|
||||
succeeded := 0
|
||||
for _, htlc := range payment.Htlcs {
|
||||
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
|
||||
const minExpectedShards = 3
|
||||
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
|
||||
"expected num of shards not reached")
|
||||
|
||||
// Fetch Bob's invoices. There should only be one invoice.
|
||||
invoices := ht.AssertNumInvoices(mts.bob, 1)
|
||||
rpcInvoice := invoices[0]
|
||||
|
||||
// Assert that the invoice is settled for the total payment amount and
|
||||
// has the correct payment address.
|
||||
require.True(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, rpcInvoice.State)
|
||||
require.Equal(ht, int64(paymentAmt), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(paymentAmt*1000), rpcInvoice.AmtPaidMsat)
|
||||
|
||||
// Finally, assert that the same set id is recorded for each htlc, and
|
||||
// that the preimage hash pair is valid.
|
||||
var setID []byte
|
||||
require.Equal(ht, succeeded, len(rpcInvoice.Htlcs))
|
||||
for _, htlc := range rpcInvoice.Htlcs {
|
||||
require.NotNil(ht, htlc.Amp)
|
||||
if setID == nil {
|
||||
setID = make([]byte, 32)
|
||||
copy(setID, htlc.Amp.SetId)
|
||||
}
|
||||
require.Equal(ht, setID, htlc.Amp.SetId)
|
||||
|
||||
// Parse the child hash and child preimage, and assert they are
|
||||
// well-formed.
|
||||
childHash, err := lntypes.MakeHash(htlc.Amp.Hash)
|
||||
require.NoError(ht, err)
|
||||
childPreimage, err := lntypes.MakePreimage(htlc.Amp.Preimage)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Assert that the preimage actually matches the hashes.
|
||||
validPreimage := childPreimage.Matches(childHash)
|
||||
require.True(ht, validPreimage)
|
||||
}
|
||||
|
||||
// The set ID we extract above should be shown in the final settled
|
||||
// state.
|
||||
ampState := rpcInvoice.AmpInvoiceState[hex.EncodeToString(setID)]
|
||||
require.Equal(ht, lnrpc.InvoiceHTLCState_SETTLED, ampState.State)
|
||||
|
||||
// Finally, close all channels.
|
||||
mts.closeChannels()
|
||||
}
|
||||
|
||||
func testSendToRouteAMP(ht *lntemp.HarnessTest) {
|
||||
mts := newMppTestScenario(ht)
|
||||
const (
|
||||
paymentAmt = btcutil.Amount(300000)
|
||||
numShards = 3
|
||||
shardAmt = paymentAmt / numShards
|
||||
chanAmt = shardAmt * 3 / 2
|
||||
)
|
||||
|
||||
// Subscribe to bob's invoices.
|
||||
req := &lnrpc.InvoiceSubscription{}
|
||||
bobInvoiceSubscription := mts.bob.RPC.SubscribeInvoices(req)
|
||||
|
||||
// Set up a network with three different paths Alice <-> Bob.
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
///
|
||||
mppReq := &mppOpenChannelRequest{
|
||||
// Since the channel Alice-> Carol will have to carry two
|
||||
// shards, we make it larger.
|
||||
amtAliceCarol: chanAmt + shardAmt,
|
||||
amtAliceDave: chanAmt,
|
||||
amtCarolBob: chanAmt,
|
||||
amtCarolEve: chanAmt,
|
||||
amtDaveBob: chanAmt,
|
||||
amtEveBob: chanAmt,
|
||||
}
|
||||
mts.openChannels(mppReq)
|
||||
|
||||
// We'll send shards along three routes from Alice.
|
||||
sendRoutes := [numShards][]*node.HarnessNode{
|
||||
{mts.carol, mts.bob},
|
||||
{mts.dave, mts.bob},
|
||||
{mts.carol, mts.eve, mts.bob},
|
||||
}
|
||||
|
||||
payAddr := ht.Random32Bytes()
|
||||
setID := ht.Random32Bytes()
|
||||
|
||||
var sharer amp.Sharer
|
||||
sharer, err := amp.NewSeedSharer()
|
||||
require.NoError(ht, err)
|
||||
|
||||
childPreimages := make(map[lntypes.Preimage]uint32)
|
||||
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
||||
|
||||
// Define a closure for sending each of the three shards.
|
||||
sendShard := func(i int, hops []*node.HarnessNode) {
|
||||
// Build a route for the specified hops.
|
||||
r := mts.buildRoute(shardAmt, mts.alice, hops)
|
||||
|
||||
// Set the MPP records to indicate this is a payment shard.
|
||||
hop := r.Hops[len(r.Hops)-1]
|
||||
hop.TlvPayload = true
|
||||
hop.MppRecord = &lnrpc.MPPRecord{
|
||||
PaymentAddr: payAddr,
|
||||
TotalAmtMsat: int64(paymentAmt * 1000),
|
||||
}
|
||||
|
||||
var child *amp.Child
|
||||
if i < len(sendRoutes)-1 {
|
||||
var left amp.Sharer
|
||||
left, sharer, err = sharer.Split()
|
||||
require.NoError(ht, err)
|
||||
|
||||
child = left.Child(uint32(i))
|
||||
} else {
|
||||
child = sharer.Child(uint32(i))
|
||||
}
|
||||
childPreimages[child.Preimage] = child.Index
|
||||
|
||||
hop.AmpRecord = &lnrpc.AMPRecord{
|
||||
RootShare: child.Share[:],
|
||||
SetId: setID,
|
||||
ChildIndex: child.Index,
|
||||
}
|
||||
|
||||
// Send the shard.
|
||||
sendReq := &routerrpc.SendToRouteRequest{
|
||||
PaymentHash: child.Hash[:],
|
||||
Route: r,
|
||||
}
|
||||
|
||||
// We'll send all shards in their own goroutine, since
|
||||
// SendToRoute will block as long as the payment is in flight.
|
||||
go func() {
|
||||
resp := mts.alice.RPC.SendToRouteV2(sendReq)
|
||||
responses <- resp
|
||||
}()
|
||||
}
|
||||
|
||||
// Send the first shard, this cause Bob to JIT add an invoice.
|
||||
sendShard(0, sendRoutes[0])
|
||||
|
||||
// Ensure we get a notification of the invoice being added by Bob.
|
||||
rpcInvoice, err := bobInvoiceSubscription.Recv()
|
||||
require.NoError(ht, err)
|
||||
|
||||
require.False(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_OPEN, rpcInvoice.State)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(0), rpcInvoice.AmtPaidMsat)
|
||||
require.Equal(ht, payAddr, rpcInvoice.PaymentAddr)
|
||||
|
||||
require.Equal(ht, 0, len(rpcInvoice.Htlcs))
|
||||
|
||||
sendShard(1, sendRoutes[1])
|
||||
sendShard(2, sendRoutes[2])
|
||||
|
||||
// Assert that all of the child preimages are unique.
|
||||
require.Equal(ht, len(sendRoutes), len(childPreimages))
|
||||
|
||||
// Make a copy of the childPreimages map for validating the resulting
|
||||
// invoice.
|
||||
childPreimagesCopy := make(map[lntypes.Preimage]uint32)
|
||||
for preimage, childIndex := range childPreimages {
|
||||
childPreimagesCopy[preimage] = childIndex
|
||||
}
|
||||
|
||||
// Wait for all responses to be back, and check that they all
|
||||
// succeeded.
|
||||
timer := time.After(defaultTimeout)
|
||||
for range sendRoutes {
|
||||
var resp *lnrpc.HTLCAttempt
|
||||
select {
|
||||
case resp = <-responses:
|
||||
case <-timer:
|
||||
require.Fail(ht, "response not received")
|
||||
}
|
||||
|
||||
require.Nil(ht, resp.Failure, "received payment failure")
|
||||
|
||||
preimage, err := lntypes.MakePreimage(resp.Preimage)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Assert that the response includes one of our child preimages.
|
||||
_, ok := childPreimages[preimage]
|
||||
require.True(ht, ok)
|
||||
|
||||
// Remove this preimage from out set so that we ensure all
|
||||
// responses have a unique child preimage.
|
||||
delete(childPreimages, preimage)
|
||||
}
|
||||
childPreimages = childPreimagesCopy
|
||||
|
||||
// There should now be a settle event for the invoice.
|
||||
rpcInvoice, err = bobInvoiceSubscription.Recv()
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Also fetch Bob's invoice from ListInvoices and assert it is equal to
|
||||
// the one received via the subscription.
|
||||
invoices := ht.AssertNumInvoices(mts.bob, 1)
|
||||
ht.AssertInvoiceEqual(rpcInvoice, invoices[0])
|
||||
|
||||
// Assert that the invoice is settled for the total payment amount and
|
||||
// has the correct payment address.
|
||||
require.True(ht, rpcInvoice.Settled)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, rpcInvoice.State)
|
||||
require.Equal(ht, int64(paymentAmt), rpcInvoice.AmtPaidSat)
|
||||
require.Equal(ht, int64(paymentAmt*1000), rpcInvoice.AmtPaidMsat)
|
||||
require.Equal(ht, payAddr, rpcInvoice.PaymentAddr)
|
||||
|
||||
// Finally, assert that the proper set id is recorded for each htlc, and
|
||||
// that the preimage hash pair is valid.
|
||||
require.Equal(ht, numShards, len(rpcInvoice.Htlcs))
|
||||
for _, htlc := range rpcInvoice.Htlcs {
|
||||
require.NotNil(ht, htlc.Amp)
|
||||
require.Equal(ht, setID, htlc.Amp.SetId)
|
||||
|
||||
// Parse the child hash and child preimage, and assert they are
|
||||
// well-formed.
|
||||
childHash, err := lntypes.MakeHash(htlc.Amp.Hash)
|
||||
require.NoError(ht, err)
|
||||
childPreimage, err := lntypes.MakePreimage(htlc.Amp.Preimage)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Assert that the preimage actually matches the hashes.
|
||||
validPreimage := childPreimage.Matches(childHash)
|
||||
require.True(ht, validPreimage)
|
||||
|
||||
// Assert that the HTLC includes one of our child preimages.
|
||||
childIndex, ok := childPreimages[childPreimage]
|
||||
require.True(ht, ok)
|
||||
|
||||
// Assert that the correct child index is reflected.
|
||||
require.Equal(ht, childIndex, htlc.Amp.ChildIndex)
|
||||
|
||||
// Remove this preimage from our set so that we ensure all HTLCs
|
||||
// have a unique child preimage.
|
||||
delete(childPreimages, childPreimage)
|
||||
}
|
||||
|
||||
// Finally, close all channels.
|
||||
mts.closeChannels()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,208 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testChannelBalance creates a new channel between Alice and Bob, then checks
|
||||
// channel balance to be equal amount specified while creation of channel.
|
||||
func testChannelBalance(ht *lntemp.HarnessTest) {
|
||||
// Open a channel with 0.16 BTC between Alice and Bob, ensuring the
|
||||
// channel has been opened properly.
|
||||
amount := funding.MaxBtcFundingAmount
|
||||
|
||||
// Creates a helper closure to be used below which asserts the proper
|
||||
// response to a channel balance RPC.
|
||||
checkChannelBalance := func(node *node.HarnessNode,
|
||||
local, remote btcutil.Amount) {
|
||||
|
||||
expectedResponse := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(local),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(local)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(remote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
remote,
|
||||
)),
|
||||
},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(local),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(node, expectedResponse)
|
||||
}
|
||||
|
||||
// Before beginning, make sure alice and bob are connected.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: amount},
|
||||
)
|
||||
cType := ht.GetChannelCommitType(alice, chanPoint)
|
||||
|
||||
// As this is a single funder channel, Alice's balance should be
|
||||
// exactly 0.5 BTC since now state transitions have taken place yet.
|
||||
checkChannelBalance(alice, amount-calcStaticFee(cType, 0), 0)
|
||||
|
||||
// Ensure Bob currently has no available balance within the channel.
|
||||
checkChannelBalance(bob, 0, amount-calcStaticFee(cType, 0))
|
||||
|
||||
// Finally close the channel between Alice and Bob, asserting that the
|
||||
// channel has been properly closed on-chain.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testChannelUnsettledBalance will test that the UnsettledBalance field
|
||||
// is updated according to the number of Pending Htlcs.
|
||||
// Alice will send Htlcs to Carol while she is in hodl mode. This will result
|
||||
// in a build of pending Htlcs. We expect the channels unsettled balance to
|
||||
// equal the sum of all the Pending Htlcs.
|
||||
func testChannelUnsettledBalance(ht *lntemp.HarnessTest) {
|
||||
const chanAmt = btcutil.Amount(1000000)
|
||||
|
||||
// Creates a helper closure to be used below which asserts the proper
|
||||
// response to a channel balance RPC.
|
||||
checkChannelBalance := func(node *node.HarnessNode,
|
||||
local, remote, unsettledLocal, unsettledRemote btcutil.Amount) {
|
||||
|
||||
expectedResponse := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(local),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
local,
|
||||
)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(remote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
remote,
|
||||
)),
|
||||
},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(unsettledLocal),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
unsettledLocal,
|
||||
)),
|
||||
},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(unsettledRemote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
unsettledRemote,
|
||||
)),
|
||||
},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(local),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(node, expectedResponse)
|
||||
}
|
||||
|
||||
// Create carol in hodl mode.
|
||||
carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"})
|
||||
|
||||
// Connect Alice to Carol.
|
||||
alice := ht.Alice
|
||||
ht.ConnectNodes(alice, carol)
|
||||
|
||||
// Open a channel between Alice and Carol.
|
||||
chanPointAlice := ht.OpenChannel(
|
||||
alice, carol, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
cType := ht.GetChannelCommitType(alice, chanPointAlice)
|
||||
|
||||
// Check alice's channel balance, which should have zero remote and zero
|
||||
// pending balance.
|
||||
checkChannelBalance(alice, chanAmt-calcStaticFee(cType, 0), 0, 0, 0)
|
||||
|
||||
// Check carol's channel balance, which should have zero local and zero
|
||||
// pending balance.
|
||||
checkChannelBalance(carol, 0, chanAmt-calcStaticFee(cType, 0), 0, 0)
|
||||
|
||||
// Channel should be ready for payments.
|
||||
const (
|
||||
payAmt = 100
|
||||
numInvoices = 6
|
||||
)
|
||||
|
||||
// Simulateneously send numInvoices payments from Alice to Carol.
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
go func() {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
Dest: carol.PubKey[:],
|
||||
Amt: int64(payAmt),
|
||||
PaymentHash: ht.Random32Bytes(),
|
||||
FinalCltvDelta: finalCltvDelta,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
alice.RPC.SendPayment(req)
|
||||
}()
|
||||
}
|
||||
|
||||
// There should be a number of PendingHtlcs equal
|
||||
// to the amount of Invoices sent.
|
||||
ht.AssertNumActiveHtlcs(alice, numInvoices)
|
||||
ht.AssertNumActiveHtlcs(carol, numInvoices)
|
||||
|
||||
// Set the amount expected for the Unsettled Balance for this channel.
|
||||
expectedBalance := numInvoices * payAmt
|
||||
|
||||
// Test that the UnsettledBalance for both Alice and Carol
|
||||
// is equal to the amount of invoices * payAmt.
|
||||
checkUnsettledBalance := func() error {
|
||||
// Get channel info for the Alice.
|
||||
chanInfo := ht.QueryChannelByChanPoint(alice, chanPointAlice)
|
||||
|
||||
// Check that UnsettledBalance is what we expect.
|
||||
if int(chanInfo.UnsettledBalance) != expectedBalance {
|
||||
return fmt.Errorf("unsettled balance failed "+
|
||||
"expected: %v, received: %v", expectedBalance,
|
||||
chanInfo.UnsettledBalance)
|
||||
}
|
||||
|
||||
// Get channel info for the Carol.
|
||||
chanInfo = ht.QueryChannelByChanPoint(carol, chanPointAlice)
|
||||
|
||||
// Check that UnsettledBalance is what we expect.
|
||||
if int(chanInfo.UnsettledBalance) != expectedBalance {
|
||||
return fmt.Errorf("unsettled balance failed "+
|
||||
"expected: %v, received: %v", expectedBalance,
|
||||
chanInfo.UnsettledBalance)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
require.NoError(ht, wait.NoError(checkUnsettledBalance, defaultTimeout),
|
||||
"timeout while checking unsettled balance")
|
||||
|
||||
// Check alice's channel balance, which should have a remote unsettled
|
||||
// balance that equals to the amount of invoices * payAmt. The remote
|
||||
// balance remains zero.
|
||||
aliceLocal := chanAmt - calcStaticFee(cType, 0) - numInvoices*payAmt
|
||||
checkChannelBalance(alice, aliceLocal, 0, 0, numInvoices*payAmt)
|
||||
|
||||
// Check carol's channel balance, which should have a local unsettled
|
||||
// balance that equals to the amount of invoices * payAmt. The local
|
||||
// balance remains zero.
|
||||
checkChannelBalance(carol, 0, aliceLocal, numInvoices*payAmt, 0)
|
||||
|
||||
// Force and assert the channel closure.
|
||||
ht.ForceCloseChannel(alice, chanPointAlice)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,716 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/peersrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testUpdateChanStatus checks that calls to the UpdateChanStatus RPC update
|
||||
// the channel graph as expected, and that channel state is properly updated
|
||||
// in the presence of interleaved node disconnects / reconnects.
|
||||
//
|
||||
// NOTE: this test can be flaky as we are testing the chan-enable-timeout and
|
||||
// chan-disable-timeout flags here.
|
||||
//
|
||||
// For chan-enable-timeout, setting this value too small will cause an enabled
|
||||
// channel never be considered active by our channel status manager. Upon
|
||||
// reconnection, our Brontide will send a request to enable this channel after
|
||||
// the "chan-enable-timeout" has passed. The request is handled by the channel
|
||||
// status manager, which will check the channel's eligibility to forward links
|
||||
// by asking htlcswitch/link. Meanwhile, the htlcswitch/link won't mark the
|
||||
// link as eligible unless it has finished its initialization, which takes some
|
||||
// time. Thus, if the Brontide sends a request too early it will get a false
|
||||
// report saying the channel link is not eligible because that link hasn't
|
||||
// finished its initialization.
|
||||
//
|
||||
// For chan-disable-timeout, setting this value too small will cause an already
|
||||
// enabled channel being marked as disabled. For instance, if some operations
|
||||
// take more than 5 seconds to finish, the channel will be marked as disabled,
|
||||
// thus a following operation will fail if it relies on the channel being
|
||||
// enabled.
|
||||
func testUpdateChanStatus(ht *lntemp.HarnessTest) {
|
||||
// Create two fresh nodes and open a channel between them.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
args := []string{
|
||||
"--minbackoff=60s",
|
||||
"--chan-enable-timeout=3s",
|
||||
"--chan-disable-timeout=6s",
|
||||
"--chan-status-sample-interval=0.5s",
|
||||
}
|
||||
ht.RestartNodeWithExtraArgs(alice, args)
|
||||
ht.RestartNodeWithExtraArgs(bob, args)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanAmt := btcutil.Amount(100000)
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
defer ht.CloseChannel(alice, chanPoint)
|
||||
|
||||
// assertEdgeDisabled ensures that Alice has the correct Disabled state
|
||||
// for given channel from her DescribeGraph.
|
||||
assertEdgeDisabled := func(disabled bool) {
|
||||
outPoint := ht.OutPointFromChannelPoint(chanPoint)
|
||||
|
||||
err := wait.NoError(func() error {
|
||||
edge := ht.AssertNumEdges(alice, 1, true)[0]
|
||||
if edge.ChanPoint != outPoint.String() {
|
||||
return fmt.Errorf("expected chan_point %v, "+
|
||||
"got %v", outPoint, edge.ChanPoint)
|
||||
}
|
||||
var policy *lnrpc.RoutingPolicy
|
||||
if alice.PubKeyStr == edge.Node1Pub {
|
||||
policy = edge.Node1Policy
|
||||
} else {
|
||||
policy = edge.Node2Policy
|
||||
}
|
||||
if disabled != policy.Disabled {
|
||||
return fmt.Errorf("expected policy.Disabled "+
|
||||
"to be %v, but policy was %v", disabled,
|
||||
policy.Disabled)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "assert edge disabled timeout")
|
||||
}
|
||||
|
||||
// aliceSendReq sends an UpdateChanStatus request to Alice.
|
||||
aliceSendReq := func(action routerrpc.ChanStatusAction) {
|
||||
req := &routerrpc.UpdateChanStatusRequest{
|
||||
ChanPoint: chanPoint,
|
||||
Action: action,
|
||||
}
|
||||
alice.RPC.UpdateChanStatus(req)
|
||||
}
|
||||
|
||||
// Initially, the channel between Alice and Bob should not be disabled.
|
||||
//
|
||||
// NOTE: This check should happen right after the channel openning as
|
||||
// we've used a short timeout value for `--chan-disable-timeout`. If we
|
||||
// wait longer than that we might get a flake saying the channel is
|
||||
// disabled.
|
||||
assertEdgeDisabled(false)
|
||||
|
||||
// Launch a node for Carol which will connect to Alice and Bob in order
|
||||
// to receive graph updates. This will ensure that the channel updates
|
||||
// are propagated throughout the network.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// assertChannelUpdate checks that the required policy update has been
|
||||
// heard in Carol's network.
|
||||
assertChannelUpdate := func(node *node.HarnessNode,
|
||||
policy *lnrpc.RoutingPolicy) {
|
||||
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
carol, node, policy, chanPoint, false,
|
||||
)
|
||||
}
|
||||
|
||||
// Connect both Alice and Bob to the new node Carol, so she can sync
|
||||
// her graph.
|
||||
ht.ConnectNodes(alice, carol)
|
||||
ht.ConnectNodes(bob, carol)
|
||||
ht.WaitForGraphSync(carol)
|
||||
|
||||
// If the above waitForGraphSync takes more than 4 seconds, the channel
|
||||
// Alice=>Bob will be marked as disabled now. Thus we connect Alice and
|
||||
// Bob again to make sure the channel is alive.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// When updating the state of the channel between Alice and Bob, we
|
||||
// should expect to see channel updates with the default routing
|
||||
// policy. The value of "Disabled" will depend on the specific scenario
|
||||
// being tested.
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: int64(chainreg.DefaultBitcoinBaseFeeMSat),
|
||||
FeeRateMilliMsat: int64(chainreg.DefaultBitcoinFeeRate),
|
||||
TimeLockDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
MinHtlc: 1000, // default value
|
||||
MaxHtlcMsat: calculateMaxHtlc(chanAmt),
|
||||
}
|
||||
|
||||
// Manually disable the channel and ensure that a "Disabled = true"
|
||||
// update is propagated.
|
||||
aliceSendReq(routerrpc.ChanStatusAction_DISABLE)
|
||||
expectedPolicy.Disabled = true
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
|
||||
// Re-enable the channel and ensure that a "Disabled = false" update is
|
||||
// propagated.
|
||||
aliceSendReq(routerrpc.ChanStatusAction_ENABLE)
|
||||
expectedPolicy.Disabled = false
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
|
||||
// Manually enabling a channel should NOT prevent subsequent
|
||||
// disconnections from automatically disabling the channel again (we
|
||||
// don't want to clutter the network with channels that are falsely
|
||||
// advertised as enabled when they don't work).
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
expectedPolicy.Disabled = true
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
|
||||
// Reconnecting the nodes should propagate a "Disabled = false" update.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
expectedPolicy.Disabled = false
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
|
||||
// Manually disabling the channel should prevent a subsequent
|
||||
// disconnect/reconnect from re-enabling the channel on Alice's end.
|
||||
// Note the asymmetry between manual enable and manual disable!
|
||||
aliceSendReq(routerrpc.ChanStatusAction_DISABLE)
|
||||
|
||||
// Alice sends out the "Disabled = true" update in response to the
|
||||
// ChanStatusAction_DISABLE request.
|
||||
expectedPolicy.Disabled = true
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
|
||||
// Bob sends a "Disabled = true" update upon detecting the disconnect.
|
||||
expectedPolicy.Disabled = true
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
|
||||
// Bob sends a "Disabled = false" update upon detecting the reconnect.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
expectedPolicy.Disabled = false
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
|
||||
// However, since we manually disabled the channel on Alice's end, the
|
||||
// policy on Alice's end should still be "Disabled = true". Again, note
|
||||
// the asymmetry between manual enable and manual disable!
|
||||
assertEdgeDisabled(true)
|
||||
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
|
||||
// Bob sends a "Disabled = true" update upon detecting the disconnect.
|
||||
expectedPolicy.Disabled = true
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
|
||||
// After restoring automatic channel state management on Alice's end,
|
||||
// BOTH Alice and Bob should set the channel state back to "enabled" on
|
||||
// reconnect.
|
||||
aliceSendReq(routerrpc.ChanStatusAction_AUTO)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
expectedPolicy.Disabled = false
|
||||
assertChannelUpdate(alice, expectedPolicy)
|
||||
assertChannelUpdate(bob, expectedPolicy)
|
||||
assertEdgeDisabled(false)
|
||||
}
|
||||
|
||||
// testUnannouncedChannels checks unannounced channels are not returned by
|
||||
// describeGraph RPC request unless explicitly asked for.
|
||||
func testUnannouncedChannels(ht *lntemp.HarnessTest) {
|
||||
amount := funding.MaxBtcFundingAmount
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Open a channel between Alice and Bob, ensuring the
|
||||
// channel has been opened properly.
|
||||
chanOpenUpdate := ht.OpenChannelAssertStream(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: amount},
|
||||
)
|
||||
|
||||
// Mine 2 blocks, and check that the channel is opened but not yet
|
||||
// announced to the network.
|
||||
ht.MineBlocksAndAssertNumTxes(2, 1)
|
||||
|
||||
// One block is enough to make the channel ready for use, since the
|
||||
// nodes have defaultNumConfs=1 set.
|
||||
fundingChanPoint := ht.WaitForChannelOpenEvent(chanOpenUpdate)
|
||||
|
||||
// Alice should have 1 edge in her graph.
|
||||
ht.AssertNumEdges(alice, 1, true)
|
||||
|
||||
// Channels should not be announced yet, hence Alice should have no
|
||||
// announced edges in her graph.
|
||||
ht.AssertNumEdges(alice, 0, false)
|
||||
|
||||
// Mine 4 more blocks, and check that the channel is now announced.
|
||||
ht.MineBlocks(4)
|
||||
|
||||
// Give the network a chance to learn that auth proof is confirmed.
|
||||
ht.AssertNumEdges(alice, 1, false)
|
||||
|
||||
// Close the channel used during the test.
|
||||
ht.CloseChannel(alice, fundingChanPoint)
|
||||
}
|
||||
|
||||
func testGraphTopologyNotifications(ht *lntemp.HarnessTest) {
|
||||
ht.Run("pinned", func(t *testing.T) {
|
||||
subT := ht.Subtest(t)
|
||||
testGraphTopologyNtfns(subT, true)
|
||||
})
|
||||
ht.Run("unpinned", func(t *testing.T) {
|
||||
subT := ht.Subtest(t)
|
||||
testGraphTopologyNtfns(subT, false)
|
||||
})
|
||||
}
|
||||
|
||||
func testGraphTopologyNtfns(ht *lntemp.HarnessTest, pinned bool) {
|
||||
const chanAmt = funding.MaxBtcFundingAmount
|
||||
|
||||
// Spin up Bob first, since we will need to grab his pubkey when
|
||||
// starting Alice to test pinned syncing.
|
||||
bob := ht.Bob
|
||||
bobInfo := bob.RPC.GetInfo()
|
||||
bobPubkey := bobInfo.IdentityPubkey
|
||||
|
||||
// Restart Bob as he may have leftover announcements from previous
|
||||
// tests, causing the graph to be unsynced.
|
||||
ht.RestartNodeWithExtraArgs(bob, nil)
|
||||
|
||||
// For unpinned syncing, start Alice as usual. Otherwise grab Bob's
|
||||
// pubkey to include in his pinned syncer set.
|
||||
var aliceArgs []string
|
||||
if pinned {
|
||||
aliceArgs = []string{
|
||||
"--numgraphsyncpeers=0",
|
||||
fmt.Sprintf("--gossip.pinned-syncers=%s", bobPubkey),
|
||||
}
|
||||
}
|
||||
|
||||
alice := ht.Alice
|
||||
ht.RestartNodeWithExtraArgs(alice, aliceArgs)
|
||||
|
||||
// Connect Alice and Bob.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// Assert that Bob has the correct sync type before proceeding.
|
||||
if pinned {
|
||||
assertSyncType(ht, alice, bobPubkey, lnrpc.Peer_PINNED_SYNC)
|
||||
} else {
|
||||
assertSyncType(ht, alice, bobPubkey, lnrpc.Peer_ACTIVE_SYNC)
|
||||
}
|
||||
|
||||
// Regardless of syncer type, ensure that both peers report having
|
||||
// completed their initial sync before continuing to make a channel.
|
||||
ht.WaitForGraphSync(alice)
|
||||
|
||||
// Open a new channel between Alice and Bob.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// The channel opening above should have triggered a few notifications
|
||||
// sent to the notification client. We'll expect two channel updates,
|
||||
// and two node announcements.
|
||||
ht.AssertNumChannelUpdates(alice, chanPoint, 2)
|
||||
ht.AssertNumNodeAnns(alice, alice.PubKeyStr, 1)
|
||||
ht.AssertNumNodeAnns(alice, bob.PubKeyStr, 1)
|
||||
|
||||
_, blockHeight := ht.Miner.GetBestBlock()
|
||||
|
||||
// Now we'll test that updates are properly sent after channels are
|
||||
// closed within the network.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
|
||||
// Now that the channel has been closed, we should receive a
|
||||
// notification indicating so.
|
||||
closedChan := ht.AssertTopologyChannelClosed(alice, chanPoint)
|
||||
|
||||
require.Equal(ht, uint32(blockHeight+1), closedChan.ClosedHeight,
|
||||
"close heights of channel mismatch")
|
||||
|
||||
fundingTxid := ht.OutPointFromChannelPoint(chanPoint)
|
||||
closeTxid := ht.OutPointFromChannelPoint(closedChan.ChanPoint)
|
||||
require.EqualValues(ht, fundingTxid, closeTxid,
|
||||
"channel point hash mismatch")
|
||||
|
||||
// For the final portion of the test, we'll ensure that once a new node
|
||||
// appears in the network, the proper notification is dispatched. Note
|
||||
// that a node that does not have any channels open is ignored, so first
|
||||
// we disconnect Alice and Bob, open a channel between Bob and Carol,
|
||||
// and finally connect Alice to Bob again.
|
||||
ht.DisconnectNodes(alice, bob)
|
||||
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
ht.ConnectNodes(bob, carol)
|
||||
chanPoint = ht.OpenChannel(
|
||||
bob, carol, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Reconnect Alice and Bob. This should result in the nodes syncing up
|
||||
// their respective graph state, with the new addition being the
|
||||
// existence of Carol in the graph, and also the channel between Bob
|
||||
// and Carol. Note that we will also receive a node announcement from
|
||||
// Bob, since a node will update its node announcement after a new
|
||||
// channel is opened.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// We should receive an update advertising the newly connected node,
|
||||
// Bob's new node announcement, and the channel between Bob and Carol.
|
||||
ht.AssertNumChannelUpdates(alice, chanPoint, 2)
|
||||
ht.AssertNumNodeAnns(alice, bob.PubKeyStr, 1)
|
||||
|
||||
// Close the channel between Bob and Carol.
|
||||
ht.CloseChannel(bob, chanPoint)
|
||||
}
|
||||
|
||||
// testNodeAnnouncement ensures that when a node is started with one or more
|
||||
// external IP addresses specified on the command line, that those addresses
|
||||
// announced to the network and reported in the network graph.
|
||||
func testNodeAnnouncement(ht *lntemp.HarnessTest) {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
advertisedAddrs := []string{
|
||||
"192.168.1.1:8333",
|
||||
"[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8337",
|
||||
"bkb6azqggsaiskzi.onion:9735",
|
||||
"fomvuglh6h6vcag73xo5t5gv56ombih3zr2xvplkpbfd7wrog4swj" +
|
||||
"wid.onion:1234",
|
||||
}
|
||||
|
||||
var lndArgs []string
|
||||
for _, addr := range advertisedAddrs {
|
||||
lndArgs = append(lndArgs, "--externalip="+addr)
|
||||
}
|
||||
|
||||
dave := ht.NewNode("Dave", lndArgs)
|
||||
|
||||
// We must let Dave have an open channel before he can send a node
|
||||
// announcement, so we open a channel with Bob,
|
||||
ht.ConnectNodes(bob, dave)
|
||||
|
||||
// We'll then go ahead and open a channel between Bob and Dave. This
|
||||
// ensures that Alice receives the node announcement from Bob as part of
|
||||
// the announcement broadcast.
|
||||
chanPoint := ht.OpenChannel(
|
||||
bob, dave, lntemp.OpenChannelParams{Amt: 1000000},
|
||||
)
|
||||
|
||||
assertAddrs := func(addrsFound []string, targetAddrs ...string) {
|
||||
addrs := make(map[string]struct{}, len(addrsFound))
|
||||
for _, addr := range addrsFound {
|
||||
addrs[addr] = struct{}{}
|
||||
}
|
||||
|
||||
for _, addr := range targetAddrs {
|
||||
_, ok := addrs[addr]
|
||||
require.True(ht, ok, "address %v not found in node "+
|
||||
"announcement", addr)
|
||||
}
|
||||
}
|
||||
// We'll then wait for Alice to receive Dave's node announcement
|
||||
// including the expected advertised addresses from Bob since they
|
||||
// should already be connected.
|
||||
allUpdates := ht.AssertNumNodeAnns(alice, dave.PubKeyStr, 1)
|
||||
nodeUpdate := allUpdates[len(allUpdates)-1]
|
||||
assertAddrs(nodeUpdate.Addresses, advertisedAddrs...)
|
||||
|
||||
// Close the channel between Bob and Dave.
|
||||
ht.CloseChannel(bob, chanPoint)
|
||||
}
|
||||
|
||||
// testUpdateNodeAnnouncement ensures that the RPC endpoint validates
|
||||
// the requests correctly and that the new node announcement is brodcasted
|
||||
// with the right information after updating our node.
|
||||
func testUpdateNodeAnnouncement(ht *lntemp.HarnessTest) {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
var lndArgs []string
|
||||
|
||||
// Add some exta addresses to the default ones.
|
||||
extraAddrs := []string{
|
||||
"192.168.1.1:8333",
|
||||
"[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8337",
|
||||
"bkb6azqggsaiskzi.onion:9735",
|
||||
"fomvuglh6h6vcag73xo5t5gv56ombih3zr2xvplkpbfd7wrog4swj" +
|
||||
"wid.onion:1234",
|
||||
}
|
||||
for _, addr := range extraAddrs {
|
||||
lndArgs = append(lndArgs, "--externalip="+addr)
|
||||
}
|
||||
dave := ht.NewNode("Dave", lndArgs)
|
||||
|
||||
// assertNodeAnn is a helper closure that checks a given node update
|
||||
// from Dave is seen by Alice.
|
||||
assertNodeAnn := func(expected *lnrpc.NodeUpdate) {
|
||||
err := wait.NoError(func() error {
|
||||
// Get a list of node updates seen by Alice.
|
||||
updates := alice.Watcher.GetNodeUpdates(dave.PubKeyStr)
|
||||
|
||||
// Check at least one of the updates matches the given
|
||||
// node update.
|
||||
for _, update := range updates {
|
||||
err := compareNodeAnns(update, expected)
|
||||
// Found a match, return nil.
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// We've check all the updates and no match found.
|
||||
return fmt.Errorf("alice didn't see the update: %v",
|
||||
expected)
|
||||
}, defaultTimeout)
|
||||
|
||||
require.NoError(ht, err, "assertNodeAnn failed")
|
||||
}
|
||||
|
||||
// Get dave default information so we can compare it lately with the
|
||||
// brodcasted updates.
|
||||
resp := dave.RPC.GetInfo()
|
||||
defaultAddrs := make([]*lnrpc.NodeAddress, 0, len(resp.Uris))
|
||||
for _, uri := range resp.GetUris() {
|
||||
values := strings.Split(uri, "@")
|
||||
defaultAddrs = append(
|
||||
defaultAddrs, &lnrpc.NodeAddress{
|
||||
Addr: values[1],
|
||||
Network: "tcp",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// This feature bit is used to test that our endpoint sets/unsets
|
||||
// feature bits properly. If the current FeatureBit is set by default
|
||||
// update this one for another one unset by default at random.
|
||||
featureBit := lnrpc.FeatureBit_WUMBO_CHANNELS_REQ
|
||||
featureIdx := uint32(featureBit)
|
||||
_, ok := resp.Features[featureIdx]
|
||||
require.False(ht, ok, "unexpected feature bit enabled by default")
|
||||
|
||||
defaultDaveNodeAnn := &lnrpc.NodeUpdate{
|
||||
Alias: resp.Alias,
|
||||
Color: resp.Color,
|
||||
NodeAddresses: defaultAddrs,
|
||||
}
|
||||
|
||||
// Dave must have an open channel before he can send a node
|
||||
// announcement, so we open a channel with Bob.
|
||||
ht.ConnectNodes(bob, dave)
|
||||
|
||||
// Go ahead and open a channel between Bob and Dave. This
|
||||
// ensures that Alice receives the node announcement from Bob as part of
|
||||
// the announcement broadcast.
|
||||
chanPoint := ht.OpenChannel(
|
||||
bob, dave, lntemp.OpenChannelParams{
|
||||
Amt: 1000000,
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for Alice to receive dave's node announcement with the default
|
||||
// values.
|
||||
assertNodeAnn(defaultDaveNodeAnn)
|
||||
|
||||
// We cannot differentiate between requests with Alias = "" and
|
||||
// requests that do not provide that field. If a user sets Alias = ""
|
||||
// in the request the field will simply be ignored. The request must
|
||||
// fail because no modifiers are applied.
|
||||
invalidNodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{Alias: ""}
|
||||
dave.RPC.UpdateNodeAnnouncementErr(invalidNodeAnnReq)
|
||||
|
||||
// Alias too long.
|
||||
invalidNodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{
|
||||
Alias: strings.Repeat("a", 50),
|
||||
}
|
||||
dave.RPC.UpdateNodeAnnouncementErr(invalidNodeAnnReq)
|
||||
|
||||
// Update Node.
|
||||
newAlias := "new-alias"
|
||||
newColor := "#2288ee"
|
||||
|
||||
newAddresses := []string{
|
||||
"192.168.1.10:8333",
|
||||
"192.168.1.11:8333",
|
||||
}
|
||||
|
||||
updateAddressActions := []*peersrpc.UpdateAddressAction{
|
||||
{
|
||||
Action: peersrpc.UpdateAction_ADD,
|
||||
Address: newAddresses[0],
|
||||
},
|
||||
{
|
||||
Action: peersrpc.UpdateAction_ADD,
|
||||
Address: newAddresses[1],
|
||||
},
|
||||
{
|
||||
Action: peersrpc.UpdateAction_REMOVE,
|
||||
Address: defaultAddrs[0].Addr,
|
||||
},
|
||||
}
|
||||
|
||||
updateFeatureActions := []*peersrpc.UpdateFeatureAction{
|
||||
{
|
||||
Action: peersrpc.UpdateAction_ADD,
|
||||
FeatureBit: featureBit,
|
||||
},
|
||||
}
|
||||
|
||||
nodeAnnReq := &peersrpc.NodeAnnouncementUpdateRequest{
|
||||
Alias: newAlias,
|
||||
Color: newColor,
|
||||
AddressUpdates: updateAddressActions,
|
||||
FeatureUpdates: updateFeatureActions,
|
||||
}
|
||||
|
||||
response := dave.RPC.UpdateNodeAnnouncement(nodeAnnReq)
|
||||
|
||||
expectedOps := map[string]int{
|
||||
"features": 1,
|
||||
"color": 1,
|
||||
"alias": 1,
|
||||
"addresses": 3,
|
||||
}
|
||||
assertUpdateNodeAnnouncementResponse(ht, response, expectedOps)
|
||||
|
||||
newNodeAddresses := []*lnrpc.NodeAddress{}
|
||||
// We removed the first address.
|
||||
newNodeAddresses = append(newNodeAddresses, defaultAddrs[1:]...)
|
||||
newNodeAddresses = append(
|
||||
newNodeAddresses,
|
||||
&lnrpc.NodeAddress{Addr: newAddresses[0], Network: "tcp"},
|
||||
&lnrpc.NodeAddress{Addr: newAddresses[1], Network: "tcp"},
|
||||
)
|
||||
|
||||
// After updating the node we expect the update to contain
|
||||
// the requested color, requested alias and the new added addresses.
|
||||
newDaveNodeAnn := &lnrpc.NodeUpdate{
|
||||
Alias: newAlias,
|
||||
Color: newColor,
|
||||
NodeAddresses: newNodeAddresses,
|
||||
}
|
||||
|
||||
// We'll then wait for Alice to receive dave's node announcement
|
||||
// with the new values.
|
||||
assertNodeAnn(newDaveNodeAnn)
|
||||
|
||||
// Check that the feature bit was set correctly.
|
||||
resp = dave.RPC.GetInfo()
|
||||
_, ok = resp.Features[featureIdx]
|
||||
require.True(ht, ok, "failed to set feature bit")
|
||||
|
||||
// Check that we cannot set a feature bit that is already set.
|
||||
nodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{
|
||||
FeatureUpdates: updateFeatureActions,
|
||||
}
|
||||
dave.RPC.UpdateNodeAnnouncementErr(nodeAnnReq)
|
||||
|
||||
// Check that we can unset feature bits.
|
||||
updateFeatureActions = []*peersrpc.UpdateFeatureAction{
|
||||
{
|
||||
Action: peersrpc.UpdateAction_REMOVE,
|
||||
FeatureBit: featureBit,
|
||||
},
|
||||
}
|
||||
|
||||
nodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{
|
||||
FeatureUpdates: updateFeatureActions,
|
||||
}
|
||||
response = dave.RPC.UpdateNodeAnnouncement(nodeAnnReq)
|
||||
|
||||
expectedOps = map[string]int{
|
||||
"features": 1,
|
||||
}
|
||||
assertUpdateNodeAnnouncementResponse(ht, response, expectedOps)
|
||||
|
||||
resp = dave.RPC.GetInfo()
|
||||
_, ok = resp.Features[featureIdx]
|
||||
require.False(ht, ok, "failed to unset feature bit")
|
||||
|
||||
// Check that we cannot unset a feature bit that is already unset.
|
||||
nodeAnnReq = &peersrpc.NodeAnnouncementUpdateRequest{
|
||||
FeatureUpdates: updateFeatureActions,
|
||||
}
|
||||
dave.RPC.UpdateNodeAnnouncementErr(nodeAnnReq)
|
||||
|
||||
// Close the channel between Bob and Dave.
|
||||
ht.CloseChannel(bob, chanPoint)
|
||||
}
|
||||
|
||||
// assertSyncType asserts that the peer has an expected syncType.
|
||||
//
|
||||
// NOTE: only made for tests in this file.
|
||||
func assertSyncType(ht *lntemp.HarnessTest, hn *node.HarnessNode,
|
||||
peer string, syncType lnrpc.Peer_SyncType) {
|
||||
|
||||
resp := hn.RPC.ListPeers()
|
||||
for _, rpcPeer := range resp.Peers {
|
||||
if rpcPeer.PubKey != peer {
|
||||
continue
|
||||
}
|
||||
|
||||
require.Equal(ht, syncType, rpcPeer.SyncType)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ht.Fatalf("unable to find peer: %s", peer)
|
||||
}
|
||||
|
||||
// compareNodeAnns compares that two node announcements match or returns an
|
||||
// error.
|
||||
//
|
||||
// NOTE: only used for tests in this file.
|
||||
func compareNodeAnns(n1, n2 *lnrpc.NodeUpdate) error {
|
||||
// Alias should match.
|
||||
if n1.Alias != n2.Alias {
|
||||
return fmt.Errorf("alias not match")
|
||||
}
|
||||
|
||||
// Color should match.
|
||||
if n1.Color != n2.Color {
|
||||
return fmt.Errorf("color not match")
|
||||
}
|
||||
|
||||
// NodeAddresses should match.
|
||||
if len(n1.NodeAddresses) != len(n2.NodeAddresses) {
|
||||
return fmt.Errorf("node addresses don't match")
|
||||
}
|
||||
|
||||
addrs := make(map[string]struct{}, len(n1.NodeAddresses))
|
||||
for _, nodeAddr := range n1.NodeAddresses {
|
||||
addrs[nodeAddr.Addr] = struct{}{}
|
||||
}
|
||||
|
||||
for _, nodeAddr := range n2.NodeAddresses {
|
||||
if _, ok := addrs[nodeAddr.Addr]; !ok {
|
||||
return fmt.Errorf("address %v not found in node "+
|
||||
"announcement", nodeAddr.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// assertUpdateNodeAnnouncementResponse is a helper function to assert
|
||||
// the response expected values.
|
||||
//
|
||||
// NOTE: only used for tests in this file.
|
||||
func assertUpdateNodeAnnouncementResponse(ht *lntemp.HarnessTest,
|
||||
response *peersrpc.NodeAnnouncementUpdateResponse,
|
||||
expectedOps map[string]int) {
|
||||
|
||||
require.Equal(
|
||||
ht, len(response.Ops), len(expectedOps),
|
||||
"unexpected number of Ops updating dave's node announcement",
|
||||
)
|
||||
|
||||
ops := make(map[string]int, len(response.Ops))
|
||||
for _, op := range response.Ops {
|
||||
ops[op.Entity] = len(op.Actions)
|
||||
}
|
||||
|
||||
for k, v := range expectedOps {
|
||||
if v != ops[k] {
|
||||
ht.Fatalf("unexpected number of actions for operation "+
|
||||
"%s: got %d wanted %d", k, ops[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,853 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// assertPolicyUpdate checks that a given policy update has been received by a
|
||||
// list of given nodes.
|
||||
// TODO(yy): delete.
|
||||
func assertPolicyUpdate(t *harnessTest, nodes []*lntest.HarnessNode,
|
||||
advertisingNode string, policy *lnrpc.RoutingPolicy,
|
||||
chanPoint *lnrpc.ChannelPoint) {
|
||||
|
||||
for _, node := range nodes {
|
||||
assertChannelPolicyUpdate(
|
||||
t.t, node, advertisingNode, policy, chanPoint, false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// testUpdateChannelPolicy tests that policy updates made to a channel
|
||||
// gets propagated to other nodes in the network.
|
||||
func testUpdateChannelPolicy(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
defaultFeeBase = 1000
|
||||
defaultFeeRate = 1
|
||||
defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta
|
||||
defaultMinHtlc = 1000
|
||||
)
|
||||
defaultMaxHtlc := calculateMaxHtlc(funding.MaxBtcFundingAmount)
|
||||
|
||||
chanAmt := funding.MaxBtcFundingAmount
|
||||
pushAmt := chanAmt / 2
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Create a channel Alice->Bob.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// We add all the nodes' update channels to a slice, such that we can
|
||||
// make sure they all receive the expected updates.
|
||||
nodes := []*node.HarnessNode{alice, bob}
|
||||
|
||||
// Alice and Bob should see each other's ChannelUpdates, advertising the
|
||||
// default routing policies.
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: defaultFeeBase,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
}
|
||||
|
||||
assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint)
|
||||
assertNodesPolicyUpdate(ht, nodes, bob, expectedPolicy, chanPoint)
|
||||
|
||||
// They should now know about the default policies.
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicy(
|
||||
node, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
node, bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
}
|
||||
|
||||
// Create Carol with options to rate limit channel updates up to 2 per
|
||||
// day, and create a new channel Bob->Carol.
|
||||
carol := ht.NewNode(
|
||||
"Carol", []string{
|
||||
"--gossip.max-channel-update-burst=2",
|
||||
"--gossip.channel-update-interval=24h",
|
||||
},
|
||||
)
|
||||
ht.ConnectNodes(carol, bob)
|
||||
nodes = append(nodes, carol)
|
||||
|
||||
// Send some coins to Carol that can be used for channel funding.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// Open the channel Carol->Bob with a custom min_htlc value set. Since
|
||||
// Carol is opening the channel, she will require Bob to not forward
|
||||
// HTLCs smaller than this value, and hence he should advertise it as
|
||||
// part of his ChannelUpdate.
|
||||
const customMinHtlc = 5000
|
||||
chanPoint2 := ht.OpenChannel(
|
||||
carol, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
MinHtlc: customMinHtlc,
|
||||
},
|
||||
)
|
||||
|
||||
expectedPolicyBob := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: defaultFeeBase,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: customMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
}
|
||||
expectedPolicyCarol := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: defaultFeeBase,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
}
|
||||
|
||||
assertNodesPolicyUpdate(ht, nodes, bob, expectedPolicyBob, chanPoint2)
|
||||
assertNodesPolicyUpdate(
|
||||
ht, nodes, carol, expectedPolicyCarol, chanPoint2,
|
||||
)
|
||||
|
||||
// Check that all nodes now know about the updated policies.
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicy(
|
||||
node, bob.PubKeyStr, expectedPolicyBob, chanPoint2,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
node, carol.PubKeyStr, expectedPolicyCarol, chanPoint2,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure Alice and Carol have seen each other's channels.
|
||||
ht.AssertTopologyChannelOpen(alice, chanPoint2)
|
||||
ht.AssertTopologyChannelOpen(carol, chanPoint)
|
||||
|
||||
// First we'll try to send a payment from Alice to Carol with an amount
|
||||
// less than the min_htlc value required by Carol. This payment should
|
||||
// fail, as the channel Bob->Carol cannot carry HTLCs this small.
|
||||
payAmt := btcutil.Amount(4)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
resp := carol.RPC.AddInvoice(invoice)
|
||||
|
||||
// Alice knows about the channel policy of Carol and should therefore
|
||||
// not be able to find a path during routing.
|
||||
payReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: resp.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
ht.SendPaymentAssertFail(
|
||||
alice, payReq,
|
||||
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
|
||||
)
|
||||
|
||||
// Now we try to send a payment over the channel with a value too low
|
||||
// to be accepted. First we query for a route to route a payment of
|
||||
// 5000 mSAT, as this is accepted.
|
||||
payAmt = btcutil.Amount(5)
|
||||
routesReq := &lnrpc.QueryRoutesRequest{
|
||||
PubKey: carol.PubKeyStr,
|
||||
Amt: int64(payAmt),
|
||||
FinalCltvDelta: defaultTimeLockDelta,
|
||||
}
|
||||
routes := alice.RPC.QueryRoutes(routesReq)
|
||||
require.Len(ht, routes.Routes, 1)
|
||||
|
||||
// We change the route to carry a payment of 4000 mSAT instead of 5000
|
||||
// mSAT.
|
||||
payAmt = btcutil.Amount(4)
|
||||
amtSat := int64(payAmt)
|
||||
amtMSat := int64(lnwire.NewMSatFromSatoshis(payAmt))
|
||||
routes.Routes[0].Hops[0].AmtToForward = amtSat // nolint:staticcheck
|
||||
routes.Routes[0].Hops[0].AmtToForwardMsat = amtMSat
|
||||
routes.Routes[0].Hops[1].AmtToForward = amtSat // nolint:staticcheck
|
||||
routes.Routes[0].Hops[1].AmtToForwardMsat = amtMSat
|
||||
|
||||
// Send the payment with the modified value.
|
||||
alicePayStream := alice.RPC.SendToRoute()
|
||||
|
||||
sendReq := &lnrpc.SendToRouteRequest{
|
||||
PaymentHash: resp.RHash,
|
||||
Route: routes.Routes[0],
|
||||
}
|
||||
err := alicePayStream.Send(sendReq)
|
||||
require.NoError(ht, err, "unable to send payment")
|
||||
|
||||
// We expect this payment to fail, and that the min_htlc value is
|
||||
// communicated back to us, since the attempted HTLC value was too low.
|
||||
sendResp, err := ht.ReceiveSendToRouteUpdate(alicePayStream)
|
||||
require.NoError(ht, err, "unable to receive payment stream")
|
||||
|
||||
// Expected as part of the error message.
|
||||
substrs := []string{
|
||||
"AmountBelowMinimum",
|
||||
"HtlcMinimumMsat: (lnwire.MilliSatoshi) 5000 mSAT",
|
||||
}
|
||||
for _, s := range substrs {
|
||||
require.Contains(ht, sendResp.PaymentError, s)
|
||||
}
|
||||
|
||||
// Make sure sending using the original value succeeds.
|
||||
payAmt = btcutil.Amount(5)
|
||||
amtSat = int64(payAmt)
|
||||
amtMSat = int64(lnwire.NewMSatFromSatoshis(payAmt))
|
||||
routes.Routes[0].Hops[0].AmtToForward = amtSat // nolint:staticcheck
|
||||
routes.Routes[0].Hops[0].AmtToForwardMsat = amtMSat
|
||||
routes.Routes[0].Hops[1].AmtToForward = amtSat // nolint:staticcheck
|
||||
routes.Routes[0].Hops[1].AmtToForwardMsat = amtMSat
|
||||
|
||||
// Manually set the MPP payload a new for each payment since
|
||||
// the payment addr will change with each invoice, although we
|
||||
// can re-use the route itself.
|
||||
route := routes.Routes[0]
|
||||
route.Hops[len(route.Hops)-1].TlvPayload = true
|
||||
route.Hops[len(route.Hops)-1].MppRecord = &lnrpc.MPPRecord{
|
||||
PaymentAddr: resp.PaymentAddr,
|
||||
TotalAmtMsat: amtMSat,
|
||||
}
|
||||
|
||||
sendReq = &lnrpc.SendToRouteRequest{
|
||||
PaymentHash: resp.RHash,
|
||||
Route: route,
|
||||
}
|
||||
|
||||
err = alicePayStream.Send(sendReq)
|
||||
require.NoError(ht, err, "unable to send payment")
|
||||
|
||||
sendResp, err = ht.ReceiveSendToRouteUpdate(alicePayStream)
|
||||
require.NoError(ht, err, "unable to receive payment stream")
|
||||
require.Empty(ht, sendResp.PaymentError, "expected payment to succeed")
|
||||
|
||||
// With our little cluster set up, we'll update the fees and the max
|
||||
// htlc size for the Bob side of the Alice->Bob channel, and make sure
|
||||
// all nodes learn about it.
|
||||
baseFee := int64(1500)
|
||||
feeRate := int64(12)
|
||||
timeLockDelta := uint32(66)
|
||||
maxHtlc := uint64(500000)
|
||||
|
||||
expectedPolicy = &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: baseFee,
|
||||
FeeRateMilliMsat: testFeeBase * feeRate,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
}
|
||||
|
||||
req := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: float64(feeRate),
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
ChanPoint: chanPoint,
|
||||
},
|
||||
}
|
||||
bob.RPC.UpdateChannelPolicy(req)
|
||||
|
||||
// Wait for all nodes to have seen the policy update done by Bob.
|
||||
assertNodesPolicyUpdate(ht, nodes, bob, expectedPolicy, chanPoint)
|
||||
|
||||
// Check that all nodes now know about Bob's updated policy.
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicy(
|
||||
node, bob.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
}
|
||||
|
||||
// Now that all nodes have received the new channel update, we'll try
|
||||
// to send a payment from Alice to Carol to ensure that Alice has
|
||||
// internalized this fee update. This shouldn't affect the route that
|
||||
// Alice takes though: we updated the Alice -> Bob channel and she
|
||||
// doesn't pay for transit over that channel as it's direct.
|
||||
// Note that the payment amount is >= the min_htlc value for the
|
||||
// channel Bob->Carol, so it should successfully be forwarded.
|
||||
payAmt = btcutil.Amount(5)
|
||||
invoice = &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
resp = carol.RPC.AddInvoice(invoice)
|
||||
|
||||
ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest})
|
||||
|
||||
// We'll now open a channel from Alice directly to Carol.
|
||||
ht.ConnectNodes(alice, carol)
|
||||
chanPoint3 := ht.OpenChannel(
|
||||
alice, carol, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// Make sure Bob knows this channel.
|
||||
ht.AssertTopologyChannelOpen(bob, chanPoint3)
|
||||
|
||||
// Make a global update, and check that both channels' new policies get
|
||||
// propagated.
|
||||
baseFee = int64(800)
|
||||
feeRate = int64(123)
|
||||
timeLockDelta = uint32(22)
|
||||
maxHtlc *= 2
|
||||
|
||||
expectedPolicy.FeeBaseMsat = baseFee
|
||||
expectedPolicy.FeeRateMilliMsat = testFeeBase * feeRate
|
||||
expectedPolicy.TimeLockDelta = timeLockDelta
|
||||
expectedPolicy.MaxHtlcMsat = maxHtlc
|
||||
|
||||
req = &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: float64(feeRate),
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
}
|
||||
req.Scope = &lnrpc.PolicyUpdateRequest_Global{}
|
||||
alice.RPC.UpdateChannelPolicy(req)
|
||||
|
||||
// Wait for all nodes to have seen the policy updates for both of
|
||||
// Alice's channels.
|
||||
assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint)
|
||||
assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint3)
|
||||
|
||||
// And finally check that all nodes remembers the policy update they
|
||||
// received.
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicy(
|
||||
node, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
node, alice.PubKeyStr, expectedPolicy, chanPoint3,
|
||||
)
|
||||
}
|
||||
|
||||
// Now, to test that Carol is properly rate limiting incoming updates,
|
||||
// we'll send two more update from Alice. Carol should accept the first,
|
||||
// but not the second, as she only allows two updates per day and a day
|
||||
// has yet to elapse from the previous update.
|
||||
|
||||
// assertAliceAndBob is a helper closure which updates Alice's policy
|
||||
// and asserts that both Alice and Bob have heard and updated the
|
||||
// policy in their graph.
|
||||
assertAliceAndBob := func(req *lnrpc.PolicyUpdateRequest,
|
||||
expectedPolicy *lnrpc.RoutingPolicy) {
|
||||
|
||||
alice.RPC.UpdateChannelPolicy(req)
|
||||
|
||||
// Wait for all nodes to have seen the policy updates for both
|
||||
// of Alice's channels. Carol will not see the last update as
|
||||
// the limit has been reached.
|
||||
assertNodesPolicyUpdate(
|
||||
ht, []*node.HarnessNode{alice, bob},
|
||||
alice, expectedPolicy, chanPoint,
|
||||
)
|
||||
assertNodesPolicyUpdate(
|
||||
ht, []*node.HarnessNode{alice, bob},
|
||||
alice, expectedPolicy, chanPoint3,
|
||||
)
|
||||
|
||||
// Check that all nodes remember the policy update
|
||||
// they received.
|
||||
ht.AssertChannelPolicy(
|
||||
alice, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
alice, alice.PubKeyStr, expectedPolicy, chanPoint3,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
bob, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
bob, alice.PubKeyStr, expectedPolicy, chanPoint3,
|
||||
)
|
||||
}
|
||||
|
||||
// Double the base fee and attach to the policy.
|
||||
baseFee1 := baseFee * 2
|
||||
expectedPolicy.FeeBaseMsat = baseFee1
|
||||
req.BaseFeeMsat = baseFee1
|
||||
assertAliceAndBob(req, expectedPolicy)
|
||||
|
||||
// Check that Carol has both heard the policy and updated it in her
|
||||
// graph.
|
||||
assertNodesPolicyUpdate(
|
||||
ht, []*node.HarnessNode{carol},
|
||||
alice, expectedPolicy, chanPoint,
|
||||
)
|
||||
assertNodesPolicyUpdate(
|
||||
ht, []*node.HarnessNode{carol},
|
||||
alice, expectedPolicy, chanPoint3,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
carol, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
carol, alice.PubKeyStr, expectedPolicy, chanPoint3,
|
||||
)
|
||||
|
||||
// Double the base fee and attach to the policy.
|
||||
baseFee2 := baseFee1 * 2
|
||||
expectedPolicy.FeeBaseMsat = baseFee2
|
||||
req.BaseFeeMsat = baseFee2
|
||||
assertAliceAndBob(req, expectedPolicy)
|
||||
|
||||
// Since Carol didn't receive the last update, she still has Alice's
|
||||
// old policy. We validate this by checking the base fee is the older
|
||||
// one.
|
||||
expectedPolicy.FeeBaseMsat = baseFee1
|
||||
ht.AssertChannelPolicy(
|
||||
carol, alice.PubKeyStr, expectedPolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
carol, alice.PubKeyStr, expectedPolicy, chanPoint3,
|
||||
)
|
||||
|
||||
// Close all channels.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
ht.CloseChannel(bob, chanPoint2)
|
||||
ht.CloseChannel(alice, chanPoint3)
|
||||
}
|
||||
|
||||
// testSendUpdateDisableChannel ensures that a channel update with the disable
|
||||
// flag set is sent once a channel has been either unilaterally or cooperatively
|
||||
// closed.
|
||||
//
|
||||
// NOTE: this test can be flaky as we are testing the chan-enable-timeout and
|
||||
// chan-disable-timeout flags here. For instance, if some operations take more
|
||||
// than 6 seconds to finish, the channel will be marked as disabled, thus a
|
||||
// following operation will fail if it relies on the channel being enabled.
|
||||
func testSendUpdateDisableChannel(ht *lntemp.HarnessTest) {
|
||||
const chanAmt = 100000
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Create a new node Eve, which will be restarted later with a config
|
||||
// that has an inactive channel timeout of just 6 seconds (down from
|
||||
// the default 20m). It will be used to test channel updates for
|
||||
// channels going inactive.
|
||||
//
|
||||
// NOTE: we don't create Eve with the chan-disable-timeout here because
|
||||
// the following channel openings might take longer than that timeout
|
||||
// value, which will cause the channel Eve=>Carol being marked as
|
||||
// disabled.
|
||||
eve := ht.NewNode("Eve", nil)
|
||||
|
||||
// Create a new node Carol, which will later be restarted with the same
|
||||
// config as Eve's.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Launch a node for Dave which will connect to Bob in order to receive
|
||||
// graph updates from. This will ensure that the channel updates are
|
||||
// propagated throughout the network.
|
||||
dave := ht.NewNode("Dave", nil)
|
||||
|
||||
// We will start our test by creating the following topology,
|
||||
// Alice --- Bob --- Dave
|
||||
// | |
|
||||
// Carol --- Eve
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.ConnectNodes(alice, carol)
|
||||
ht.ConnectNodes(bob, dave)
|
||||
ht.ConnectNodes(eve, carol)
|
||||
|
||||
// Connect Eve and Bob using a persistent connection. Later after Eve
|
||||
// is restarted, they will connect again automatically.
|
||||
ht.ConnectNodesPerm(bob, eve)
|
||||
|
||||
// Give Eve some coins.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, eve)
|
||||
|
||||
// We now proceed to open channels: Alice=>Bob, Alice=>Carol and
|
||||
// Eve=>Carol.
|
||||
p := lntemp.OpenChannelParams{Amt: chanAmt}
|
||||
reqs := []*lntemp.OpenChannelRequest{
|
||||
{Local: alice, Remote: bob, Param: p},
|
||||
{Local: alice, Remote: carol, Param: p},
|
||||
{Local: eve, Remote: carol, Param: p},
|
||||
}
|
||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
||||
|
||||
// Extract channel points from the response.
|
||||
chanPointAliceBob := resp[0]
|
||||
chanPointAliceCarol := resp[1]
|
||||
chanPointEveCarol := resp[2]
|
||||
|
||||
// We will use 10 seconds as the disable timeout.
|
||||
chanDisableTimeout := 10
|
||||
chanEnableTimeout := 5
|
||||
|
||||
// waitChanDisabled is a helper closure to wait the chanDisableTimeout
|
||||
// seconds such that the channel disable logic is taking effect.
|
||||
waitChanDisabled := func() {
|
||||
time.Sleep(time.Duration(chanDisableTimeout) * time.Second)
|
||||
}
|
||||
|
||||
// With the channels open, we now restart Carol and Eve to use
|
||||
// customized timeout values.
|
||||
nodeCfg := []string{
|
||||
"--minbackoff=60s",
|
||||
fmt.Sprintf("--chan-enable-timeout=%ds", chanEnableTimeout),
|
||||
fmt.Sprintf("--chan-disable-timeout=%ds", chanDisableTimeout),
|
||||
"--chan-status-sample-interval=.5s",
|
||||
}
|
||||
ht.RestartNodeWithExtraArgs(carol, nodeCfg)
|
||||
ht.RestartNodeWithExtraArgs(eve, nodeCfg)
|
||||
|
||||
// Dave should know all the channels.
|
||||
ht.AssertTopologyChannelOpen(dave, chanPointAliceBob)
|
||||
ht.AssertTopologyChannelOpen(dave, chanPointAliceCarol)
|
||||
ht.AssertTopologyChannelOpen(dave, chanPointEveCarol)
|
||||
|
||||
// We should expect to see a channel update with the default routing
|
||||
// policy, except that it should indicate the channel is disabled.
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: int64(chainreg.DefaultBitcoinBaseFeeMSat),
|
||||
FeeRateMilliMsat: int64(chainreg.DefaultBitcoinFeeRate),
|
||||
TimeLockDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
MinHtlc: 1000, // default value
|
||||
MaxHtlcMsat: calculateMaxHtlc(chanAmt),
|
||||
Disabled: true,
|
||||
}
|
||||
|
||||
// assertPolicyUpdate checks that the required policy update has
|
||||
// happened on the given node.
|
||||
assertPolicyUpdate := func(node *node.HarnessNode,
|
||||
policy *lnrpc.RoutingPolicy, chanPoint *lnrpc.ChannelPoint,
|
||||
numUpdates int) {
|
||||
|
||||
ht.AssertNumPolicyUpdates(dave, chanPoint, node, numUpdates)
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
dave, node, policy, chanPoint, false,
|
||||
)
|
||||
}
|
||||
|
||||
// Let Carol go offline. Since Eve has an inactive timeout of 6s, we
|
||||
// expect her to send an update disabling the channel.
|
||||
restartCarol := ht.SuspendNode(carol)
|
||||
|
||||
// We expect to see a total of 2 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Eve using the route
|
||||
// Eve->Bob->Dave.
|
||||
waitChanDisabled()
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 2)
|
||||
|
||||
// We restart Carol. Since the channel now becomes active again, Eve
|
||||
// should send a ChannelUpdate setting the channel no longer disabled.
|
||||
require.NoError(ht, restartCarol(), "unable to restart carol")
|
||||
|
||||
expectedPolicy.Disabled = false
|
||||
// We expect to see a total of 3 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Eve using the route
|
||||
// Eve->Bob->Dave.
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 3)
|
||||
|
||||
// Wait until Carol and Eve are reconnected before we disconnect them
|
||||
// again.
|
||||
ht.EnsureConnected(eve, carol)
|
||||
|
||||
// Now we'll test a long disconnection. Disconnect Carol and Eve and
|
||||
// ensure they both detect each other as disabled. Their min backoffs
|
||||
// are high enough to not interfere with disabling logic.
|
||||
ht.DisconnectNodes(carol, eve)
|
||||
|
||||
// Wait for a disable from both Carol and Eve to come through.
|
||||
expectedPolicy.Disabled = true
|
||||
// We expect to see a total of 4 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Eve using the route
|
||||
// Eve->Bob->Dave.
|
||||
waitChanDisabled()
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 4)
|
||||
|
||||
// Because Carol has restarted twice before, depending on how much time
|
||||
// it has taken, she might mark the channel disabled and enable it
|
||||
// multiple times. Thus we could see a total of 2 or 4 or 6 channel
|
||||
// policy updates from the channel Carol <-> Eve and advertised by
|
||||
// Carol using the route Carol->Alice->Bob->Dave.
|
||||
//
|
||||
// Assume there are 2 channel policy updates from Carol, and update it
|
||||
// if more has found
|
||||
numCarol := 2
|
||||
op := ht.OutPointFromChannelPoint(chanPointEveCarol)
|
||||
policyMap := dave.Watcher.GetPolicyUpdates(op)
|
||||
nodePolicy, ok := policyMap[carol.PubKeyStr]
|
||||
switch {
|
||||
case !ok:
|
||||
break
|
||||
case len(nodePolicy) > 2:
|
||||
numCarol = 4
|
||||
case len(nodePolicy) > 4:
|
||||
numCarol = 6
|
||||
}
|
||||
assertPolicyUpdate(carol, expectedPolicy, chanPointEveCarol, numCarol)
|
||||
|
||||
// Reconnect Carol and Eve, this should cause them to reenable the
|
||||
// channel from both ends after a short delay.
|
||||
ht.EnsureConnected(carol, eve)
|
||||
|
||||
expectedPolicy.Disabled = false
|
||||
// We expect to see a total of 5 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Eve using the route
|
||||
// Eve->Bob->Dave.
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 5)
|
||||
// We expect to see a total of 3 or 5 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Carol using the route
|
||||
// Carol->Alice->Bob->Dave.
|
||||
numCarol++
|
||||
assertPolicyUpdate(carol, expectedPolicy, chanPointEveCarol, numCarol)
|
||||
|
||||
// Now we'll test a short disconnection. Disconnect Carol and Eve, then
|
||||
// reconnect them after one second so that their scheduled disables are
|
||||
// aborted. One second is twice the status sample interval, so this
|
||||
// should allow for the disconnect to be detected, but still leave time
|
||||
// to cancel the announcement before the 6 second inactive timeout is
|
||||
// hit.
|
||||
ht.DisconnectNodes(carol, eve)
|
||||
time.Sleep(time.Second)
|
||||
ht.EnsureConnected(eve, carol)
|
||||
|
||||
// Since the disable should have been canceled by both Carol and Eve,
|
||||
// we expect no channel updates to appear on the network, which means
|
||||
// we expect the polices stay unchanged(Disable == false).
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 5)
|
||||
assertPolicyUpdate(carol, expectedPolicy, chanPointEveCarol, numCarol)
|
||||
|
||||
// Close Alice's channels with Bob and Carol cooperatively and
|
||||
// unilaterally respectively. Note that the CloseChannel will mine a
|
||||
// block and check that the closing transaction can be found in both
|
||||
// the mempool and the block.
|
||||
ht.CloseChannel(alice, chanPointAliceBob)
|
||||
ht.ForceCloseChannel(alice, chanPointAliceCarol)
|
||||
|
||||
// Now that the channel close processes have been started, we should
|
||||
// receive an update marking each as disabled.
|
||||
expectedPolicy.Disabled = true
|
||||
// We expect to see a total of 2 channel policy updates from the
|
||||
// channel Alice <-> Bob and advertised by Alice using the route
|
||||
// Alice->Bob->Dave.
|
||||
assertPolicyUpdate(alice, expectedPolicy, chanPointAliceBob, 2)
|
||||
// We expect to see a total of 2 channel policy updates from the
|
||||
// channel Alice <-> Carol and advertised by Alice using the route
|
||||
// Alice->Bob->Dave.
|
||||
assertPolicyUpdate(alice, expectedPolicy, chanPointAliceCarol, 2)
|
||||
|
||||
// Also do this check for Eve's channel with Carol.
|
||||
ht.CloseChannel(eve, chanPointEveCarol)
|
||||
|
||||
// We expect to see a total of 5 channel policy updates from the
|
||||
// channel Carol <-> Eve and advertised by Eve using the route
|
||||
// Eve->Bob->Dave.
|
||||
assertPolicyUpdate(eve, expectedPolicy, chanPointEveCarol, 6)
|
||||
}
|
||||
|
||||
// testUpdateChannelPolicyForPrivateChannel tests when a private channel
|
||||
// updates its channel edge policy, we will use the updated policy to send our
|
||||
// payment.
|
||||
// The topology is created as: Alice -> Bob -> Carol, where Alice -> Bob is
|
||||
// public and Bob -> Carol is private. After an invoice is created by Carol,
|
||||
// Bob will update the base fee via UpdateChannelPolicy, we will test that
|
||||
// Alice will not fail the payment and send it using the updated channel
|
||||
// policy.
|
||||
func testUpdateChannelPolicyForPrivateChannel(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(100000)
|
||||
paymentAmt = 20000
|
||||
baseFeeMSat = 33000
|
||||
)
|
||||
|
||||
// We'll create the following topology first,
|
||||
// Alice <--public:100k--> Bob <--private:100k--> Carol
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Open a channel with 100k satoshis between Alice and Bob.
|
||||
chanPointAliceBob := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// Create a new node Carol.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Connect Carol to Bob.
|
||||
ht.ConnectNodes(carol, bob)
|
||||
|
||||
// Open a channel with 100k satoshis between Bob and Carol.
|
||||
chanPointBobCarol := ht.OpenChannel(
|
||||
bob, carol, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
Private: true,
|
||||
},
|
||||
)
|
||||
|
||||
// Carol should be aware of the channel between Alice and Bob.
|
||||
ht.AssertTopologyChannelOpen(carol, chanPointAliceBob)
|
||||
|
||||
// We should have the following topology now,
|
||||
// Alice <--public:100k--> Bob <--private:100k--> Carol
|
||||
//
|
||||
// Now we will create an invoice for Carol.
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "routing hints",
|
||||
Value: paymentAmt,
|
||||
Private: true,
|
||||
}
|
||||
resp := carol.RPC.AddInvoice(invoice)
|
||||
|
||||
// Bob now updates the channel edge policy for the private channel.
|
||||
timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
||||
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFeeMSat,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
ChanPoint: chanPointBobCarol,
|
||||
},
|
||||
}
|
||||
bob.RPC.UpdateChannelPolicy(updateFeeReq)
|
||||
|
||||
// Alice pays the invoices. She will use the updated baseFeeMSat in the
|
||||
// payment
|
||||
payReqs := []string{resp.PaymentRequest}
|
||||
ht.CompletePaymentRequests(alice, payReqs)
|
||||
|
||||
// Check that Alice did make the payment with two HTLCs, one failed and
|
||||
// one succeeded.
|
||||
payment := ht.AssertNumPayments(alice, 1)[0]
|
||||
|
||||
htlcs := payment.Htlcs
|
||||
require.Equal(ht, 2, len(htlcs), "expected to have 2 HTLCs")
|
||||
require.Equal(ht, lnrpc.HTLCAttempt_FAILED, htlcs[0].Status,
|
||||
"the first HTLC attempt should fail")
|
||||
require.Equal(ht, lnrpc.HTLCAttempt_SUCCEEDED, htlcs[1].Status,
|
||||
"the second HTLC attempt should succeed")
|
||||
|
||||
// Carol should have received 20k satoshis from Bob.
|
||||
ht.AssertAmountPaid("Carol(remote) [<=private] Bob(local)",
|
||||
carol, chanPointBobCarol, 0, paymentAmt)
|
||||
|
||||
// Bob should have sent 20k satoshis to Carol.
|
||||
ht.AssertAmountPaid("Bob(local) [private=>] Carol(remote)",
|
||||
bob, chanPointBobCarol, paymentAmt, 0)
|
||||
|
||||
// Calculate the amount in satoshis.
|
||||
amtExpected := int64(paymentAmt + baseFeeMSat/1000)
|
||||
|
||||
// Bob should have received 20k satoshis + fee from Alice.
|
||||
ht.AssertAmountPaid("Bob(remote) <= Alice(local)",
|
||||
bob, chanPointAliceBob, 0, amtExpected)
|
||||
|
||||
// Alice should have sent 20k satoshis + fee to Bob.
|
||||
ht.AssertAmountPaid("Alice(local) => Bob(remote)",
|
||||
alice, chanPointAliceBob, amtExpected, 0)
|
||||
|
||||
// Finally, close the channels.
|
||||
ht.CloseChannel(alice, chanPointAliceBob)
|
||||
ht.CloseChannel(bob, chanPointBobCarol)
|
||||
}
|
||||
|
||||
// testUpdateChannelPolicyFeeRateAccuracy tests that updating the channel policy
|
||||
// rounds fee rate values correctly as well as setting fee rate with ppm works
|
||||
// as expected.
|
||||
func testUpdateChannelPolicyFeeRateAccuracy(ht *lntemp.HarnessTest) {
|
||||
chanAmt := funding.MaxBtcFundingAmount
|
||||
pushAmt := chanAmt / 2
|
||||
|
||||
// Create a channel Alice -> Bob.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// Nodes that we need to make sure receive the channel updates.
|
||||
nodes := []*node.HarnessNode{alice, bob}
|
||||
|
||||
baseFee := int64(1500)
|
||||
timeLockDelta := uint32(66)
|
||||
maxHtlc := uint64(500000)
|
||||
defaultMinHtlc := int64(1000)
|
||||
|
||||
// Originally LND did not properly round up fee rates which caused
|
||||
// inaccuracy where fee rates were simply rounded down due to the
|
||||
// integer conversion.
|
||||
//
|
||||
// We'll use a fee rate of 0.031337 which without rounding up would
|
||||
// have resulted in a fee rate ppm of 31336.
|
||||
feeRate := 0.031337
|
||||
|
||||
// Expected fee rate will be rounded up.
|
||||
expectedFeeRateMilliMsat := int64(math.Round(testFeeBase * feeRate))
|
||||
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: baseFee,
|
||||
FeeRateMilliMsat: expectedFeeRateMilliMsat,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
}
|
||||
|
||||
req := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: feeRate,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
ChanPoint: chanPoint,
|
||||
},
|
||||
}
|
||||
alice.RPC.UpdateChannelPolicy(req)
|
||||
|
||||
// Make sure that both Alice and Bob sees the same policy after update.
|
||||
assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint)
|
||||
|
||||
// Now use the new PPM feerate field and make sure that the feerate is
|
||||
// correctly set.
|
||||
feeRatePPM := uint32(32337)
|
||||
req.FeeRate = 0 // Can't set both at the same time.
|
||||
req.FeeRatePpm = feeRatePPM
|
||||
expectedPolicy.FeeRateMilliMsat = int64(feeRatePPM)
|
||||
|
||||
alice.RPC.UpdateChannelPolicy(req)
|
||||
|
||||
// Make sure that both Alice and Bob sees the same policy after update.
|
||||
assertNodesPolicyUpdate(ht, nodes, alice, expectedPolicy, chanPoint)
|
||||
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// assertNodesPolicyUpdate checks that a given policy update has been received
|
||||
// by a list of given nodes.
|
||||
func assertNodesPolicyUpdate(ht *lntemp.HarnessTest, nodes []*node.HarnessNode,
|
||||
advertisingNode *node.HarnessNode, policy *lnrpc.RoutingPolicy,
|
||||
chanPoint *lnrpc.ChannelPoint) {
|
||||
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
node, advertisingNode, policy, chanPoint, false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testCustomMessage tests sending and receiving of overridden custom message
|
||||
// types (within the message type range usually reserved for protocol messages)
|
||||
// via the send and subscribe custom message APIs.
|
||||
func testCustomMessage(ht *lntemp.HarnessTest) {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
var (
|
||||
overrideType1 uint32 = 554
|
||||
overrideType2 uint32 = 555
|
||||
msgOverrideArg = "--protocol.custom-message=%v"
|
||||
)
|
||||
|
||||
// Update Alice to accept custom protocol messages with type 1 but do
|
||||
// not allow Bob to handle them yet.
|
||||
extraArgs := []string{
|
||||
fmt.Sprintf(msgOverrideArg, overrideType1),
|
||||
}
|
||||
ht.RestartNodeWithExtraArgs(alice, extraArgs)
|
||||
|
||||
// Subscribe Alice to custom messages before we send any, so that we
|
||||
// don't miss any.
|
||||
msgClient, cancel := alice.RPC.SubscribeCustomMessages()
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to receive custom messages on.
|
||||
messages := make(chan *lnrpc.CustomMessage)
|
||||
go func() {
|
||||
for {
|
||||
// If we fail to receive, just exit. The test should
|
||||
// fail elsewhere if it doesn't get a message that it
|
||||
// was expecting.
|
||||
msg, err := msgClient.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deliver the message into our channel or exit if the
|
||||
// test is shutting down.
|
||||
select {
|
||||
case messages <- msg:
|
||||
case <-ht.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Connect alice and bob so that they can exchange messages.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// Create a custom message that is within our allowed range.
|
||||
msgType := uint32(lnwire.CustomTypeStart + 1)
|
||||
msgData := []byte{1, 2, 3}
|
||||
|
||||
// Send it from Bob to Alice.
|
||||
bobMsg := &lnrpc.SendCustomMessageRequest{
|
||||
Peer: alice.PubKey[:],
|
||||
Type: msgType,
|
||||
Data: msgData,
|
||||
}
|
||||
bob.RPC.SendCustomMessage(bobMsg)
|
||||
|
||||
// Wait for Alice to receive the message. It should come through
|
||||
// because it is within our allowed range.
|
||||
select {
|
||||
case msg := <-messages:
|
||||
// Check our type and data and (sanity) check the peer we got
|
||||
// it from.
|
||||
require.Equal(ht, msgType, msg.Type, "first msg type wrong")
|
||||
require.Equal(ht, msgData, msg.Data, "first msg data wrong")
|
||||
require.Equal(ht, bob.PubKey[:], msg.Peer, "first msg "+
|
||||
"peer wrong")
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
ht.Fatalf("alice did not receive first custom message: %v",
|
||||
msgType)
|
||||
}
|
||||
|
||||
// Try to send a message from Bob to Alice which has a message type
|
||||
// outside of the custom type range and assert that it fails.
|
||||
bobMsg = &lnrpc.SendCustomMessageRequest{
|
||||
Peer: alice.PubKey[:],
|
||||
Type: overrideType1,
|
||||
Data: msgData,
|
||||
}
|
||||
_, err := bob.RPC.LN.SendCustomMessage(ht.Context(), bobMsg)
|
||||
require.Error(ht, err, "bob should not be able to send type 1")
|
||||
|
||||
// Now, restart Bob with the ability to send two different custom
|
||||
// protocol messages.
|
||||
extraArgs = []string{
|
||||
fmt.Sprintf(msgOverrideArg, overrideType1),
|
||||
fmt.Sprintf(msgOverrideArg, overrideType2),
|
||||
}
|
||||
ht.RestartNodeWithExtraArgs(bob, extraArgs)
|
||||
|
||||
// Make sure Bob and Alice are connected after his restart.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// Send a message from Bob to Alice with a type that Bob is allowed to
|
||||
// send, but Alice will not handle as a custom message.
|
||||
bobMsg = &lnrpc.SendCustomMessageRequest{
|
||||
Peer: alice.PubKey[:],
|
||||
Type: overrideType2,
|
||||
Data: msgData,
|
||||
}
|
||||
bob.RPC.SendCustomMessage(bobMsg)
|
||||
|
||||
// Do a quick check that Alice did not receive this message in her
|
||||
// stream. Note that this is an instant check, so could miss the
|
||||
// message being received. We'll also check below that she didn't get
|
||||
// it, this is just a sanity check.
|
||||
select {
|
||||
case msg := <-messages:
|
||||
ht.Fatalf("unexpected message: %v", msg)
|
||||
default:
|
||||
}
|
||||
|
||||
// Finally, send a custom message with a type that Bob is allowed to
|
||||
// send and Alice is configured to receive.
|
||||
bobMsg = &lnrpc.SendCustomMessageRequest{
|
||||
Peer: alice.PubKey[:],
|
||||
Type: overrideType1,
|
||||
Data: msgData,
|
||||
}
|
||||
bob.RPC.SendCustomMessage(bobMsg)
|
||||
|
||||
// Wait to receive a message from Bob. This check serves to ensure that
|
||||
// our message type 1 was delivered, and assert that the preceding one
|
||||
// was not (we could have missed it in our check above). When we
|
||||
// receive the second message, we know that the first one did not go
|
||||
// through, because we expect our messages to deliver in order.
|
||||
select {
|
||||
case msg := <-messages:
|
||||
// Check our type and data and (sanity) check the peer we got
|
||||
// it from.
|
||||
require.Equal(ht, overrideType1, msg.Type, "second message "+
|
||||
"type")
|
||||
require.Equal(ht, msgData, msg.Data, "second message data")
|
||||
require.Equal(ht, bob.PubKey[:], msg.Peer, "second "+
|
||||
"message peer")
|
||||
|
||||
case <-time.After(defaultTimeout):
|
||||
ht.Fatalf("alice did not receive second custom message")
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
//go:build kvdb_etcd
|
||||
// +build kvdb_etcd
|
||||
|
||||
package itest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/cluster"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func assertLeader(ht *lntemp.HarnessTest, observer cluster.LeaderElector,
|
||||
expected string) {
|
||||
|
||||
leader, err := observer.Leader(ht.Context())
|
||||
require.NoError(ht, err, "Unable to query leader")
|
||||
require.Equalf(ht, expected, leader,
|
||||
"Leader should be '%v', got: '%v'", expected, leader)
|
||||
}
|
||||
|
||||
// testEtcdFailover tests that in a cluster setup where two LND nodes form a
|
||||
// single cluster (sharing the same identity) one can hand over the leader role
|
||||
// to the other (failing over after graceful shutdown or forceful abort).
|
||||
func testEtcdFailover(ht *lntemp.HarnessTest) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
kill bool
|
||||
}{{
|
||||
name: "failover after shutdown",
|
||||
kill: false,
|
||||
}, {
|
||||
name: "failover after abort",
|
||||
kill: true,
|
||||
}}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
success := ht.Run(test.name, func(t1 *testing.T) {
|
||||
st := ht.Subtest(t1)
|
||||
testEtcdFailoverCase(st, test.kill)
|
||||
})
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testEtcdFailoverCase(ht *lntemp.HarnessTest, kill bool) {
|
||||
etcdCfg, cleanup, err := kvdb.StartEtcdTestBackend(
|
||||
ht.T.TempDir(), uint16(lntest.NextAvailablePort()),
|
||||
uint16(lntest.NextAvailablePort()), "",
|
||||
)
|
||||
require.NoError(ht, err, "Failed to start etcd instance")
|
||||
defer cleanup()
|
||||
|
||||
alice := ht.NewNode("Alice", nil)
|
||||
|
||||
// Give Alice some coins to fund the channel.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
|
||||
// Make leader election session TTL 5 sec to make the test run fast.
|
||||
const leaderSessionTTL = 5
|
||||
|
||||
observer, err := cluster.MakeLeaderElector(
|
||||
ht.Context(), cluster.EtcdLeaderElector, "observer",
|
||||
lncfg.DefaultEtcdElectionPrefix, leaderSessionTTL, etcdCfg,
|
||||
)
|
||||
require.NoError(ht, err, "Cannot start election observer")
|
||||
|
||||
password := []byte("the quick brown fox jumps the lazy dog")
|
||||
entropy := [16]byte{1, 2, 3}
|
||||
stateless := false
|
||||
cluster := true
|
||||
|
||||
carol1, _, _ := ht.NewNodeWithSeedEtcd(
|
||||
"Carol-1", etcdCfg, password, entropy[:], stateless, cluster,
|
||||
leaderSessionTTL,
|
||||
)
|
||||
info1 := carol1.RPC.GetInfo()
|
||||
ht.ConnectNodes(carol1, alice)
|
||||
|
||||
// Open a channel with 100k satoshis between Carol and Alice with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanAmt := btcutil.Amount(100_000)
|
||||
ht.OpenChannel(alice, carol1, lntemp.OpenChannelParams{Amt: chanAmt})
|
||||
|
||||
// At this point Carol-1 is the elected leader, while Carol-2 will wait
|
||||
// to become the leader when Carol-1 stops.
|
||||
carol2 := ht.NewNodeEtcd(
|
||||
"Carol-2", etcdCfg, password, cluster, leaderSessionTTL,
|
||||
)
|
||||
assertLeader(ht, observer, "Carol-1")
|
||||
|
||||
amt := btcutil.Amount(1000)
|
||||
payReqs, _, _ := ht.CreatePayReqs(carol1, amt, 2)
|
||||
ht.CompletePaymentRequests(alice, []string{payReqs[0]})
|
||||
|
||||
// Shut down or kill Carol-1 and wait for Carol-2 to become the leader.
|
||||
failoverTimeout := time.Duration(2*leaderSessionTTL) * time.Second
|
||||
if kill {
|
||||
ht.KillNode(carol1)
|
||||
} else {
|
||||
ht.Shutdown(carol1)
|
||||
}
|
||||
|
||||
err = carol2.WaitUntilLeader(failoverTimeout)
|
||||
require.NoError(ht, err, "Waiting for Carol-2 to become the leader "+
|
||||
"failed")
|
||||
assertLeader(ht, observer, "Carol-2")
|
||||
|
||||
req := &lnrpc.UnlockWalletRequest{WalletPassword: password}
|
||||
err = carol2.Unlock(req)
|
||||
require.NoError(ht, err, "Unlocking Carol-2 failed")
|
||||
|
||||
// Make sure Carol-1 and Carol-2 have the same identity.
|
||||
info2 := carol2.RPC.GetInfo()
|
||||
require.Equal(ht, info1.IdentityPubkey, info2.IdentityPubkey,
|
||||
"Carol-1 and Carol-2 must have the same identity")
|
||||
|
||||
// Make sure the nodes are connected before moving forward. Otherwise
|
||||
// we may get a link not found error.
|
||||
ht.AssertConnected(alice, carol2)
|
||||
|
||||
// Now let Alice pay the second invoice but this time we expect Carol-2
|
||||
// to receive the payment.
|
||||
ht.CompletePaymentRequests(alice, []string{payReqs[1]})
|
||||
|
||||
// Manually shutdown the node as it will mess up with our cleanup
|
||||
// process.
|
||||
ht.Shutdown(carol2)
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
customTestKey uint64 = 394829
|
||||
customTestValue = []byte{1, 3, 5}
|
||||
)
|
||||
|
||||
type interceptorTestCase struct {
|
||||
amountMsat int64
|
||||
payAddr []byte
|
||||
invoice *lnrpc.Invoice
|
||||
shouldHold bool
|
||||
interceptorAction routerrpc.ResolveHoldForwardAction
|
||||
}
|
||||
|
||||
// testForwardInterceptorDedupHtlc tests that upon reconnection, duplicate
|
||||
// HTLCs aren't re-notified using the HTLC interceptor API.
|
||||
func testForwardInterceptorDedupHtlc(ht *lntemp.HarnessTest) {
|
||||
// Initialize the test context with 3 connected nodes.
|
||||
ts := newInterceptorTestScenario(ht)
|
||||
|
||||
alice, bob, carol := ts.alice, ts.bob, ts.carol
|
||||
|
||||
// Open and wait for channels.
|
||||
const chanAmt = btcutil.Amount(300000)
|
||||
p := lntemp.OpenChannelParams{Amt: chanAmt}
|
||||
reqs := []*lntemp.OpenChannelRequest{
|
||||
{Local: alice, Remote: bob, Param: p},
|
||||
{Local: bob, Remote: carol, Param: p},
|
||||
}
|
||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
||||
cpAB, cpBC := resp[0], resp[1]
|
||||
|
||||
// Make sure Alice is aware of channel Bob=>Carol.
|
||||
ht.AssertTopologyChannelOpen(alice, cpBC)
|
||||
|
||||
// Connect the interceptor.
|
||||
interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
|
||||
|
||||
// Prepare the test cases.
|
||||
req := &lnrpc.Invoice{ValueMsat: 1000}
|
||||
addResponse := carol.RPC.AddInvoice(req)
|
||||
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
|
||||
tc := &interceptorTestCase{
|
||||
amountMsat: 1000,
|
||||
invoice: invoice,
|
||||
payAddr: invoice.PaymentAddr,
|
||||
}
|
||||
|
||||
// We initiate a payment from Alice.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
// Signal that all the payments have been sent.
|
||||
defer close(done)
|
||||
|
||||
ts.sendPaymentAndAssertAction(tc)
|
||||
}()
|
||||
|
||||
// We start the htlc interceptor with a simple implementation that
|
||||
// saves all intercepted packets. These packets are held to simulate a
|
||||
// pending payment.
|
||||
packet := ht.ReceiveHtlcInterceptor(interceptor)
|
||||
|
||||
// Here we should wait for the channel to contain a pending htlc, and
|
||||
// also be shown as being active.
|
||||
err := wait.NoError(func() error {
|
||||
channel := ht.QueryChannelByChanPoint(bob, cpAB)
|
||||
|
||||
if len(channel.PendingHtlcs) == 0 {
|
||||
return fmt.Errorf("expect alice <> bob channel to " +
|
||||
"have pending htlcs")
|
||||
}
|
||||
if channel.Active {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("channel not active")
|
||||
}, defaultTimeout)
|
||||
require.NoError(
|
||||
ht, err, "alice <> bob channel pending htlc never arrived",
|
||||
)
|
||||
|
||||
// At this point we want to make bob's link send all pending htlcs to
|
||||
// the switch again. We force this behavior by disconnecting and
|
||||
// connecting to the peer.
|
||||
ht.DisconnectNodes(bob, alice)
|
||||
ht.EnsureConnected(bob, alice)
|
||||
|
||||
// Here we wait for the channel to be active again.
|
||||
ht.AssertChannelExists(bob, cpAB)
|
||||
|
||||
// Now that the channel is active we make sure the test passes as
|
||||
// expected.
|
||||
|
||||
// We expect one in flight payment since we held the htlcs.
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], invoice.RPreimage)
|
||||
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_IN_FLIGHT)
|
||||
|
||||
// At this point if we have more than one held htlcs then we should
|
||||
// fail. This means we hold the same htlc twice which is a risk we want
|
||||
// to eliminate. If we don't have the same htlc twice in theory we can
|
||||
// cancel one and settle the other by mistake.
|
||||
errDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(errDone)
|
||||
|
||||
_, err := interceptor.Recv()
|
||||
require.Error(ht, err, "expected an error from interceptor")
|
||||
|
||||
status, ok := status.FromError(err)
|
||||
switch {
|
||||
// If it is just the error result of the context cancellation
|
||||
// the we exit silently.
|
||||
case ok && status.Code() == codes.Canceled:
|
||||
fallthrough
|
||||
|
||||
// When the test ends, during the node's shutdown it will close
|
||||
// the connection.
|
||||
case strings.Contains(err.Error(), "closed network connection"):
|
||||
fallthrough
|
||||
|
||||
case strings.Contains(err.Error(), "EOF"):
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise we receive an unexpected error.
|
||||
require.Failf(ht, "interceptor", "unexpected err: %v", err)
|
||||
}()
|
||||
|
||||
// We now fail all htlcs to cancel the payment.
|
||||
err = interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||
Action: routerrpc.ResolveHoldForwardAction_FAIL,
|
||||
})
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
|
||||
// Cancel the context, which will disconnect the above interceptor.
|
||||
cancelInterceptor()
|
||||
|
||||
// Make sure all goroutines are finished.
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for sending payment")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-errDone:
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for interceptor error")
|
||||
}
|
||||
|
||||
// Finally, close channels.
|
||||
ht.CloseChannel(alice, cpAB)
|
||||
ht.CloseChannel(bob, cpBC)
|
||||
}
|
||||
|
||||
// testForwardInterceptorBasic tests the forward interceptor RPC layer.
|
||||
// The test creates a cluster of 3 connected nodes: Alice -> Bob -> Carol
|
||||
// Alice sends 4 different payments to Carol while the interceptor handles
|
||||
// differently the htlcs.
|
||||
// The test ensures that:
|
||||
// 1. Intercepted failed htlcs result in no payment (invoice is not settled).
|
||||
// 2. Intercepted resumed htlcs result in a payment (invoice is settled).
|
||||
// 3. Intercepted held htlcs result in no payment (invoice is not settled).
|
||||
// 4. When Interceptor disconnects it resumes all held htlcs, which result in
|
||||
// valid payment (invoice is settled).
|
||||
func testForwardInterceptorBasic(ht *lntemp.HarnessTest) {
|
||||
ts := newInterceptorTestScenario(ht)
|
||||
|
||||
alice, bob, carol := ts.alice, ts.bob, ts.carol
|
||||
|
||||
// Open and wait for channels.
|
||||
const chanAmt = btcutil.Amount(300000)
|
||||
p := lntemp.OpenChannelParams{Amt: chanAmt}
|
||||
reqs := []*lntemp.OpenChannelRequest{
|
||||
{Local: alice, Remote: bob, Param: p},
|
||||
{Local: bob, Remote: carol, Param: p},
|
||||
}
|
||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
||||
cpAB, cpBC := resp[0], resp[1]
|
||||
|
||||
// Make sure Alice is aware of channel Bob=>Carol.
|
||||
ht.AssertTopologyChannelOpen(alice, cpBC)
|
||||
|
||||
// Connect the interceptor.
|
||||
interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
|
||||
|
||||
// Prepare the test cases.
|
||||
testCases := ts.prepareTestCases()
|
||||
|
||||
// For each test case make sure we initiate a payment from Alice to
|
||||
// Carol routed through Bob. For each payment we also test its final
|
||||
// status according to the interceptorAction specified in the test
|
||||
// case.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
// Signal that all the payments have been sent.
|
||||
defer close(done)
|
||||
|
||||
for _, tc := range testCases {
|
||||
attempt := ts.sendPaymentAndAssertAction(tc)
|
||||
ts.assertAction(tc, attempt)
|
||||
}
|
||||
}()
|
||||
|
||||
// We make sure here the interceptor has processed all packets before
|
||||
// we check the payment statuses.
|
||||
for _, tc := range testCases {
|
||||
request := ht.ReceiveHtlcInterceptor(interceptor)
|
||||
|
||||
// Assert sanity of informational packet data.
|
||||
require.NotZero(ht, request.OutgoingRequestedChanId)
|
||||
require.NotZero(ht, request.IncomingExpiry)
|
||||
require.NotZero(ht, request.IncomingAmountMsat)
|
||||
|
||||
require.Less(ht, request.OutgoingExpiry,
|
||||
request.IncomingExpiry)
|
||||
require.Less(ht, request.OutgoingAmountMsat,
|
||||
request.IncomingAmountMsat)
|
||||
|
||||
value, ok := request.CustomRecords[customTestKey]
|
||||
require.True(ht, ok, "expected custom record")
|
||||
require.Equal(ht, customTestValue, value)
|
||||
|
||||
// For held packets we ignore, keeping them in hold status.
|
||||
if tc.shouldHold {
|
||||
continue
|
||||
}
|
||||
|
||||
// For all other packets we resolve according to the test case.
|
||||
err := interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||
Action: tc.interceptorAction,
|
||||
Preimage: tc.invoice.RPreimage,
|
||||
})
|
||||
require.NoError(ht, err, "failed to send request")
|
||||
}
|
||||
|
||||
// At this point we are left with the held packets, we want to make
|
||||
// sure each one of them has a corresponding 'in-flight' payment at
|
||||
// Alice's node.
|
||||
for _, testCase := range testCases {
|
||||
if !testCase.shouldHold {
|
||||
continue
|
||||
}
|
||||
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], testCase.invoice.RPreimage)
|
||||
|
||||
payment := ht.AssertPaymentStatus(
|
||||
alice, preimage, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
expectedAmt := testCase.invoice.ValueMsat
|
||||
require.Equal(ht, expectedAmt, payment.ValueMsat,
|
||||
"incorrect in flight amount")
|
||||
}
|
||||
|
||||
// Cancel the context, which will disconnect the above interceptor.
|
||||
cancelInterceptor()
|
||||
|
||||
// Disconnect interceptor should cause resume held packets. After that
|
||||
// we wait for all go routines to finish, including the one that tests
|
||||
// the payment final status for the held payment.
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for sending payment")
|
||||
}
|
||||
|
||||
// Verify that we don't get notified about already completed HTLCs
|
||||
// We do that by restarting alice, the sender the HTLCs. Under
|
||||
// https://github.com/lightningnetwork/lnd/issues/5115
|
||||
// this should cause all HTLCs settled or failed by the interceptor to
|
||||
// renotify.
|
||||
restartAlice := ht.SuspendNode(alice)
|
||||
require.NoError(ht, restartAlice(), "failed to restart alice")
|
||||
|
||||
// Make sure the channel is active from Bob's PoV.
|
||||
ht.AssertChannelExists(bob, cpAB)
|
||||
|
||||
// Create a new interceptor as the old one has quit.
|
||||
interceptor, cancelInterceptor = bob.RPC.HtlcInterceptor()
|
||||
|
||||
done = make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
_, err := interceptor.Recv()
|
||||
require.Error(ht, err, "expected an error from interceptor")
|
||||
|
||||
status, ok := status.FromError(err)
|
||||
switch {
|
||||
// If it is just the error result of the context cancellation
|
||||
// the we exit silently.
|
||||
case ok && status.Code() == codes.Canceled:
|
||||
fallthrough
|
||||
|
||||
// When the test ends, during the node's shutdown it will close
|
||||
// the connection.
|
||||
case strings.Contains(err.Error(), "closed network connection"):
|
||||
fallthrough
|
||||
|
||||
case strings.Contains(err.Error(), "EOF"):
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise we receive an unexpected error.
|
||||
require.Failf(ht, "iinterceptor", "unexpected err: %v", err)
|
||||
}()
|
||||
|
||||
// Cancel the context, which will disconnect the above interceptor.
|
||||
cancelInterceptor()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(ht, "timeout waiting for interceptor error")
|
||||
}
|
||||
|
||||
// Finally, close channels.
|
||||
ht.CloseChannel(alice, cpAB)
|
||||
ht.CloseChannel(bob, cpBC)
|
||||
}
|
||||
|
||||
// interceptorTestScenario is a helper struct to hold the test context and
|
||||
// provide the needed functionality.
|
||||
type interceptorTestScenario struct {
|
||||
ht *lntemp.HarnessTest
|
||||
alice, bob, carol *node.HarnessNode
|
||||
}
|
||||
|
||||
// newInterceptorTestScenario initializes a new test scenario with three nodes
|
||||
// and connects them to have the following topology,
|
||||
//
|
||||
// Alice --> Bob --> Carol
|
||||
//
|
||||
// Among them, Alice and Bob are standby nodes and Carol is a new node.
|
||||
func newInterceptorTestScenario(
|
||||
ht *lntemp.HarnessTest) *interceptorTestScenario {
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
carol := ht.NewNode("carol", nil)
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.EnsureConnected(bob, carol)
|
||||
|
||||
return &interceptorTestScenario{
|
||||
ht: ht,
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
carol: carol,
|
||||
}
|
||||
}
|
||||
|
||||
// prepareTestCases prepares 4 tests:
|
||||
// 1. failed htlc.
|
||||
// 2. resumed htlc.
|
||||
// 3. settling htlc externally.
|
||||
// 4. held htlc that is resumed later.
|
||||
func (c *interceptorTestScenario) prepareTestCases() []*interceptorTestCase {
|
||||
var (
|
||||
actionFail = routerrpc.ResolveHoldForwardAction_FAIL
|
||||
actionResume = routerrpc.ResolveHoldForwardAction_RESUME
|
||||
actionSettle = routerrpc.ResolveHoldForwardAction_SETTLE
|
||||
)
|
||||
|
||||
cases := []*interceptorTestCase{
|
||||
{
|
||||
amountMsat: 1000, shouldHold: false,
|
||||
interceptorAction: actionFail,
|
||||
},
|
||||
{
|
||||
amountMsat: 1000, shouldHold: false,
|
||||
interceptorAction: actionResume,
|
||||
},
|
||||
{
|
||||
amountMsat: 1000, shouldHold: false,
|
||||
interceptorAction: actionSettle,
|
||||
},
|
||||
{
|
||||
amountMsat: 1000, shouldHold: true,
|
||||
interceptorAction: actionResume,
|
||||
},
|
||||
}
|
||||
|
||||
for _, t := range cases {
|
||||
inv := &lnrpc.Invoice{ValueMsat: t.amountMsat}
|
||||
addResponse := c.carol.RPC.AddInvoice(inv)
|
||||
invoice := c.carol.RPC.LookupInvoice(addResponse.RHash)
|
||||
|
||||
// We'll need to also decode the returned invoice so we can
|
||||
// grab the payment address which is now required for ALL
|
||||
// payments.
|
||||
payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||
|
||||
t.invoice = invoice
|
||||
t.payAddr = payReq.PaymentAddr
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
// sendPaymentAndAssertAction sends a payment from alice to carol and asserts
|
||||
// that the specified interceptor action is taken.
|
||||
func (c *interceptorTestScenario) sendPaymentAndAssertAction(
|
||||
tc *interceptorTestCase) *lnrpc.HTLCAttempt {
|
||||
|
||||
// Build a route from alice to carol.
|
||||
route := c.buildRoute(
|
||||
tc.amountMsat, []*node.HarnessNode{c.bob, c.carol}, tc.payAddr,
|
||||
)
|
||||
|
||||
// Send a custom record to the forwarding node.
|
||||
route.Hops[0].CustomRecords = map[uint64][]byte{
|
||||
customTestKey: customTestValue,
|
||||
}
|
||||
|
||||
// Send the payment.
|
||||
sendReq := &routerrpc.SendToRouteRequest{
|
||||
PaymentHash: tc.invoice.RHash,
|
||||
Route: route,
|
||||
}
|
||||
|
||||
return c.alice.RPC.SendToRouteV2(sendReq)
|
||||
}
|
||||
|
||||
func (c *interceptorTestScenario) assertAction(tc *interceptorTestCase,
|
||||
attempt *lnrpc.HTLCAttempt) {
|
||||
|
||||
// Now check the expected action has been taken.
|
||||
switch tc.interceptorAction {
|
||||
// For 'fail' interceptor action we make sure the payment failed.
|
||||
case routerrpc.ResolveHoldForwardAction_FAIL:
|
||||
require.Equal(c.ht, lnrpc.HTLCAttempt_FAILED, attempt.Status,
|
||||
"expected payment to fail")
|
||||
|
||||
// Assert that we get a temporary channel failure which has a
|
||||
// channel update.
|
||||
require.NotNil(c.ht, attempt.Failure)
|
||||
require.NotNil(c.ht, attempt.Failure.ChannelUpdate)
|
||||
|
||||
require.Equal(c.ht, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
|
||||
attempt.Failure.Code)
|
||||
|
||||
// For settle and resume we make sure the payment is successful.
|
||||
case routerrpc.ResolveHoldForwardAction_SETTLE:
|
||||
fallthrough
|
||||
|
||||
case routerrpc.ResolveHoldForwardAction_RESUME:
|
||||
require.Equal(c.ht, lnrpc.HTLCAttempt_SUCCEEDED,
|
||||
attempt.Status, "expected payment to succeed")
|
||||
}
|
||||
}
|
||||
|
||||
// buildRoute is a helper function to build a route with given hops.
|
||||
func (c *interceptorTestScenario) buildRoute(amtMsat int64,
|
||||
hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route {
|
||||
|
||||
rpcHops := make([][]byte, 0, len(hops))
|
||||
for _, hop := range hops {
|
||||
k := hop.PubKeyStr
|
||||
pubkey, err := route.NewVertexFromStr(k)
|
||||
require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err)
|
||||
rpcHops = append(rpcHops, pubkey[:])
|
||||
}
|
||||
|
||||
req := &routerrpc.BuildRouteRequest{
|
||||
AmtMsat: amtMsat,
|
||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
HopPubkeys: rpcHops,
|
||||
PaymentAddr: payAddr,
|
||||
}
|
||||
|
||||
routeResp := c.alice.RPC.BuildRoute(req)
|
||||
|
||||
return routeResp.Route
|
||||
}
|
||||
@@ -1,886 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/labels"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testBasicChannelFunding performs a test exercising expected behavior from a
|
||||
// basic funding workflow. The test creates a new channel between Alice and
|
||||
// Bob, then immediately closes the channel after asserting some expected post
|
||||
// conditions. Finally, the chain itself is checked to ensure the closing
|
||||
// transaction was mined.
|
||||
func testBasicChannelFunding(ht *lntemp.HarnessTest) {
|
||||
// Run through the test with combinations of all the different
|
||||
// commitment types.
|
||||
allTypes := []lnrpc.CommitmentType{
|
||||
lnrpc.CommitmentType_LEGACY,
|
||||
lnrpc.CommitmentType_STATIC_REMOTE_KEY,
|
||||
lnrpc.CommitmentType_ANCHORS,
|
||||
}
|
||||
|
||||
// testFunding is a function closure that takes Carol and Dave's
|
||||
// commitment types and test the funding flow.
|
||||
testFunding := func(ht *lntemp.HarnessTest, carolCommitType,
|
||||
daveCommitType lnrpc.CommitmentType) {
|
||||
|
||||
// Based on the current tweak variable for Carol, we'll
|
||||
// preferentially signal the legacy commitment format. We do
|
||||
// the same for Dave shortly below.
|
||||
carolArgs := nodeArgsForCommitType(carolCommitType)
|
||||
carol := ht.NewNode("Carol", carolArgs)
|
||||
|
||||
// Each time, we'll send Carol a new set of coins in order to
|
||||
// fund the channel.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
daveArgs := nodeArgsForCommitType(daveCommitType)
|
||||
dave := ht.NewNode("Dave", daveArgs)
|
||||
|
||||
// Before we start the test, we'll ensure both sides are
|
||||
// connected to the funding flow can properly be executed.
|
||||
ht.EnsureConnected(carol, dave)
|
||||
|
||||
carolChan, daveChan, closeChan := basicChannelFundingTest(
|
||||
ht, carol, dave, nil,
|
||||
)
|
||||
|
||||
// Both nodes should report the same commitment
|
||||
// type.
|
||||
chansCommitType := carolChan.CommitmentType
|
||||
require.Equal(ht, chansCommitType, daveChan.CommitmentType,
|
||||
"commit types don't match")
|
||||
|
||||
// Now check that the commitment type reported by both nodes is
|
||||
// what we expect. It will be the minimum of the two nodes'
|
||||
// preference, in the order Legacy, Tweakless, Anchors.
|
||||
expType := carolCommitType
|
||||
|
||||
switch daveCommitType {
|
||||
// Dave supports anchors, type will be what Carol supports.
|
||||
case lnrpc.CommitmentType_ANCHORS:
|
||||
|
||||
// Dave only supports tweakless, channel will be downgraded to
|
||||
// this type if Carol supports anchors.
|
||||
case lnrpc.CommitmentType_STATIC_REMOTE_KEY:
|
||||
if expType == lnrpc.CommitmentType_ANCHORS {
|
||||
expType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
|
||||
}
|
||||
|
||||
// Dave only supoprts legacy type, channel will be downgraded
|
||||
// to this type.
|
||||
case lnrpc.CommitmentType_LEGACY:
|
||||
expType = lnrpc.CommitmentType_LEGACY
|
||||
|
||||
default:
|
||||
ht.Fatalf("invalid commit type %v", daveCommitType)
|
||||
}
|
||||
|
||||
// Check that the signalled type matches what we expect.
|
||||
switch {
|
||||
case expType == lnrpc.CommitmentType_ANCHORS &&
|
||||
chansCommitType == lnrpc.CommitmentType_ANCHORS:
|
||||
|
||||
case expType == lnrpc.CommitmentType_STATIC_REMOTE_KEY &&
|
||||
chansCommitType == lnrpc.CommitmentType_STATIC_REMOTE_KEY:
|
||||
|
||||
case expType == lnrpc.CommitmentType_LEGACY &&
|
||||
chansCommitType == lnrpc.CommitmentType_LEGACY:
|
||||
|
||||
default:
|
||||
ht.Fatalf("expected nodes to signal "+
|
||||
"commit type %v, instead got "+
|
||||
"%v", expType, chansCommitType)
|
||||
}
|
||||
|
||||
// As we've concluded this sub-test case we'll now close out
|
||||
// the channel for both sides.
|
||||
closeChan()
|
||||
}
|
||||
|
||||
test:
|
||||
// We'll test all possible combinations of the feature bit presence
|
||||
// that both nodes can signal for this new channel type. We'll make a
|
||||
// new Carol+Dave for each test instance as well.
|
||||
for _, carolCommitType := range allTypes {
|
||||
for _, daveCommitType := range allTypes {
|
||||
cc := carolCommitType
|
||||
dc := daveCommitType
|
||||
|
||||
testName := fmt.Sprintf(
|
||||
"carol_commit=%v,dave_commit=%v", cc, dc,
|
||||
)
|
||||
success := ht.Run(testName, func(t *testing.T) {
|
||||
st := ht.Subtest(t)
|
||||
testFunding(st, cc, dc)
|
||||
})
|
||||
|
||||
if !success {
|
||||
break test
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// basicChannelFundingTest is a sub-test of the main testBasicChannelFunding
|
||||
// test. Given two nodes: Alice and Bob, it'll assert proper channel creation,
|
||||
// then return a function closure that should be called to assert proper
|
||||
// channel closure.
|
||||
func basicChannelFundingTest(ht *lntemp.HarnessTest,
|
||||
alice, bob *node.HarnessNode,
|
||||
fundingShim *lnrpc.FundingShim) (*lnrpc.Channel,
|
||||
*lnrpc.Channel, func()) {
|
||||
|
||||
chanAmt := funding.MaxBtcFundingAmount
|
||||
pushAmt := btcutil.Amount(100000)
|
||||
satPerVbyte := btcutil.Amount(1)
|
||||
|
||||
// Record nodes' channel balance before testing.
|
||||
aliceChannelBalance := alice.RPC.ChannelBalance()
|
||||
bobChannelBalance := bob.RPC.ChannelBalance()
|
||||
|
||||
// Creates a helper closure to be used below which asserts the proper
|
||||
// response to a channel balance RPC.
|
||||
checkChannelBalance := func(node *node.HarnessNode,
|
||||
oldChannelBalance *lnrpc.ChannelBalanceResponse,
|
||||
local, remote btcutil.Amount) {
|
||||
|
||||
newResp := oldChannelBalance
|
||||
|
||||
newResp.LocalBalance.Sat += uint64(local)
|
||||
newResp.LocalBalance.Msat += uint64(
|
||||
lnwire.NewMSatFromSatoshis(local),
|
||||
)
|
||||
newResp.RemoteBalance.Sat += uint64(remote)
|
||||
newResp.RemoteBalance.Msat += uint64(
|
||||
lnwire.NewMSatFromSatoshis(remote),
|
||||
)
|
||||
// Deprecated fields.
|
||||
newResp.Balance += int64(local) // nolint:staticcheck
|
||||
ht.AssertChannelBalanceResp(node, newResp)
|
||||
}
|
||||
|
||||
// First establish a channel with a capacity of 0.5 BTC between Alice
|
||||
// and Bob with Alice pushing 100k satoshis to Bob's side during
|
||||
// funding. This function will block until the channel itself is fully
|
||||
// open or an error occurs in the funding process. A series of
|
||||
// assertions will be executed to ensure the funding process completed
|
||||
// successfully.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
FundingShim: fundingShim,
|
||||
SatPerVByte: satPerVbyte,
|
||||
},
|
||||
)
|
||||
|
||||
cType := ht.GetChannelCommitType(alice, chanPoint)
|
||||
|
||||
// With the channel open, ensure that the amount specified above has
|
||||
// properly been pushed to Bob.
|
||||
aliceLocalBalance := chanAmt - pushAmt - calcStaticFee(cType, 0)
|
||||
checkChannelBalance(
|
||||
alice, aliceChannelBalance, aliceLocalBalance, pushAmt,
|
||||
)
|
||||
checkChannelBalance(
|
||||
bob, bobChannelBalance, pushAmt, aliceLocalBalance,
|
||||
)
|
||||
|
||||
aliceChannel := ht.GetChannelByChanPoint(alice, chanPoint)
|
||||
bobChannel := ht.GetChannelByChanPoint(bob, chanPoint)
|
||||
|
||||
closeChan := func() {
|
||||
// Finally, immediately close the channel. This function will
|
||||
// also block until the channel is closed and will additionally
|
||||
// assert the relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
return aliceChannel, bobChannel, closeChan
|
||||
}
|
||||
|
||||
// testUnconfirmedChannelFunding tests that our unconfirmed change outputs can
|
||||
// be used to fund channels.
|
||||
func testUnconfirmedChannelFunding(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
pushAmt = btcutil.Amount(100000)
|
||||
)
|
||||
|
||||
// We'll start off by creating a node for Carol.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
alice := ht.Alice
|
||||
|
||||
// We'll send her some unconfirmed funds.
|
||||
ht.FundCoinsUnconfirmed(2*chanAmt, carol)
|
||||
|
||||
// Now, we'll connect her to Alice so that they can open a channel
|
||||
// together. The funding flow should select Carol's unconfirmed output
|
||||
// as she doesn't have any other funds since it's a new node.
|
||||
ht.ConnectNodes(carol, alice)
|
||||
|
||||
chanOpenUpdate := ht.OpenChannelAssertStream(
|
||||
carol, alice, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
SpendUnconfirmed: true,
|
||||
},
|
||||
)
|
||||
|
||||
// Creates a helper closure to be used below which asserts the proper
|
||||
// response to a channel balance RPC.
|
||||
checkChannelBalance := func(node *node.HarnessNode,
|
||||
local, remote, pendingLocal, pendingRemote btcutil.Amount) {
|
||||
|
||||
expectedResponse := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(local),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
local,
|
||||
)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(remote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
remote,
|
||||
)),
|
||||
},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(pendingLocal),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
pendingLocal,
|
||||
)),
|
||||
},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(pendingRemote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
pendingRemote,
|
||||
)),
|
||||
},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(local),
|
||||
PendingOpenBalance: int64(pendingLocal),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(node, expectedResponse)
|
||||
}
|
||||
|
||||
// As the channel is pending open, it's expected Carol has both zero
|
||||
// local and remote balances, and pending local/remote should not be
|
||||
// zero.
|
||||
//
|
||||
// Note that atm we haven't obtained the chanPoint yet, so we use the
|
||||
// type directly.
|
||||
cType := lnrpc.CommitmentType_STATIC_REMOTE_KEY
|
||||
carolLocalBalance := chanAmt - pushAmt - calcStaticFee(cType, 0)
|
||||
checkChannelBalance(carol, 0, 0, carolLocalBalance, pushAmt)
|
||||
|
||||
// For Alice, her local/remote balances should be zero, and the
|
||||
// local/remote balances are the mirror of Carol's.
|
||||
checkChannelBalance(alice, 0, 0, pushAmt, carolLocalBalance)
|
||||
|
||||
// Confirm the channel and wait for it to be recognized by both
|
||||
// parties. For neutrino backend, the funding transaction should be
|
||||
// mined. Otherwise, two transactions should be mined, the unconfirmed
|
||||
// spend and the funding tx.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.MineBlocksAndAssertNumTxes(6, 1)
|
||||
} else {
|
||||
ht.MineBlocksAndAssertNumTxes(6, 2)
|
||||
}
|
||||
|
||||
chanPoint := ht.WaitForChannelOpenEvent(chanOpenUpdate)
|
||||
|
||||
// With the channel open, we'll check the balances on each side of the
|
||||
// channel as a sanity check to ensure things worked out as intended.
|
||||
checkChannelBalance(carol, carolLocalBalance, pushAmt, 0, 0)
|
||||
checkChannelBalance(alice, pushAmt, carolLocalBalance, 0, 0)
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
//
|
||||
// We may get the error `unable to gracefully close channel while peer
|
||||
// is offline (try force closing it instead): channel link not found`.
|
||||
// This happens because the channel link hasn't been added yet but we
|
||||
// now proceed to closing the channel. We may need to revisit how the
|
||||
// channel open event is created and make sure the event is only sent
|
||||
// after all relevant states have been updated.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Now that we're done with the test, the channel can be closed.
|
||||
ht.CloseChannel(carol, chanPoint)
|
||||
}
|
||||
|
||||
// testChannelFundingInputTypes tests that any type of supported input type can
|
||||
// be used to fund channels.
|
||||
func testChannelFundingInputTypes(ht *lntemp.HarnessTest) {
|
||||
// We'll start off by creating a node for Carol.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Now, we'll connect her to Alice so that they can open a
|
||||
// channel together.
|
||||
ht.ConnectNodes(carol, ht.Alice)
|
||||
|
||||
runChannelFundingInputTypes(ht, ht.Alice, carol)
|
||||
}
|
||||
|
||||
// runChannelFundingInputTypes tests that any type of supported input type can
|
||||
// be used to fund channels.
|
||||
func runChannelFundingInputTypes(ht *lntemp.HarnessTest, alice,
|
||||
carol *node.HarnessNode) {
|
||||
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
burnAddr = "bcrt1qxsnqpdc842lu8c0xlllgvejt6rhy49u6fmpgyz"
|
||||
)
|
||||
|
||||
fundMixed := func(amt btcutil.Amount, target *node.HarnessNode) {
|
||||
ht.FundCoins(amt/5, target)
|
||||
ht.FundCoins(amt/5, target)
|
||||
ht.FundCoinsP2TR(amt/5, target)
|
||||
ht.FundCoinsP2TR(amt/5, target)
|
||||
ht.FundCoinsP2TR(amt/5, target)
|
||||
}
|
||||
fundMultipleP2TR := func(amt btcutil.Amount, target *node.HarnessNode) {
|
||||
ht.FundCoinsP2TR(amt/4, target)
|
||||
ht.FundCoinsP2TR(amt/4, target)
|
||||
ht.FundCoinsP2TR(amt/4, target)
|
||||
ht.FundCoinsP2TR(amt/4, target)
|
||||
}
|
||||
fundWithTypes := []func(amt btcutil.Amount, target *node.HarnessNode){
|
||||
ht.FundCoins, ht.FundCoinsNP2WKH, ht.FundCoinsP2TR, fundMixed,
|
||||
fundMultipleP2TR,
|
||||
}
|
||||
|
||||
// Creates a helper closure to be used below which asserts the
|
||||
// proper response to a channel balance RPC.
|
||||
checkChannelBalance := func(node *node.HarnessNode, local,
|
||||
remote, pendingLocal, pendingRemote btcutil.Amount) {
|
||||
|
||||
expectedResponse := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(local),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(local)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(remote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
remote,
|
||||
)),
|
||||
},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(pendingLocal),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
pendingLocal,
|
||||
)),
|
||||
},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(pendingRemote),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
pendingRemote,
|
||||
)),
|
||||
},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(local),
|
||||
PendingOpenBalance: int64(pendingLocal),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(node, expectedResponse)
|
||||
}
|
||||
|
||||
for _, funder := range fundWithTypes {
|
||||
// We'll send her some confirmed funds. We send 10% more than
|
||||
// we need to account for fees.
|
||||
funder((chanAmt*11)/10, carol)
|
||||
|
||||
chanOpenUpdate := ht.OpenChannelAssertStream(
|
||||
carol, alice, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// As the channel is pending open, it's expected Carol has both
|
||||
// zero local and remote balances, and pending local/remote
|
||||
// should not be zero.
|
||||
//
|
||||
// Note that atm we haven't obtained the chanPoint yet, so we
|
||||
// use the type directly.
|
||||
cType := lnrpc.CommitmentType_STATIC_REMOTE_KEY
|
||||
carolLocalBalance := chanAmt - calcStaticFee(cType, 0)
|
||||
checkChannelBalance(carol, 0, 0, carolLocalBalance, 0)
|
||||
|
||||
// For Alice, her local/remote balances should be zero, and the
|
||||
// local/remote balances are the mirror of Carol's.
|
||||
checkChannelBalance(alice, 0, 0, 0, carolLocalBalance)
|
||||
|
||||
// Confirm the channel and wait for it to be recognized by both
|
||||
// parties. Two transactions should be mined, the unconfirmed
|
||||
// spend and the funding tx.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
chanPoint := ht.WaitForChannelOpenEvent(chanOpenUpdate)
|
||||
|
||||
// With the channel open, we'll check the balances on each side
|
||||
// of the channel as a sanity check to ensure things worked out
|
||||
// as intended.
|
||||
checkChannelBalance(carol, carolLocalBalance, 0, 0, 0)
|
||||
checkChannelBalance(alice, 0, carolLocalBalance, 0, 0)
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
//
|
||||
// We may get the error `unable to gracefully close channel
|
||||
// while peer is offline (try force closing it instead):
|
||||
// channel link not found`. This happens because the channel
|
||||
// link hasn't been added yet but we now proceed to closing the
|
||||
// channel. We may need to revisit how the channel open event
|
||||
// is created and make sure the event is only sent after all
|
||||
// relevant states have been updated.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Now that we're done with the test, the channel can be closed.
|
||||
ht.CloseChannel(carol, chanPoint)
|
||||
|
||||
// Empty out the wallet so there aren't any lingering coins.
|
||||
sendAllCoinsConfirm(ht, carol, burnAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// sendAllCoinsConfirm sends all coins of the node's wallet to the given address
|
||||
// and awaits one confirmation.
|
||||
func sendAllCoinsConfirm(ht *lntemp.HarnessTest, node *node.HarnessNode,
|
||||
addr string) {
|
||||
|
||||
sweepReq := &lnrpc.SendCoinsRequest{
|
||||
Addr: addr,
|
||||
SendAll: true,
|
||||
}
|
||||
node.RPC.SendCoins(sweepReq)
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
}
|
||||
|
||||
// testExternalFundingChanPoint tests that we're able to carry out a normal
|
||||
// channel funding workflow given a channel point that was constructed outside
|
||||
// the main daemon.
|
||||
func testExternalFundingChanPoint(ht *lntemp.HarnessTest) {
|
||||
// First, we'll create two new nodes that we'll use to open channel
|
||||
// between for this test.
|
||||
carol := ht.NewNode("carol", nil)
|
||||
dave := ht.NewNode("dave", nil)
|
||||
|
||||
// Carol will be funding the channel, so we'll send some coins over to
|
||||
// her and ensure they have enough confirmations before we proceed.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// Before we start the test, we'll ensure both sides are connected to
|
||||
// the funding flow can properly be executed.
|
||||
ht.EnsureConnected(carol, dave)
|
||||
|
||||
// At this point, we're ready to simulate our external channel funding
|
||||
// flow. To start with, we'll create a pending channel with a shim for
|
||||
// a transaction that will never be published.
|
||||
const thawHeight uint32 = 10
|
||||
const chanSize = funding.MaxBtcFundingAmount
|
||||
fundingShim1, chanPoint1, _ := deriveFundingShim(
|
||||
ht, carol, dave, chanSize, thawHeight, false,
|
||||
)
|
||||
ht.OpenChannelAssertPending(
|
||||
carol, dave, lntemp.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
FundingShim: fundingShim1,
|
||||
},
|
||||
)
|
||||
ht.AssertNodesNumPendingOpenChannels(carol, dave, 1)
|
||||
|
||||
// That channel is now pending forever and normally would saturate the
|
||||
// max pending channel limit for both nodes. But because the channel is
|
||||
// externally funded, we should still be able to open another one. Let's
|
||||
// do exactly that now. For this one we publish the transaction so we
|
||||
// can mine it later.
|
||||
fundingShim2, chanPoint2, _ := deriveFundingShim(
|
||||
ht, carol, dave, chanSize, thawHeight, true,
|
||||
)
|
||||
|
||||
// At this point, we'll now carry out the normal basic channel funding
|
||||
// test as everything should now proceed as normal (a regular channel
|
||||
// funding flow).
|
||||
carolChan, daveChan, _ := basicChannelFundingTest(
|
||||
ht, carol, dave, fundingShim2,
|
||||
)
|
||||
|
||||
// Both channels should be marked as frozen with the proper thaw
|
||||
// height.
|
||||
require.Equal(ht, thawHeight, carolChan.ThawHeight,
|
||||
"thaw height unmatched")
|
||||
require.Equal(ht, thawHeight, daveChan.ThawHeight,
|
||||
"thaw height unmatched")
|
||||
|
||||
// Next, to make sure the channel functions as normal, we'll make some
|
||||
// payments within the channel.
|
||||
payAmt := btcutil.Amount(100000)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
resp := dave.RPC.AddInvoice(invoice)
|
||||
ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest})
|
||||
|
||||
// Now that the channels are open, and we've confirmed that they're
|
||||
// operational, we'll now ensure that the channels are frozen as
|
||||
// intended (if requested).
|
||||
//
|
||||
// First, we'll try to close the channel as Carol, the initiator. This
|
||||
// should fail as a frozen channel only allows the responder to
|
||||
// initiate a channel close.
|
||||
err := ht.CloseChannelAssertErr(carol, chanPoint2, false)
|
||||
require.Contains(ht, err.Error(), "cannot co-op close frozen channel")
|
||||
|
||||
// Before Dave closes the channel, he needs to check the invoice is
|
||||
// settled to avoid an error saying cannot close channel due to active
|
||||
// HTLCs.
|
||||
ht.AssertInvoiceSettled(dave, resp.PaymentAddr)
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Next we'll try but this time with Dave (the responder) as the
|
||||
// initiator. This time the channel should be closed as normal.
|
||||
ht.CloseChannel(dave, chanPoint2)
|
||||
|
||||
// As a last step, we check if we still have the pending channel
|
||||
// hanging around because we never published the funding TX.
|
||||
ht.AssertNodesNumPendingOpenChannels(carol, dave, 1)
|
||||
|
||||
// Let's make sure we can abandon it.
|
||||
carol.RPC.AbandonChannel(&lnrpc.AbandonChannelRequest{
|
||||
ChannelPoint: chanPoint1,
|
||||
PendingFundingShimOnly: true,
|
||||
})
|
||||
dave.RPC.AbandonChannel(&lnrpc.AbandonChannelRequest{
|
||||
ChannelPoint: chanPoint1,
|
||||
PendingFundingShimOnly: true,
|
||||
})
|
||||
|
||||
// It should now not appear in the pending channels anymore.
|
||||
ht.AssertNodesNumPendingOpenChannels(carol, dave, 0)
|
||||
}
|
||||
|
||||
// testFundingPersistence is intended to ensure that the Funding Manager
|
||||
// persists the state of new channels prior to broadcasting the channel's
|
||||
// funding transaction. This ensures that the daemon maintains an up-to-date
|
||||
// representation of channels if the system is restarted or disconnected.
|
||||
// testFundingPersistence mirrors testBasicChannelFunding, but adds restarts
|
||||
// and checks for the state of channels with unconfirmed funding transactions.
|
||||
func testChannelFundingPersistence(ht *lntemp.HarnessTest) {
|
||||
chanAmt := funding.MaxBtcFundingAmount
|
||||
pushAmt := btcutil.Amount(0)
|
||||
|
||||
// As we need to create a channel that requires more than 1
|
||||
// confirmation before it's open, with the current set of defaults,
|
||||
// we'll need to create a new node instance.
|
||||
const numConfs = 5
|
||||
carolArgs := []string{
|
||||
fmt.Sprintf("--bitcoin.defaultchanconfs=%v", numConfs),
|
||||
}
|
||||
carol := ht.NewNode("Carol", carolArgs)
|
||||
|
||||
alice := ht.Alice
|
||||
ht.ConnectNodes(alice, carol)
|
||||
|
||||
// Create a new channel that requires 5 confs before it's considered
|
||||
// open, then broadcast the funding transaction
|
||||
param := lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
}
|
||||
update := ht.OpenChannelAssertPending(alice, carol, param)
|
||||
|
||||
// At this point, the channel's funding transaction will have been
|
||||
// broadcast, but not confirmed. Alice and Bob's nodes should reflect
|
||||
// this when queried via RPC.
|
||||
ht.AssertNumPendingOpenChannels(alice, 1)
|
||||
ht.AssertNumPendingOpenChannels(carol, 1)
|
||||
|
||||
// Restart both nodes to test that the appropriate state has been
|
||||
// persisted and that both nodes recover gracefully.
|
||||
ht.RestartNode(alice)
|
||||
ht.RestartNode(carol)
|
||||
|
||||
fundingTxID, err := chainhash.NewHash(update.Txid)
|
||||
require.NoError(ht, err, "unable to convert funding txid "+
|
||||
"into chainhash.Hash")
|
||||
|
||||
// Mine a block, then wait for Alice's node to notify us that the
|
||||
// channel has been opened. The funding transaction should be found
|
||||
// within the newly mined block.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, fundingTxID)
|
||||
|
||||
// Get the height that our transaction confirmed at.
|
||||
_, height := ht.Miner.GetBestBlock()
|
||||
|
||||
// Restart both nodes to test that the appropriate state has been
|
||||
// persisted and that both nodes recover gracefully.
|
||||
ht.RestartNode(alice)
|
||||
ht.RestartNode(carol)
|
||||
|
||||
// The following block ensures that after both nodes have restarted,
|
||||
// they have reconnected before the execution of the next test.
|
||||
ht.EnsureConnected(alice, carol)
|
||||
|
||||
// Next, mine enough blocks s.t the channel will open with a single
|
||||
// additional block mined.
|
||||
ht.MineBlocks(3)
|
||||
|
||||
// Assert that our wallet has our opening transaction with a label
|
||||
// that does not have a channel ID set yet, because we have not
|
||||
// reached our required confirmations.
|
||||
tx := ht.AssertTxAtHeight(alice, height, fundingTxID)
|
||||
|
||||
// At this stage, we expect the transaction to be labelled, but not with
|
||||
// our channel ID because our transaction has not yet confirmed.
|
||||
label := labels.MakeLabel(labels.LabelTypeChannelOpen, nil)
|
||||
require.Equal(ht, label, tx.Label, "open channel label wrong")
|
||||
|
||||
// Both nodes should still show a single channel as pending.
|
||||
ht.AssertNumPendingOpenChannels(alice, 1)
|
||||
ht.AssertNumPendingOpenChannels(carol, 1)
|
||||
|
||||
// Finally, mine the last block which should mark the channel as open.
|
||||
ht.MineBlocks(1)
|
||||
|
||||
// At this point, the channel should be fully opened and there should
|
||||
// be no pending channels remaining for either node.
|
||||
ht.AssertNumPendingOpenChannels(alice, 0)
|
||||
ht.AssertNumPendingOpenChannels(carol, 0)
|
||||
|
||||
// The channel should be listed in the peer information returned by
|
||||
// both peers.
|
||||
chanPoint := lntemp.ChanPointFromPendingUpdate(update)
|
||||
|
||||
// Re-lookup our transaction in the block that it confirmed in.
|
||||
tx = ht.AssertTxAtHeight(alice, height, fundingTxID)
|
||||
|
||||
// Check both nodes to ensure that the channel is ready for operation.
|
||||
chanAlice := ht.AssertChannelExists(alice, chanPoint)
|
||||
ht.AssertChannelExists(carol, chanPoint)
|
||||
|
||||
// Create an additional check for our channel assertion that will
|
||||
// check that our label is as expected.
|
||||
shortChanID := lnwire.NewShortChanIDFromInt(chanAlice.ChanId)
|
||||
label = labels.MakeLabel(labels.LabelTypeChannelOpen, &shortChanID)
|
||||
require.Equal(ht, label, tx.Label, "open channel label not updated")
|
||||
|
||||
// Finally, immediately close the channel. This function will also
|
||||
// block until the channel is closed and will additionally assert the
|
||||
// relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testBatchChanFunding makes sure multiple channels can be opened in one batch
|
||||
// transaction in an atomic way.
|
||||
func testBatchChanFunding(ht *lntemp.HarnessTest) {
|
||||
// First, we'll create two new nodes that we'll use to open channels
|
||||
// to during this test. Carol has a high minimum funding amount that
|
||||
// we'll use to trigger an error during the batch channel open.
|
||||
carol := ht.NewNode("carol", []string{"--minchansize=200000"})
|
||||
dave := ht.NewNode("dave", nil)
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Before we start the test, we'll ensure Alice is connected to Carol
|
||||
// and Dave so she can open channels to both of them (and Bob).
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.EnsureConnected(alice, carol)
|
||||
ht.EnsureConnected(alice, dave)
|
||||
|
||||
// Let's create our batch TX request. This first one should fail as we
|
||||
// open a channel to Carol that is too small for her min chan size.
|
||||
batchReq := &lnrpc.BatchOpenChannelRequest{
|
||||
SatPerVbyte: 12,
|
||||
MinConfs: 1,
|
||||
Channels: []*lnrpc.BatchOpenChannel{{
|
||||
NodePubkey: bob.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}, {
|
||||
NodePubkey: carol.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}, {
|
||||
NodePubkey: dave.PubKey[:],
|
||||
LocalFundingAmount: 100_000,
|
||||
}},
|
||||
}
|
||||
|
||||
err := alice.RPC.BatchOpenChannelAssertErr(batchReq)
|
||||
require.Contains(ht, err.Error(), "initial negotiation failed")
|
||||
|
||||
// Let's fix the minimum amount for Alice now and try again.
|
||||
batchReq.Channels[1].LocalFundingAmount = 200_000
|
||||
batchResp := alice.RPC.BatchOpenChannel(batchReq)
|
||||
require.Len(ht, batchResp.PendingChannels, 3)
|
||||
|
||||
txHash, err := chainhash.NewHash(batchResp.PendingChannels[0].Txid)
|
||||
require.NoError(ht, err)
|
||||
|
||||
chanPoint1 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[0].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[0].OutputIndex,
|
||||
}
|
||||
chanPoint2 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[1].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[1].OutputIndex,
|
||||
}
|
||||
chanPoint3 := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: batchResp.PendingChannels[2].Txid,
|
||||
},
|
||||
OutputIndex: batchResp.PendingChannels[2].OutputIndex,
|
||||
}
|
||||
|
||||
block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, txHash)
|
||||
ht.AssertTopologyChannelOpen(alice, chanPoint1)
|
||||
ht.AssertTopologyChannelOpen(alice, chanPoint2)
|
||||
ht.AssertTopologyChannelOpen(alice, chanPoint3)
|
||||
|
||||
// With the channel open, ensure that it is counted towards Alice's
|
||||
// total channel balance.
|
||||
balRes := alice.RPC.ChannelBalance()
|
||||
require.NotEqual(ht, int64(0), balRes.LocalBalance.Sat)
|
||||
|
||||
// Next, to make sure the channel functions as normal, we'll make some
|
||||
// payments within the channel.
|
||||
payAmt := btcutil.Amount(100000)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
resp := carol.RPC.AddInvoice(invoice)
|
||||
ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest})
|
||||
|
||||
// To conclude, we'll close the newly created channel between Carol and
|
||||
// Dave. This function will also block until the channel is closed and
|
||||
// will additionally assert the relevant channel closing post
|
||||
// conditions.
|
||||
ht.CloseChannel(alice, chanPoint1)
|
||||
ht.CloseChannel(alice, chanPoint2)
|
||||
ht.CloseChannel(alice, chanPoint3)
|
||||
}
|
||||
|
||||
// deriveFundingShim creates a channel funding shim by deriving the necessary
|
||||
// keys on both sides.
|
||||
func deriveFundingShim(ht *lntemp.HarnessTest,
|
||||
carol, dave *node.HarnessNode, chanSize btcutil.Amount,
|
||||
thawHeight uint32, publish bool) (*lnrpc.FundingShim,
|
||||
*lnrpc.ChannelPoint, *chainhash.Hash) {
|
||||
|
||||
keyLoc := &signrpc.KeyLocator{KeyFamily: 9999}
|
||||
carolFundingKey := carol.RPC.DeriveKey(keyLoc)
|
||||
daveFundingKey := dave.RPC.DeriveKey(keyLoc)
|
||||
|
||||
// Now that we have the multi-sig keys for each party, we can manually
|
||||
// construct the funding transaction. We'll instruct the backend to
|
||||
// immediately create and broadcast a transaction paying out an exact
|
||||
// amount. Normally this would reside in the mempool, but we just
|
||||
// confirm it now for simplicity.
|
||||
_, fundingOutput, err := input.GenFundingPkScript(
|
||||
carolFundingKey.RawKeyBytes, daveFundingKey.RawKeyBytes,
|
||||
int64(chanSize),
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
var txid *chainhash.Hash
|
||||
targetOutputs := []*wire.TxOut{fundingOutput}
|
||||
if publish {
|
||||
txid = ht.Miner.SendOutputsWithoutChange(targetOutputs, 5)
|
||||
} else {
|
||||
tx := ht.Miner.CreateTransaction(targetOutputs, 5)
|
||||
|
||||
txHash := tx.TxHash()
|
||||
txid = &txHash
|
||||
}
|
||||
|
||||
// At this point, we can being our external channel funding workflow.
|
||||
// We'll start by generating a pending channel ID externally that will
|
||||
// be used to track this new funding type.
|
||||
pendingChanID := ht.Random32Bytes()
|
||||
|
||||
// Now that we have the pending channel ID, Dave (our responder) will
|
||||
// register the intent to receive a new channel funding workflow using
|
||||
// the pending channel ID.
|
||||
chanPoint := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: txid[:],
|
||||
},
|
||||
}
|
||||
chanPointShim := &lnrpc.ChanPointShim{
|
||||
Amt: int64(chanSize),
|
||||
ChanPoint: chanPoint,
|
||||
LocalKey: &lnrpc.KeyDescriptor{
|
||||
RawKeyBytes: daveFundingKey.RawKeyBytes,
|
||||
KeyLoc: &lnrpc.KeyLocator{
|
||||
KeyFamily: daveFundingKey.KeyLoc.KeyFamily,
|
||||
KeyIndex: daveFundingKey.KeyLoc.KeyIndex,
|
||||
},
|
||||
},
|
||||
RemoteKey: carolFundingKey.RawKeyBytes,
|
||||
PendingChanId: pendingChanID,
|
||||
ThawHeight: thawHeight,
|
||||
}
|
||||
fundingShim := &lnrpc.FundingShim{
|
||||
Shim: &lnrpc.FundingShim_ChanPointShim{
|
||||
ChanPointShim: chanPointShim,
|
||||
},
|
||||
}
|
||||
dave.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{
|
||||
Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{
|
||||
ShimRegister: fundingShim,
|
||||
},
|
||||
})
|
||||
|
||||
// If we attempt to register the same shim (has the same pending chan
|
||||
// ID), then we should get an error.
|
||||
dave.RPC.FundingStateStepAssertErr(&lnrpc.FundingTransitionMsg{
|
||||
Trigger: &lnrpc.FundingTransitionMsg_ShimRegister{
|
||||
ShimRegister: fundingShim,
|
||||
},
|
||||
})
|
||||
|
||||
// We'll take the chan point shim we just registered for Dave (the
|
||||
// responder), and swap the local/remote keys before we feed it in as
|
||||
// Carol's funding shim as the initiator.
|
||||
fundingShim.GetChanPointShim().LocalKey = &lnrpc.KeyDescriptor{
|
||||
RawKeyBytes: carolFundingKey.RawKeyBytes,
|
||||
KeyLoc: &lnrpc.KeyLocator{
|
||||
KeyFamily: carolFundingKey.KeyLoc.KeyFamily,
|
||||
KeyIndex: carolFundingKey.KeyLoc.KeyIndex,
|
||||
},
|
||||
}
|
||||
fundingShim.GetChanPointShim().RemoteKey = daveFundingKey.RawKeyBytes
|
||||
|
||||
return fundingShim, chanPoint, txid
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testHoldInvoiceForceClose tests cancellation of accepted hold invoices which
|
||||
// would otherwise trigger force closes when they expire.
|
||||
func testHoldInvoiceForceClose(ht *lntemp.HarnessTest) {
|
||||
// Open a channel between alice and bob.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: 300000},
|
||||
)
|
||||
|
||||
// Create a non-dust hold invoice for bob.
|
||||
var (
|
||||
preimage = lntypes.Preimage{1, 2, 3}
|
||||
payHash = preimage.Hash()
|
||||
)
|
||||
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Value: 30000,
|
||||
CltvExpiry: 40,
|
||||
Hash: payHash[:],
|
||||
}
|
||||
bobInvoice := bob.RPC.AddHoldInvoice(invoiceReq)
|
||||
|
||||
// Subscribe the invoice.
|
||||
stream := bob.RPC.SubscribeSingleInvoice(payHash[:])
|
||||
|
||||
// Pay this invoice from Alice -> Bob, we should achieve this with a
|
||||
// single htlc.
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: bobInvoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
alice.RPC.SendPayment(req)
|
||||
|
||||
ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED)
|
||||
|
||||
// Once the HTLC has cleared, alice and bob should both have a single
|
||||
// htlc locked in.
|
||||
ht.AssertActiveHtlcs(alice, payHash[:])
|
||||
ht.AssertActiveHtlcs(bob, payHash[:])
|
||||
|
||||
// Get our htlc expiry height and current block height so that we
|
||||
// can mine the exact number of blocks required to expire the htlc.
|
||||
channel := ht.QueryChannelByChanPoint(alice, chanPoint)
|
||||
require.Len(ht, channel.PendingHtlcs, 1)
|
||||
activeHtlc := channel.PendingHtlcs[0]
|
||||
|
||||
_, currentHeight := ht.Miner.GetBestBlock()
|
||||
|
||||
// Now we will mine blocks until the htlc expires, and wait for each
|
||||
// node to sync to our latest height. Sanity check that we won't
|
||||
// underflow.
|
||||
require.Greater(ht, activeHtlc.ExpirationHeight, uint32(currentHeight),
|
||||
"expected expiry after current height")
|
||||
blocksTillExpiry := activeHtlc.ExpirationHeight - uint32(currentHeight)
|
||||
|
||||
// Alice will go to chain with some delta, sanity check that we won't
|
||||
// underflow and subtract this from our mined blocks.
|
||||
require.Greater(ht, blocksTillExpiry,
|
||||
uint32(lncfg.DefaultOutgoingBroadcastDelta))
|
||||
|
||||
// blocksTillForce is the number of blocks should be mined to
|
||||
// trigger a force close from Alice iff the invoice cancelation
|
||||
// failed. This value is 48 in current test setup.
|
||||
blocksTillForce := blocksTillExpiry -
|
||||
lncfg.DefaultOutgoingBroadcastDelta
|
||||
|
||||
// blocksTillCancel is the number of blocks should be mined to trigger
|
||||
// an invoice cancelation from Bob. This value is 30 in current test
|
||||
// setup.
|
||||
blocksTillCancel := blocksTillExpiry -
|
||||
lncfg.DefaultHoldInvoiceExpiryDelta
|
||||
|
||||
// When using ht.MineBlocks, for bitcoind backend, the block height
|
||||
// synced differ significantly among subsystems. From observation, the
|
||||
// LNWL syncs much faster than other subsystems, with more than 10
|
||||
// blocks ahead. For this test case, CRTR may be lagging behind for
|
||||
// more than 20 blocks. Thus we use slow mining instead.
|
||||
// TODO(yy): fix block height asymmetry among all the subsystems.
|
||||
//
|
||||
// We first mine enough blocks to trigger an invoice cancelation.
|
||||
ht.MineBlocks(blocksTillCancel)
|
||||
|
||||
// Wait for the nodes to be synced.
|
||||
ht.WaitForBlockchainSync(alice)
|
||||
ht.WaitForBlockchainSync(bob)
|
||||
|
||||
// Check that the invoice is canceled by Bob.
|
||||
err := wait.NoError(func() error {
|
||||
inv := bob.RPC.LookupInvoice(payHash[:])
|
||||
|
||||
if inv.State != lnrpc.Invoice_CANCELED {
|
||||
return fmt.Errorf("expected canceled invoice, got: %v",
|
||||
inv.State)
|
||||
}
|
||||
|
||||
for _, htlc := range inv.Htlcs {
|
||||
if htlc.State != lnrpc.InvoiceHTLCState_CANCELED {
|
||||
return fmt.Errorf("expected htlc canceled, "+
|
||||
"got: %v", htlc.State)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "expected canceled invoice")
|
||||
|
||||
// We now continue to mine more blocks to the point where it could have
|
||||
// triggered a force close if the invoice cancelation was failed.
|
||||
//
|
||||
// NOTE: we need to mine blocks in two sections because of a following
|
||||
// case has happened frequently with bitcoind backend,
|
||||
// - when mining all the blocks together, subsystems were syncing
|
||||
// blocks under very different speed.
|
||||
// - Bob would cancel the invoice in INVC, and send an UpdateFailHTLC
|
||||
// in PEER.
|
||||
// - Alice, however, would need to receive the message before her
|
||||
// subsystem CNCT being synced to the force close height. This didn't
|
||||
// happen in bitcoind backend, as Alice's CNCT was syncing way faster
|
||||
// than Bob's INVC, causing the channel being force closed before the
|
||||
// invoice cancelation message was received by Alice.
|
||||
ht.MineBlocks(blocksTillForce - blocksTillCancel)
|
||||
|
||||
// Wait for the nodes to be synced.
|
||||
ht.WaitForBlockchainSync(alice)
|
||||
ht.WaitForBlockchainSync(bob)
|
||||
|
||||
// Check that Alice has not closed the channel because there are no
|
||||
// outgoing HTLCs in her channel as the only HTLC has already been
|
||||
// canceled.
|
||||
ht.AssertNumPendingForceClose(alice, 0)
|
||||
|
||||
// Clean up the channel.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/rpc"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testHoldInvoicePersistence tests that a sender to a hold-invoice, can be
|
||||
// restarted before the payment gets settled, and still be able to receive the
|
||||
// preimage.
|
||||
func testHoldInvoicePersistence(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(1000000)
|
||||
numPayments = 10
|
||||
reason = lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS //nolint:lll
|
||||
)
|
||||
|
||||
// Create carol, and clean up when the test finishes.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Connect Alice to Carol.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
ht.ConnectNodes(alice, carol)
|
||||
|
||||
// Open a channel between Alice and Carol which is private so that we
|
||||
// cover the addition of hop hints for hold invoices.
|
||||
chanPointAlice := ht.OpenChannel(
|
||||
alice, carol, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
Private: true,
|
||||
},
|
||||
)
|
||||
|
||||
// For Carol to include her private channel with Alice as a hop hint,
|
||||
// we need Alice to be perceived as a "public" node, meaning that she
|
||||
// has at least one public channel in the graph. We open a public
|
||||
// channel from Alice -> Bob and wait for Carol to see it.
|
||||
chanPointBob := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for Carol to see the open channel Alice-Bob.
|
||||
ht.AssertTopologyChannelOpen(carol, chanPointBob)
|
||||
|
||||
// Create preimages for all payments we are going to initiate.
|
||||
var preimages []lntypes.Preimage
|
||||
for i := 0; i < numPayments; i++ {
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], ht.Random32Bytes())
|
||||
preimages = append(preimages, preimage)
|
||||
}
|
||||
|
||||
// Let Carol create hold-invoices for all the payments.
|
||||
var (
|
||||
payAmt = btcutil.Amount(4)
|
||||
payReqs []string
|
||||
invoiceStreams []rpc.SingleInvoiceClient
|
||||
)
|
||||
|
||||
assertInvoiceState := func(state lnrpc.Invoice_InvoiceState) {
|
||||
for _, client := range invoiceStreams {
|
||||
ht.AssertInvoiceState(client, state)
|
||||
}
|
||||
}
|
||||
|
||||
for _, preimage := range preimages {
|
||||
payHash := preimage.Hash()
|
||||
|
||||
// Make our invoices private so that we get coverage for adding
|
||||
// hop hints.
|
||||
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Memo: "testing",
|
||||
Value: int64(payAmt),
|
||||
Hash: payHash[:],
|
||||
Private: true,
|
||||
}
|
||||
resp := carol.RPC.AddHoldInvoice(invoiceReq)
|
||||
payReqs = append(payReqs, resp.PaymentRequest)
|
||||
|
||||
// We expect all of our invoices to have hop hints attached,
|
||||
// since Carol and Alice are connected with a private channel.
|
||||
// We assert that we have one hop hint present to ensure that
|
||||
// we've got coverage for hop hints.
|
||||
invoice := alice.RPC.DecodePayReq(resp.PaymentRequest)
|
||||
require.Len(ht, invoice.RouteHints, 1)
|
||||
|
||||
stream := carol.RPC.SubscribeSingleInvoice(payHash[:])
|
||||
invoiceStreams = append(invoiceStreams, stream)
|
||||
}
|
||||
|
||||
// Wait for all the invoices to reach the OPEN state.
|
||||
assertInvoiceState(lnrpc.Invoice_OPEN)
|
||||
|
||||
// Let Alice initiate payments for all the created invoices.
|
||||
for _, payReq := range payReqs {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitSat: 1000000,
|
||||
}
|
||||
|
||||
// Wait for inflight status update.
|
||||
ht.SendPaymentAndAssertStatus(
|
||||
alice, req, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
}
|
||||
|
||||
// The payments should now show up in Alice's ListPayments, with a zero
|
||||
// preimage, indicating they are not yet settled.
|
||||
var zeroPreimg lntypes.Preimage
|
||||
err := wait.NoError(func() error {
|
||||
payments := ht.AssertNumPayments(alice, numPayments)
|
||||
|
||||
// Gather the payment hashes we are looking for in the
|
||||
// response.
|
||||
payHashes := make(map[string]struct{})
|
||||
for _, preimg := range preimages {
|
||||
payHashes[preimg.Hash().String()] = struct{}{}
|
||||
}
|
||||
|
||||
for _, payment := range payments {
|
||||
_, ok := payHashes[payment.PaymentHash]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// The preimage should NEVER be non-zero at this point.
|
||||
require.Equal(ht, zeroPreimg.String(),
|
||||
payment.PaymentPreimage,
|
||||
"expected zero preimage")
|
||||
|
||||
// We wait for the payment attempt to have been
|
||||
// properly recorded in the DB.
|
||||
if len(payment.Htlcs) == 0 {
|
||||
return fmt.Errorf("no attempt recorded")
|
||||
}
|
||||
|
||||
delete(payHashes, payment.PaymentHash)
|
||||
}
|
||||
|
||||
if len(payHashes) != 0 {
|
||||
return fmt.Errorf("payhash not found in response")
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking alice's payments")
|
||||
|
||||
// Wait for all invoices to be accepted.
|
||||
assertInvoiceState(lnrpc.Invoice_ACCEPTED)
|
||||
|
||||
// Restart alice. This to ensure she will still be able to handle
|
||||
// settling the invoices after a restart.
|
||||
ht.RestartNode(alice)
|
||||
|
||||
// Ensure the connections are made.
|
||||
//
|
||||
// TODO(yy): we shouldn't need these two lines since the connections
|
||||
// are permanent, they'd reconnect automatically upon Alice's restart.
|
||||
// However, we'd sometimes see the error `unable to gracefully close
|
||||
// channel while peer is offline (try force closing it instead):
|
||||
// channel link not found` from closing the channels in the end,
|
||||
// indicating there's something wrong with the peer conn. We need to
|
||||
// investigate and fix it in peer conn management.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.EnsureConnected(alice, carol)
|
||||
|
||||
// Now after a restart, we must re-track the payments. We set up a
|
||||
// goroutine for each to track their status updates.
|
||||
for _, preimg := range preimages {
|
||||
hash := preimg.Hash()
|
||||
|
||||
payStream := alice.RPC.TrackPaymentV2(hash[:])
|
||||
ht.ReceiveTrackPayment(payStream)
|
||||
|
||||
ht.AssertPaymentStatus(
|
||||
alice, preimg, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
}
|
||||
|
||||
// Settle invoices half the invoices, cancel the rest.
|
||||
for i, preimage := range preimages {
|
||||
if i%2 == 0 {
|
||||
carol.RPC.SettleInvoice(preimage[:])
|
||||
ht.AssertInvoiceState(
|
||||
invoiceStreams[i], lnrpc.Invoice_SETTLED,
|
||||
)
|
||||
} else {
|
||||
hash := preimage.Hash()
|
||||
carol.RPC.CancelInvoice(hash[:])
|
||||
ht.AssertInvoiceState(
|
||||
invoiceStreams[i], lnrpc.Invoice_CANCELED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that Alice's invoices to be shown as settled and failed
|
||||
// accordingly, and preimages matching up.
|
||||
for i, preimg := range preimages {
|
||||
if i%2 == 0 {
|
||||
ht.AssertPaymentStatus(
|
||||
alice, preimg, lnrpc.Payment_SUCCEEDED,
|
||||
)
|
||||
} else {
|
||||
payment := ht.AssertPaymentStatus(
|
||||
alice, preimg, lnrpc.Payment_FAILED,
|
||||
)
|
||||
require.Equal(ht, reason, payment.FailureReason,
|
||||
"wrong failure reason")
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, close all channels.
|
||||
ht.CloseChannel(alice, chanPointBob)
|
||||
ht.CloseChannel(alice, chanPointAlice)
|
||||
}
|
||||
@@ -1,728 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
// testMacaroonAuthentication makes sure that if macaroon authentication is
|
||||
// enabled on the gRPC interface, no requests with missing or invalid
|
||||
// macaroons are allowed. Further, the specific access rights (read/write,
|
||||
// entity based) and first-party caveats are tested as well.
|
||||
func testMacaroonAuthentication(ht *lntemp.HarnessTest) {
|
||||
var (
|
||||
infoReq = &lnrpc.GetInfoRequest{}
|
||||
newAddrReq = &lnrpc.NewAddressRequest{
|
||||
Type: AddrTypeWitnessPubkeyHash,
|
||||
}
|
||||
testNode = ht.Alice
|
||||
testClient = testNode.RPC.LN
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(ctxt context.Context, t *testing.T)
|
||||
}{{
|
||||
// First test: Make sure we get an error if we use no macaroons
|
||||
// but try to connect to a node that has macaroon authentication
|
||||
// enabled.
|
||||
name: "no macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
conn, err := testNode.ConnectRPCWithMacaroon(nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = conn.Close() }()
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "expected 1 macaroon")
|
||||
},
|
||||
}, {
|
||||
// Second test: Ensure that an invalid macaroon also triggers an
|
||||
// error.
|
||||
name: "invalid macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
invalidMac, _ := macaroon.New(
|
||||
[]byte("dummy_root_key"), []byte("0"), "itest",
|
||||
macaroon.LatestVersion,
|
||||
)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, invalidMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err := client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot get macaroon")
|
||||
},
|
||||
}, {
|
||||
// Third test: Try to access a write method with read-only
|
||||
// macaroon.
|
||||
name: "read only macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.ReadMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, readonlyMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.NewAddress(ctxt, newAddrReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}, {
|
||||
// Fourth test: Check first-party caveat with timeout that
|
||||
// expired 30 seconds ago.
|
||||
name: "expired macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.ReadMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
timeoutMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.TimeoutConstraint(-30),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, timeoutMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "macaroon has expired")
|
||||
},
|
||||
}, {
|
||||
// Fifth test: Check first-party caveat with invalid IP address.
|
||||
name: "invalid IP macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
readonlyMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.ReadMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
invalidIPAddrMac, err := macaroons.AddConstraints(
|
||||
readonlyMac, macaroons.IPLockConstraint(
|
||||
"1.1.1.1",
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, invalidIPAddrMac,
|
||||
)
|
||||
defer cleanup()
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "different IP address")
|
||||
},
|
||||
}, {
|
||||
// Sixth test: Make sure that if we do everything correct and
|
||||
// send the admin macaroon with first-party caveats that we can
|
||||
// satisfy, we get a correct answer.
|
||||
name: "correct macaroon",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
adminMac, err = macaroons.AddConstraints(
|
||||
adminMac, macaroons.TimeoutConstraint(30),
|
||||
macaroons.IPLockConstraint("127.0.0.1"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(t, testNode, adminMac)
|
||||
defer cleanup()
|
||||
res, err := client.NewAddress(ctxt, newAddrReq)
|
||||
require.NoError(t, err, "get new address")
|
||||
assert.Contains(t, res.Address, "bcrt1")
|
||||
},
|
||||
}, {
|
||||
// Seventh test: Bake a macaroon that can only access exactly
|
||||
// two RPCs and make sure it works as expected.
|
||||
name: "custom URI permissions",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
entity := macaroons.PermissionEntityCustomURI
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: entity,
|
||||
Action: "/lnrpc.Lightning/GetInfo",
|
||||
}, {
|
||||
Entity: entity,
|
||||
Action: "/lnrpc.Lightning/List" +
|
||||
"Permissions",
|
||||
}},
|
||||
}
|
||||
bakeRes, err := testClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a connection that uses the custom macaroon.
|
||||
customMacBytes, err := hex.DecodeString(
|
||||
bakeRes.Macaroon,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
customMac := &macaroon.Macaroon{}
|
||||
err = customMac.UnmarshalBinary(customMacBytes)
|
||||
require.NoError(t, err)
|
||||
cleanup, client := macaroonClient(
|
||||
t, testNode, customMac,
|
||||
)
|
||||
defer cleanup()
|
||||
|
||||
// Call GetInfo which should succeed.
|
||||
_, err = client.GetInfo(ctxt, infoReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call ListPermissions which should also succeed.
|
||||
permReq := &lnrpc.ListPermissionsRequest{}
|
||||
permRes, err := client.ListPermissions(ctxt, permReq)
|
||||
require.NoError(t, err)
|
||||
require.Greater(
|
||||
t, len(permRes.MethodPermissions), 10,
|
||||
"permissions",
|
||||
)
|
||||
|
||||
// Try NewAddress which should be denied.
|
||||
_, err = client.NewAddress(ctxt, newAddrReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}, {
|
||||
// Eighth test: check that with the CheckMacaroonPermissions
|
||||
// RPC, we can check that a macaroon follows (or doesn't)
|
||||
// permissions and constraints.
|
||||
name: "unknown permissions",
|
||||
run: func(ctxt context.Context, t *testing.T) {
|
||||
// A test macaroon created with permissions from pool,
|
||||
// to make sure CheckMacaroonPermissions RPC accepts
|
||||
// them.
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "account",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "recommendation",
|
||||
Action: "read",
|
||||
}},
|
||||
AllowExternalPermissions: true,
|
||||
}
|
||||
bakeResp, err := testClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
macBytes, err := hex.DecodeString(bakeResp.Macaroon)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkReq := &lnrpc.CheckMacPermRequest{
|
||||
Macaroon: macBytes,
|
||||
Permissions: req.Permissions,
|
||||
}
|
||||
|
||||
// Test that CheckMacaroonPermissions accurately
|
||||
// characterizes macaroon as valid, even if the
|
||||
// permissions are not native to LND.
|
||||
checkResp, err := testClient.CheckMacaroonPermissions(
|
||||
ctxt, checkReq,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, checkResp.Valid, true)
|
||||
|
||||
mac, err := readMacaroonFromHex(bakeResp.Macaroon)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test that CheckMacaroonPermissions responds that the
|
||||
// macaroon is invalid if timed out.
|
||||
timeoutMac, err := macaroons.AddConstraints(
|
||||
mac, macaroons.TimeoutConstraint(-30),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
timeoutMacBytes, err := timeoutMac.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
|
||||
checkReq.Macaroon = timeoutMacBytes
|
||||
|
||||
_, err = testClient.CheckMacaroonPermissions(
|
||||
ctxt, checkReq,
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "macaroon has expired")
|
||||
|
||||
// Test that CheckMacaroonPermissions labels macaroon
|
||||
// input with wrong permissions as invalid.
|
||||
wrongPermissions := []*lnrpc.MacaroonPermission{{
|
||||
Entity: "invoice",
|
||||
Action: "read",
|
||||
}}
|
||||
|
||||
checkReq.Permissions = wrongPermissions
|
||||
checkReq.Macaroon = macBytes
|
||||
|
||||
_, err = testClient.CheckMacaroonPermissions(
|
||||
ctxt, checkReq,
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
ht.Run(tc.name, func(tt *testing.T) {
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
ht.Context(), defaultTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
tc.run(ctxt, tt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testBakeMacaroon checks that when creating macaroons, the permissions param
|
||||
// in the request must be set correctly, and the baked macaroon has the intended
|
||||
// permissions.
|
||||
func testBakeMacaroon(ht *lntemp.HarnessTest) {
|
||||
var testNode = ht.Alice
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient)
|
||||
}{{
|
||||
// First test: when the permission list is empty in the request,
|
||||
// an error should be returned.
|
||||
name: "no permission list",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "permission list cannot be "+
|
||||
"empty",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Second test: when the action in the permission list is not
|
||||
// valid, an error should be returned.
|
||||
name: "invalid permission list",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "invalid123",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "invalid permission action",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Third test: when the entity in the permission list is not
|
||||
// valid, an error should be returned.
|
||||
name: "invalid permission entity",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "invalid123",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(
|
||||
t, err.Error(), "invalid permission entity",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
// Fourth test: check that when no root key ID is specified, the
|
||||
// default root keyID is used.
|
||||
name: "default root key ID",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
||||
},
|
||||
}, {
|
||||
// Fifth test: create a macaroon use a non-default root key ID.
|
||||
name: "custom root key ID",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
_, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := adminClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the ListMacaroonIDs should give a list of two IDs,
|
||||
// the default ID 0, and the newly created ID. The
|
||||
// returned response is sorted to guarantee the order so
|
||||
// that we can compare them one by one.
|
||||
sort.Slice(resp.RootKeyIds, func(i, j int) bool {
|
||||
return resp.RootKeyIds[i] < resp.RootKeyIds[j]
|
||||
})
|
||||
require.Equal(t, resp.RootKeyIds[0], uint64(0))
|
||||
require.Equal(t, resp.RootKeyIds[1], rootKeyID)
|
||||
},
|
||||
}, {
|
||||
// Sixth test: check the baked macaroon has the intended
|
||||
// permissions. It should succeed in reading, and fail to write
|
||||
// a macaroon.
|
||||
name: "custom macaroon permissions",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
bakeResp, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
newMac, err := readMacaroonFromHex(bakeResp.Macaroon)
|
||||
require.NoError(t, err)
|
||||
cleanup, readOnlyClient := macaroonClient(
|
||||
t, testNode, newMac,
|
||||
)
|
||||
defer cleanup()
|
||||
|
||||
// BakeMacaroon requires a write permission, so this
|
||||
// call should return an error.
|
||||
_, err = readOnlyClient.BakeMacaroon(ctxt, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
|
||||
// ListMacaroon requires a read permission, so this call
|
||||
// should succeed.
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
_, err = readOnlyClient.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Current macaroon can only work on entity macaroon, so
|
||||
// a GetInfo request will fail.
|
||||
infoReq := &lnrpc.GetInfoRequest{}
|
||||
_, err = readOnlyClient.GetInfo(ctxt, infoReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
},
|
||||
}, {
|
||||
// Seventh test: check that if the allow_external_permissions
|
||||
// flag is set, we are able to feed BakeMacaroons permissions
|
||||
// that LND is not familiar with.
|
||||
name: "allow external macaroon permissions",
|
||||
run: func(ctxt context.Context, t *testing.T,
|
||||
adminClient lnrpc.LightningClient) {
|
||||
|
||||
// We'll try permissions from Pool to test that the
|
||||
// allow_external_permissions flag properly allows it.
|
||||
rootKeyID := uint64(4200)
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "account",
|
||||
Action: "read",
|
||||
}},
|
||||
AllowExternalPermissions: true,
|
||||
}
|
||||
|
||||
resp, err := adminClient.BakeMacaroon(ctxt, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We'll also check that the external permission was
|
||||
// successfully added to the macaroon.
|
||||
macBytes, err := hex.DecodeString(resp.Macaroon)
|
||||
require.NoError(t, err)
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
err = mac.UnmarshalBinary(macBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
rawID := mac.Id()
|
||||
decodedID := &lnrpc.MacaroonId{}
|
||||
idProto := rawID[1:]
|
||||
err = proto.Unmarshal(idProto, decodedID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "account", decodedID.Ops[0].Entity)
|
||||
require.Equal(t, "read", decodedID.Ops[0].Actions[0])
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
ht.Run(tc.name, func(tt *testing.T) {
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
ht.Context(), defaultTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(tt, err)
|
||||
cleanup, client := macaroonClient(tt, testNode, adminMac)
|
||||
defer cleanup()
|
||||
|
||||
tc.run(ctxt, tt, client)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testDeleteMacaroonID checks that when deleting a macaroon ID, it removes the
|
||||
// specified ID and invalidates all macaroons derived from the key with that ID.
|
||||
// Also, it checks deleting the reserved marcaroon ID, DefaultRootKeyID or is
|
||||
// forbidden.
|
||||
func testDeleteMacaroonID(ht *lntemp.HarnessTest) {
|
||||
var (
|
||||
ctxb = ht.Context()
|
||||
testNode = ht.Alice
|
||||
)
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Use admin macaroon to create a connection.
|
||||
adminMac, err := testNode.ReadMacaroon(
|
||||
testNode.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
cleanup, client := macaroonClient(ht.T, testNode, adminMac)
|
||||
defer cleanup()
|
||||
|
||||
// Record the number of macaroon IDs before creation.
|
||||
listReq := &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err := client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(ht, err)
|
||||
numMacIDs := len(listResp.RootKeyIds)
|
||||
|
||||
// Create macaroons for testing.
|
||||
rootKeyIDs := []uint64{1, 2, 3}
|
||||
macList := make([]string, 0, len(rootKeyIDs))
|
||||
for _, id := range rootKeyIDs {
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
RootKeyId: id,
|
||||
Permissions: []*lnrpc.MacaroonPermission{{
|
||||
Entity: "macaroon",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
resp, err := client.BakeMacaroon(ctxt, req)
|
||||
require.NoError(ht, err)
|
||||
macList = append(macList, resp.Macaroon)
|
||||
}
|
||||
|
||||
// Check that the creation is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// The number of macaroon IDs should be increased by len(rootKeyIDs).
|
||||
require.Equal(ht, numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds))
|
||||
|
||||
// First test: check deleting the DefaultRootKeyID returns an error.
|
||||
defaultID, _ := strconv.ParseUint(
|
||||
string(macaroons.DefaultRootKeyID), 10, 64,
|
||||
)
|
||||
req := &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: defaultID,
|
||||
}
|
||||
_, err = client.DeleteMacaroonID(ctxt, req)
|
||||
require.Error(ht, err)
|
||||
require.Contains(ht, err.Error(),
|
||||
macaroons.ErrDeletionForbidden.Error())
|
||||
|
||||
// Second test: check deleting the customized ID returns success.
|
||||
req = &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: rootKeyIDs[0],
|
||||
}
|
||||
resp, err := client.DeleteMacaroonID(ctxt, req)
|
||||
require.NoError(ht, err)
|
||||
require.True(ht, resp.Deleted)
|
||||
|
||||
// Check that the deletion is successful.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
listResp, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// The number of macaroon IDs should be decreased by 1.
|
||||
require.Equal(ht, numMacIDs+len(rootKeyIDs)-1, len(listResp.RootKeyIds))
|
||||
|
||||
// Check that the deleted macaroon can no longer access macaroon:read.
|
||||
deletedMac, err := readMacaroonFromHex(macList[0])
|
||||
require.NoError(ht, err)
|
||||
cleanup, client = macaroonClient(ht.T, testNode, deletedMac)
|
||||
defer cleanup()
|
||||
|
||||
// Because the macaroon is deleted, it will be treated as an invalid
|
||||
// one.
|
||||
listReq = &lnrpc.ListMacaroonIDsRequest{}
|
||||
_, err = client.ListMacaroonIDs(ctxt, listReq)
|
||||
require.Error(ht, err)
|
||||
require.Contains(ht, err.Error(), "cannot get macaroon")
|
||||
}
|
||||
|
||||
// testStatelessInit checks that the stateless initialization of the daemon
|
||||
// does not write any macaroon files to the daemon's file system and returns
|
||||
// the admin macaroon in the response. It then checks that the password
|
||||
// change of the wallet can also happen stateless.
|
||||
func testStatelessInit(ht *lntemp.HarnessTest) {
|
||||
var (
|
||||
initPw = []byte("stateless")
|
||||
newPw = []byte("stateless-new")
|
||||
newAddrReq = &lnrpc.NewAddressRequest{
|
||||
Type: AddrTypeWitnessPubkeyHash,
|
||||
}
|
||||
)
|
||||
|
||||
// First, create a new node and request it to initialize stateless.
|
||||
// This should return us the binary serialized admin macaroon that we
|
||||
// can then use for further calls.
|
||||
carol, _, macBytes := ht.NewNodeWithSeed("Carol", nil, initPw, true)
|
||||
require.NotEmpty(ht, macBytes,
|
||||
"invalid macaroon returned in stateless init")
|
||||
|
||||
// Now make sure no macaroon files have been created by the node Carol.
|
||||
_, err := os.Stat(carol.Cfg.AdminMacPath)
|
||||
require.Error(ht, err)
|
||||
_, err = os.Stat(carol.Cfg.ReadMacPath)
|
||||
require.Error(ht, err)
|
||||
_, err = os.Stat(carol.Cfg.InvoiceMacPath)
|
||||
require.Error(ht, err)
|
||||
|
||||
// Then check that we can unmarshal the binary serialized macaroon.
|
||||
adminMac := &macaroon.Macaroon{}
|
||||
err = adminMac.UnmarshalBinary(macBytes)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Find out if we can actually use the macaroon that has been returned
|
||||
// to us for a RPC call.
|
||||
conn, err := carol.ConnectRPCWithMacaroon(adminMac)
|
||||
require.NoError(ht, err)
|
||||
defer conn.Close()
|
||||
adminMacClient := lnrpc.NewLightningClient(conn)
|
||||
ctxt, cancel := context.WithTimeout(ht.Context(), defaultTimeout)
|
||||
defer cancel()
|
||||
res, err := adminMacClient.NewAddress(ctxt, newAddrReq)
|
||||
require.NoError(ht, err)
|
||||
if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) {
|
||||
require.Fail(ht, "returned address was not a regtest address")
|
||||
}
|
||||
|
||||
// As a second part, shut down the node and then try to change the
|
||||
// password when we start it up again.
|
||||
ht.RestartNodeNoUnlock(carol)
|
||||
changePwReq := &lnrpc.ChangePasswordRequest{
|
||||
CurrentPassword: initPw,
|
||||
NewPassword: newPw,
|
||||
StatelessInit: true,
|
||||
}
|
||||
response, err := carol.ChangePasswordAndInit(changePwReq)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Again, make sure no macaroon files have been created by the node
|
||||
// Carol.
|
||||
_, err = os.Stat(carol.Cfg.AdminMacPath)
|
||||
require.Error(ht, err)
|
||||
_, err = os.Stat(carol.Cfg.ReadMacPath)
|
||||
require.Error(ht, err)
|
||||
_, err = os.Stat(carol.Cfg.InvoiceMacPath)
|
||||
require.Error(ht, err)
|
||||
|
||||
// Then check that we can unmarshal the new binary serialized macaroon
|
||||
// and that it really is a new macaroon.
|
||||
err = adminMac.UnmarshalBinary(response.AdminMacaroon)
|
||||
require.NoError(ht, err, "unable to unmarshal macaroon")
|
||||
require.NotEqual(ht, response.AdminMacaroon, macBytes,
|
||||
"expected new macaroon to be different")
|
||||
|
||||
// Finally, find out if we can actually use the new macaroon that has
|
||||
// been returned to us for a RPC call.
|
||||
conn2, err := carol.ConnectRPCWithMacaroon(adminMac)
|
||||
require.NoError(ht, err)
|
||||
defer conn2.Close()
|
||||
adminMacClient = lnrpc.NewLightningClient(conn2)
|
||||
|
||||
// Changing the password takes a while, so we use the default timeout
|
||||
// of 30 seconds to wait for the connection to be ready.
|
||||
ctxt, cancel = context.WithTimeout(ht.Context(), defaultTimeout)
|
||||
defer cancel()
|
||||
res, err = adminMacClient.NewAddress(ctxt, newAddrReq)
|
||||
require.NoError(ht, err)
|
||||
if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) {
|
||||
require.Fail(ht, "returned address was not a regtest address")
|
||||
}
|
||||
}
|
||||
|
||||
// readMacaroonFromHex loads a macaroon from a hex string.
|
||||
func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) {
|
||||
macBytes, err := hex.DecodeString(macHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
func macaroonClient(t *testing.T, testNode *node.HarnessNode,
|
||||
mac *macaroon.Macaroon) (func(), lnrpc.LightningClient) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
conn, err := testNode.ConnectRPCWithMacaroon(mac)
|
||||
require.NoError(t, err, "connect to alice")
|
||||
|
||||
cleanup := func() {
|
||||
err := conn.Close()
|
||||
require.NoError(t, err, "close")
|
||||
}
|
||||
return cleanup, lnrpc.NewLightningClient(conn)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//go:build rpctest
|
||||
// +build rpctest
|
||||
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
// testMaxChannelSize tests that lnd handles --maxchansize parameter correctly.
|
||||
// Wumbo nodes should enforce a default soft limit of 10 BTC by default. This
|
||||
// limit can be adjusted with --maxchansize config option.
|
||||
func testMaxChannelSize(ht *lntemp.HarnessTest) {
|
||||
// We'll make two new nodes, both wumbo but with the default limit on
|
||||
// maximum channel size (10 BTC)
|
||||
wumboNode := ht.NewNode(
|
||||
"wumbo", []string{"--protocol.wumbo-channels"},
|
||||
)
|
||||
wumboNode2 := ht.NewNode(
|
||||
"wumbo2", []string{"--protocol.wumbo-channels"},
|
||||
)
|
||||
|
||||
// We'll send 11 BTC to the wumbo node so it can test the wumbo soft
|
||||
// limit.
|
||||
ht.FundCoins(11*btcutil.SatoshiPerBitcoin, wumboNode)
|
||||
|
||||
// Next we'll connect both nodes, then attempt to make a wumbo channel
|
||||
// funding request, which should fail as it exceeds the default wumbo
|
||||
// soft limit of 10 BTC.
|
||||
ht.EnsureConnected(wumboNode, wumboNode2)
|
||||
|
||||
chanAmt := funding.MaxBtcFundingAmountWumbo + 1
|
||||
// The test should show failure due to the channel exceeding our max
|
||||
// size.
|
||||
expectedErr := lnwallet.ErrChanTooLarge(
|
||||
chanAmt, funding.MaxBtcFundingAmountWumbo,
|
||||
)
|
||||
ht.OpenChannelAssertErr(
|
||||
wumboNode, wumboNode2,
|
||||
lntemp.OpenChannelParams{Amt: chanAmt}, expectedErr,
|
||||
)
|
||||
|
||||
// We'll now make another wumbo node with appropriate maximum channel
|
||||
// size to accept our wumbo channel funding.
|
||||
wumboNode3 := ht.NewNode(
|
||||
"wumbo3", []string{
|
||||
"--protocol.wumbo-channels",
|
||||
fmt.Sprintf(
|
||||
"--maxchansize=%v",
|
||||
int64(funding.MaxBtcFundingAmountWumbo+1),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
// Creating a wumbo channel between these two nodes should succeed.
|
||||
ht.EnsureConnected(wumboNode, wumboNode3)
|
||||
chanPoint := ht.OpenChannel(
|
||||
wumboNode, wumboNode3, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
ht.CloseChannel(wumboNode, chanPoint)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testMaxHtlcPathfind tests the case where we try to send a payment over a
|
||||
// channel where we have already reached the limit of the number of htlcs that
|
||||
// we may add to the remote party's commitment. This test asserts that we do
|
||||
// not attempt to use the full channel at all in our pathfinding.
|
||||
func testMaxHtlcPathfind(ht *lntemp.HarnessTest) {
|
||||
// Setup a channel between Alice and Bob where Alice will only allow
|
||||
// Bob to add a maximum of 5 htlcs to her commitment.
|
||||
maxHtlcs := 5
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: 1000000,
|
||||
PushAmt: 800000,
|
||||
RemoteMaxHtlcs: uint16(maxHtlcs),
|
||||
},
|
||||
)
|
||||
|
||||
// Alice and bob should have one channel open with each other now.
|
||||
ht.AssertNodeNumChannels(alice, 1)
|
||||
ht.AssertNodeNumChannels(bob, 1)
|
||||
|
||||
// Send our maximum number of htlcs from Bob -> Alice so that we get
|
||||
// to a point where Alice won't accept any more htlcs on the channel.
|
||||
subscriptions := make([]*holdSubscription, maxHtlcs)
|
||||
|
||||
for i := 0; i < maxHtlcs; i++ {
|
||||
subscriptions[i] = acceptHoldInvoice(ht, i, bob, alice)
|
||||
}
|
||||
|
||||
ht.AssertNumActiveHtlcs(alice, maxHtlcs)
|
||||
ht.AssertNumActiveHtlcs(bob, maxHtlcs)
|
||||
|
||||
// Now we send a payment from Alice -> Bob to sanity check that our
|
||||
// commitment limit is not applied in the opposite direction.
|
||||
aliceBobSub := acceptHoldInvoice(ht, maxHtlcs, alice, bob)
|
||||
ht.AssertNumActiveHtlcs(alice, maxHtlcs+1)
|
||||
ht.AssertNumActiveHtlcs(bob, maxHtlcs+1)
|
||||
|
||||
// Now, we're going to try to send another payment from Bob -> Alice.
|
||||
// We've hit our max remote htlcs, so we expect this payment to spin
|
||||
// out dramatically with pathfinding.
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
Amt: 1000,
|
||||
Dest: alice.PubKey[:],
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitSat: 1000000,
|
||||
MaxParts: 10,
|
||||
Amp: true,
|
||||
}
|
||||
ht.SendPaymentAndAssertStatus(bob, sendReq, lnrpc.Payment_FAILED)
|
||||
|
||||
// Now that we're done, we cancel all our pending htlcs so that we
|
||||
// can cleanup the channel with a coop close.
|
||||
for _, sub := range subscriptions {
|
||||
sub.cancel(ht)
|
||||
}
|
||||
aliceBobSub.cancel(ht)
|
||||
|
||||
ht.AssertNumActiveHtlcs(alice, 0)
|
||||
ht.AssertNumActiveHtlcs(bob, 0)
|
||||
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
type holdSubscription struct {
|
||||
recipient *node.HarnessNode
|
||||
hash lntypes.Hash
|
||||
invSubscription invoicesrpc.Invoices_SubscribeSingleInvoiceClient
|
||||
paymentSubscription routerrpc.Router_SendPaymentV2Client
|
||||
}
|
||||
|
||||
// cancel updates a hold invoice to cancel from the recipient and consumes
|
||||
// updates from the payer until it has reached a final, failed state.
|
||||
func (h *holdSubscription) cancel(ht *lntemp.HarnessTest) {
|
||||
h.recipient.RPC.CancelInvoice(h.hash[:])
|
||||
|
||||
invUpdate := ht.ReceiveSingleInvoice(h.invSubscription)
|
||||
require.Equal(ht, lnrpc.Invoice_CANCELED, invUpdate.State,
|
||||
"expected invoice canceled")
|
||||
|
||||
// We expect one in flight update when our htlc is canceled back, and
|
||||
// another when we fail the payment as a whole.
|
||||
payUpdate := ht.AssertPaymentStatusFromStream(
|
||||
h.paymentSubscription, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
require.Len(ht, payUpdate.Htlcs, 1)
|
||||
|
||||
payUpdate = ht.AssertPaymentStatusFromStream(
|
||||
h.paymentSubscription, lnrpc.Payment_FAILED,
|
||||
)
|
||||
require.Equal(ht, lnrpc.Payment_FAILED, payUpdate.Status,
|
||||
"expected payment failed")
|
||||
require.Equal(ht, lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll
|
||||
payUpdate.FailureReason, "expected unknown details")
|
||||
}
|
||||
|
||||
// acceptHoldInvoice adds a hold invoice to the recipient node, pays it from
|
||||
// the sender and asserts that we have reached the accepted state where htlcs
|
||||
// are locked in for the payment.
|
||||
func acceptHoldInvoice(ht *lntemp.HarnessTest, idx int, sender,
|
||||
receiver *node.HarnessNode) *holdSubscription {
|
||||
|
||||
hash := [lntypes.HashSize]byte{byte(idx + 1)}
|
||||
|
||||
req := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
ValueMsat: 10000,
|
||||
Hash: hash[:],
|
||||
}
|
||||
invoice := receiver.RPC.AddHoldInvoice(req)
|
||||
|
||||
invStream := receiver.RPC.SubscribeSingleInvoice(hash[:])
|
||||
inv := ht.ReceiveSingleInvoice(invStream)
|
||||
require.Equal(ht, lnrpc.Invoice_OPEN, inv.State, "expect open")
|
||||
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitSat: 1000000,
|
||||
}
|
||||
payStream := sender.RPC.SendPayment(sendReq)
|
||||
|
||||
// Finally, assert that we progress to an accepted state. We expect
|
||||
// the payer to get one update for the creation of the payment, and
|
||||
// another when a htlc is dispatched.
|
||||
payment := ht.AssertPaymentStatusFromStream(
|
||||
payStream, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
require.Empty(ht, payment.Htlcs)
|
||||
|
||||
payment = ht.AssertPaymentStatusFromStream(
|
||||
payStream, lnrpc.Payment_IN_FLIGHT,
|
||||
)
|
||||
require.Len(ht, payment.Htlcs, 1)
|
||||
|
||||
inv = ht.ReceiveSingleInvoice(invStream)
|
||||
require.Equal(ht, lnrpc.Invoice_ACCEPTED, inv.State,
|
||||
"expected accepted")
|
||||
|
||||
return &holdSubscription{
|
||||
recipient: receiver,
|
||||
hash: hash,
|
||||
invSubscription: invStream,
|
||||
paymentSubscription: payStream,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,385 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testSendToRouteMultiPath tests that we are able to successfully route a
|
||||
// payment using multiple shards across different paths, by using SendToRoute.
|
||||
func testSendToRouteMultiPath(ht *lntemp.HarnessTest) {
|
||||
mts := newMppTestScenario(ht)
|
||||
|
||||
// To ensure the payment goes through separate paths, we'll set a
|
||||
// channel size that can only carry one shard at a time. We'll divide
|
||||
// the payment into 3 shards.
|
||||
const (
|
||||
paymentAmt = btcutil.Amount(300000)
|
||||
shardAmt = paymentAmt / 3
|
||||
chanAmt = shardAmt * 3 / 2
|
||||
)
|
||||
|
||||
// Set up a network with three different paths Alice <-> Bob.
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
//
|
||||
req := &mppOpenChannelRequest{
|
||||
// Since the channel Alice-> Carol will have to carry two
|
||||
// shards, we make it larger.
|
||||
amtAliceCarol: chanAmt + shardAmt,
|
||||
amtAliceDave: chanAmt,
|
||||
amtCarolBob: chanAmt,
|
||||
amtCarolEve: chanAmt,
|
||||
amtDaveBob: chanAmt,
|
||||
amtEveBob: chanAmt,
|
||||
}
|
||||
mts.openChannels(req)
|
||||
|
||||
// Make Bob create an invoice for Alice to pay.
|
||||
payReqs, rHashes, invoices := ht.CreatePayReqs(mts.bob, paymentAmt, 1)
|
||||
|
||||
rHash := rHashes[0]
|
||||
payReq := payReqs[0]
|
||||
|
||||
decodeResp := mts.bob.RPC.DecodePayReq(payReq)
|
||||
payAddr := decodeResp.PaymentAddr
|
||||
|
||||
// Subscribe the invoice.
|
||||
stream := mts.bob.RPC.SubscribeSingleInvoice(rHash)
|
||||
|
||||
// We'll send shards along three routes from Alice.
|
||||
sendRoutes := [][]*node.HarnessNode{
|
||||
{mts.carol, mts.bob},
|
||||
{mts.dave, mts.bob},
|
||||
{mts.carol, mts.eve, mts.bob},
|
||||
}
|
||||
|
||||
responses := make(chan *lnrpc.HTLCAttempt, len(sendRoutes))
|
||||
for _, hops := range sendRoutes {
|
||||
// Build a route for the specified hops.
|
||||
r := mts.buildRoute(shardAmt, mts.alice, hops)
|
||||
|
||||
// Set the MPP records to indicate this is a payment shard.
|
||||
hop := r.Hops[len(r.Hops)-1]
|
||||
hop.TlvPayload = true
|
||||
hop.MppRecord = &lnrpc.MPPRecord{
|
||||
PaymentAddr: payAddr,
|
||||
TotalAmtMsat: int64(paymentAmt * 1000),
|
||||
}
|
||||
|
||||
// Send the shard.
|
||||
sendReq := &routerrpc.SendToRouteRequest{
|
||||
PaymentHash: rHash,
|
||||
Route: r,
|
||||
}
|
||||
|
||||
// We'll send all shards in their own goroutine, since
|
||||
// SendToRoute will block as long as the payment is in flight.
|
||||
go func() {
|
||||
resp := mts.alice.RPC.SendToRouteV2(sendReq)
|
||||
responses <- resp
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all responses to be back, and check that they all
|
||||
// succeeded.
|
||||
timer := time.After(defaultTimeout)
|
||||
for range sendRoutes {
|
||||
var resp *lnrpc.HTLCAttempt
|
||||
select {
|
||||
case resp = <-responses:
|
||||
case <-timer:
|
||||
require.Fail(ht, "response not received")
|
||||
}
|
||||
|
||||
require.Nil(ht, resp.Failure, "received payment failure")
|
||||
|
||||
// All shards should come back with the preimage.
|
||||
require.Equal(ht, resp.Preimage, invoices[0].RPreimage,
|
||||
"preimage doesn't match")
|
||||
}
|
||||
|
||||
// assertNumHtlcs is a helper that checks the node's latest payment,
|
||||
// and asserts it was split into num shards.
|
||||
assertNumHtlcs := func(hn *node.HarnessNode, num int) {
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], invoices[0].RPreimage)
|
||||
|
||||
payment := ht.AssertPaymentStatus(
|
||||
hn, preimage, lnrpc.Payment_SUCCEEDED,
|
||||
)
|
||||
|
||||
htlcs := payment.Htlcs
|
||||
require.NotEmpty(ht, htlcs, "no htlcs")
|
||||
|
||||
succeeded := 0
|
||||
for _, htlc := range htlcs {
|
||||
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
require.Equal(ht, num, succeeded, "HTLCs not matched")
|
||||
}
|
||||
|
||||
// assertSettledInvoice checks that the invoice for the given payment
|
||||
// hash is settled, and has been paid using num HTLCs.
|
||||
assertSettledInvoice := func(rhash []byte, num int) {
|
||||
var payHash lntypes.Hash
|
||||
copy(payHash[:], rhash)
|
||||
inv := ht.AssertInvoiceState(stream, lnrpc.Invoice_SETTLED)
|
||||
|
||||
// Assert that the amount paid to the invoice is correct.
|
||||
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
|
||||
"incorrect payment amt")
|
||||
|
||||
require.Len(ht, inv.Htlcs, num, "wrong num of HTLCs")
|
||||
}
|
||||
|
||||
// Finally check that the payment shows up with three settled HTLCs in
|
||||
// Alice's list of payments...
|
||||
assertNumHtlcs(mts.alice, 3)
|
||||
|
||||
// ...and in Bob's list of paid invoices.
|
||||
assertSettledInvoice(rHash, 3)
|
||||
|
||||
// Finally, close all channels.
|
||||
mts.closeChannels()
|
||||
}
|
||||
|
||||
// mppTestScenario defines a test scenario used for testing MPP-related tests.
|
||||
// It has two standby nodes, alice and bob, and three new nodes, carol, dave,
|
||||
// and eve.
|
||||
type mppTestScenario struct {
|
||||
ht *lntemp.HarnessTest
|
||||
|
||||
alice, bob, carol, dave, eve *node.HarnessNode
|
||||
nodes []*node.HarnessNode
|
||||
|
||||
// Keep a list of all our active channels.
|
||||
channelPoints []*lnrpc.ChannelPoint
|
||||
}
|
||||
|
||||
// newMppTestScenario initializes a new mpp test scenario with five funded
|
||||
// nodes and connects them to have the following topology,
|
||||
//
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
func newMppTestScenario(ht *lntemp.HarnessTest) *mppTestScenario {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
ht.RestartNodeWithExtraArgs(bob, []string{
|
||||
"--maxpendingchannels=2",
|
||||
"--accept-amp",
|
||||
})
|
||||
|
||||
// Create a five-node context consisting of Alice, Bob and three new
|
||||
// nodes.
|
||||
carol := ht.NewNode("carol", []string{
|
||||
"--maxpendingchannels=2",
|
||||
"--accept-amp",
|
||||
})
|
||||
dave := ht.NewNode("dave", nil)
|
||||
eve := ht.NewNode("eve", nil)
|
||||
|
||||
// Connect nodes to ensure propagation of channels.
|
||||
ht.EnsureConnected(alice, carol)
|
||||
ht.EnsureConnected(alice, dave)
|
||||
ht.EnsureConnected(carol, bob)
|
||||
ht.EnsureConnected(carol, eve)
|
||||
ht.EnsureConnected(dave, bob)
|
||||
ht.EnsureConnected(eve, bob)
|
||||
|
||||
// Send coins to the nodes and mine 1 blocks to confirm them.
|
||||
for i := 0; i < 2; i++ {
|
||||
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
|
||||
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
|
||||
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, eve)
|
||||
ht.MineBlocks(1)
|
||||
}
|
||||
|
||||
mts := &mppTestScenario{
|
||||
ht: ht,
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
carol: carol,
|
||||
dave: dave,
|
||||
eve: eve,
|
||||
nodes: []*node.HarnessNode{alice, bob, carol, dave, eve},
|
||||
}
|
||||
|
||||
return mts
|
||||
}
|
||||
|
||||
// mppOpenChannelRequest defines the amounts used for each channel opening.
|
||||
type mppOpenChannelRequest struct {
|
||||
// Channel Alice=>Carol.
|
||||
amtAliceCarol btcutil.Amount
|
||||
|
||||
// Channel Alice=>Dave.
|
||||
amtAliceDave btcutil.Amount
|
||||
|
||||
// Channel Carol=>Bob.
|
||||
amtCarolBob btcutil.Amount
|
||||
|
||||
// Channel Carol=>Eve.
|
||||
amtCarolEve btcutil.Amount
|
||||
|
||||
// Channel Dave=>Bob.
|
||||
amtDaveBob btcutil.Amount
|
||||
|
||||
// Channel Eve=>Bob.
|
||||
amtEveBob btcutil.Amount
|
||||
}
|
||||
|
||||
// openChannels is a helper to open channels that sets up a network topology
|
||||
// with three different paths Alice <-> Bob as following,
|
||||
//
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
//
|
||||
// NOTE: all the channels are open together to save blocks mined.
|
||||
func (m *mppTestScenario) openChannels(r *mppOpenChannelRequest) {
|
||||
reqs := []*lntemp.OpenChannelRequest{
|
||||
{
|
||||
Local: m.alice,
|
||||
Remote: m.carol,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtAliceCarol},
|
||||
},
|
||||
{
|
||||
Local: m.alice,
|
||||
Remote: m.dave,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtAliceDave},
|
||||
},
|
||||
{
|
||||
Local: m.carol,
|
||||
Remote: m.bob,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtCarolBob},
|
||||
},
|
||||
{
|
||||
Local: m.carol,
|
||||
Remote: m.eve,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtCarolEve},
|
||||
},
|
||||
{
|
||||
Local: m.dave,
|
||||
Remote: m.bob,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtDaveBob},
|
||||
},
|
||||
{
|
||||
Local: m.eve,
|
||||
Remote: m.bob,
|
||||
Param: lntemp.OpenChannelParams{Amt: r.amtEveBob},
|
||||
},
|
||||
}
|
||||
|
||||
m.channelPoints = m.ht.OpenMultiChannelsAsync(reqs)
|
||||
|
||||
// Make sure every node has heard every channel.
|
||||
for _, hn := range m.nodes {
|
||||
for _, cp := range m.channelPoints {
|
||||
m.ht.AssertTopologyChannelOpen(hn, cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// closeChannels closes all the open channels from `openChannels`.
|
||||
func (m *mppTestScenario) closeChannels() {
|
||||
if m.ht.Failed() {
|
||||
m.ht.Log("Skipped closing channels for failed test")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed. When the
|
||||
// payment is reported as settled by Alice, 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 all channels without mining the closing transactions.
|
||||
m.ht.CloseChannelAssertPending(m.alice, m.channelPoints[0], false)
|
||||
m.ht.CloseChannelAssertPending(m.alice, m.channelPoints[1], false)
|
||||
m.ht.CloseChannelAssertPending(m.carol, m.channelPoints[2], false)
|
||||
m.ht.CloseChannelAssertPending(m.carol, m.channelPoints[3], false)
|
||||
m.ht.CloseChannelAssertPending(m.dave, m.channelPoints[4], false)
|
||||
m.ht.CloseChannelAssertPending(m.eve, m.channelPoints[5], false)
|
||||
|
||||
// Now mine a block to include all the closing transactions.
|
||||
m.ht.MineBlocks(1)
|
||||
|
||||
// Assert that the channels are closed.
|
||||
for _, hn := range m.nodes {
|
||||
m.ht.AssertNumWaitingClose(hn, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for Alice to build a route from pubkeys.
|
||||
func (m *mppTestScenario) buildRoute(amt btcutil.Amount,
|
||||
sender *node.HarnessNode, hops []*node.HarnessNode) *lnrpc.Route {
|
||||
|
||||
rpcHops := make([][]byte, 0, len(hops))
|
||||
for _, hop := range hops {
|
||||
k := hop.PubKeyStr
|
||||
pubkey, err := route.NewVertexFromStr(k)
|
||||
require.NoErrorf(m.ht, err, "error parsing %v: %v", k, err)
|
||||
rpcHops = append(rpcHops, pubkey[:])
|
||||
}
|
||||
|
||||
req := &routerrpc.BuildRouteRequest{
|
||||
AmtMsat: int64(amt * 1000),
|
||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||
HopPubkeys: rpcHops,
|
||||
}
|
||||
routeResp := sender.RPC.BuildRoute(req)
|
||||
|
||||
return routeResp.Route
|
||||
}
|
||||
|
||||
// updatePolicy updates a Dave's global channel policy and returns the expected
|
||||
// policy for further check. It changes Dave's `FeeBaseMsat` from 1000 msat to
|
||||
// 500,000 msat, and `FeeProportionalMillonths` from 1 msat to 1000 msat.
|
||||
func (m *mppTestScenario) updateDaveGlobalPolicy() *lnrpc.RoutingPolicy {
|
||||
const (
|
||||
baseFeeMsat = 500_000
|
||||
feeRate = 0.001
|
||||
maxHtlcMsat = 133_650_000
|
||||
)
|
||||
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: baseFeeMsat,
|
||||
FeeRateMilliMsat: feeRate * testFeeBase,
|
||||
TimeLockDelta: 40,
|
||||
MinHtlc: 1000, // default value
|
||||
MaxHtlcMsat: maxHtlcMsat,
|
||||
}
|
||||
|
||||
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFeeMsat,
|
||||
FeeRate: feeRate,
|
||||
TimeLockDelta: 40,
|
||||
Scope: &lnrpc.PolicyUpdateRequest_Global{Global: true},
|
||||
MaxHtlcMsat: maxHtlcMsat,
|
||||
}
|
||||
m.dave.RPC.UpdateChannelPolicy(updateFeeReq)
|
||||
|
||||
return expectedPolicy
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testHtlcErrorPropagation(ht *lntemp.HarnessTest) {
|
||||
// In this test we wish to exercise the daemon's correct parsing,
|
||||
// handling, and propagation of errors that occur while processing a
|
||||
// multi-hop payment.
|
||||
const chanAmt = funding.MaxBtcFundingAmount
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Since we'd like to test some multi-hop failure scenarios, we'll
|
||||
// introduce another node into our test network: Carol.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
ht.ConnectNodes(bob, carol)
|
||||
|
||||
// Before we start sending payments, subscribe to htlc events for each
|
||||
// node.
|
||||
aliceEvents := alice.RPC.SubscribeHtlcEvents()
|
||||
bobEvents := bob.RPC.SubscribeHtlcEvents()
|
||||
carolEvents := carol.RPC.SubscribeHtlcEvents()
|
||||
|
||||
// Once subscribed, the first event will be UNKNOWN.
|
||||
ht.AssertHtlcEventType(aliceEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
|
||||
// First establish a channel with a capacity of 0.5 BTC between Alice
|
||||
// and Bob.
|
||||
chanPointAlice := ht.OpenChannel(
|
||||
alice, bob,
|
||||
lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Next, we'll create a connection from Bob to Carol, and open a
|
||||
// channel between them so we have the topology: Alice -> Bob -> Carol.
|
||||
// The channel created will be of lower capacity that the one created
|
||||
// above.
|
||||
const bobChanAmt = funding.MaxBtcFundingAmount
|
||||
chanPointBob := ht.OpenChannel(
|
||||
bob, carol, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Ensure that Alice has Carol in her routing table before proceeding.
|
||||
ht.AssertTopologyChannelOpen(alice, chanPointBob)
|
||||
|
||||
cType := ht.GetChannelCommitType(alice, chanPointAlice)
|
||||
commitFee := calcStaticFee(cType, 0)
|
||||
|
||||
assertBaseBalance := func() {
|
||||
// Alice has opened a channel with Bob with zero push amount,
|
||||
// so it's remote balance is zero.
|
||||
expBalanceAlice := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(chanAmt - commitFee),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
chanAmt - commitFee,
|
||||
)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(chanAmt - commitFee),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(alice, expBalanceAlice)
|
||||
|
||||
// Bob has a channel with Alice and another with Carol, so it's
|
||||
// local and remote balances are both chanAmt - commitFee.
|
||||
expBalanceBob := &lnrpc.ChannelBalanceResponse{
|
||||
LocalBalance: &lnrpc.Amount{
|
||||
Sat: uint64(chanAmt - commitFee),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
chanAmt - commitFee,
|
||||
)),
|
||||
},
|
||||
RemoteBalance: &lnrpc.Amount{
|
||||
Sat: uint64(chanAmt - commitFee),
|
||||
Msat: uint64(lnwire.NewMSatFromSatoshis(
|
||||
chanAmt - commitFee,
|
||||
)),
|
||||
},
|
||||
UnsettledLocalBalance: &lnrpc.Amount{},
|
||||
UnsettledRemoteBalance: &lnrpc.Amount{},
|
||||
PendingOpenLocalBalance: &lnrpc.Amount{},
|
||||
PendingOpenRemoteBalance: &lnrpc.Amount{},
|
||||
// Deprecated fields.
|
||||
Balance: int64(chanAmt - commitFee),
|
||||
}
|
||||
ht.AssertChannelBalanceResp(bob, expBalanceBob)
|
||||
}
|
||||
|
||||
// assertLinkFailure checks that the stream provided has a single link
|
||||
// failure the failure detail provided.
|
||||
assertLinkFailure := func(event *routerrpc.HtlcEvent,
|
||||
failureDetail routerrpc.FailureDetail) {
|
||||
|
||||
linkFail, ok := event.Event.(*routerrpc.HtlcEvent_LinkFailEvent)
|
||||
require.Truef(ht, ok, "expected forwarding failure, got: %T",
|
||||
linkFail)
|
||||
|
||||
require.Equal(ht, failureDetail,
|
||||
linkFail.LinkFailEvent.FailureDetail,
|
||||
"wrong link fail detail")
|
||||
}
|
||||
|
||||
// With the channels, open we can now start to test our multi-hop error
|
||||
// scenarios. First, we'll generate an invoice from carol that we'll
|
||||
// use to test some error cases.
|
||||
const payAmt = 10000
|
||||
invoiceReq := &lnrpc.Invoice{
|
||||
Memo: "kek99",
|
||||
Value: payAmt,
|
||||
}
|
||||
|
||||
carolInvoice := carol.RPC.AddInvoice(invoiceReq)
|
||||
carolPayReq := carol.RPC.DecodePayReq(carolInvoice.PaymentRequest)
|
||||
|
||||
// For the first scenario, we'll test the cancellation of an HTLC with
|
||||
// an unknown payment hash.
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentHash: ht.Random32Bytes(),
|
||||
Dest: carol.PubKey[:],
|
||||
Amt: payAmt,
|
||||
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
MaxParts: 1,
|
||||
}
|
||||
ht.SendPaymentAssertFail(
|
||||
alice, sendReq,
|
||||
lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll
|
||||
)
|
||||
ht.AssertLastHTLCError(
|
||||
alice, lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
||||
)
|
||||
|
||||
// assertAliceAndBob is a helper closure that asserts Alice and Bob
|
||||
// each has one forward and one forward fail event, and Bob has the
|
||||
// final htlc fail event.
|
||||
assertAliceAndBob := func() {
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForward,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForwardFail,
|
||||
)
|
||||
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_FORWARD,
|
||||
lntemp.HtlcEventForward,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_FORWARD,
|
||||
lntemp.HtlcEventForwardFail,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_UNKNOWN,
|
||||
lntemp.HtlcEventFinal,
|
||||
)
|
||||
}
|
||||
|
||||
// We expect alice and bob to each have one forward and one forward
|
||||
// fail event at this stage.
|
||||
assertAliceAndBob()
|
||||
|
||||
// Carol should have a link failure because the htlc failed on her
|
||||
// incoming link.
|
||||
event := ht.AssertHtlcEventType(
|
||||
carolEvents, routerrpc.HtlcEvent_RECEIVE,
|
||||
)
|
||||
assertLinkFailure(event, routerrpc.FailureDetail_UNKNOWN_INVOICE)
|
||||
|
||||
// There's also a final htlc event that gives the final outcome of the
|
||||
// htlc.
|
||||
ht.AssertHtlcEventTypes(
|
||||
carolEvents, routerrpc.HtlcEvent_UNKNOWN, lntemp.HtlcEventFinal,
|
||||
)
|
||||
|
||||
// The balances of all parties should be the same as initially since
|
||||
// the HTLC was canceled.
|
||||
assertBaseBalance()
|
||||
|
||||
// Next, we'll test the case of a recognized payHash but, an incorrect
|
||||
// value on the extended HTLC.
|
||||
htlcAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||
sendReq = &routerrpc.SendPaymentRequest{
|
||||
PaymentHash: carolInvoice.RHash,
|
||||
Dest: carol.PubKey[:],
|
||||
// 10k satoshis are expected.
|
||||
Amt: int64(htlcAmt.ToSatoshis()),
|
||||
FinalCltvDelta: int32(carolPayReq.CltvExpiry),
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
MaxParts: 1,
|
||||
}
|
||||
ht.SendPaymentAssertFail(
|
||||
alice, sendReq,
|
||||
lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll
|
||||
)
|
||||
ht.AssertLastHTLCError(
|
||||
alice, lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
||||
)
|
||||
|
||||
// We expect alice and bob to each have one forward and one forward
|
||||
// fail event at this stage.
|
||||
assertAliceAndBob()
|
||||
|
||||
// Carol should have a link failure because the htlc failed on her
|
||||
// incoming link.
|
||||
event = ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_RECEIVE)
|
||||
assertLinkFailure(event, routerrpc.FailureDetail_INVOICE_UNDERPAID)
|
||||
|
||||
// There's also a final htlc event that gives the final outcome of the
|
||||
// htlc.
|
||||
ht.AssertHtlcEventTypes(
|
||||
carolEvents, routerrpc.HtlcEvent_UNKNOWN, lntemp.HtlcEventFinal,
|
||||
)
|
||||
|
||||
// The balances of all parties should be the same as initially since
|
||||
// the HTLC was canceled.
|
||||
assertBaseBalance()
|
||||
|
||||
// Next we'll test an error that occurs mid-route due to an outgoing
|
||||
// link having insufficient capacity. In order to do so, we'll first
|
||||
// need to unbalance the link connecting Bob<->Carol.
|
||||
//
|
||||
// To do so, we'll push most of the funds in the channel over to
|
||||
// Alice's side, leaving on 10k satoshis of available balance for bob.
|
||||
// There's a max payment amount, so we'll have to do this
|
||||
// incrementally.
|
||||
chanReserve := int64(chanAmt / 100)
|
||||
amtToSend := int64(chanAmt) - chanReserve - 20000
|
||||
amtSent := int64(0)
|
||||
for amtSent != amtToSend {
|
||||
// We'll send in chunks of the max payment amount. If we're
|
||||
// about to send too much, then we'll only send the amount
|
||||
// remaining.
|
||||
toSend := int64(math.MaxUint32)
|
||||
if toSend+amtSent > amtToSend {
|
||||
toSend = amtToSend - amtSent
|
||||
}
|
||||
|
||||
invoiceReq = &lnrpc.Invoice{
|
||||
Value: toSend,
|
||||
}
|
||||
carolInvoice2 := carol.RPC.AddInvoice(invoiceReq)
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: carolInvoice2.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
MaxParts: 1,
|
||||
}
|
||||
ht.SendPaymentAndAssertStatus(bob, req, lnrpc.Payment_SUCCEEDED)
|
||||
|
||||
// For each send bob makes, we need to check that bob has a
|
||||
// forward and settle event for his send, and carol has a
|
||||
// settle event and a final htlc event for her receive.
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForward,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventSettle,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
carolEvents, routerrpc.HtlcEvent_RECEIVE,
|
||||
lntemp.HtlcEventSettle,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
carolEvents, routerrpc.HtlcEvent_UNKNOWN,
|
||||
lntemp.HtlcEventFinal,
|
||||
)
|
||||
|
||||
amtSent += toSend
|
||||
}
|
||||
|
||||
// At this point, Alice has 50mil satoshis on her side of the channel,
|
||||
// but Bob only has 10k available on his side of the channel. So a
|
||||
// payment from Alice to Carol worth 100k satoshis should fail.
|
||||
invoiceReq = &lnrpc.Invoice{
|
||||
Value: 100000,
|
||||
}
|
||||
carolInvoice3 := carol.RPC.AddInvoice(invoiceReq)
|
||||
|
||||
sendReq = &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: carolInvoice3.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
MaxParts: 1,
|
||||
}
|
||||
ht.SendPaymentAssertFail(
|
||||
alice, sendReq,
|
||||
lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
|
||||
)
|
||||
ht.AssertLastHTLCError(
|
||||
alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
|
||||
)
|
||||
|
||||
// Alice should have a forwarding event and a forwarding failure.
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForward,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForwardFail,
|
||||
)
|
||||
|
||||
// Bob should have a link failure because the htlc failed on his
|
||||
// outgoing link.
|
||||
event = ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_FORWARD)
|
||||
assertLinkFailure(event, routerrpc.FailureDetail_INSUFFICIENT_BALANCE)
|
||||
|
||||
// There's also a final htlc event that gives the final outcome of the
|
||||
// htlc.
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_UNKNOWN, lntemp.HtlcEventFinal,
|
||||
)
|
||||
|
||||
// Generate new invoice to not pay same invoice twice.
|
||||
carolInvoice = carol.RPC.AddInvoice(invoiceReq)
|
||||
|
||||
// For our final test, we'll ensure that if a target link isn't
|
||||
// available for what ever reason then the payment fails accordingly.
|
||||
//
|
||||
// We'll attempt to complete the original invoice we created with Carol
|
||||
// above, but before we do so, Carol will go offline, resulting in a
|
||||
// failed payment.
|
||||
ht.Shutdown(carol)
|
||||
|
||||
// Reset mission control to forget the temporary channel failure above.
|
||||
alice.RPC.ResetMissionControl()
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: carolInvoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
MaxParts: 1,
|
||||
}
|
||||
ht.SendPaymentAssertFail(
|
||||
alice, req, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE,
|
||||
)
|
||||
ht.AssertLastHTLCError(alice, lnrpc.Failure_UNKNOWN_NEXT_PEER)
|
||||
|
||||
// Alice should have a forwarding event and subsequent fail.
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForward,
|
||||
)
|
||||
ht.AssertHtlcEventTypes(
|
||||
aliceEvents, routerrpc.HtlcEvent_SEND,
|
||||
lntemp.HtlcEventForwardFail,
|
||||
)
|
||||
|
||||
// Bob should have a link failure because he could not find the next
|
||||
// peer.
|
||||
event = ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_FORWARD)
|
||||
assertLinkFailure(event, routerrpc.FailureDetail_NO_DETAIL)
|
||||
|
||||
// There's also a final htlc event that gives the final outcome of the
|
||||
// htlc.
|
||||
ht.AssertHtlcEventTypes(
|
||||
bobEvents, routerrpc.HtlcEvent_UNKNOWN, lntemp.HtlcEventFinal,
|
||||
)
|
||||
|
||||
// Finally, immediately close the channel. This function will also
|
||||
// block until the channel is closed and will additionally assert the
|
||||
// relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPointAlice)
|
||||
|
||||
// Force close Bob's final channel.
|
||||
ht.ForceCloseChannel(bob, chanPointBob)
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testMultiHopPayments(ht *lntemp.HarnessTest) {
|
||||
const chanAmt = btcutil.Amount(100000)
|
||||
|
||||
// As preliminary setup, we'll create two new nodes: Carol and Dave,
|
||||
// such that we now have a 4 node, 3 channel topology. Dave will make a
|
||||
// channel with Alice, and Carol with Dave. After this setup, the
|
||||
// network topology should now look like:
|
||||
// Carol -> Dave -> Alice -> Bob
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
daveArgs := []string{"--protocol.legacy.onion"}
|
||||
dave := ht.NewNode("Dave", daveArgs)
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Subscribe events early so we don't miss it out.
|
||||
aliceEvents := alice.RPC.SubscribeHtlcEvents()
|
||||
bobEvents := bob.RPC.SubscribeHtlcEvents()
|
||||
carolEvents := carol.RPC.SubscribeHtlcEvents()
|
||||
daveEvents := dave.RPC.SubscribeHtlcEvents()
|
||||
|
||||
// Once subscribed, the first event will be UNKNOWN.
|
||||
ht.AssertHtlcEventType(aliceEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
ht.AssertHtlcEventType(daveEvents, routerrpc.HtlcEvent_UNKNOWN)
|
||||
|
||||
// Connect the nodes.
|
||||
ht.ConnectNodes(dave, alice)
|
||||
ht.ConnectNodes(carol, dave)
|
||||
|
||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanPointAlice := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// We'll create Dave and establish a channel to Alice. Dave will be
|
||||
// running an older node that requires the legacy onion payload.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
chanPointDave := ht.OpenChannel(
|
||||
dave, alice, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Next, we'll create Carol and establish a channel to from her to
|
||||
// Dave.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
chanPointCarol := ht.OpenChannel(
|
||||
carol, dave, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Create 5 invoices for Bob, which expect a payment from Carol for 1k
|
||||
// satoshis with a different preimage each time.
|
||||
const numPayments = 5
|
||||
const paymentAmt = 1000
|
||||
payReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numPayments)
|
||||
|
||||
// Set the fee policies of the Alice -> Bob and the Dave -> Alice
|
||||
// channel edges to relatively large non default values. This makes it
|
||||
// possible to pick up more subtle fee calculation errors.
|
||||
maxHtlc := calculateMaxHtlc(chanAmt)
|
||||
const aliceBaseFeeSat = 1
|
||||
const aliceFeeRatePPM = 100000
|
||||
updateChannelPolicy(
|
||||
ht, alice, chanPointAlice, aliceBaseFeeSat*1000,
|
||||
aliceFeeRatePPM, chainreg.DefaultBitcoinTimeLockDelta,
|
||||
maxHtlc, carol,
|
||||
)
|
||||
|
||||
const daveBaseFeeSat = 5
|
||||
const daveFeeRatePPM = 150000
|
||||
updateChannelPolicy(
|
||||
ht, dave, chanPointDave, daveBaseFeeSat*1000, daveFeeRatePPM,
|
||||
chainreg.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
|
||||
)
|
||||
|
||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||
// above.
|
||||
ht.CompletePaymentRequests(carol, payReqs)
|
||||
|
||||
// At this point all the channels within our proto network should be
|
||||
// shifted by 5k satoshis in the direction of Bob, the sink within the
|
||||
// payment flow generated above. The order of asserts corresponds to
|
||||
// increasing of time is needed to embed the HTLC in commitment
|
||||
// transaction, in channel Carol->David->Alice->Bob, order is Bob,
|
||||
// Alice, David, Carol.
|
||||
|
||||
// The final node bob expects to get paid five times 1000 sat.
|
||||
expectedAmountPaidAtoB := int64(numPayments * paymentAmt)
|
||||
|
||||
ht.AssertAmountPaid("Alice(local) => Bob(remote)", bob,
|
||||
chanPointAlice, int64(0), expectedAmountPaidAtoB)
|
||||
ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice,
|
||||
chanPointAlice, expectedAmountPaidAtoB, int64(0))
|
||||
|
||||
// To forward a payment of 1000 sat, Alice is charging a fee of
|
||||
// 1 sat + 10% = 101 sat.
|
||||
const aliceFeePerPayment = aliceBaseFeeSat +
|
||||
(paymentAmt * aliceFeeRatePPM / 1_000_000)
|
||||
const expectedFeeAlice = numPayments * aliceFeePerPayment
|
||||
|
||||
// Dave needs to pay what Alice pays plus Alice's fee.
|
||||
expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice
|
||||
|
||||
ht.AssertAmountPaid("Dave(local) => Alice(remote)", alice,
|
||||
chanPointDave, int64(0), expectedAmountPaidDtoA)
|
||||
ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave,
|
||||
chanPointDave, expectedAmountPaidDtoA, int64(0))
|
||||
|
||||
// To forward a payment of 1101 sat, Dave is charging a fee of
|
||||
// 5 sat + 15% = 170.15 sat. This is rounded down in rpcserver to 170.
|
||||
const davePaymentAmt = paymentAmt + aliceFeePerPayment
|
||||
const daveFeePerPayment = daveBaseFeeSat +
|
||||
(davePaymentAmt * daveFeeRatePPM / 1_000_000)
|
||||
const expectedFeeDave = numPayments * daveFeePerPayment
|
||||
|
||||
// Carol needs to pay what Dave pays plus Dave's fee.
|
||||
expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave
|
||||
|
||||
ht.AssertAmountPaid("Carol(local) => Dave(remote)", dave,
|
||||
chanPointCarol, int64(0), expectedAmountPaidCtoD)
|
||||
ht.AssertAmountPaid("Carol(local) => Dave(remote)", carol,
|
||||
chanPointCarol, expectedAmountPaidCtoD, int64(0))
|
||||
|
||||
// Now that we know all the balances have been settled out properly,
|
||||
// we'll ensure that our internal record keeping for completed circuits
|
||||
// was properly updated.
|
||||
|
||||
// First, check that the FeeReport response shows the proper fees
|
||||
// accrued over each time range. Dave should've earned 170 satoshi for
|
||||
// each of the forwarded payments.
|
||||
ht.AssertFeeReport(
|
||||
dave, expectedFeeDave, expectedFeeDave, expectedFeeDave,
|
||||
)
|
||||
|
||||
// Next, ensure that if we issue the vanilla query for the forwarding
|
||||
// history, it returns 5 values, and each entry is formatted properly.
|
||||
// From David's perspective he receives a payement from Carol and
|
||||
// forwards it to Alice. So let's ensure that the forwarding history
|
||||
// returns Carol's peer alias as inbound and Alice's alias as outbound.
|
||||
info := carol.RPC.GetInfo()
|
||||
carolAlias := info.Alias
|
||||
|
||||
info = alice.RPC.GetInfo()
|
||||
aliceAlias := info.Alias
|
||||
|
||||
fwdingHistory := dave.RPC.ForwardingHistory(nil)
|
||||
require.Len(ht, fwdingHistory.ForwardingEvents, numPayments)
|
||||
|
||||
expectedForwardingFee := uint64(expectedFeeDave / numPayments)
|
||||
for _, event := range fwdingHistory.ForwardingEvents {
|
||||
// Each event should show a fee of 170 satoshi.
|
||||
require.Equal(ht, expectedForwardingFee, event.Fee)
|
||||
|
||||
// Check that peer aliases are empty since the
|
||||
// ForwardingHistoryRequest did not specify the PeerAliasLookup
|
||||
// flag.
|
||||
require.Empty(ht, event.PeerAliasIn)
|
||||
require.Empty(ht, event.PeerAliasOut)
|
||||
}
|
||||
|
||||
// Lookup the forwarding history again but this time also lookup the
|
||||
// peers' alias names.
|
||||
fwdingHistory = dave.RPC.ForwardingHistory(
|
||||
&lnrpc.ForwardingHistoryRequest{
|
||||
PeerAliasLookup: true,
|
||||
},
|
||||
)
|
||||
require.Len(ht, fwdingHistory.ForwardingEvents, numPayments)
|
||||
for _, event := range fwdingHistory.ForwardingEvents {
|
||||
// Each event should show a fee of 170 satoshi.
|
||||
require.Equal(ht, expectedForwardingFee, event.Fee)
|
||||
|
||||
// Check that peer aliases adhere to payment flow, namely
|
||||
// Carol->Dave->Alice.
|
||||
require.Equal(ht, carolAlias, event.PeerAliasIn)
|
||||
require.Equal(ht, aliceAlias, event.PeerAliasOut)
|
||||
}
|
||||
|
||||
// We expect Carol to have successful forwards and settles for
|
||||
// her sends.
|
||||
ht.AssertHtlcEvents(
|
||||
carolEvents, numPayments, 0, numPayments,
|
||||
routerrpc.HtlcEvent_SEND,
|
||||
)
|
||||
|
||||
// Dave and Alice should both have forwards and settles for
|
||||
// their role as forwarding nodes.
|
||||
ht.AssertHtlcEvents(
|
||||
daveEvents, numPayments, 0, numPayments,
|
||||
routerrpc.HtlcEvent_FORWARD,
|
||||
)
|
||||
ht.AssertHtlcEvents(
|
||||
aliceEvents, numPayments, 0, numPayments,
|
||||
routerrpc.HtlcEvent_FORWARD,
|
||||
)
|
||||
|
||||
// Bob should only have settle events for his receives.
|
||||
ht.AssertHtlcEvents(
|
||||
bobEvents, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE,
|
||||
)
|
||||
|
||||
// Finally, close all channels.
|
||||
ht.CloseChannel(alice, chanPointAlice)
|
||||
ht.CloseChannel(dave, chanPointDave)
|
||||
ht.CloseChannel(carol, chanPointCarol)
|
||||
}
|
||||
|
||||
// updateChannelPolicy updates the channel policy of node to the given fees and
|
||||
// timelock delta. This function blocks until listenerNode has received the
|
||||
// policy update.
|
||||
//
|
||||
// NOTE: only used in current test.
|
||||
func updateChannelPolicy(ht *lntemp.HarnessTest, hn *node.HarnessNode,
|
||||
chanPoint *lnrpc.ChannelPoint, baseFee int64,
|
||||
feeRate int64, timeLockDelta uint32,
|
||||
maxHtlc uint64, listenerNode *node.HarnessNode) {
|
||||
|
||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: baseFee,
|
||||
FeeRateMilliMsat: feeRate,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
MinHtlc: 1000, // default value
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
}
|
||||
|
||||
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||
BaseFeeMsat: baseFee,
|
||||
FeeRate: float64(feeRate) / testFeeBase,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||
ChanPoint: chanPoint,
|
||||
},
|
||||
MaxHtlcMsat: maxHtlc,
|
||||
}
|
||||
|
||||
hn.RPC.UpdateChannelPolicy(updateFeeReq)
|
||||
|
||||
// Wait for listener node to receive the channel update from node.
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
listenerNode, hn, expectedPolicy, chanPoint, false,
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,240 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testNetworkConnectionTimeout checks that the connectiontimeout is taking
|
||||
// effect. It creates a node with a small connection timeout value, and
|
||||
// connects it to a non-routable IP address.
|
||||
func testNetworkConnectionTimeout(ht *lntemp.HarnessTest) {
|
||||
var (
|
||||
// testPub is a random public key for testing only.
|
||||
testPub = "0332bda7da70fefe4b6ab92f53b3c4f4ee7999" +
|
||||
"f312284a8e89c8670bb3f67dbee2"
|
||||
|
||||
// testHost is a non-routable IP address. It's used to cause a
|
||||
// connection timeout.
|
||||
testHost = "10.255.255.255"
|
||||
)
|
||||
|
||||
// First, test the global timeout settings.
|
||||
// Create Carol with a connection timeout of 1 millisecond.
|
||||
carol := ht.NewNode("Carol", []string{"--connectiontimeout=1ms"})
|
||||
|
||||
// Try to connect Carol to a non-routable IP address, which should give
|
||||
// us a timeout error.
|
||||
req := &lnrpc.ConnectPeerRequest{
|
||||
Addr: &lnrpc.LightningAddress{
|
||||
Pubkey: testPub,
|
||||
Host: testHost,
|
||||
},
|
||||
}
|
||||
|
||||
// assertTimeoutError asserts that a connection timeout error is
|
||||
// raised. A context with a default timeout is used to make the
|
||||
// request. If our customized connection timeout is less than the
|
||||
// default, we won't see the request context times out, instead a
|
||||
// network connection timeout will be returned.
|
||||
assertTimeoutError := func(hn *node.HarnessNode,
|
||||
req *lnrpc.ConnectPeerRequest) {
|
||||
|
||||
err := hn.RPC.ConnectPeerAssertErr(req)
|
||||
|
||||
// Check that the network returns a timeout error.
|
||||
require.Containsf(ht, err.Error(), "i/o timeout",
|
||||
"expected to get a timeout error, instead got: %v", err)
|
||||
}
|
||||
|
||||
assertTimeoutError(carol, req)
|
||||
|
||||
// Second, test timeout on the connect peer request.
|
||||
// Create Dave with the default timeout setting.
|
||||
dave := ht.NewNode("Dave", nil)
|
||||
|
||||
// Try to connect Dave to a non-routable IP address, using a timeout
|
||||
// value of 1ms, which should give us a timeout error immediately.
|
||||
req = &lnrpc.ConnectPeerRequest{
|
||||
Addr: &lnrpc.LightningAddress{
|
||||
Pubkey: testPub,
|
||||
Host: testHost,
|
||||
},
|
||||
Timeout: 1,
|
||||
}
|
||||
assertTimeoutError(dave, req)
|
||||
}
|
||||
|
||||
// testReconnectAfterIPChange verifies that if a persistent inbound node changes
|
||||
// its listening address then it's peer will still be able to reconnect to it.
|
||||
func testReconnectAfterIPChange(ht *lntemp.HarnessTest) {
|
||||
// In this test, the following network will be set up. A single
|
||||
// dash line represents a peer connection and a double dash line
|
||||
// represents a channel.
|
||||
// Charlie will create a connection to Dave so that Dave is the inbound
|
||||
// peer. This will be made a persistent connection for Charlie so that
|
||||
// Charlie will attempt to reconnect to Dave if Dave restarts.
|
||||
// A channel will be opened between Dave and Alice to ensure that any
|
||||
// NodeAnnouncements that Dave sends will reach Alice.
|
||||
// The connection between Alice and Charlie ensures that Charlie
|
||||
// receives all of Dave's NodeAnnouncements.
|
||||
// The desired behaviour is that if Dave changes his P2P IP address then
|
||||
// Charlie should still be able to reconnect to him.
|
||||
//
|
||||
// /------- Charlie <-----\
|
||||
// | |
|
||||
// v |
|
||||
// Dave <===============> Alice
|
||||
|
||||
// The first thing we will test is the case where Dave advertises two
|
||||
// external IP addresses and then switches from the first one listed
|
||||
// to the second one listed. The desired behaviour is that Charlie will
|
||||
// attempt both of Dave's advertised addresses when attempting to
|
||||
// reconnect.
|
||||
|
||||
// Create a new node, Charlie.
|
||||
charlie := ht.NewNode("Charlie", nil)
|
||||
|
||||
// We derive an extra port for Dave, and we initialise his node with
|
||||
// the port advertised as `--externalip` arguments.
|
||||
ip2 := lntest.NextAvailablePort()
|
||||
|
||||
// Create a new node, Dave, which will initialize a P2P port for him.
|
||||
daveArgs := []string{fmt.Sprintf("--externalip=127.0.0.1:%d", ip2)}
|
||||
dave := ht.NewNode("Dave", daveArgs)
|
||||
|
||||
// We now have two ports, the initial P2P port from creating the node,
|
||||
// and the `externalip` specified above.
|
||||
advertisedAddrs := []string{
|
||||
fmt.Sprintf("127.0.0.1:%d", dave.Cfg.P2PPort),
|
||||
fmt.Sprintf("127.0.0.1:%d", ip2),
|
||||
}
|
||||
|
||||
// Connect Alice to Dave and Charlie.
|
||||
alice := ht.Alice
|
||||
ht.ConnectNodes(alice, dave)
|
||||
ht.ConnectNodes(alice, charlie)
|
||||
|
||||
// We'll then go ahead and open a channel between Alice and Dave. This
|
||||
// ensures that Charlie receives the node announcement from Alice as
|
||||
// part of the announcement broadcast.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, dave, lntemp.OpenChannelParams{Amt: 1000000},
|
||||
)
|
||||
|
||||
// waitForNodeAnnouncement is a closure used to wait on the given graph
|
||||
// subscription for a node announcement from a node with the given
|
||||
// public key. It also waits for the node announcement that advertises
|
||||
// a particular set of addresses.
|
||||
waitForNodeAnnouncement := func(nodePubKey string, addrs []string) {
|
||||
err := wait.NoError(func() error {
|
||||
// Expect to have at least 1 node announcement now.
|
||||
updates := ht.AssertNumNodeAnns(charlie, nodePubKey, 1)
|
||||
|
||||
// Get latest node update from the node.
|
||||
update := updates[len(updates)-1]
|
||||
|
||||
addrMap := make(map[string]bool)
|
||||
for _, addr := range update.NodeAddresses {
|
||||
addrMap[addr.GetAddr()] = true
|
||||
}
|
||||
|
||||
// Check that our wanted addresses can be found from
|
||||
// the node update.
|
||||
for _, addr := range addrs {
|
||||
if !addrMap[addr] {
|
||||
return fmt.Errorf("address %s not "+
|
||||
"found", addr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking node ann")
|
||||
}
|
||||
|
||||
// Wait for Charlie to receive Dave's initial NodeAnnouncement.
|
||||
waitForNodeAnnouncement(dave.PubKeyStr, advertisedAddrs)
|
||||
|
||||
// Now create a persistent connection between Charlie and Dave with no
|
||||
// channels. Charlie is the outbound node and Dave is the inbound node.
|
||||
ht.ConnectNodesPerm(charlie, dave)
|
||||
|
||||
// Change Dave's P2P port to the second IP address that he advertised
|
||||
// and restart his node.
|
||||
dave.Cfg.P2PPort = ip2
|
||||
ht.RestartNode(dave)
|
||||
|
||||
// assert that Dave and Charlie reconnect successfully after Dave
|
||||
// changes to his second advertised address.
|
||||
ht.AssertConnected(dave, charlie)
|
||||
|
||||
// Next we test the case where Dave changes his listening address to one
|
||||
// that was not listed in his original advertised addresses. The desired
|
||||
// behaviour is that Charlie will update his connection requests to Dave
|
||||
// when he receives the Node Announcement from Dave with his updated
|
||||
// address.
|
||||
|
||||
// Change Dave's listening port and restart.
|
||||
dave.Cfg.P2PPort = lntest.NextAvailablePort()
|
||||
dave.Cfg.ExtraArgs = []string{
|
||||
fmt.Sprintf(
|
||||
"--externalip=127.0.0.1:%d", dave.Cfg.P2PPort,
|
||||
),
|
||||
}
|
||||
ht.RestartNode(dave)
|
||||
|
||||
// Show that Charlie does receive Dave's new listening address in
|
||||
// a Node Announcement.
|
||||
waitForNodeAnnouncement(
|
||||
dave.PubKeyStr,
|
||||
[]string{fmt.Sprintf("127.0.0.1:%d", dave.Cfg.P2PPort)},
|
||||
)
|
||||
|
||||
// assert that Dave and Charlie do reconnect after Dave changes his P2P
|
||||
// address to one not listed in Dave's original advertised list of
|
||||
// addresses.
|
||||
ht.AssertConnected(dave, charlie)
|
||||
|
||||
// Finally, close the channel.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testAddPeerConfig tests that the "--addpeer" config flag successfully adds
|
||||
// a new peer.
|
||||
func testAddPeerConfig(ht *lntemp.HarnessTest) {
|
||||
alice := ht.Alice
|
||||
info := alice.RPC.GetInfo()
|
||||
|
||||
alicePeerAddress := info.Uris[0]
|
||||
|
||||
// Create a new node (Carol) with Alice as a peer.
|
||||
args := []string{fmt.Sprintf("--addpeer=%v", alicePeerAddress)}
|
||||
carol := ht.NewNode("Carol", args)
|
||||
|
||||
ht.EnsureConnected(alice, carol)
|
||||
|
||||
// If we list Carol's peers, Alice should already be
|
||||
// listed as one, since we specified her using the
|
||||
// addpeer flag.
|
||||
listPeersResp := carol.RPC.ListPeers()
|
||||
|
||||
parsedPeerAddr, err := lncfg.ParseLNAddressString(
|
||||
alicePeerAddress, "9735", net.ResolveTCPAddr,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
parsedKeyStr := fmt.Sprintf(
|
||||
"%x", parsedPeerAddr.IdentityKey.SerializeCompressed(),
|
||||
)
|
||||
|
||||
require.Equal(ht, parsedKeyStr, listPeersResp.Peers[0].PubKey)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testNeutrino checks that the neutrino sub-server can fetch compact
|
||||
// block filters, server status and connect to a connected peer.
|
||||
func testNeutrino(ht *lntemp.HarnessTest) {
|
||||
if ht.ChainBackendName() != lntest.NeutrinoBackendName {
|
||||
ht.Skipf("skipping test for non neutrino backends")
|
||||
}
|
||||
|
||||
// Check if the neutrino sub server is running.
|
||||
statusRes := ht.Alice.RPC.Status(nil)
|
||||
require.True(ht, statusRes.Active)
|
||||
require.Len(ht, statusRes.Peers, 1, "unable to find a peer")
|
||||
|
||||
// Request the compact block filter of the best block.
|
||||
cFilterReq := &neutrinorpc.GetCFilterRequest{
|
||||
Hash: statusRes.GetBlockHash(),
|
||||
}
|
||||
ht.Alice.RPC.GetCFilter(cFilterReq)
|
||||
|
||||
// Try to reconnect to a connected peer.
|
||||
addPeerReq := &neutrinorpc.AddPeerRequest{
|
||||
PeerAddrs: statusRes.Peers[0],
|
||||
}
|
||||
ht.Alice.RPC.AddPeer(addPeerReq)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//go:build !kvdb_etcd
|
||||
// +build !kvdb_etcd
|
||||
|
||||
package itest
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lntemp"
|
||||
|
||||
// testEtcdFailover is an empty itest when LND is not compiled with etcd
|
||||
// support.
|
||||
func testEtcdFailover(ht *lntemp.HarnessTest) {}
|
||||
@@ -1,135 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testNonstdSweep(ht *lntemp.HarnessTest) {
|
||||
p2shAddr, err := btcutil.NewAddressScriptHash(
|
||||
make([]byte, 1), harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
p2pkhAddr, err := btcutil.NewAddressPubKeyHash(
|
||||
make([]byte, 20), harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
p2wshAddr, err := btcutil.NewAddressWitnessScriptHash(
|
||||
make([]byte, 32), harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
make([]byte, 20), harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
p2trAddr, err := btcutil.NewAddressTaproot(
|
||||
make([]byte, 32), harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
}{
|
||||
{
|
||||
name: "p2sh SendCoins standardness",
|
||||
address: p2shAddr.EncodeAddress(),
|
||||
},
|
||||
{
|
||||
name: "p2pkh SendCoins standardness",
|
||||
address: p2pkhAddr.EncodeAddress(),
|
||||
},
|
||||
{
|
||||
name: "p2wsh SendCoins standardness",
|
||||
address: p2wshAddr.EncodeAddress(),
|
||||
},
|
||||
{
|
||||
name: "p2wkh SendCoins standardness",
|
||||
address: p2wkhAddr.EncodeAddress(),
|
||||
},
|
||||
{
|
||||
name: "p2tr SendCoins standardness",
|
||||
address: p2trAddr.EncodeAddress(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
success := ht.Run(test.name, func(t *testing.T) {
|
||||
st := ht.Subtest(t)
|
||||
|
||||
testNonStdSweepInner(st, test.address)
|
||||
})
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testNonStdSweepInner(ht *lntemp.HarnessTest, address string) {
|
||||
carol := ht.NewNode("carol", nil)
|
||||
|
||||
// Give Carol a UTXO so SendCoins will behave as expected.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// Set the fee estimate to 1sat/vbyte.
|
||||
ht.SetFeeEstimate(250)
|
||||
|
||||
// Make Carol call SendCoins with the SendAll flag and the created
|
||||
// address.
|
||||
sendReq := &lnrpc.SendCoinsRequest{
|
||||
Addr: address,
|
||||
SatPerVbyte: 1,
|
||||
SendAll: true,
|
||||
}
|
||||
|
||||
// If a non-standard transaction was created, then this SendCoins call
|
||||
// will fail.
|
||||
carol.RPC.SendCoins(sendReq)
|
||||
|
||||
// Fetch the txid so we can grab the raw transaction.
|
||||
txid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
tx := ht.Miner.GetRawTransaction(txid)
|
||||
|
||||
msgTx := tx.MsgTx()
|
||||
|
||||
// Fetch the fee of the transaction.
|
||||
var (
|
||||
inputVal int
|
||||
outputVal int
|
||||
fee int
|
||||
)
|
||||
|
||||
for _, inp := range msgTx.TxIn {
|
||||
// Fetch the previous outpoint's value.
|
||||
prevOut := inp.PreviousOutPoint
|
||||
ptx := ht.Miner.GetRawTransaction(&prevOut.Hash)
|
||||
|
||||
pout := ptx.MsgTx().TxOut[prevOut.Index]
|
||||
inputVal += int(pout.Value)
|
||||
}
|
||||
|
||||
for _, outp := range msgTx.TxOut {
|
||||
outputVal += int(outp.Value)
|
||||
}
|
||||
|
||||
fee = inputVal - outputVal
|
||||
|
||||
// Fetch the vsize of the transaction so we can determine if the
|
||||
// transaction pays >= 1 sat/vbyte.
|
||||
rawTx := ht.Miner.GetRawTransactionVerbose(txid)
|
||||
|
||||
// Require fee >= vbytes.
|
||||
require.True(ht, fee >= int(rawTx.Vsize))
|
||||
|
||||
// Mine a block to keep the mempool clean.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testChainKit tests ChainKit RPC endpoints.
|
||||
func testChainKit(ht *lntemp.HarnessTest) {
|
||||
// Test functions registered as test cases spin up separate nodes
|
||||
// during execution. By calling sub-test functions as seen below we
|
||||
// avoid the need to start separate nodes.
|
||||
testChainKitGetBlock(ht)
|
||||
testChainKitGetBlockHash(ht)
|
||||
}
|
||||
|
||||
// testChainKitGetBlock ensures that given a block hash, the RPC endpoint
|
||||
// returns the correct target block.
|
||||
func testChainKitGetBlock(ht *lntemp.HarnessTest) {
|
||||
// Get best block hash.
|
||||
bestBlockRes := ht.Alice.RPC.GetBestBlock(nil)
|
||||
|
||||
var bestBlockHash chainhash.Hash
|
||||
err := bestBlockHash.SetBytes(bestBlockRes.BlockHash)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Retrieve the best block by hash.
|
||||
getBlockReq := &chainrpc.GetBlockRequest{
|
||||
BlockHash: bestBlockHash[:],
|
||||
}
|
||||
getBlockRes := ht.Alice.RPC.GetBlock(getBlockReq)
|
||||
|
||||
// Deserialize the block which was retrieved by hash.
|
||||
msgBlock := &wire.MsgBlock{}
|
||||
blockReader := bytes.NewReader(getBlockRes.RawBlock)
|
||||
err = msgBlock.Deserialize(blockReader)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Ensure best block hash is the same as retrieved block hash.
|
||||
expected := bestBlockHash
|
||||
actual := msgBlock.BlockHash()
|
||||
require.Equal(ht, expected, actual)
|
||||
}
|
||||
|
||||
// testChainKitGetBlockHash ensures that given a block height, the RPC endpoint
|
||||
// returns the correct target block hash.
|
||||
func testChainKitGetBlockHash(ht *lntemp.HarnessTest) {
|
||||
// Get best block hash.
|
||||
bestBlockRes := ht.Alice.RPC.GetBestBlock(nil)
|
||||
|
||||
// Retrieve the block hash at best block height.
|
||||
req := &chainrpc.GetBlockHashRequest{
|
||||
BlockHeight: int64(bestBlockRes.BlockHeight),
|
||||
}
|
||||
getBlockHashRes := ht.Alice.RPC.GetBlockHash(req)
|
||||
|
||||
// Ensure best block hash is the same as retrieved block hash.
|
||||
expected := bestBlockRes.BlockHash
|
||||
actual := getBlockHashRes.BlockHash
|
||||
require.Equal(ht, expected, actual)
|
||||
}
|
||||
|
||||
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee
|
||||
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
|
||||
//
|
||||
// TODO(wilmer): Add RBF case once btcd supports it.
|
||||
func testCPFP(ht *lntemp.HarnessTest) {
|
||||
runCPFP(ht, ht.Alice, ht.Bob)
|
||||
}
|
||||
|
||||
// runCPFP ensures that the daemon can bump an unconfirmed transaction's fee
|
||||
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction.
|
||||
func runCPFP(ht *lntemp.HarnessTest, alice, bob *node.HarnessNode) {
|
||||
// Skip this test for neutrino, as it's not aware of mempool
|
||||
// transactions.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.Skipf("skipping CPFP test for neutrino backend")
|
||||
}
|
||||
|
||||
// We'll start the test by sending Alice some coins, which she'll use
|
||||
// to send to Bob.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
|
||||
// Create an address for Bob to send the coins to.
|
||||
req := &lnrpc.NewAddressRequest{
|
||||
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
}
|
||||
resp := bob.RPC.NewAddress(req)
|
||||
|
||||
// Send the coins from Alice to Bob. We should expect a transaction to
|
||||
// be broadcast and seen in the mempool.
|
||||
sendReq := &lnrpc.SendCoinsRequest{
|
||||
Addr: resp.Address,
|
||||
Amount: btcutil.SatoshiPerBitcoin,
|
||||
}
|
||||
alice.RPC.SendCoins(sendReq)
|
||||
txid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// We'll then extract the raw transaction from the mempool in order to
|
||||
// determine the index of Bob's output.
|
||||
tx := ht.Miner.GetRawTransaction(txid)
|
||||
bobOutputIdx := -1
|
||||
for i, txOut := range tx.MsgTx().TxOut {
|
||||
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||
txOut.PkScript, ht.Miner.ActiveNet,
|
||||
)
|
||||
require.NoErrorf(ht, err, "unable to extract address "+
|
||||
"from pkScript=%x: %v", txOut.PkScript, err)
|
||||
|
||||
if addrs[0].String() == resp.Address {
|
||||
bobOutputIdx = i
|
||||
}
|
||||
}
|
||||
require.NotEqual(ht, -1, bobOutputIdx, "bob's output was not found "+
|
||||
"within the transaction")
|
||||
|
||||
// Wait until bob has seen the tx and considers it as owned.
|
||||
op := &lnrpc.OutPoint{
|
||||
TxidBytes: txid[:],
|
||||
OutputIndex: uint32(bobOutputIdx),
|
||||
}
|
||||
ht.AssertUTXOInWallet(bob, op, "")
|
||||
|
||||
// We'll attempt to bump the fee of this transaction by performing a
|
||||
// CPFP from Alice's point of view.
|
||||
bumpFeeReq := &walletrpc.BumpFeeRequest{
|
||||
Outpoint: op,
|
||||
SatPerVbyte: uint64(
|
||||
sweep.DefaultMaxFeeRate.FeePerKVByte() / 2000,
|
||||
),
|
||||
}
|
||||
bob.RPC.BumpFee(bumpFeeReq)
|
||||
|
||||
// We should now expect to see two transactions within the mempool, a
|
||||
// parent and its child.
|
||||
ht.Miner.AssertNumTxsInMempool(2)
|
||||
|
||||
// We should also expect to see the output being swept by the
|
||||
// UtxoSweeper. We'll ensure it's using the fee rate specified.
|
||||
pendingSweepsResp := bob.RPC.PendingSweeps()
|
||||
require.Len(ht, pendingSweepsResp.PendingSweeps, 1,
|
||||
"expected to find 1 pending sweep")
|
||||
pendingSweep := pendingSweepsResp.PendingSweeps[0]
|
||||
require.Equal(ht, pendingSweep.Outpoint.TxidBytes, op.TxidBytes,
|
||||
"output txid not matched")
|
||||
require.Equal(ht, pendingSweep.Outpoint.OutputIndex, op.OutputIndex,
|
||||
"output index not matched")
|
||||
require.Equal(ht, pendingSweep.SatPerVbyte, bumpFeeReq.SatPerVbyte,
|
||||
"sweep sat per vbyte not matched")
|
||||
|
||||
// Mine a block to clean up the unconfirmed transactions.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 2)
|
||||
|
||||
// The input used to CPFP should no longer be pending.
|
||||
err := wait.NoError(func() error {
|
||||
resp := bob.RPC.PendingSweeps()
|
||||
if len(resp.PendingSweeps) != 0 {
|
||||
return fmt.Errorf("expected 0 pending sweeps, found %d",
|
||||
len(resp.PendingSweeps))
|
||||
}
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking bob's pending sweeps")
|
||||
}
|
||||
|
||||
// testAnchorReservedValue tests that we won't allow sending transactions when
|
||||
// that would take the value we reserve for anchor fee bumping out of our
|
||||
// wallet.
|
||||
func testAnchorReservedValue(ht *lntemp.HarnessTest) {
|
||||
// Start two nodes supporting anchor channels.
|
||||
args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
|
||||
|
||||
// NOTE: we cannot reuse the standby node here as the test requires the
|
||||
// node to start with no UTXOs.
|
||||
alice := ht.NewNode("Alice", args)
|
||||
bob := ht.Bob
|
||||
ht.RestartNodeWithExtraArgs(bob, args)
|
||||
|
||||
ht.ConnectNodes(alice, bob)
|
||||
|
||||
// Send just enough coins for Alice to open a channel without a change
|
||||
// output.
|
||||
const (
|
||||
chanAmt = 1000000
|
||||
feeEst = 8000
|
||||
)
|
||||
|
||||
ht.FundCoins(chanAmt+feeEst, alice)
|
||||
|
||||
// wallet, without a change output. This should not be allowed.
|
||||
ht.OpenChannelAssertErr(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
}, lnwallet.ErrReservedValueInvalidated,
|
||||
)
|
||||
|
||||
// Alice opens a smaller channel. This works since it will have a
|
||||
// change output.
|
||||
chanPoint1 := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt / 4},
|
||||
)
|
||||
|
||||
// If Alice tries to open another anchor channel to Bob, Bob should not
|
||||
// reject it as he is not contributing any funds.
|
||||
chanPoint2 := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt / 4},
|
||||
)
|
||||
|
||||
// Similarly, if Alice tries to open a legacy channel to Bob, Bob
|
||||
// should not reject it as he is not contributing any funds. We'll
|
||||
// restart Bob to remove his support for anchors.
|
||||
ht.RestartNode(bob)
|
||||
|
||||
// Before opening the channel, make sure the nodes are connected.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
chanPoint3 := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt / 4},
|
||||
)
|
||||
chanPoints := []*lnrpc.ChannelPoint{chanPoint1, chanPoint2, chanPoint3}
|
||||
|
||||
// Alice tries to send all coins to an internal address. This is
|
||||
// allowed, since the final wallet balance will still be above the
|
||||
// reserved value.
|
||||
req := &lnrpc.NewAddressRequest{
|
||||
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
}
|
||||
resp := alice.RPC.NewAddress(req)
|
||||
|
||||
sweepReq := &lnrpc.SendCoinsRequest{
|
||||
Addr: resp.Address,
|
||||
SendAll: true,
|
||||
}
|
||||
alice.RPC.SendCoins(sweepReq)
|
||||
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
assertNumTxInAndTxOut := func(tx *wire.MsgTx, in, out int) {
|
||||
require.Len(ht, tx.TxIn, in, "num inputs not matched")
|
||||
require.Len(ht, tx.TxOut, out, "num outputs not matched")
|
||||
}
|
||||
|
||||
// The sweep transaction should have exactly one input, the change from
|
||||
// the previous SendCoins call.
|
||||
sweepTx := block.Transactions[1]
|
||||
|
||||
// It should have a single output.
|
||||
assertNumTxInAndTxOut(sweepTx, 1, 1)
|
||||
|
||||
// Wait for Alice to see her balance as confirmed.
|
||||
waitForConfirmedBalance := func() int64 {
|
||||
var balance int64
|
||||
err := wait.NoError(func() error {
|
||||
resp := alice.RPC.WalletBalance()
|
||||
|
||||
if resp.TotalBalance == 0 {
|
||||
return fmt.Errorf("no balance")
|
||||
}
|
||||
|
||||
if resp.UnconfirmedBalance > 0 {
|
||||
return fmt.Errorf("unconfirmed balance")
|
||||
}
|
||||
|
||||
balance = resp.TotalBalance
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking alice's balance")
|
||||
|
||||
return balance
|
||||
}
|
||||
|
||||
waitForConfirmedBalance()
|
||||
|
||||
// Alice tries to send all funds to an external address, the reserved
|
||||
// value must stay in her wallet.
|
||||
minerAddr := ht.Miner.NewMinerAddress()
|
||||
|
||||
sweepReq = &lnrpc.SendCoinsRequest{
|
||||
Addr: minerAddr.String(),
|
||||
SendAll: true,
|
||||
}
|
||||
alice.RPC.SendCoins(sweepReq)
|
||||
|
||||
// We'll mine a block which should include the sweep transaction we
|
||||
// generated above.
|
||||
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// The sweep transaction should have exactly one inputs as we only had
|
||||
// the single output from above in the wallet.
|
||||
sweepTx = block.Transactions[1]
|
||||
|
||||
// It should have two outputs, one being the miner address, the other
|
||||
// one being the reserve going back to our wallet.
|
||||
assertNumTxInAndTxOut(sweepTx, 1, 2)
|
||||
|
||||
// The reserved value is now back in Alice's wallet.
|
||||
aliceBalance := waitForConfirmedBalance()
|
||||
|
||||
// Alice closes channel, should now be allowed to send everything to an
|
||||
// external address.
|
||||
for _, chanPoint := range chanPoints {
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
newBalance := waitForConfirmedBalance()
|
||||
require.Greater(ht, newBalance, aliceBalance,
|
||||
"Alice's balance did not increase after channel close")
|
||||
|
||||
// Assert there are no open or pending channels anymore.
|
||||
ht.AssertNumWaitingClose(alice, 0)
|
||||
ht.AssertNodeNumChannels(alice, 0)
|
||||
|
||||
// We'll wait for the balance to reflect that the channel has been
|
||||
// closed and the funds are in the wallet.
|
||||
sweepReq = &lnrpc.SendCoinsRequest{
|
||||
Addr: minerAddr.String(),
|
||||
SendAll: true,
|
||||
}
|
||||
alice.RPC.SendCoins(sweepReq)
|
||||
|
||||
// We'll mine a block which should include the sweep transaction we
|
||||
// generated above.
|
||||
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// The sweep transaction should have four inputs, the change output from
|
||||
// the previous sweep, and the outputs from the coop closed channels.
|
||||
sweepTx = block.Transactions[1]
|
||||
|
||||
// It should have a single output.
|
||||
assertNumTxInAndTxOut(sweepTx, 4, 1)
|
||||
}
|
||||
|
||||
// testAnchorThirdPartySpend tests that if we force close a channel, but then
|
||||
// don't sweep the anchor in time and a 3rd party spends it, that we remove any
|
||||
// transactions that are a descendent of that sweep.
|
||||
func testAnchorThirdPartySpend(ht *lntemp.HarnessTest) {
|
||||
// First, we'll create two new nodes that both default to anchor
|
||||
// channels.
|
||||
//
|
||||
// NOTE: The itests differ here as anchors is default off vs the normal
|
||||
// lnd binary.
|
||||
args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
|
||||
alice := ht.NewNode("Alice", args)
|
||||
defer ht.Shutdown(alice)
|
||||
|
||||
bob := ht.NewNode("Bob", args)
|
||||
defer ht.Shutdown(bob)
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// We'll fund our Alice with coins, as she'll be opening the channel.
|
||||
// We'll fund her with *just* enough coins to open the channel and
|
||||
// sweep the anchor.
|
||||
const (
|
||||
firstChanSize = 1_000_000
|
||||
anchorFeeBuffer = 500_000
|
||||
)
|
||||
ht.FundCoins(firstChanSize+anchorFeeBuffer, alice)
|
||||
|
||||
// Open the channel between the two nodes and wait for it to confirm
|
||||
// fully.
|
||||
aliceChanPoint1 := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: firstChanSize,
|
||||
},
|
||||
)
|
||||
|
||||
// Send another UTXO if this is a neutrino backend. When sweeping
|
||||
// anchors, there are two transactions created, `local_sweep_tx` for
|
||||
// sweeping Alice's anchor on the local commitment, `remote_sweep_tx`
|
||||
// for sweeping her anchor on the remote commitment. Whenever the force
|
||||
// close transaction is published, Alice will always create these two
|
||||
// transactions to sweep her anchor.
|
||||
// On the other hand, when creating the sweep txes, the anchor itself
|
||||
// is not able to cover the fee, so another wallet UTXO is needed. In
|
||||
// our test case, there's a change output that can be used from the
|
||||
// above funding process. And it's used by both sweep txes - when `lnd`
|
||||
// happens to create the `remote_sweep_tx` first, it will receive an
|
||||
// error since its parent tx, the remote commitment, is not known,
|
||||
// hence freeing the change output to be used by `local_sweep_tx`.
|
||||
// For neutrino client, however, it will consider the transaction which
|
||||
// sweeps the remote anchor as an orphan tx, and it will neither send
|
||||
// it to the mempool nor return an error to free the change output.
|
||||
// Thus, if the change output is already used in `remote_sweep_tx`, we
|
||||
// won't have UTXO to create `local_sweep_tx`.
|
||||
//
|
||||
// NOTE: the order of the sweep requests for the two anchors cannot be
|
||||
// guaranteed. If the sweeper happens to sweep the remote anchor first,
|
||||
// then the test won't pass without the extra UTXO, which is the source
|
||||
// of the flakeness.
|
||||
//
|
||||
// TODO(yy): make a RPC server for sweeper so we can explicitly check
|
||||
// and control its state.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.FundCoins(anchorFeeBuffer, alice)
|
||||
}
|
||||
|
||||
// With the channel open, we'll actually immediately force close it. We
|
||||
// don't care about network announcements here since there's no routing
|
||||
// in this test.
|
||||
ht.CloseChannelAssertPending(alice, aliceChanPoint1, true)
|
||||
|
||||
// Now that the channel has been force closed, it should show up in the
|
||||
// PendingChannels RPC under the waiting close section.
|
||||
waitingClose := ht.AssertChannelWaitingClose(alice, aliceChanPoint1)
|
||||
|
||||
// At this point, the channel is waiting close, and we have both the
|
||||
// commitment transaction and anchor sweep in the mempool.
|
||||
const expectedTxns = 2
|
||||
sweepTxns := ht.Miner.GetNumTxsFromMempool(expectedTxns)
|
||||
aliceCloseTx := waitingClose.Commitments.LocalTxid
|
||||
_, aliceAnchor := ht.FindCommitAndAnchor(sweepTxns, aliceCloseTx)
|
||||
|
||||
// We'll now mine _only_ the commitment force close transaction, as we
|
||||
// want the anchor sweep to stay unconfirmed.
|
||||
forceCloseTxID, _ := chainhash.NewHashFromStr(aliceCloseTx)
|
||||
commitTxn := ht.Miner.GetRawTransaction(forceCloseTxID)
|
||||
ht.Miner.MineBlockWithTxes([]*btcutil.Tx{commitTxn})
|
||||
|
||||
// With the anchor output located, and the main commitment mined we'll
|
||||
// instruct the wallet to send all coins in the wallet to a new address
|
||||
// (to the miner), including unconfirmed change.
|
||||
minerAddr := ht.Miner.NewMinerAddress()
|
||||
sweepReq := &lnrpc.SendCoinsRequest{
|
||||
Addr: minerAddr.String(),
|
||||
SendAll: true,
|
||||
MinConfs: 0,
|
||||
SpendUnconfirmed: true,
|
||||
}
|
||||
sweepAllResp := alice.RPC.SendCoins(sweepReq)
|
||||
|
||||
// Both the original anchor sweep transaction, as well as the
|
||||
// transaction we created to sweep all the coins from Alice's wallet
|
||||
// should be found in her transaction store.
|
||||
sweepAllTxID, _ := chainhash.NewHashFromStr(sweepAllResp.Txid)
|
||||
ht.AssertTransactionInWallet(alice, aliceAnchor.SweepTx.TxHash())
|
||||
ht.AssertTransactionInWallet(alice, *sweepAllTxID)
|
||||
|
||||
// Next, we'll shutdown Alice, and allow 16 blocks to pass so that the
|
||||
// anchor output can be swept by anyone. Rather than use the normal API
|
||||
// call, we'll generate a series of _empty_ blocks here.
|
||||
aliceRestart := ht.SuspendNode(alice)
|
||||
const anchorCsv = 16
|
||||
ht.MineEmptyBlocks(anchorCsv)
|
||||
|
||||
// Before we sweep the anchor, we'll restart Alice.
|
||||
require.NoErrorf(ht, aliceRestart(), "unable to restart alice")
|
||||
|
||||
// Now that the channel has been closed, and Alice has an unconfirmed
|
||||
// transaction spending the output produced by her anchor sweep, we'll
|
||||
// mine a transaction that double spends the output.
|
||||
thirdPartyAnchorSweep := genAnchorSweep(ht, aliceAnchor, anchorCsv)
|
||||
ht.Miner.MineBlockWithTxes([]*btcutil.Tx{thirdPartyAnchorSweep})
|
||||
|
||||
// At this point, we should no longer find Alice's transaction that
|
||||
// tried to sweep the anchor in her wallet.
|
||||
ht.AssertTransactionNotInWallet(alice, aliceAnchor.SweepTx.TxHash())
|
||||
|
||||
// In addition, the transaction she sent to sweep all her coins to the
|
||||
// miner also should no longer be found.
|
||||
ht.AssertTransactionNotInWallet(alice, *sweepAllTxID)
|
||||
|
||||
// The anchor should now show as being "lost", while the force close
|
||||
// response is still present.
|
||||
assertAnchorOutputLost(ht, alice, aliceChanPoint1)
|
||||
|
||||
// At this point Alice's CSV output should already be fully spent and
|
||||
// the channel marked as being resolved. We mine a block first, as so
|
||||
// far we've been generating custom blocks this whole time.
|
||||
commitSweepOp := wire.OutPoint{
|
||||
Hash: *forceCloseTxID,
|
||||
Index: 1,
|
||||
}
|
||||
ht.Miner.AssertOutpointInMempool(commitSweepOp)
|
||||
ht.MineBlocks(1)
|
||||
|
||||
ht.AssertNumWaitingClose(alice, 0)
|
||||
}
|
||||
|
||||
// assertAnchorOutputLost asserts that the anchor output for the given channel
|
||||
// has the state of being lost.
|
||||
func assertAnchorOutputLost(ht *lntemp.HarnessTest, hn *node.HarnessNode,
|
||||
chanPoint *lnrpc.ChannelPoint) {
|
||||
|
||||
cp := ht.OutPointFromChannelPoint(chanPoint)
|
||||
|
||||
expected := lnrpc.PendingChannelsResponse_ForceClosedChannel_LOST
|
||||
|
||||
err := wait.NoError(func() error {
|
||||
resp := hn.RPC.PendingChannels()
|
||||
channels := resp.PendingForceClosingChannels
|
||||
|
||||
for _, c := range channels {
|
||||
// Not the wanted channel, skipped.
|
||||
if c.Channel.ChannelPoint != cp.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found the channel, check the anchor state.
|
||||
if c.Anchor == expected {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected anchor state, want %v, "+
|
||||
"got %v", expected, c.Anchor)
|
||||
}
|
||||
|
||||
return fmt.Errorf("channel not found using cp=%v", cp)
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "anchor doesn't show as being lost")
|
||||
}
|
||||
|
||||
// genAnchorSweep generates a "3rd party" anchor sweeping from an existing one.
|
||||
// In practice, we just re-use the existing witness, and track on our own
|
||||
// output producing a 1-in-1-out transaction.
|
||||
func genAnchorSweep(ht *lntemp.HarnessTest,
|
||||
aliceAnchor *lntemp.SweptOutput, anchorCsv uint32) *btcutil.Tx {
|
||||
|
||||
// At this point, we have the transaction that Alice used to try to
|
||||
// sweep her anchor. As this is actually just something anyone can
|
||||
// spend, just need to find the input spending the anchor output, then
|
||||
// we can swap the output address.
|
||||
aliceAnchorTxIn := func() wire.TxIn {
|
||||
sweepCopy := aliceAnchor.SweepTx.Copy()
|
||||
for _, txIn := range sweepCopy.TxIn {
|
||||
if txIn.PreviousOutPoint == aliceAnchor.OutPoint {
|
||||
return *txIn
|
||||
}
|
||||
}
|
||||
|
||||
require.FailNow(ht, "anchor op not found")
|
||||
|
||||
return wire.TxIn{}
|
||||
}()
|
||||
|
||||
// We'll set the signature on the input to nil, and then set the
|
||||
// sequence to 16 (the anchor CSV period).
|
||||
aliceAnchorTxIn.Witness[0] = nil
|
||||
aliceAnchorTxIn.Sequence = anchorCsv
|
||||
|
||||
minerAddr := ht.Miner.NewMinerAddress()
|
||||
addrScript, err := txscript.PayToAddrScript(minerAddr)
|
||||
require.NoError(ht, err, "unable to gen addr script")
|
||||
|
||||
// Now that we have the txIn, we can just make a new transaction that
|
||||
// uses a different script for the output.
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxIn(&aliceAnchorTxIn)
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
PkScript: addrScript,
|
||||
Value: anchorSize - 1,
|
||||
})
|
||||
|
||||
return btcutil.NewTx(tx)
|
||||
}
|
||||
@@ -1,548 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcjson"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntemp/rpc"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testOpenChannelAfterReorg tests that in the case where we have an open
|
||||
// channel where the funding tx gets reorged out, the channel will no
|
||||
// longer be present in the node's routing table.
|
||||
func testOpenChannelAfterReorg(ht *lntemp.HarnessTest) {
|
||||
// Skip test for neutrino, as we cannot disconnect the miner at will.
|
||||
// TODO(halseth): remove when either can disconnect at will, or restart
|
||||
// node with connection to new miner.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.Skipf("skipping reorg test for neutrino backend")
|
||||
}
|
||||
|
||||
temp := "temp"
|
||||
|
||||
// Set up a new miner that we can use to cause a reorg.
|
||||
tempLogDir := ".tempminerlogs"
|
||||
logFilename := "output-open_channel_reorg-temp_miner.log"
|
||||
tempMiner := lntemp.NewTempMiner(
|
||||
ht.Context(), ht.T, tempLogDir, logFilename,
|
||||
)
|
||||
defer tempMiner.Stop()
|
||||
|
||||
// Setup the temp miner
|
||||
require.NoError(ht, tempMiner.SetUp(false, 0),
|
||||
"unable to set up mining node")
|
||||
|
||||
miner := ht.Miner
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// We start by connecting the new miner to our original miner,
|
||||
// such that it will sync to our original chain.
|
||||
err := miner.Client.Node(
|
||||
btcjson.NConnect, tempMiner.P2PAddress(), &temp,
|
||||
)
|
||||
require.NoError(ht, err, "unable to connect miners")
|
||||
|
||||
nodeSlice := []*rpctest.Harness{miner.Harness, tempMiner.Harness}
|
||||
err = rpctest.JoinNodes(nodeSlice, rpctest.Blocks)
|
||||
require.NoError(ht, err, "unable to join node on blocks")
|
||||
|
||||
// The two miners should be on the same blockheight.
|
||||
assertMinerBlockHeightDelta(ht, miner, tempMiner, 0)
|
||||
|
||||
// We disconnect the two miners, such that we can mine two different
|
||||
// chains and can cause a reorg later.
|
||||
err = miner.Client.Node(
|
||||
btcjson.NDisconnect, tempMiner.P2PAddress(), &temp,
|
||||
)
|
||||
require.NoError(ht, err, "unable to disconnect miners")
|
||||
|
||||
// Create a new channel that requires 1 confs before it's considered
|
||||
// open, then broadcast the funding transaction
|
||||
params := lntemp.OpenChannelParams{
|
||||
Amt: funding.MaxBtcFundingAmount,
|
||||
Private: true,
|
||||
}
|
||||
pendingUpdate := ht.OpenChannelAssertPending(alice, bob, params)
|
||||
|
||||
// Wait for miner to have seen the funding tx. The temporary miner is
|
||||
// disconnected, and won't see the transaction.
|
||||
ht.Miner.AssertNumTxsInMempool(1)
|
||||
|
||||
// At this point, the channel's funding transaction will have been
|
||||
// broadcast, but not confirmed, and the channel should be pending.
|
||||
ht.AssertNodesNumPendingOpenChannels(alice, bob, 1)
|
||||
|
||||
fundingTxID, err := chainhash.NewHash(pendingUpdate.Txid)
|
||||
require.NoError(ht, err, "convert funding txid into chainhash failed")
|
||||
|
||||
// We now cause a fork, by letting our original miner mine 10 blocks,
|
||||
// and our new miner mine 15. This will also confirm our pending
|
||||
// channel on the original miner's chain, which should be considered
|
||||
// open.
|
||||
block := ht.MineBlocks(10)[0]
|
||||
ht.Miner.AssertTxInBlock(block, fundingTxID)
|
||||
_, err = tempMiner.Client.Generate(15)
|
||||
require.NoError(ht, err, "unable to generate blocks")
|
||||
|
||||
// Ensure the chain lengths are what we expect, with the temp miner
|
||||
// being 5 blocks ahead.
|
||||
assertMinerBlockHeightDelta(ht, miner, tempMiner, 5)
|
||||
|
||||
chanPoint := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: pendingUpdate.Txid,
|
||||
},
|
||||
OutputIndex: pendingUpdate.OutputIndex,
|
||||
}
|
||||
|
||||
// Ensure channel is no longer pending.
|
||||
ht.AssertNodesNumPendingOpenChannels(alice, bob, 0)
|
||||
|
||||
// Wait for Alice and Bob to recognize and advertise the new channel
|
||||
// generated above.
|
||||
ht.AssertTopologyChannelOpen(alice, chanPoint)
|
||||
ht.AssertTopologyChannelOpen(bob, chanPoint)
|
||||
|
||||
// Alice should now have 1 edge in her graph.
|
||||
ht.AssertNumEdges(alice, 1, true)
|
||||
|
||||
// Now we disconnect Alice's chain backend from the original miner, and
|
||||
// connect the two miners together. Since the temporary miner knows
|
||||
// about a longer chain, both miners should sync to that chain.
|
||||
ht.DisconnectMiner()
|
||||
|
||||
// Connecting to the temporary miner should now cause our original
|
||||
// chain to be re-orged out.
|
||||
err = miner.Client.Node(btcjson.NConnect, tempMiner.P2PAddress(), &temp)
|
||||
require.NoError(ht, err, "unable to connect temp miner")
|
||||
|
||||
nodes := []*rpctest.Harness{tempMiner.Harness, miner.Harness}
|
||||
err = rpctest.JoinNodes(nodes, rpctest.Blocks)
|
||||
require.NoError(ht, err, "unable to join node on blocks")
|
||||
|
||||
// Once again they should be on the same chain.
|
||||
assertMinerBlockHeightDelta(ht, miner, tempMiner, 0)
|
||||
|
||||
// Now we disconnect the two miners, and connect our original miner to
|
||||
// our chain backend once again.
|
||||
err = miner.Client.Node(
|
||||
btcjson.NDisconnect, tempMiner.P2PAddress(), &temp,
|
||||
)
|
||||
require.NoError(ht, err, "unable to disconnect temp miner")
|
||||
|
||||
ht.ConnectMiner()
|
||||
|
||||
// This should have caused a reorg, and Alice should sync to the longer
|
||||
// chain, where the funding transaction is not confirmed.
|
||||
_, tempMinerHeight, err := tempMiner.Client.GetBestBlock()
|
||||
require.NoError(ht, err, "unable to get current blockheight")
|
||||
ht.WaitForNodeBlockHeight(alice, tempMinerHeight)
|
||||
|
||||
// Since the fundingtx was reorged out, Alice should now have no edges
|
||||
// in her graph.
|
||||
ht.AssertNumEdges(alice, 0, true)
|
||||
|
||||
// Cleanup by mining the funding tx again, then closing the channel.
|
||||
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, fundingTxID)
|
||||
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testOpenChannelFeePolicy checks if different channel fee scenarios
|
||||
// are correctly handled when the optional channel fee parameters
|
||||
// baseFee and feeRate are provided. If the OpenChannelRequest is not
|
||||
// provided with a value for baseFee/feeRate the expectation is that the
|
||||
// default baseFee/feeRate is applied.
|
||||
// 1.) no params provided to OpenChannelRequest
|
||||
// ChannelUpdate --> defaultBaseFee, defaultFeeRate
|
||||
// 2.) only baseFee provided to OpenChannelRequest
|
||||
// ChannelUpdate --> provided baseFee, defaultFeeRate
|
||||
// 3.) only feeRate provided to OpenChannelRequest
|
||||
// ChannelUpdate --> defaultBaseFee, provided FeeRate
|
||||
// 4.) baseFee and feeRate provided to OpenChannelRequest
|
||||
// ChannelUpdate --> provided baseFee, provided feeRate.
|
||||
func testOpenChannelUpdateFeePolicy(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
defaultBaseFee = 1000
|
||||
defaultFeeRate = 1
|
||||
defaultTimeLockDelta = chainreg.DefaultBitcoinTimeLockDelta
|
||||
defaultMinHtlc = 1000
|
||||
optionalBaseFee = 1337
|
||||
optionalFeeRate = 1337
|
||||
)
|
||||
|
||||
defaultMaxHtlc := calculateMaxHtlc(funding.MaxBtcFundingAmount)
|
||||
|
||||
chanAmt := funding.MaxBtcFundingAmount
|
||||
pushAmt := chanAmt / 2
|
||||
|
||||
feeScenarios := []lntemp.OpenChannelParams{
|
||||
{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
UseBaseFee: false,
|
||||
UseFeeRate: false,
|
||||
},
|
||||
{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
BaseFee: optionalBaseFee,
|
||||
UseBaseFee: true,
|
||||
UseFeeRate: false,
|
||||
},
|
||||
{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
FeeRate: optionalFeeRate,
|
||||
UseBaseFee: false,
|
||||
UseFeeRate: true,
|
||||
},
|
||||
{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
BaseFee: optionalBaseFee,
|
||||
FeeRate: optionalFeeRate,
|
||||
UseBaseFee: true,
|
||||
UseFeeRate: true,
|
||||
},
|
||||
}
|
||||
|
||||
expectedPolicies := []lnrpc.RoutingPolicy{
|
||||
{
|
||||
FeeBaseMsat: defaultBaseFee,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
},
|
||||
{
|
||||
FeeBaseMsat: optionalBaseFee,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
},
|
||||
{
|
||||
FeeBaseMsat: defaultBaseFee,
|
||||
FeeRateMilliMsat: optionalFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
},
|
||||
{
|
||||
FeeBaseMsat: optionalBaseFee,
|
||||
FeeRateMilliMsat: optionalFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
},
|
||||
}
|
||||
|
||||
bobExpectedPolicy := lnrpc.RoutingPolicy{
|
||||
FeeBaseMsat: defaultBaseFee,
|
||||
FeeRateMilliMsat: defaultFeeRate,
|
||||
TimeLockDelta: defaultTimeLockDelta,
|
||||
MinHtlc: defaultMinHtlc,
|
||||
MaxHtlcMsat: defaultMaxHtlc,
|
||||
}
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
runTestCase := func(ht *lntemp.HarnessTest,
|
||||
fs lntemp.OpenChannelParams,
|
||||
alicePolicy, bobPolicy *lnrpc.RoutingPolicy) {
|
||||
|
||||
// Create a channel Alice->Bob.
|
||||
chanPoint := ht.OpenChannel(alice, bob, fs)
|
||||
defer ht.CloseChannel(alice, chanPoint)
|
||||
|
||||
// We add all the nodes' update channels to a slice, such that
|
||||
// we can make sure they all receive the expected updates.
|
||||
nodes := []*node.HarnessNode{alice, bob}
|
||||
|
||||
// Alice and Bob should see each other's ChannelUpdates,
|
||||
// advertising the preferred routing policies.
|
||||
assertNodesPolicyUpdate(
|
||||
ht, nodes, alice, alicePolicy, chanPoint,
|
||||
)
|
||||
assertNodesPolicyUpdate(ht, nodes, bob, bobPolicy, chanPoint)
|
||||
|
||||
// They should now know about the default policies.
|
||||
for _, node := range nodes {
|
||||
ht.AssertChannelPolicy(
|
||||
node, alice.PubKeyStr, alicePolicy, chanPoint,
|
||||
)
|
||||
ht.AssertChannelPolicy(
|
||||
node, bob.PubKeyStr, bobPolicy, chanPoint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for i, feeScenario := range feeScenarios {
|
||||
ht.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
st := ht.Subtest(t)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
runTestCase(
|
||||
st, feeScenario,
|
||||
&expectedPolicies[i], &bobExpectedPolicy,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testBasicChannelCreationAndUpdates tests multiple channel opening and
|
||||
// closing, and ensures that if a node is subscribed to channel updates they
|
||||
// will be received correctly for both cooperative and force closed channels.
|
||||
func testBasicChannelCreationAndUpdates(ht *lntemp.HarnessTest) {
|
||||
runBasicChannelCreationAndUpdates(ht, ht.Alice, ht.Bob)
|
||||
}
|
||||
|
||||
// runBasicChannelCreationAndUpdates tests multiple channel opening and closing,
|
||||
// and ensures that if a node is subscribed to channel updates they will be
|
||||
// received correctly for both cooperative and force closed channels.
|
||||
func runBasicChannelCreationAndUpdates(ht *lntemp.HarnessTest,
|
||||
alice, bob *node.HarnessNode) {
|
||||
|
||||
const (
|
||||
numChannels = 2
|
||||
amount = funding.MaxBtcFundingAmount
|
||||
)
|
||||
|
||||
// Subscribe Bob and Alice to channel event notifications.
|
||||
bobChanSub := bob.RPC.SubscribeChannelEvents()
|
||||
aliceChanSub := alice.RPC.SubscribeChannelEvents()
|
||||
|
||||
// Open the channels between Alice and Bob, asserting that the channels
|
||||
// have been properly opened on-chain.
|
||||
chanPoints := make([]*lnrpc.ChannelPoint, numChannels)
|
||||
for i := 0; i < numChannels; i++ {
|
||||
chanPoints[i] = ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: amount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Since each of the channels just became open, Bob and Alice should
|
||||
// each receive an open and an active notification for each channel.
|
||||
const numExpectedOpenUpdates = 3 * numChannels
|
||||
verifyOpenUpdatesReceived := func(sub rpc.ChannelEventsClient) error {
|
||||
for i := 0; i < numExpectedOpenUpdates; i++ {
|
||||
update := ht.ReceiveChannelEvent(sub)
|
||||
|
||||
switch update.Type {
|
||||
case lnrpc.ChannelEventUpdate_PENDING_OPEN_CHANNEL:
|
||||
if i%3 == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected open or active" +
|
||||
"channel ntfn, got pending open " +
|
||||
"channel ntfn instead")
|
||||
|
||||
case lnrpc.ChannelEventUpdate_OPEN_CHANNEL:
|
||||
if i%3 == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected pending open or " +
|
||||
"active channel ntfn, got open" +
|
||||
"channel ntfn instead")
|
||||
|
||||
case lnrpc.ChannelEventUpdate_ACTIVE_CHANNEL:
|
||||
if i%3 == 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("expected pending open or " +
|
||||
"open channel ntfn, got active " +
|
||||
"channel ntfn instead")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("update type mismatch: "+
|
||||
"expected open or active channel "+
|
||||
"notification, got: %v", update.Type)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
require.NoError(ht, verifyOpenUpdatesReceived(bobChanSub),
|
||||
"bob open channels")
|
||||
require.NoError(ht, verifyOpenUpdatesReceived(aliceChanSub),
|
||||
"alice open channels")
|
||||
|
||||
// Close the channels between Alice and Bob, asserting that the
|
||||
// channels have been properly closed on-chain.
|
||||
for i, chanPoint := range chanPoints {
|
||||
// Force close the first of the two channels.
|
||||
force := i%2 == 0
|
||||
if force {
|
||||
ht.ForceCloseChannel(alice, chanPoint)
|
||||
} else {
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyCloseUpdatesReceived is used to verify that Alice and Bob
|
||||
// receive the correct channel updates in order.
|
||||
const numExpectedCloseUpdates = 3 * numChannels
|
||||
verifyCloseUpdatesReceived := func(sub rpc.ChannelEventsClient,
|
||||
forceType lnrpc.ChannelCloseSummary_ClosureType,
|
||||
closeInitiator lnrpc.Initiator) error {
|
||||
|
||||
// Ensure one inactive and one closed notification is received
|
||||
// for each closed channel.
|
||||
for i := 0; i < numExpectedCloseUpdates; i++ {
|
||||
expectedCloseType := lnrpc.
|
||||
ChannelCloseSummary_COOPERATIVE_CLOSE
|
||||
|
||||
// Every other channel should be force closed. If this
|
||||
// channel was force closed, set the expected close type
|
||||
// to the type passed in.
|
||||
force := (i/3)%2 == 0
|
||||
if force {
|
||||
expectedCloseType = forceType
|
||||
}
|
||||
|
||||
chanUpdate := ht.ReceiveChannelEvent(sub)
|
||||
err := verifyCloseUpdate(
|
||||
chanUpdate, expectedCloseType,
|
||||
closeInitiator,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify Bob receives all closed channel notifications. He should
|
||||
// receive a remote force close notification for force closed channels.
|
||||
// All channels (cooperatively and force closed) should have a remote
|
||||
// close initiator because Alice closed the channels.
|
||||
require.NoError(
|
||||
ht, verifyCloseUpdatesReceived(
|
||||
bobChanSub,
|
||||
lnrpc.ChannelCloseSummary_REMOTE_FORCE_CLOSE,
|
||||
lnrpc.Initiator_INITIATOR_REMOTE,
|
||||
), "verifying bob close updates",
|
||||
)
|
||||
|
||||
// Verify Alice receives all closed channel notifications. She should
|
||||
// receive a remote force close notification for force closed channels.
|
||||
// All channels (cooperatively and force closed) should have a local
|
||||
// close initiator because Alice closed the channels.
|
||||
require.NoError(
|
||||
ht, verifyCloseUpdatesReceived(
|
||||
aliceChanSub,
|
||||
lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE,
|
||||
lnrpc.Initiator_INITIATOR_LOCAL,
|
||||
), "verifying alice close updates",
|
||||
)
|
||||
}
|
||||
|
||||
// assertMinerBlockHeightDelta ensures that tempMiner is 'delta' blocks ahead
|
||||
// of miner.
|
||||
func assertMinerBlockHeightDelta(ht *lntemp.HarnessTest,
|
||||
miner, tempMiner *lntemp.HarnessMiner, delta int32) {
|
||||
|
||||
// Ensure the chain lengths are what we expect.
|
||||
err := wait.NoError(func() error {
|
||||
_, tempMinerHeight, err := tempMiner.Client.GetBestBlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current "+
|
||||
"blockheight %v", err)
|
||||
}
|
||||
|
||||
_, minerHeight, err := miner.Client.GetBestBlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current "+
|
||||
"blockheight %v", err)
|
||||
}
|
||||
|
||||
if tempMinerHeight != minerHeight+delta {
|
||||
return fmt.Errorf("expected new miner(%d) to be %d "+
|
||||
"blocks ahead of original miner(%d)",
|
||||
tempMinerHeight, delta, minerHeight)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "failed to assert block height delta")
|
||||
}
|
||||
|
||||
// verifyCloseUpdate is used to verify that a closed channel update is of the
|
||||
// expected type.
|
||||
func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate,
|
||||
closeType lnrpc.ChannelCloseSummary_ClosureType,
|
||||
closeInitiator lnrpc.Initiator) error {
|
||||
|
||||
// We should receive one inactive and one closed notification
|
||||
// for each channel.
|
||||
switch update := chanUpdate.Channel.(type) {
|
||||
case *lnrpc.ChannelEventUpdate_InactiveChannel:
|
||||
if chanUpdate.Type !=
|
||||
lnrpc.ChannelEventUpdate_INACTIVE_CHANNEL {
|
||||
|
||||
return fmt.Errorf("update type mismatch: "+
|
||||
"expected %v, got %v",
|
||||
lnrpc.ChannelEventUpdate_INACTIVE_CHANNEL,
|
||||
chanUpdate.Type)
|
||||
}
|
||||
|
||||
case *lnrpc.ChannelEventUpdate_ClosedChannel:
|
||||
if chanUpdate.Type !=
|
||||
lnrpc.ChannelEventUpdate_CLOSED_CHANNEL {
|
||||
|
||||
return fmt.Errorf("update type mismatch: "+
|
||||
"expected %v, got %v",
|
||||
lnrpc.ChannelEventUpdate_CLOSED_CHANNEL,
|
||||
chanUpdate.Type)
|
||||
}
|
||||
|
||||
if update.ClosedChannel.CloseType != closeType {
|
||||
return fmt.Errorf("channel closure type "+
|
||||
"mismatch: expected %v, got %v",
|
||||
closeType,
|
||||
update.ClosedChannel.CloseType)
|
||||
}
|
||||
|
||||
if update.ClosedChannel.CloseInitiator != closeInitiator {
|
||||
return fmt.Errorf("expected close intiator: %v, "+
|
||||
"got: %v", closeInitiator,
|
||||
update.ClosedChannel.CloseInitiator)
|
||||
}
|
||||
|
||||
case *lnrpc.ChannelEventUpdate_FullyResolvedChannel:
|
||||
if chanUpdate.Type !=
|
||||
lnrpc.ChannelEventUpdate_FULLY_RESOLVED_CHANNEL {
|
||||
|
||||
return fmt.Errorf("update type mismatch: "+
|
||||
"expected %v, got %v",
|
||||
lnrpc.ChannelEventUpdate_FULLY_RESOLVED_CHANNEL,
|
||||
chanUpdate.Type)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("channel update channel of wrong type, "+
|
||||
"expected closed channel, got %T",
|
||||
update)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testListPayments(ht *lntemp.HarnessTest) {
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Check that there are no payments before test.
|
||||
ht.AssertNumPayments(alice, 0)
|
||||
|
||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanAmt := btcutil.Amount(100000)
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Get the number of invoices Bob already has.
|
||||
//
|
||||
// TODO(yy): we can remove this check once the `DeleteAllInvoices` rpc
|
||||
// is added.
|
||||
invResp := bob.RPC.ListInvoices(nil)
|
||||
numOldInvoices := len(invResp.Invoices)
|
||||
|
||||
// Now that the channel is open, create an invoice for Bob which
|
||||
// expects a payment of 1000 satoshis from Alice paid via a particular
|
||||
// preimage.
|
||||
const paymentAmt = 1000
|
||||
preimage := ht.Random32Bytes()
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
RPreimage: preimage,
|
||||
Value: paymentAmt,
|
||||
}
|
||||
invoiceResp := bob.RPC.AddInvoice(invoice)
|
||||
|
||||
// Check that Bob has added the invoice.
|
||||
numInvoices := numOldInvoices + 1
|
||||
ht.AssertNumInvoices(bob, 1)
|
||||
|
||||
// With the invoice for Bob added, send a payment towards Alice paying
|
||||
// to the above generated invoice.
|
||||
payReqs := []string{invoiceResp.PaymentRequest}
|
||||
ht.CompletePaymentRequests(alice, payReqs)
|
||||
|
||||
// Grab Alice's list of payments, she should show the existence of
|
||||
// exactly one payment.
|
||||
p := ht.AssertNumPayments(alice, 1)[0]
|
||||
path := p.Htlcs[len(p.Htlcs)-1].Route.Hops
|
||||
|
||||
// Ensure that the stored path shows a direct payment to Bob with no
|
||||
// other nodes in-between.
|
||||
require.Len(ht, path, 1, "wrong number of routes in path")
|
||||
require.Equal(ht, bob.PubKeyStr, path[0].PubKey, "wrong pub key")
|
||||
|
||||
// The payment amount should also match our previous payment directly.
|
||||
require.EqualValues(ht, paymentAmt, p.ValueSat, "incorrect sat amount")
|
||||
require.EqualValues(ht, paymentAmt*1000, p.ValueMsat,
|
||||
"incorrect msat amount")
|
||||
|
||||
// The payment hash (or r-hash) should have been stored correctly.
|
||||
correctRHash := hex.EncodeToString(invoiceResp.RHash)
|
||||
require.Equal(ht, correctRHash, p.PaymentHash, "incorrect RHash")
|
||||
|
||||
// As we made a single-hop direct payment, there should have been no
|
||||
// fee applied.
|
||||
require.Zero(ht, p.FeeSat, "fee should be 0")
|
||||
require.Zero(ht, p.FeeMsat, "fee should be 0")
|
||||
|
||||
// Now verify that the payment request returned by the rpc matches the
|
||||
// invoice that we paid.
|
||||
require.Equal(ht, invoiceResp.PaymentRequest, p.PaymentRequest,
|
||||
"incorrect payreq")
|
||||
|
||||
// We now check the timestamp filters in `ListPayments`.
|
||||
//
|
||||
// Use a start date long time ago should return us the payment.
|
||||
req := &lnrpc.ListPaymentsRequest{
|
||||
CreationDateStart: 1227035905,
|
||||
}
|
||||
resp := alice.RPC.ListPayments(req)
|
||||
require.Len(ht, resp.Payments, 1)
|
||||
|
||||
// Use an end date long time ago should return us nothing.
|
||||
req = &lnrpc.ListPaymentsRequest{
|
||||
CreationDateEnd: 1227035905,
|
||||
}
|
||||
resp = alice.RPC.ListPayments(req)
|
||||
require.Empty(ht, resp.Payments)
|
||||
|
||||
// Use a start date far in the future should return us nothing.
|
||||
req = &lnrpc.ListPaymentsRequest{
|
||||
CreationDateStart: 5392552705,
|
||||
}
|
||||
resp = alice.RPC.ListPayments(req)
|
||||
require.Empty(ht, resp.Payments)
|
||||
|
||||
// Use an end date far in the future should return us the payment.
|
||||
req = &lnrpc.ListPaymentsRequest{
|
||||
CreationDateEnd: 5392552705,
|
||||
}
|
||||
resp = alice.RPC.ListPayments(req)
|
||||
require.Len(ht, resp.Payments, 1)
|
||||
|
||||
// We now do the same check for `ListInvoices`
|
||||
//
|
||||
// Use a start date long time ago should return us the invoice.
|
||||
invReq := &lnrpc.ListInvoiceRequest{
|
||||
CreationDateStart: 1227035905,
|
||||
}
|
||||
invResp = bob.RPC.ListInvoices(invReq)
|
||||
require.Len(ht, invResp.Invoices, numInvoices)
|
||||
|
||||
// Use an end date long time ago should return us nothing.
|
||||
invReq = &lnrpc.ListInvoiceRequest{
|
||||
CreationDateEnd: 1227035905,
|
||||
}
|
||||
invResp = bob.RPC.ListInvoices(invReq)
|
||||
require.Empty(ht, invResp.Invoices)
|
||||
|
||||
// Use a start date far in the future should return us nothing.
|
||||
invReq = &lnrpc.ListInvoiceRequest{
|
||||
CreationDateStart: 5392552705,
|
||||
}
|
||||
invResp = bob.RPC.ListInvoices(invReq)
|
||||
require.Empty(ht, invResp.Invoices)
|
||||
|
||||
// Use an end date far in the future should return us the invoice.
|
||||
invReq = &lnrpc.ListInvoiceRequest{
|
||||
CreationDateEnd: 5392552705,
|
||||
}
|
||||
invResp = bob.RPC.ListInvoices(invReq)
|
||||
require.Len(ht, invResp.Invoices, numInvoices)
|
||||
|
||||
// Delete all payments from Alice. DB should have no payments.
|
||||
alice.RPC.DeleteAllPayments()
|
||||
|
||||
// Check that there are no payments after test.
|
||||
ht.AssertNumPayments(alice, 0)
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Close the channel.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testPaymentFollowingChannelOpen tests that the channel transition from
|
||||
// 'pending' to 'open' state does not cause any inconsistencies within other
|
||||
// subsystems trying to update the channel state in the db. We follow this
|
||||
// transition with a payment that updates the commitment state and verify that
|
||||
// the pending state is up to date.
|
||||
func testPaymentFollowingChannelOpen(ht *lntemp.HarnessTest) {
|
||||
const paymentAmt = btcutil.Amount(100)
|
||||
channelCapacity := paymentAmt * 1000
|
||||
|
||||
// We first establish a channel between Alice and Bob.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
p := lntemp.OpenChannelParams{
|
||||
Amt: channelCapacity,
|
||||
}
|
||||
pendingUpdate := ht.OpenChannelAssertPending(alice, bob, p)
|
||||
|
||||
// At this point, the channel's funding transaction will have been
|
||||
// broadcast, but not confirmed. Alice and Bob's nodes
|
||||
// should reflect this when queried via RPC.
|
||||
ht.AssertNodesNumPendingOpenChannels(alice, bob, 1)
|
||||
|
||||
// We are restarting Bob's node to let the link be created for the
|
||||
// pending channel.
|
||||
ht.RestartNode(bob)
|
||||
|
||||
// We ensure that Bob reconnects to Alice.
|
||||
ht.EnsureConnected(bob, alice)
|
||||
|
||||
// We mine six blocks for the channel to be confirmed.
|
||||
ht.MineBlocksAndAssertNumTxes(6, 1)
|
||||
|
||||
// We verify that the channel is open from both nodes point of view.
|
||||
chanPoint := lntemp.ChanPointFromPendingUpdate(pendingUpdate)
|
||||
ht.AssertNodesNumPendingOpenChannels(alice, bob, 0)
|
||||
ht.AssertChannelExists(alice, chanPoint)
|
||||
ht.AssertChannelExists(bob, chanPoint)
|
||||
|
||||
// With the channel open, we'll create invoices for Bob that Alice will
|
||||
// pay to in order to advance the state of the channel.
|
||||
bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, 1)
|
||||
|
||||
// Send payment to Bob so that a channel update to disk will be
|
||||
// executed.
|
||||
ht.CompletePaymentRequests(alice, []string{bobPayReqs[0]})
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Finally, immediately close the channel. This function will also
|
||||
// block until the channel is closed and will additionally assert the
|
||||
// relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testAsyncPayments tests the performance of the async payments.
|
||||
func testAsyncPayments(ht *lntemp.HarnessTest) {
|
||||
// We use new nodes here as the benchmark test creates lots of data
|
||||
// which can be costly to be carried on.
|
||||
alice := ht.NewNode("Alice", []string{"--pending-commit-interval=3m"})
|
||||
bob := ht.NewNode("Bob", []string{"--pending-commit-interval=3m"})
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
|
||||
runAsyncPayments(ht, alice, bob)
|
||||
}
|
||||
|
||||
// runAsyncPayments tests the performance of the async payments.
|
||||
func runAsyncPayments(ht *lntemp.HarnessTest, alice, bob *node.HarnessNode) {
|
||||
const paymentAmt = 100
|
||||
|
||||
// First establish a channel with a capacity equals to the overall
|
||||
// amount of payments, between Alice and Bob, at the end of the test
|
||||
// Alice should send all money from her side to Bob.
|
||||
channelCapacity := btcutil.Amount(paymentAmt * 2000)
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: channelCapacity},
|
||||
)
|
||||
|
||||
info := ht.QueryChannelByChanPoint(alice, chanPoint)
|
||||
|
||||
// We'll create a number of invoices equal the max number of HTLCs that
|
||||
// can be carried in one direction. The number on the commitment will
|
||||
// likely be lower, but we can't guarantee that any more HTLCs will
|
||||
// succeed due to the limited path diversity and inability of the router
|
||||
// to retry via another path.
|
||||
numInvoices := int(input.MaxHTLCNumber / 2)
|
||||
|
||||
bobAmt := int64(numInvoices * paymentAmt)
|
||||
aliceAmt := info.LocalBalance - bobAmt
|
||||
|
||||
// With the channel open, we'll create invoices for Bob that Alice
|
||||
// will pay to in order to advance the state of the channel.
|
||||
bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numInvoices)
|
||||
|
||||
// Simultaneously send payments from Alice to Bob using of Bob's
|
||||
// payment hashes generated above.
|
||||
now := time.Now()
|
||||
|
||||
settled := make(chan struct{})
|
||||
defer close(settled)
|
||||
|
||||
timeout := lntest.AsyncBenchmarkTimeout * 2
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
payReq := bobPayReqs[i]
|
||||
go func() {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
TimeoutSeconds: int32(timeout.Seconds()),
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
// AssertPaymentStatusWithTimeout will assert that the
|
||||
// payment is settled.
|
||||
stream := alice.RPC.SendPayment(req)
|
||||
ht.AssertPaymentSucceedWithTimeout(stream, timeout)
|
||||
|
||||
settled <- struct{}{}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait until all the payments have settled.
|
||||
timer := time.After(timeout)
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
select {
|
||||
case <-settled:
|
||||
case <-timer:
|
||||
require.Fail(ht, "timeout", "wait payment failed")
|
||||
}
|
||||
}
|
||||
|
||||
// All payments have been sent, mark the finish time.
|
||||
timeTaken := time.Since(now)
|
||||
|
||||
// Wait for the revocation to be received so alice no longer has
|
||||
// pending htlcs listed and has correct balances. This is needed due to
|
||||
// the fact that we now pipeline the settles.
|
||||
assertChannelState(ht, alice, chanPoint, aliceAmt, bobAmt)
|
||||
|
||||
// Wait for Bob to receive revocation from Alice.
|
||||
assertChannelState(ht, bob, chanPoint, bobAmt, aliceAmt)
|
||||
|
||||
ht.Log("\tBenchmark info: Elapsed time: ", timeTaken)
|
||||
ht.Log("\tBenchmark info: TPS: ",
|
||||
float64(numInvoices)/timeTaken.Seconds())
|
||||
|
||||
// Finally, immediately close the channel. This function will also
|
||||
// block until the channel is closed and will additionally assert the
|
||||
// relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// testBidirectionalAsyncPayments tests that nodes are able to send the
|
||||
// payments to each other in async manner without blocking.
|
||||
func testBidirectionalAsyncPayments(ht *lntemp.HarnessTest) {
|
||||
const paymentAmt = 1000
|
||||
|
||||
// We use new nodes here as the benchmark test creates lots of data
|
||||
// which can be costly to be carried on.
|
||||
alice := ht.NewNode("Alice", []string{"--pending-commit-interval=3m"})
|
||||
bob := ht.NewNode("Bob", []string{"--pending-commit-interval=3m"})
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
|
||||
// First establish a channel with a capacity equals to the overall
|
||||
// amount of payments, between Alice and Bob, at the end of the test
|
||||
// Alice should send all money from her side to Bob.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: paymentAmt * 2000,
|
||||
PushAmt: paymentAmt * 1000,
|
||||
},
|
||||
)
|
||||
|
||||
info := ht.QueryChannelByChanPoint(alice, chanPoint)
|
||||
|
||||
// We'll create a number of invoices equal the max number of HTLCs that
|
||||
// can be carried in one direction. The number on the commitment will
|
||||
// likely be lower, but we can't guarantee that any more HTLCs will
|
||||
// succeed due to the limited path diversity and inability of the router
|
||||
// to retry via another path.
|
||||
numInvoices := int(input.MaxHTLCNumber / 2)
|
||||
|
||||
// Nodes should exchange the same amount of money and because of this
|
||||
// at the end balances should remain the same.
|
||||
aliceAmt := info.LocalBalance
|
||||
bobAmt := info.RemoteBalance
|
||||
|
||||
// With the channel open, we'll create invoices for Bob that Alice
|
||||
// will pay to in order to advance the state of the channel.
|
||||
bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numInvoices)
|
||||
|
||||
// With the channel open, we'll create invoices for Alice that Bob
|
||||
// will pay to in order to advance the state of the channel.
|
||||
alicePayReqs, _, _ := ht.CreatePayReqs(alice, paymentAmt, numInvoices)
|
||||
|
||||
// Reset mission control to prevent previous payment results from
|
||||
// interfering with this test. A new channel has been opened, but
|
||||
// mission control operates on node pairs.
|
||||
alice.RPC.ResetMissionControl()
|
||||
|
||||
// Send payments from Alice to Bob and from Bob to Alice in async
|
||||
// manner.
|
||||
settled := make(chan struct{})
|
||||
defer close(settled)
|
||||
|
||||
timeout := lntest.AsyncBenchmarkTimeout * 4
|
||||
send := func(node *node.HarnessNode, payReq string) {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
TimeoutSeconds: int32(timeout.Seconds()),
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
// AssertPaymentStatusWithTimeout will assert that the
|
||||
// payment is settled.
|
||||
stream := node.RPC.SendPayment(req)
|
||||
ht.AssertPaymentSucceedWithTimeout(stream, timeout)
|
||||
|
||||
settled <- struct{}{}
|
||||
}
|
||||
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
go send(bob, alicePayReqs[i])
|
||||
go send(alice, bobPayReqs[i])
|
||||
}
|
||||
|
||||
// Expect all payments to succeed.
|
||||
timer := time.After(timeout)
|
||||
for i := 0; i < 2*numInvoices; i++ {
|
||||
select {
|
||||
case <-settled:
|
||||
case <-timer:
|
||||
require.Fail(ht, "timeout", "wait payment failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Alice and Bob to receive revocations messages, and update
|
||||
// states, i.e. balance info.
|
||||
assertChannelState(ht, alice, chanPoint, aliceAmt, bobAmt)
|
||||
|
||||
// Next query for Bob's and Alice's channel states, in order to confirm
|
||||
// that all payment have been successful transmitted.
|
||||
assertChannelState(ht, bob, chanPoint, bobAmt, aliceAmt)
|
||||
|
||||
// Finally, immediately close the channel. This function will also
|
||||
// block until the channel is closed and will additionally assert the
|
||||
// relevant channel closing post conditions.
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
func testInvoiceSubscriptions(ht *lntemp.HarnessTest) {
|
||||
const chanAmt = btcutil.Amount(500000)
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Create a new invoice subscription client for Bob, the notification
|
||||
// should be dispatched shortly below.
|
||||
req := &lnrpc.InvoiceSubscription{}
|
||||
bobInvoiceSubscription := bob.RPC.SubscribeInvoices(req)
|
||||
|
||||
// Open a channel with 500k satoshis between Alice and Bob with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanPoint := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Next create a new invoice for Bob requesting 1k satoshis.
|
||||
const paymentAmt = 1000
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
RPreimage: ht.Random32Bytes(),
|
||||
Value: paymentAmt,
|
||||
}
|
||||
invoiceResp := bob.RPC.AddInvoice(invoice)
|
||||
lastAddIndex := invoiceResp.AddIndex
|
||||
|
||||
// With the above invoice added, we should receive an update event.
|
||||
invoiceUpdate := ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
require.NotEqual(ht, lnrpc.Invoice_SETTLED, invoiceUpdate.State,
|
||||
"invoice should not be settled")
|
||||
|
||||
// With the assertion above set up, send a payment from Alice to Bob
|
||||
// which should finalize and settle the invoice.
|
||||
ht.CompletePaymentRequests(alice, []string{invoiceResp.PaymentRequest})
|
||||
|
||||
// The invoice update should exactly match the invoice created
|
||||
// above, but should now be settled and have SettleDate
|
||||
invoiceUpdate = ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, invoiceUpdate.State,
|
||||
"invoice not settled but should be")
|
||||
require.NotZero(ht, invoiceUpdate.SettleDate,
|
||||
"invoice should have non zero settle date, but doesn't")
|
||||
require.Equal(ht, invoice.RPreimage, invoiceUpdate.RPreimage,
|
||||
"payment preimages don't match")
|
||||
require.NotZero(ht, invoiceUpdate.SettleIndex,
|
||||
"invoice should have settle index")
|
||||
settleIndex := invoiceUpdate.SettleIndex
|
||||
|
||||
// We'll now add 3 more invoices to Bob's invoice registry.
|
||||
const numInvoices = 3
|
||||
payReqs, _, newInvoices := ht.CreatePayReqs(
|
||||
bob, paymentAmt, numInvoices,
|
||||
)
|
||||
|
||||
// Now that the set of invoices has been added, we'll re-register for
|
||||
// streaming invoice notifications for Bob, this time specifying the
|
||||
// add invoice of the last prior invoice.
|
||||
req = &lnrpc.InvoiceSubscription{AddIndex: lastAddIndex}
|
||||
bobInvoiceSubscription = bob.RPC.SubscribeInvoices(req)
|
||||
|
||||
// Since we specified a value of the prior add index above, we should
|
||||
// now immediately get the invoices we just added as we should get the
|
||||
// backlog of notifications.
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
invoiceUpdate := ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
|
||||
// We should now get the ith invoice we added, as they should
|
||||
// be returned in order.
|
||||
require.NotEqual(ht, lnrpc.Invoice_SETTLED, invoiceUpdate.State,
|
||||
"should have only received add events")
|
||||
|
||||
originalInvoice := newInvoices[i]
|
||||
rHash := sha256.Sum256(originalInvoice.RPreimage)
|
||||
require.Equal(ht, rHash[:], invoiceUpdate.RHash,
|
||||
"invoices have mismatched payment hashes")
|
||||
}
|
||||
|
||||
// We'll now have Bob settle out the remainder of these invoices so we
|
||||
// can test that all settled invoices are properly notified.
|
||||
ht.CompletePaymentRequests(alice, payReqs)
|
||||
|
||||
// With the set of invoices paid, we'll now cancel the old
|
||||
// subscription, and create a new one for Bob, this time using the
|
||||
// settle index to obtain the backlog of settled invoices.
|
||||
req = &lnrpc.InvoiceSubscription{
|
||||
SettleIndex: settleIndex,
|
||||
}
|
||||
bobInvoiceSubscription = bob.RPC.SubscribeInvoices(req)
|
||||
|
||||
// As we specified the index of the past settle index, we should now
|
||||
// receive notifications for the three HTLCs that we just settled. As
|
||||
// the order that the HTLCs will be settled in is partially randomized,
|
||||
// we'll use a map to assert that the proper set has been settled.
|
||||
settledInvoices := make(map[[32]byte]struct{})
|
||||
for _, invoice := range newInvoices {
|
||||
rHash := sha256.Sum256(invoice.RPreimage)
|
||||
settledInvoices[rHash] = struct{}{}
|
||||
}
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
invoiceUpdate := ht.ReceiveInvoiceUpdate(bobInvoiceSubscription)
|
||||
|
||||
// We should now get the ith invoice we added, as they should
|
||||
// be returned in order.
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, invoiceUpdate.State,
|
||||
"should have only received settle events")
|
||||
|
||||
var rHash [32]byte
|
||||
copy(rHash[:], invoiceUpdate.RHash)
|
||||
require.Contains(ht, settledInvoices, rHash,
|
||||
"unknown invoice settled")
|
||||
|
||||
delete(settledInvoices, rHash)
|
||||
}
|
||||
|
||||
// At this point, all the invoices should be fully settled.
|
||||
require.Empty(ht, settledInvoices, "not all invoices settled")
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
ht.CloseChannel(alice, chanPoint)
|
||||
}
|
||||
|
||||
// assertChannelState asserts the channel state by checking the values in
|
||||
// fields, LocalBalance, RemoteBalance and num of PendingHtlcs.
|
||||
func assertChannelState(ht *lntemp.HarnessTest, hn *node.HarnessNode,
|
||||
cp *lnrpc.ChannelPoint, localBalance, remoteBalance int64) {
|
||||
|
||||
// Get the funding point.
|
||||
err := wait.NoError(func() error {
|
||||
// Find the target channel first.
|
||||
target := ht.GetChannelByChanPoint(hn, cp)
|
||||
|
||||
if len(target.PendingHtlcs) != 0 {
|
||||
return fmt.Errorf("pending htlcs is "+
|
||||
"incorrect, got %v, expected %v",
|
||||
len(target.PendingHtlcs), 0)
|
||||
}
|
||||
|
||||
if target.LocalBalance != localBalance {
|
||||
return fmt.Errorf("local balance is "+
|
||||
"incorrect, got %v, expected %v",
|
||||
target.LocalBalance, localBalance)
|
||||
}
|
||||
|
||||
if target.RemoteBalance != remoteBalance {
|
||||
return fmt.Errorf("remote balance is "+
|
||||
"incorrect, got %v, expected %v",
|
||||
target.RemoteBalance, remoteBalance)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, lntemp.DefaultTimeout)
|
||||
require.NoError(ht, err, "timeout while chekcing for balance")
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,443 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/aezeed"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testGetRecoveryInfo checks whether lnd gives the right information about
|
||||
// the wallet recovery process.
|
||||
func testGetRecoveryInfo(ht *lntemp.HarnessTest) {
|
||||
// First, create a new node with strong passphrase and grab the mnemonic
|
||||
// used for key derivation. This will bring up Carol with an empty
|
||||
// wallet, and such that she is synced up.
|
||||
password := []byte("The Magic Words are Squeamish Ossifrage")
|
||||
carol, mnemonic, _ := ht.NewNodeWithSeed("Carol", nil, password, false)
|
||||
|
||||
checkInfo := func(expectedRecoveryMode, expectedRecoveryFinished bool,
|
||||
expectedProgress float64, recoveryWindow int32) {
|
||||
|
||||
// Restore Carol, passing in the password, mnemonic, and
|
||||
// desired recovery window.
|
||||
node := ht.RestoreNodeWithSeed(
|
||||
carol.Name(), nil, password, mnemonic, "",
|
||||
recoveryWindow, nil,
|
||||
)
|
||||
|
||||
// Query carol for her current wallet recovery progress.
|
||||
err := wait.NoError(func() error {
|
||||
// Verify that recovery info gives the right response.
|
||||
resp := node.RPC.GetRecoveryInfo(nil)
|
||||
|
||||
mode := resp.RecoveryMode
|
||||
finished := resp.RecoveryFinished
|
||||
progress := resp.Progress
|
||||
|
||||
if mode != expectedRecoveryMode {
|
||||
return fmt.Errorf("expected recovery mode %v "+
|
||||
"got %v", expectedRecoveryMode, mode)
|
||||
}
|
||||
if finished != expectedRecoveryFinished {
|
||||
return fmt.Errorf("expected finished %v "+
|
||||
"got %v", expectedRecoveryFinished,
|
||||
finished)
|
||||
}
|
||||
if progress != expectedProgress {
|
||||
return fmt.Errorf("expected progress %v"+
|
||||
"got %v", expectedProgress, progress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Lastly, shutdown this Carol so we can move on to the next
|
||||
// restoration.
|
||||
ht.Shutdown(node)
|
||||
}
|
||||
|
||||
// Restore Carol with a recovery window of 0. Since it's not in recovery
|
||||
// mode, the recovery info will give a response with recoveryMode=false,
|
||||
// recoveryFinished=false, and progress=0
|
||||
checkInfo(false, false, 0, 0)
|
||||
|
||||
// Change the recovery windown to be 1 to turn on recovery mode. Since
|
||||
// the current chain height is the same as the birthday height, it
|
||||
// should indicate the recovery process is finished.
|
||||
checkInfo(true, true, 1, 1)
|
||||
|
||||
// We now go ahead 5 blocks. Because the wallet's syncing process is
|
||||
// controlled by a goroutine in the background, it will catch up
|
||||
// quickly. This makes the recovery progress back to 1.
|
||||
ht.MineBlocks(5)
|
||||
checkInfo(true, true, 1, 1)
|
||||
}
|
||||
|
||||
// testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs
|
||||
// when providing a valid aezeed that owns outputs on the chain. This test
|
||||
// performs multiple restorations using the same seed and various recovery
|
||||
// windows to ensure we detect funds properly.
|
||||
func testOnchainFundRecovery(ht *lntemp.HarnessTest) {
|
||||
// First, create a new node with strong passphrase and grab the mnemonic
|
||||
// used for key derivation. This will bring up Carol with an empty
|
||||
// wallet, and such that she is synced up.
|
||||
password := []byte("The Magic Words are Squeamish Ossifrage")
|
||||
carol, mnemonic, _ := ht.NewNodeWithSeed("Carol", nil, password, false)
|
||||
|
||||
// As long as the mnemonic is non-nil and the extended key is empty, the
|
||||
// closure below will always restore the node from the seed. The tests
|
||||
// need to manually overwrite this value to change that behavior.
|
||||
rootKey := ""
|
||||
|
||||
// Create a closure for testing the recovery of Carol's wallet. This
|
||||
// method takes the expected value of Carol's balance when using the
|
||||
// given recovery window. Additionally, the caller can specify an action
|
||||
// to perform on the restored node before the node is shutdown.
|
||||
restoreCheckBalance := func(expAmount int64, expectedNumUTXOs uint32,
|
||||
recoveryWindow int32, fn func(*node.HarnessNode)) {
|
||||
|
||||
ht.Helper()
|
||||
|
||||
// Restore Carol, passing in the password, mnemonic, and
|
||||
// desired recovery window.
|
||||
node := ht.RestoreNodeWithSeed(
|
||||
carol.Name(), nil, password, mnemonic, rootKey,
|
||||
recoveryWindow, nil,
|
||||
)
|
||||
|
||||
// Query carol for her current wallet balance, and also that we
|
||||
// gain the expected number of UTXOs.
|
||||
var (
|
||||
currBalance int64
|
||||
currNumUTXOs uint32
|
||||
)
|
||||
err := wait.NoError(func() error {
|
||||
resp := node.RPC.WalletBalance()
|
||||
currBalance = resp.ConfirmedBalance
|
||||
|
||||
req := &walletrpc.ListUnspentRequest{
|
||||
Account: "",
|
||||
MaxConfs: math.MaxInt32,
|
||||
MinConfs: 0,
|
||||
}
|
||||
utxoResp := node.RPC.ListUnspent(req)
|
||||
currNumUTXOs = uint32(len(utxoResp.Utxos))
|
||||
|
||||
// Verify that Carol's balance and number of UTXOs
|
||||
// matches what's expected.
|
||||
if expAmount != currBalance {
|
||||
return fmt.Errorf("balance not matched, want "+
|
||||
"%d, got %d", expAmount, currBalance)
|
||||
}
|
||||
if currNumUTXOs != expectedNumUTXOs {
|
||||
return fmt.Errorf("num of UTXOs not matched, "+
|
||||
"want %d, got %d", expectedNumUTXOs,
|
||||
currNumUTXOs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking Carol")
|
||||
|
||||
// If the user provided a callback, execute the commands against
|
||||
// the restored Carol.
|
||||
if fn != nil {
|
||||
fn(node)
|
||||
}
|
||||
|
||||
// Lastly, shutdown this Carol so we can move on to the next
|
||||
// restoration.
|
||||
ht.Shutdown(node)
|
||||
}
|
||||
|
||||
// Create a closure-factory for building closures that can generate and
|
||||
// skip a configurable number of addresses, before finally sending coins
|
||||
// to a next generated address. The returned closure will apply the same
|
||||
// behavior to both default P2WKH and NP2WKH scopes.
|
||||
skipAndSend := func(nskip int) func(*node.HarnessNode) {
|
||||
return func(node *node.HarnessNode) {
|
||||
ht.Helper()
|
||||
|
||||
// Generate and skip the number of addresses requested.
|
||||
for i := 0; i < nskip; i++ {
|
||||
req := &lnrpc.NewAddressRequest{}
|
||||
|
||||
req.Type = AddrTypeWitnessPubkeyHash
|
||||
node.RPC.NewAddress(req)
|
||||
|
||||
req.Type = AddrTypeNestedPubkeyHash
|
||||
node.RPC.NewAddress(req)
|
||||
|
||||
req.Type = AddrTypeTaprootPubkey
|
||||
node.RPC.NewAddress(req)
|
||||
}
|
||||
|
||||
// Send one BTC to the next P2WKH address.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, node)
|
||||
|
||||
// And another to the next NP2WKH address.
|
||||
ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, node)
|
||||
|
||||
// Add another whole coin to the P2TR address.
|
||||
ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, node)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Carol with a recovery window of 0. Since no coins have been
|
||||
// sent, her balance should be zero.
|
||||
//
|
||||
// After, one BTC is sent to both her first external P2WKH and NP2WKH
|
||||
// addresses.
|
||||
restoreCheckBalance(0, 0, 0, skipAndSend(0))
|
||||
|
||||
// Check that restoring without a look-ahead results in having no funds
|
||||
// in the wallet, even though they exist on-chain.
|
||||
restoreCheckBalance(0, 0, 0, nil)
|
||||
|
||||
// Now, check that using a look-ahead of 1 recovers the balance from
|
||||
// the two transactions above. We should also now have 2 UTXOs in the
|
||||
// wallet at the end of the recovery attempt.
|
||||
//
|
||||
// After, we will generate and skip 9 P2WKH, NP2WKH and P2TR addresses,
|
||||
// and send another BTC to the subsequent 10th address in each
|
||||
// derivation path.
|
||||
restoreCheckBalance(3*btcutil.SatoshiPerBitcoin, 3, 1, skipAndSend(9))
|
||||
|
||||
// Check that using a recovery window of 9 does not find the two most
|
||||
// recent txns.
|
||||
restoreCheckBalance(3*btcutil.SatoshiPerBitcoin, 3, 9, nil)
|
||||
|
||||
// Extending our recovery window to 10 should find the most recent
|
||||
// transactions, leaving the wallet with 6 BTC total. We should also
|
||||
// learn of the two additional UTXOs created above.
|
||||
//
|
||||
// After, we will skip 19 more addrs, sending to the 20th address past
|
||||
// our last found address, and repeat the same checks.
|
||||
restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 10, skipAndSend(19))
|
||||
|
||||
// Check that recovering with a recovery window of 19 fails to find the
|
||||
// most recent transactions.
|
||||
restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 19, nil)
|
||||
|
||||
// Ensure that using a recovery window of 20 succeeds with all UTXOs
|
||||
// found and the final balance reflected.
|
||||
|
||||
// After these checks are done, we'll want to make sure we can also
|
||||
// recover change address outputs. This is mainly motivated by a now
|
||||
// fixed bug in the wallet in which change addresses could at times be
|
||||
// created outside of the default key scopes. Recovery only used to be
|
||||
// performed on the default key scopes, so ideally this test case
|
||||
// would've caught the bug earlier. Carol has received 9 BTC so far from
|
||||
// the miner, we'll send 8 back to ensure all of her UTXOs get spent to
|
||||
// avoid fee discrepancies and a change output is formed.
|
||||
const minerAmt = 8 * btcutil.SatoshiPerBitcoin
|
||||
const finalBalance = 9 * btcutil.SatoshiPerBitcoin
|
||||
promptChangeAddr := func(node *node.HarnessNode) {
|
||||
ht.Helper()
|
||||
|
||||
minerAddr := ht.Miner.NewMinerAddress()
|
||||
req := &lnrpc.SendCoinsRequest{
|
||||
Addr: minerAddr.String(),
|
||||
Amount: minerAmt,
|
||||
}
|
||||
resp := node.RPC.SendCoins(req)
|
||||
|
||||
txid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
require.Equal(ht, txid.String(), resp.Txid)
|
||||
|
||||
block := ht.MineBlocks(1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, txid)
|
||||
}
|
||||
restoreCheckBalance(finalBalance, 9, 20, promptChangeAddr)
|
||||
|
||||
// We should expect a static fee of 36400 satoshis for spending 9
|
||||
// inputs (3 P2WPKH, 3 NP2WPKH, 3 P2TR) to two P2TR outputs. Carol
|
||||
// should therefore only have one UTXO present (the change output) of
|
||||
// 9 - 8 - fee BTC.
|
||||
const fee = 37000
|
||||
restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
|
||||
|
||||
// Last of all, make sure we can also restore a node from the extended
|
||||
// master root key directly instead of the seed.
|
||||
var seedMnemonic aezeed.Mnemonic
|
||||
copy(seedMnemonic[:], mnemonic)
|
||||
cipherSeed, err := seedMnemonic.ToCipherSeed(password)
|
||||
require.NoError(ht, err)
|
||||
extendedRootKey, err := hdkeychain.NewMaster(
|
||||
cipherSeed.Entropy[:], harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
rootKey = extendedRootKey.String()
|
||||
mnemonic = nil
|
||||
|
||||
restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
|
||||
}
|
||||
|
||||
// testRescanAddressDetection makes sure that addresses created from internal
|
||||
// (m/1017' scope) keys aren't detected as UTXOs when re-scanning the wallet
|
||||
// with --reset-wallet-transactions to avoid showing them as un-spent ghost
|
||||
// UTXOs even if they are being spent. This is to test a fix in the wallet that
|
||||
// addresses the following scenario:
|
||||
// 1. A key is derived from the internal 1017' scope with a custom key family
|
||||
// and a p2wkh address is derived from that key.
|
||||
// 2. Funds are sent to the address created above in a way that also creates a
|
||||
// change output. The change output is recognized as belonging to the
|
||||
// wallet, which is correct.
|
||||
// 3. The funds on the address created in step 1 are fully spent (without
|
||||
// creating a change output) into an output that doesn't belong to the
|
||||
// wallet (e.g. a channel funding output).
|
||||
// 4. At some point the user re-scans their wallet by using the
|
||||
// --reset-wallet-transactions flag.
|
||||
// 5. The wallet re-scan detects the change output created in step 2 and flags
|
||||
// the transaction as relevant.
|
||||
// 6. While adding the relevant TX to the wallet DB, the wallet also detects
|
||||
// the address from step 1 as belonging to the wallet (because the internal
|
||||
// key scope is defined as having the address type p2wkh) and adds that
|
||||
// output as an UTXO as well (<- this is the bug). The wallet now has two
|
||||
// UTXOs in its database.
|
||||
// 7. The transaction that spends the UTXO of the address from step 1 is not
|
||||
// detected by the wallet as belonging to it (because the output is a
|
||||
// channel output and the input (correctly) isn't recognized as belonging to
|
||||
// the wallet in that part of the code, it is never marked as spent and
|
||||
// stays in the wallet as a ghost UTXO forever.
|
||||
//
|
||||
// The fix in the wallet is simple: In step 6, don't detect addresses from
|
||||
// internal scopes while re-scanning to be in line with the logic in other areas
|
||||
// of the wallet code.
|
||||
func testRescanAddressDetection(ht *lntemp.HarnessTest) {
|
||||
// We start off by creating a new node with the wallet re-scan flag
|
||||
// enabled. This won't have any effect on the first startup but will
|
||||
// come into effect after we re-start the node.
|
||||
walletPassword := []byte("some-password")
|
||||
carol, _, _ := ht.NewNodeWithSeed(
|
||||
"carol", []string{"--reset-wallet-transactions"},
|
||||
walletPassword, false,
|
||||
)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// Create an address generated from internal keys.
|
||||
keyDesc := carol.RPC.DeriveNextKey(&walletrpc.KeyReq{KeyFamily: 123})
|
||||
pubKeyHash := btcutil.Hash160(keyDesc.RawKeyBytes)
|
||||
ghostUtxoAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
pubKeyHash, harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Send funds to the (p2wkh!) address generated from the internal
|
||||
// (m/1017') key scope. Because the internal key scope is defined as
|
||||
// p2wkh address type, this might be incorrectly detected by the wallet
|
||||
// in some situations (which this test makes sure is fixed).
|
||||
const ghostUtxoAmount = 456_000
|
||||
carol.RPC.SendCoins(&lnrpc.SendCoinsRequest{
|
||||
Addr: ghostUtxoAddr.String(),
|
||||
Amount: ghostUtxoAmount,
|
||||
SatPerVbyte: 1,
|
||||
})
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Make sure we see the change output in our list of unspent outputs.
|
||||
// We _don't_ expect to see the ghost UTXO here as in this step it's
|
||||
// ignored as an internal address correctly.
|
||||
ht.AssertNumUTXOsConfirmed(carol, 1)
|
||||
unspent := carol.RPC.ListUnspent(&walletrpc.ListUnspentRequest{
|
||||
MinConfs: 1,
|
||||
})
|
||||
|
||||
// Which one was the change output and which one the ghost UTXO output?
|
||||
var ghostUtxoIndex uint32
|
||||
if unspent.Utxos[0].Outpoint.OutputIndex == 0 {
|
||||
ghostUtxoIndex = 1
|
||||
}
|
||||
|
||||
ghostUtxoHash, err := chainhash.NewHash(
|
||||
unspent.Utxos[0].Outpoint.TxidBytes,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
burnScript, _ := ht.CreateBurnAddr(AddrTypeWitnessPubkeyHash)
|
||||
|
||||
// Create fee estimation for a p2wkh input and p2wkh output.
|
||||
feeRate := chainfee.SatPerKWeight(12500)
|
||||
estimator := input.TxWeightEstimator{}
|
||||
estimator.AddP2WKHInput()
|
||||
estimator.AddP2WKHOutput()
|
||||
estimatedWeight := int64(estimator.Weight())
|
||||
requiredFee := feeRate.FeeForWeight(estimatedWeight)
|
||||
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.TxIn = []*wire.TxIn{{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *ghostUtxoHash,
|
||||
Index: ghostUtxoIndex,
|
||||
},
|
||||
}}
|
||||
value := int64(ghostUtxoAmount - requiredFee)
|
||||
tx.TxOut = []*wire.TxOut{{
|
||||
PkScript: burnScript,
|
||||
Value: value,
|
||||
}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(ht, tx.Serialize(&buf))
|
||||
|
||||
ghostUtxoScript := ht.PayToAddrScript(ghostUtxoAddr)
|
||||
utxoInfo := []*signrpc.TxOut{{
|
||||
PkScript: ghostUtxoScript,
|
||||
Value: ghostUtxoAmount,
|
||||
}}
|
||||
|
||||
// Let's sign the input now.
|
||||
signResp := carol.RPC.SignOutputRaw(&signrpc.SignReq{
|
||||
RawTxBytes: buf.Bytes(),
|
||||
SignDescs: []*signrpc.SignDescriptor{{
|
||||
Output: utxoInfo[0],
|
||||
InputIndex: 0,
|
||||
KeyDesc: keyDesc,
|
||||
Sighash: uint32(txscript.SigHashAll),
|
||||
WitnessScript: utxoInfo[0].PkScript,
|
||||
}},
|
||||
})
|
||||
|
||||
// Add the witness to the input and publish the tx.
|
||||
tx.TxIn[0].Witness = wire.TxWitness{
|
||||
append(signResp.RawSigs[0], byte(txscript.SigHashAll)),
|
||||
keyDesc.RawKeyBytes,
|
||||
}
|
||||
buf.Reset()
|
||||
require.NoError(ht, tx.Serialize(&buf))
|
||||
carol.RPC.PublishTransaction(&walletrpc.Transaction{
|
||||
TxHex: buf.Bytes(),
|
||||
})
|
||||
|
||||
// Wait until the spending tx is found and mine a block to confirm it.
|
||||
ht.Miner.AssertNumTxsInMempool(1)
|
||||
ht.MineBlocks(1)
|
||||
|
||||
// The wallet should still just see a single UTXO of the change output
|
||||
// created earlier.
|
||||
ht.AssertNumUTXOsConfirmed(carol, 1)
|
||||
|
||||
// Let's now re-start the node, causing it to do the wallet re-scan.
|
||||
ht.RestartNode(carol)
|
||||
|
||||
// There should now still only be a single UTXO from the change output
|
||||
// instead of two (the ghost UTXO should be missing if the fix works).
|
||||
ht.AssertNumUTXOsConfirmed(carol, 1)
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
rootKey = "tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3" +
|
||||
"PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg"
|
||||
|
||||
nodePubKey = "033f55d436d4f7d24aeffb1b976647380f22ebf9e74390e8c76dcff" +
|
||||
"9fea0093b7a"
|
||||
|
||||
accounts = []*lnrpc.WatchOnlyAccount{{
|
||||
Purpose: waddrmgr.KeyScopeBIP0049Plus.Purpose,
|
||||
// We always use the mainnet coin type for our BIP49/84/86
|
||||
// addresses!
|
||||
CoinType: 0,
|
||||
Account: 0,
|
||||
Xpub: "tpubDDXEYWvGCTytEF6hBog9p4qr2QBUvJhh4P2wM4qHHv9N489khk" +
|
||||
"QoGkBXDVoquuiyBf8SKBwrYseYdtq9j2v2nttPpE8qbuW3sE2MCk" +
|
||||
"FPhTq",
|
||||
}, {
|
||||
Purpose: waddrmgr.KeyScopeBIP0084.Purpose,
|
||||
// We always use the mainnet coin type for our BIP49/84/86
|
||||
// addresses!
|
||||
CoinType: 0,
|
||||
Account: 0,
|
||||
Xpub: "tpubDDWAWrSLRSFrG1KdqXMQQyTKYGSKLKaY7gxpvK7RdV3e3Dkhvu" +
|
||||
"W2GgsFvsPN4RGmuoYtUgZ1LHZE8oftz7T4mzc1BxGt5rt8zJcVQi" +
|
||||
"KTPPV",
|
||||
}, {
|
||||
Purpose: waddrmgr.KeyScopeBIP0086.Purpose,
|
||||
// We always use the mainnet coin type for our BIP49/84/86
|
||||
// addresses!
|
||||
CoinType: 0,
|
||||
Account: 0,
|
||||
Xpub: "tpubDDtdXpdJFU2zFKWHJwe5M2WtYtcV7qSWtKohT9VP9zarNSwKnm" +
|
||||
"kwDQawsu1vUf9xwXhUDYXbdUqpcrRTn9bLyW4BAVRimZ4K7r5o1J" +
|
||||
"S924u",
|
||||
}}
|
||||
)
|
||||
|
||||
// testRemoteSigner tests that a watch-only wallet can use a remote signing
|
||||
// wallet to perform any signing or ECDH operations.
|
||||
func testRemoteSigner(ht *lntemp.HarnessTest) {
|
||||
type testCase struct {
|
||||
name string
|
||||
randomSeed bool
|
||||
sendCoins bool
|
||||
fn func(tt *lntemp.HarnessTest,
|
||||
wo, carol *node.HarnessNode)
|
||||
}
|
||||
|
||||
subTests := []testCase{{
|
||||
name: "random seed",
|
||||
randomSeed: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
// Nothing more to test here.
|
||||
},
|
||||
}, {
|
||||
name: "account import",
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runWalletImportAccountScenario(
|
||||
tt, walletrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
carol, wo,
|
||||
)
|
||||
},
|
||||
}, {
|
||||
name: "basic channel open close",
|
||||
sendCoins: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runBasicChannelCreationAndUpdates(tt, wo, carol)
|
||||
},
|
||||
}, {
|
||||
name: "channel funding input types",
|
||||
sendCoins: false,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runChannelFundingInputTypes(tt, carol, wo)
|
||||
},
|
||||
}, {
|
||||
name: "async payments",
|
||||
sendCoins: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runAsyncPayments(tt, wo, carol)
|
||||
},
|
||||
}, {
|
||||
name: "shared key",
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runDeriveSharedKey(tt, wo)
|
||||
},
|
||||
}, {
|
||||
name: "cpfp",
|
||||
sendCoins: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runCPFP(tt, wo, carol)
|
||||
},
|
||||
}, {
|
||||
name: "psbt",
|
||||
randomSeed: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runPsbtChanFunding(tt, carol, wo)
|
||||
runSignPsbtSegWitV0P2WKH(tt, wo)
|
||||
runSignPsbtSegWitV1KeySpendBip86(tt, wo)
|
||||
runSignPsbtSegWitV1KeySpendRootHash(tt, wo)
|
||||
runSignPsbtSegWitV1ScriptSpend(tt, wo)
|
||||
|
||||
// The above tests all make sure we can sign for keys
|
||||
// that aren't in the wallet. But we also want to make
|
||||
// sure we can fund and then sign PSBTs from our wallet.
|
||||
runFundAndSignPsbt(ht, wo)
|
||||
},
|
||||
}, {
|
||||
name: "sign output raw",
|
||||
sendCoins: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runSignOutputRaw(tt, wo)
|
||||
},
|
||||
}, {
|
||||
name: "sign verify msg",
|
||||
sendCoins: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
runSignVerifyMessage(tt, wo)
|
||||
},
|
||||
}, {
|
||||
name: "taproot",
|
||||
sendCoins: true,
|
||||
randomSeed: true,
|
||||
fn: func(tt *lntemp.HarnessTest, wo, carol *node.HarnessNode) {
|
||||
testTaprootSendCoinsKeySpendBip86(tt, wo)
|
||||
testTaprootComputeInputScriptKeySpendBip86(tt, wo)
|
||||
testTaprootSignOutputRawScriptSpend(tt, wo)
|
||||
testTaprootSignOutputRawKeySpendBip86(tt, wo)
|
||||
testTaprootSignOutputRawKeySpendRootHash(tt, wo)
|
||||
|
||||
muSig2Versions := []signrpc.MuSig2Version{
|
||||
signrpc.MuSig2Version_MUSIG2_VERSION_V040,
|
||||
signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2,
|
||||
}
|
||||
for _, version := range muSig2Versions {
|
||||
testTaprootMuSig2KeySpendRootHash(
|
||||
tt, wo, version,
|
||||
)
|
||||
testTaprootMuSig2ScriptSpend(tt, wo, version)
|
||||
testTaprootMuSig2KeySpendBip86(tt, wo, version)
|
||||
testTaprootMuSig2CombinedLeafKeySpend(
|
||||
tt, wo, version,
|
||||
)
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
||||
prepareTest := func(st *lntemp.HarnessTest,
|
||||
subTest testCase) (*node.HarnessNode,
|
||||
*node.HarnessNode, *node.HarnessNode) {
|
||||
|
||||
// Signer is our signing node and has the wallet with the full
|
||||
// master private key. We test that we can create the watch-only
|
||||
// wallet from the exported accounts but also from a static key
|
||||
// to make sure the derivation of the account public keys is
|
||||
// correct in both cases.
|
||||
password := []byte("itestpassword")
|
||||
var (
|
||||
signerNodePubKey = nodePubKey
|
||||
watchOnlyAccounts = deriveCustomScopeAccounts(ht.T)
|
||||
signer *node.HarnessNode
|
||||
err error
|
||||
)
|
||||
if !subTest.randomSeed {
|
||||
signer = st.RestoreNodeWithSeed(
|
||||
"Signer", nil, password, nil, rootKey, 0, nil,
|
||||
)
|
||||
} else {
|
||||
signer = st.NewNode("Signer", nil)
|
||||
signerNodePubKey = signer.PubKeyStr
|
||||
|
||||
rpcAccts := signer.RPC.ListAccounts(
|
||||
&walletrpc.ListAccountsRequest{},
|
||||
)
|
||||
|
||||
watchOnlyAccounts, err = walletrpc.AccountsToWatchOnly(
|
||||
rpcAccts.Accounts,
|
||||
)
|
||||
require.NoError(st, err)
|
||||
}
|
||||
|
||||
// WatchOnly is the node that has a watch-only wallet and uses
|
||||
// the Signer node for any operation that requires access to
|
||||
// private keys.
|
||||
watchOnly := st.NewNodeRemoteSigner(
|
||||
"WatchOnly", []string{
|
||||
"--remotesigner.enable",
|
||||
fmt.Sprintf(
|
||||
"--remotesigner.rpchost=localhost:%d",
|
||||
signer.Cfg.RPCPort,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"--remotesigner.tlscertpath=%s",
|
||||
signer.Cfg.TLSCertPath,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"--remotesigner.macaroonpath=%s",
|
||||
signer.Cfg.AdminMacPath,
|
||||
),
|
||||
}, password, &lnrpc.WatchOnly{
|
||||
MasterKeyBirthdayTimestamp: 0,
|
||||
MasterKeyFingerprint: nil,
|
||||
Accounts: watchOnlyAccounts,
|
||||
},
|
||||
)
|
||||
|
||||
resp := watchOnly.RPC.GetInfo()
|
||||
require.Equal(st, signerNodePubKey, resp.IdentityPubkey)
|
||||
|
||||
if subTest.sendCoins {
|
||||
st.FundCoins(btcutil.SatoshiPerBitcoin, watchOnly)
|
||||
ht.AssertWalletAccountBalance(
|
||||
watchOnly, "default",
|
||||
btcutil.SatoshiPerBitcoin, 0,
|
||||
)
|
||||
}
|
||||
|
||||
carol := st.NewNode("carol", nil)
|
||||
st.EnsureConnected(watchOnly, carol)
|
||||
|
||||
return signer, watchOnly, carol
|
||||
}
|
||||
|
||||
for _, testCase := range subTests {
|
||||
subTest := testCase
|
||||
|
||||
success := ht.Run(subTest.name, func(tt *testing.T) {
|
||||
// Skip the cleanup here as no standby node is used.
|
||||
st := ht.Subtest(tt)
|
||||
|
||||
_, watchOnly, carol := prepareTest(st, subTest)
|
||||
subTest.fn(st, watchOnly, carol)
|
||||
})
|
||||
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deriveCustomScopeAccounts derives the first 255 default accounts of the custom lnd
|
||||
// internal key scope.
|
||||
func deriveCustomScopeAccounts(t *testing.T) []*lnrpc.WatchOnlyAccount {
|
||||
allAccounts := make([]*lnrpc.WatchOnlyAccount, 0, 255+len(accounts))
|
||||
allAccounts = append(allAccounts, accounts...)
|
||||
|
||||
extendedRootKey, err := hdkeychain.NewKeyFromString(rootKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := []uint32{
|
||||
keychain.BIP0043Purpose + hdkeychain.HardenedKeyStart,
|
||||
harnessNetParams.HDCoinType + hdkeychain.HardenedKeyStart,
|
||||
}
|
||||
coinTypeKey, err := derivePath(extendedRootKey, path)
|
||||
require.NoError(t, err)
|
||||
for idx := uint32(0); idx <= 255; idx++ {
|
||||
accountPath := []uint32{idx + hdkeychain.HardenedKeyStart}
|
||||
accountKey, err := derivePath(coinTypeKey, accountPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
accountXPub, err := accountKey.Neuter()
|
||||
require.NoError(t, err)
|
||||
|
||||
allAccounts = append(allAccounts, &lnrpc.WatchOnlyAccount{
|
||||
Purpose: keychain.BIP0043Purpose,
|
||||
CoinType: harnessNetParams.HDCoinType,
|
||||
Account: idx,
|
||||
Xpub: accountXPub.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return allAccounts
|
||||
}
|
||||
|
||||
// derivePath derives the given path from an extended key.
|
||||
func derivePath(key *hdkeychain.ExtendedKey, path []uint32) (
|
||||
*hdkeychain.ExtendedKey, error) {
|
||||
|
||||
var (
|
||||
currentKey = key
|
||||
err error
|
||||
)
|
||||
for _, pathPart := range path {
|
||||
currentKey, err = currentKey.Derive(pathPart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return currentKey, nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testResHandoff tests that the contractcourt is able to properly hand-off
|
||||
// resolution messages to the switch.
|
||||
func testResHandoff(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(1000000)
|
||||
paymentAmt = 50000
|
||||
)
|
||||
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// First we'll create a channel between Alice and Bob.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
params := lntemp.OpenChannelParams{Amt: chanAmt}
|
||||
chanPointAlice := ht.OpenChannel(alice, bob, params)
|
||||
|
||||
// Create a new node Carol that will be in hodl mode. This is used to
|
||||
// trigger the behavior of checkRemoteDanglingActions in the
|
||||
// contractcourt. This will cause Bob to fail the HTLC back to Alice.
|
||||
carol := ht.NewNode("Carol", []string{"--hodl.commit"})
|
||||
defer ht.Shutdown(carol)
|
||||
|
||||
// Connect Bob to Carol.
|
||||
ht.ConnectNodes(bob, carol)
|
||||
|
||||
// Open a channel between Bob and Carol.
|
||||
chanPointCarol := ht.OpenChannel(bob, carol, params)
|
||||
|
||||
// Wait for Alice to see the channel edge in the graph.
|
||||
ht.AssertTopologyChannelOpen(alice, chanPointCarol)
|
||||
|
||||
// We'll create an invoice for Carol that Alice will attempt to pay.
|
||||
// Since Carol is in hodl.commit mode, she won't send back any commit
|
||||
// sigs.
|
||||
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, 1)
|
||||
|
||||
// Alice will now attempt to fulfill the invoice.
|
||||
ht.CompletePaymentRequestsNoWait(alice, carolPayReqs, chanPointAlice)
|
||||
|
||||
// Wait until Carol has received the Add, CommitSig from Bob, and has
|
||||
// responded with a RevokeAndAck. We expect NumUpdates to be 1 meaning
|
||||
// Carol's CommitHeight is 1.
|
||||
ht.AssertChannelCommitHeight(carol, chanPointCarol, 1)
|
||||
|
||||
// Before we shutdown Alice, we'll assert that she only has 1 update.
|
||||
ht.AssertChannelCommitHeight(alice, chanPointAlice, 1)
|
||||
|
||||
// We'll shutdown Alice so that Bob can't connect to her.
|
||||
restartAlice := ht.SuspendNode(alice)
|
||||
|
||||
// Bob will now force close his channel with Carol such that resolution
|
||||
// messages are created and forwarded backwards to Alice.
|
||||
ht.CloseChannelAssertPending(bob, chanPointCarol, true)
|
||||
|
||||
// The channel should be listed in the PendingChannels result.
|
||||
ht.AssertNumWaitingClose(bob, 1)
|
||||
|
||||
// Mine a block to confirm the closing tx.
|
||||
ht.MineBlocks(1)
|
||||
|
||||
// We sleep here so we can be sure that the hand-off has occurred from
|
||||
// Bob's contractcourt to Bob's htlcswitch. This sleep could be removed
|
||||
// if there was some feedback (i.e. API in switch) that allowed for
|
||||
// querying the state of resolution messages.
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
// Mine blocks until Bob has no waiting close channels. This tests that
|
||||
// the circuit-deletion logic is skipped if a resolution message
|
||||
// exists.
|
||||
ht.CleanupForceClose(bob, chanPointCarol)
|
||||
|
||||
// We will now restart Bob so that we can test whether the resolution
|
||||
// messages are re-forwarded on start-up.
|
||||
ht.RestartNode(bob)
|
||||
|
||||
// We'll now also restart Alice and connect her with Bob.
|
||||
require.NoError(ht, restartAlice())
|
||||
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// We'll assert that Alice has received the failure resolution message.
|
||||
ht.AssertChannelCommitHeight(alice, chanPointAlice, 2)
|
||||
|
||||
// Assert that Alice's payment failed.
|
||||
ht.AssertFirstHTLCError(alice, lnrpc.Failure_PERMANENT_CHANNEL_FAILURE)
|
||||
|
||||
ht.CloseChannel(alice, chanPointAlice)
|
||||
}
|
||||
@@ -1,796 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
insecureTransport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
restClient = &http.Client{
|
||||
Transport: insecureTransport,
|
||||
}
|
||||
jsonMarshaler = &jsonpb.Marshaler{
|
||||
EmitDefaults: true,
|
||||
OrigName: true,
|
||||
Indent: " ",
|
||||
}
|
||||
urlEnc = base64.URLEncoding
|
||||
webSocketDialer = &websocket.Dialer{
|
||||
HandshakeTimeout: time.Second,
|
||||
TLSClientConfig: insecureTransport.TLSClientConfig,
|
||||
}
|
||||
resultPattern = regexp.MustCompile("{\"result\":(.*)}")
|
||||
closeMsg = websocket.FormatCloseMessage(
|
||||
websocket.CloseNormalClosure, "done",
|
||||
)
|
||||
|
||||
pingInterval = time.Millisecond * 200
|
||||
pongWait = time.Millisecond * 50
|
||||
)
|
||||
|
||||
// testRestAPI tests that the most important features of the REST API work
|
||||
// correctly.
|
||||
func testRestAPI(ht *lntemp.HarnessTest) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(*testing.T, *node.HarnessNode, *node.HarnessNode)
|
||||
}{{
|
||||
name: "simple GET",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
// Check that the parsing into the response proto
|
||||
// message works.
|
||||
resp := &lnrpc.GetInfoResponse{}
|
||||
err := invokeGET(a, "/v1/getinfo", resp)
|
||||
require.Nil(t, err, "getinfo")
|
||||
require.Equal(t, "#3399ff", resp.Color, "node color")
|
||||
|
||||
// Make sure we get the correct field names (snake
|
||||
// case).
|
||||
_, resp2, err := makeRequest(
|
||||
a, "/v1/getinfo", "GET", nil, nil,
|
||||
)
|
||||
require.Nil(t, err, "getinfo")
|
||||
require.Contains(
|
||||
t, string(resp2), "best_header_timestamp",
|
||||
"getinfo",
|
||||
)
|
||||
},
|
||||
}, {
|
||||
name: "simple POST and GET with query param",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
// Add an invoice, testing POST in the process.
|
||||
req := &lnrpc.Invoice{Value: 1234}
|
||||
resp := &lnrpc.AddInvoiceResponse{}
|
||||
err := invokePOST(a, "/v1/invoices", req, resp)
|
||||
require.Nil(t, err, "add invoice")
|
||||
require.Equal(t, 32, len(resp.RHash), "invoice rhash")
|
||||
|
||||
// Make sure we can call a GET endpoint with a hex
|
||||
// encoded URL part.
|
||||
url := fmt.Sprintf("/v1/invoice/%x", resp.RHash)
|
||||
resp2 := &lnrpc.Invoice{}
|
||||
err = invokeGET(a, url, resp2)
|
||||
require.Nil(t, err, "query invoice")
|
||||
require.Equal(t, int64(1234), resp2.Value,
|
||||
"invoice amt")
|
||||
},
|
||||
}, {
|
||||
name: "GET with base64 encoded byte slice in path",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
url := "/v2/router/mc/probability/%s/%s/%d"
|
||||
url = fmt.Sprintf(
|
||||
url, urlEnc.EncodeToString(a.PubKey[:]),
|
||||
urlEnc.EncodeToString(b.PubKey[:]), 1234,
|
||||
)
|
||||
resp := &routerrpc.QueryProbabilityResponse{}
|
||||
err := invokeGET(a, url, resp)
|
||||
require.Nil(t, err, "query probability")
|
||||
require.Zero(t, resp.Probability)
|
||||
},
|
||||
}, {
|
||||
name: "GET with map type query param",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
// Use a fake address.
|
||||
addr := "bcrt1qlutnwklt4u2548cufrjmsjclewugr9lcpnkzag"
|
||||
|
||||
// Create the full URL with the map query param.
|
||||
url := "/v1/transactions/fee?target_conf=%d&" +
|
||||
"AddrToAmount[%s]=%d"
|
||||
url = fmt.Sprintf(url, 2, addr, 50000)
|
||||
resp := &lnrpc.EstimateFeeResponse{}
|
||||
err := invokeGET(a, url, resp)
|
||||
require.Nil(t, err, "estimate fee")
|
||||
require.Greater(t, resp.FeeSat, int64(253), "fee")
|
||||
},
|
||||
}, {
|
||||
name: "sub RPC servers REST support",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
// Query autopilot status.
|
||||
res1 := &autopilotrpc.StatusResponse{}
|
||||
err := invokeGET(a, "/v2/autopilot/status", res1)
|
||||
require.Nil(t, err, "autopilot status")
|
||||
require.Equal(t, false, res1.Active, "autopilot status")
|
||||
|
||||
// Query the version RPC.
|
||||
res2 := &verrpc.Version{}
|
||||
err = invokeGET(a, "/v2/versioner/version", res2)
|
||||
require.Nil(t, err, "version")
|
||||
require.Greater(
|
||||
t, res2.AppMinor, uint32(0), "lnd minor version",
|
||||
)
|
||||
|
||||
// Request a new external address from the wallet kit.
|
||||
req1 := &walletrpc.AddrRequest{}
|
||||
res3 := &walletrpc.AddrResponse{}
|
||||
err = invokePOST(
|
||||
a, "/v2/wallet/address/next", req1, res3,
|
||||
)
|
||||
require.Nil(t, err, "address")
|
||||
require.NotEmpty(t, res3.Addr, "address")
|
||||
},
|
||||
}, {
|
||||
name: "CORS headers",
|
||||
run: func(t *testing.T, a, b *node.HarnessNode) {
|
||||
t.Helper()
|
||||
|
||||
// Alice allows all origins. Make sure we get the same
|
||||
// value back in the CORS header that we send in the
|
||||
// Origin header.
|
||||
reqHeaders := make(http.Header)
|
||||
reqHeaders.Add("Origin", "https://foo.bar:9999")
|
||||
resHeaders, body, err := makeRequest(
|
||||
a, "/v1/getinfo", "OPTIONS", nil, reqHeaders,
|
||||
)
|
||||
require.Nil(t, err, "getinfo")
|
||||
require.Equal(
|
||||
t, "https://foo.bar:9999",
|
||||
resHeaders.Get("Access-Control-Allow-Origin"),
|
||||
"CORS header",
|
||||
)
|
||||
require.Equal(t, 0, len(body))
|
||||
|
||||
// Make sure that we don't get a value set for Bob which
|
||||
// doesn't allow any CORS origin.
|
||||
resHeaders, body, err = makeRequest(
|
||||
b, "/v1/getinfo", "OPTIONS", nil, reqHeaders,
|
||||
)
|
||||
require.Nil(t, err, "getinfo")
|
||||
require.Equal(
|
||||
t, "",
|
||||
resHeaders.Get("Access-Control-Allow-Origin"),
|
||||
"CORS header",
|
||||
)
|
||||
require.Equal(t, 0, len(body))
|
||||
},
|
||||
}}
|
||||
wsTestCases := []struct {
|
||||
name string
|
||||
run func(ht *lntemp.HarnessTest)
|
||||
}{{
|
||||
name: "websocket subscription",
|
||||
run: wsTestCaseSubscription,
|
||||
}, {
|
||||
name: "websocket subscription with macaroon in protocol",
|
||||
run: wsTestCaseSubscriptionMacaroon,
|
||||
}, {
|
||||
name: "websocket bi-directional subscription",
|
||||
run: wsTestCaseBiDirectionalSubscription,
|
||||
}, {
|
||||
name: "websocket ping and pong timeout",
|
||||
run: wsTestPingPongTimeout,
|
||||
}}
|
||||
|
||||
// Make sure Alice allows all CORS origins. Bob will keep the default.
|
||||
// We also make sure the ping/pong messages are sent very often, so we
|
||||
// can test them without waiting half a minute.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
alice.Cfg.ExtraArgs = append(
|
||||
alice.Cfg.ExtraArgs, "--restcors=\"*\"",
|
||||
fmt.Sprintf("--ws-ping-interval=%s", pingInterval),
|
||||
fmt.Sprintf("--ws-pong-wait=%s", pongWait),
|
||||
)
|
||||
ht.RestartNode(alice)
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
ht.Run(tc.name, func(t *testing.T) {
|
||||
tc.run(t, alice, bob)
|
||||
})
|
||||
}
|
||||
|
||||
for _, tc := range wsTestCases {
|
||||
tc := tc
|
||||
ht.Run(tc.name, func(t *testing.T) {
|
||||
st := ht.Subtest(t)
|
||||
tc.run(st)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func wsTestCaseSubscription(ht *lntemp.HarnessTest) {
|
||||
// Find out the current best block so we can subscribe to the next one.
|
||||
hash, height := ht.Miner.GetBestBlock()
|
||||
|
||||
// Create a new subscription to get block epoch events.
|
||||
req := &chainrpc.BlockEpoch{
|
||||
Hash: hash.CloneBytes(),
|
||||
Height: uint32(height),
|
||||
}
|
||||
url := "/v2/chainnotifier/register/blocks"
|
||||
c, err := openWebSocket(ht.Alice, url, "POST", req, nil)
|
||||
require.NoError(ht, err, "websocket")
|
||||
defer func() {
|
||||
err := c.WriteMessage(websocket.CloseMessage, closeMsg)
|
||||
require.NoError(ht, err)
|
||||
_ = c.Close()
|
||||
}()
|
||||
|
||||
msgChan := make(chan *chainrpc.BlockEpoch)
|
||||
errChan := make(chan error)
|
||||
timeout := time.After(defaultTimeout)
|
||||
|
||||
// We want to read exactly one message.
|
||||
go func() {
|
||||
defer close(msgChan)
|
||||
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// The chunked/streamed responses come wrapped in either a
|
||||
// {"result":{}} or {"error":{}} wrapper which we'll get rid of
|
||||
// here.
|
||||
msgStr := string(msg)
|
||||
if !strings.Contains(msgStr, "\"result\":") {
|
||||
errChan <- fmt.Errorf("invalid msg: %s", msgStr)
|
||||
return
|
||||
}
|
||||
msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
|
||||
|
||||
// Make sure we can parse the unwrapped message into the
|
||||
// expected proto message.
|
||||
protoMsg := &chainrpc.BlockEpoch{}
|
||||
err = jsonpb.UnmarshalString(msgStr, protoMsg)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case msgChan <- protoMsg:
|
||||
case <-timeout:
|
||||
}
|
||||
}()
|
||||
|
||||
// Mine a block and make sure we get a message for it.
|
||||
blockHashes := ht.Miner.GenerateBlocks(1)
|
||||
select {
|
||||
case msg := <-msgChan:
|
||||
require.Equal(
|
||||
ht, blockHashes[0].CloneBytes(), msg.Hash,
|
||||
"block hash",
|
||||
)
|
||||
|
||||
case err := <-errChan:
|
||||
ht.Fatalf("Received error from WS: %v", err)
|
||||
|
||||
case <-timeout:
|
||||
ht.Fatalf("Timeout before message was received")
|
||||
}
|
||||
}
|
||||
|
||||
func wsTestCaseSubscriptionMacaroon(ht *lntemp.HarnessTest) {
|
||||
// Find out the current best block so we can subscribe to the next one.
|
||||
hash, height := ht.Miner.GetBestBlock()
|
||||
|
||||
// Create a new subscription to get block epoch events.
|
||||
req := &chainrpc.BlockEpoch{
|
||||
Hash: hash.CloneBytes(),
|
||||
Height: uint32(height),
|
||||
}
|
||||
url := "/v2/chainnotifier/register/blocks"
|
||||
|
||||
// This time we send the macaroon in the special header
|
||||
// Sec-Websocket-Protocol which is the only header field available to
|
||||
// browsers when opening a WebSocket.
|
||||
alice := ht.Alice
|
||||
mac, err := alice.ReadMacaroon(
|
||||
alice.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err, "read admin mac")
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
require.NoError(ht, err, "marshal admin mac")
|
||||
|
||||
customHeader := make(http.Header)
|
||||
customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
|
||||
"Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
|
||||
))
|
||||
c, err := openWebSocket(alice, url, "POST", req, customHeader)
|
||||
require.Nil(ht, err, "websocket")
|
||||
defer func() {
|
||||
err := c.WriteMessage(websocket.CloseMessage, closeMsg)
|
||||
require.NoError(ht, err)
|
||||
_ = c.Close()
|
||||
}()
|
||||
|
||||
msgChan := make(chan *chainrpc.BlockEpoch)
|
||||
errChan := make(chan error)
|
||||
timeout := time.After(defaultTimeout)
|
||||
|
||||
// We want to read exactly one message.
|
||||
go func() {
|
||||
defer close(msgChan)
|
||||
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// The chunked/streamed responses come wrapped in either a
|
||||
// {"result":{}} or {"error":{}} wrapper which we'll get rid of
|
||||
// here.
|
||||
msgStr := string(msg)
|
||||
if !strings.Contains(msgStr, "\"result\":") {
|
||||
errChan <- fmt.Errorf("invalid msg: %s", msgStr)
|
||||
return
|
||||
}
|
||||
msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
|
||||
|
||||
// Make sure we can parse the unwrapped message into the
|
||||
// expected proto message.
|
||||
protoMsg := &chainrpc.BlockEpoch{}
|
||||
err = jsonpb.UnmarshalString(msgStr, protoMsg)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case msgChan <- protoMsg:
|
||||
case <-timeout:
|
||||
}
|
||||
}()
|
||||
|
||||
// Mine a block and make sure we get a message for it.
|
||||
blockHashes := ht.Miner.GenerateBlocks(1)
|
||||
select {
|
||||
case msg := <-msgChan:
|
||||
require.Equal(
|
||||
ht, blockHashes[0].CloneBytes(), msg.Hash,
|
||||
"block hash",
|
||||
)
|
||||
|
||||
case err := <-errChan:
|
||||
ht.Fatalf("Received error from WS: %v", err)
|
||||
|
||||
case <-timeout:
|
||||
ht.Fatalf("Timeout before message was received")
|
||||
}
|
||||
}
|
||||
|
||||
func wsTestCaseBiDirectionalSubscription(ht *lntemp.HarnessTest) {
|
||||
initialRequest := &lnrpc.ChannelAcceptResponse{}
|
||||
url := "/v1/channels/acceptor"
|
||||
|
||||
// This time we send the macaroon in the special header
|
||||
// Sec-Websocket-Protocol which is the only header field available to
|
||||
// browsers when opening a WebSocket.
|
||||
alice := ht.Alice
|
||||
mac, err := alice.ReadMacaroon(
|
||||
alice.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err, "read admin mac")
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
require.NoError(ht, err, "marshal admin mac")
|
||||
|
||||
customHeader := make(http.Header)
|
||||
customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
|
||||
"Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
|
||||
))
|
||||
conn, err := openWebSocket(
|
||||
alice, url, "POST", initialRequest, customHeader,
|
||||
)
|
||||
require.Nil(ht, err, "websocket")
|
||||
defer func() {
|
||||
err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
|
||||
_ = conn.Close()
|
||||
require.NoError(ht, err)
|
||||
}()
|
||||
|
||||
// Buffer the message channel to make sure we're always blocking on
|
||||
// conn.ReadMessage() to allow the ping/pong mechanism to work.
|
||||
msgChan := make(chan *lnrpc.ChannelAcceptResponse, 1)
|
||||
errChan := make(chan error)
|
||||
done := make(chan struct{})
|
||||
timeout := time.After(defaultTimeout)
|
||||
|
||||
// We want to read messages over and over again. We just accept any
|
||||
// channels that are opened.
|
||||
defer close(done)
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The chunked/streamed responses come wrapped in either
|
||||
// a {"result":{}} or {"error":{}} wrapper which we'll
|
||||
// get rid of here.
|
||||
msgStr := string(msg)
|
||||
if !strings.Contains(msgStr, "\"result\":") {
|
||||
select {
|
||||
case errChan <- fmt.Errorf("invalid msg: %s",
|
||||
msgStr):
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
|
||||
|
||||
// Make sure we can parse the unwrapped message into the
|
||||
// expected proto message.
|
||||
protoMsg := &lnrpc.ChannelAcceptRequest{}
|
||||
err = jsonpb.UnmarshalString(msgStr, protoMsg)
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send the response that we accept the channel.
|
||||
res := &lnrpc.ChannelAcceptResponse{
|
||||
Accept: true,
|
||||
PendingChanId: protoMsg.PendingChanId,
|
||||
}
|
||||
resMsg, err := jsonMarshaler.MarshalToString(res)
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
err = conn.WriteMessage(
|
||||
websocket.TextMessage, []byte(resMsg),
|
||||
)
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Also send the message on our message channel to make
|
||||
// sure we count it as successful.
|
||||
msgChan <- res
|
||||
|
||||
// Are we done or should there be more messages?
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Before we start opening channels, make sure the two nodes are
|
||||
// connected.
|
||||
bob := ht.Bob
|
||||
ht.EnsureConnected(alice, bob)
|
||||
|
||||
// Open 3 channels to make sure multiple requests and responses can be
|
||||
// sent over the web socket.
|
||||
const numChannels = 3
|
||||
for i := 0; i < numChannels; i++ {
|
||||
chanPoint := ht.OpenChannel(
|
||||
bob, alice, lntemp.OpenChannelParams{Amt: 500000},
|
||||
)
|
||||
defer ht.CloseChannel(bob, chanPoint)
|
||||
|
||||
select {
|
||||
case <-msgChan:
|
||||
case err := <-errChan:
|
||||
ht.Fatalf("Received error from WS: %v", err)
|
||||
|
||||
case <-timeout:
|
||||
ht.Fatalf("Timeout before message was received")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wsTestPingPongTimeout(ht *lntemp.HarnessTest) {
|
||||
initialRequest := &lnrpc.InvoiceSubscription{
|
||||
AddIndex: 1, SettleIndex: 1,
|
||||
}
|
||||
url := "/v1/invoices/subscribe"
|
||||
|
||||
// This time we send the macaroon in the special header
|
||||
// Sec-Websocket-Protocol which is the only header field available to
|
||||
// browsers when opening a WebSocket.
|
||||
alice := ht.Alice
|
||||
mac, err := alice.ReadMacaroon(
|
||||
alice.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err, "read admin mac")
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
require.NoError(ht, err, "marshal admin mac")
|
||||
|
||||
customHeader := make(http.Header)
|
||||
customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
|
||||
"Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
|
||||
))
|
||||
conn, err := openWebSocket(
|
||||
alice, url, "GET", initialRequest, customHeader,
|
||||
)
|
||||
require.Nil(ht, err, "websocket")
|
||||
defer func() {
|
||||
err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
|
||||
_ = conn.Close()
|
||||
require.NoError(ht, err)
|
||||
}()
|
||||
|
||||
// We want to be able to read invoices for a long time, making sure we
|
||||
// can continue to read even after we've gone through several ping/pong
|
||||
// cycles.
|
||||
invoices := make(chan *lnrpc.Invoice, 1)
|
||||
errChan := make(chan error)
|
||||
done := make(chan struct{})
|
||||
timeout := time.After(defaultTimeout)
|
||||
|
||||
defer close(done)
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The chunked/streamed responses come wrapped in either
|
||||
// a {"result":{}} or {"error":{}} wrapper which we'll
|
||||
// get rid of here.
|
||||
msgStr := string(msg)
|
||||
if !strings.Contains(msgStr, "\"result\":") {
|
||||
select {
|
||||
case errChan <- fmt.Errorf("invalid msg: %s",
|
||||
msgStr):
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
|
||||
|
||||
// Make sure we can parse the unwrapped message into the
|
||||
// expected proto message.
|
||||
protoMsg := &lnrpc.Invoice{}
|
||||
err = jsonpb.UnmarshalString(msgStr, protoMsg)
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-done:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
invoices <- protoMsg
|
||||
|
||||
// Make sure we exit the loop once we've sent through
|
||||
// all expected test messages.
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// The SubscribeInvoices call returns immediately after the gRPC/REST
|
||||
// connection is established. But it can happen that the goroutine in
|
||||
// lnd that actually registers the subscriber in the invoice backend
|
||||
// didn't get any CPU time just yet. So we can run into the situation
|
||||
// where we add our first invoice _before_ the subscription client is
|
||||
// registered. If that happens, we'll never get notified about the
|
||||
// invoice in question. So all we really can do is wait a bit here to
|
||||
// make sure the subscription is registered correctly.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Let's create five invoices and wait for them to arrive. We'll wait
|
||||
// for at least one ping/pong cycle between each invoice.
|
||||
const numInvoices = 5
|
||||
const value = 123
|
||||
const memo = "websocket"
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
invoice := &lnrpc.Invoice{
|
||||
Value: value,
|
||||
Memo: memo,
|
||||
}
|
||||
alice.RPC.AddInvoice(invoice)
|
||||
|
||||
select {
|
||||
case streamMsg := <-invoices:
|
||||
require.Equal(ht, int64(value), streamMsg.Value)
|
||||
require.Equal(ht, memo, streamMsg.Memo)
|
||||
|
||||
case err := <-errChan:
|
||||
require.Fail(ht, "Error reading invoice: %v", err)
|
||||
|
||||
case <-timeout:
|
||||
require.Fail(ht, "No invoice msg received in time")
|
||||
}
|
||||
|
||||
// Let's wait for at least a whole ping/pong cycle to happen, so
|
||||
// we can be sure the read/write deadlines are set correctly.
|
||||
// We double the pong wait just to add some extra margin.
|
||||
time.Sleep(pingInterval + 2*pongWait)
|
||||
}
|
||||
}
|
||||
|
||||
// invokeGET calls the given URL with the GET method and appropriate macaroon
|
||||
// header fields then tries to unmarshal the response into the given response
|
||||
// proto message.
|
||||
func invokeGET(node *node.HarnessNode, url string, resp proto.Message) error {
|
||||
_, rawResp, err := makeRequest(node, url, "GET", nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return jsonpb.Unmarshal(bytes.NewReader(rawResp), resp)
|
||||
}
|
||||
|
||||
// invokePOST calls the given URL with the POST method, request body and
|
||||
// appropriate macaroon header fields then tries to unmarshal the response into
|
||||
// the given response proto message.
|
||||
func invokePOST(node *node.HarnessNode, url string, req,
|
||||
resp proto.Message) error {
|
||||
|
||||
// Marshal the request to JSON using the jsonpb marshaler to get correct
|
||||
// field names.
|
||||
var buf bytes.Buffer
|
||||
if err := jsonMarshaler.Marshal(&buf, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, rawResp, err := makeRequest(node, url, "POST", &buf, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return jsonpb.Unmarshal(bytes.NewReader(rawResp), resp)
|
||||
}
|
||||
|
||||
// makeRequest calls the given URL with the given method, request body and
|
||||
// appropriate macaroon header fields and returns the raw response body.
|
||||
func makeRequest(node *node.HarnessNode, url, method string,
|
||||
request io.Reader, additionalHeaders http.Header) (http.Header, []byte,
|
||||
error) {
|
||||
|
||||
// Assemble the full URL from the node's listening address then create
|
||||
// the request so we can set the macaroon on it.
|
||||
fullURL := fmt.Sprintf("https://%s%s", node.Cfg.RESTAddr(), url)
|
||||
req, err := http.NewRequest(method, fullURL, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := addAdminMacaroon(node, req.Header); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for key, values := range additionalHeaders {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Do the actual call with the completed request object now.
|
||||
resp, err := restClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
return resp.Header, data, err
|
||||
}
|
||||
|
||||
// openWebSocket opens a new WebSocket connection to the given URL with the
|
||||
// appropriate macaroon headers and sends the request message over the socket.
|
||||
func openWebSocket(node *node.HarnessNode, url, method string,
|
||||
req proto.Message, customHeader http.Header) (*websocket.Conn, error) {
|
||||
|
||||
// Prepare our macaroon headers and assemble the full URL from the
|
||||
// node's listening address. WebSockets always work over GET so we need
|
||||
// to append the target request method as a query parameter.
|
||||
header := customHeader
|
||||
if header == nil {
|
||||
header = make(http.Header)
|
||||
if err := addAdminMacaroon(node, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fullURL := fmt.Sprintf(
|
||||
"wss://%s%s?method=%s", node.Cfg.RESTAddr(), url, method,
|
||||
)
|
||||
conn, resp, err := webSocketDialer.Dial(fullURL, header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// Send the given request message as the first message on the socket.
|
||||
reqMsg, err := jsonMarshaler.MarshalToString(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.WriteMessage(websocket.TextMessage, []byte(reqMsg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// addAdminMacaroon reads the admin macaroon from the node and appends it to
|
||||
// the HTTP header fields.
|
||||
func addAdminMacaroon(node *node.HarnessNode, header http.Header) error {
|
||||
mac, err := node.ReadMacaroon(node.Cfg.AdminMacPath, defaultTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Set("Grpc-Metadata-Macaroon", hex.EncodeToString(macBytes))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,879 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testRevokedCloseRetribution tests that Carol is able carry out
|
||||
// retribution in the event that she fails immediately after detecting Bob's
|
||||
// breach txn in the mempool.
|
||||
func testRevokedCloseRetribution(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
paymentAmt = 10000
|
||||
numInvoices = 6
|
||||
)
|
||||
|
||||
// Carol will be the breached party. We set --nolisten to ensure Bob
|
||||
// won't be able to connect to her and trigger the channel data
|
||||
// protection logic automatically. We also can't have Carol
|
||||
// automatically re-connect too early, otherwise DLP would be initiated
|
||||
// instead of the breach we want to provoke.
|
||||
carol := ht.NewNode(
|
||||
"Carol",
|
||||
[]string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"},
|
||||
)
|
||||
|
||||
// We must let Bob communicate with Carol before they are able to open
|
||||
// channel, so we connect Bob and Carol,
|
||||
bob := ht.Bob
|
||||
ht.ConnectNodes(carol, bob)
|
||||
|
||||
// Before we make a channel, we'll load up Carol with some coins sent
|
||||
// directly from the miner.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// In order to test Carol's response to an uncooperative channel
|
||||
// closure by Bob, we'll first open up a channel between them with a
|
||||
// 0.5 BTC value.
|
||||
chanPoint := ht.OpenChannel(
|
||||
carol, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// With the channel open, we'll create a few invoices for Bob that
|
||||
// Carol will pay to in order to advance the state of the channel.
|
||||
bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numInvoices)
|
||||
|
||||
// Send payments from Carol to Bob using 3 of Bob's payment hashes
|
||||
// generated above.
|
||||
ht.CompletePaymentRequests(carol, bobPayReqs[:numInvoices/2])
|
||||
|
||||
// Next query for Bob's channel state, as we sent 3 payments of 10k
|
||||
// satoshis each, Bob should now see his balance as being 30k satoshis.
|
||||
bobChan := ht.AssertChannelLocalBalance(bob, chanPoint, 30_000)
|
||||
|
||||
// Grab Bob's current commitment height (update number), we'll later
|
||||
// revert him to this state after additional updates to force him to
|
||||
// broadcast this soon to be revoked state.
|
||||
bobStateNumPreCopy := bobChan.NumUpdates
|
||||
|
||||
// With the temporary file created, copy Bob's current state into the
|
||||
// temporary file we created above. Later after more updates, we'll
|
||||
// restore this state.
|
||||
ht.BackupDB(bob)
|
||||
|
||||
// Reconnect the peers after the restart that was needed for the db
|
||||
// backup.
|
||||
ht.EnsureConnected(carol, bob)
|
||||
|
||||
// Because Bob has been restarted, we need to make sure Carol has
|
||||
// remarked the channel as active after that.
|
||||
ht.AssertChannelExists(carol, chanPoint)
|
||||
|
||||
// Finally, send payments from Carol to Bob, consuming Bob's remaining
|
||||
// payment hashes.
|
||||
ht.CompletePaymentRequests(carol, bobPayReqs[numInvoices/2:])
|
||||
|
||||
// Now we shutdown Bob, copying over the his temporary database state
|
||||
// which has the *prior* channel state over his current most up to date
|
||||
// state. With this, we essentially force Bob to travel back in time
|
||||
// within the channel's history.
|
||||
ht.RestartNodeAndRestoreDB(bob)
|
||||
|
||||
// Now query for Bob's channel state, it should show that he's at a
|
||||
// state number in the past, not the *latest* state.
|
||||
ht.AssertChannelCommitHeight(bob, chanPoint, int(bobStateNumPreCopy))
|
||||
|
||||
// Now force Bob to execute a *force* channel closure by unilaterally
|
||||
// broadcasting his current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||
// feel the wrath of Carol's retribution.
|
||||
_, breachTXID := ht.CloseChannelAssertPending(bob, chanPoint, true)
|
||||
|
||||
// Here, Carol sees Bob's breach transaction in the mempool, but is
|
||||
// waiting for it to confirm before continuing her retribution. We
|
||||
// restart Carol to ensure that she is persisting her retribution state
|
||||
// and continues watching for the breach transaction to confirm even
|
||||
// after her node restarts.
|
||||
ht.RestartNode(carol)
|
||||
|
||||
// Finally, generate a single block, wait for the final close status
|
||||
// update, then ensure that the closing transaction was included in the
|
||||
// block.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, breachTXID)
|
||||
|
||||
// Query the mempool for Carol's justice transaction, this should be
|
||||
// broadcast as Bob's contract breaching transaction gets confirmed
|
||||
// above.
|
||||
justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Query for the mempool transaction found above. Then assert that all
|
||||
// the inputs of this transaction are spending outputs generated by
|
||||
// Bob's breach transaction above.
|
||||
justiceTx := ht.Miner.GetRawTransaction(justiceTXID)
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
|
||||
"justice tx not spending commitment utxo")
|
||||
}
|
||||
|
||||
// We restart Carol here to ensure that she persists her retribution
|
||||
// state and successfully continues exacting retribution after
|
||||
// restarting. At this point, Carol has broadcast the justice
|
||||
// transaction, but it hasn't been confirmed yet; when Carol restarts,
|
||||
// she should start waiting for the justice transaction to confirm
|
||||
// again.
|
||||
ht.RestartNode(carol)
|
||||
|
||||
// Now mine a block, this transaction should include Carol's justice
|
||||
// transaction which was just accepted into the mempool.
|
||||
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// The block should have exactly *two* transactions, one of which is
|
||||
// the justice transaction.
|
||||
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
|
||||
|
||||
justiceSha := block.Transactions[1].TxHash()
|
||||
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
|
||||
"justice tx wasn't mined")
|
||||
|
||||
ht.AssertNodeNumChannels(carol, 0)
|
||||
|
||||
// Mine enough blocks for Bob's channel arbitrator to wrap up the
|
||||
// references to the breached channel. The chanarb waits for commitment
|
||||
// tx's confHeight+CSV-1 blocks and since we've already mined one that
|
||||
// included the justice tx we only need to mine extra DefaultCSV-2
|
||||
// blocks to unlock it.
|
||||
ht.MineBlocks(lntest.DefaultCSV - 2)
|
||||
|
||||
ht.AssertNumPendingForceClose(bob, 0)
|
||||
}
|
||||
|
||||
// testRevokedCloseRetributionZeroValueRemoteOutput tests that Dave is able
|
||||
// carry out retribution in the event that he fails in state where the remote
|
||||
// commitment output has zero-value.
|
||||
func testRevokedCloseRetributionZeroValueRemoteOutput(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
paymentAmt = 10000
|
||||
numInvoices = 6
|
||||
)
|
||||
|
||||
// Since we'd like to test some multi-hop failure scenarios, we'll
|
||||
// introduce another node into our test network: Carol.
|
||||
carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"})
|
||||
|
||||
// Dave will be the breached party. We set --nolisten to ensure Carol
|
||||
// won't be able to connect to him and trigger the channel data
|
||||
// protection logic automatically. We also can't have Dave automatically
|
||||
// re-connect too early, otherwise DLP would be initiated instead of the
|
||||
// breach we want to provoke.
|
||||
dave := ht.NewNode(
|
||||
"Dave",
|
||||
[]string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"},
|
||||
)
|
||||
|
||||
// We must let Dave have an open channel before he can send a node
|
||||
// announcement, so we open a channel with Carol,
|
||||
ht.ConnectNodes(dave, carol)
|
||||
|
||||
// Before we make a channel, we'll load up Dave with some coins sent
|
||||
// directly from the miner.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
|
||||
// In order to test Dave's response to an uncooperative channel
|
||||
// closure by Carol, we'll first open up a channel between them with a
|
||||
// 0.5 BTC value.
|
||||
chanPoint := ht.OpenChannel(
|
||||
dave, carol, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// With the channel open, we'll create a few invoices for Carol that
|
||||
// Dave will pay to in order to advance the state of the channel.
|
||||
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
|
||||
|
||||
// Next query for Carol's channel state, as we sent 0 payments, Carol
|
||||
// should now see her balance as being 0 satoshis.
|
||||
carolChan := ht.AssertChannelLocalBalance(carol, chanPoint, 0)
|
||||
|
||||
// Grab Carol's current commitment height (update number), we'll later
|
||||
// revert her to this state after additional updates to force her to
|
||||
// broadcast this soon to be revoked state.
|
||||
carolStateNumPreCopy := int(carolChan.NumUpdates)
|
||||
|
||||
// With the temporary file created, copy Carol's current state into the
|
||||
// temporary file we created above. Later after more updates, we'll
|
||||
// restore this state.
|
||||
ht.BackupDB(carol)
|
||||
|
||||
// Reconnect the peers after the restart that was needed for the db
|
||||
// backup.
|
||||
ht.EnsureConnected(dave, carol)
|
||||
|
||||
// Finally, send payments from Dave to Carol, consuming Carol's
|
||||
// remaining payment hashes.
|
||||
ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint)
|
||||
|
||||
// Now we shutdown Carol, copying over the her temporary database state
|
||||
// which has the *prior* channel state over her current most up to date
|
||||
// state. With this, we essentially force Carol to travel back in time
|
||||
// within the channel's history.
|
||||
ht.RestartNodeAndRestoreDB(carol)
|
||||
|
||||
// Now query for Carol's channel state, it should show that she's at a
|
||||
// state number in the past, not the *latest* state.
|
||||
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
|
||||
|
||||
// Now force Carol to execute a *force* channel closure by unilaterally
|
||||
// broadcasting her current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so she'll soon
|
||||
// feel the wrath of Dave's retribution.
|
||||
stream, closeTxID := ht.CloseChannelAssertPending(
|
||||
carol, chanPoint, true,
|
||||
)
|
||||
|
||||
// Finally, generate a single block, wait for the final close status
|
||||
// update, then ensure that the closing transaction was included in the
|
||||
// block.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Here, Dave receives a confirmation of Carol's breach transaction.
|
||||
// We restart Dave to ensure that he is persisting his retribution
|
||||
// state and continues exacting justice after his node restarts.
|
||||
ht.RestartNode(dave)
|
||||
|
||||
// The breachTXID should match the above closeTxID.
|
||||
breachTXID := ht.WaitForChannelCloseEvent(stream)
|
||||
require.EqualValues(ht, breachTXID, closeTxID)
|
||||
|
||||
// Query the mempool for Dave's justice transaction, this should be
|
||||
// broadcast as Carol's contract breaching transaction gets confirmed
|
||||
// above.
|
||||
justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Query for the mempool transaction found above. Then assert that all
|
||||
// the inputs of this transaction are spending outputs generated by
|
||||
// Carol's breach transaction above.
|
||||
justiceTx := ht.Miner.GetRawTransaction(justiceTXID)
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
|
||||
"justice tx not spending commitment utxo ")
|
||||
}
|
||||
|
||||
// We restart Dave here to ensure that he persists his retribution state
|
||||
// and successfully continues exacting retribution after restarting. At
|
||||
// this point, Dave has broadcast the justice transaction, but it hasn't
|
||||
// been confirmed yet; when Dave restarts, he should start waiting for
|
||||
// the justice transaction to confirm again.
|
||||
ht.RestartNode(dave)
|
||||
|
||||
// Now mine a block, this transaction should include Dave's justice
|
||||
// transaction which was just accepted into the mempool.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// The block should have exactly *two* transactions, one of which is
|
||||
// the justice transaction.
|
||||
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
|
||||
justiceSha := block.Transactions[1].TxHash()
|
||||
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
|
||||
"justice tx wasn't mined")
|
||||
|
||||
ht.AssertNodeNumChannels(dave, 0)
|
||||
}
|
||||
|
||||
// testRevokedCloseRetributionRemoteHodl tests that Dave properly responds to a
|
||||
// channel breach made by the remote party, specifically in the case that the
|
||||
// remote party breaches before settling extended HTLCs.
|
||||
func testRevokedCloseRetributionRemoteHodl(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
pushAmt = 200000
|
||||
paymentAmt = 10000
|
||||
numInvoices = 6
|
||||
)
|
||||
|
||||
// Since this test will result in the counterparty being left in a
|
||||
// weird state, we will introduce another node into our test network:
|
||||
// Carol.
|
||||
carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"})
|
||||
|
||||
// We'll also create a new node Dave, who will have a channel with
|
||||
// Carol, and also use similar settings so we can broadcast a commit
|
||||
// with active HTLCs. Dave will be the breached party. We set
|
||||
// --nolisten to ensure Carol won't be able to connect to him and
|
||||
// trigger the channel data protection logic automatically.
|
||||
dave := ht.NewNode(
|
||||
"Dave",
|
||||
[]string{"--hodl.exit-settle", "--nolisten"},
|
||||
)
|
||||
|
||||
// We must let Dave communicate with Carol before they are able to open
|
||||
// channel, so we connect Dave and Carol,
|
||||
ht.ConnectNodes(dave, carol)
|
||||
|
||||
// Before we make a channel, we'll load up Dave with some coins sent
|
||||
// directly from the miner.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
|
||||
// In order to test Dave's response to an uncooperative channel closure
|
||||
// by Carol, we'll first open up a channel between them with a
|
||||
// funding.MaxBtcFundingAmount (2^24) satoshis value.
|
||||
chanPoint := ht.OpenChannel(
|
||||
dave, carol, lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
},
|
||||
)
|
||||
|
||||
// With the channel open, we'll create a few invoices for Carol that
|
||||
// Dave will pay to in order to advance the state of the channel.
|
||||
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
|
||||
|
||||
// We'll introduce an closure to validate that Carol's current
|
||||
// number of updates is at least as large as the provided minimum
|
||||
// number.
|
||||
checkCarolNumUpdatesAtLeast := func(carolChan *lnrpc.Channel,
|
||||
minimum int) {
|
||||
|
||||
require.GreaterOrEqual(ht, int(carolChan.NumUpdates), minimum,
|
||||
"carol's numupdates is incorrect")
|
||||
}
|
||||
|
||||
// Ensure that carol's balance starts with the amount we pushed to her.
|
||||
ht.AssertChannelLocalBalance(carol, chanPoint, pushAmt)
|
||||
|
||||
// Send payments from Dave to Carol using 3 of Carol's payment hashes
|
||||
// generated above.
|
||||
ht.CompletePaymentRequestsNoWait(
|
||||
dave, carolPayReqs[:numInvoices/2], chanPoint,
|
||||
)
|
||||
|
||||
// At this point, we'll also send over a set of HTLC's from Carol to
|
||||
// Dave. This ensures that the final revoked transaction has HTLC's in
|
||||
// both directions.
|
||||
davePayReqs, _, _ := ht.CreatePayReqs(dave, paymentAmt, numInvoices)
|
||||
|
||||
// Send payments from Carol to Dave using 3 of Dave's payment hashes
|
||||
// generated above.
|
||||
ht.CompletePaymentRequestsNoWait(
|
||||
carol, davePayReqs[:numInvoices/2], chanPoint,
|
||||
)
|
||||
|
||||
// Next query for Carol's channel state, as we sent 3 payments of 10k
|
||||
// satoshis each, however Carol should now see her balance as being
|
||||
// equal to the push amount in satoshis since she has not settled.
|
||||
|
||||
// Ensure that carol's balance still reflects the original amount we
|
||||
// pushed to her, minus the HTLCs she just sent to Dave.
|
||||
carolChan := ht.AssertChannelLocalBalance(
|
||||
carol, chanPoint, pushAmt-3*paymentAmt,
|
||||
)
|
||||
|
||||
// Grab Carol's current commitment height (update number), we'll later
|
||||
// revert her to this state after additional updates to force her to
|
||||
// broadcast this soon to be revoked state.
|
||||
carolStateNumPreCopy := int(carolChan.NumUpdates)
|
||||
|
||||
// Since Carol has not settled, she should only see at least one update
|
||||
// to her channel.
|
||||
checkCarolNumUpdatesAtLeast(carolChan, 1)
|
||||
|
||||
// With the temporary file created, copy Carol's current state into the
|
||||
// temporary file we created above. Later after more updates, we'll
|
||||
// restore this state.
|
||||
ht.BackupDB(carol)
|
||||
|
||||
// Reconnect the peers after the restart that was needed for the db
|
||||
// backup.
|
||||
ht.EnsureConnected(dave, carol)
|
||||
|
||||
// Finally, send payments from Dave to Carol, consuming Carol's
|
||||
// remaining payment hashes.
|
||||
ht.CompletePaymentRequestsNoWait(
|
||||
dave, carolPayReqs[numInvoices/2:], chanPoint,
|
||||
)
|
||||
|
||||
// Ensure that carol's balance still shows the amount we originally
|
||||
// pushed to her (minus the HTLCs she sent to Bob), and that at least
|
||||
// one more update has occurred.
|
||||
carolChan = ht.AssertChannelLocalBalance(
|
||||
carol, chanPoint, pushAmt-3*paymentAmt,
|
||||
)
|
||||
checkCarolNumUpdatesAtLeast(carolChan, carolStateNumPreCopy+1)
|
||||
|
||||
// Suspend Dave, such that Carol won't reconnect at startup, triggering
|
||||
// the data loss protection.
|
||||
restartDave := ht.SuspendNode(dave)
|
||||
|
||||
// Now we shutdown Carol, copying over the her temporary database state
|
||||
// which has the *prior* channel state over her current most up to date
|
||||
// state. With this, we essentially force Carol to travel back in time
|
||||
// within the channel's history.
|
||||
ht.RestartNodeAndRestoreDB(carol)
|
||||
|
||||
// Ensure that Carol's view of the channel is consistent with the state
|
||||
// of the channel just before it was snapshotted.
|
||||
carolChan = ht.AssertChannelLocalBalance(
|
||||
carol, chanPoint, pushAmt-3*paymentAmt,
|
||||
)
|
||||
checkCarolNumUpdatesAtLeast(carolChan, 1)
|
||||
|
||||
// Now query for Carol's channel state, it should show that she's at a
|
||||
// state number in the past, *not* the latest state.
|
||||
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
|
||||
|
||||
// Now force Carol to execute a *force* channel closure by unilaterally
|
||||
// broadcasting her current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so she'll soon
|
||||
// feel the wrath of Dave's retribution.
|
||||
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
|
||||
carol, chanPoint, true,
|
||||
)
|
||||
|
||||
// Generate a single block to mine the breach transaction.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// We resurrect Dave to ensure he will be exacting justice after his
|
||||
// node restarts.
|
||||
require.NoError(ht, restartDave(), "unable to restart Dave's node")
|
||||
|
||||
// Finally, wait for the final close status update, then ensure that
|
||||
// the closing transaction was included in the block.
|
||||
breachTXID := ht.WaitForChannelCloseEvent(closeUpdates)
|
||||
require.Equal(ht, closeTxID[:], breachTXID[:],
|
||||
"expected breach ID to be equal to close ID")
|
||||
ht.Miner.AssertTxInBlock(block, breachTXID)
|
||||
|
||||
// Query the mempool for Dave's justice transaction, this should be
|
||||
// broadcast as Carol's contract breaching transaction gets confirmed
|
||||
// above. Since Carol might have had the time to take some of the HTLC
|
||||
// outputs to the second level before Dave broadcasts his justice tx,
|
||||
// we'll search through the mempool for a tx that matches the number of
|
||||
// expected inputs in the justice tx.
|
||||
var justiceTxid *chainhash.Hash
|
||||
errNotFound := errors.New("justice tx not found")
|
||||
findJusticeTx := func() (*chainhash.Hash, error) {
|
||||
mempool := ht.Miner.GetRawMempool()
|
||||
|
||||
for _, txid := range mempool {
|
||||
// Check that the justice tx has the appropriate number
|
||||
// of inputs.
|
||||
tx := ht.Miner.GetRawTransaction(txid)
|
||||
|
||||
exNumInputs := 2 + numInvoices
|
||||
if len(tx.MsgTx().TxIn) == exNumInputs {
|
||||
return txid, nil
|
||||
}
|
||||
}
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
err := wait.NoError(func() error {
|
||||
txid, err := findJusticeTx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
justiceTxid = txid
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
|
||||
if err != nil && errors.Is(err, errNotFound) {
|
||||
// If Dave is unable to broadcast his justice tx on first
|
||||
// attempt because of the second layer transactions, he will
|
||||
// wait until the next block epoch before trying again. Because
|
||||
// of this, we'll mine a block if we cannot find the justice tx
|
||||
// immediately. Since we cannot tell for sure how many
|
||||
// transactions will be in the mempool at this point, we pass 0
|
||||
// as the last argument, indicating we don't care what's in the
|
||||
// mempool.
|
||||
ht.MineBlocks(1)
|
||||
err = wait.NoError(func() error {
|
||||
txid, err := findJusticeTx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
justiceTxid = txid
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
}
|
||||
require.NoError(ht, err, "timeout finding justice tx")
|
||||
|
||||
justiceTx := ht.Miner.GetRawTransaction(justiceTxid)
|
||||
|
||||
// isSecondLevelSpend checks that the passed secondLevelTxid is a
|
||||
// potentitial second level spend spending from the commit tx.
|
||||
isSecondLevelSpend := func(commitTxid,
|
||||
secondLevelTxid *chainhash.Hash) bool {
|
||||
|
||||
secondLevel := ht.Miner.GetRawTransaction(secondLevelTxid)
|
||||
|
||||
// A second level spend should have only one input, and one
|
||||
// output.
|
||||
if len(secondLevel.MsgTx().TxIn) != 1 {
|
||||
return false
|
||||
}
|
||||
if len(secondLevel.MsgTx().TxOut) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// The sole input should be spending from the commit tx.
|
||||
txIn := secondLevel.MsgTx().TxIn[0]
|
||||
|
||||
return bytes.Equal(txIn.PreviousOutPoint.Hash[:], commitTxid[:])
|
||||
}
|
||||
|
||||
// Check that all the inputs of this transaction are spending outputs
|
||||
// generated by Carol's breach transaction above.
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
if bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the justice tx is spending from an output that was not on
|
||||
// the breach tx, Carol might have had the time to take an
|
||||
// output to the second level. In that case, check that the
|
||||
// justice tx is spending this second level output.
|
||||
if isSecondLevelSpend(breachTXID, &txIn.PreviousOutPoint.Hash) {
|
||||
continue
|
||||
}
|
||||
require.Fail(ht, "justice tx not spending commitment utxo "+
|
||||
"instead is: %v", txIn.PreviousOutPoint)
|
||||
}
|
||||
|
||||
// We restart Dave here to ensure that he persists he retribution state
|
||||
// and successfully continues exacting retribution after restarting. At
|
||||
// this point, Dave has broadcast the justice transaction, but it
|
||||
// hasn't been confirmed yet; when Dave restarts, he should start
|
||||
// waiting for the justice transaction to confirm again.
|
||||
ht.RestartNode(dave)
|
||||
|
||||
// Now mine a block, this transaction should include Dave's justice
|
||||
// transaction which was just accepted into the mempool.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Dave should have no open channels.
|
||||
ht.AssertNodeNumChannels(dave, 0)
|
||||
}
|
||||
|
||||
// testRevokedCloseRetributionAltruistWatchtower establishes a channel between
|
||||
// Carol and Dave, where Carol is using a third node Willy as her watchtower.
|
||||
// After sending some payments, Dave reverts his state and force closes to
|
||||
// trigger a breach. Carol is kept offline throughout the process and the test
|
||||
// asserts that Willy responds by broadcasting the justice transaction on
|
||||
// Carol's behalf sweeping her funds without a reward.
|
||||
func testRevokedCloseRetributionAltruistWatchtower(ht *lntemp.HarnessTest) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
anchors bool
|
||||
}{{
|
||||
name: "anchors",
|
||||
anchors: true,
|
||||
}, {
|
||||
name: "legacy",
|
||||
anchors: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
testFunc := func(ht *lntemp.HarnessTest) {
|
||||
testRevokedCloseRetributionAltruistWatchtowerCase(
|
||||
ht, tc.anchors,
|
||||
)
|
||||
}
|
||||
|
||||
success := ht.Run(tc.name, func(tt *testing.T) {
|
||||
st := ht.Subtest(tt)
|
||||
|
||||
st.RunTestCase(&lntemp.TestCase{
|
||||
Name: tc.name,
|
||||
TestFunc: testFunc,
|
||||
})
|
||||
})
|
||||
|
||||
if !success {
|
||||
// Log failure time to help relate the lnd logs to the
|
||||
// failure.
|
||||
ht.Logf("Failure time: %v", time.Now().Format(
|
||||
"2006-01-02 15:04:05.000",
|
||||
))
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntemp.HarnessTest,
|
||||
anchors bool) {
|
||||
|
||||
const (
|
||||
chanAmt = funding.MaxBtcFundingAmount
|
||||
paymentAmt = 10000
|
||||
numInvoices = 6
|
||||
externalIP = "1.2.3.4"
|
||||
)
|
||||
|
||||
// Since we'd like to test some multi-hop failure scenarios, we'll
|
||||
// introduce another node into our test network: Carol.
|
||||
carolArgs := []string{"--hodl.exit-settle"}
|
||||
if anchors {
|
||||
carolArgs = append(carolArgs, "--protocol.anchors")
|
||||
}
|
||||
carol := ht.NewNode("Carol", carolArgs)
|
||||
|
||||
// Willy the watchtower will protect Dave from Carol's breach. He will
|
||||
// remain online in order to punish Carol on Dave's behalf, since the
|
||||
// breach will happen while Dave is offline.
|
||||
willy := ht.NewNode(
|
||||
"Willy", []string{"--watchtower.active",
|
||||
"--watchtower.externalip=" + externalIP},
|
||||
)
|
||||
|
||||
willyInfo := willy.RPC.GetInfoWatchtower()
|
||||
|
||||
// Assert that Willy has one listener and it is 0.0.0.0:9911 or
|
||||
// [::]:9911. Since no listener is explicitly specified, one of these
|
||||
// should be the default depending on whether the host supports IPv6 or
|
||||
// not.
|
||||
require.Len(ht, willyInfo.Listeners, 1, "Willy should have 1 listener")
|
||||
listener := willyInfo.Listeners[0]
|
||||
if listener != "0.0.0.0:9911" && listener != "[::]:9911" {
|
||||
ht.Fatalf("expected listener on 0.0.0.0:9911 or [::]:9911, "+
|
||||
"got %v", listener)
|
||||
}
|
||||
|
||||
// Assert the Willy's URIs properly display the chosen external IP.
|
||||
require.Len(ht, willyInfo.Uris, 1, "Willy should have 1 uri")
|
||||
require.Contains(ht, willyInfo.Uris[0], externalIP)
|
||||
|
||||
// Dave will be the breached party. We set --nolisten to ensure Carol
|
||||
// won't be able to connect to him and trigger the channel data
|
||||
// protection logic automatically.
|
||||
daveArgs := []string{
|
||||
"--nolisten",
|
||||
"--wtclient.active",
|
||||
}
|
||||
if anchors {
|
||||
daveArgs = append(daveArgs, "--protocol.anchors")
|
||||
}
|
||||
dave := ht.NewNode("Dave", daveArgs)
|
||||
|
||||
addTowerReq := &wtclientrpc.AddTowerRequest{
|
||||
Pubkey: willyInfo.Pubkey,
|
||||
Address: listener,
|
||||
}
|
||||
dave.RPC.AddTower(addTowerReq)
|
||||
|
||||
// We must let Dave have an open channel before she can send a node
|
||||
// announcement, so we open a channel with Carol,
|
||||
ht.ConnectNodes(dave, carol)
|
||||
|
||||
// Before we make a channel, we'll load up Dave with some coins sent
|
||||
// directly from the miner.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
|
||||
// Send one more UTXOs if this is a neutrino backend.
|
||||
if ht.IsNeutrinoBackend() {
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
}
|
||||
|
||||
// In order to test Dave's response to an uncooperative channel
|
||||
// closure by Carol, we'll first open up a channel between them with a
|
||||
// 0.5 BTC value.
|
||||
params := lntemp.OpenChannelParams{
|
||||
Amt: 3 * (chanAmt / 4),
|
||||
PushAmt: chanAmt / 4,
|
||||
}
|
||||
chanPoint := ht.OpenChannel(dave, carol, params)
|
||||
|
||||
// With the channel open, we'll create a few invoices for Carol that
|
||||
// Dave will pay to in order to advance the state of the channel.
|
||||
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
|
||||
|
||||
// Next query for Carol's channel state, as we sent 0 payments, Carol
|
||||
// should still see her balance as the push amount, which is 1/4 of the
|
||||
// capacity.
|
||||
carolChan := ht.AssertChannelLocalBalance(
|
||||
carol, chanPoint, int64(chanAmt/4),
|
||||
)
|
||||
|
||||
// Grab Carol's current commitment height (update number), we'll later
|
||||
// revert her to this state after additional updates to force him to
|
||||
// broadcast this soon to be revoked state.
|
||||
carolStateNumPreCopy := int(carolChan.NumUpdates)
|
||||
|
||||
// With the temporary file created, copy Carol's current state into the
|
||||
// temporary file we created above. Later after more updates, we'll
|
||||
// restore this state.
|
||||
ht.BackupDB(carol)
|
||||
|
||||
// Reconnect the peers after the restart that was needed for the db
|
||||
// backup.
|
||||
ht.EnsureConnected(dave, carol)
|
||||
|
||||
// Finally, send payments from Dave to Carol, consuming Carol's
|
||||
// remaining payment hashes.
|
||||
ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint)
|
||||
|
||||
daveBalResp := dave.RPC.WalletBalance()
|
||||
davePreSweepBalance := daveBalResp.ConfirmedBalance
|
||||
|
||||
// Wait until the backup has been accepted by the watchtower before
|
||||
// shutting down Dave.
|
||||
err := wait.NoError(func() error {
|
||||
bkpStats := dave.RPC.WatchtowerStats()
|
||||
if bkpStats == nil {
|
||||
return errors.New("no active backup sessions")
|
||||
}
|
||||
if bkpStats.NumBackups == 0 {
|
||||
return errors.New("no backups accepted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "unable to verify backup task completed")
|
||||
|
||||
// Shutdown Dave to simulate going offline for an extended period of
|
||||
// time. Once he's not watching, Carol will try to breach the channel.
|
||||
restart := ht.SuspendNode(dave)
|
||||
|
||||
// Now we shutdown Carol, copying over the his temporary database state
|
||||
// which has the *prior* channel state over his current most up to date
|
||||
// state. With this, we essentially force Carol to travel back in time
|
||||
// within the channel's history.
|
||||
ht.RestartNodeAndRestoreDB(carol)
|
||||
|
||||
// Now query for Carol's channel state, it should show that he's at a
|
||||
// state number in the past, not the *latest* state.
|
||||
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
|
||||
|
||||
// Now force Carol to execute a *force* channel closure by unilaterally
|
||||
// broadcasting his current channel state. This is actually the
|
||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||
// feel the wrath of Dave's retribution.
|
||||
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
|
||||
carol, chanPoint, true,
|
||||
)
|
||||
|
||||
// Finally, generate a single block, wait for the final close status
|
||||
// update, then ensure that the closing transaction was included in the
|
||||
// block.
|
||||
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
breachTXID := ht.WaitForChannelCloseEvent(closeUpdates)
|
||||
ht.Miner.AssertTxInBlock(block, breachTXID)
|
||||
|
||||
// The breachTXID should match the above closeTxID.
|
||||
require.EqualValues(ht, breachTXID, closeTxID)
|
||||
|
||||
// Query the mempool for Dave's justice transaction, this should be
|
||||
// broadcast as Carol's contract breaching transaction gets confirmed
|
||||
// above.
|
||||
justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
// Query for the mempool transaction found above. Then assert that all
|
||||
// the inputs of this transaction are spending outputs generated by
|
||||
// Carol's breach transaction above.
|
||||
justiceTx := ht.Miner.GetRawTransaction(justiceTXID)
|
||||
for _, txIn := range justiceTx.MsgTx().TxIn {
|
||||
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
|
||||
"justice tx not spending commitment utxo")
|
||||
}
|
||||
|
||||
willyBalResp := willy.RPC.WalletBalance()
|
||||
require.Zero(ht, willyBalResp.ConfirmedBalance,
|
||||
"willy should have 0 balance before mining justice transaction")
|
||||
|
||||
// Now mine a block, this transaction should include Dave's justice
|
||||
// transaction which was just accepted into the mempool.
|
||||
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
||||
|
||||
// The block should have exactly *two* transactions, one of which is
|
||||
// the justice transaction.
|
||||
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
|
||||
justiceSha := block.Transactions[1].TxHash()
|
||||
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
|
||||
"justice tx wasn't mined")
|
||||
|
||||
// Ensure that Willy doesn't get any funds, as he is acting as an
|
||||
// altruist watchtower.
|
||||
err = wait.NoError(func() error {
|
||||
willyBalResp := willy.RPC.WalletBalance()
|
||||
|
||||
if willyBalResp.ConfirmedBalance != 0 {
|
||||
return fmt.Errorf("Expected Willy to have no funds "+
|
||||
"after justice transaction was mined, found %v",
|
||||
willyBalResp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, time.Second*5)
|
||||
require.NoError(ht, err, "timeout checking willy's balance")
|
||||
|
||||
// Before restarting Dave, shutdown Carol so Dave won't sync with her.
|
||||
// Otherwise, during the restart, Dave will realize Carol is falling
|
||||
// behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing
|
||||
// the channel. Although this force close tx will be later replaced by
|
||||
// the breach tx, it will create two anchor sweeping txes for neutrino
|
||||
// backend, causing the confirmed wallet balance to be zero later on
|
||||
// because the utxos are used in sweeping.
|
||||
ht.Shutdown(carol)
|
||||
|
||||
// Restart Dave, who will still think his channel with Carol is open.
|
||||
// We should him to detect the breach, but realize that the funds have
|
||||
// then been swept to his wallet by Willy.
|
||||
require.NoError(ht, restart(), "unable to restart dave")
|
||||
|
||||
err = wait.NoError(func() error {
|
||||
daveBalResp := dave.RPC.ChannelBalance()
|
||||
if daveBalResp.LocalBalance.Sat != 0 {
|
||||
return fmt.Errorf("Dave should end up with zero "+
|
||||
"channel balance, instead has %d",
|
||||
daveBalResp.LocalBalance.Sat)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking dave's channel balance")
|
||||
|
||||
ht.AssertNumPendingForceClose(dave, 0)
|
||||
|
||||
// If this is an anchor channel, Dave would sweep the anchor.
|
||||
if anchors {
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
}
|
||||
|
||||
// Check that Dave's wallet balance is increased.
|
||||
err = wait.NoError(func() error {
|
||||
daveBalResp := dave.RPC.WalletBalance()
|
||||
|
||||
if daveBalResp.ConfirmedBalance <= davePreSweepBalance {
|
||||
return fmt.Errorf("Dave should have more than %d "+
|
||||
"after sweep, instead has %d",
|
||||
davePreSweepBalance,
|
||||
daveBalResp.ConfirmedBalance)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(ht, err, "timeout checking dave's wallet balance")
|
||||
|
||||
// Dave should have no open channels.
|
||||
ht.AssertNodeNumChannels(dave, 0)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,870 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
// testRPCMiddlewareInterceptor tests that the RPC middleware interceptor can
|
||||
// be used correctly and in a safe way.
|
||||
func testRPCMiddlewareInterceptor(ht *lntemp.HarnessTest) {
|
||||
// Let's first enable the middleware interceptor.
|
||||
//
|
||||
// NOTE: we cannot use standby nodes here as the test messes with
|
||||
// middleware interceptor. Thus we also skip the calling of cleanup of
|
||||
// each of the following subtests because no standby nodes are used.
|
||||
alice := ht.NewNode("alice", []string{"--rpcmiddleware.enable"})
|
||||
bob := ht.NewNode("bob", nil)
|
||||
|
||||
// Let's set up a channel between Alice and Bob, just to get some useful
|
||||
// data to inspect when doing RPC calls to Alice later.
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
|
||||
ht.OpenChannel(alice, bob, lntemp.OpenChannelParams{Amt: 1_234_567})
|
||||
|
||||
// Load or bake the macaroons that the simulated users will use to
|
||||
// access the RPC.
|
||||
readonlyMac, err := alice.ReadMacaroon(
|
||||
alice.Cfg.ReadMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
adminMac, err := alice.ReadMacaroon(
|
||||
alice.Cfg.AdminMacPath, defaultTimeout,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
customCaveatReadonlyMac, err := macaroons.SafeCopyMacaroon(readonlyMac)
|
||||
require.NoError(ht, err)
|
||||
addConstraint := macaroons.CustomConstraint(
|
||||
"itest-caveat", "itest-value",
|
||||
)
|
||||
require.NoError(ht, addConstraint(customCaveatReadonlyMac))
|
||||
customCaveatAdminMac, err := macaroons.SafeCopyMacaroon(adminMac)
|
||||
require.NoError(ht, err)
|
||||
require.NoError(ht, addConstraint(customCaveatAdminMac))
|
||||
|
||||
// Run all sub-tests now. We can't run anything in parallel because that
|
||||
// would cause the main test function to exit and the nodes being
|
||||
// cleaned up.
|
||||
ht.Run("registration restrictions", func(tt *testing.T) {
|
||||
middlewareRegistrationRestrictionTests(tt, alice)
|
||||
})
|
||||
|
||||
ht.Run("read-only intercept", func(tt *testing.T) {
|
||||
registration := registerMiddleware(
|
||||
tt, alice, &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor-1",
|
||||
ReadOnlyMode: true,
|
||||
}, true,
|
||||
)
|
||||
defer registration.cancel()
|
||||
|
||||
middlewareInterceptionTest(
|
||||
tt, alice, bob, registration, readonlyMac,
|
||||
customCaveatReadonlyMac, true,
|
||||
)
|
||||
})
|
||||
|
||||
// We've manually disconnected Bob from Alice in the previous test, make
|
||||
// sure they're connected again.
|
||||
//
|
||||
// NOTE: we may get an error here saying "interceptor RPC client quit"
|
||||
// as it takes some time for the interceptor to fully quit. Thus we
|
||||
// restart the node here to make sure the old interceptor is removed
|
||||
// from registration.
|
||||
ht.RestartNode(alice)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.Run("encumbered macaroon intercept", func(tt *testing.T) {
|
||||
registration := registerMiddleware(
|
||||
tt, alice, &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor-2",
|
||||
CustomMacaroonCaveatName: "itest-caveat",
|
||||
}, true,
|
||||
)
|
||||
defer registration.cancel()
|
||||
|
||||
middlewareInterceptionTest(
|
||||
tt, alice, bob, registration,
|
||||
customCaveatReadonlyMac, readonlyMac, false,
|
||||
)
|
||||
})
|
||||
|
||||
// Next, run the response manipulation tests.
|
||||
//
|
||||
// NOTE: we may get an error here saying "interceptor RPC client quit"
|
||||
// as it takes some time for the interceptor to fully quit. Thus we
|
||||
// restart the node here to make sure the old interceptor is removed
|
||||
// from registration.
|
||||
ht.RestartNode(alice)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.Run("read-only not allowed to manipulate", func(tt *testing.T) {
|
||||
registration := registerMiddleware(
|
||||
tt, alice, &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor-3",
|
||||
ReadOnlyMode: true,
|
||||
}, true,
|
||||
)
|
||||
defer registration.cancel()
|
||||
|
||||
middlewareRequestManipulationTest(
|
||||
tt, alice, registration, adminMac, true,
|
||||
)
|
||||
middlewareResponseManipulationTest(
|
||||
tt, alice, bob, registration, readonlyMac, true,
|
||||
)
|
||||
})
|
||||
|
||||
// NOTE: we may get an error here saying "interceptor RPC client quit"
|
||||
// as it takes some time for the interceptor to fully quit. Thus we
|
||||
// restart the node here to make sure the old interceptor is removed
|
||||
// from registration.
|
||||
ht.RestartNode(alice)
|
||||
ht.EnsureConnected(alice, bob)
|
||||
ht.Run("encumbered macaroon manipulate", func(tt *testing.T) {
|
||||
registration := registerMiddleware(
|
||||
tt, alice, &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor-4",
|
||||
CustomMacaroonCaveatName: "itest-caveat",
|
||||
}, true,
|
||||
)
|
||||
defer registration.cancel()
|
||||
|
||||
middlewareRequestManipulationTest(
|
||||
tt, alice, registration, customCaveatAdminMac, false,
|
||||
)
|
||||
middlewareResponseManipulationTest(
|
||||
tt, alice, bob, registration,
|
||||
customCaveatReadonlyMac, false,
|
||||
)
|
||||
})
|
||||
|
||||
// And finally make sure mandatory middleware is always checked for any
|
||||
// RPC request.
|
||||
ht.Run("mandatory middleware", func(tt *testing.T) {
|
||||
st := ht.Subtest(tt)
|
||||
middlewareMandatoryTest(st, alice)
|
||||
})
|
||||
}
|
||||
|
||||
// middlewareRegistrationRestrictionTests tests all restrictions that apply to
|
||||
// registering a middleware.
|
||||
func middlewareRegistrationRestrictionTests(t *testing.T,
|
||||
node *node.HarnessNode) {
|
||||
|
||||
testCases := []struct {
|
||||
registration *lnrpc.MiddlewareRegistration
|
||||
expectedErr string
|
||||
}{{
|
||||
registration: &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "foo",
|
||||
},
|
||||
expectedErr: "invalid middleware name",
|
||||
}, {
|
||||
registration: &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor",
|
||||
CustomMacaroonCaveatName: "foo",
|
||||
},
|
||||
expectedErr: "custom caveat name of at least",
|
||||
}, {
|
||||
registration: &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor",
|
||||
CustomMacaroonCaveatName: "itest-caveat",
|
||||
ReadOnlyMode: true,
|
||||
},
|
||||
expectedErr: "cannot set read-only and custom caveat name",
|
||||
}}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(fmt.Sprintf("%d", idx), func(tt *testing.T) {
|
||||
invalidName := registerMiddleware(
|
||||
tt, node, tc.registration, false,
|
||||
)
|
||||
_, err := invalidName.stream.Recv()
|
||||
require.Error(tt, err)
|
||||
require.Contains(tt, err.Error(), tc.expectedErr)
|
||||
|
||||
invalidName.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// middlewareInterceptionTest tests that unary and streaming requests can be
|
||||
// intercepted. It also makes sure that depending on the mode (read-only or
|
||||
// custom macaroon caveat) a middleware only gets access to the requests it
|
||||
// should be allowed access to.
|
||||
func middlewareInterceptionTest(t *testing.T,
|
||||
node, peer *node.HarnessNode, registration *middlewareHarness,
|
||||
userMac *macaroon.Macaroon,
|
||||
disallowedMac *macaroon.Macaroon, readOnly bool) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
// Everything we test here should be executed in a matter of
|
||||
// milliseconds, so we can use one single timeout context for all calls.
|
||||
ctxb := context.Background()
|
||||
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a client connection that we'll use to simulate user requests
|
||||
// to lnd with.
|
||||
cleanup, client := macaroonClient(t, node, userMac)
|
||||
defer cleanup()
|
||||
|
||||
// We're going to send a simple RPC request to list all channels.
|
||||
// We need to invoke the intercept logic in a goroutine because we'd
|
||||
// block the execution of the main task otherwise.
|
||||
req := &lnrpc.ListChannelsRequest{ActiveOnly: true}
|
||||
go registration.interceptUnary(
|
||||
"/lnrpc.Lightning/ListChannels", req, nil, readOnly, false,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
resp, err := client.ListChannels(ctxc, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Did we receive the correct intercept message?
|
||||
assertInterceptedType(t, resp, <-registration.responsesChan)
|
||||
|
||||
// Also try the interception of a request that is expected to result in
|
||||
// an error being returned from lnd.
|
||||
expectedErr := "either `active_only` or `inactive_only` can be set"
|
||||
invalidReq := &lnrpc.ListChannelsRequest{
|
||||
ActiveOnly: true,
|
||||
InactiveOnly: true,
|
||||
}
|
||||
go registration.interceptUnary(
|
||||
"/lnrpc.Lightning/ListChannels", invalidReq, nil, readOnly,
|
||||
false,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
_, err = client.ListChannels(ctxc, invalidReq)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr)
|
||||
|
||||
// Did we receive the correct intercept message with the error?
|
||||
assertInterceptedErr(t, expectedErr, <-registration.responsesChan)
|
||||
|
||||
// Let's test the same for a streaming endpoint.
|
||||
req2 := &lnrpc.PeerEventSubscription{}
|
||||
go registration.interceptStream(
|
||||
"/lnrpc.Lightning/SubscribePeerEvents", req2, nil, readOnly,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
peerCtx, peerCancel := context.WithCancel(ctxb)
|
||||
resp2, err := client.SubscribePeerEvents(peerCtx, req2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Disconnect Bob to trigger a peer event without using Alice's RPC
|
||||
// interface itself.
|
||||
peer.RPC.DisconnectPeer(node.PubKeyStr)
|
||||
peerEvent, err := resp2.Recv()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, lnrpc.PeerEvent_PEER_OFFLINE, peerEvent.GetType())
|
||||
|
||||
// Stop the peer stream again, otherwise we'll produce more events.
|
||||
peerCancel()
|
||||
|
||||
// Did we receive the correct intercept message?
|
||||
assertInterceptedType(t, peerEvent, <-registration.responsesChan)
|
||||
|
||||
// Make sure that with the other macaroon we aren't allowed to access
|
||||
// the interceptor. If we registered for read-only access then there is
|
||||
// no middleware that handles the custom macaroon caveat. If we
|
||||
// registered for a custom caveat then there is no middleware that
|
||||
// handles unencumbered read-only access.
|
||||
cleanup, client = macaroonClient(t, node, disallowedMac)
|
||||
defer cleanup()
|
||||
|
||||
// We need to make sure we don't get any interception messages for
|
||||
// requests made with the disallowed macaroon.
|
||||
var (
|
||||
errChan = make(chan error, 1)
|
||||
msgChan = make(chan *lnrpc.RPCMiddlewareRequest, 1)
|
||||
)
|
||||
go func() {
|
||||
req, err := registration.stream.Recv()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
msgChan <- req
|
||||
}()
|
||||
|
||||
// Let's invoke the same request again but with the other macaroon.
|
||||
resp, err = client.ListChannels(ctxc, req)
|
||||
|
||||
// Depending on what mode we're in, we expect something different. If we
|
||||
// are in read-only mode then an encumbered macaroon cannot be used
|
||||
// since there is no middleware registered for it. If we registered for
|
||||
// a custom macaroon caveat and a request with anon-encumbered macaroon
|
||||
// comes in, we expect to just not get any intercept messages.
|
||||
if readOnly {
|
||||
require.Error(t, err)
|
||||
require.Contains(
|
||||
t, err.Error(), "cannot accept macaroon with custom "+
|
||||
"caveat 'itest-caveat', no middleware "+
|
||||
"registered",
|
||||
)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// We disconnected Bob so there should be no active channels.
|
||||
require.Len(t, resp.Channels, 0)
|
||||
}
|
||||
|
||||
// There should be neither an error nor any interception messages in the
|
||||
// channels.
|
||||
select {
|
||||
case err := <-errChan:
|
||||
t.Fatalf("Unexpected error, not expecting messages: %v", err)
|
||||
|
||||
case msg := <-msgChan:
|
||||
t.Fatalf("Unexpected intercept message: %v", msg)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
// Nothing came in for a second, we're fine.
|
||||
}
|
||||
}
|
||||
|
||||
// middlewareResponseManipulationTest tests that unary and streaming responses
|
||||
// can be intercepted and also manipulated, at least if the middleware didn't
|
||||
// register for read-only access.
|
||||
func middlewareResponseManipulationTest(t *testing.T,
|
||||
node, peer *node.HarnessNode, registration *middlewareHarness,
|
||||
userMac *macaroon.Macaroon, readOnly bool) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
// Everything we test here should be executed in a matter of
|
||||
// milliseconds, so we can use one single timeout context for all calls.
|
||||
ctxb := context.Background()
|
||||
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a client connection that we'll use to simulate user requests
|
||||
// to lnd with.
|
||||
cleanup, client := macaroonClient(t, node, userMac)
|
||||
defer cleanup()
|
||||
|
||||
// We're going to attempt to replace the response with our own. But
|
||||
// since we only registered for read-only access, our replacement should
|
||||
// just be ignored.
|
||||
replacementResponse := &lnrpc.ListChannelsResponse{
|
||||
Channels: []*lnrpc.Channel{{
|
||||
ChannelPoint: "f000:0",
|
||||
}, {
|
||||
ChannelPoint: "f000:1",
|
||||
}},
|
||||
}
|
||||
|
||||
// We're going to send a simple RPC request to list all channels.
|
||||
// We need to invoke the intercept logic in a goroutine because we'd
|
||||
// block the execution of the main task otherwise.
|
||||
req := &lnrpc.ListChannelsRequest{ActiveOnly: true}
|
||||
go registration.interceptUnary(
|
||||
"/lnrpc.Lightning/ListChannels", req, replacementResponse,
|
||||
readOnly, false,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
resp, err := client.ListChannels(ctxc, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Did we get the manipulated response (2 fake channels) or the original
|
||||
// one (1 channel)?
|
||||
if readOnly {
|
||||
require.Len(t, resp.Channels, 1)
|
||||
} else {
|
||||
require.Len(t, resp.Channels, 2)
|
||||
}
|
||||
|
||||
// Let's see if we can also replace an error message sent out by lnd
|
||||
// with a custom one.
|
||||
betterError := fmt.Errorf("yo, this request is no good")
|
||||
invalidReq := &lnrpc.ListChannelsRequest{
|
||||
ActiveOnly: true,
|
||||
InactiveOnly: true,
|
||||
}
|
||||
go registration.interceptUnary(
|
||||
"/lnrpc.Lightning/ListChannels", invalidReq, betterError,
|
||||
readOnly, false,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
_, err = client.ListChannels(ctxc, invalidReq)
|
||||
require.Error(t, err)
|
||||
|
||||
// Did we get the manipulated error or the original one?
|
||||
if readOnly {
|
||||
require.Contains(
|
||||
t, err.Error(), "either `active_only` or "+
|
||||
"`inactive_only` can be set",
|
||||
)
|
||||
} else {
|
||||
require.Contains(t, err.Error(), betterError.Error())
|
||||
}
|
||||
|
||||
// Let's test the same for a streaming endpoint.
|
||||
replacementResponse2 := &lnrpc.PeerEvent{
|
||||
Type: lnrpc.PeerEvent_PEER_ONLINE,
|
||||
PubKey: "foo",
|
||||
}
|
||||
req2 := &lnrpc.PeerEventSubscription{}
|
||||
go registration.interceptStream(
|
||||
"/lnrpc.Lightning/SubscribePeerEvents", req2,
|
||||
replacementResponse2, readOnly,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
peerCtx, peerCancel := context.WithCancel(ctxb)
|
||||
resp2, err := client.SubscribePeerEvents(peerCtx, req2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Disconnect Bob to trigger a peer event without using Alice's RPC
|
||||
// interface itself.
|
||||
peer.RPC.DisconnectPeer(node.PubKeyStr)
|
||||
peerEvent, err := resp2.Recv()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Did we get the correct, original response?
|
||||
if readOnly {
|
||||
require.Equal(
|
||||
t, lnrpc.PeerEvent_PEER_OFFLINE, peerEvent.GetType(),
|
||||
)
|
||||
require.Equal(t, peer.PubKeyStr, peerEvent.PubKey)
|
||||
} else {
|
||||
require.Equal(
|
||||
t, lnrpc.PeerEvent_PEER_ONLINE, peerEvent.GetType(),
|
||||
)
|
||||
require.Equal(t, "foo", peerEvent.PubKey)
|
||||
}
|
||||
|
||||
// Stop the peer stream again, otherwise we'll produce more events.
|
||||
peerCancel()
|
||||
}
|
||||
|
||||
// middlewareRequestManipulationTest tests that unary and streaming requests
|
||||
// can be intercepted and also manipulated, at least if the middleware didn't
|
||||
// register for read-only access.
|
||||
func middlewareRequestManipulationTest(t *testing.T, node *node.HarnessNode,
|
||||
registration *middlewareHarness, userMac *macaroon.Macaroon,
|
||||
readOnly bool) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
// Everything we test here should be executed in a matter of
|
||||
// milliseconds, so we can use one single timeout context for all calls.
|
||||
ctxb := context.Background()
|
||||
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a client connection that we'll use to simulate user requests
|
||||
// to lnd with.
|
||||
cleanup, client := macaroonClient(t, node, userMac)
|
||||
defer cleanup()
|
||||
|
||||
// We're going to attempt to replace the request with our own. But since
|
||||
// we only registered for read-only access, our replacement should just
|
||||
// be ignored.
|
||||
replacementRequest := &lnrpc.Invoice{
|
||||
Memo: "This is the replaced memo",
|
||||
}
|
||||
|
||||
// We're going to send a simple RPC request to add an invoice. We need
|
||||
// to invoke the intercept logic in a goroutine because we'd block the
|
||||
// execution of the main task otherwise.
|
||||
req := &lnrpc.Invoice{
|
||||
Memo: "Plz pay me",
|
||||
Value: 123456,
|
||||
}
|
||||
go registration.interceptUnary(
|
||||
"/lnrpc.Lightning/AddInvoice", req, replacementRequest,
|
||||
readOnly, true,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
resp, err := client.AddInvoice(ctxc, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Did we get the manipulated response or the original one?
|
||||
invoice, err := zpay32.Decode(resp.PaymentRequest, harnessNetParams)
|
||||
require.NoError(t, err)
|
||||
if readOnly {
|
||||
require.Equal(t, req.Memo, *invoice.Description)
|
||||
} else {
|
||||
require.Equal(t, replacementRequest.Memo, *invoice.Description)
|
||||
}
|
||||
|
||||
// Read the message from the registration, otherwise we cannot check the
|
||||
// next one.
|
||||
<-registration.responsesChan
|
||||
|
||||
// Make sure we also get errors intercepted for stream events. We do
|
||||
// this in the request manipulation test because only here we have a
|
||||
// macaroon with sufficient permissions (admin macaroon) to attempt to
|
||||
// call a streaming RPC that actually accepts request parameters that we
|
||||
// can use to provoke an error message.
|
||||
expectedErr := "channel is too small"
|
||||
invalidReq := &lnrpc.OpenChannelRequest{
|
||||
LocalFundingAmount: 5,
|
||||
}
|
||||
go registration.interceptStream(
|
||||
"/lnrpc.Lightning/OpenChannel", invalidReq, nil, readOnly,
|
||||
)
|
||||
|
||||
// Do the actual call now and wait for the interceptor to do its thing.
|
||||
// We don't receive the error on the RPC call directly, because that
|
||||
// only returns the stream. We have to attempt to read a response first
|
||||
// to get the expected error.
|
||||
responseStream, err := client.OpenChannel(ctxc, invalidReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = responseStream.Recv()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr)
|
||||
|
||||
assertInterceptedErr(t, expectedErr, <-registration.responsesChan)
|
||||
}
|
||||
|
||||
// middlewareMandatoryTest tests that all RPC requests are blocked if there is
|
||||
// a mandatory middleware declared that's currently not registered.
|
||||
func middlewareMandatoryTest(ht *lntemp.HarnessTest, node *node.HarnessNode) {
|
||||
// Let's declare our itest interceptor as mandatory but don't register
|
||||
// it just yet. That should cause all RPC requests to fail, except for
|
||||
// the registration itself.
|
||||
node.Cfg.SkipUnlock = true
|
||||
ht.RestartNodeWithExtraArgs(node, []string{
|
||||
"--noseedbackup", "--rpcmiddleware.enable",
|
||||
"--rpcmiddleware.addmandatory=itest-interceptor",
|
||||
})
|
||||
|
||||
// The "wait for node to start" flag of the above restart does too much
|
||||
// and has a call to GetInfo built in, which will fail in this special
|
||||
// test case. So we need to do the wait and client setup manually here.
|
||||
conn, err := node.ConnectRPC()
|
||||
require.NoError(ht, err)
|
||||
node.Initialize(conn)
|
||||
err = node.WaitUntilServerActive()
|
||||
require.NoError(ht, err)
|
||||
|
||||
ctxb := context.Background()
|
||||
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Test a unary request first.
|
||||
_, err = node.RPC.LN.ListChannels(ctxc, &lnrpc.ListChannelsRequest{})
|
||||
require.Contains(ht, err.Error(), "middleware 'itest-interceptor' is "+
|
||||
"currently not registered")
|
||||
|
||||
// Then a streaming one.
|
||||
stream := node.RPC.SubscribeInvoices(&lnrpc.InvoiceSubscription{})
|
||||
_, err = stream.Recv()
|
||||
require.Error(ht, err)
|
||||
require.Contains(ht, err.Error(), "middleware 'itest-interceptor' is "+
|
||||
"currently not registered")
|
||||
|
||||
// Now let's register the middleware and try again.
|
||||
registration := registerMiddleware(
|
||||
ht.T, node, &lnrpc.MiddlewareRegistration{
|
||||
MiddlewareName: "itest-interceptor",
|
||||
CustomMacaroonCaveatName: "itest-caveat",
|
||||
}, true,
|
||||
)
|
||||
defer registration.cancel()
|
||||
|
||||
// Both the unary and streaming requests should now be allowed.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
node.RPC.ListChannels(&lnrpc.ListChannelsRequest{})
|
||||
node.RPC.SubscribeInvoices(&lnrpc.InvoiceSubscription{})
|
||||
|
||||
// We now shut down the node manually to prevent the test from failing
|
||||
// because we can't call the stop RPC if we unregister the middleware
|
||||
// in the defer statement above.
|
||||
ht.KillNode(node)
|
||||
}
|
||||
|
||||
// assertInterceptedType makes sure that the intercept message sent by the RPC
|
||||
// interceptor is correct for a proto message that was sent or received over the
|
||||
// RPC interface.
|
||||
func assertInterceptedType(t *testing.T, rpcMessage proto.Message,
|
||||
interceptMessage *lnrpc.RPCMessage) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
require.Equal(
|
||||
t, string(proto.MessageName(rpcMessage)),
|
||||
interceptMessage.TypeName,
|
||||
)
|
||||
rawRequest, err := proto.Marshal(rpcMessage)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make sure we don't trip over nil vs. empty slice in the equality
|
||||
// check below.
|
||||
if len(rawRequest) == 0 {
|
||||
rawRequest = nil
|
||||
}
|
||||
|
||||
require.Equal(t, rawRequest, interceptMessage.Serialized)
|
||||
}
|
||||
|
||||
// assertInterceptedErr makes sure that the intercept message sent by the RPC
|
||||
// interceptor contains an error message instead of a serialized proto message.
|
||||
func assertInterceptedErr(t *testing.T, errString string,
|
||||
interceptMessage *lnrpc.RPCMessage) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
require.Equal(t, "error", interceptMessage.TypeName)
|
||||
require.True(t, interceptMessage.IsError)
|
||||
|
||||
require.Contains(t, string(interceptMessage.Serialized), errString)
|
||||
}
|
||||
|
||||
// middlewareStream is a type alias to shorten the long definition.
|
||||
type middlewareStream lnrpc.Lightning_RegisterRPCMiddlewareClient
|
||||
|
||||
// middlewareHarness is a test harness that holds one instance of a simulated
|
||||
// middleware.
|
||||
type middlewareHarness struct {
|
||||
t *testing.T
|
||||
cancel func()
|
||||
stream middlewareStream
|
||||
|
||||
responsesChan chan *lnrpc.RPCMessage
|
||||
}
|
||||
|
||||
// registerMiddleware creates a new middleware harness and sends the initial
|
||||
// register message to the RPC server.
|
||||
func registerMiddleware(t *testing.T, node *node.HarnessNode,
|
||||
registration *lnrpc.MiddlewareRegistration,
|
||||
waitForRegister bool) *middlewareHarness {
|
||||
|
||||
t.Helper()
|
||||
|
||||
middlewareStream, cancel := node.RPC.RegisterRPCMiddleware()
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
msg := &lnrpc.RPCMiddlewareResponse_Register{
|
||||
Register: registration,
|
||||
}
|
||||
err := middlewareStream.Send(&lnrpc.RPCMiddlewareResponse{
|
||||
MiddlewareMessage: msg,
|
||||
})
|
||||
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(t, "registerMiddleware send timeout")
|
||||
case err := <-errChan:
|
||||
require.NoError(t, err, "registerMiddleware send failed")
|
||||
}
|
||||
|
||||
mh := &middlewareHarness{
|
||||
t: t,
|
||||
cancel: cancel,
|
||||
stream: middlewareStream,
|
||||
responsesChan: make(chan *lnrpc.RPCMessage),
|
||||
}
|
||||
|
||||
if !waitForRegister {
|
||||
return mh
|
||||
}
|
||||
|
||||
// Wait for the registration complete message.
|
||||
msg := make(chan *lnrpc.RPCMiddlewareRequest)
|
||||
go func() {
|
||||
regCompleteMsg, err := middlewareStream.Recv()
|
||||
require.NoError(t, err, "registerMiddleware recv failed")
|
||||
|
||||
msg <- regCompleteMsg
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(defaultTimeout):
|
||||
require.Fail(t, "registerMiddleware recv timeout")
|
||||
|
||||
case m := <-msg:
|
||||
require.True(t, m.GetRegComplete())
|
||||
}
|
||||
|
||||
return mh
|
||||
}
|
||||
|
||||
// interceptUnary intercepts a unary call, optionally requesting to replace the
|
||||
// response sent to the client. A unary call is expected to receive one
|
||||
// intercept message for the request and one for the response.
|
||||
//
|
||||
// NOTE: Must be called in a goroutine as this will block until the response is
|
||||
// read from the response channel.
|
||||
func (h *middlewareHarness) interceptUnary(methodURI string,
|
||||
expectedRequest proto.Message, responseReplacement interface{},
|
||||
readOnly bool, replaceRequest bool) {
|
||||
|
||||
// Read intercept message and make sure it's for an RPC request.
|
||||
reqIntercept, err := h.stream.Recv()
|
||||
require.NoError(h.t, err)
|
||||
|
||||
// Make sure the custom condition is populated correctly (if we're using
|
||||
// a macaroon with a custom condition).
|
||||
if !readOnly {
|
||||
require.Equal(
|
||||
h.t, "itest-value", reqIntercept.CustomCaveatCondition,
|
||||
)
|
||||
}
|
||||
|
||||
req := reqIntercept.GetRequest()
|
||||
require.NotNil(h.t, req)
|
||||
|
||||
// We know the request we're going to send so make sure we get the right
|
||||
// type and content from the interceptor.
|
||||
require.Equal(h.t, methodURI, req.MethodFullUri)
|
||||
assertInterceptedType(h.t, expectedRequest, req)
|
||||
|
||||
// We need to accept the request.
|
||||
if replaceRequest {
|
||||
h.sendAccept(reqIntercept.MsgId, responseReplacement)
|
||||
} else {
|
||||
h.sendAccept(reqIntercept.MsgId, nil)
|
||||
}
|
||||
|
||||
// Now read the intercept message for the response.
|
||||
respIntercept, err := h.stream.Recv()
|
||||
require.NoError(h.t, err)
|
||||
res := respIntercept.GetResponse()
|
||||
require.NotNil(h.t, res)
|
||||
|
||||
// We expect the request ID to be the same for the request intercept
|
||||
// and the response intercept messages. But the message IDs must be
|
||||
// different/unique.
|
||||
require.Equal(h.t, reqIntercept.RequestId, respIntercept.RequestId)
|
||||
require.NotEqual(h.t, reqIntercept.MsgId, respIntercept.MsgId)
|
||||
|
||||
// We need to accept the response as well.
|
||||
if replaceRequest {
|
||||
h.sendAccept(respIntercept.MsgId, nil)
|
||||
} else {
|
||||
h.sendAccept(respIntercept.MsgId, responseReplacement)
|
||||
}
|
||||
|
||||
h.responsesChan <- res
|
||||
}
|
||||
|
||||
// interceptStream intercepts a streaming call, optionally requesting to replace
|
||||
// the (first) response sent to the client. A streaming call is expected to
|
||||
// receive one intercept message for the stream authentication, one for the
|
||||
// first request and one for the first response.
|
||||
//
|
||||
// NOTE: Must be called in a goroutine as this will block until the first
|
||||
// response is read from the response channel.
|
||||
func (h *middlewareHarness) interceptStream(methodURI string,
|
||||
expectedRequest proto.Message, responseReplacement proto.Message,
|
||||
readOnly bool) {
|
||||
|
||||
// Read intercept message and make sure it's for an RPC stream auth.
|
||||
authIntercept, err := h.stream.Recv()
|
||||
require.NoError(h.t, err)
|
||||
|
||||
// Make sure the custom condition is populated correctly (if we're using
|
||||
// a macaroon with a custom condition).
|
||||
if !readOnly {
|
||||
require.Equal(
|
||||
h.t, "itest-value", authIntercept.CustomCaveatCondition,
|
||||
)
|
||||
}
|
||||
|
||||
auth := authIntercept.GetStreamAuth()
|
||||
require.NotNil(h.t, auth)
|
||||
|
||||
// This is just the authentication, so we can only look at the URI.
|
||||
require.Equal(h.t, methodURI, auth.MethodFullUri)
|
||||
|
||||
// We need to accept the auth.
|
||||
h.sendAccept(authIntercept.MsgId, nil)
|
||||
|
||||
// Read intercept message and make sure it's for an RPC request.
|
||||
reqIntercept, err := h.stream.Recv()
|
||||
require.NoError(h.t, err)
|
||||
req := reqIntercept.GetRequest()
|
||||
require.NotNil(h.t, req)
|
||||
|
||||
// We know the request we're going to send so make sure we get the right
|
||||
// type and content from the interceptor.
|
||||
require.Equal(h.t, methodURI, req.MethodFullUri)
|
||||
assertInterceptedType(h.t, expectedRequest, req)
|
||||
|
||||
// We need to accept the request.
|
||||
h.sendAccept(reqIntercept.MsgId, nil)
|
||||
|
||||
// Now read the intercept message for the response.
|
||||
respIntercept, err := h.stream.Recv()
|
||||
require.NoError(h.t, err)
|
||||
res := respIntercept.GetResponse()
|
||||
require.NotNil(h.t, res)
|
||||
|
||||
// We expect the request ID to be the same for the auth intercept,
|
||||
// request intercept and the response intercept messages. But the
|
||||
// message IDs must be different/unique.
|
||||
require.Equal(h.t, authIntercept.RequestId, respIntercept.RequestId)
|
||||
require.Equal(h.t, reqIntercept.RequestId, respIntercept.RequestId)
|
||||
require.NotEqual(h.t, authIntercept.MsgId, reqIntercept.MsgId)
|
||||
require.NotEqual(h.t, authIntercept.MsgId, respIntercept.MsgId)
|
||||
require.NotEqual(h.t, reqIntercept.MsgId, respIntercept.MsgId)
|
||||
|
||||
// We need to accept the response as well.
|
||||
h.sendAccept(respIntercept.MsgId, responseReplacement)
|
||||
|
||||
h.responsesChan <- res
|
||||
}
|
||||
|
||||
// sendAccept sends an accept feedback to the RPC server.
|
||||
func (h *middlewareHarness) sendAccept(msgID uint64,
|
||||
responseReplacement interface{}) {
|
||||
|
||||
var replacementBytes []byte
|
||||
if responseReplacement != nil {
|
||||
var err error
|
||||
|
||||
switch t := responseReplacement.(type) {
|
||||
case proto.Message:
|
||||
replacementBytes, err = proto.Marshal(t)
|
||||
require.NoError(h.t, err)
|
||||
|
||||
case error:
|
||||
replacementBytes = []byte(t.Error())
|
||||
|
||||
default:
|
||||
require.Fail(h.t, "invalid replacement type")
|
||||
}
|
||||
}
|
||||
|
||||
err := h.stream.Send(&lnrpc.RPCMiddlewareResponse{
|
||||
MiddlewareMessage: &lnrpc.RPCMiddlewareResponse_Feedback{
|
||||
Feedback: &lnrpc.InterceptFeedback{
|
||||
ReplaceResponse: len(replacementBytes) > 0,
|
||||
ReplacementSerialized: replacementBytes,
|
||||
},
|
||||
},
|
||||
RefMsgId: msgID,
|
||||
})
|
||||
require.NoError(h.t, err)
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testSendMultiPathPayment tests that we are able to successfully route a
|
||||
// payment using multiple shards across different paths.
|
||||
func testSendMultiPathPayment(ht *lntemp.HarnessTest) {
|
||||
mts := newMppTestScenario(ht)
|
||||
|
||||
const paymentAmt = btcutil.Amount(300000)
|
||||
|
||||
// Set up a network with three different paths Alice <-> Bob. Channel
|
||||
// capacities are set such that the payment can only succeed if (at
|
||||
// least) three paths are used.
|
||||
//
|
||||
// _ Eve _
|
||||
// / \
|
||||
// Alice -- Carol ---- Bob
|
||||
// \ /
|
||||
// \__ Dave ____/
|
||||
//
|
||||
req := &mppOpenChannelRequest{
|
||||
amtAliceCarol: 235000,
|
||||
amtAliceDave: 135000,
|
||||
amtCarolBob: 135000,
|
||||
amtCarolEve: 135000,
|
||||
amtDaveBob: 135000,
|
||||
amtEveBob: 135000,
|
||||
}
|
||||
mts.openChannels(req)
|
||||
chanPointAliceDave := mts.channelPoints[1]
|
||||
|
||||
// Increase Dave's fee to make the test deterministic. Otherwise it
|
||||
// would be unpredictable whether pathfinding would go through Charlie
|
||||
// or Dave for the first shard.
|
||||
expectedPolicy := mts.updateDaveGlobalPolicy()
|
||||
|
||||
// Make sure Alice has heard it.
|
||||
ht.AssertChannelPolicyUpdate(
|
||||
mts.alice, mts.dave, expectedPolicy, chanPointAliceDave, false,
|
||||
)
|
||||
|
||||
// Our first test will be Alice paying Bob using a SendPayment call.
|
||||
// Let Bob create an invoice for Alice to pay.
|
||||
payReqs, rHashes, invoices := ht.CreatePayReqs(mts.bob, paymentAmt, 1)
|
||||
|
||||
rHash := rHashes[0]
|
||||
payReq := payReqs[0]
|
||||
|
||||
sendReq := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
MaxParts: 10,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
payment := ht.SendPaymentAssertSettled(mts.alice, sendReq)
|
||||
|
||||
// Make sure we got the preimage.
|
||||
require.Equal(ht, hex.EncodeToString(invoices[0].RPreimage),
|
||||
payment.PaymentPreimage, "preimage doesn't match")
|
||||
|
||||
// Check that Alice split the payment in at least three shards. Because
|
||||
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
||||
// there is some non-determinism in the process. Depending on whether
|
||||
// the new pathfinding round is started before or after the htlc is
|
||||
// locked into the channel, different sharding may occur. Therefore we
|
||||
// can only check if the number of shards isn't below the theoretical
|
||||
// minimum.
|
||||
succeeded := 0
|
||||
for _, htlc := range payment.Htlcs {
|
||||
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
|
||||
const minExpectedShards = 3
|
||||
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
|
||||
"expected shards not reached")
|
||||
|
||||
// Make sure Bob show the invoice as settled for the full amount.
|
||||
inv := mts.bob.RPC.LookupInvoice(rHash)
|
||||
|
||||
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
|
||||
"incorrect payment amt")
|
||||
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
|
||||
"Invoice not settled")
|
||||
|
||||
settled := 0
|
||||
for _, htlc := range inv.Htlcs {
|
||||
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
|
||||
settled++
|
||||
}
|
||||
}
|
||||
require.Equal(ht, succeeded, settled,
|
||||
"num of HTLCs wrong")
|
||||
|
||||
// Finally, close all channels.
|
||||
mts.closeChannels()
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testDeriveSharedKey checks the ECDH performed by the endpoint
|
||||
// DeriveSharedKey. It creates an ephemeral private key, performing an ECDH with
|
||||
// the node's pubkey and a customized public key to check the validity of the
|
||||
// result.
|
||||
func testDeriveSharedKey(ht *lntemp.HarnessTest) {
|
||||
runDeriveSharedKey(ht, ht.Alice)
|
||||
}
|
||||
|
||||
// runDeriveSharedKey checks the ECDH performed by the endpoint
|
||||
// DeriveSharedKey. It creates an ephemeral private key, performing an ECDH with
|
||||
// the node's pubkey and a customized public key to check the validity of the
|
||||
// result.
|
||||
func runDeriveSharedKey(ht *lntemp.HarnessTest, alice *node.HarnessNode) {
|
||||
// Create an ephemeral key, extracts its public key, and make a
|
||||
// PrivKeyECDH using the ephemeral key.
|
||||
ephemeralPriv, err := btcec.NewPrivateKey()
|
||||
require.NoError(ht, err, "failed to create ephemeral key")
|
||||
|
||||
ephemeralPubBytes := ephemeralPriv.PubKey().SerializeCompressed()
|
||||
privKeyECDH := &keychain.PrivKeyECDH{PrivKey: ephemeralPriv}
|
||||
|
||||
// assertECDHMatch checks the correctness of the ECDH between the
|
||||
// ephemeral key and the given public key.
|
||||
assertECDHMatch := func(pub *btcec.PublicKey,
|
||||
req *signrpc.SharedKeyRequest) {
|
||||
|
||||
resp := alice.RPC.DeriveSharedKey(req)
|
||||
|
||||
sharedKey, _ := privKeyECDH.ECDH(pub)
|
||||
require.Equal(ht, sharedKey[:], resp.SharedKey,
|
||||
"failed to derive the expected key")
|
||||
}
|
||||
|
||||
nodePub, err := btcec.ParsePubKey(alice.PubKey[:])
|
||||
require.NoError(ht, err, "failed to parse node pubkey")
|
||||
|
||||
customizedKeyFamily := int32(keychain.KeyFamilyMultiSig)
|
||||
customizedIndex := int32(1)
|
||||
|
||||
// Derive a customized key.
|
||||
deriveReq := &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
KeyIndex: customizedIndex,
|
||||
}
|
||||
resp := alice.RPC.DeriveKey(deriveReq)
|
||||
customizedPub, err := btcec.ParsePubKey(resp.RawKeyBytes)
|
||||
require.NoError(ht, err, "failed to parse node pubkey")
|
||||
|
||||
// Test DeriveSharedKey with no optional arguments. It will result in
|
||||
// performing an ECDH between the ephemeral key and the node's pubkey.
|
||||
req := &signrpc.SharedKeyRequest{EphemeralPubkey: ephemeralPubBytes}
|
||||
assertECDHMatch(nodePub, req)
|
||||
|
||||
// Test DeriveSharedKey with a KeyLoc which points to the node's pubkey.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: int32(keychain.KeyFamilyNodeKey),
|
||||
KeyIndex: 0,
|
||||
},
|
||||
}
|
||||
assertECDHMatch(nodePub, req)
|
||||
|
||||
// Test DeriveSharedKey with a KeyLoc being set in KeyDesc. The KeyLoc
|
||||
// points to the node's pubkey.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: int32(keychain.KeyFamilyNodeKey),
|
||||
KeyIndex: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
assertECDHMatch(nodePub, req)
|
||||
|
||||
// Test DeriveSharedKey with RawKeyBytes set in KeyDesc. The RawKeyBytes
|
||||
// is the node's pubkey bytes, and the KeyFamily is KeyFamilyNodeKey.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: alice.PubKey[:],
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: int32(keychain.KeyFamilyNodeKey),
|
||||
},
|
||||
},
|
||||
}
|
||||
assertECDHMatch(nodePub, req)
|
||||
|
||||
// Test DeriveSharedKey with a KeyLoc which points to the customized
|
||||
// public key.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
KeyIndex: customizedIndex,
|
||||
},
|
||||
}
|
||||
assertECDHMatch(customizedPub, req)
|
||||
|
||||
// Test DeriveSharedKey with a KeyLoc being set in KeyDesc. The KeyLoc
|
||||
// points to the customized public key.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
KeyIndex: customizedIndex,
|
||||
},
|
||||
},
|
||||
}
|
||||
assertECDHMatch(customizedPub, req)
|
||||
|
||||
// Test DeriveSharedKey with RawKeyBytes set in KeyDesc. The RawKeyBytes
|
||||
// is the customized public key. The KeyLoc is also set with the family
|
||||
// being the customizedKeyFamily.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: customizedPub.SerializeCompressed(),
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
},
|
||||
},
|
||||
}
|
||||
assertECDHMatch(customizedPub, req)
|
||||
|
||||
// assertErrorMatch checks when calling DeriveSharedKey with invalid
|
||||
// params, the expected error is returned.
|
||||
assertErrorMatch := func(match string, req *signrpc.SharedKeyRequest) {
|
||||
err := alice.RPC.DeriveSharedKeyErr(req)
|
||||
require.Contains(ht, err.Error(), match, "error not match")
|
||||
}
|
||||
|
||||
// Test that EphemeralPubkey must be supplied.
|
||||
req = &signrpc.SharedKeyRequest{}
|
||||
assertErrorMatch("must provide ephemeral pubkey", req)
|
||||
|
||||
// Test that cannot use both KeyDesc and KeyLoc.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: customizedPub.SerializeCompressed(),
|
||||
},
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
KeyIndex: 0,
|
||||
},
|
||||
}
|
||||
assertErrorMatch("use either key_desc or key_loc", req)
|
||||
|
||||
// Test when KeyDesc is used, KeyLoc must be set.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: alice.PubKey[:],
|
||||
},
|
||||
}
|
||||
assertErrorMatch("key_desc.key_loc must also be set", req)
|
||||
|
||||
// Test that cannot use both RawKeyBytes and KeyIndex.
|
||||
req = &signrpc.SharedKeyRequest{
|
||||
EphemeralPubkey: ephemeralPubBytes,
|
||||
KeyDesc: &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: customizedPub.SerializeCompressed(),
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: customizedKeyFamily,
|
||||
KeyIndex: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
assertErrorMatch("use either raw_key_bytes or key_index", req)
|
||||
}
|
||||
|
||||
// testSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all
|
||||
// custom ways of specifying the signing key in the key descriptor/locator.
|
||||
func testSignOutputRaw(ht *lntemp.HarnessTest) {
|
||||
runSignOutputRaw(ht, ht.Alice)
|
||||
}
|
||||
|
||||
// runSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all
|
||||
// custom ways of specifying the signing key in the key descriptor/locator.
|
||||
func runSignOutputRaw(ht *lntemp.HarnessTest, alice *node.HarnessNode) {
|
||||
// For the next step, we need a public key. Let's use a special family
|
||||
// for this. We want this to be an index of zero.
|
||||
const testCustomKeyFamily = 44
|
||||
req := &walletrpc.KeyReq{
|
||||
KeyFamily: testCustomKeyFamily,
|
||||
}
|
||||
keyDesc := alice.RPC.DeriveNextKey(req)
|
||||
require.Equal(ht, int32(0), keyDesc.KeyLoc.KeyIndex)
|
||||
|
||||
targetPubKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// First, try with a key descriptor that only sets the public key.
|
||||
assertSignOutputRaw(
|
||||
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: keyDesc.RawKeyBytes,
|
||||
}, txscript.SigHashAll,
|
||||
)
|
||||
|
||||
// Now try again, this time only with the (0 index!) key locator.
|
||||
assertSignOutputRaw(
|
||||
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: keyDesc.KeyLoc.KeyFamily,
|
||||
KeyIndex: keyDesc.KeyLoc.KeyIndex,
|
||||
},
|
||||
}, txscript.SigHashAll,
|
||||
)
|
||||
|
||||
// And now test everything again with a new key where we know the index
|
||||
// is not 0.
|
||||
req = &walletrpc.KeyReq{
|
||||
KeyFamily: testCustomKeyFamily,
|
||||
}
|
||||
keyDesc = alice.RPC.DeriveNextKey(req)
|
||||
require.Equal(ht, int32(1), keyDesc.KeyLoc.KeyIndex)
|
||||
|
||||
targetPubKey, err = btcec.ParsePubKey(keyDesc.RawKeyBytes)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// First, try with a key descriptor that only sets the public key.
|
||||
assertSignOutputRaw(
|
||||
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
|
||||
RawKeyBytes: keyDesc.RawKeyBytes,
|
||||
}, txscript.SigHashAll,
|
||||
)
|
||||
|
||||
// Now try again, this time only with the key locator.
|
||||
assertSignOutputRaw(
|
||||
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: keyDesc.KeyLoc.KeyFamily,
|
||||
KeyIndex: keyDesc.KeyLoc.KeyIndex,
|
||||
},
|
||||
}, txscript.SigHashAll,
|
||||
)
|
||||
|
||||
// Finally, we'll try again, but this time with a non-default sighash.
|
||||
assertSignOutputRaw(
|
||||
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
|
||||
KeyLoc: &signrpc.KeyLocator{
|
||||
KeyFamily: keyDesc.KeyLoc.KeyFamily,
|
||||
KeyIndex: keyDesc.KeyLoc.KeyIndex,
|
||||
},
|
||||
}, txscript.SigHashSingle,
|
||||
)
|
||||
}
|
||||
|
||||
// assertSignOutputRaw sends coins to a p2wkh address derived from the given
|
||||
// target public key and then tries to spend that output again by invoking the
|
||||
// SignOutputRaw RPC with the key descriptor provided.
|
||||
func assertSignOutputRaw(ht *lntemp.HarnessTest,
|
||||
alice *node.HarnessNode, targetPubKey *btcec.PublicKey,
|
||||
keyDesc *signrpc.KeyDescriptor,
|
||||
sigHash txscript.SigHashType) {
|
||||
|
||||
pubKeyHash := btcutil.Hash160(targetPubKey.SerializeCompressed())
|
||||
targetAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
pubKeyHash, harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
targetScript, err := txscript.PayToAddrScript(targetAddr)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Send some coins to the generated p2wpkh address.
|
||||
req := &lnrpc.SendCoinsRequest{
|
||||
Addr: targetAddr.String(),
|
||||
Amount: 800_000,
|
||||
}
|
||||
alice.RPC.SendCoins(req)
|
||||
|
||||
// Wait until the TX is found in the mempool.
|
||||
txid := ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
|
||||
targetOutputIndex := ht.GetOutputIndex(txid, targetAddr.String())
|
||||
|
||||
// Clear the mempool.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Try to spend the output now to a new p2wkh address.
|
||||
addrReq := &lnrpc.NewAddressRequest{Type: AddrTypeWitnessPubkeyHash}
|
||||
p2wkhResp := alice.RPC.NewAddress(addrReq)
|
||||
|
||||
p2wkhAdrr, err := btcutil.DecodeAddress(
|
||||
p2wkhResp.Address, harnessNetParams,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
|
||||
p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAdrr)
|
||||
require.NoError(ht, err)
|
||||
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.TxIn = []*wire.TxIn{{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *txid,
|
||||
Index: uint32(targetOutputIndex),
|
||||
},
|
||||
}}
|
||||
value := int64(800_000 - 200)
|
||||
tx.TxOut = []*wire.TxOut{{
|
||||
PkScript: p2wkhPkScript,
|
||||
Value: value,
|
||||
}}
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(ht, tx.Serialize(&buf))
|
||||
|
||||
signReq := &signrpc.SignReq{
|
||||
RawTxBytes: buf.Bytes(),
|
||||
SignDescs: []*signrpc.SignDescriptor{{
|
||||
Output: &signrpc.TxOut{
|
||||
PkScript: targetScript,
|
||||
Value: 800_000,
|
||||
},
|
||||
InputIndex: 0,
|
||||
KeyDesc: keyDesc,
|
||||
Sighash: uint32(sigHash),
|
||||
WitnessScript: targetScript,
|
||||
}},
|
||||
}
|
||||
signResp := alice.RPC.SignOutputRaw(signReq)
|
||||
|
||||
tx.TxIn[0].Witness = wire.TxWitness{
|
||||
append(signResp.RawSigs[0], byte(sigHash)),
|
||||
targetPubKey.SerializeCompressed(),
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
require.NoError(ht, tx.Serialize(&buf))
|
||||
|
||||
alice.RPC.PublishTransaction(&walletrpc.Transaction{
|
||||
TxHex: buf.Bytes(),
|
||||
})
|
||||
|
||||
// Wait until the spending tx is found.
|
||||
txid = ht.Miner.AssertNumTxsInMempool(1)[0]
|
||||
p2wkhOutputIndex := ht.GetOutputIndex(txid, p2wkhAdrr.String())
|
||||
|
||||
op := &lnrpc.OutPoint{
|
||||
TxidBytes: txid[:],
|
||||
OutputIndex: uint32(p2wkhOutputIndex),
|
||||
}
|
||||
ht.AssertUTXOInWallet(alice, op, "")
|
||||
|
||||
// Mine another block to clean up the mempool and to make sure the
|
||||
// spend tx is actually included in a block.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
}
|
||||
|
||||
// testSignVerifyMessage makes sure that the SignMessage RPC can be used with
|
||||
// all custom flags by verifying with VerifyMessage. Tests both ECDSA and
|
||||
// Schnorr signatures.
|
||||
func testSignVerifyMessage(ht *lntemp.HarnessTest) {
|
||||
runSignVerifyMessage(ht, ht.Alice)
|
||||
}
|
||||
|
||||
// runSignVerifyMessage makes sure that the SignMessage RPC can be used with
|
||||
// all custom flags by verifying with VerifyMessage. Tests both ECDSA and
|
||||
// Schnorr signatures.
|
||||
func runSignVerifyMessage(ht *lntemp.HarnessTest, alice *node.HarnessNode) {
|
||||
aliceMsg := []byte("alice msg")
|
||||
keyLoc := &signrpc.KeyLocator{
|
||||
KeyFamily: int32(keychain.KeyFamilyNodeKey),
|
||||
KeyIndex: 1,
|
||||
}
|
||||
|
||||
// Sign a message with the default ECDSA.
|
||||
signMsgReq := &signrpc.SignMessageReq{
|
||||
Msg: aliceMsg,
|
||||
KeyLoc: keyLoc,
|
||||
SchnorrSig: false,
|
||||
}
|
||||
|
||||
signMsgResp := alice.RPC.SignMessageSigner(signMsgReq)
|
||||
|
||||
deriveCustomizedKey := func() *btcec.PublicKey {
|
||||
resp := alice.RPC.DeriveKey(keyLoc)
|
||||
pub, err := btcec.ParsePubKey(resp.RawKeyBytes)
|
||||
require.NoError(ht, err, "failed to parse node pubkey")
|
||||
|
||||
return pub
|
||||
}
|
||||
|
||||
customPubKey := deriveCustomizedKey()
|
||||
|
||||
verifyReq := &signrpc.VerifyMessageReq{
|
||||
Msg: aliceMsg,
|
||||
Signature: signMsgResp.Signature,
|
||||
Pubkey: customPubKey.SerializeCompressed(),
|
||||
IsSchnorrSig: false,
|
||||
}
|
||||
verifyResp := alice.RPC.VerifyMessageSigner(verifyReq)
|
||||
require.True(ht, verifyResp.Valid, "failed to verify message")
|
||||
|
||||
// Use a different key locator.
|
||||
keyLoc = &signrpc.KeyLocator{
|
||||
KeyFamily: int32(keychain.KeyFamilyNodeKey),
|
||||
KeyIndex: 2,
|
||||
}
|
||||
|
||||
// Sign a message with Schnorr signature.
|
||||
signMsgReq = &signrpc.SignMessageReq{
|
||||
Msg: aliceMsg,
|
||||
KeyLoc: keyLoc,
|
||||
SchnorrSig: true,
|
||||
}
|
||||
signMsgResp = alice.RPC.SignMessageSigner(signMsgReq)
|
||||
customPubKey = deriveCustomizedKey()
|
||||
|
||||
// Verify the Schnorr signature.
|
||||
verifyReq = &signrpc.VerifyMessageReq{
|
||||
Msg: aliceMsg,
|
||||
Signature: signMsgResp.Signature,
|
||||
Pubkey: schnorr.SerializePubKey(customPubKey),
|
||||
IsSchnorrSig: true,
|
||||
}
|
||||
verifyResp = alice.RPC.VerifyMessageSigner(verifyReq)
|
||||
require.True(ht, verifyResp.Valid, "failed to verify message")
|
||||
|
||||
// Also test that we can tweak a private key and verify the message
|
||||
// against the tweaked public key.
|
||||
tweakBytes := sha256.Sum256([]byte("some text"))
|
||||
tweakedPubKey := txscript.ComputeTaprootOutputKey(
|
||||
customPubKey, tweakBytes[:],
|
||||
)
|
||||
|
||||
signMsgReq.SchnorrSigTapTweak = tweakBytes[:]
|
||||
signMsgResp = alice.RPC.SignMessageSigner(signMsgReq)
|
||||
|
||||
verifyReq = &signrpc.VerifyMessageReq{
|
||||
Msg: aliceMsg,
|
||||
Signature: signMsgResp.Signature,
|
||||
Pubkey: schnorr.SerializePubKey(tweakedPubKey),
|
||||
IsSchnorrSig: true,
|
||||
}
|
||||
verifyResp = alice.RPC.VerifyMessageSigner(verifyReq)
|
||||
require.True(ht, verifyResp.Valid, "failed to verify message")
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testSingleHopInvoice(ht *lntemp.HarnessTest) {
|
||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||
// being the sole funder of the channel.
|
||||
chanAmt := btcutil.Amount(100000)
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
cp := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// assertAmountPaid is a helper closure that asserts the amount paid by
|
||||
// Alice and received by Bob are expected.
|
||||
assertAmountPaid := func(expected int64) {
|
||||
ht.AssertAmountPaid("alice -> bob", alice, cp, expected, 0)
|
||||
ht.AssertAmountPaid("bob <- alice", bob, cp, 0, expected)
|
||||
}
|
||||
|
||||
// Now that the channel is open, create an invoice for Bob which
|
||||
// expects a payment of 1000 satoshis from Alice paid via a particular
|
||||
// preimage.
|
||||
const paymentAmt = 1000
|
||||
preimage := bytes.Repeat([]byte("A"), 32)
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
RPreimage: preimage,
|
||||
Value: paymentAmt,
|
||||
}
|
||||
invoiceResp := bob.RPC.AddInvoice(invoice)
|
||||
|
||||
// With the invoice for Bob added, send a payment towards Alice paying
|
||||
// to the above generated invoice.
|
||||
ht.CompletePaymentRequests(alice, []string{invoiceResp.PaymentRequest})
|
||||
|
||||
// Bob's invoice should now be found and marked as settled.
|
||||
dbInvoice := bob.RPC.LookupInvoice(invoiceResp.RHash)
|
||||
require.Equal(ht, lnrpc.Invoice_SETTLED, dbInvoice.State,
|
||||
"bob's invoice should be marked as settled")
|
||||
|
||||
// With the payment completed all balance related stats should be
|
||||
// properly updated.
|
||||
assertAmountPaid(paymentAmt)
|
||||
|
||||
// Create another invoice for Bob, this time leaving off the preimage
|
||||
// to one will be randomly generated. We'll test the proper
|
||||
// encoding/decoding of the zpay32 payment requests.
|
||||
invoice = &lnrpc.Invoice{
|
||||
Memo: "test3",
|
||||
Value: paymentAmt,
|
||||
}
|
||||
invoiceResp = bob.RPC.AddInvoice(invoice)
|
||||
|
||||
// Next send another payment, but this time using a zpay32 encoded
|
||||
// invoice rather than manually specifying the payment details.
|
||||
ht.CompletePaymentRequests(alice, []string{invoiceResp.PaymentRequest})
|
||||
|
||||
// The second payment should also have succeeded, with the balances
|
||||
// being update accordingly.
|
||||
assertAmountPaid(paymentAmt * 2)
|
||||
|
||||
// Next send a keysend payment.
|
||||
keySendPreimage := lntypes.Preimage{3, 4, 5, 11}
|
||||
keySendHash := keySendPreimage.Hash()
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
Dest: bob.PubKey[:],
|
||||
Amt: paymentAmt,
|
||||
FinalCltvDelta: 40,
|
||||
PaymentHash: keySendHash[:],
|
||||
DestCustomRecords: map[uint64][]byte{
|
||||
record.KeySendType: keySendPreimage[:],
|
||||
},
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
}
|
||||
ht.SendPaymentAssertSettled(alice, req)
|
||||
|
||||
// The keysend payment should also have succeeded, with the balances
|
||||
// being update accordingly.
|
||||
assertAmountPaid(paymentAmt * 3)
|
||||
|
||||
// Assert that the invoice has the proper AMP fields set, since the
|
||||
// legacy keysend payment should have been promoted into an AMP payment
|
||||
// internally.
|
||||
keysendInvoice := bob.RPC.LookupInvoice(keySendHash[:])
|
||||
require.Len(ht, keysendInvoice.Htlcs, 1)
|
||||
htlc := keysendInvoice.Htlcs[0]
|
||||
require.Zero(ht, htlc.MppTotalAmtMsat)
|
||||
require.Nil(ht, htlc.Amp)
|
||||
|
||||
// Now create an invoice and specify routing hints.
|
||||
// We will test that the routing hints are encoded properly.
|
||||
hintChannel := lnwire.ShortChannelID{BlockHeight: 10}
|
||||
bobPubKey := hex.EncodeToString(bob.PubKey[:])
|
||||
hint := &lnrpc.HopHint{
|
||||
NodeId: bobPubKey,
|
||||
ChanId: hintChannel.ToUint64(),
|
||||
FeeBaseMsat: 1,
|
||||
FeeProportionalMillionths: 1000000,
|
||||
CltvExpiryDelta: 20,
|
||||
}
|
||||
hints := []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{hint}}}
|
||||
|
||||
invoice = &lnrpc.Invoice{
|
||||
Memo: "hints",
|
||||
Value: paymentAmt,
|
||||
RouteHints: hints,
|
||||
}
|
||||
|
||||
invoiceResp = bob.RPC.AddInvoice(invoice)
|
||||
payreq := bob.RPC.DecodePayReq(invoiceResp.PaymentRequest)
|
||||
require.Len(ht, payreq.RouteHints, 1, "expected one routing hint")
|
||||
routingHint := payreq.RouteHints[0]
|
||||
require.Len(ht, routingHint.HopHints, 1, "expected one hop hint")
|
||||
|
||||
hopHint := routingHint.HopHints[0]
|
||||
require.EqualValues(ht, 1000000, hopHint.FeeProportionalMillionths,
|
||||
"wrong FeeProportionalMillionths")
|
||||
require.Equal(ht, bobPubKey, hopHint.NodeId, "wrong NodeId")
|
||||
require.Equal(ht, hintChannel.ToUint64(), hopHint.ChanId,
|
||||
"wrong ChanId")
|
||||
require.EqualValues(ht, 1, hopHint.FeeBaseMsat, "wrong FeeBaseMsat")
|
||||
require.EqualValues(ht, 20, hopHint.CltvExpiryDelta,
|
||||
"wrong CltvExpiryDelta")
|
||||
|
||||
ht.CloseChannel(alice, cp)
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
numPayments = 5
|
||||
paymentAmt = 1000
|
||||
baseFee = 1
|
||||
)
|
||||
|
||||
// testSwitchCircuitPersistence creates a multihop network to ensure the sender
|
||||
// and intermediaries are persisting their open payment circuits. After
|
||||
// forwarding a packet via an outgoing link, all are restarted, and expected to
|
||||
// forward a response back from the receiver once back online.
|
||||
//
|
||||
// The general flow of this test:
|
||||
// 1. Carol --> Dave --> Alice --> Bob forward payment
|
||||
// 2. -------X X X Bob restart sender and intermediaries
|
||||
// 3. Carol <-- Dave <-- Alice <-- Bob expect settle to propagate
|
||||
//
|
||||
//nolint:dupword
|
||||
func testSwitchCircuitPersistence(ht *lntemp.HarnessTest) {
|
||||
// Setup our test scenario. We should now have four nodes running with
|
||||
// three channels.
|
||||
s := setupScenarioFourNodes(ht)
|
||||
defer s.cleanUp()
|
||||
|
||||
// Restart the intermediaries and the sender.
|
||||
ht.RestartNode(s.dave)
|
||||
ht.RestartNode(s.alice)
|
||||
ht.RestartNode(s.bob)
|
||||
|
||||
// Ensure all of the intermediate links are reconnected.
|
||||
ht.EnsureConnected(s.alice, s.dave)
|
||||
ht.EnsureConnected(s.bob, s.alice)
|
||||
|
||||
// Ensure all nodes in the network still have 5 outstanding htlcs.
|
||||
s.assertHTLCs(ht, numPayments)
|
||||
|
||||
// Now restart carol without hodl mode, to settle back the outstanding
|
||||
// payments.
|
||||
s.carol.SetExtraArgs(nil)
|
||||
ht.RestartNode(s.carol)
|
||||
|
||||
ht.EnsureConnected(s.dave, s.carol)
|
||||
|
||||
// After the payments settle, there should be no active htlcs on any of
|
||||
// the nodes in the network.
|
||||
s.assertHTLCs(ht, 0)
|
||||
|
||||
// When asserting the amount of satoshis moved, we'll factor in the
|
||||
// default base fee, as we didn't modify the fee structure when
|
||||
// creating the seed nodes in the network.
|
||||
|
||||
// At this point all the channels within our proto network should be
|
||||
// shifted by 5k satoshis in the direction of Carol, the sink within
|
||||
// the payment flow generated above. The order of asserts corresponds
|
||||
// to increasing of time is needed to embed the HTLC in commitment
|
||||
// transaction, in channel Bob->Alice->David->Carol, order is Carol,
|
||||
// David, Alice, Bob.
|
||||
var amountPaid = int64(5000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments)
|
||||
|
||||
// Lastly, we will send one more payment to ensure all channels are
|
||||
// still functioning properly.
|
||||
finalInvoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
Value: paymentAmt,
|
||||
}
|
||||
resp := s.carol.RPC.AddInvoice(finalInvoice)
|
||||
payReqs := []string{resp.PaymentRequest}
|
||||
|
||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||
// above.
|
||||
ht.CompletePaymentRequests(s.bob, payReqs)
|
||||
|
||||
amountPaid = int64(6000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments+1)
|
||||
}
|
||||
|
||||
// testSwitchOfflineDelivery constructs a set of multihop payments, and tests
|
||||
// that the returning payments are not lost if a peer on the backwards path is
|
||||
// offline when the settle/fails are received. We expect the payments to be
|
||||
// buffered in memory, and transmitted as soon as the disconnect link comes back
|
||||
// online.
|
||||
//
|
||||
// The general flow of this test:
|
||||
// 1. Carol --> Dave --> Alice --> Bob forward payment
|
||||
// 2. Carol --- Dave X Alice --- Bob disconnect intermediaries
|
||||
// 3. Carol --- Dave X Alice <-- Bob settle last hop
|
||||
// 4. Carol <-- Dave <-- Alice --- Bob reconnect, expect settle to propagate
|
||||
func testSwitchOfflineDelivery(ht *lntemp.HarnessTest) {
|
||||
// Setup our test scenario. We should now have four nodes running with
|
||||
// three channels.
|
||||
s := setupScenarioFourNodes(ht)
|
||||
defer s.cleanUp()
|
||||
|
||||
// First, disconnect Dave and Alice so that their link is broken.
|
||||
ht.DisconnectNodes(s.dave, s.alice)
|
||||
|
||||
// Then, reconnect them to ensure Dave doesn't just fail back the htlc.
|
||||
ht.ConnectNodes(s.dave, s.alice)
|
||||
|
||||
// Wait to ensure that the payment remain are not failed back after
|
||||
// reconnecting. All node should report the number payments initiated
|
||||
// for the duration of the interval.
|
||||
s.assertHTLCs(ht, numPayments)
|
||||
|
||||
// Now, disconnect Dave from Alice again before settling back the
|
||||
// payment.
|
||||
ht.DisconnectNodes(s.dave, s.alice)
|
||||
|
||||
// Now restart carol without hodl mode, to settle back the outstanding
|
||||
// payments.
|
||||
s.carol.SetExtraArgs(nil)
|
||||
ht.RestartNode(s.carol)
|
||||
|
||||
// Wait for Carol to report no outstanding htlcs.
|
||||
ht.AssertNumActiveHtlcs(s.carol, 0)
|
||||
|
||||
// Now that the settles have reached Dave, reconnect him with Alice,
|
||||
// allowing the settles to return to the sender.
|
||||
ht.EnsureConnected(s.dave, s.alice)
|
||||
|
||||
// Wait until all outstanding htlcs in the network have been settled.
|
||||
s.assertHTLCs(ht, 0)
|
||||
|
||||
// When asserting the amount of satoshis moved, we'll factor in the
|
||||
// default base fee, as we didn't modify the fee structure when
|
||||
// creating the seed nodes in the network.
|
||||
const baseFee = 1
|
||||
|
||||
// At this point all the channels within our proto network should be
|
||||
// shifted by 5k satoshis in the direction of Carol, the sink within the
|
||||
// payment flow generated above. The order of asserts corresponds to
|
||||
// increasing of time is needed to embed the HTLC in commitment
|
||||
// transaction, in channel Bob->Alice->David->Carol, order is Carol,
|
||||
// David, Alice, Bob.
|
||||
var amountPaid = int64(5000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments)
|
||||
|
||||
// Lastly, we will send one more payment to ensure all channels are
|
||||
// still functioning properly.
|
||||
finalInvoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
Value: paymentAmt,
|
||||
}
|
||||
resp := s.carol.RPC.AddInvoice(finalInvoice)
|
||||
payReqs := []string{resp.PaymentRequest}
|
||||
|
||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||
// above.
|
||||
ht.CompletePaymentRequests(s.bob, payReqs)
|
||||
|
||||
amountPaid = int64(6000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments+1)
|
||||
}
|
||||
|
||||
// testSwitchOfflineDeliveryPersistence constructs a set of multihop payments,
|
||||
// and tests that the returning payments are not lost if a peer on the backwards
|
||||
// path is offline when the settle/fails are received AND the peer buffering the
|
||||
// responses is completely restarts. We expect the payments to be reloaded from
|
||||
// disk, and transmitted as soon as the intermediaries are reconnected.
|
||||
//
|
||||
// The general flow of this test:
|
||||
// 1. Carol --> Dave --> Alice --> Bob forward payment
|
||||
// 2. Carol --- Dave X Alice --- Bob disconnect intermediaries
|
||||
// 3. Carol --- Dave X Alice <-- Bob settle last hop
|
||||
// 4. Carol --- Dave X X Bob restart Alice
|
||||
// 5. Carol <-- Dave <-- Alice --- Bob expect settle to propagate
|
||||
func testSwitchOfflineDeliveryPersistence(ht *lntemp.HarnessTest) {
|
||||
// Setup our test scenario. We should now have four nodes running with
|
||||
// three channels.
|
||||
s := setupScenarioFourNodes(ht)
|
||||
defer s.cleanUp()
|
||||
|
||||
// Disconnect the two intermediaries, Alice and Dave, by shutting down
|
||||
// Alice.
|
||||
restartAlice := ht.SuspendNode(s.alice)
|
||||
|
||||
// Now restart carol without hodl mode, to settle back the outstanding
|
||||
// payments.
|
||||
s.carol.SetExtraArgs(nil)
|
||||
ht.RestartNode(s.carol)
|
||||
|
||||
// Make Carol and Dave are reconnected before waiting for the htlcs to
|
||||
// clear.
|
||||
ht.EnsureConnected(s.dave, s.carol)
|
||||
|
||||
// Wait for Carol to report no outstanding htlcs, and also for Dave to
|
||||
// receive all the settles from Carol.
|
||||
ht.AssertNumActiveHtlcs(s.carol, 0)
|
||||
// As an intermediate node, Dave should now have zero outgoing HTLCs
|
||||
// and 5 incoming HTLCs from Alice.
|
||||
ht.AssertNumActiveHtlcs(s.dave, numPayments)
|
||||
|
||||
// Finally, restart dave who received the settles, but was unable to
|
||||
// deliver them to Alice since they were disconnected.
|
||||
ht.RestartNode(s.dave)
|
||||
require.NoError(ht, restartAlice(), "restart alice failed")
|
||||
|
||||
// Force Dave and Alice to reconnect before waiting for the htlcs to
|
||||
// clear.
|
||||
ht.EnsureConnected(s.dave, s.alice)
|
||||
|
||||
// After reconnection succeeds, the settles should be propagated all
|
||||
// the way back to the sender. All nodes should report no active htlcs.
|
||||
s.assertHTLCs(ht, 0)
|
||||
|
||||
// When asserting the amount of satoshis moved, we'll factor in the
|
||||
// default base fee, as we didn't modify the fee structure when
|
||||
// creating the seed nodes in the network.
|
||||
|
||||
// At this point all the channels within our proto network should be
|
||||
// shifted by 5k satoshis in the direction of Carol, the sink within the
|
||||
// payment flow generated above. The order of asserts corresponds to
|
||||
// increasing of time is needed to embed the HTLC in commitment
|
||||
// transaction, in channel Bob->Alice->David->Carol, order is Carol,
|
||||
// David, Alice, Bob.
|
||||
var amountPaid = int64(5000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments)
|
||||
|
||||
// Lastly, we will send one more payment to ensure all channels are
|
||||
// still functioning properly.
|
||||
finalInvoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
Value: paymentAmt,
|
||||
}
|
||||
resp := s.carol.RPC.AddInvoice(finalInvoice)
|
||||
payReqs := []string{resp.PaymentRequest}
|
||||
|
||||
// Before completing the final payment request, ensure that the
|
||||
// connection between Dave and Carol has been healed.
|
||||
ht.EnsureConnected(s.dave, s.carol)
|
||||
|
||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||
// above.
|
||||
ht.CompletePaymentRequests(s.bob, payReqs)
|
||||
|
||||
amountPaid = int64(6000)
|
||||
s.assertAmoutPaid(ht, amountPaid, numPayments+1)
|
||||
}
|
||||
|
||||
// testSwitchOfflineDeliveryOutgoingOffline constructs a set of multihop
|
||||
// payments, and tests that the returning payments are not lost if a peer on
|
||||
// the backwards path is offline when the settle/fails are received AND the
|
||||
// peer buffering the responses is completely restarts. We expect the payments
|
||||
// to be reloaded from disk, and transmitted as soon as the intermediaries are
|
||||
// reconnected.
|
||||
//
|
||||
// The general flow of this test:
|
||||
// 1. Carol --> Dave --> Alice --> Bob forward payment
|
||||
// 2. Carol --- Dave X Alice --- Bob disconnect intermediaries
|
||||
// 3. Carol --- Dave X Alice <-- Bob settle last hop
|
||||
// 4. Carol --- Dave X X shutdown Bob, restart Alice
|
||||
// 5. Carol <-- Dave <-- Alice X expect settle to propagate
|
||||
func testSwitchOfflineDeliveryOutgoingOffline(ht *lntemp.HarnessTest) {
|
||||
// Setup our test scenario. We should now have four nodes running with
|
||||
// three channels. Note that we won't call the cleanUp function here as
|
||||
// we will manually stop the node Carol and her channel.
|
||||
s := setupScenarioFourNodes(ht)
|
||||
|
||||
// Disconnect the two intermediaries, Alice and Dave, so that when carol
|
||||
// restarts, the response will be held by Dave.
|
||||
restartAlice := ht.SuspendNode(s.alice)
|
||||
|
||||
// Now restart carol without hodl mode, to settle back the outstanding
|
||||
// payments.
|
||||
s.carol.SetExtraArgs(nil)
|
||||
ht.RestartNode(s.carol)
|
||||
|
||||
// Wait for Carol to report no outstanding htlcs.
|
||||
ht.AssertNumActiveHtlcs(s.carol, 0)
|
||||
// As an intermediate node, Dave should now have zero outgoing HTLCs
|
||||
// and 5 incoming HTLCs from Alice.
|
||||
ht.AssertNumActiveHtlcs(s.dave, numPayments)
|
||||
|
||||
// Now check that the total amount was transferred from Dave to Carol.
|
||||
// The amount transferred should be exactly equal to the invoice total
|
||||
// payment amount, 5k satsohis.
|
||||
const amountPaid = int64(5000)
|
||||
ht.AssertAmountPaid(
|
||||
"Dave(local) => Carol(remote)", s.carol,
|
||||
s.chanPointCarolDave, int64(0), amountPaid,
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Dave(local) => Carol(remote)", s.dave,
|
||||
s.chanPointCarolDave, amountPaid, int64(0),
|
||||
)
|
||||
|
||||
// Shutdown carol and leave her offline for the rest of the test. This
|
||||
// is critical, as we wish to see if Dave can propragate settles even if
|
||||
// the outgoing link is never revived.
|
||||
ht.Shutdown(s.carol)
|
||||
|
||||
// Now restart Dave, ensuring he is both persisting the settles, and is
|
||||
// able to reforward them to Alice after recovering from a restart.
|
||||
ht.RestartNode(s.dave)
|
||||
require.NoErrorf(ht, restartAlice(), "restart alice failed")
|
||||
|
||||
// Ensure that Dave is reconnected to Alice before waiting for the
|
||||
// htlcs to clear.
|
||||
ht.EnsureConnected(s.dave, s.alice)
|
||||
|
||||
// Since Carol has been shutdown permanently, we will wait until all
|
||||
// other nodes in the network report no active htlcs.
|
||||
ht.AssertNumActiveHtlcs(s.alice, 0)
|
||||
ht.AssertNumActiveHtlcs(s.bob, 0)
|
||||
ht.AssertNumActiveHtlcs(s.dave, 0)
|
||||
|
||||
// When asserting the amount of satoshis moved, we'll factor in the
|
||||
// default base fee, as we didn't modify the fee structure when
|
||||
// creating the seed nodes in the network.
|
||||
|
||||
// At this point, all channels (minus Carol, who is shutdown) should
|
||||
// show a shift of 5k satoshis towards Carol. The order of asserts
|
||||
// corresponds to increasing of time is needed to embed the HTLC in
|
||||
// commitment transaction, in channel Bob->Alice->David, order is
|
||||
// David, Alice, Bob.
|
||||
ht.AssertAmountPaid(
|
||||
"Alice(local) => Dave(remote)", s.dave, s.chanPointDaveAlice,
|
||||
int64(0), amountPaid+(baseFee*numPayments),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Alice(local) => Dave(remote)", s.alice, s.chanPointDaveAlice,
|
||||
amountPaid+(baseFee*numPayments), int64(0),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Bob(local) => Alice(remote)", s.alice, s.chanPointAliceBob,
|
||||
int64(0), amountPaid+((baseFee*numPayments)*2),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Bob(local) => Alice(remote)", s.bob, s.chanPointAliceBob,
|
||||
amountPaid+(baseFee*numPayments)*2, int64(0),
|
||||
)
|
||||
|
||||
ht.CloseChannel(s.alice, s.chanPointAliceBob)
|
||||
ht.CloseChannel(s.dave, s.chanPointDaveAlice)
|
||||
}
|
||||
|
||||
// scenarioFourNodes specifies a scenario which we have a topology that has
|
||||
// four nodes and three channels.
|
||||
type scenarioFourNodes struct {
|
||||
alice *node.HarnessNode
|
||||
bob *node.HarnessNode
|
||||
carol *node.HarnessNode
|
||||
dave *node.HarnessNode
|
||||
|
||||
chanPointAliceBob *lnrpc.ChannelPoint
|
||||
chanPointCarolDave *lnrpc.ChannelPoint
|
||||
chanPointDaveAlice *lnrpc.ChannelPoint
|
||||
|
||||
cleanUp func()
|
||||
}
|
||||
|
||||
// setupScenarioFourNodes creates a topology for switch tests. It will create
|
||||
// two new nodes: Carol and Dave, such that there will be a 4 nodes, 3 channel
|
||||
// topology. Dave will make a channel with Alice, and Carol with Dave. After
|
||||
// this setup, the network topology should now look like:
|
||||
//
|
||||
// Carol -> Dave -> Alice -> Bob
|
||||
//
|
||||
// Once the network is created, Carol will generate 5 invoices and Bob will pay
|
||||
// them using the above path.
|
||||
//
|
||||
// NOTE: caller needs to call cleanUp to clean the nodes and channels created
|
||||
// from this setup.
|
||||
func setupScenarioFourNodes(ht *lntemp.HarnessTest) *scenarioFourNodes {
|
||||
const (
|
||||
chanAmt = btcutil.Amount(1000000)
|
||||
pushAmt = btcutil.Amount(900000)
|
||||
)
|
||||
|
||||
params := lntemp.OpenChannelParams{
|
||||
Amt: chanAmt,
|
||||
PushAmt: pushAmt,
|
||||
}
|
||||
|
||||
// Grab the standby nodes.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// As preliminary setup, we'll create two new nodes: Carol and Dave,
|
||||
// such that we now have a 4 node, 3 channel topology. Dave will make
|
||||
// a channel with Alice, and Carol with Dave. After this setup, the
|
||||
// network topology should now look like:
|
||||
// Carol -> Dave -> Alice -> Bob
|
||||
//
|
||||
// First, we'll create Dave and establish a channel to Alice.
|
||||
dave := ht.NewNode("Dave", nil)
|
||||
ht.ConnectNodes(dave, alice)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
||||
|
||||
// Next, we'll create Carol and establish a channel to from her to
|
||||
// Dave. Carol is started in htlchodl mode so that we can disconnect
|
||||
// the intermediary hops before starting the settle.
|
||||
carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"})
|
||||
ht.ConnectNodes(carol, dave)
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
||||
|
||||
// Open channels in batch to save blocks mined.
|
||||
reqs := []*lntemp.OpenChannelRequest{
|
||||
{Local: alice, Remote: bob, Param: params},
|
||||
{Local: dave, Remote: alice, Param: params},
|
||||
{Local: carol, Remote: dave, Param: params},
|
||||
}
|
||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
||||
|
||||
// Wait for all nodes to have seen all channels.
|
||||
nodes := []*node.HarnessNode{alice, bob, carol, dave}
|
||||
for _, chanPoint := range resp {
|
||||
for _, node := range nodes {
|
||||
ht.AssertTopologyChannelOpen(node, chanPoint)
|
||||
}
|
||||
}
|
||||
|
||||
chanPointAliceBob := resp[0]
|
||||
chanPointDaveAlice := resp[1]
|
||||
chanPointCarolDave := resp[2]
|
||||
|
||||
// Create 5 invoices for Carol, which expect a payment from Bob for 1k
|
||||
// satoshis with a different preimage each time.
|
||||
payReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numPayments)
|
||||
|
||||
// Using Carol as the source, pay to the 5 invoices from Bob created
|
||||
// above.
|
||||
ht.CompletePaymentRequestsNoWait(bob, payReqs, chanPointAliceBob)
|
||||
|
||||
// Create a cleanUp to wipe the states.
|
||||
cleanUp := func() {
|
||||
if ht.Failed() {
|
||||
ht.Skip("Skipped cleanup for failed test")
|
||||
return
|
||||
}
|
||||
|
||||
ht.CloseChannel(alice, chanPointAliceBob)
|
||||
ht.CloseChannel(dave, chanPointDaveAlice)
|
||||
ht.CloseChannel(carol, chanPointCarolDave)
|
||||
}
|
||||
|
||||
s := &scenarioFourNodes{
|
||||
alice, bob, carol, dave, chanPointAliceBob,
|
||||
chanPointCarolDave, chanPointDaveAlice, cleanUp,
|
||||
}
|
||||
|
||||
// Wait until all nodes in the network have 5 outstanding htlcs.
|
||||
s.assertHTLCs(ht, numPayments)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// assertHTLCs is a helper function which asserts the desired num of
|
||||
// HTLCs has been seen in the nodes.
|
||||
func (s *scenarioFourNodes) assertHTLCs(ht *lntemp.HarnessTest, num int) {
|
||||
// Alice should have both the same number of outgoing and
|
||||
// incoming HTLCs.
|
||||
ht.AssertNumActiveHtlcs(s.alice, num*2)
|
||||
// Bob should have num of incoming HTLCs.
|
||||
ht.AssertNumActiveHtlcs(s.bob, num)
|
||||
// Dave should have both the same number of outgoing and
|
||||
// incoming HTLCs.
|
||||
ht.AssertNumActiveHtlcs(s.dave, num*2)
|
||||
// Carol should have the num of outgoing HTLCs.
|
||||
ht.AssertNumActiveHtlcs(s.carol, num)
|
||||
}
|
||||
|
||||
// assertAmoutPaid is a helper method which takes a given paid amount
|
||||
// and number of payments and asserts the desired payments are made in
|
||||
// the four nodes.
|
||||
func (s *scenarioFourNodes) assertAmoutPaid(ht *lntemp.HarnessTest,
|
||||
amt int64, num int64) {
|
||||
|
||||
ht.AssertAmountPaid(
|
||||
"Dave(local) => Carol(remote)", s.carol,
|
||||
s.chanPointCarolDave, int64(0), amt,
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Dave(local) => Carol(remote)", s.dave,
|
||||
s.chanPointCarolDave, amt, int64(0),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Alice(local) => Dave(remote)", s.dave,
|
||||
s.chanPointDaveAlice,
|
||||
int64(0), amt+(baseFee*num),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Alice(local) => Dave(remote)", s.alice,
|
||||
s.chanPointDaveAlice,
|
||||
amt+(baseFee*num), int64(0),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Bob(local) => Alice(remote)", s.alice,
|
||||
s.chanPointAliceBob,
|
||||
int64(0), amt+((baseFee*num)*2),
|
||||
)
|
||||
ht.AssertAmountPaid(
|
||||
"Bob(local) => Alice(remote)", s.bob,
|
||||
s.chanPointAliceBob,
|
||||
amt+(baseFee*num)*2, int64(0),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,197 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultSplitTranches is the default number of tranches we split the
|
||||
// test cases into.
|
||||
defaultSplitTranches uint = 1
|
||||
|
||||
// defaultRunTranche is the default index of the test cases tranche that
|
||||
// we run.
|
||||
defaultRunTranche uint = 0
|
||||
)
|
||||
|
||||
var (
|
||||
// testCasesSplitParts is the number of tranches the test cases should
|
||||
// be split into. By default this is set to 1, so no splitting happens.
|
||||
// If this value is increased, then the -runtranche flag must be
|
||||
// specified as well to indicate which part should be run in the current
|
||||
// invocation.
|
||||
testCasesSplitTranches = flag.Uint(
|
||||
"splittranches", defaultSplitTranches, "split the test cases "+
|
||||
"in this many tranches and run the tranche at "+
|
||||
"0-based index specified by the -runtranche flag",
|
||||
)
|
||||
|
||||
// testCasesRunTranche is the 0-based index of the split test cases
|
||||
// tranche to run in the current invocation.
|
||||
testCasesRunTranche = flag.Uint(
|
||||
"runtranche", defaultRunTranche, "run the tranche of the "+
|
||||
"split test cases with the given (0-based) index",
|
||||
)
|
||||
|
||||
// dbBackendFlag specifies the backend to use.
|
||||
dbBackendFlag = flag.String("dbbackend", "bbolt", "Database backend "+
|
||||
"(bbolt, etcd, postgres)")
|
||||
)
|
||||
|
||||
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||
// programmatically driven network of lnd nodes.
|
||||
func TestLightningNetworkDaemon(t *testing.T) {
|
||||
// If no tests are registered, then we can exit early.
|
||||
if len(allTestCases) == 0 {
|
||||
t.Skip("integration tests not selected with flag 'rpctest'")
|
||||
}
|
||||
|
||||
// Get the test cases to be run in this tranche.
|
||||
testCases, trancheIndex, trancheOffset := getTestCaseSplitTranche()
|
||||
lntest.ApplyPortOffset(uint32(trancheIndex) * 1000)
|
||||
|
||||
// Create a simple fee service.
|
||||
feeService := lntemp.NewFeeService(t)
|
||||
|
||||
// Get the binary path and setup the harness test.
|
||||
binary := getLndBinary(t)
|
||||
harnessTest := lntemp.SetupHarness(
|
||||
t, binary, *dbBackendFlag, feeService,
|
||||
)
|
||||
defer harnessTest.Stop()
|
||||
|
||||
// Setup standby nodes, Alice and Bob, which will be alive and shared
|
||||
// among all the test cases.
|
||||
harnessTest.SetupStandbyNodes()
|
||||
|
||||
// Run the subset of the test cases selected in this tranche.
|
||||
for idx, testCase := range testCases {
|
||||
testCase := testCase
|
||||
name := fmt.Sprintf("tranche%02d/%02d-of-%d/%s/%s",
|
||||
trancheIndex, trancheOffset+uint(idx)+1,
|
||||
len(allTestCases), harnessTest.ChainBackendName(),
|
||||
testCase.Name)
|
||||
|
||||
success := t.Run(name, func(t1 *testing.T) {
|
||||
// Create a separate harness test for the testcase to
|
||||
// avoid overwriting the external harness test that is
|
||||
// tied to the parent test.
|
||||
ht := harnessTest.Subtest(t1)
|
||||
|
||||
// TODO(yy): split log files.
|
||||
cleanTestCaseName := strings.ReplaceAll(
|
||||
testCase.Name, " ", "_",
|
||||
)
|
||||
ht.SetTestName(cleanTestCaseName)
|
||||
|
||||
logLine := fmt.Sprintf(
|
||||
"STARTING ============ %v ============\n",
|
||||
testCase.Name,
|
||||
)
|
||||
|
||||
ht.Alice.AddToLogf(logLine)
|
||||
ht.Bob.AddToLogf(logLine)
|
||||
|
||||
ht.EnsureConnected(ht.Alice, ht.Bob)
|
||||
|
||||
ht.RunTestCase(testCase)
|
||||
})
|
||||
|
||||
// Stop at the first failure. Mimic behavior of original test
|
||||
// framework.
|
||||
if !success {
|
||||
// Log failure time to help relate the lnd logs to the
|
||||
// failure.
|
||||
t.Logf("Failure time: %v", time.Now().Format(
|
||||
"2006-01-02 15:04:05.000",
|
||||
))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, height := harnessTest.Miner.GetBestBlock()
|
||||
t.Logf("=========> tests finished for tranche: %v, tested %d "+
|
||||
"cases, end height: %d\n", trancheIndex, len(testCases), height)
|
||||
}
|
||||
|
||||
// getTestCaseSplitTranche returns the sub slice of the test cases that should
|
||||
// be run as the current split tranche as well as the index and slice offset of
|
||||
// the tranche.
|
||||
func getTestCaseSplitTranche() ([]*lntemp.TestCase, uint, uint) {
|
||||
numTranches := defaultSplitTranches
|
||||
if testCasesSplitTranches != nil {
|
||||
numTranches = *testCasesSplitTranches
|
||||
}
|
||||
runTranche := defaultRunTranche
|
||||
if testCasesRunTranche != nil {
|
||||
runTranche = *testCasesRunTranche
|
||||
}
|
||||
|
||||
// There's a special flake-hunt mode where we run the same test multiple
|
||||
// times in parallel. In that case the tranche index is equal to the
|
||||
// thread ID, but we need to actually run all tests for the regex
|
||||
// selection to work.
|
||||
threadID := runTranche
|
||||
if numTranches == 1 {
|
||||
runTranche = 0
|
||||
}
|
||||
|
||||
numCases := uint(len(allTestCases))
|
||||
testsPerTranche := numCases / numTranches
|
||||
trancheOffset := runTranche * testsPerTranche
|
||||
trancheEnd := trancheOffset + testsPerTranche
|
||||
if trancheEnd > numCases || runTranche == numTranches-1 {
|
||||
trancheEnd = numCases
|
||||
}
|
||||
|
||||
return allTestCases[trancheOffset:trancheEnd], threadID,
|
||||
trancheOffset
|
||||
}
|
||||
|
||||
func getLndBinary(t *testing.T) string {
|
||||
binary := itestLndBinary
|
||||
lndExec := ""
|
||||
if lndExecutable != nil && *lndExecutable != "" {
|
||||
lndExec = *lndExecutable
|
||||
}
|
||||
if lndExec == "" && runtime.GOOS == "windows" {
|
||||
// Windows (even in a bash like environment like git bash as on
|
||||
// Travis) doesn't seem to like relative paths to exe files...
|
||||
currentDir, err := os.Getwd()
|
||||
require.NoError(t, err, "unable to get working directory")
|
||||
|
||||
targetPath := filepath.Join(currentDir, "../../lnd-itest.exe")
|
||||
binary, err = filepath.Abs(targetPath)
|
||||
require.NoError(t, err, "unable to get absolute path")
|
||||
} else if lndExec != "" {
|
||||
binary = lndExec
|
||||
}
|
||||
|
||||
return binary
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Before we start any node, we need to make sure that any btcd node
|
||||
// that is started through the RPC harness uses a unique port as well
|
||||
// to avoid any port collisions.
|
||||
rpctest.ListenAddressGenerator = lntest.GenerateBtcdListenerAddresses
|
||||
|
||||
// Swap out grpc's default logger with out fake logger which drops the
|
||||
// statements on the floor.
|
||||
fakeLogger := grpclog.NewLoggerV2(io.Discard, io.Discard, io.Discard)
|
||||
grpclog.SetLoggerV2(fakeLogger)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testTrackPayments tests whether a client that calls the TrackPayments api
|
||||
// receives payment updates.
|
||||
func testTrackPayments(ht *lntemp.HarnessTest) {
|
||||
// Open a channel between alice and bob.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
channel := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{
|
||||
Amt: btcutil.Amount(300000),
|
||||
},
|
||||
)
|
||||
|
||||
// Call the TrackPayments api to listen for payment updates.
|
||||
req := &routerrpc.TrackPaymentsRequest{
|
||||
NoInflightUpdates: false,
|
||||
}
|
||||
tracker := alice.RPC.TrackPayments(req)
|
||||
|
||||
// Create an invoice from bob.
|
||||
var amountMsat int64 = 1000
|
||||
invoiceResp := bob.RPC.AddInvoice(
|
||||
&lnrpc.Invoice{
|
||||
ValueMsat: amountMsat,
|
||||
},
|
||||
)
|
||||
invoice := bob.RPC.LookupInvoice(invoiceResp.RHash)
|
||||
|
||||
// Send payment from alice to bob.
|
||||
paymentClient := alice.RPC.SendPayment(
|
||||
&routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: invoice.PaymentRequest,
|
||||
TimeoutSeconds: 60,
|
||||
},
|
||||
)
|
||||
|
||||
// Make sure the payment doesn't error due to invalid parameters or so.
|
||||
_, err := paymentClient.Recv()
|
||||
require.NoError(ht, err, "unable to get payment update.")
|
||||
|
||||
// Assert the first payment update is an inflight update.
|
||||
update1, err := tracker.Recv()
|
||||
require.NoError(ht, err, "unable to receive payment update 1.")
|
||||
|
||||
require.Equal(
|
||||
ht, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
||||
update1.FailureReason,
|
||||
)
|
||||
require.Equal(ht, lnrpc.Payment_IN_FLIGHT, update1.Status)
|
||||
require.Equal(ht, invoice.PaymentRequest, update1.PaymentRequest)
|
||||
require.Equal(ht, amountMsat, update1.ValueMsat)
|
||||
|
||||
// Assert the second payment update is a payment success update.
|
||||
update2, err := tracker.Recv()
|
||||
require.NoError(ht, err, "unable to receive payment update 2.")
|
||||
|
||||
require.Equal(
|
||||
ht, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
||||
update2.FailureReason,
|
||||
)
|
||||
require.Equal(ht, lnrpc.Payment_SUCCEEDED, update2.Status)
|
||||
require.Equal(ht, invoice.PaymentRequest, update2.PaymentRequest)
|
||||
require.Equal(ht, amountMsat, update2.ValueMsat)
|
||||
require.Equal(
|
||||
ht, hex.EncodeToString(invoice.RPreimage),
|
||||
update2.PaymentPreimage,
|
||||
)
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
ht.CloseChannel(alice, channel)
|
||||
}
|
||||
@@ -1,781 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAccount = lnwallet.DefaultAccountName
|
||||
defaultImportedAccount = waddrmgr.ImportedAddrAccountName
|
||||
)
|
||||
|
||||
// walletToLNAddrType maps walletrpc.AddressType to lnrpc.AddressType.
|
||||
func walletToLNAddrType(t *testing.T,
|
||||
addrType walletrpc.AddressType) lnrpc.AddressType {
|
||||
|
||||
switch addrType {
|
||||
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
|
||||
walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
|
||||
|
||||
return lnrpc.AddressType_NESTED_PUBKEY_HASH
|
||||
|
||||
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
|
||||
return lnrpc.AddressType_WITNESS_PUBKEY_HASH
|
||||
|
||||
case walletrpc.AddressType_TAPROOT_PUBKEY:
|
||||
return lnrpc.AddressType_TAPROOT_PUBKEY
|
||||
|
||||
default:
|
||||
t.Fatalf("unhandled addr type %v", addrType)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// newExternalAddr generates a new external address of an imported account for a
|
||||
// pair of nodes, where one acts as the funder and the other as the signer.
|
||||
func newExternalAddr(ht *lntemp.HarnessTest, funder, signer *node.HarnessNode,
|
||||
importedAccount string, addrType walletrpc.AddressType) string {
|
||||
|
||||
// We'll generate a new address for Carol from Dave's node to receive
|
||||
// and fund a new channel.
|
||||
req := &lnrpc.NewAddressRequest{
|
||||
Type: walletToLNAddrType(ht.T, addrType),
|
||||
Account: importedAccount,
|
||||
}
|
||||
funderResp := funder.RPC.NewAddress(req)
|
||||
|
||||
// Carol also needs to generate the address for the sake of this test
|
||||
// to be able to sign the channel funding input.
|
||||
req = &lnrpc.NewAddressRequest{
|
||||
Type: walletToLNAddrType(ht.T, addrType),
|
||||
}
|
||||
signerResp := signer.RPC.NewAddress(req)
|
||||
|
||||
// Sanity check that the generated addresses match.
|
||||
require.Equal(ht, funderResp.Address, signerResp.Address)
|
||||
assertExternalAddrType(ht.T, funderResp.Address, addrType)
|
||||
|
||||
return funderResp.Address
|
||||
}
|
||||
|
||||
// assertExternalAddrType asserts that an external address generated for an
|
||||
// imported account is of the expected type.
|
||||
func assertExternalAddrType(t *testing.T, addrStr string,
|
||||
accountAddrType walletrpc.AddressType) {
|
||||
|
||||
addr, err := btcutil.DecodeAddress(addrStr, harnessNetParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
switch accountAddrType {
|
||||
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
|
||||
require.IsType(t, addr, &btcutil.AddressWitnessPubKeyHash{})
|
||||
|
||||
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
|
||||
walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
|
||||
|
||||
require.IsType(t, addr, &btcutil.AddressScriptHash{})
|
||||
|
||||
case walletrpc.AddressType_TAPROOT_PUBKEY:
|
||||
require.IsType(t, addr, &btcutil.AddressTaproot{})
|
||||
|
||||
default:
|
||||
t.Fatalf("unsupported account addr type %v", accountAddrType)
|
||||
}
|
||||
}
|
||||
|
||||
// assertOutputScriptType asserts that a transaction's output, indicated by the
|
||||
// output with the given amount, has a script of the expected type. This assumes
|
||||
// all transaction outputs have unique amounts.
|
||||
func assertOutputScriptType(t *testing.T, expType txscript.ScriptClass,
|
||||
tx *wire.MsgTx, outputAmt int64) {
|
||||
|
||||
for _, txOut := range tx.TxOut {
|
||||
if txOut.Value != outputAmt {
|
||||
continue
|
||||
}
|
||||
|
||||
pkScript, err := txscript.ParsePkScript(txOut.PkScript)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expType, pkScript.Class())
|
||||
return
|
||||
}
|
||||
|
||||
// No output with the given amount was found.
|
||||
t.Fatalf("output with amount %v not found in transaction %v", outputAmt,
|
||||
spew.Sdump(tx))
|
||||
}
|
||||
|
||||
// psbtSendFromImportedAccount attempts to fund a PSBT from the given imported
|
||||
// account, originating from the source node to the destination.
|
||||
func psbtSendFromImportedAccount(ht *lntemp.HarnessTest, srcNode, destNode,
|
||||
signer *node.HarnessNode, account string,
|
||||
accountAddrType walletrpc.AddressType) {
|
||||
|
||||
balanceResp := srcNode.RPC.WalletBalance()
|
||||
require.Contains(ht, balanceResp.AccountBalance, account)
|
||||
confBalance := balanceResp.AccountBalance[account].ConfirmedBalance
|
||||
|
||||
destAmt := confBalance - 10000
|
||||
destAddrResp := destNode.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
||||
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
})
|
||||
|
||||
fundReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Raw{
|
||||
Raw: &walletrpc.TxTemplate{
|
||||
Outputs: map[string]uint64{
|
||||
destAddrResp.Address: uint64(destAmt),
|
||||
},
|
||||
},
|
||||
},
|
||||
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
|
||||
SatPerVbyte: 1,
|
||||
},
|
||||
Account: account,
|
||||
}
|
||||
fundResp := srcNode.RPC.FundPsbt(fundReq)
|
||||
|
||||
// Have Carol sign the PSBT input since Dave doesn't have any private
|
||||
// key information.
|
||||
finalizeReq := &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
}
|
||||
finalizeResp := signer.RPC.FinalizePsbt(finalizeReq)
|
||||
|
||||
// With the PSBT signed, we can broadcast the resulting transaction.
|
||||
publishReq := &walletrpc.Transaction{
|
||||
TxHex: finalizeResp.RawFinalTx,
|
||||
}
|
||||
srcNode.RPC.PublishTransaction(publishReq)
|
||||
|
||||
// Carol's balance from Dave's perspective should update accordingly.
|
||||
var (
|
||||
expTxFee int64
|
||||
expChangeScriptType txscript.ScriptClass
|
||||
)
|
||||
switch accountAddrType {
|
||||
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
|
||||
expTxFee = 141
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH:
|
||||
if account != defaultImportedAccount {
|
||||
expTxFee = 165
|
||||
expChangeScriptType = txscript.ScriptHashTy
|
||||
break
|
||||
}
|
||||
|
||||
// Spends from the default NP2WKH imported account have the same
|
||||
// fee rate as the hybrid address type since a NP2WKH input is
|
||||
// spent and a P2WKH change output is created.
|
||||
fallthrough
|
||||
|
||||
case walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
|
||||
expTxFee = 164
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
case walletrpc.AddressType_TAPROOT_PUBKEY:
|
||||
if account != defaultImportedAccount {
|
||||
expTxFee = 143
|
||||
expChangeScriptType = txscript.WitnessV1TaprootTy
|
||||
break
|
||||
}
|
||||
|
||||
// Spends from the default imported account fall back to a P2WKH
|
||||
// change. We'll want to change that, but in a separate PR.
|
||||
expTxFee = 131
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
default:
|
||||
ht.Fatalf("unsupported addr type %v", accountAddrType)
|
||||
}
|
||||
changeUtxoAmt := confBalance - destAmt - expTxFee
|
||||
|
||||
// If the transaction was created from the default imported account,
|
||||
// then any change produced is moved to the default wallet account.
|
||||
accountWithBalance := account
|
||||
if account == defaultImportedAccount {
|
||||
accountWithBalance = defaultAccount
|
||||
}
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, accountWithBalance, 0, changeUtxoAmt,
|
||||
)
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, accountWithBalance, changeUtxoAmt, 0,
|
||||
)
|
||||
|
||||
// Finally, assert that the transaction has the expected change address
|
||||
// type based on the account.
|
||||
var tx wire.MsgTx
|
||||
err := tx.Deserialize(bytes.NewReader(finalizeResp.RawFinalTx))
|
||||
require.NoError(ht, err)
|
||||
assertOutputScriptType(ht.T, expChangeScriptType, &tx, changeUtxoAmt)
|
||||
}
|
||||
|
||||
// fundChanAndCloseFromImportedAccount attempts to a fund a channel from the
|
||||
// given imported account, originating from the source node to the destination
|
||||
// node. To ensure the channel is operational before closing it, a test payment
|
||||
// is made. Several balance assertions are made along the way for the sake of
|
||||
// correctness.
|
||||
func fundChanAndCloseFromImportedAccount(ht *lntemp.HarnessTest, srcNode,
|
||||
destNode, signer *node.HarnessNode, account string,
|
||||
accountAddrType walletrpc.AddressType, utxoAmt, chanSize int64) {
|
||||
|
||||
// Retrieve the current confirmed balance to make some assertions later
|
||||
// on.
|
||||
balanceResp := srcNode.RPC.WalletBalance()
|
||||
require.Contains(ht, balanceResp.AccountBalance, account)
|
||||
accountConfBalance := balanceResp.
|
||||
AccountBalance[account].ConfirmedBalance
|
||||
defaultAccountConfBalance := balanceResp.
|
||||
AccountBalance[defaultAccount].ConfirmedBalance
|
||||
|
||||
// Now, start the channel funding process. We'll need to connect both
|
||||
// nodes first.
|
||||
ht.EnsureConnected(srcNode, destNode)
|
||||
|
||||
// The source node will then fund the channel through a PSBT shim.
|
||||
pendingChanID := ht.Random32Bytes()
|
||||
chanUpdates, rawPsbt := ht.OpenChannelPsbt(
|
||||
srcNode, destNode, lntemp.OpenChannelParams{
|
||||
Amt: btcutil.Amount(chanSize),
|
||||
FundingShim: &lnrpc.FundingShim{
|
||||
Shim: &lnrpc.FundingShim_PsbtShim{
|
||||
PsbtShim: &lnrpc.PsbtShim{
|
||||
PendingChanId: pendingChanID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fundReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Psbt{
|
||||
Psbt: rawPsbt,
|
||||
},
|
||||
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
|
||||
SatPerVbyte: 1,
|
||||
},
|
||||
Account: account,
|
||||
}
|
||||
fundResp := srcNode.RPC.FundPsbt(fundReq)
|
||||
|
||||
srcNode.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{
|
||||
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
|
||||
PsbtVerify: &lnrpc.FundingPsbtVerify{
|
||||
PendingChanId: pendingChanID,
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Now that we have a PSBT to fund the channel, our signer needs to sign
|
||||
// it.
|
||||
finalizeReq := &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
}
|
||||
finalizeResp := signer.RPC.FinalizePsbt(finalizeReq)
|
||||
|
||||
// The source node can then submit the signed PSBT and complete the
|
||||
// channel funding process.
|
||||
srcNode.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{
|
||||
Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{
|
||||
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
|
||||
PendingChanId: pendingChanID,
|
||||
SignedPsbt: finalizeResp.SignedPsbt,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// We should receive a notification of the channel funding transaction
|
||||
// being broadcast.
|
||||
updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates)
|
||||
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
||||
require.True(ht, ok)
|
||||
|
||||
// Mine enough blocks to announce the channel to the network, making
|
||||
// balance assertions along the way.
|
||||
var (
|
||||
expChanTxFee int64
|
||||
expChangeScriptType txscript.ScriptClass
|
||||
)
|
||||
switch accountAddrType {
|
||||
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
|
||||
expChanTxFee = 153
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH:
|
||||
if account != defaultImportedAccount {
|
||||
expChanTxFee = 177
|
||||
expChangeScriptType = txscript.ScriptHashTy
|
||||
break
|
||||
}
|
||||
|
||||
// Spends from the default NP2WKH imported account have the same
|
||||
// fee rate as the hybrid address type since a NP2WKH input is
|
||||
// spent and a P2WKH change output is created.
|
||||
fallthrough
|
||||
|
||||
case walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
|
||||
expChanTxFee = 176
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
case walletrpc.AddressType_TAPROOT_PUBKEY:
|
||||
if account != defaultImportedAccount {
|
||||
expChanTxFee = 155
|
||||
expChangeScriptType = txscript.WitnessV1TaprootTy
|
||||
break
|
||||
}
|
||||
|
||||
// Spends from the default imported account fall back to a P2WKH
|
||||
// change. We'll want to change that, but in a separate PR.
|
||||
expChanTxFee = 143
|
||||
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
|
||||
|
||||
default:
|
||||
ht.Fatalf("unsupported addr type %v", accountAddrType)
|
||||
}
|
||||
chanChangeUtxoAmt := utxoAmt - chanSize - expChanTxFee
|
||||
txHash, err := chainhash.NewHash(upd.ChanPending.Txid)
|
||||
require.NoError(ht, err)
|
||||
|
||||
// If we're spending from the default imported account, then any change
|
||||
// outputs produced are moved to the default wallet account, so we
|
||||
// should expect to see balances there.
|
||||
var confBalanceAfterChan int64
|
||||
if account == defaultImportedAccount {
|
||||
confBalanceAfterChan = defaultAccountConfBalance
|
||||
ht.AssertWalletAccountBalance(srcNode, account, 0, 0)
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, defaultAccount, defaultAccountConfBalance,
|
||||
chanChangeUtxoAmt,
|
||||
)
|
||||
|
||||
block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, txHash)
|
||||
|
||||
confBalanceAfterChan += chanChangeUtxoAmt
|
||||
ht.AssertWalletAccountBalance(srcNode, account, 0, 0)
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, defaultAccount, confBalanceAfterChan, 0,
|
||||
)
|
||||
} else {
|
||||
// Otherwise, all interactions remain within Carol's imported
|
||||
// account.
|
||||
confBalanceAfterChan = accountConfBalance - utxoAmt
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, account, confBalanceAfterChan,
|
||||
chanChangeUtxoAmt,
|
||||
)
|
||||
|
||||
block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
|
||||
ht.Miner.AssertTxInBlock(block, txHash)
|
||||
|
||||
confBalanceAfterChan += chanChangeUtxoAmt
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, account, confBalanceAfterChan, 0,
|
||||
)
|
||||
}
|
||||
|
||||
// Assert that the transaction has the expected change address type
|
||||
// based on the account.
|
||||
var tx wire.MsgTx
|
||||
err = tx.Deserialize(bytes.NewReader(finalizeResp.RawFinalTx))
|
||||
require.NoError(ht, err)
|
||||
assertOutputScriptType(
|
||||
ht.T, expChangeScriptType, &tx, chanChangeUtxoAmt,
|
||||
)
|
||||
|
||||
// Wait for the channel to be announced by both parties.
|
||||
chanPoint := &lnrpc.ChannelPoint{
|
||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||
FundingTxidBytes: upd.ChanPending.Txid,
|
||||
},
|
||||
OutputIndex: upd.ChanPending.OutputIndex,
|
||||
}
|
||||
ht.AssertTopologyChannelOpen(srcNode, chanPoint)
|
||||
ht.AssertTopologyChannelOpen(destNode, chanPoint)
|
||||
|
||||
// Send a test payment to ensure the channel is operating as normal.
|
||||
const invoiceAmt = 100000
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "psbt import chan",
|
||||
Value: invoiceAmt,
|
||||
}
|
||||
resp := destNode.RPC.AddInvoice(invoice)
|
||||
|
||||
ht.CompletePaymentRequests(srcNode, []string{resp.PaymentRequest})
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed. When the
|
||||
// payment is reported as settled by srcNode, 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)
|
||||
|
||||
// Now that we've confirmed the opened channel works, we'll close it.
|
||||
ht.CloseChannel(srcNode, chanPoint)
|
||||
|
||||
// Since the channel still had funds left on the source node's side,
|
||||
// they must've been redeemed after the close. Without a pre-negotiated
|
||||
// close address, the funds will go into the source node's wallet
|
||||
// instead of the imported account.
|
||||
const chanCloseTxFee = 9650
|
||||
balanceFromClosedChan := chanSize - invoiceAmt - chanCloseTxFee
|
||||
|
||||
if account == defaultImportedAccount {
|
||||
ht.AssertWalletAccountBalance(srcNode, account, 0, 0)
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, defaultAccount,
|
||||
confBalanceAfterChan+balanceFromClosedChan, 0,
|
||||
)
|
||||
} else {
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, account, confBalanceAfterChan, 0,
|
||||
)
|
||||
ht.AssertWalletAccountBalance(
|
||||
srcNode, defaultAccount, balanceFromClosedChan, 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// testWalletImportAccount tests that an imported account can fund transactions
|
||||
// and channels through PSBTs, by having one node (the one with the imported
|
||||
// account) craft the transactions and another node act as the signer.
|
||||
func testWalletImportAccount(ht *lntemp.HarnessTest) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
addrType walletrpc.AddressType
|
||||
}{
|
||||
{
|
||||
name: "standard BIP-0049",
|
||||
addrType: walletrpc.
|
||||
AddressType_NESTED_WITNESS_PUBKEY_HASH,
|
||||
},
|
||||
{
|
||||
name: "lnd BIP-0049 variant",
|
||||
addrType: walletrpc.
|
||||
AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH,
|
||||
},
|
||||
{
|
||||
name: "standard BIP-0084",
|
||||
addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
},
|
||||
{
|
||||
name: "standard BIP-0086",
|
||||
addrType: walletrpc.AddressType_TAPROOT_PUBKEY,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
success := ht.Run(tc.name, func(tt *testing.T) {
|
||||
testFunc := func(ht *lntemp.HarnessTest) {
|
||||
testWalletImportAccountScenario(
|
||||
ht, tc.addrType,
|
||||
)
|
||||
}
|
||||
|
||||
st := ht.Subtest(tt)
|
||||
|
||||
st.RunTestCase(&lntemp.TestCase{
|
||||
Name: tc.name,
|
||||
TestFunc: testFunc,
|
||||
})
|
||||
})
|
||||
if !success {
|
||||
// Log failure time to help relate the lnd logs to the
|
||||
// failure.
|
||||
ht.Logf("Failure time: %v", time.Now().Format(
|
||||
"2006-01-02 15:04:05.000",
|
||||
))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testWalletImportAccountScenario(ht *lntemp.HarnessTest,
|
||||
addrType walletrpc.AddressType) {
|
||||
|
||||
// We'll start our test by having two nodes, Carol and Dave. Carol's
|
||||
// default wallet account will be imported into Dave's node.
|
||||
//
|
||||
// NOTE: we won't use standby nodes here since the test will change
|
||||
// each of the node's wallet state.
|
||||
carol := ht.NewNode("carol", nil)
|
||||
dave := ht.NewNode("dave", nil)
|
||||
|
||||
runWalletImportAccountScenario(ht, addrType, carol, dave)
|
||||
}
|
||||
|
||||
func runWalletImportAccountScenario(ht *lntemp.HarnessTest,
|
||||
addrType walletrpc.AddressType, carol, dave *node.HarnessNode) {
|
||||
|
||||
const utxoAmt int64 = btcutil.SatoshiPerBitcoin
|
||||
|
||||
listReq := &walletrpc.ListAccountsRequest{
|
||||
Name: "default",
|
||||
AddressType: addrType,
|
||||
}
|
||||
listResp := carol.RPC.ListAccounts(listReq)
|
||||
require.Len(ht, listResp.Accounts, 1)
|
||||
carolAccount := listResp.Accounts[0]
|
||||
|
||||
const importedAccount = "carol"
|
||||
importReq := &walletrpc.ImportAccountRequest{
|
||||
Name: importedAccount,
|
||||
ExtendedPublicKey: carolAccount.ExtendedPublicKey,
|
||||
AddressType: addrType,
|
||||
}
|
||||
dave.RPC.ImportAccount(importReq)
|
||||
|
||||
// We'll generate an address for Carol from Dave's node to receive some
|
||||
// funds.
|
||||
externalAddr := newExternalAddr(
|
||||
ht, dave, carol, importedAccount, addrType,
|
||||
)
|
||||
|
||||
// Send coins to Carol's address and confirm them, making sure the
|
||||
// balance updates accordingly.
|
||||
alice := ht.Alice
|
||||
req := &lnrpc.SendCoinsRequest{
|
||||
Addr: externalAddr,
|
||||
Amount: utxoAmt,
|
||||
SatPerByte: 1,
|
||||
}
|
||||
alice.RPC.SendCoins(req)
|
||||
|
||||
ht.AssertWalletAccountBalance(dave, importedAccount, 0, utxoAmt)
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertWalletAccountBalance(dave, importedAccount, utxoAmt, 0)
|
||||
|
||||
// To ensure that Dave can use Carol's account as watch-only, we'll
|
||||
// construct a PSBT that sends funds to Alice, which we'll then hand
|
||||
// over to Carol to sign.
|
||||
psbtSendFromImportedAccount(
|
||||
ht, dave, alice, carol, importedAccount, addrType,
|
||||
)
|
||||
|
||||
// We'll generate a new address for Carol from Dave's node to receive
|
||||
// and fund a new channel.
|
||||
externalAddr = newExternalAddr(
|
||||
ht, dave, carol, importedAccount, addrType,
|
||||
)
|
||||
|
||||
// Retrieve the current confirmed balance of the imported account for
|
||||
// some assertions we'll make later on.
|
||||
balanceResp := dave.RPC.WalletBalance()
|
||||
require.Contains(ht, balanceResp.AccountBalance, importedAccount)
|
||||
confBalance := balanceResp.AccountBalance[importedAccount].
|
||||
ConfirmedBalance
|
||||
|
||||
// Send coins to Carol's address and confirm them, making sure the
|
||||
// balance updates accordingly.
|
||||
req = &lnrpc.SendCoinsRequest{
|
||||
Addr: externalAddr,
|
||||
Amount: utxoAmt,
|
||||
SatPerByte: 1,
|
||||
}
|
||||
alice.RPC.SendCoins(req)
|
||||
|
||||
ht.AssertWalletAccountBalance(
|
||||
dave, importedAccount, confBalance, utxoAmt,
|
||||
)
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertWalletAccountBalance(
|
||||
dave, importedAccount, confBalance+utxoAmt, 0,
|
||||
)
|
||||
|
||||
// Now that we have enough funds, it's time to fund the channel, make a
|
||||
// test payment, and close it. This contains several balance assertions
|
||||
// along the way.
|
||||
fundChanAndCloseFromImportedAccount(
|
||||
ht, dave, alice, carol, importedAccount, addrType, utxoAmt,
|
||||
int64(funding.MaxBtcFundingAmount),
|
||||
)
|
||||
}
|
||||
|
||||
// testWalletImportPubKey tests that an imported public keys can fund
|
||||
// transactions and channels through PSBTs, by having one node (the one with the
|
||||
// imported account) craft the transactions and another node act as the signer.
|
||||
func testWalletImportPubKey(ht *lntemp.HarnessTest) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
addrType walletrpc.AddressType
|
||||
}{
|
||||
{
|
||||
name: "BIP-0049",
|
||||
addrType: walletrpc.
|
||||
AddressType_NESTED_WITNESS_PUBKEY_HASH,
|
||||
},
|
||||
{
|
||||
name: "BIP-0084",
|
||||
addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
},
|
||||
{
|
||||
name: "BIP-0086",
|
||||
addrType: walletrpc.AddressType_TAPROOT_PUBKEY,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
success := ht.Run(tc.name, func(tt *testing.T) {
|
||||
testFunc := func(ht *lntemp.HarnessTest) {
|
||||
testWalletImportPubKeyScenario(
|
||||
ht, tc.addrType,
|
||||
)
|
||||
}
|
||||
|
||||
st := ht.Subtest(tt)
|
||||
|
||||
st.RunTestCase(&lntemp.TestCase{
|
||||
Name: tc.name,
|
||||
TestFunc: testFunc,
|
||||
})
|
||||
})
|
||||
if !success {
|
||||
// Log failure time to help relate the lnd logs to the
|
||||
// failure.
|
||||
ht.Logf("Failure time: %v", time.Now().Format(
|
||||
"2006-01-02 15:04:05.000",
|
||||
))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testWalletImportPubKeyScenario(ht *lntemp.HarnessTest,
|
||||
addrType walletrpc.AddressType) {
|
||||
|
||||
const utxoAmt int64 = btcutil.SatoshiPerBitcoin
|
||||
alice := ht.Alice
|
||||
|
||||
// We'll start our test by having two nodes, Carol and Dave.
|
||||
//
|
||||
// NOTE: we won't use standby nodes here since the test will change
|
||||
// each of the node's wallet state.
|
||||
carol := ht.NewNode("carol", nil)
|
||||
dave := ht.NewNode("dave", nil)
|
||||
|
||||
// We'll define a helper closure that we'll use throughout the test to
|
||||
// generate a new address of the given type from Carol's perspective,
|
||||
// import it into Dave's wallet, and fund it.
|
||||
importPubKey := func(keyIndex uint32, prevConfBalance,
|
||||
prevUnconfBalance int64) {
|
||||
|
||||
// Retrieve Carol's account public key for the corresponding
|
||||
// address type.
|
||||
listReq := &walletrpc.ListAccountsRequest{
|
||||
Name: "default",
|
||||
AddressType: addrType,
|
||||
}
|
||||
listResp := carol.RPC.ListAccounts(listReq)
|
||||
require.Len(ht, listResp.Accounts, 1)
|
||||
p2wkhAccount := listResp.Accounts[0]
|
||||
|
||||
// Derive the external address at the given index.
|
||||
accountPubKey, err := hdkeychain.NewKeyFromString(
|
||||
p2wkhAccount.ExtendedPublicKey,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
externalAccountExtKey, err := accountPubKey.Derive(0)
|
||||
require.NoError(ht, err)
|
||||
externalAddrExtKey, err := externalAccountExtKey.Derive(
|
||||
keyIndex,
|
||||
)
|
||||
require.NoError(ht, err)
|
||||
externalAddrPubKey, err := externalAddrExtKey.ECPubKey()
|
||||
require.NoError(ht, err)
|
||||
|
||||
// Serialize as 32-byte x-only pubkey for Taproot addresses.
|
||||
serializedPubKey := externalAddrPubKey.SerializeCompressed()
|
||||
if addrType == walletrpc.AddressType_TAPROOT_PUBKEY {
|
||||
serializedPubKey = schnorr.SerializePubKey(
|
||||
externalAddrPubKey,
|
||||
)
|
||||
}
|
||||
|
||||
// Import the public key into Dave.
|
||||
importReq := &walletrpc.ImportPublicKeyRequest{
|
||||
PublicKey: serializedPubKey,
|
||||
AddressType: addrType,
|
||||
}
|
||||
dave.RPC.ImportPublicKey(importReq)
|
||||
|
||||
// We'll also generate the same address for Carol, as it'll be
|
||||
// required later when signing.
|
||||
carolAddrResp := carol.RPC.NewAddress(&lnrpc.NewAddressRequest{
|
||||
Type: walletToLNAddrType(ht.T, addrType),
|
||||
})
|
||||
|
||||
// Send coins to Carol's address and confirm them, making sure
|
||||
// the balance updates accordingly.
|
||||
req := &lnrpc.SendCoinsRequest{
|
||||
Addr: carolAddrResp.Address,
|
||||
Amount: utxoAmt,
|
||||
SatPerByte: 1,
|
||||
}
|
||||
alice.RPC.SendCoins(req)
|
||||
|
||||
ht.AssertWalletAccountBalance(
|
||||
dave, defaultImportedAccount, prevConfBalance,
|
||||
prevUnconfBalance+utxoAmt,
|
||||
)
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
ht.AssertWalletAccountBalance(
|
||||
dave, defaultImportedAccount,
|
||||
prevConfBalance+utxoAmt, prevUnconfBalance,
|
||||
)
|
||||
}
|
||||
|
||||
// We'll have Carol generate a new external address, which we'll import
|
||||
// into Dave.
|
||||
importPubKey(0, 0, 0)
|
||||
|
||||
// To ensure that Dave can use Carol's public key as watch-only, we'll
|
||||
// construct a PSBT that sends funds to Alice, which we'll then hand
|
||||
// over to Carol to sign.
|
||||
psbtSendFromImportedAccount(
|
||||
ht, dave, alice, carol, defaultImportedAccount, addrType,
|
||||
)
|
||||
|
||||
// We'll now attempt to fund a channel.
|
||||
//
|
||||
// We'll have Carol generate another external address, which we'll
|
||||
// import into Dave again.
|
||||
balanceResp := dave.RPC.WalletBalance()
|
||||
require.Contains(ht, balanceResp.AccountBalance, defaultImportedAccount)
|
||||
confBalance := balanceResp.
|
||||
AccountBalance[defaultImportedAccount].ConfirmedBalance
|
||||
importPubKey(1, confBalance, 0)
|
||||
|
||||
// Now that we have enough funds, it's time to fund the channel, make a
|
||||
// test payment, and close it. This contains several balance assertions
|
||||
// along the way.
|
||||
fundChanAndCloseFromImportedAccount(
|
||||
ht, dave, alice, carol, defaultImportedAccount, addrType,
|
||||
utxoAmt, int64(funding.MaxBtcFundingAmount),
|
||||
)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type pendingChan *lnrpc.PendingChannelsResponse_PendingChannel
|
||||
|
||||
// testWipeForwardingPackagesLocal tests that when a channel is closed, either
|
||||
// through local force close, remote close, or coop close, all the forwarding
|
||||
// packages of that particular channel are deleted. The test creates a
|
||||
// topology as Alice -> Bob -> Carol, and sends payments from Alice to Carol
|
||||
// through Bob. It then performs the test in two steps,
|
||||
// - Bob force closes the channel Alice->Bob, and checks from both Bob's
|
||||
// PoV(local force close) and Alice's Pov(remote close) that the forwarding
|
||||
// packages are wiped.
|
||||
// - Bob coop closes the channel Bob->Carol, and checks from both Bob PoVs
|
||||
// that the forwarding packages are wiped.
|
||||
func testWipeForwardingPackages(ht *lntemp.HarnessTest) {
|
||||
const (
|
||||
chanAmt = 10e6
|
||||
paymentAmt = 10e4
|
||||
finalCTLVDelta = chainreg.DefaultBitcoinTimeLockDelta
|
||||
numInvoices = 3
|
||||
)
|
||||
|
||||
// Grab Alice and Bob from HarnessTest.
|
||||
alice, bob := ht.Alice, ht.Bob
|
||||
|
||||
// Create a new node Carol, which will create invoices that require
|
||||
// Alice to pay.
|
||||
carol := ht.NewNode("Carol", nil)
|
||||
|
||||
// Connect Bob to Carol.
|
||||
ht.ConnectNodes(bob, carol)
|
||||
|
||||
// Open a channel between Alice and Bob.
|
||||
chanPointAB := ht.OpenChannel(
|
||||
alice, bob, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Open a channel between Bob and Carol.
|
||||
chanPointBC := ht.OpenChannel(
|
||||
bob, carol, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
|
||||
// Before we continue, make sure Alice has seen the channel between Bob
|
||||
// and Carol.
|
||||
ht.AssertTopologyChannelOpen(alice, chanPointBC)
|
||||
|
||||
// Alice sends several payments to Carol through Bob, which triggers
|
||||
// Bob to create forwarding packages.
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
// Add an invoice for Carol.
|
||||
invoice := &lnrpc.Invoice{Memo: "testing", Value: paymentAmt}
|
||||
resp := carol.RPC.AddInvoice(invoice)
|
||||
|
||||
// Alice sends a payment to Carol through Bob.
|
||||
ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest})
|
||||
}
|
||||
|
||||
// TODO(yy): remove the sleep once the following bug is fixed.
|
||||
// When the invoice is reported settled, the commitment dance is not
|
||||
// yet finished, which can cause an error when closing the channel,
|
||||
// saying there's active HTLCs. We need to investigate this issue and
|
||||
// reverse the order to, first finish the commitment dance, then report
|
||||
// the invoice as settled.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Firstly, Bob force closes the channel.
|
||||
ht.CloseChannelAssertPending(bob, chanPointAB, true)
|
||||
|
||||
// Now that the channel has been force closed, it should show up in
|
||||
// bob's PendingChannels RPC under the waiting close section.
|
||||
pendingAB := ht.AssertChannelWaitingClose(bob, chanPointAB).Channel
|
||||
|
||||
// Check that Bob has created forwarding packages. We don't care the
|
||||
// exact number here as long as these packages are deleted when the
|
||||
// channel is closed.
|
||||
require.NotZero(ht, pendingAB.NumForwardingPackages)
|
||||
|
||||
// Secondly, Bob coop closes the channel.
|
||||
ht.CloseChannelAssertPending(bob, chanPointBC, false)
|
||||
|
||||
// Now that the channel has been coop closed, it should show up in
|
||||
// bob's PendingChannels RPC under the waiting close section.
|
||||
pendingBC := ht.AssertChannelWaitingClose(bob, chanPointBC).Channel
|
||||
|
||||
// Check that Bob has created forwarding packages. We don't care the
|
||||
// exact number here as long as these packages are deleted when the
|
||||
// channel is closed.
|
||||
require.NotZero(ht, pendingBC.NumForwardingPackages)
|
||||
|
||||
// Since it's a coop close, Carol should see the waiting close channel
|
||||
// too.
|
||||
pendingBC = ht.AssertChannelWaitingClose(carol, chanPointBC).Channel
|
||||
require.NotZero(ht, pendingBC.NumForwardingPackages)
|
||||
|
||||
// Mine 1 block to get the two closing transactions confirmed.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 2)
|
||||
|
||||
// Now that the closing transaction is confirmed, the above waiting
|
||||
// close channel should now become pending force closed channel.
|
||||
pendingAB = ht.AssertChannelPendingForceClose(bob, chanPointAB).Channel
|
||||
|
||||
// Check the forwarding pacakges are deleted.
|
||||
require.Zero(ht, pendingAB.NumForwardingPackages)
|
||||
|
||||
// For Alice, the forwarding packages should have been wiped too.
|
||||
pending := ht.AssertChannelPendingForceClose(alice, chanPointAB)
|
||||
pendingAB = pending.Channel
|
||||
require.Zero(ht, pendingAB.NumForwardingPackages)
|
||||
|
||||
// Mine 1 block to get Alice's sweeping tx confirmed.
|
||||
ht.MineBlocksAndAssertNumTxes(1, 1)
|
||||
|
||||
// Clean up the force closed channel.
|
||||
ht.CleanupForceClose(bob, chanPointAB)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
// testWumboChannels tests that only a node that signals wumbo channel
|
||||
// acceptances will allow a wumbo channel to be created. Additionally, if a
|
||||
// node is running with mini channels only enabled, then they should reject any
|
||||
// inbound wumbo channel requests.
|
||||
func testWumboChannels(ht *lntemp.HarnessTest) {
|
||||
// With all the channel types exercised, we'll now make sure the wumbo
|
||||
// signalling support works properly.
|
||||
//
|
||||
// We'll make two new nodes, with one of them signalling support for
|
||||
// wumbo channels while the other doesn't.
|
||||
wumboNode := ht.NewNode("wumbo", []string{"--protocol.wumbo-channels"})
|
||||
miniNode := ht.NewNode("mini", nil)
|
||||
|
||||
// We'll send coins to the wumbo node, as it'll be the one imitating
|
||||
// the channel funding.
|
||||
ht.FundCoins(btcutil.SatoshiPerBitcoin, wumboNode)
|
||||
|
||||
// Next we'll connect both nodes, then attempt to make a wumbo channel
|
||||
// funding request to the mini node we created above. The wumbo request
|
||||
// should fail as the node isn't advertising wumbo channels.
|
||||
ht.EnsureConnected(wumboNode, miniNode)
|
||||
|
||||
chanAmt := funding.MaxBtcFundingAmount + 1
|
||||
// The test should indicate a failure due to the channel being too
|
||||
// large.
|
||||
ht.OpenChannelAssertErr(
|
||||
wumboNode, miniNode, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
lnwallet.ErrChanTooLarge(chanAmt, funding.MaxBtcFundingAmount),
|
||||
)
|
||||
|
||||
// We'll now make another wumbo node to accept our wumbo channel
|
||||
// funding.
|
||||
wumboNode2 := ht.NewNode(
|
||||
"wumbo2", []string{"--protocol.wumbo-channels"},
|
||||
)
|
||||
|
||||
// Creating a wumbo channel between these two nodes should succeed.
|
||||
ht.EnsureConnected(wumboNode, wumboNode2)
|
||||
chanPoint := ht.OpenChannel(
|
||||
wumboNode, wumboNode2, lntemp.OpenChannelParams{Amt: chanAmt},
|
||||
)
|
||||
ht.CloseChannel(wumboNode, chanPoint)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
echo ""
|
||||
|
||||
# Filter all log files for errors, substitute variable data and match against whitelist.
|
||||
find $BASEDIR -name "*.log" | xargs grep -h "\[ERR\]" | \
|
||||
sed -r -f $BASEDIR/log_substitutions.txt | \
|
||||
sort | uniq | \
|
||||
grep -Fvi -f $BASEDIR/log_error_whitelist.txt
|
||||
|
||||
# If something shows up (not on whitelist) exit with error code 1.
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo ""
|
||||
echo "In the itest logs, the log line (patterns) above were detected."
|
||||
echo "[ERR] lines are generally reserved for internal errors."
|
||||
echo "Resolve the issue by either changing the log level or adding an "
|
||||
echo "exception to log_error_whitelist.txt"
|
||||
echo ""
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No itest errors detected."
|
||||
echo ""
|
||||
@@ -1,302 +0,0 @@
|
||||
<time> [ERR] BRAR: Unable to broadcast justice tx: Transaction rejected: output already spent
|
||||
<time> [ERR] BRAR: Unable to check for spentness of outpoint=<chan_point>: TxNotifier is exiting
|
||||
<time> [ERR] BRAR: Unable to register for conf for txid(<hex>): TxNotifier is exiting
|
||||
<time> [ERR] BRAR: Unable to register for block notifications: chainntnfs: system interrupt while attempting to register for block epoch notification.
|
||||
<time> [ERR] BTCN: Broadcast attempt failed: rejected by <ip>: replacement transaction <hex> has an insufficient absolute fee: needs <amt>, has <amt>
|
||||
<time> [ERR] BTCN: Broadcast attempt failed: rejected by <ip>: replacement transaction <hex> has an insufficient fee rate: needs more than <amt>, has <amt>
|
||||
<time> [ERR] BTCN: Broadcast attempt failed: rejected by <ip>: transaction already exists
|
||||
<time> [ERR] BTCN: Can't accept connection: unable to accept connection from <ip>: chacha20poly1305: message authentication failed
|
||||
<time> [ERR] BTCN: Can't accept connection: unable to accept connection from <ip>: EOF
|
||||
<time> [ERR] BTCN: Can't accept connection: unable to accept connection from <ip>: read tcp <ip>-><ip>: i/o timeout
|
||||
<time> [ERR] BTCN: Query failed with 0 out of 1 filters received
|
||||
<time> [ERR] BTCN: unable to get filter for hash=<hex>, retrying: neutrino shutting down
|
||||
<time> [ERR] BTCN: unable to get filter for hash=<hex>, retrying: unable to fetch cfilter
|
||||
<time> [ERR] BTCN: Unable to process block connected (height=<height>, hash=<hex>): out of order block <hex>: expected PrevBlock <hex>, got <hex>
|
||||
<time> [ERR] BTCN: Unknown connid=<id>
|
||||
<time> [ERR] CHAC: Received an error: rpc error: code = Canceled desc = context canceled, shutting down
|
||||
<time> [ERR] CHFT: Close channel <chan_point> unknown to store
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to advance state: channel not found
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to broadcast close tx: Transaction rejected: output already spent
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to force close: channel not found
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to mark commitment broadcasted: channel not found
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.commitSweepResolver: remote party swept utxo
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.commitSweepResolver: chainntnfs: system interrupt while attempting to register for block epoch notification.
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcOutgoingContestResolver: chain notifier shutting down
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcOutgoingContestResolver: chainntnfs: system interrupt while attempting to register for block epoch notification.
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcOutgoingContestResolver: resolver canceled
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcOutgoingContestResolver: the client has been shutdown
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcOutgoingContestResolver: unable to create pre-image from witness: invalid preimage length of 33, want 32
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcSuccessResolver: Transaction rejected: output already spent
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcTimeoutResolver: htlcswitch shutting down
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unable to progress *contractcourt.htlcTimeoutResolver: TxNotifier is exiting
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unexpected local commitment confirmed while in StateDefault
|
||||
<time> [ERR] CNCT: ChannelArbitrator(<chan_point>): unexpected local on-chain channel close
|
||||
<time> [ERR] CNCT: *contractcourt.commitSweepResolver(<chan_point>): unable to sweep input: remote party swept utxo
|
||||
<time> [ERR] CNCT: Unable to advance state: channel not found
|
||||
<time> [ERR] CNCT: unable to hand breached contract off to breachArbiter: server is shutting down
|
||||
<time> [ERR] CNCT: unable to handle channel breach for chan_point=<chan_point>: server is shutting down
|
||||
<time> [ERR] CNCT: Unable to handle known remote state: unable to handle channel breach for chan_point=<chan_point>: server is shutting down
|
||||
<time> [ERR] CRTR: Channel update of ourselves received
|
||||
<time> [ERR] CRTR: Error collecting result for shard <number> for payment <hex>: shard handler exiting
|
||||
<time> [ERR] CRTR: Error encountered during rescan: rescan exited
|
||||
<time> [ERR] CRTR: Failed sending attempt <number> for payment <hex> to switch: could not add downstream htlc
|
||||
<time> [ERR] CRTR: Failed sending attempt <number> for payment <hex> to switch: insufficient bandwidth to route htlc
|
||||
<time> [ERR] CRTR: Failed sending attempt <number> for payment <hex> to switch: UnknownNextPeer
|
||||
<time> [ERR] CRTR: out of order block: expecting height=<height>, got height=<height>
|
||||
<time> [ERR] CRTR: Payment <hex> failed: error
|
||||
<time> [ERR] CRTR: Payment <hex> failed: incorrect_payment_details
|
||||
<time> [ERR] CRTR: Payment <hex> failed: insufficient_balance
|
||||
<time> [ERR] CRTR: Payment <hex> failed: no_route
|
||||
<time> [ERR] CRTR: Payment <hex> failed: router shutting down
|
||||
<time> [ERR] CRTR: Payment <hex> failed: timeout
|
||||
<time> [ERR] CRTR: Resuming payment <hex> failed: error.
|
||||
<time> [ERR] CRTR: Resuming payment <hex> failed: incorrect_payment_details.
|
||||
<time> [ERR] CRTR: Resuming payment <hex> failed: no_route.
|
||||
<time> [ERR] CRTR: Resuming payment <hex> failed: router shutting down.
|
||||
<time> [ERR] CRTR: unable to add channel: edge not found
|
||||
<time> [ERR] CRTR: Unable to retrieve channel by id: edge not found
|
||||
<time> [ERR] DISC: channel announcement proof for short_chan_id=<cid> isn't valid: can't verify first bitcoin signature
|
||||
<time> [ERR] DISC: router shutting down
|
||||
<time> [ERR] DISC: unable add proof to the channel chanID=<hex>: edge marked as zombie
|
||||
<time> [ERR] DISC: unable add proof to the channel chanID=<hex>: edge not found
|
||||
<time> [ERR] DISC: unable to add channel: edge not found
|
||||
<time> [ERR] DISC: Unable to reply to peer query: set tcp <ip>: use of closed network connection
|
||||
<time> [ERR] DISC: Unable to reply to peer query: write tcp <ip>-><ip>: use of closed network connection
|
||||
<time> [ERR] DISC: Unable to reply to peer query: write tcp <ip>-><ip>: write: broken pipe
|
||||
<time> [ERR] DISC: Unable to reply to peer query: write tcp <ip>-><ip>: write: connection reset by peer
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=channel too large
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.16777216 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 10 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=Number of pending channels exceed maximum
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=Synchronizing blockchain
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] FNDG: received funding error from <hex>: chan_id=<hex>, err=funding failed due to internal error
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: canceled adding new channel
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: peer exiting
|
||||
<time> [ERR] FNDG: Unable to add new channel <chan_point> with peer <hex>: unable to get best block: the client has been shutdown
|
||||
<time> [ERR] FNDG: Unable to advance pending state of ChannelPoint(<chan_point>): error waiting for funding confirmation for ChannelPoint(<chan_point>): epoch client shutting down
|
||||
<time> [ERR] FNDG: Unable to advance pending state of ChannelPoint(<chan_point>): error waiting for funding confirmation for ChannelPoint(<chan_point>): funding manager shutting down
|
||||
<time> [ERR] FNDG: Unable to advance pending state of ChannelPoint(<chan_point>): error waiting for funding confirmation for ChannelPoint(<chan_point>): waiting for fundingconfirmation failed
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: ChainNotifier shutting down, cannot complete funding flow for ChannelPoint(<chan_point>)
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: channel announcement proof for short_chan_id=<cid> isn't valid: can't verify first bitcoin signature
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: funding manager shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: gossiper is shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: router shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: unable add proof to the channel chanID=<hex>: edge marked as zombie
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: channel announcement failed: unable add proof to the channel chanID=<hex>: edge not found
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: unable to register for confirmation of ChannelPoint(<chan_point>): chain notifier shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): error sending channel announcement: unable to register for confirmation of ChannelPoint(<chan_point>): TxNotifier is exiting
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed adding to router graph: error sending channel announcement: gossiper is shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed adding to router graph: error sending channel announcement: router shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed adding to router graph: error sending channel update: router shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed adding to router graph: funding manager shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): failed sending fundingLocked: funding manager shutting down
|
||||
<time> [ERR] FNDG: Unable to advance state(<chan_point>): funding manager shutting down
|
||||
<time> [ERR] FNDG: unable to cancel reservation: no active reservations for peer(<hex>)
|
||||
<time> [ERR] FNDG: Unable to handle funding accept message for peer_key=<hex>, pending_chan_id=<hex>: aborting PSBT flow: user canceled funding
|
||||
<time> [ERR] FNDG: unable to report short chan id: link <hex> not found
|
||||
<time> [ERR] FNDG: Unable to send channel proof: channel announcement proof for short_chan_id=<cid> isn't valid: can't verify first bitcoin signature
|
||||
<time> [ERR] FNDG: Unable to send channel proof: gossiper is shutting down
|
||||
<time> [ERR] FNDG: Unable to send channel proof: unable add proof to the channel chanID=<hex>: edge marked as zombie
|
||||
<time> [ERR] FNDG: Unable to send channel proof: unable add proof to the channel chanID=<hex>: edge not found
|
||||
<time> [ERR] FNDG: Unable to send node announcement: gossiper is shutting down
|
||||
<time> [ERR] FNDG: Unable to send node announcement: router shutting down
|
||||
<time> [ERR] FNDG: unable to open channel to NodeKey(<hex>): remote canceled funding, possibly timed out: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] HSWC: AmountBelowMinimum(amt=<amt>, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=internal error with error: remote error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=invalid update with error: remote error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=sync error with error: remote error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: ChannelPoint(<chan_point>): received error from peer: chan_id=<hex>, err=unable to resume channel, recovery required with error: remote error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to handle upstream settle HTLC: Invalid payment preimage <hex> for hash <hex> with error: invalid update
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: ChannelPoint(<chan_point>) with CommitPoint(<hex>) had possible local commitment state data loss with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: possible remote commitment state data loss with error: sync error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: Unable to send chan sync message for ChannelPoint(<chan_point>): peer exiting with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: unable to send chan sync message for ChannelPoint(<chan_point>): set tcp <ip>: use of closed network connection with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: unable to send chan sync message for ChannelPoint(<chan_point>): set tcp <ip>: use of closed network connection with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: Unable to send chan sync message for ChannelPoint(<chan_point>): write tcp <ip>-><ip>: use of closed network connection with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: Unable to send chan sync message for ChannelPoint(<chan_point>): write tcp <ip>-><ip>: write: broken pipe with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to synchronize channel states: unable to send chan sync message for ChannelPoint(<chan_point>): write tcp <ip>-><ip>: write: connection reset by peer with error: unable to resume channel, recovery required
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: unable to update commitment: link shutting down with error: internal error
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): link failed, exiting htlcManager
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unable to cancel incoming HTLC for circuit-key=(Chan ID=<chan>, HTLC ID=0): HTLC with ID 0 has already been failed
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unable to decode onion hop iterator: TemporaryChannelFailure
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unable to update signals
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unhandled error while forwarding htlc packet over htlcswitch: AmountBelowMinimum(amt=4000 mSAT, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unhandled error while forwarding htlc packet over htlcswitch: circuit has already been closed
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unhandled error while forwarding htlc packet over htlcswitch: insufficient bandwidth to route htlc
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unhandled error while forwarding htlc packet over htlcswitch: node configured to disallow forwards
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): unhandled error while forwarding htlc packet over htlcswitch: UnknownNextPeer
|
||||
<time> [ERR] HSWC: FeeInsufficient(htlc_amt==<amt>, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: insufficient bandwidth to route htlc
|
||||
<time> [ERR] HSWC: Link <chan> not found
|
||||
<time> [ERR] HSWC: Link <chan> policy for local forward not satisfied
|
||||
<time> [ERR] HSWC: node configured to disallow forwards
|
||||
<time> [ERR] HSWC: unable to de-obfuscate onion failure (hash=<hex>, pid=<pid>): invalid error length: expected 292 got 0
|
||||
<time> [ERR] HSWC: unable to find target channel for HTLC fail: channel ID = <chan>, HTLC ID = <id>
|
||||
<time> [ERR] HSWC: Unable to forward resolution msg: unable to find target channel for HTLC fail: channel ID = <chan>, HTLC ID = <id>
|
||||
<time> [ERR] HSWC: unable to process onion packet: sphinx packet replay attempted
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: AmountBelowMinimum(amt=<amt>, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: circuit has already been closed
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: FeeInsufficient(htlc_amt==<amt>, update=(lnwire.ChannelUpdate) {
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: insufficient bandwidth to route htlc
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: node configured to disallow forwards
|
||||
<time> [ERR] HSWC: Unhandled error while reforwarding htlc settle/fail over htlcswitch: UnknownNextPeer
|
||||
<time> [ERR] HSWC: UnknownNextPeer
|
||||
<time> [ERR] LNWL: ChannelPoint(<chan_point>): sync failed: remote believes our tail height is <height>, while we have <height>!
|
||||
<time> [ERR] LNWL: ChannelPoint(<chan_point>): sync failed: remote's next commit height is <height>, while we believe it is <height>!
|
||||
<time> [ERR] LNWL: ChannelPoint(<chan_point>): sync failed with local data loss: remote believes our tail height is <height>, while we have <height>!
|
||||
<time> [ERR] LNWL: Neutrino rescan ended with error: rescan exited
|
||||
<time> [ERR] LNWL: Notifying unmined tx notification (<hex>) while creating notification for blocks
|
||||
<time> [ERR] LNWL: Rescan for <num> addresses failed: the client has been shutdown
|
||||
<time> [ERR] LTND: Unable to lookup witness: no witnesses
|
||||
<time> [ERR] NANN: Unable to retrieve chan status for Channel(<chan_point>): edge not found
|
||||
<time> [ERR] NANN: Unable to retrieve chan status for Channel(<chan_point>): unable to extract ChannelUpdate for channel <chan_point>
|
||||
<time> [ERR] NANN: Unable to sign update disabling channel(<chan_point>): edge not found
|
||||
<time> [ERR] NTFN: chain notifier shutting down
|
||||
<time> [ERR] NTFN: Error during rescan: rescan exited
|
||||
<time> [ERR] NTFN: Failed getting UTXO: get utxo request cancelled
|
||||
<time> [ERR] NTFN: Rescan to determine the spend details of <chan_point> failed: the client has been shutdown
|
||||
<time> [ERR] NTFN: Unable to fetch block header: the client has been shutdown
|
||||
<time> [ERR] NTFN: unable to find blockhash for height=<height>: -1: Block number out of range
|
||||
<time> [ERR] NTFN: unable to get block: Post "http://<ip>": dial tcp <ip>: connect: connection refused
|
||||
<time> [ERR] NTFN: unable to get block: Post "http://<ip>": dial tcp <ip>: connect: connection reset by peer
|
||||
<time> [ERR] NTFN: unable to get block: Post "http://<ip>": read tcp <ip>-><ip>: read: connection reset by peer
|
||||
<time> [ERR] NTFN: unable to get block: the client has been shutdown
|
||||
<time> [ERR] NTFN: unable to get hash from block with height 790
|
||||
<time> [ERR] NTFN: unable to get missed blocks: starting height <height> is greater than ending height <height>
|
||||
<time> [ERR] NTFN: Unable to rewind chain from height <height> to height <height>: unable to find blockhash for disconnected height=<height>: -1: Block number out of range
|
||||
<time> [ERR] NTFN: Unable to rewind chain from height <height> to height <height>: unable to find blockhash for disconnected height=<height>: -8: Block height out of range
|
||||
<time> [ERR] NTNF: unable to get hash from block with height <height>
|
||||
<time> [ERR] PEER: Allowed test error from <ip> (inbound): ReadMessage: unhandled command [sendaddrv2]
|
||||
<time> [ERR] PEER: resend failed: unable to fetch channel sync messages for peer <hex>@<ip>: unable to find closed channel summary
|
||||
<time> [ERR] PEER: unable to close channel, ChannelID(<hex>) is unknown
|
||||
<time> [ERR] PEER: unable to force close link(<chan>): ChainArbitrator exiting
|
||||
<time> [ERR] PEER: unable to force close link(<chan>): channel not found
|
||||
<time> [ERR] PEER: unable to force close link(<chan>): unable to find arbitrator
|
||||
<time> [ERR] PEER: unable to get best block: the client has been shutdown
|
||||
<time> [ERR] PEER: unable to send msg to remote peer: peer exiting
|
||||
<time> [ERR] PEER: unable to send msg to remote peer: write tcp <ip>-><ip>: write: broken pipe
|
||||
<time> [ERR] PEER: unable to send msg to remote peer: write tcp <ip>-><ip>: write: connection reset by peer
|
||||
<time> [ERR] RPCS: [/chainrpc.ChainNotifier/RegisterBlockEpochNtfn]: chain notifier shutting down
|
||||
<time> [ERR] RPCS: [/chainrpc.ChainNotifier/RegisterBlockEpochNtfn]: context canceled
|
||||
<time> [ERR] RPCS: [closechannel] unable to close ChannelPoint(<chan_point>): chain notifier shutting down
|
||||
<time> [ERR] RPCS: [connectpeer]: error connecting to peer: already connected to peer: <hex>@<ip>
|
||||
<time> [ERR] RPCS: [connectpeer]: error connecting to peer: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] RPCS: [connectpeer]: error connecting to peer: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] RPCS: [connectpeer]: error connecting to peer: read tcp <ip>-><ip>: i/o timeout
|
||||
<time> [ERR] RPCS: Failed receiving from stream: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: Failed receiving from stream: rpc error: code = DeadlineExceeded desc = context deadline exceeded
|
||||
<time> [ERR] RPCS: Failed sending error response: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: Failed sending error response: rpc error: code = Internal desc = transport: transport: the stream is done or WriteHeader was already called
|
||||
<time> [ERR] RPCS: Failed sending response: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: Failed sending response: rpc error: code = Internal desc = transport: transport: the stream is done or WriteHeader was already called
|
||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SubscribeSingleInvoice]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BatchOpenChannel]: batch funding failed: error batch opening channel, initial negotiation failed: remote canceled funding, possibly timed out: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.001 BTC is below min chan size of 0.002 BTC
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission action. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: invalid permission entity. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: permission list cannot be empty. specify at least one action/entity pair. supported actions are [read write generate], supported entities are [onchain offchain address message peers info invoices signer macaroon uri]
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ChannelAcceptor]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: cannot close channel with state: ChanStatusRestored
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: cannot co-op close frozen channel as initiator until height=3059, (current_height=3055)
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: cannot co-op close frozen channel as initiator until height=<height>, (current_height=<height>)
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: chain notifier shutting down
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: must specify channel point in close channel
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: rpc error: code = DeadlineExceeded desc = context deadline exceeded
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: server is still in the process of starting
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ConnectPeer]: already connected to peer: <hex>@<ip>
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ConnectPeer]: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ConnectPeer]: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ConnectPeer]: read tcp <ip>-><ip>: i/o timeout
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ConnectPeer]: server is still in the process of starting
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/DeleteMacaroonID]: the specified ID cannot be deleted
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/FundingStateStep]: pendingChanID(<hex>) already has intent registered
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetChanInfo]: edge marked as zombie
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetChanInfo]: edge not found
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: channels cannot be created before the wallet is fully synced
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=channel too large
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=chan size of 0.16777216 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 10 BTC
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=Number of pending channels exceed maximum
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: received funding error from <hex>: chan_id=<hex>, err=Synchronizing blockchain
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/PendingChannels]: unable to find arbitrator
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: address: tb1qfc8fusa98jx8uvnhzavxccqlzvg749tvjw82tg is not valid for this network: regtest
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: amount set while SendAll is active
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: cannot send coins to pubkeys
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: unknown address type
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendPayment]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendPayment]: rpc error: code = DeadlineExceeded desc = context deadline exceeded
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendPayment]: rpc server shutting down
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendToRoute]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendToRoute]: rpc error: code = DeadlineExceeded desc = context deadline exceeded
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendToRoute]: rpc server shutting down
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelEvents]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: ChannelRouter shutting down
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: router not started
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: rpc error: code = DeadlineExceeded desc = context deadline exceeded
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: rpc error: code = Internal desc = transport: transport: the stream is done or WriteHeader was already called
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeInvoices]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/HtlcInterceptor]: rpc error: code = Canceled desc = context canceled
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SendPayment]: routerrpc server shutting down
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SendPaymentV2]: context canceled
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SendPaymentV2]: context deadline exceeded
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SendPaymentV2]: routerrpc server shutting down
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SubscribeHtlcEvents]: context canceled
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SubscribeHtlcEvents]: htlc event subscription terminated
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/SubscribeHtlcEvents]: context deadline exceeded
|
||||
<time> [ERR] RPCS: [/routerrpc.Route<time> [INF] LTND: Listening on the p2p interface is disabled!
|
||||
<time> [ERR] RPCS: [/signrpc.Signer/DeriveSharedKey]: must provide ephemeral pubkey
|
||||
<time> [ERR] RPCS: [/signrpc.Signer/DeriveSharedKey]: use either key_desc or key_loc
|
||||
<time> [ERR] RPCS: [/signrpc.Signer/DeriveSharedKey]: use either raw_key_bytes or key_index
|
||||
<time> [ERR] RPCS: [/signrpc.Signer/DeriveSharedKey]: when setting key_desc the field key_desc.key_loc must also be set
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/BakeMacaroon]: permission denied
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: cannot retrieve macaroon: cannot get macaroon: root key with id doesn't exist
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: caveat "ipaddr 1.1.1.1" not satisfied: macaroon locked to different IP address
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: caveat "time-before <time>" not satisfied: macaroon has expired
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: expected 1 macaroon, got 0
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: permission denied
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: the RPC server is in the process of starting up, but not yet ready to accept calls
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/GetInfo]: wallet locked, unlock it to enable full RPC access
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/ListMacaroonIDs]: cannot retrieve macaroon: cannot get macaroon: root key with id 1 doesn't exist
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/NewAddress]: permission denied
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=channel too large
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=chan size of 0.16777216 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 0.16777215 BTC
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=chan size of 10.00000001 BTC exceeds maximum chan size of 10 BTC
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=Number of pending channels exceed maximum
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): received funding error from <hex>: chan_id=<hex>, err=Synchronizing blockchain
|
||||
<time> [ERR] RPCS: [/walletrpc.WalletKit/LabelTransaction]: cannot label transaction with empty label
|
||||
<time> [ERR] RPCS: [/walletrpc.WalletKit/LabelTransaction]: transaction already labelled
|
||||
<time> [ERR] RPCS: Websocket receive error from <ip>: read tcp4 <ip>-><ip>: use of closed network connection
|
||||
<time> [ERR] RPCS: Websocket receive error from <ip>: websocket: close 1006 unexpected EOF
|
||||
<time> [ERR] RPCS: WS: error closing upgraded conn: write tcp4 <ip>-><ip>: write: connection reset by peer
|
||||
<time> [ERR] SRVR: Unable to connect to <hex>@<ip>: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] SRVR: Unable to connect to <hex>@<ip>: dial tcp <ip>: i/o timeout
|
||||
<time> [ERR] SRVR: Unable to connect to <hex>@<ip>: read tcp <ip>-><ip>: i/o timeout
|
||||
<time> [ERR] SRVR: Unable to retrieve advertised address for node <hex>: no advertised addresses found
|
||||
<time> [ERR] SRVR: Unable to retrieve advertised address for node <hex>: unable to find node
|
||||
<time> [ERR] UTXN: error while graduating class at height=<height>: TxNotifier is exiting
|
||||
<time> [ERR] UTXN: Failed to sweep first-stage HTLC (CLTV-delayed) output <chan_point>
|
||||
<time> [ERR] UTXN: Notification chan closed, can't advance output <chan_point>
|
||||
<time> [ERR] DISC: Unable to rebroadcast stale announcements: unable to retrieve outgoing channels: channel from self node has no policy
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/OpenChannel]: reserved wallet balance invalidated: transaction would leave insufficient funds for fee bumping anchor channel closings (see debug log for details)
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SendCoins]: reserved wallet balance invalidated: transaction would leave insufficient funds for fee bumping anchor channel closings (see debug log for details)
|
||||
<time> [ERR] RPCS: unable to open channel to NodeKey(<hex>): reserved wallet balance invalidated: transaction would leave insufficient funds for fee bumping anchor channel closings (see debug log for details)
|
||||
<time> [ERR] NANN: Unable to retrieve chan status for Channel(<chan_point>): unable to extract ChannelUpdate
|
||||
<time> [ERR] NTFN: Unable to rewind chain from height 1 to height -1: unable to find blockhash for disconnected height=<height>: -8: Block height out of range
|
||||
<time> [ERR] RPCS: WS: error writing message: websocket: close sent
|
||||
<time> [ERR] RPCS: [/routerrpc.Router/XImportMissionControl]: pair: <hex> -> <hex>: invalid failure: msat: <amt> and sat: 0.0000002 BTC values not equal
|
||||
<time> [ERR] BTCN: utxo scan failed: neutrino shutting down
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeInvoices]: context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/SubscribeChannelGraph]: context deadline exceeded
|
||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SubscribeSingleInvoice]: context canceled
|
||||
<time> [ERR] RPCS: [/lnrpc.State/SubscribeState]: context canceled
|
||||
<time> [ERR] NTFN: Failed to update rescan progress: database not open
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): failing link: process hodl queue: unable to update commitment: link shutting down with error: internal error
|
||||
<time> [ERR] INVC: SettleHodlInvoice with preimage <hex>: invoice already canceled
|
||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SettleInvoice]: invoice already canceled
|
||||
<time> [ERR] HSWC: ChannelLink(<chan_point>): outgoing htlc(<hex>) has insufficient fee: expected 33000, got 1020
|
||||
<time> [ERR] RPCS: [/lnrpc.Lightning/CloseChannel]: rpc error: code = Canceled desc = context canceled
|
||||
@@ -1,23 +0,0 @@
|
||||
s/[0-9a-f]{64}\:[[:digit:]]+/<chan_point>/g
|
||||
s/short_chan_id=[[:digit:]]+/short_chan_id=<cid>/g
|
||||
s/[0-9a-f]{16,}/<hex>/g
|
||||
s/[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+\:[[:digit:]]+/<ip>/g
|
||||
s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{3}/<time>/g
|
||||
s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}T[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{1,18}Z/<time>/g
|
||||
s/[[:digit:]]+\:[[:digit:]]+\:[[:digit:]]+/<chan>/g
|
||||
s/[[:digit:]]+ mSAT/<amt>/g
|
||||
s/HTLC ID = [[:digit:]]+/HTLC ID = <id>/g
|
||||
s/height=[[:digit:]]+/height=<height>/g
|
||||
s/collecting result for shard [[:digit:]]+/collecting result for shard <number>/g
|
||||
s/sending attempt [[:digit:]]+/sending attempt <number>/g
|
||||
s/Unable to rewind chain from height [[:digit:]]+ to height -?[[:digit:]]+/Unable to rewind chain from height <height> to height <height>/g
|
||||
s/NTFN: unable to get missed blocks: starting height [[:digit:]]+ is greater than ending height [[:digit:]]+/NTFN: unable to get missed blocks: starting height <height> is greater than ending height <height>/g
|
||||
s/BTCN: Broadcast attempt failed: rejected by <ip>: replacement transaction <hex> has an insufficient fee rate: needs more than [[:digit:]]+, has [[:digit:]]+/BTCN: Broadcast attempt failed: rejected by <ip>: replacement transaction <hex> has an insufficient fee rate: needs more than <amt>, has <amt>/g
|
||||
s/pid=[[:digit:]]+/pid=<pid>/g
|
||||
s/connid=[[:digit:]]+/connid=<id>/g
|
||||
s/sync failed with local data loss: remote believes our tail height is [[:digit:]]+, while we have [[:digit:]]+/sync failed with local data loss: remote believes our tail height is <height>, while we have <height>/g
|
||||
s/sync failed: remote believes our tail height is [[:digit:]]+, while we have [[:digit:]]+/sync failed: remote believes our tail height is <height>, while we have <height>/g
|
||||
s/sync failed: remote's next commit height is [[:digit:]]+, while we believe it is [[:digit:]]+/sync failed: remote's next commit height is <height>, while we believe it is <height>/g
|
||||
s/replacement transaction <hex> has an insufficient absolute fee: needs [[:digit:]]+, has [[:digit:]]+/replacement transaction <hex> has an insufficient absolute fee: needs <amt>, has <amt>/g
|
||||
s/Rescan for [[:digit:]]+ addresses/Rescan for <num> addresses/g
|
||||
s/unable to get hash from block with height [[:digit:]]+/unable to get hash from block with height <height>/g
|
||||
@@ -1,351 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
harnessNetParams = &chaincfg.RegressionNetParams
|
||||
|
||||
// lndExecutable is the full path to the lnd binary.
|
||||
lndExecutable = flag.String(
|
||||
"lndexec", itestLndBinary, "full path to lnd binary",
|
||||
)
|
||||
|
||||
slowMineDelay = 20 * time.Millisecond
|
||||
)
|
||||
|
||||
const (
|
||||
testFeeBase = 1e+6
|
||||
defaultCSV = lntest.DefaultCSV
|
||||
defaultTimeout = lntest.DefaultTimeout
|
||||
minerMempoolTimeout = lntest.MinerMempoolTimeout
|
||||
channelCloseTimeout = lntest.ChannelCloseTimeout
|
||||
itestLndBinary = "../../lnd-itest"
|
||||
anchorSize = 330
|
||||
noFeeLimitMsat = math.MaxInt64
|
||||
|
||||
AddrTypeWitnessPubkeyHash = lnrpc.AddressType_WITNESS_PUBKEY_HASH
|
||||
AddrTypeNestedPubkeyHash = lnrpc.AddressType_NESTED_PUBKEY_HASH
|
||||
AddrTypeTaprootPubkey = lnrpc.AddressType_TAPROOT_PUBKEY
|
||||
)
|
||||
|
||||
// harnessTest wraps a regular testing.T providing enhanced error detection
|
||||
// and propagation. All error will be augmented with a full stack-trace in
|
||||
// order to aid in debugging. Additionally, any panics caused by active
|
||||
// test cases will also be handled and represented as fatals.
|
||||
type harnessTest struct {
|
||||
t *testing.T
|
||||
|
||||
// testCase is populated during test execution and represents the
|
||||
// current test case.
|
||||
testCase *testCase
|
||||
|
||||
// lndHarness is a reference to the current network harness. Will be
|
||||
// nil if not yet set up.
|
||||
lndHarness *lntest.NetworkHarness
|
||||
}
|
||||
|
||||
// newHarnessTest creates a new instance of a harnessTest from a regular
|
||||
// testing.T instance.
|
||||
func newHarnessTest(t *testing.T, net *lntest.NetworkHarness) *harnessTest {
|
||||
return &harnessTest{t, nil, net}
|
||||
}
|
||||
|
||||
// Skipf calls the underlying testing.T's Skip method, causing the current test
|
||||
// to be skipped.
|
||||
func (h *harnessTest) Skipf(format string, args ...interface{}) {
|
||||
h.t.Skipf(format, args...)
|
||||
}
|
||||
|
||||
// Fatalf causes the current active test case to fail with a fatal error. All
|
||||
// integration tests should mark test failures solely with this method due to
|
||||
// the error stack traces it produces.
|
||||
func (h *harnessTest) Fatalf(format string, a ...interface{}) {
|
||||
if h.lndHarness != nil {
|
||||
h.lndHarness.SaveProfilesPages(h.t)
|
||||
}
|
||||
|
||||
stacktrace := errors.Wrap(fmt.Sprintf(format, a...), 1).ErrorStack()
|
||||
|
||||
if h.testCase != nil {
|
||||
h.t.Fatalf("Failed: (%v): exited with error: \n"+
|
||||
"%v", h.testCase.name, stacktrace)
|
||||
} else {
|
||||
h.t.Fatalf("Error outside of test: %v", stacktrace)
|
||||
}
|
||||
}
|
||||
|
||||
// RunTestCase executes a harness test case. Any errors or panics will be
|
||||
// represented as fatal.
|
||||
func (h *harnessTest) RunTestCase(testCase *testCase) {
|
||||
h.testCase = testCase
|
||||
defer func() {
|
||||
h.testCase = nil
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
description := errors.Wrap(err, 2).ErrorStack()
|
||||
h.t.Fatalf("Failed: (%v) panicked with: \n%v",
|
||||
h.testCase.name, description)
|
||||
}
|
||||
}()
|
||||
|
||||
testCase.test(h.lndHarness, h)
|
||||
}
|
||||
|
||||
func (h *harnessTest) Logf(format string, args ...interface{}) {
|
||||
h.t.Logf(format, args...)
|
||||
}
|
||||
|
||||
func (h *harnessTest) Log(args ...interface{}) {
|
||||
h.t.Log(args...)
|
||||
}
|
||||
|
||||
func (h *harnessTest) getLndBinary() string {
|
||||
binary := itestLndBinary
|
||||
lndExec := ""
|
||||
if lndExecutable != nil && *lndExecutable != "" {
|
||||
lndExec = *lndExecutable
|
||||
}
|
||||
if lndExec == "" && runtime.GOOS == "windows" {
|
||||
// Windows (even in a bash like environment like git bash as on
|
||||
// Travis) doesn't seem to like relative paths to exe files...
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
h.Fatalf("unable to get working directory: %v", err)
|
||||
}
|
||||
targetPath := filepath.Join(currentDir, "../../lnd-itest.exe")
|
||||
binary, err = filepath.Abs(targetPath)
|
||||
if err != nil {
|
||||
h.Fatalf("unable to get absolute path: %v", err)
|
||||
}
|
||||
} else if lndExec != "" {
|
||||
binary = lndExec
|
||||
}
|
||||
|
||||
return binary
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
test func(net *lntest.NetworkHarness, t *harnessTest)
|
||||
}
|
||||
|
||||
// waitForTxInMempool polls until finding one transaction in the provided
|
||||
// miner's mempool. An error is returned if *one* transaction isn't found within
|
||||
// the given timeout.
|
||||
func waitForTxInMempool(miner *rpcclient.Client,
|
||||
timeout time.Duration) (*chainhash.Hash, error) {
|
||||
|
||||
txs, err := waitForNTxsInMempool(miner, 1, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txs[0], err
|
||||
}
|
||||
|
||||
// waitForNTxsInMempool polls until finding the desired number of transactions
|
||||
// in the provided miner's mempool. An error is returned if this number is not
|
||||
// met after the given timeout.
|
||||
func waitForNTxsInMempool(miner *rpcclient.Client, n int,
|
||||
timeout time.Duration) ([]*chainhash.Hash, error) {
|
||||
|
||||
breakTimeout := time.After(timeout)
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
var err error
|
||||
var mempool []*chainhash.Hash
|
||||
for {
|
||||
select {
|
||||
case <-breakTimeout:
|
||||
return nil, fmt.Errorf("wanted %v, found %v txs "+
|
||||
"in mempool: %v", n, len(mempool), mempool)
|
||||
case <-ticker.C:
|
||||
mempool, err = miner.GetRawMempool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(mempool) == n {
|
||||
return mempool, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mineBlocks mine 'num' of blocks and check that blocks are present in
|
||||
// node blockchain. numTxs should be set to the number of transactions
|
||||
// (excluding the coinbase) we expect to be included in the first mined block.
|
||||
func mineBlocksFast(t *harnessTest, net *lntest.NetworkHarness,
|
||||
num uint32, numTxs int) []*wire.MsgBlock {
|
||||
|
||||
// If we expect transactions to be included in the blocks we'll mine,
|
||||
// we wait here until they are seen in the miner's mempool.
|
||||
var txids []*chainhash.Hash
|
||||
var err error
|
||||
if numTxs > 0 {
|
||||
txids, err = waitForNTxsInMempool(
|
||||
net.Miner.Client, numTxs, minerMempoolTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find txns in mempool: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
blocks := make([]*wire.MsgBlock, num)
|
||||
|
||||
blockHashes, err := net.Miner.Client.Generate(num)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate blocks: %v", err)
|
||||
}
|
||||
|
||||
for i, blockHash := range blockHashes {
|
||||
block, err := net.Miner.Client.GetBlock(blockHash)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get block: %v", err)
|
||||
}
|
||||
|
||||
blocks[i] = block
|
||||
}
|
||||
|
||||
// Finally, assert that all the transactions were included in the first
|
||||
// block.
|
||||
for _, txid := range txids {
|
||||
assertTxInBlock(t, blocks[0], txid)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
// mineBlocksSlow mines 'num' of blocks and checks that blocks are present in
|
||||
// the mining node's blockchain. numTxs should be set to the number of
|
||||
// transactions (excluding the coinbase) we expect to be included in the first
|
||||
// mined block. Between each mined block an artificial delay is introduced to
|
||||
// give all network participants time to catch up.
|
||||
//
|
||||
// NOTE: This function currently is just an alias for mineBlocksSlow.
|
||||
func mineBlocks(t *harnessTest, net *lntest.NetworkHarness,
|
||||
num uint32, numTxs int) []*wire.MsgBlock {
|
||||
|
||||
return mineBlocksSlow(t, net, num, numTxs)
|
||||
}
|
||||
|
||||
// mineBlocksSlow mines 'num' of blocks and checks that blocks are present in
|
||||
// the mining node's blockchain. numTxs should be set to the number of
|
||||
// transactions (excluding the coinbase) we expect to be included in the first
|
||||
// mined block. Between each mined block an artificial delay is introduced to
|
||||
// give all network participants time to catch up.
|
||||
func mineBlocksSlow(t *harnessTest, net *lntest.NetworkHarness,
|
||||
num uint32, numTxs int) []*wire.MsgBlock {
|
||||
|
||||
t.t.Helper()
|
||||
|
||||
// If we expect transactions to be included in the blocks we'll mine,
|
||||
// we wait here until they are seen in the miner's mempool.
|
||||
var txids []*chainhash.Hash
|
||||
var err error
|
||||
if numTxs > 0 {
|
||||
txids, err = waitForNTxsInMempool(
|
||||
net.Miner.Client, numTxs, minerMempoolTimeout,
|
||||
)
|
||||
require.NoError(t.t, err, "unable to find txns in mempool")
|
||||
}
|
||||
|
||||
blocks := make([]*wire.MsgBlock, num)
|
||||
blockHashes := make([]*chainhash.Hash, 0, num)
|
||||
|
||||
for i := uint32(0); i < num; i++ {
|
||||
generatedHashes, err := net.Miner.Client.Generate(1)
|
||||
require.NoError(t.t, err, "generate blocks")
|
||||
blockHashes = append(blockHashes, generatedHashes...)
|
||||
|
||||
time.Sleep(slowMineDelay)
|
||||
}
|
||||
|
||||
for i, blockHash := range blockHashes {
|
||||
block, err := net.Miner.Client.GetBlock(blockHash)
|
||||
require.NoError(t.t, err, "get blocks")
|
||||
|
||||
blocks[i] = block
|
||||
}
|
||||
|
||||
// Finally, assert that all the transactions were included in the first
|
||||
// block.
|
||||
for _, txid := range txids {
|
||||
assertTxInBlock(t, blocks[0], txid)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
func assertTxInBlock(t *harnessTest, block *wire.MsgBlock, txid *chainhash.Hash) {
|
||||
for _, tx := range block.Transactions {
|
||||
sha := tx.TxHash()
|
||||
if bytes.Equal(txid[:], sha[:]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("tx was not included in block")
|
||||
}
|
||||
|
||||
func assertWalletUnspent(t *harnessTest, node *lntest.HarnessNode,
|
||||
out *lnrpc.OutPoint, account string) {
|
||||
|
||||
t.t.Helper()
|
||||
|
||||
ctxb := context.Background()
|
||||
err := wait.NoError(func() error {
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
unspent, err := node.WalletKitClient.ListUnspent(
|
||||
ctxt, &walletrpc.ListUnspentRequest{
|
||||
Account: account,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = errors.New("tx with wanted txhash never found")
|
||||
for _, utxo := range unspent.Utxos {
|
||||
if !bytes.Equal(utxo.Outpoint.TxidBytes, out.TxidBytes) {
|
||||
continue
|
||||
}
|
||||
|
||||
err = errors.New("wanted output is not a wallet utxo")
|
||||
if utxo.Outpoint.OutputIndex != out.OutputIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}, defaultTimeout)
|
||||
require.NoError(t.t, err)
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
package itest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/rpcclient"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntemp"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// completePaymentRequests sends payments from a lightning node to complete all
|
||||
// payment requests. If the awaitResponse parameter is true, this function
|
||||
// does not return until all payments successfully complete without errors.
|
||||
func completePaymentRequests(client lnrpc.LightningClient,
|
||||
routerClient routerrpc.RouterClient, paymentRequests []string,
|
||||
awaitResponse bool) error {
|
||||
|
||||
ctxb := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// We start by getting the current state of the client's channels. This
|
||||
// is needed to ensure the payments actually have been committed before
|
||||
// we return.
|
||||
req := &lnrpc.ListChannelsRequest{}
|
||||
listResp, err := client.ListChannels(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send sends a payment and returns an error if it doesn't succeeded.
|
||||
send := func(payReq string) error {
|
||||
ctxc, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
payStream, err := routerClient.SendPaymentV2(
|
||||
ctxc,
|
||||
&routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: payReq,
|
||||
TimeoutSeconds: 60,
|
||||
FeeLimitMsat: noFeeLimitMsat,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := getPaymentResult(payStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Status != lnrpc.Payment_SUCCEEDED {
|
||||
return errors.New(resp.FailureReason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Launch all payments simultaneously.
|
||||
results := make(chan error)
|
||||
for _, payReq := range paymentRequests {
|
||||
go func(payReq string) {
|
||||
err := send(payReq)
|
||||
if awaitResponse {
|
||||
results <- err
|
||||
}
|
||||
}(payReq)
|
||||
}
|
||||
|
||||
// If awaiting a response, verify that all payments succeeded.
|
||||
if awaitResponse {
|
||||
for range paymentRequests {
|
||||
err := <-results
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We are not waiting for feedback in the form of a response, but we
|
||||
// should still wait long enough for the server to receive and handle
|
||||
// the send before cancelling the request. We wait for the number of
|
||||
// updates to one of our channels has increased before we return.
|
||||
err = wait.Predicate(func() bool {
|
||||
newListResp, err := client.ListChannels(ctx, req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the number of open channels is now lower than before
|
||||
// attempting the payments, it means one of the payments
|
||||
// triggered a force closure (for example, due to an incorrect
|
||||
// preimage). Return early since it's clear the payment was
|
||||
// attempted.
|
||||
if len(newListResp.Channels) < len(listResp.Channels) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, c1 := range listResp.Channels {
|
||||
for _, c2 := range newListResp.Channels {
|
||||
if c1.ChannelPoint != c2.ChannelPoint {
|
||||
continue
|
||||
}
|
||||
|
||||
// If this channel has an increased numbr of
|
||||
// updates, we assume the payments are
|
||||
// committed, and we can return.
|
||||
if c2.NumUpdates > c1.NumUpdates {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, defaultTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeFakePayHash creates random pre image hash.
|
||||
func makeFakePayHash(t *harnessTest) []byte {
|
||||
randBuf := make([]byte, 32)
|
||||
|
||||
if _, err := rand.Read(randBuf); err != nil {
|
||||
t.Fatalf("internal error, cannot generate random string: %v", err)
|
||||
}
|
||||
|
||||
return randBuf
|
||||
}
|
||||
|
||||
// createPayReqs is a helper method that will create a slice of payment
|
||||
// requests for the given node.
|
||||
func createPayReqs(node *lntest.HarnessNode, paymentAmt btcutil.Amount,
|
||||
numInvoices int) ([]string, [][]byte, []*lnrpc.Invoice, error) {
|
||||
|
||||
payReqs := make([]string, numInvoices)
|
||||
rHashes := make([][]byte, numInvoices)
|
||||
invoices := make([]*lnrpc.Invoice, numInvoices)
|
||||
for i := 0; i < numInvoices; i++ {
|
||||
preimage := make([]byte, 32)
|
||||
_, err := rand.Read(preimage)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("unable to generate "+
|
||||
"preimage: %v", err)
|
||||
}
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: "testing",
|
||||
RPreimage: preimage,
|
||||
Value: int64(paymentAmt),
|
||||
}
|
||||
ctxt, _ := context.WithTimeout(
|
||||
context.Background(), defaultTimeout,
|
||||
)
|
||||
resp, err := node.AddInvoice(ctxt, invoice)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("unable to add "+
|
||||
"invoice: %v", err)
|
||||
}
|
||||
|
||||
// Set the payment address in the invoice so the caller can
|
||||
// properly use it.
|
||||
invoice.PaymentAddr = resp.PaymentAddr
|
||||
|
||||
payReqs[i] = resp.PaymentRequest
|
||||
rHashes[i] = resp.RHash
|
||||
invoices[i] = invoice
|
||||
}
|
||||
return payReqs, rHashes, invoices, nil
|
||||
}
|
||||
|
||||
// getChanInfo is a helper method for getting channel info for a node's sole
|
||||
// channel.
|
||||
func getChanInfo(node *lntest.HarnessNode) (*lnrpc.Channel, error) {
|
||||
ctxb := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
req := &lnrpc.ListChannelsRequest{}
|
||||
channelInfo, err := node.ListChannels(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(channelInfo.Channels) != 1 {
|
||||
return nil, fmt.Errorf("node should only have a single "+
|
||||
"channel, instead it has %v", len(channelInfo.Channels))
|
||||
}
|
||||
|
||||
return channelInfo.Channels[0], nil
|
||||
}
|
||||
|
||||
// commitTypeHasAnchors returns whether commitType uses anchor outputs.
|
||||
func commitTypeHasAnchors(commitType lnrpc.CommitmentType) bool {
|
||||
switch commitType {
|
||||
case lnrpc.CommitmentType_ANCHORS,
|
||||
lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// nodeArgsForCommitType returns the command line flag to supply to enable this
|
||||
// commitment type.
|
||||
func nodeArgsForCommitType(commitType lnrpc.CommitmentType) []string {
|
||||
switch commitType {
|
||||
case lnrpc.CommitmentType_LEGACY:
|
||||
return []string{"--protocol.legacy.committweak"}
|
||||
case lnrpc.CommitmentType_STATIC_REMOTE_KEY:
|
||||
return []string{}
|
||||
case lnrpc.CommitmentType_ANCHORS:
|
||||
return []string{"--protocol.anchors"}
|
||||
case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE:
|
||||
return []string{
|
||||
"--protocol.anchors",
|
||||
"--protocol.script-enforced-lease",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calcStaticFee calculates appropriate fees for commitment transactions. This
|
||||
// function provides a simple way to allow test balance assertions to take fee
|
||||
// calculations into account.
|
||||
func calcStaticFee(c lnrpc.CommitmentType, numHTLCs int) btcutil.Amount {
|
||||
const htlcWeight = input.HTLCWeight
|
||||
var (
|
||||
feePerKw = chainfee.SatPerKWeight(
|
||||
lntemp.DefaultFeeRateSatPerKw,
|
||||
)
|
||||
commitWeight = input.CommitWeight
|
||||
anchors = btcutil.Amount(0)
|
||||
)
|
||||
|
||||
// The anchor commitment type is slightly heavier, and we must also add
|
||||
// the value of the two anchors to the resulting fee the initiator
|
||||
// pays. In addition the fee rate is capped at 10 sat/vbyte for anchor
|
||||
// channels.
|
||||
if commitTypeHasAnchors(c) {
|
||||
feePerKw = chainfee.SatPerKVByte(
|
||||
lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte * 1000,
|
||||
).FeePerKWeight()
|
||||
commitWeight = input.AnchorCommitWeight
|
||||
anchors = 2 * anchorSize
|
||||
}
|
||||
|
||||
return feePerKw.FeeForWeight(int64(commitWeight+htlcWeight*numHTLCs)) +
|
||||
anchors
|
||||
}
|
||||
|
||||
// channelCommitType retrieves the active channel commitment type for the given
|
||||
// chan point.
|
||||
func channelCommitType(node *lntest.HarnessNode,
|
||||
chanPoint *lnrpc.ChannelPoint) (lnrpc.CommitmentType, error) {
|
||||
|
||||
ctxb := context.Background()
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
|
||||
req := &lnrpc.ListChannelsRequest{}
|
||||
channels, err := node.ListChannels(ctxt, req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("listchannels failed: %v", err)
|
||||
}
|
||||
|
||||
for _, c := range channels.Channels {
|
||||
if c.ChannelPoint == txStr(chanPoint) {
|
||||
return c.CommitmentType, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("channel point %v not found", chanPoint)
|
||||
}
|
||||
|
||||
// calculateMaxHtlc re-implements the RequiredRemoteChannelReserve of the
|
||||
// funding manager's config, which corresponds to the maximum MaxHTLC value we
|
||||
// allow users to set when updating a channel policy.
|
||||
func calculateMaxHtlc(chanCap btcutil.Amount) uint64 {
|
||||
reserve := lnwire.NewMSatFromSatoshis(chanCap / 100)
|
||||
max := lnwire.NewMSatFromSatoshis(chanCap) - reserve
|
||||
return uint64(max)
|
||||
}
|
||||
|
||||
// waitForNodeBlockHeight queries the node for its current block height until
|
||||
// it reaches the passed height.
|
||||
func waitForNodeBlockHeight(node *lntest.HarnessNode, height int32) error {
|
||||
ctxb := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
var predErr error
|
||||
err := wait.Predicate(func() bool {
|
||||
info, err := node.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
||||
if err != nil {
|
||||
predErr = err
|
||||
return false
|
||||
}
|
||||
|
||||
if int32(info.BlockHeight) != height {
|
||||
predErr = fmt.Errorf("expected block height to "+
|
||||
"be %v, was %v", height, info.BlockHeight)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, defaultTimeout)
|
||||
if err != nil {
|
||||
return predErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getNTxsFromMempool polls until finding the desired number of transactions in
|
||||
// the provided miner's mempool and returns the full transactions to the caller.
|
||||
func getNTxsFromMempool(miner *rpcclient.Client, n int,
|
||||
timeout time.Duration) ([]*wire.MsgTx, error) {
|
||||
|
||||
txids, err := waitForNTxsInMempool(miner, n, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var txes []*wire.MsgTx
|
||||
for _, txid := range txids {
|
||||
tx, err := miner.GetRawTransaction(txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txes = append(txes, tx.MsgTx())
|
||||
}
|
||||
return txes, nil
|
||||
}
|
||||
|
||||
// getTxFee retrieves parent transactions and reconstructs the fee paid.
|
||||
func getTxFee(miner *rpcclient.Client, tx *wire.MsgTx) (btcutil.Amount, error) {
|
||||
var balance btcutil.Amount
|
||||
for _, in := range tx.TxIn {
|
||||
parentHash := in.PreviousOutPoint.Hash
|
||||
rawTx, err := miner.GetRawTransaction(&parentHash)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
parent := rawTx.MsgTx()
|
||||
balance += btcutil.Amount(
|
||||
parent.TxOut[in.PreviousOutPoint.Index].Value,
|
||||
)
|
||||
}
|
||||
|
||||
for _, out := range tx.TxOut {
|
||||
balance -= btcutil.Amount(out.Value)
|
||||
}
|
||||
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
// channelSubscription houses the proxied update and error chans for a node's
|
||||
// channel subscriptions.
|
||||
type channelSubscription struct {
|
||||
updateChan chan *lnrpc.ChannelEventUpdate
|
||||
errChan chan error
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// subscribeChannelNotifications subscribes to channel updates and launches a
|
||||
// goroutine that forwards these to the returned channel.
|
||||
func subscribeChannelNotifications(ctxb context.Context, t *harnessTest,
|
||||
node *lntest.HarnessNode) channelSubscription {
|
||||
|
||||
// We'll first start by establishing a notification client which will
|
||||
// send us notifications upon channels becoming active, inactive or
|
||||
// closed.
|
||||
req := &lnrpc.ChannelEventSubscription{}
|
||||
ctx, cancelFunc := context.WithCancel(ctxb)
|
||||
|
||||
chanUpdateClient, err := node.SubscribeChannelEvents(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create channel update client: %v", err)
|
||||
}
|
||||
|
||||
// We'll launch a goroutine that will be responsible for proxying all
|
||||
// notifications recv'd from the client into the channel below.
|
||||
errChan := make(chan error, 1)
|
||||
quit := make(chan struct{})
|
||||
chanUpdates := make(chan *lnrpc.ChannelEventUpdate, 20)
|
||||
go func() {
|
||||
defer cancelFunc()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
default:
|
||||
chanUpdate, err := chanUpdateClient.Recv()
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
case <-quit:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case chanUpdates <- chanUpdate:
|
||||
case <-quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return channelSubscription{
|
||||
updateChan: chanUpdates,
|
||||
errChan: errChan,
|
||||
quit: quit,
|
||||
}
|
||||
}
|
||||
|
||||
// findTxAtHeight gets all of the transactions that a node's wallet has a record
|
||||
// of at the target height, and finds and returns the tx with the target txid,
|
||||
// failing if it is not found.
|
||||
func findTxAtHeight(t *harnessTest, height int32,
|
||||
target string, node *lntest.HarnessNode) *lnrpc.Transaction {
|
||||
|
||||
ctxb := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
txns, err := node.LightningClient.GetTransactions(
|
||||
ctx, &lnrpc.GetTransactionsRequest{
|
||||
StartHeight: height,
|
||||
EndHeight: height,
|
||||
},
|
||||
)
|
||||
require.NoError(t.t, err, "could not get transactions")
|
||||
|
||||
for _, tx := range txns.Transactions {
|
||||
if tx.TxHash == target {
|
||||
return tx
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user