diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 6e6b3b74f..7fd1358ff 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -351,7 +351,8 @@ PRs([6776](https://github.com/lightningnetwork/lnd/pull/6776), [6822](https://github.com/lightningnetwork/lnd/pull/6822), [7172](https://github.com/lightningnetwork/lnd/pull/7172), [7242](https://github.com/lightningnetwork/lnd/pull/7242), -[7245](https://github.com/lightningnetwork/lnd/pull/7245)) have been made to +[7245](https://github.com/lightningnetwork/lnd/pull/7245)), +[6823](https://github.com/lightningnetwork/lnd/pull/6823)) have been made to refactor the itest for code health and maintenance. # Contributors (Alphabetical Order) diff --git a/funding/manager.go b/funding/manager.go index c357d6dc7..809a56a07 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1505,6 +1505,10 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, return } + log.Debugf("Initialized channel reservation: zeroConf=%v, psbt=%v, "+ + "cannedShim=%v", reservation.IsZeroConf(), + reservation.IsPsbt(), reservation.IsCannedShim()) + if zeroConf { // Store an alias for zero-conf channels. Other option-scid // channels will do this at a later point. diff --git a/lntemp/harness.go b/lntemp/harness.go index d69cc330a..033633f3a 100644 --- a/lntemp/harness.go +++ b/lntemp/harness.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/kvdb/etcd" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" @@ -155,6 +156,16 @@ func (h *HarnessTest) ChainBackendName() string { return h.manager.chainBackend.Name() } +// Context returns the run context used in this test. Usaually it should be +// managed by the test itself otherwise undefined behaviors will occur. It can +// be used, however, when a test needs to have its own context being managed +// differently. In that case, instead of using a background context, the run +// context should be used such that the test context scope can be fully +// controlled. +func (h *HarnessTest) Context() context.Context { + return h.runCtx +} + // SetUp starts the initial seeder nodes within the test harness. The initial // node's wallets will be funded wallets with 10x10 BTC outputs each. func (h *HarnessTest) SetupStandbyNodes() { @@ -205,7 +216,8 @@ func (h *HarnessTest) SetupStandbyNodes() { // We generate several blocks in order to give the outputs created // above a good number of confirmations. - h.MineBlocks(numBlocksSendOutput) + const totalTxes = 20 + h.MineBlocksAndAssertNumTxes(numBlocksSendOutput, totalTxes) // Now we want to wait for the nodes to catch up. h.WaitForBlockchainSync(h.Alice) @@ -283,6 +295,9 @@ func (h *HarnessTest) resetStandbyNodes(t *testing.T) { // config for the coming test. This will also inherit the // test's running context. h.RestartNodeWithExtraArgs(hn, hn.Cfg.OriginalExtraArgs) + + // Update the node's internal state. + hn.UpdateState() } } @@ -338,10 +353,6 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { return } - // We require the mempool to be cleaned from the test. - require.Empty(st, st.Miner.GetRawMempool(), "mempool not "+ - "cleaned, please mine blocks to clean them all.") - // When we finish the test, reset the nodes' configs and take a // snapshot of each of the nodes' internal states. for _, node := range st.manager.standbyNodes { @@ -351,8 +362,9 @@ func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { // If found running nodes, shut them down. st.shutdownNonStandbyNodes() - // Assert that mempool is cleaned - st.Miner.AssertNumTxsInMempool(0) + // We require the mempool to be cleaned from the test. + require.Empty(st, st.Miner.GetRawMempool(), "mempool not "+ + "cleaned, please mine blocks to clean them all.") // Finally, cancel the run context. We have to do it here // because we need to keep the context alive for the above @@ -397,9 +409,6 @@ func (h *HarnessTest) cleanupStandbyNode(hn *node.HarnessNode) { // Delete all payments made from this test. hn.RPC.DeleteAllPayments() - // Update the node's internal state. - hn.UpdateState() - // Finally, check the node is in a clean state for the following tests. h.validateNodeState(hn) } @@ -469,17 +478,48 @@ func (h *HarnessTest) SuspendNode(node *node.HarnessNode) func() error { return func() error { h.manager.registerNode(node) - return node.Start(h.runCtx) + if err := node.Start(h.runCtx); err != nil { + return err + } + h.WaitForBlockchainSync(node) + + return nil } } -// RestartNode restarts a given node and asserts. -func (h *HarnessTest) RestartNode(hn *node.HarnessNode, +// RestartNode restarts a given node, unlocks it and asserts it's successfully +// started. +func (h *HarnessTest) RestartNode(hn *node.HarnessNode) { + err := h.manager.restartNode(h.runCtx, hn, nil) + require.NoErrorf(h, err, "failed to restart node %s", hn.Name()) + + err = h.manager.unlockNode(hn) + require.NoErrorf(h, err, "failed to unlock node %s", hn.Name()) + + if !hn.Cfg.SkipUnlock { + // Give the node some time to catch up with the chain before we + // continue with the tests. + h.WaitForBlockchainSync(hn) + } +} + +// RestartNodeNoUnlock restarts a given node without unlocking its wallet. +func (h *HarnessTest) RestartNodeNoUnlock(hn *node.HarnessNode) { + err := h.manager.restartNode(h.runCtx, hn, nil) + require.NoErrorf(h, err, "failed to restart node %s", hn.Name()) +} + +// RestartNodeWithChanBackups restarts a given node with the specified channel +// backups. +func (h *HarnessTest) RestartNodeWithChanBackups(hn *node.HarnessNode, chanBackups ...*lnrpc.ChanBackupSnapshot) { - err := h.manager.restartNode(h.runCtx, hn, nil, chanBackups...) + err := h.manager.restartNode(h.runCtx, hn, nil) require.NoErrorf(h, err, "failed to restart node %s", hn.Name()) + err = h.manager.unlockNode(hn, chanBackups...) + require.NoErrorf(h, err, "failed to unlock node %s", hn.Name()) + // Give the node some time to catch up with the chain before we // continue with the tests. h.WaitForBlockchainSync(hn) @@ -490,7 +530,7 @@ func (h *HarnessTest) RestartNodeWithExtraArgs(hn *node.HarnessNode, extraArgs []string) { hn.SetExtraArgs(extraArgs) - h.RestartNode(hn, nil) + h.RestartNode(hn) } // NewNodeWithSeed fully initializes a new HarnessNode after creating a fresh @@ -525,7 +565,7 @@ func (h *HarnessTest) newNodeWithSeed(name string, // Start the node with seed only, which will only create the `State` // and `WalletUnlocker` clients. - err = node.StartWithSeed(h.runCtx) + err = node.StartWithNoAuth(h.runCtx) require.NoErrorf(h, err, "failed to start node %s", node.Name()) // Generate a new seed. @@ -568,7 +608,7 @@ func (h *HarnessTest) RestoreNodeWithSeed(name string, extraArgs []string, // Start the node with seed only, which will only create the `State` // and `WalletUnlocker` clients. - err = node.StartWithSeed(h.runCtx) + err = node.StartWithNoAuth(h.runCtx) require.NoErrorf(h, err, "failed to start node %s", node.Name()) // Create the wallet. @@ -587,6 +627,61 @@ func (h *HarnessTest) RestoreNodeWithSeed(name string, extraArgs []string, return node } +// NewNodeEtcd starts a new node with seed that'll use an external etcd +// database as its storage. The passed cluster flag indicates that we'd like +// the node to join the cluster leader election. We won't wait until RPC is +// available (this is useful when the node is not expected to become the leader +// right away). +func (h *HarnessTest) NewNodeEtcd(name string, etcdCfg *etcd.Config, + password []byte, cluster bool, + leaderSessionTTL int) *node.HarnessNode { + + // We don't want to use the embedded etcd instance. + h.manager.dbBackend = lntest.BackendBbolt + + extraArgs := node.ExtraArgsEtcd( + etcdCfg, name, cluster, leaderSessionTTL, + ) + node, err := h.manager.newNode(h.T, name, extraArgs, password, true) + require.NoError(h, err, "failed to create new node with etcd") + + // Start the node daemon only. + err = node.StartLndCmd(h.runCtx) + require.NoError(h, err, "failed to start node %s", node.Name()) + + return node +} + +// NewNodeWithSeedEtcd starts a new node with seed that'll use an external etcd +// database as its storage. The passed cluster flag indicates that we'd like +// the node to join the cluster leader election. +func (h *HarnessTest) NewNodeWithSeedEtcd(name string, etcdCfg *etcd.Config, + password []byte, entropy []byte, statelessInit, cluster bool, + leaderSessionTTL int) (*node.HarnessNode, []string, []byte) { + + // We don't want to use the embedded etcd instance. + h.manager.dbBackend = lntest.BackendBbolt + + // Create a request to generate a new aezeed. The new seed will have + // the same password as the internal wallet. + req := &lnrpc.GenSeedRequest{ + AezeedPassphrase: password, + SeedEntropy: nil, + } + + extraArgs := node.ExtraArgsEtcd( + etcdCfg, name, cluster, leaderSessionTTL, + ) + + return h.newNodeWithSeed(name, extraArgs, req, statelessInit) +} + +// KillNode kills the node (but won't wait for the node process to stop). +func (h *HarnessTest) KillNode(hn *node.HarnessNode) { + require.NoErrorf(h, hn.Kill(), "%s: kill got error", hn.Name()) + delete(h.manager.activeNodes, hn.Cfg.NodeID) +} + // SetFeeEstimate sets a fee rate to be returned from fee estimator. // // NOTE: this method will set the fee rate for a conf target of 1, which is the @@ -1276,6 +1371,9 @@ func (h *HarnessTest) RestartNodeAndRestoreDB(hn *node.HarnessNode) { err := h.manager.restartNode(h.runCtx, hn, cb) require.NoErrorf(h, err, "failed to restart node %s", hn.Name()) + err = h.manager.unlockNode(hn) + require.NoErrorf(h, err, "failed to unlock node %s", hn.Name()) + // Give the node some time to catch up with the chain before we // continue with the tests. h.WaitForBlockchainSync(hn) @@ -1325,6 +1423,19 @@ func (h *HarnessTest) MineBlocksAndAssertNumTxes(num uint32, return blocks } +// MineEmptyBlocks mines a given number of empty blocks. +// +// NOTE: this differs from miner's `MineEmptyBlocks` as it requires the nodes +// to be synced. +func (h *HarnessTest) MineEmptyBlocks(num int) []*wire.MsgBlock { + blocks := h.Miner.MineEmptyBlocks(num) + + // Finally, make sure all the active nodes are synced. + h.AssertActiveNodesSynced() + + return blocks +} + // QueryChannelByChanPoint tries to find a channel matching the channel point // and asserts. It returns the channel found. func (h *HarnessTest) QueryChannelByChanPoint(hn *node.HarnessNode, @@ -1358,6 +1469,14 @@ func (h *HarnessTest) SendPaymentAssertFail(hn *node.HarnessNode, return payment } +// SendPaymentAssertSettled sends a payment from the passed node and asserts the +// payment is settled. +func (h *HarnessTest) SendPaymentAssertSettled(hn *node.HarnessNode, + req *routerrpc.SendPaymentRequest) *lnrpc.Payment { + + return h.SendPaymentAndAssertStatus(hn, req, lnrpc.Payment_SUCCEEDED) +} + // OpenChannelRequest is used to open a channel using the method // OpenMultiChannelsAsync. type OpenChannelRequest struct { @@ -1630,3 +1749,51 @@ func findSweepInDetails(ht *HarnessTest, sweepTxid string, return false } + +// ConnectMiner connects the miner with the chain backend in the network. +func (h *HarnessTest) ConnectMiner() { + err := h.manager.chainBackend.ConnectMiner() + require.NoError(h, err, "failed to connect miner") +} + +// DisconnectMiner removes the connection between the miner and the chain +// backend in the network. +func (h *HarnessTest) DisconnectMiner() { + err := h.manager.chainBackend.DisconnectMiner() + require.NoError(h, err, "failed to disconnect miner") +} + +// QueryRoutesAndRetry attempts to keep querying a route until timeout is +// reached. +// +// NOTE: when a channel is opened, we may need to query multiple times to get +// it in our QueryRoutes RPC. This happens even after we check the channel is +// heard by the node using ht.AssertChannelOpen. Deep down, this is because our +// GraphTopologySubscription and QueryRoutes give different results regarding a +// specific channel, with the formal reporting it being open while the latter +// not, resulting GraphTopologySubscription acting "faster" than QueryRoutes. +// TODO(yy): make sure related subsystems share the same view on a given +// channel. +func (h *HarnessTest) QueryRoutesAndRetry(hn *node.HarnessNode, + req *lnrpc.QueryRoutesRequest) *lnrpc.QueryRoutesResponse { + + var routes *lnrpc.QueryRoutesResponse + err := wait.NoError(func() error { + ctxt, cancel := context.WithCancel(h.runCtx) + defer cancel() + + resp, err := hn.RPC.LN.QueryRoutes(ctxt, req) + if err != nil { + return fmt.Errorf("%s: failed to query route: %w", + hn.Name(), err) + } + + routes = resp + + return nil + }, DefaultTimeout) + + require.NoError(h, err, "timeout querying routes") + + return routes +} diff --git a/lntemp/harness_assertion.go b/lntemp/harness_assertion.go index 690fbff44..e2d7c05e3 100644 --- a/lntemp/harness_assertion.go +++ b/lntemp/harness_assertion.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntemp/node" "github.com/lightningnetwork/lnd/lntemp/rpc" @@ -564,8 +565,10 @@ func (h *HarnessTest) AssertStreamChannelCoopClosed(hn *node.HarnessNode, h.AssertNumWaitingClose(hn, 0) // Finally, check that the node's topology graph has seen this channel - // closed. - h.AssertTopologyChannelClosed(hn, cp) + // closed if it's a public channel. + if !resp.Channel.Private { + h.AssertTopologyChannelClosed(hn, cp) + } return closingTxid } @@ -610,8 +613,10 @@ func (h *HarnessTest) AssertStreamChannelForceClosed(hn *node.HarnessNode, h.AssertNumPendingForceClose(hn, 1) // Finally, check that the node's topology graph has seen this channel - // closed. - h.AssertTopologyChannelClosed(hn, cp) + // closed if it's a public channel. + if !resp.Channel.Private { + h.AssertTopologyChannelClosed(hn, cp) + } return closingTxid } @@ -884,7 +889,11 @@ func (h *HarnessTest) assertPaymentStatusWithTimeout(stream rpc.PaymentClient, err := wait.NoError(func() error { // Consume one message. This will raise an error if the message // is not received within DefaultTimeout. - payment := h.ReceivePaymentUpdate(stream) + payment, err := h.ReceivePaymentUpdate(stream) + if err != nil { + return fmt.Errorf("received error from payment "+ + "stream: %s", err) + } // Return if the desired payment state is reached. if payment.Status == status { @@ -895,8 +904,8 @@ func (h *HarnessTest) assertPaymentStatusWithTimeout(stream rpc.PaymentClient, // Return the err so that it can be used for debugging when // timeout is reached. - return fmt.Errorf("payment status, got %v, want %v", - payment.Status, status) + return fmt.Errorf("payment %v status, got %v, want %v", + payment.PaymentHash, payment.Status, status) }, timeout) require.NoError(h, err, "timeout while waiting payment") @@ -907,7 +916,7 @@ func (h *HarnessTest) assertPaymentStatusWithTimeout(stream rpc.PaymentClient, // ReceivePaymentUpdate waits until a message is received on the payment client // stream or the timeout is reached. func (h *HarnessTest) ReceivePaymentUpdate( - stream rpc.PaymentClient) *lnrpc.Payment { + stream rpc.PaymentClient) (*lnrpc.Payment, error) { chanMsg := make(chan *lnrpc.Payment, 1) errChan := make(chan error, 1) @@ -926,16 +935,14 @@ func (h *HarnessTest) ReceivePaymentUpdate( select { case <-time.After(DefaultTimeout): require.Fail(h, "timeout", "timeout waiting for payment update") + return nil, nil case err := <-errChan: - require.Failf(h, "payment stream", - "received err from payment stream: %v", err) + return nil, err case updateMsg := <-chanMsg: - return updateMsg + return updateMsg, nil } - - return nil } // AssertInvoiceSettled asserts a given invoice specified by its payment @@ -1724,3 +1731,346 @@ func (h *HarnessTest) CreateBurnAddr(addrType lnrpc.AddressType) ([]byte, return h.PayToAddrScript(addr), addr } + +// ReceiveTrackPayment waits until a message is received on the track payment +// stream or the timeout is reached. +func (h *HarnessTest) ReceiveTrackPayment( + stream rpc.TrackPaymentClient) *lnrpc.Payment { + + chanMsg := make(chan *lnrpc.Payment) + errChan := make(chan error) + go func() { + // Consume one message. This will block until the message is + // received. + resp, err := stream.Recv() + if err != nil { + errChan <- err + return + } + chanMsg <- resp + }() + + select { + case <-time.After(DefaultTimeout): + require.Fail(h, "timeout", "timeout trakcing payment") + + case err := <-errChan: + require.Failf(h, "err from stream", + "received err from stream: %v", err) + + case updateMsg := <-chanMsg: + return updateMsg + } + + return nil +} + +// ReceiveHtlcEvent waits until a message is received on the subscribe +// htlc event stream or the timeout is reached. +func (h *HarnessTest) ReceiveHtlcEvent( + stream rpc.HtlcEventsClient) *routerrpc.HtlcEvent { + + chanMsg := make(chan *routerrpc.HtlcEvent) + errChan := make(chan error) + go func() { + // Consume one message. This will block until the message is + // received. + resp, err := stream.Recv() + if err != nil { + errChan <- err + return + } + chanMsg <- resp + }() + + select { + case <-time.After(DefaultTimeout): + require.Fail(h, "timeout", "timeout receiving htlc "+ + "event update") + + case err := <-errChan: + require.Failf(h, "err from stream", + "received err from stream: %v", err) + + case updateMsg := <-chanMsg: + return updateMsg + } + + return nil +} + +// AssertHtlcEventType consumes one event from a client and asserts the event +// type is matched. +func (h *HarnessTest) AssertHtlcEventType(client rpc.HtlcEventsClient, + userType routerrpc.HtlcEvent_EventType) *routerrpc.HtlcEvent { + + event := h.ReceiveHtlcEvent(client) + require.Equalf(h, userType, event.EventType, "wrong event type, "+ + "want %v got %v", userType, event.EventType) + + return event +} + +// HtlcEvent maps the series of event types used in `*routerrpc.HtlcEvent_*`. +type HtlcEvent int + +const ( + HtlcEventForward HtlcEvent = iota + HtlcEventForwardFail + HtlcEventSettle + HtlcEventLinkFail + HtlcEventFinal +) + +// AssertHtlcEventType consumes one event from a client and asserts both the +// user event type the event.Event type is matched. +func (h *HarnessTest) AssertHtlcEventTypes(client rpc.HtlcEventsClient, + userType routerrpc.HtlcEvent_EventType, + eventType HtlcEvent) *routerrpc.HtlcEvent { + + event := h.ReceiveHtlcEvent(client) + require.Equalf(h, userType, event.EventType, "wrong event type, "+ + "want %v got %v", userType, event.EventType) + + var ok bool + + switch eventType { + case HtlcEventForward: + _, ok = event.Event.(*routerrpc.HtlcEvent_ForwardEvent) + + case HtlcEventForwardFail: + _, ok = event.Event.(*routerrpc.HtlcEvent_ForwardFailEvent) + + case HtlcEventSettle: + _, ok = event.Event.(*routerrpc.HtlcEvent_SettleEvent) + + case HtlcEventLinkFail: + _, ok = event.Event.(*routerrpc.HtlcEvent_LinkFailEvent) + + case HtlcEventFinal: + _, ok = event.Event.(*routerrpc.HtlcEvent_FinalHtlcEvent) + } + + require.Truef(h, ok, "wrong event type: %T, want %T", event.Event, + eventType) + + return event +} + +// AssertFeeReport checks that the fee report from the given node has the +// desired day, week, and month sum values. +func (h *HarnessTest) AssertFeeReport(hn *node.HarnessNode, + day, week, month int) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + feeReport, err := hn.RPC.LN.FeeReport(ctxt, &lnrpc.FeeReportRequest{}) + require.NoError(h, err, "unable to query for fee report") + + require.EqualValues(h, day, feeReport.DayFeeSum, "day fee mismatch") + require.EqualValues(h, week, feeReport.WeekFeeSum, "day week mismatch") + require.EqualValues(h, month, feeReport.MonthFeeSum, + "day month mismatch") +} + +// AssertHtlcEvents consumes events from a client and ensures that they are of +// the expected type and contain the expected number of forwards, forward +// failures and settles. +// +// TODO(yy): needs refactor to reduce its complexity. +func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient, + fwdCount, fwdFailCount, settleCount int, + userType routerrpc.HtlcEvent_EventType) []*routerrpc.HtlcEvent { + + var forwards, forwardFails, settles int + + numEvents := fwdCount + fwdFailCount + settleCount + events := make([]*routerrpc.HtlcEvent, 0) + + // It's either the userType or the unknown type. + // + // TODO(yy): maybe the FinalHtlcEvent shouldn't be in UNKNOWN type? + eventTypes := []routerrpc.HtlcEvent_EventType{ + userType, routerrpc.HtlcEvent_UNKNOWN, + } + + for i := 0; i < numEvents; i++ { + event := h.ReceiveHtlcEvent(client) + + require.Containsf(h, eventTypes, event.EventType, + "wrong event type, got %v", userType, event.EventType) + + events = append(events, event) + + switch e := event.Event.(type) { + case *routerrpc.HtlcEvent_ForwardEvent: + forwards++ + + case *routerrpc.HtlcEvent_ForwardFailEvent: + forwardFails++ + + case *routerrpc.HtlcEvent_SettleEvent: + settles++ + + case *routerrpc.HtlcEvent_FinalHtlcEvent: + if e.FinalHtlcEvent.Settled { + settles++ + } + + default: + require.Fail(h, "assert event fail", + "unexpected event: %T", event.Event) + } + } + + require.Equal(h, fwdCount, forwards, "num of forwards mismatch") + require.Equal(h, fwdFailCount, forwardFails, + "num of forward fails mismatch") + require.Equal(h, settleCount, settles, "num of settles mismatch") + + return events +} + +// AssertTransactionInWallet asserts a given txid can be found in the node's +// wallet. +func (h *HarnessTest) AssertTransactionInWallet(hn *node.HarnessNode, + txid chainhash.Hash) { + + req := &lnrpc.GetTransactionsRequest{} + err := wait.NoError(func() error { + txResp := hn.RPC.GetTransactions(req) + for _, txn := range txResp.Transactions { + if txn.TxHash == txid.String() { + return nil + } + } + + return fmt.Errorf("%s: expected txid=%v not found in wallet", + hn.Name(), txid) + }, DefaultTimeout) + + require.NoError(h, err, "failed to find tx") +} + +// AssertTransactionNotInWallet asserts a given txid can NOT be found in the +// node's wallet. +func (h *HarnessTest) AssertTransactionNotInWallet(hn *node.HarnessNode, + txid chainhash.Hash) { + + req := &lnrpc.GetTransactionsRequest{} + err := wait.NoError(func() error { + txResp := hn.RPC.GetTransactions(req) + for _, txn := range txResp.Transactions { + if txn.TxHash == txid.String() { + return fmt.Errorf("expected txid=%v to be "+ + "not found", txid) + } + } + + return nil + }, DefaultTimeout) + + require.NoErrorf(h, err, "%s: failed to assert tx not found", hn.Name()) +} + +// WaitForNodeBlockHeight queries the node for its current block height until +// it reaches the passed height. +func (h *HarnessTest) WaitForNodeBlockHeight(hn *node.HarnessNode, + height int32) { + + err := wait.NoError(func() error { + info := hn.RPC.GetInfo() + if int32(info.BlockHeight) != height { + return fmt.Errorf("expected block height to "+ + "be %v, was %v", height, info.BlockHeight) + } + + return nil + }, DefaultTimeout) + + require.NoErrorf(h, err, "%s: timeout while waiting for height", + hn.Name()) +} + +// AssertChannelCommitHeight asserts the given channel for the node has the +// expected commit height(`NumUpdates`). +func (h *HarnessTest) AssertChannelCommitHeight(hn *node.HarnessNode, + cp *lnrpc.ChannelPoint, height int) { + + err := wait.NoError(func() error { + c, err := h.findChannel(hn, cp) + if err != nil { + return err + } + + if int(c.NumUpdates) == height { + return nil + } + + return fmt.Errorf("expected commit height to be %v, was %v", + height, c.NumUpdates) + }, DefaultTimeout) + + require.NoError(h, err, "timeout while waiting for commit height") +} + +// AssertNumInvoices asserts that the number of invoices made within the test +// scope is as expected. +func (h *HarnessTest) AssertNumInvoices(hn *node.HarnessNode, + num int) []*lnrpc.Invoice { + + have := hn.State.Invoice.Total + req := &lnrpc.ListInvoiceRequest{ + NumMaxInvoices: math.MaxUint64, + IndexOffset: hn.State.Invoice.LastIndexOffset, + } + + var invoices []*lnrpc.Invoice + err := wait.NoError(func() error { + resp := hn.RPC.ListInvoices(req) + + invoices = resp.Invoices + if len(invoices) == num { + return nil + } + + return errNumNotMatched(hn.Name(), "num of invoices", + num, len(invoices), have+len(invoices), have) + }, DefaultTimeout) + require.NoError(h, err, "timeout checking num of invoices") + + return invoices +} + +// ReceiveSendToRouteUpdate waits until a message is received on the +// SendToRoute client stream or the timeout is reached. +func (h *HarnessTest) ReceiveSendToRouteUpdate( + stream rpc.SendToRouteClient) (*lnrpc.SendResponse, error) { + + chanMsg := make(chan *lnrpc.SendResponse, 1) + errChan := make(chan error, 1) + go func() { + // Consume one message. This will block until the message is + // received. + resp, err := stream.Recv() + if err != nil { + errChan <- err + + return + } + chanMsg <- resp + }() + + select { + case <-time.After(DefaultTimeout): + require.Fail(h, "timeout", "timeout waiting for send resp") + return nil, nil + + case err := <-errChan: + return nil, err + + case updateMsg := <-chanMsg: + return updateMsg, nil + } +} diff --git a/lntemp/harness_miner.go b/lntemp/harness_miner.go index bcccb8656..916b57500 100644 --- a/lntemp/harness_miner.go +++ b/lntemp/harness_miner.go @@ -57,6 +57,16 @@ func NewMiner(ctxt context.Context, t *testing.T) *HarnessMiner { return newMiner(ctxt, t, minerLogDir, minerLogFilename) } +// NewTempMiner creates a new miner using btcd backend with the specified log +// file dir and name. +func NewTempMiner(ctxt context.Context, t *testing.T, + tempDir, tempLogFilename string) *HarnessMiner { + + t.Helper() + + return newMiner(ctxt, t, tempDir, tempLogFilename) +} + // newMiner creates a new miner using btcd's rpctest. func newMiner(ctxb context.Context, t *testing.T, minerDirName, logFilename string) *HarnessMiner { @@ -383,3 +393,35 @@ func (h *HarnessMiner) NewMinerAddress() btcutil.Address { require.NoError(h, err, "failed to create new miner address") return addr } + +// MineBlocksWithTxes mines a single block to include the specifies +// transactions only. +func (h *HarnessMiner) MineBlockWithTxes(txes []*btcutil.Tx) *wire.MsgBlock { + var emptyTime time.Time + + // Generate a block. + b, err := h.GenerateAndSubmitBlock(txes, -1, emptyTime) + require.NoError(h, err, "unable to mine block") + + block, err := h.Client.GetBlock(b.Hash()) + require.NoError(h, err, "unable to get block") + + return block +} + +// MineEmptyBlocks mines a given number of empty blocks. +func (h *HarnessMiner) MineEmptyBlocks(num int) []*wire.MsgBlock { + var emptyTime time.Time + + blocks := make([]*wire.MsgBlock, num) + for i := 0; i < num; i++ { + // Generate an empty block. + b, err := h.GenerateAndSubmitBlock(nil, -1, emptyTime) + require.NoError(h, err, "unable to mine empty block") + + block := h.GetBlock(b.Hash()) + blocks[i] = block + } + + return blocks +} diff --git a/lntemp/harness_node_manager.go b/lntemp/harness_node_manager.go index 2b5f87e08..1ee502354 100644 --- a/lntemp/harness_node_manager.go +++ b/lntemp/harness_node_manager.go @@ -71,8 +71,7 @@ func (nm *nodeManager) nextNodeID() uint32 { // node can be used immediately. Otherwise, the node will require an additional // initialization phase where the wallet is either created or restored. func (nm *nodeManager) newNode(t *testing.T, name string, extraArgs []string, - password []byte, useSeed bool, - opts ...node.Option) (*node.HarnessNode, error) { + password []byte, noAuth bool) (*node.HarnessNode, error) { cfg := &node.BaseNodeConfig{ Name: name, @@ -85,10 +84,7 @@ func (nm *nodeManager) newNode(t *testing.T, name string, extraArgs []string, NodeID: nm.nextNodeID(), LndBinary: nm.lndBinary, NetParams: harnessNetParams, - HasSeed: useSeed, - } - for _, opt := range opts { - opt(cfg) + SkipUnlock: noAuth, } node, err := node.NewHarnessNode(t, cfg) @@ -129,56 +125,11 @@ func (nm *nodeManager) shutdownNode(node *node.HarnessNode) error { // the connection attempt is successful. If the callback parameter is non-nil, // then the function will be executed after the node shuts down, but *before* // the process has been started up again. -// -// This method can be useful when testing edge cases such as a node broadcast -// and invalidated prior state, or persistent state recovery, simulating node -// crashes, etc. Additionally, each time the node is restarted, the caller can -// pass a set of SCBs to pass in via the Unlock method allowing them to restore -// channels during restart. -func (nm *nodeManager) restartNode(ctxt context.Context, node *node.HarnessNode, - callback func() error, chanBackups ...*lnrpc.ChanBackupSnapshot) error { +func (nm *nodeManager) restartNode(ctxt context.Context, + hn *node.HarnessNode, callback func() error) error { - err := nm.restartNodeNoUnlock(ctxt, node, callback) - if err != nil { - return err - } - - // If the node doesn't have a password set, then we can exit here as we - // don't need to unlock it. - if len(node.Cfg.Password) == 0 { - return nil - } - - // Otherwise, we'll unlock the wallet, then complete the final steps - // for the node initialization process. - unlockReq := &lnrpc.UnlockWalletRequest{ - WalletPassword: node.Cfg.Password, - } - if len(chanBackups) != 0 { - unlockReq.ChannelBackups = chanBackups[0] - unlockReq.RecoveryWindow = 100 - } - - err = wait.NoError(func() error { - return node.Unlock(unlockReq) - }, DefaultTimeout) - if err != nil { - return fmt.Errorf("%s: failed to unlock: %w", node.Name(), err) - } - - return nil -} - -// restartNodeNoUnlock attempts to restart a lightning node by shutting it down -// cleanly, then restarting the process. In case the node was setup with a -// seed, it will be left in the unlocked state. This function is fully -// blocking. If the callback parameter is non-nil, then the function will be -// executed after the node shuts down, but *before* the process has been -// started up again. -func (nm *nodeManager) restartNodeNoUnlock(ctxt context.Context, - node *node.HarnessNode, callback func() error) error { - - if err := node.Stop(); err != nil { + // Stop the node. + if err := hn.Stop(); err != nil { return fmt.Errorf("restart node got error: %w", err) } @@ -188,11 +139,45 @@ func (nm *nodeManager) restartNodeNoUnlock(ctxt context.Context, } } - if node.Cfg.HasSeed { - return node.StartWithSeed(ctxt) + // Start the node without unlocking the wallet. + if hn.Cfg.SkipUnlock { + return hn.StartWithNoAuth(ctxt) } - return node.Start(ctxt) + return hn.Start(ctxt) +} + +// unlockNode unlocks the node's wallet if the password is configured. +// Additionally, each time the node is unlocked, the caller can pass a set of +// SCBs to pass in via the Unlock method allowing them to restore channels +// during restart. +func (nm *nodeManager) unlockNode(hn *node.HarnessNode, + chanBackups ...*lnrpc.ChanBackupSnapshot) error { + + // If the node doesn't have a password set, then we can exit here as we + // don't need to unlock it. + if len(hn.Cfg.Password) == 0 { + return nil + } + + // Otherwise, we'll unlock the wallet, then complete the final steps + // for the node initialization process. + unlockReq := &lnrpc.UnlockWalletRequest{ + WalletPassword: hn.Cfg.Password, + } + if len(chanBackups) != 0 { + unlockReq.ChannelBackups = chanBackups[0] + unlockReq.RecoveryWindow = 100 + } + + err := wait.NoError(func() error { + return hn.Unlock(unlockReq) + }, DefaultTimeout) + if err != nil { + return fmt.Errorf("%s: failed to unlock: %w", hn.Name(), err) + } + + return nil } // initWalletAndNode will unlock the node's wallet and finish setting up the diff --git a/lntemp/node/config.go b/lntemp/node/config.go index f9798ede4..c0f12d70b 100644 --- a/lntemp/node/config.go +++ b/lntemp/node/config.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/kvdb/etcd" "github.com/lightningnetwork/lnd/lntest" ) @@ -62,8 +63,8 @@ type BaseNodeConfig struct { ReadMacPath string InvoiceMacPath string - HasSeed bool - Password []byte + SkipUnlock bool + Password []byte P2PPort int RPCPort int @@ -189,7 +190,7 @@ func (cfg *BaseNodeConfig) GenArgs() []string { } args = append(args, nodeArgs...) - if !cfg.HasSeed { + if cfg.Password == nil { args = append(args, "--noseedbackup") } @@ -232,3 +233,33 @@ func (cfg *BaseNodeConfig) GenArgs() []string { return args } + +// ExtraArgsEtcd returns extra args for configuring LND to use an external etcd +// database (for remote channel DB and wallet DB). +func ExtraArgsEtcd(etcdCfg *etcd.Config, name string, cluster bool, + leaderSessionTTL int) []string { + + extraArgs := []string{ + "--db.backend=etcd", + fmt.Sprintf("--db.etcd.host=%v", etcdCfg.Host), + fmt.Sprintf("--db.etcd.user=%v", etcdCfg.User), + fmt.Sprintf("--db.etcd.pass=%v", etcdCfg.Pass), + fmt.Sprintf("--db.etcd.namespace=%v", etcdCfg.Namespace), + } + + if etcdCfg.InsecureSkipVerify { + extraArgs = append(extraArgs, "--db.etcd.insecure_skip_verify") + } + + if cluster { + clusterArgs := []string{ + "--cluster.enable-leader-election", + fmt.Sprintf("--cluster.id=%v", name), + fmt.Sprintf("--cluster.leader-session-ttl=%v", + leaderSessionTTL), + } + extraArgs = append(extraArgs, clusterArgs...) + } + + return extraArgs +} diff --git a/lntemp/node/harness_node.go b/lntemp/node/harness_node.go index 0cb8f2341..1dc484bed 100644 --- a/lntemp/node/harness_node.go +++ b/lntemp/node/harness_node.go @@ -215,6 +215,33 @@ func (hn *HarnessNode) WaitUntilServerActive() error { }) } +// WaitUntilLeader attempts to finish the start procedure by initiating an RPC +// connection and setting up the wallet unlocker client. This is needed when +// a node that has recently been started was waiting to become the leader and +// we're at the point when we expect that it is the leader now (awaiting +// unlock). +func (hn *HarnessNode) WaitUntilLeader(timeout time.Duration) error { + var ( + conn *grpc.ClientConn + connErr error + ) + + if err := wait.NoError(func() error { + conn, connErr = hn.ConnectRPCWithMacaroon(nil) + return connErr + }, timeout); err != nil { + return err + } + + // Since the conn is not authed, only the `WalletUnlocker` and `State` + // clients can be inited from this conn. + hn.conn = conn + hn.RPC = rpc.NewHarnessRPC(hn.runCtx, hn.T, conn, hn.Name()) + + // Wait till the server is starting. + return hn.WaitUntilStarted() +} + // Unlock attempts to unlock the wallet of the target HarnessNode. This method // should be called after the restart of a HarnessNode that was created with a // seed+password. Once this method returns, the HarnessNode will be ready to @@ -360,12 +387,12 @@ func (hn *HarnessNode) StartLndCmd(ctxb context.Context) error { return nil } -// StartWithSeed will start the lnd process, creates the grpc connection +// StartWithNoAuth will start the lnd process, creates the grpc connection // without macaroon auth, and waits until the server is reported as waiting to // start. // // NOTE: caller needs to take extra step to create and unlock the wallet. -func (hn *HarnessNode) StartWithSeed(ctxt context.Context) error { +func (hn *HarnessNode) StartWithNoAuth(ctxt context.Context) error { // Start lnd process and prepare logs. if err := hn.StartLndCmd(ctxt); err != nil { return fmt.Errorf("start lnd error: %w", err) @@ -399,7 +426,7 @@ func (hn *HarnessNode) Start(ctxt context.Context) error { conn, err := hn.ConnectRPC() if err != nil { err = fmt.Errorf("ConnectRPC err: %w", err) - cmdErr := hn.kill() + cmdErr := hn.Kill() if cmdErr != nil { err = fmt.Errorf("kill process got err: %w: %v", cmdErr, err) @@ -453,9 +480,27 @@ func (hn *HarnessNode) InitNode(macBytes []byte) error { // Init all the RPC clients. hn.InitRPCClients(conn) + // Wait till the server is starting. + if err := hn.WaitUntilStarted(); err != nil { + return fmt.Errorf("waiting for start got: %w", err) + } + return hn.initLightningClient() } +// InitChangePassword initializes a harness node by passing the change password +// request via RPC. After the request is submitted, this method will block until +// a macaroon-authenticated RPC connection can be established to the harness +// node. Once established, the new connection is used to initialize the +// RPC clients and subscribes the HarnessNode to topology changes. +func (hn *HarnessNode) ChangePasswordAndInit( + req *lnrpc.ChangePasswordRequest) ( + *lnrpc.ChangePasswordResponse, error) { + + response := hn.RPC.ChangePassword(req) + return response, hn.InitNode(response.AdminMacaroon) +} + // waitTillServerState makes a subscription to the server's state change and // blocks until the server is in the targeted state. func (hn *HarnessNode) waitTillServerState( @@ -670,7 +715,7 @@ func (hn *HarnessNode) Stop() error { // If the rpc clients are not initiated, we'd kill the process // manually. hn.printErrf("found nil RPC clients") - if err := hn.kill(); err != nil { + if err := hn.Kill(); err != nil { return fmt.Errorf("killing process got: %v", err) } } @@ -719,8 +764,8 @@ func (hn *HarnessNode) Shutdown() error { return nil } -// kill kills the lnd process. -func (hn *HarnessNode) kill() error { +// Kill kills the lnd process. +func (hn *HarnessNode) Kill() error { return hn.cmd.Process.Kill() } diff --git a/lntemp/rpc/invoices.go b/lntemp/rpc/invoices.go index 41c5bb1fd..5b771968e 100644 --- a/lntemp/rpc/invoices.go +++ b/lntemp/rpc/invoices.go @@ -5,6 +5,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" ) // ===================== @@ -82,3 +83,19 @@ func (h *HarnessRPC) SubscribeSingleInvoice(rHash []byte) SingleInvoiceClient { return client } + +type TrackPaymentClient routerrpc.Router_TrackPaymentV2Client + +// TrackPaymentV2 creates a subscription client for given invoice and +// asserts its creation. +func (h *HarnessRPC) TrackPaymentV2(payHash []byte) TrackPaymentClient { + req := &routerrpc.TrackPaymentRequest{PaymentHash: payHash} + + // TrackPaymentV2 needs to have the context alive for the entire test + // case as the returned client will be used for send and receive events + // stream. Thus we use runCtx here instead of a timeout context. + client, err := h.Router.TrackPaymentV2(h.runCtx, req) + h.NoError(err, "TrackPaymentV2") + + return client +} diff --git a/lntemp/rpc/lnd.go b/lntemp/rpc/lnd.go index 63ab65b90..7a95fbe35 100644 --- a/lntemp/rpc/lnd.go +++ b/lntemp/rpc/lnd.go @@ -194,6 +194,10 @@ func (h *HarnessRPC) ListInvoices( ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) defer cancel() + if req == nil { + req = &lnrpc.ListInvoiceRequest{} + } + resp, err := h.LN.ListInvoices(ctxt, req) h.NoError(err, "ListInvoice") @@ -481,8 +485,10 @@ func (h *HarnessRPC) QueryRoutes( return routes } +type SendToRouteClient lnrpc.Lightning_SendToRouteClient + // SendToRoute makes a RPC call to SendToRoute and asserts. -func (h *HarnessRPC) SendToRoute() lnrpc.Lightning_SendToRouteClient { +func (h *HarnessRPC) SendToRoute() SendToRouteClient { // SendToRoute needs to have the context alive for the entire test case // as the returned client will be used for send and receive payment // stream. Thus we use runCtx here instead of a timeout context. @@ -567,3 +573,61 @@ func (h *HarnessRPC) VerifyChanBackup( return resp } + +// LookupInvoice queries the node's invoices using the specified rHash. +func (h *HarnessRPC) LookupInvoice(rHash []byte) *lnrpc.Invoice { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + payHash := &lnrpc.PaymentHash{RHash: rHash} + resp, err := h.LN.LookupInvoice(ctxt, payHash) + h.NoError(err, "LookupInvoice") + + return resp +} + +// DecodePayReq makes a RPC call to node's DecodePayReq and asserts. +func (h *HarnessRPC) DecodePayReq(req string) *lnrpc.PayReq { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + payReq := &lnrpc.PayReqString{PayReq: req} + resp, err := h.LN.DecodePayReq(ctxt, payReq) + h.NoError(err, "DecodePayReq") + + return resp +} + +// ForwardingHistory makes a RPC call to the node's ForwardingHistory and +// asserts. +func (h *HarnessRPC) ForwardingHistory( + req *lnrpc.ForwardingHistoryRequest) *lnrpc.ForwardingHistoryResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + if req == nil { + req = &lnrpc.ForwardingHistoryRequest{} + } + + resp, err := h.LN.ForwardingHistory(ctxt, req) + h.NoError(err, "ForwardingHistory") + + return resp +} + +type MiddlewareClient lnrpc.Lightning_RegisterRPCMiddlewareClient + +// RegisterRPCMiddleware makes a RPC call to the node's RegisterRPCMiddleware +// and asserts. It also returns a cancel context which can cancel the context +// used by the client. +func (h *HarnessRPC) RegisterRPCMiddleware() (MiddlewareClient, + context.CancelFunc) { + + ctxt, cancel := context.WithCancel(h.runCtx) + + stream, err := h.LN.RegisterRPCMiddleware(ctxt) + h.NoError(err, "RegisterRPCMiddleware") + + return stream, cancel +} diff --git a/lntemp/rpc/router.go b/lntemp/rpc/router.go index 77c2d8f01..368ceffae 100644 --- a/lntemp/rpc/router.go +++ b/lntemp/rpc/router.go @@ -3,7 +3,9 @@ package rpc import ( "context" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/stretchr/testify/require" ) // ===================== @@ -12,6 +14,8 @@ import ( // UpdateChanStatus makes a UpdateChanStatus RPC call to node's RouterClient // and asserts. +// +//nolint:lll func (h *HarnessRPC) UpdateChanStatus( req *routerrpc.UpdateChanStatusRequest) *routerrpc.UpdateChanStatusResponse { @@ -39,3 +43,110 @@ func (h *HarnessRPC) SendPayment( return stream } + +type HtlcEventsClient routerrpc.Router_SubscribeHtlcEventsClient + +// SubscribeHtlcEvents makes a subscription to the HTLC events and returns a +// htlc event client. +func (h *HarnessRPC) SubscribeHtlcEvents() HtlcEventsClient { + // Use runCtx here to keep the client alive for the scope of the test. + client, err := h.Router.SubscribeHtlcEvents( + h.runCtx, &routerrpc.SubscribeHtlcEventsRequest{}, + ) + h.NoError(err, "SubscribeHtlcEvents") + + return client +} + +// GetMissionControlConfig makes a RPC call to the node's +// GetMissionControlConfig and asserts. +// +//nolint:lll +func (h *HarnessRPC) GetMissionControlConfig() *routerrpc.GetMissionControlConfigResponse { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &routerrpc.GetMissionControlConfigRequest{} + resp, err := h.Router.GetMissionControlConfig(ctxt, req) + h.NoError(err, "GetMissionControlConfig") + + return resp +} + +// SetMissionControlConfig makes a RPC call to the node's +// SetMissionControlConfig and asserts. +func (h *HarnessRPC) SetMissionControlConfig( + config *routerrpc.MissionControlConfig) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &routerrpc.SetMissionControlConfigRequest{Config: config} + _, err := h.Router.SetMissionControlConfig(ctxt, req) + h.NoError(err, "SetMissionControlConfig") +} + +// ResetMissionControl makes a RPC call to the node's ResetMissionControl and +// asserts. +func (h *HarnessRPC) ResetMissionControl() { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &routerrpc.ResetMissionControlRequest{} + _, err := h.Router.ResetMissionControl(ctxt, req) + h.NoError(err, "ResetMissionControl") +} + +// SendToRouteV2 makes a RPC call to SendToRouteV2 and asserts. +func (h *HarnessRPC) SendToRouteV2( + req *routerrpc.SendToRouteRequest) *lnrpc.HTLCAttempt { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.Router.SendToRouteV2(ctxt, req) + h.NoError(err, "SendToRouteV2") + + return resp +} + +// QueryProbability makes a RPC call to the node's QueryProbability and +// asserts. +// +//nolint:lll +func (h *HarnessRPC) QueryProbability( + req *routerrpc.QueryProbabilityRequest) *routerrpc.QueryProbabilityResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.Router.QueryProbability(ctxt, req) + h.NoError(err, "QueryProbability") + + return resp +} + +// XImportMissionControl makes a RPC call to the node's XImportMissionControl +// and asserts. +func (h *HarnessRPC) XImportMissionControl( + req *routerrpc.XImportMissionControlRequest) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XImportMissionControl(ctxt, req) + h.NoError(err, "XImportMissionControl") +} + +// XImportMissionControlAssertErr makes a RPC call to the node's +// XImportMissionControl +// and asserts an error occurred. +func (h *HarnessRPC) XImportMissionControlAssertErr( + req *routerrpc.XImportMissionControlRequest) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.Router.XImportMissionControl(ctxt, req) + require.Error(h, err, "expect an error from x import mission control") +} diff --git a/lntemp/rpc/wallet_unlocker.go b/lntemp/rpc/wallet_unlocker.go index 726abef85..c4fb60c86 100644 --- a/lntemp/rpc/wallet_unlocker.go +++ b/lntemp/rpc/wallet_unlocker.go @@ -48,3 +48,17 @@ func (h *HarnessRPC) GenSeed(req *lnrpc.GenSeedRequest) *lnrpc.GenSeedResponse { return resp } + +// ChangePassword makes a RPC request to WalletUnlocker and asserts there's no +// error. +func (h *HarnessRPC) ChangePassword( + req *lnrpc.ChangePasswordRequest) *lnrpc.ChangePasswordResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WalletUnlocker.ChangePassword(ctxt, req) + h.NoError(err, "ChangePassword") + + return resp +} diff --git a/lntemp/rpc/watchtower.go b/lntemp/rpc/watchtower.go index c18734d94..1b05d15ea 100644 --- a/lntemp/rpc/watchtower.go +++ b/lntemp/rpc/watchtower.go @@ -1,5 +1,52 @@ package rpc +import ( + "context" + + "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" +) + // ===================== // WatchtowerClient and WatchtowerClientClient related RPCs. // ===================== + +// GetInfoWatchtower makes a RPC call to the watchtower of the given node and +// asserts. +func (h *HarnessRPC) GetInfoWatchtower() *watchtowerrpc.GetInfoResponse { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &watchtowerrpc.GetInfoRequest{} + info, err := h.Watchtower.GetInfo(ctxt, req) + h.NoError(err, "GetInfo from Watchtower") + + return info +} + +// AddTower makes a RPC call to the WatchtowerClient of the given node and +// asserts. +func (h *HarnessRPC) AddTower( + req *wtclientrpc.AddTowerRequest) *wtclientrpc.AddTowerResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WatchtowerClient.AddTower(ctxt, req) + h.NoError(err, "AddTower") + + return resp +} + +// WatchtowerStats makes a RPC call to the WatchtowerClient of the given node +// and asserts. +func (h *HarnessRPC) WatchtowerStats() *wtclientrpc.StatsResponse { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &wtclientrpc.StatsRequest{} + resp, err := h.WatchtowerClient.Stats(ctxt, req) + h.NoError(err, "Stats from Watchtower") + + return resp +} diff --git a/lntest/harness_node.go b/lntest/harness_node.go index 1eb0b9ac8..67cffe1bb 100644 --- a/lntest/harness_node.go +++ b/lntest/harness_node.go @@ -487,7 +487,7 @@ func executePgQuery(query string) error { postgresDatabaseDsn("postgres"), ) if err != nil { - return fmt.Errorf("unable to connect to database: %v", err) + return fmt.Errorf("unable to connect to database: %w", err) } defer pool.Close() diff --git a/lntest/itest/assertions.go b/lntest/itest/assertions.go index 0be40d324..feae9e160 100644 --- a/lntest/itest/assertions.go +++ b/lntest/itest/assertions.go @@ -690,41 +690,6 @@ func assertChannelPolicy(t *harnessTest, node *lntest.HarnessNode, } } -// assertMinerBlockHeightDelta ensures that tempMiner is 'delta' blocks ahead -// of miner. -func assertMinerBlockHeightDelta(t *harnessTest, - miner, tempMiner *lntest.HarnessMiner, delta int32) { - - // Ensure the chain lengths are what we expect. - var predErr error - err := wait.Predicate(func() bool { - _, tempMinerHeight, err := tempMiner.Client.GetBestBlock() - if err != nil { - predErr = fmt.Errorf("unable to get current "+ - "blockheight %v", err) - return false - } - - _, minerHeight, err := miner.Client.GetBestBlock() - if err != nil { - predErr = fmt.Errorf("unable to get current "+ - "blockheight %v", err) - return false - } - - if tempMinerHeight != minerHeight+delta { - predErr = fmt.Errorf("expected new miner(%d) to be %d "+ - "blocks ahead of original miner(%d)", - tempMinerHeight, delta, minerHeight) - return false - } - return true - }, defaultTimeout) - if err != nil { - t.Fatalf(predErr.Error()) - } -} - func checkCommitmentMaturity( forceClose *lnrpc.PendingChannelsResponse_ForceClosedChannel, maturityHeight uint32, blocksTilMaturity int32) error { @@ -1418,30 +1383,6 @@ func assertTransactionNotInWallet(t *testing.T, node *lntest.HarnessNode, ) } -func assertAnchorOutputLost(t *harnessTest, node *lntest.HarnessNode, - chanPoint wire.OutPoint) { - - pendingChansRequest := &lnrpc.PendingChannelsRequest{} - err := wait.Predicate(func() bool { - resp, pErr := node.PendingChannels( - context.Background(), pendingChansRequest, - ) - if pErr != nil { - return false - } - - for _, pendingChan := range resp.PendingForceClosingChannels { - if pendingChan.Channel.ChannelPoint == chanPoint.String() { - return (pendingChan.Anchor == - lnrpc.PendingChannelsResponse_ForceClosedChannel_LOST) - } - } - - return false - }, defaultTimeout) - require.NoError(t.t, err, "anchor doesn't show as being lost") -} - // assertNodeAnnouncement compares that two node announcements match. func assertNodeAnnouncement(t *harnessTest, n1, n2 *lnrpc.NodeUpdate) { // Alias should match. diff --git a/lntest/itest/list_on_test.go b/lntest/itest/list_on_test.go index 91e808e5e..151affe52 100644 --- a/lntest/itest/list_on_test.go +++ b/lntest/itest/list_on_test.go @@ -231,4 +231,142 @@ var allTestCasesTemp = []*lntemp.TestCase{ 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 external", + TestFunc: testPsbtChanFundingExternal, + }, + { + Name: "psbt channel funding single step", + TestFunc: testPsbtChanFundingSingleStep, + }, + { + 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, + }, } diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index f99b37af1..d254bb9f2 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -349,8 +349,9 @@ func testChannelBackupRestoreBasic(ht *lntemp.HarnessTest) { "", revocationWindow, nil, copyPorts(oldNode), ) - - st.RestartNode(newNode, backupSnapshot) + st.RestartNodeWithChanBackups( + newNode, backupSnapshot, + ) return newNode } diff --git a/lntest/itest/lnd_channel_force_close_test.go b/lntest/itest/lnd_channel_force_close_test.go index 4c87ef973..0857751d4 100644 --- a/lntest/itest/lnd_channel_force_close_test.go +++ b/lntest/itest/lnd_channel_force_close_test.go @@ -15,7 +15,6 @@ import ( "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/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -984,57 +983,6 @@ func padCLTV(cltv uint32) uint32 { return cltv + uint32(routing.BlockPadding) } -type sweptOutput struct { - OutPoint wire.OutPoint - SweepTx *wire.MsgTx -} - -// findCommitAndAnchor looks for a commitment sweep and anchor sweep in the -// mempool. Our anchor output is identified by having multiple inputs, because -// we have to bring another input to add fees to the anchor. Note that the -// anchor swept output may be nil if the channel did not have anchors. -// TODO(yy): delete. -func findCommitAndAnchor(t *harnessTest, net *lntest.NetworkHarness, - sweepTxns []*wire.MsgTx, closeTx string) (*sweptOutput, *sweptOutput) { - - var commitSweep, anchorSweep *sweptOutput - - for _, tx := range sweepTxns { - txHash := tx.TxHash() - sweepTx, err := net.Miner.Client.GetRawTransaction(&txHash) - require.NoError(t.t, err) - - // We expect our commitment sweep to have a single input, and, - // our anchor sweep to have more inputs (because the wallet - // needs to add balance to the anchor amount). We find their - // sweep txids here to setup appropriate resolutions. We also - // need to find the outpoint for our resolution, which we do by - // matching the inputs to the sweep to the close transaction. - inputs := sweepTx.MsgTx().TxIn - if len(inputs) == 1 { - commitSweep = &sweptOutput{ - OutPoint: inputs[0].PreviousOutPoint, - SweepTx: tx, - } - } else { - // Since we have more than one input, we run through - // them to find the outpoint that spends from the close - // tx. This will be our anchor output. - for _, txin := range inputs { - outpointStr := txin.PreviousOutPoint.Hash.String() - if outpointStr == closeTx { - anchorSweep = &sweptOutput{ - OutPoint: txin.PreviousOutPoint, - SweepTx: tx, - } - } - } - } - } - - return commitSweep, anchorSweep -} - // testFailingChannel tests that we will fail the channel by force closing it // in the case where a counterparty tries to settle an HTLC with the wrong // preimage. diff --git a/lntest/itest/lnd_channel_policy_test.go b/lntest/itest/lnd_channel_policy_test.go index 236f0e73b..faac28c39 100644 --- a/lntest/itest/lnd_channel_policy_test.go +++ b/lntest/itest/lnd_channel_policy_test.go @@ -199,7 +199,7 @@ func testUpdateChannelPolicy(ht *lntemp.HarnessTest) { // 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 := alicePayStream.Recv() + sendResp, err := ht.ReceiveSendToRouteUpdate(alicePayStream) require.NoError(ht, err, "unable to receive payment stream") // Expected as part of the error message. @@ -238,7 +238,7 @@ func testUpdateChannelPolicy(ht *lntemp.HarnessTest) { err = alicePayStream.Send(sendReq) require.NoError(ht, err, "unable to send payment") - sendResp, err = alicePayStream.Recv() + sendResp, err = ht.ReceiveSendToRouteUpdate(alicePayStream) require.NoError(ht, err, "unable to receive payment stream") require.Empty(ht, sendResp.PaymentError, "expected payment to succeed") diff --git a/lntest/itest/lnd_etcd_failover_test.go b/lntest/itest/lnd_etcd_failover_test.go index 4de2d4490..90bd5fbbe 100644 --- a/lntest/itest/lnd_etcd_failover_test.go +++ b/lntest/itest/lnd_etcd_failover_test.go @@ -4,7 +4,6 @@ package itest import ( - "context" "testing" "time" @@ -13,27 +12,24 @@ import ( "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lntest" + "github.com/stretchr/testify/require" ) -func assertLeader(ht *harnessTest, observer cluster.LeaderElector, +func assertLeader(ht *lntemp.HarnessTest, observer cluster.LeaderElector, expected string) { - leader, err := observer.Leader(context.Background()) - if err != nil { - ht.Fatalf("Unable to query leader: %v", err) - } - - if leader != expected { - ht.Fatalf("Leader should be '%v', got: '%v'", expected, leader) - } + 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(net *lntest.NetworkHarness, ht *harnessTest) { +func testEtcdFailover(ht *lntemp.HarnessTest) { testCases := []struct { name string kill bool @@ -48,148 +44,97 @@ func testEtcdFailover(net *lntest.NetworkHarness, ht *harnessTest) { for _, test := range testCases { test := test - ht.t.Run(test.name, func(t1 *testing.T) { - ht1 := newHarnessTest(t1, ht.lndHarness) - ht1.RunTestCase(&testCase{ - name: test.name, - test: func(_ *lntest.NetworkHarness, - tt *harnessTest) { - - testEtcdFailoverCase(net, tt, test.kill) - }, - }) + success := ht.Run(test.name, func(t1 *testing.T) { + st := ht.Subtest(t1) + testEtcdFailoverCase(st, test.kill) }) + if !success { + return + } } } -func testEtcdFailoverCase(net *lntest.NetworkHarness, ht *harnessTest, - kill bool) { - - ctxb := context.Background() - +func testEtcdFailoverCase(ht *lntemp.HarnessTest, kill bool) { etcdCfg, cleanup, err := kvdb.StartEtcdTestBackend( - ht.t.TempDir(), uint16(lntest.NextAvailablePort()), + ht.T.TempDir(), uint16(lntest.NextAvailablePort()), uint16(lntest.NextAvailablePort()), "", ) - if err != nil { - ht.Fatalf("Failed to start etcd instance: %v", err) - } + 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( - ctxb, cluster.EtcdLeaderElector, "observer", + ht.Context(), cluster.EtcdLeaderElector, "observer", lncfg.DefaultEtcdElectionPrefix, leaderSessionTTL, etcdCfg, ) - if err != nil { - ht.Fatalf("Cannot start election observer: %v", err) - } + 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, _, _, err := net.NewNodeWithSeedEtcd( + carol1, _, _ := ht.NewNodeWithSeedEtcd( "Carol-1", etcdCfg, password, entropy[:], stateless, cluster, leaderSessionTTL, ) - if err != nil { - ht.Fatalf("unable to start Carol-1: %v", err) - } - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - info1, err := carol1.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) - if err != nil { - ht.Fatalf("unable to get info: %v", err) - } - - net.ConnectNodes(ht.t, carol1, net.Alice) + 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(100000) - _ = openChannelAndAssert( - ht, net, net.Alice, carol1, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) + 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, err := net.NewNodeEtcd( - "Carol-2", etcdCfg, password, cluster, false, leaderSessionTTL, + carol2 := ht.NewNodeEtcd( + "Carol-2", etcdCfg, password, cluster, leaderSessionTTL, ) - if err != nil { - ht.Fatalf("Unable to start Carol-2: %v", err) - } - assertLeader(ht, observer, "Carol-1") amt := btcutil.Amount(1000) - payReqs, _, _, err := createPayReqs(carol1, amt, 2) - if err != nil { - ht.Fatalf("Carol-2 is unable to create payment requests: %v", - err) - } - sendAndAssertSuccess( - ht, net.Alice, &routerrpc.SendPaymentRequest{ - PaymentRequest: payReqs[0], - TimeoutSeconds: 60, - FeeLimitSat: noFeeLimitMsat, - }, - ) + 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 { - err = net.KillNode(carol1) - if err != nil { - ht.Fatalf("Can't kill Carol-1: %v", err) - } + ht.KillNode(carol1) } else { - shutdownAndAssert(net, ht, carol1) + ht.Shutdown(carol1) } err = carol2.WaitUntilLeader(failoverTimeout) - if err != nil { - ht.Fatalf("Waiting for Carol-2 to become the leader failed: %v", - err) - } - + require.NoError(ht, err, "Waiting for Carol-2 to become the leader "+ + "failed") assertLeader(ht, observer, "Carol-2") - err = carol2.Unlock(&lnrpc.UnlockWalletRequest{ - WalletPassword: password, - }) - if err != nil { - ht.Fatalf("Unlocking Carol-2 was not successful: %v", err) - } - - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + 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, err := carol2.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) - if err != nil { - ht.Fatalf("unable to get info: %v", err) - } - if info1.IdentityPubkey != info2.IdentityPubkey { - ht.Fatalf("Carol-1 and Carol-2 must have the same identity: "+ - "%v vs %v", info1.IdentityPubkey, info2.IdentityPubkey) - } + 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. - sendAndAssertSuccess( - ht, net.Alice, &routerrpc.SendPaymentRequest{ - PaymentRequest: payReqs[1], - TimeoutSeconds: 60, - FeeLimitSat: noFeeLimitMsat, - }, - ) + ht.CompletePaymentRequests(alice, []string{payReqs[1]}) - shutdownAndAssert(net, ht, carol2) + // Manually shutdown the node as it will mess up with our cleanup + // process. + ht.Shutdown(carol2) } diff --git a/lntest/itest/lnd_hold_invoice_force_test.go b/lntest/itest/lnd_hold_invoice_force_test.go index 7fd911d16..613281305 100644 --- a/lntest/itest/lnd_hold_invoice_force_test.go +++ b/lntest/itest/lnd_hold_invoice_force_test.go @@ -1,14 +1,13 @@ package itest import ( - "context" "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/lntest" + "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -16,17 +15,11 @@ import ( // testHoldInvoiceForceClose tests cancellation of accepted hold invoices which // would otherwise trigger force closes when they expire. -func testHoldInvoiceForceClose(net *lntest.NetworkHarness, t *harnessTest) { - ctxb, cancel := context.WithCancel(context.Background()) - defer cancel() - +func testHoldInvoiceForceClose(ht *lntemp.HarnessTest) { // Open a channel between alice and bob. - chanReq := lntest.OpenChannelParams{ - Amt: 300000, - } - - chanPoint := openChannelAndAssert( - t, net, net.Alice, net.Bob, chanReq, + alice, bob := ht.Alice, ht.Bob + chanPoint := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: 300000}, ) // Create a non-dust hold invoice for bob. @@ -39,87 +32,76 @@ func testHoldInvoiceForceClose(net *lntest.NetworkHarness, t *harnessTest) { CltvExpiry: 40, Hash: payHash[:], } + bobInvoice := bob.RPC.AddHoldInvoice(invoiceReq) - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - bobInvoice, err := net.Bob.AddHoldInvoice(ctxt, invoiceReq) - require.NoError(t.t, err) + // Subscribe the invoice. + stream := bob.RPC.SubscribeSingleInvoice(payHash[:]) // Pay this invoice from Alice -> Bob, we should achieve this with a // single htlc. - _, err = net.Alice.RouterClient.SendPaymentV2( - ctxb, &routerrpc.SendPaymentRequest{ - PaymentRequest: bobInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - }, - ) - require.NoError(t.t, err) + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: bobInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) - waitForInvoiceAccepted(t, net.Bob, payHash) + ht.AssertInvoiceState(stream, lnrpc.Invoice_ACCEPTED) // Once the HTLC has cleared, alice and bob should both have a single // htlc locked in. - nodes := []*lntest.HarnessNode{net.Alice, net.Bob} - err = wait.NoError(func() error { - return assertActiveHtlcs(nodes, payHash[:]) - }, defaultTimeout) - require.NoError(t.t, err) + 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. - chans, err := net.Alice.ListChannels(ctxb, &lnrpc.ListChannelsRequest{}) - require.NoError(t.t, err) - require.Len(t.t, chans.Channels, 1) - require.Len(t.t, chans.Channels[0].PendingHtlcs, 1) - activeHtlc := chans.Channels[0].PendingHtlcs[0] + channel := ht.QueryChannelByChanPoint(alice, chanPoint) + require.Len(ht, channel.PendingHtlcs, 1) + activeHtlc := channel.PendingHtlcs[0] - require.NoError(t.t, net.Alice.WaitForBlockchainSync()) - require.NoError(t.t, net.Bob.WaitForBlockchainSync()) - - info, err := net.Alice.GetInfo(ctxb, &lnrpc.GetInfoRequest{}) - require.NoError(t.t, err) + _, 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( - t.t, activeHtlc.ExpirationHeight, info.BlockHeight, - "expected expiry after current height", - ) - blocksTillExpiry := activeHtlc.ExpirationHeight - info.BlockHeight + 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( - t.t, blocksTillExpiry, - uint32(lncfg.DefaultOutgoingBroadcastDelta), - ) - blocksTillForce := blocksTillExpiry - lncfg.DefaultOutgoingBroadcastDelta + require.Greater(ht, blocksTillExpiry, + uint32(lncfg.DefaultOutgoingBroadcastDelta)) - mineBlocksSlow(t, net, blocksTillForce, 0) + // 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 - require.NoError(t.t, net.Alice.WaitForBlockchainSync()) - require.NoError(t.t, net.Bob.WaitForBlockchainSync()) + // 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 - // Our channel should not have been force closed, instead we expect our - // channel to still be open and our invoice to have been canceled before - // expiry. - chanInfo, err := getChanInfo(net.Alice) - require.NoError(t.t, err) + // 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) - fundingTxID, err := lnrpc.GetChanPointFundingTxid(chanPoint) - require.NoError(t.t, err) - chanStr := fmt.Sprintf("%v:%v", fundingTxID, chanPoint.OutputIndex) - require.Equal(t.t, chanStr, chanInfo.ChannelPoint) + // Wait for the nodes to be synced. + ht.WaitForBlockchainSync(alice) + ht.WaitForBlockchainSync(bob) - err = wait.NoError(func() error { - inv, err := net.Bob.LookupInvoice(ctxt, &lnrpc.PaymentHash{ - RHash: payHash[:], - }) - if err != nil { - return err - } + // 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", @@ -135,8 +117,33 @@ func testHoldInvoiceForceClose(net *lntest.NetworkHarness, t *harnessTest) { return nil }, defaultTimeout) - require.NoError(t.t, err, "expected canceled invoice") + 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. - closeChannelAndAssert(t, net, net.Alice, chanPoint, false) + ht.CloseChannel(alice, chanPoint) } diff --git a/lntest/itest/lnd_hold_persistence_test.go b/lntest/itest/lnd_hold_persistence_test.go index 11a90a056..da5424ca3 100644 --- a/lntest/itest/lnd_hold_persistence_test.go +++ b/lntest/itest/lnd_hold_persistence_test.go @@ -1,18 +1,14 @@ package itest import ( - "context" - "crypto/rand" "fmt" - "io" - "sync" - "time" "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/lntest" + "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" @@ -21,74 +17,47 @@ import ( // 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(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +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 := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", nil) // Connect Alice to Carol. - net.ConnectNodes(t.t, net.Alice, 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 := openChannelAndAssert( - t, net, net.Alice, carol, - lntest.OpenChannelParams{ + chanPointAlice := ht.OpenChannel( + alice, carol, lntemp.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) - // Wait for Alice and Carol to receive the channel edge from the - // funding manager. - err := net.Alice.WaitForNetworkChannelOpen(chanPointAlice) - if err != nil { - t.Fatalf("alice didn't see the alice->carol channel before "+ - "timeout: %v", err) - } - - err = carol.WaitForNetworkChannelOpen(chanPointAlice) - if err != nil { - t.Fatalf("carol didn't see the carol->alice channel before "+ - "timeout: %v", err) - } - // 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 := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ + chanPointBob := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{ Amt: chanAmt, }, ) - // Wait for Alice and Carol to see the open channel - err = net.Alice.WaitForNetworkChannelOpen(chanPointBob) - require.NoError(t.t, err, "alice didn't see the alice->bob "+ - "channel before timeout") - - err = carol.WaitForNetworkChannelOpen(chanPointBob) - require.NoError(t.t, err, "carol didn't see the alice->bob "+ - "channel before timeout") + // 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 - _, err = rand.Read(preimage[:]) - if err != nil { - t.Fatalf("unable to generate preimage: %v", err) - } - + copy(preimage[:], ht.Random32Bytes()) preimages = append(preimages, preimage) } @@ -96,9 +65,15 @@ func testHoldInvoicePersistence(net *lntest.NetworkHarness, t *harnessTest) { var ( payAmt = btcutil.Amount(4) payReqs []string - invoiceStreams []invoicesrpc.Invoices_SubscribeSingleInvoiceClient + invoiceStreams []rpc.SingleInvoiceClient ) + assertInvoiceState := func(state lnrpc.Invoice_InvoiceState) { + for _, client := range invoiceStreams { + ht.AssertInvoiceState(client, state) + } + } + for _, preimage := range preimages { payHash := preimage.Hash() @@ -110,97 +85,42 @@ func testHoldInvoicePersistence(net *lntest.NetworkHarness, t *harnessTest) { Hash: payHash[:], Private: true, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - resp, err := carol.AddHoldInvoice(ctxt, invoiceReq) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - - ctx, cancel := context.WithCancel(ctxb) - defer cancel() - - stream, err := carol.SubscribeSingleInvoice( - ctx, - &invoicesrpc.SubscribeSingleInvoiceRequest{ - RHash: payHash[:], - }, - ) - if err != nil { - t.Fatalf("unable to subscribe to invoice: %v", err) - } + 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. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - decodeReq := &lnrpc.PayReqString{ - PayReq: resp.PaymentRequest, - } - invoice, err := net.Alice.DecodePayReq(ctxt, decodeReq) - require.NoError(t.t, err, "could not decode invoice") - require.Len(t.t, invoice.RouteHints, 1) + invoice := alice.RPC.DecodePayReq(resp.PaymentRequest) + require.Len(ht, invoice.RouteHints, 1) + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) invoiceStreams = append(invoiceStreams, stream) - payReqs = append(payReqs, resp.PaymentRequest) } // Wait for all the invoices to reach the OPEN state. - for _, stream := range invoiceStreams { - invoice, err := stream.Recv() - if err != nil { - t.Fatalf("err: %v", err) - } - - if invoice.State != lnrpc.Invoice_OPEN { - t.Fatalf("expected OPEN, got state: %v", invoice.State) - } - } + assertInvoiceState(lnrpc.Invoice_OPEN) // Let Alice initiate payments for all the created invoices. - var paymentStreams []routerrpc.Router_SendPaymentV2Client for _, payReq := range payReqs { - ctx, cancel := context.WithCancel(ctxb) - defer cancel() + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: 60, + FeeLimitSat: 1000000, + } - payStream, err := net.Alice.RouterClient.SendPaymentV2( - ctx, &routerrpc.SendPaymentRequest{ - PaymentRequest: payReq, - TimeoutSeconds: 60, - FeeLimitSat: 1000000, - }, + // Wait for inflight status update. + ht.SendPaymentAndAssertStatus( + alice, req, lnrpc.Payment_IN_FLIGHT, ) - if err != nil { - t.Fatalf("unable to send alice htlc: %v", err) - } - - paymentStreams = append(paymentStreams, payStream) } - // Wait for inlight status update. - for _, payStream := range paymentStreams { - payment, err := payStream.Recv() - if err != nil { - t.Fatalf("Failed receiving status update: %v", err) - } - - if payment.Status != lnrpc.Payment_IN_FLIGHT { - t.Fatalf("state not in flight: %v", payment.Status) - } - } - - // The payments should now show up in Alice's ListInvoices, with a zero + // The payments should now show up in Alice's ListPayments, with a zero // preimage, indicating they are not yet settled. - err = wait.NoError(func() error { - req := &lnrpc.ListPaymentsRequest{ - IncludeIncomplete: true, - } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - paymentsResp, err := net.Alice.ListPayments(ctxt, req) - if err != nil { - return fmt.Errorf("error when obtaining payments: %v", - err) - } + 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. @@ -209,18 +129,16 @@ func testHoldInvoicePersistence(net *lntest.NetworkHarness, t *harnessTest) { payHashes[preimg.Hash().String()] = struct{}{} } - var zeroPreimg lntypes.Preimage - for _, payment := range paymentsResp.Payments { + for _, payment := range payments { _, ok := payHashes[payment.PaymentHash] if !ok { continue } // The preimage should NEVER be non-zero at this point. - if payment.PaymentPreimage != zeroPreimg.String() { - t.Fatalf("expected zero preimage, got %v", - payment.PaymentPreimage) - } + require.Equal(ht, zeroPreimg.String(), + payment.PaymentPreimage, + "expected zero preimage") // We wait for the payment attempt to have been // properly recorded in the DB. @@ -237,201 +155,73 @@ func testHoldInvoicePersistence(net *lntest.NetworkHarness, t *harnessTest) { return nil }, defaultTimeout) - if err != nil { - t.Fatalf("predicate not satisfied: %v", err) - } + require.NoError(ht, err, "timeout checking alice's payments") // Wait for all invoices to be accepted. - for _, stream := range invoiceStreams { - invoice, err := stream.Recv() - if err != nil { - t.Fatalf("err: %v", err) - } - - if invoice.State != lnrpc.Invoice_ACCEPTED { - t.Fatalf("expected ACCEPTED, got state: %v", - invoice.State) - } - } + assertInvoiceState(lnrpc.Invoice_ACCEPTED) // Restart alice. This to ensure she will still be able to handle // settling the invoices after a restart. - if err := net.RestartNode(net.Alice, nil); err != nil { - t.Fatalf("Node restart failed: %v", err) - } + 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. - var ( - statusUpdates []chan *lnrpc.Payment - wg sync.WaitGroup - quit = make(chan struct{}) - ) - - defer close(quit) for _, preimg := range preimages { hash := preimg.Hash() - ctx, cancel := context.WithCancel(ctxb) - defer cancel() + payStream := alice.RPC.TrackPaymentV2(hash[:]) + ht.ReceiveTrackPayment(payStream) - payStream, err := net.Alice.RouterClient.TrackPaymentV2( - ctx, &routerrpc.TrackPaymentRequest{ - PaymentHash: hash[:], - }, + ht.AssertPaymentStatus( + alice, preimg, lnrpc.Payment_IN_FLIGHT, ) - if err != nil { - t.Fatalf("unable to send track payment: %v", err) - } - - // We set up a channel where we'll forward any status update. - upd := make(chan *lnrpc.Payment) - wg.Add(1) - go func() { - defer wg.Done() - - for { - payment, err := payStream.Recv() - if err != nil { - close(upd) - return - } - - select { - case upd <- payment: - case <-quit: - return - } - } - }() - - statusUpdates = append(statusUpdates, upd) - } - - // Wait for the in-flight status update. - for _, upd := range statusUpdates { - select { - case payment, ok := <-upd: - if !ok { - t.Fatalf("failed getting payment update") - } - - if payment.Status != lnrpc.Payment_IN_FLIGHT { - t.Fatalf("state not in in flight: %v", - payment.Status) - } - case <-time.After(5 * time.Second): - t.Fatalf("in flight status not received") - } } // Settle invoices half the invoices, cancel the rest. for i, preimage := range preimages { - var expectedState lnrpc.Invoice_InvoiceState - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) if i%2 == 0 { - settle := &invoicesrpc.SettleInvoiceMsg{ - Preimage: preimage[:], - } - _, err = carol.SettleInvoice(ctxt, settle) - - expectedState = lnrpc.Invoice_SETTLED + carol.RPC.SettleInvoice(preimage[:]) + ht.AssertInvoiceState( + invoiceStreams[i], lnrpc.Invoice_SETTLED, + ) } else { hash := preimage.Hash() - settle := &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: hash[:], - } - _, err = carol.CancelInvoice(ctxt, settle) - - expectedState = lnrpc.Invoice_CANCELED - } - if err != nil { - t.Fatalf("unable to cancel/settle invoice: %v", err) - } - - stream := invoiceStreams[i] - invoice, err := stream.Recv() - require.NoError(t.t, err) - require.Equal(t.t, expectedState, invoice.State) - } - - // Make sure we get the expected status update. - for i, upd := range statusUpdates { - // Read until the payment is in a terminal state. - var payment *lnrpc.Payment - for payment == nil { - select { - case p, ok := <-upd: - if !ok { - t.Fatalf("failed getting payment update") - } - - if p.Status == lnrpc.Payment_IN_FLIGHT { - continue - } - - payment = p - case <-time.After(5 * time.Second): - t.Fatalf("in flight status not received") - } - } - - // Assert terminal payment state. - if i%2 == 0 { - if payment.Status != lnrpc.Payment_SUCCEEDED { - t.Fatalf("state not succeeded : %v", - payment.Status) - } - } else { - if payment.FailureReason != - lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS { - - t.Fatalf("state not failed: %v", - payment.FailureReason) - } + 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. - req := &lnrpc.ListPaymentsRequest{ - IncludeIncomplete: true, - } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - paymentsResp, err := net.Alice.ListPayments(ctxt, req) - if err != nil { - t.Fatalf("error when obtaining Alice payments: %v", err) - } - for i, preimage := range preimages { - paymentHash := preimage.Hash() - var p string - for _, resp := range paymentsResp.Payments { - if resp.PaymentHash == paymentHash.String() { - p = resp.PaymentPreimage - break - } - } - if p == "" { - t.Fatalf("payment not found") - } - + for i, preimg := range preimages { if i%2 == 0 { - if p != preimage.String() { - t.Fatalf("preimage doesn't match: %v vs %v", - p, preimage.String()) - } + ht.AssertPaymentStatus( + alice, preimg, lnrpc.Payment_SUCCEEDED, + ) } else { - if p != lntypes.ZeroHash.String() { - t.Fatalf("preimage not zero: %v", p) - } + payment := ht.AssertPaymentStatus( + alice, preimg, lnrpc.Payment_FAILED, + ) + require.Equal(ht, reason, payment.FailureReason, + "wrong failure reason") } } - // Check that all of our invoice streams are terminated by the server - // since the invoices have completed. - for _, stream := range invoiceStreams { - _, err = stream.Recv() - require.Equal(t.t, io.EOF, err) - } + // Finally, close all channels. + ht.CloseChannel(alice, chanPointBob) + ht.CloseChannel(alice, chanPointAlice) } diff --git a/lntest/itest/lnd_macaroons_test.go b/lntest/itest/lnd_macaroons_test.go index 22ae852ba..01d576f8f 100644 --- a/lntest/itest/lnd_macaroons_test.go +++ b/lntest/itest/lnd_macaroons_test.go @@ -1,7 +1,6 @@ package itest import ( - "bytes" "context" "encoding/hex" "os" @@ -12,7 +11,8 @@ import ( "github.com/golang/protobuf/proto" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lntest" + "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" @@ -23,13 +23,14 @@ import ( // 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(net *lntest.NetworkHarness, ht *harnessTest) { +func testMacaroonAuthentication(ht *lntemp.HarnessTest) { var ( infoReq = &lnrpc.GetInfoRequest{} newAddrReq = &lnrpc.NewAddressRequest{ Type: AddrTypeWitnessPubkeyHash, } - testNode = net.Alice + testNode = ht.Alice + testClient = testNode.RPC.LN ) testCases := []struct { @@ -41,7 +42,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { // enabled. name: "no macaroon", run: func(ctxt context.Context, t *testing.T) { - conn, err := testNode.ConnectRPC(false) + conn, err := testNode.ConnectRPCWithMacaroon(nil) require.NoError(t, err) defer func() { _ = conn.Close() }() client := lnrpc.NewLightningClient(conn) @@ -72,7 +73,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { name: "read only macaroon", run: func(ctxt context.Context, t *testing.T) { readonlyMac, err := testNode.ReadMacaroon( - testNode.ReadMacPath(), defaultTimeout, + testNode.Cfg.ReadMacPath, defaultTimeout, ) require.NoError(t, err) cleanup, client := macaroonClient( @@ -89,7 +90,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { name: "expired macaroon", run: func(ctxt context.Context, t *testing.T) { readonlyMac, err := testNode.ReadMacaroon( - testNode.ReadMacPath(), defaultTimeout, + testNode.Cfg.ReadMacPath, defaultTimeout, ) require.NoError(t, err) timeoutMac, err := macaroons.AddConstraints( @@ -109,7 +110,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { name: "invalid IP macaroon", run: func(ctxt context.Context, t *testing.T) { readonlyMac, err := testNode.ReadMacaroon( - testNode.ReadMacPath(), defaultTimeout, + testNode.Cfg.ReadMacPath, defaultTimeout, ) require.NoError(t, err) invalidIPAddrMac, err := macaroons.AddConstraints( @@ -133,7 +134,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { name: "correct macaroon", run: func(ctxt context.Context, t *testing.T) { adminMac, err := testNode.ReadMacaroon( - testNode.AdminMacPath(), defaultTimeout, + testNode.Cfg.AdminMacPath, defaultTimeout, ) require.NoError(t, err) adminMac, err = macaroons.AddConstraints( @@ -163,7 +164,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { "Permissions", }}, } - bakeRes, err := testNode.BakeMacaroon(ctxt, req) + bakeRes, err := testClient.BakeMacaroon(ctxt, req) require.NoError(t, err) // Create a connection that uses the custom macaroon. @@ -218,7 +219,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { }}, AllowExternalPermissions: true, } - bakeResp, err := testNode.BakeMacaroon(ctxt, req) + bakeResp, err := testClient.BakeMacaroon(ctxt, req) require.NoError(t, err) macBytes, err := hex.DecodeString(bakeResp.Macaroon) @@ -232,7 +233,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { // Test that CheckMacaroonPermissions accurately // characterizes macaroon as valid, even if the // permissions are not native to LND. - checkResp, err := testNode.CheckMacaroonPermissions( + checkResp, err := testClient.CheckMacaroonPermissions( ctxt, checkReq, ) require.NoError(t, err) @@ -253,7 +254,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { checkReq.Macaroon = timeoutMacBytes - _, err = testNode.CheckMacaroonPermissions( + _, err = testClient.CheckMacaroonPermissions( ctxt, checkReq, ) require.Error(t, err) @@ -269,7 +270,7 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { checkReq.Permissions = wrongPermissions checkReq.Macaroon = macBytes - _, err = testNode.CheckMacaroonPermissions( + _, err = testClient.CheckMacaroonPermissions( ctxt, checkReq, ) require.Error(t, err) @@ -279,9 +280,9 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { for _, tc := range testCases { tc := tc - ht.t.Run(tc.name, func(tt *testing.T) { + ht.Run(tc.name, func(tt *testing.T) { ctxt, cancel := context.WithTimeout( - context.Background(), defaultTimeout, + ht.Context(), defaultTimeout, ) defer cancel() @@ -293,8 +294,8 @@ func testMacaroonAuthentication(net *lntest.NetworkHarness, ht *harnessTest) { // 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(net *lntest.NetworkHarness, t *harnessTest) { - var testNode = net.Alice +func testBakeMacaroon(ht *lntemp.HarnessTest) { + var testNode = ht.Alice testCases := []struct { name string @@ -495,14 +496,14 @@ func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) { for _, tc := range testCases { tc := tc - t.t.Run(tc.name, func(tt *testing.T) { + ht.Run(tc.name, func(tt *testing.T) { ctxt, cancel := context.WithTimeout( - context.Background(), defaultTimeout, + ht.Context(), defaultTimeout, ) defer cancel() adminMac, err := testNode.ReadMacaroon( - testNode.AdminMacPath(), defaultTimeout, + testNode.Cfg.AdminMacPath, defaultTimeout, ) require.NoError(tt, err) cleanup, client := macaroonClient(tt, testNode, adminMac) @@ -517,26 +518,26 @@ func testBakeMacaroon(net *lntest.NetworkHarness, t *harnessTest) { // 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(net *lntest.NetworkHarness, t *harnessTest) { +func testDeleteMacaroonID(ht *lntemp.HarnessTest) { var ( - ctxb = context.Background() - testNode = net.Alice + 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.AdminMacPath(), defaultTimeout, + testNode.Cfg.AdminMacPath, defaultTimeout, ) - require.NoError(t.t, err) - cleanup, client := macaroonClient(t.t, testNode, adminMac) + 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(t.t, err) + require.NoError(ht, err) numMacIDs := len(listResp.RootKeyIds) // Create macaroons for testing. @@ -551,17 +552,17 @@ func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) { }}, } resp, err := client.BakeMacaroon(ctxt, req) - require.NoError(t.t, err) + 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(t.t, err) + require.NoError(ht, err) // The number of macaroon IDs should be increased by len(rootKeyIDs). - require.Equal(t.t, numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds)) + require.Equal(ht, numMacIDs+len(rootKeyIDs), len(listResp.RootKeyIds)) // First test: check deleting the DefaultRootKeyID returns an error. defaultID, _ := strconv.ParseUint( @@ -571,45 +572,45 @@ func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) { RootKeyId: defaultID, } _, err = client.DeleteMacaroonID(ctxt, req) - require.Error(t.t, err) - require.Contains( - t.t, err.Error(), macaroons.ErrDeletionForbidden.Error(), - ) + 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(t.t, err) - require.True(t.t, resp.Deleted) + 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(t.t, err) + require.NoError(ht, err) // The number of macaroon IDs should be decreased by 1. - require.Equal(t.t, numMacIDs+len(rootKeyIDs)-1, len(listResp.RootKeyIds)) + 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(t.t, err) - cleanup, client = macaroonClient(t.t, testNode, deletedMac) + 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. + // Because the macaroon is deleted, it will be treated as an invalid + // one. listReq = &lnrpc.ListMacaroonIDsRequest{} _, err = client.ListMacaroonIDs(ctxt, listReq) - require.Error(t.t, err) - require.Contains(t.t, err.Error(), "cannot get macaroon") + 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(net *lntest.NetworkHarness, t *harnessTest) { +func testStatelessInit(ht *lntemp.HarnessTest) { var ( initPw = []byte("stateless") newPw = []byte("stateless-new") @@ -621,85 +622,79 @@ func testStatelessInit(net *lntest.NetworkHarness, t *harnessTest) { // 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, err := net.NewNodeWithSeed( - "Carol", nil, initPw, true, - ) - require.NoError(t.t, err) - if len(macBytes) == 0 { - t.Fatalf("invalid macaroon returned in stateless init") - } + 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.AdminMacPath()) - require.Error(t.t, err) - _, err = os.Stat(carol.ReadMacPath()) - require.Error(t.t, err) - _, err = os.Stat(carol.InvoiceMacPath()) - require.Error(t.t, err) + _, 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(t.t, err) + 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(t.t, err) + require.NoError(ht, err) defer conn.Close() adminMacClient := lnrpc.NewLightningClient(conn) - ctxt, _ := context.WithTimeout(context.Background(), defaultTimeout) + ctxt, cancel := context.WithTimeout(ht.Context(), defaultTimeout) + defer cancel() res, err := adminMacClient.NewAddress(ctxt, newAddrReq) - require.NoError(t.t, err) + require.NoError(ht, err) if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) { - t.Fatalf("returned address was not a regtest address") + 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. - if err := net.RestartNodeNoUnlock(carol, nil, true); err != nil { - t.Fatalf("Node restart failed: %v", err) - } + ht.RestartNodeNoUnlock(carol) changePwReq := &lnrpc.ChangePasswordRequest{ CurrentPassword: initPw, NewPassword: newPw, StatelessInit: true, } - response, err := carol.InitChangePassword(changePwReq) - require.NoError(t.t, err) + 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.AdminMacPath()) - require.Error(t.t, err) - _, err = os.Stat(carol.ReadMacPath()) - require.Error(t.t, err) - _, err = os.Stat(carol.InvoiceMacPath()) - require.Error(t.t, err) + _, 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. - if err = adminMac.UnmarshalBinary(response.AdminMacaroon); err != nil { - t.Fatalf("unable to unmarshal macaroon: %v", err) - } - if bytes.Equal(response.AdminMacaroon, macBytes) { - t.Fatalf("expected new macaroon to be different") - } + 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(t.t, err) + 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, _ = context.WithTimeout(context.Background(), defaultTimeout) + ctxt, cancel = context.WithTimeout(ht.Context(), defaultTimeout) + defer cancel() res, err = adminMacClient.NewAddress(ctxt, newAddrReq) - require.NoError(t.t, err) + require.NoError(ht, err) if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) { - t.Fatalf("returned address was not a regtest address") + require.Fail(ht, "returned address was not a regtest address") } } @@ -717,9 +712,11 @@ func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) { return mac, nil } -func macaroonClient(t *testing.T, testNode *lntest.HarnessNode, +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") diff --git a/lntest/itest/lnd_max_channel_size_test.go b/lntest/itest/lnd_max_channel_size_test.go index 100f5bb34..d795fb2e8 100644 --- a/lntest/itest/lnd_max_channel_size_test.go +++ b/lntest/itest/lnd_max_channel_size_test.go @@ -5,79 +5,50 @@ package itest import ( "fmt" - "strings" "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/funding" - "github.com/lightningnetwork/lnd/lntest" + "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(net *lntest.NetworkHarness, t *harnessTest) { - // We'll make two new nodes, both wumbo but with the default - // limit on maximum channel size (10 BTC) - wumboNode := net.NewNode( - t.t, "wumbo", []string{"--protocol.wumbo-channels"}, +// 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"}, ) - defer shutdownAndAssert(net, t, wumboNode) - - wumboNode2 := net.NewNode( - t.t, "wumbo2", []string{"--protocol.wumbo-channels"}, + wumboNode2 := ht.NewNode( + "wumbo2", []string{"--protocol.wumbo-channels"}, ) - defer shutdownAndAssert(net, t, wumboNode2) - // We'll send 11 BTC to the wumbo node so it can test the wumbo soft limit. - net.SendCoins(t.t, 11*btcutil.SatoshiPerBitcoin, wumboNode) + // 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. - net.EnsureConnected(t.t, wumboNode, wumboNode2) + ht.EnsureConnected(wumboNode, wumboNode2) chanAmt := funding.MaxBtcFundingAmountWumbo + 1 - _, err := net.OpenChannel( - wumboNode, wumboNode2, lntest.OpenChannelParams{ - Amt: chanAmt, - }, + // The test should show failure due to the channel exceeding our max + // size. + expectedErr := lnwallet.ErrChanTooLarge( + chanAmt, funding.MaxBtcFundingAmountWumbo, ) - if err == nil { - t.Fatalf("expected channel funding to fail as it exceeds 10 BTC limit") - } - - // The test should show failure due to the channel exceeding our max size. - if !strings.Contains(err.Error(), "exceeds maximum chan size") { - t.Fatalf("channel should be rejected due to size, instead "+ - "error was: %v", err) - } - - // Next we'll create a non-wumbo node to verify that it enforces the - // BOLT-02 channel size limit and rejects our funding request. - miniNode := net.NewNode(t.t, "mini", nil) - defer shutdownAndAssert(net, t, miniNode) - - net.EnsureConnected(t.t, wumboNode, miniNode) - - _, err = net.OpenChannel( - wumboNode, miniNode, lntest.OpenChannelParams{ - Amt: chanAmt, - }, + ht.OpenChannelAssertErr( + wumboNode, wumboNode2, + lntemp.OpenChannelParams{Amt: chanAmt}, expectedErr, ) - if err == nil { - t.Fatalf("expected channel funding to fail as it exceeds 0.16 BTC limit") - } - // The test should show failure due to the channel exceeding our max size. - if !strings.Contains(err.Error(), "exceeds maximum chan size") { - t.Fatalf("channel should be rejected due to size, instead "+ - "error was: %v", err) - } - - // We'll now make another wumbo node with appropriate maximum channel size - // to accept our wumbo channel funding. - wumboNode3 := net.NewNode( - t.t, "wumbo3", []string{ + // 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", @@ -85,16 +56,12 @@ func testMaxChannelSize(net *lntest.NetworkHarness, t *harnessTest) { ), }, ) - defer shutdownAndAssert(net, t, wumboNode3) // Creating a wumbo channel between these two nodes should succeed. - net.EnsureConnected(t.t, wumboNode, wumboNode3) - chanPoint := openChannelAndAssert( - t, net, wumboNode, wumboNode3, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + ht.EnsureConnected(wumboNode, wumboNode3) + chanPoint := ht.OpenChannel( + wumboNode, wumboNode3, lntemp.OpenChannelParams{Amt: chanAmt}, ) - closeChannelAndAssert(t, net, wumboNode, chanPoint, false) + ht.CloseChannel(wumboNode, chanPoint) } diff --git a/lntest/itest/lnd_max_htlcs_test.go b/lntest/itest/lnd_max_htlcs_test.go index 9f5218cac..d457e452f 100644 --- a/lntest/itest/lnd_max_htlcs_test.go +++ b/lntest/itest/lnd_max_htlcs_test.go @@ -1,13 +1,11 @@ package itest import ( - "context" - "testing" - "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntemp" + "github.com/lightningnetwork/lnd/lntemp/node" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) @@ -16,118 +14,69 @@ import ( // 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(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +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 - chanPoint := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ + alice, bob := ht.Alice, ht.Bob + chanPoint := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{ Amt: 1000000, PushAmt: 800000, RemoteMaxHtlcs: uint16(maxHtlcs), }, ) - // Wait for Alice and Bob to receive the channel edge from the - // funding manager. - err := net.Alice.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err, "alice does not have open channel") - - err = net.Bob.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err, "bob does not have open channel") - // Alice and bob should have one channel open with each other now. - assertNodeNumChannels(t, net.Alice, 1) - assertNodeNumChannels(t, net.Bob, 1) + 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) - cancelCtxs := make([]func(), maxHtlcs) for i := 0; i < maxHtlcs; i++ { - subCtx, cancel := context.WithTimeout(ctxb, defaultTimeout) - cancelCtxs[i] = cancel - - subscriptions[i] = acceptHoldInvoice( - subCtx, t.t, i, net.Bob, net.Alice, - ) + subscriptions[i] = acceptHoldInvoice(ht, i, bob, alice) } - // Cancel all of our subscriptions on exit. - defer func() { - for _, cancel := range cancelCtxs { - cancel() - } - }() - - err = assertNumActiveHtlcs([]*lntest.HarnessNode{ - net.Alice, net.Bob, - }, maxHtlcs) - require.NoError(t.t, err, "htlcs not active") + 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. - subCtx, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - aliceBobSub := acceptHoldInvoice( - subCtx, t.t, maxHtlcs, net.Alice, net.Bob, - ) - err = assertNumActiveHtlcs([]*lntest.HarnessNode{ - net.Alice, net.Bob, - }, maxHtlcs+1) - require.NoError(t.t, err, "htlcs not active") + 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. - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - payment, err := net.Bob.RouterClient.SendPaymentV2( - ctxt, &routerrpc.SendPaymentRequest{ - Amt: 1000, - Dest: net.Alice.PubKey[:], - TimeoutSeconds: 60, - FeeLimitSat: 1000000, - MaxParts: 10, - Amp: true, - }, - ) - require.NoError(t.t, err, "send payment failed") - - update, err := payment.Recv() - require.NoError(t.t, err, "no payment in flight update") - require.Equal(t.t, lnrpc.Payment_IN_FLIGHT, update.Status, - "payment not inflight") - - update, err = payment.Recv() - require.NoError(t.t, err, "no payment failed update") - require.Equal(t.t, lnrpc.Payment_FAILED, update.Status) - require.Len(t.t, update.Htlcs, 0, "expected no htlcs dispatched") + 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 { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - sub.cancel(ctxt, t.t) + sub.cancel(ht) } + aliceBobSub.cancel(ht) - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - aliceBobSub.cancel(ctxt, t.t) + ht.AssertNumActiveHtlcs(alice, 0) + ht.AssertNumActiveHtlcs(bob, 0) - err = assertNumActiveHtlcs([]*lntest.HarnessNode{ - net.Alice, net.Bob, - }, 0) - require.NoError(t.t, err, "expected all htlcs canceled") - - closeChannelAndAssert(t, net, net.Alice, chanPoint, false) + ht.CloseChannel(alice, chanPoint) } type holdSubscription struct { - recipient invoicesrpc.InvoicesClient + recipient *node.HarnessNode hash lntypes.Hash invSubscription invoicesrpc.Invoices_SubscribeSingleInvoiceClient paymentSubscription routerrpc.Router_SendPaymentV2Client @@ -135,89 +84,73 @@ type holdSubscription struct { // 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(ctx context.Context, t *testing.T) { - _, err := h.recipient.CancelInvoice(ctx, &invoicesrpc.CancelInvoiceMsg{ - PaymentHash: h.hash[:], - }) - require.NoError(t, err, "invoice cancel failed") +func (h *holdSubscription) cancel(ht *lntemp.HarnessTest) { + h.recipient.RPC.CancelInvoice(h.hash[:]) - invUpdate, err := h.invSubscription.Recv() - require.NoError(t, err, "cancel invoice subscribe failed") - require.Equal(t, lnrpc.Invoice_CANCELED, invUpdate.State, + 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, err := h.paymentSubscription.Recv() - require.NoError(t, err, "cancel payment subscribe failed") - require.Len(t, payUpdate.Htlcs, 1) - require.Equal(t, lnrpc.Payment_IN_FLIGHT, payUpdate.Status) + payUpdate := ht.AssertPaymentStatusFromStream( + h.paymentSubscription, lnrpc.Payment_IN_FLIGHT, + ) + require.Len(ht, payUpdate.Htlcs, 1) - payUpdate, err = h.paymentSubscription.Recv() - require.NoError(t, err, "cancel payment subscribe failed") - require.Equal(t, lnrpc.Payment_FAILED, payUpdate.Status, + payUpdate = ht.AssertPaymentStatusFromStream( + h.paymentSubscription, lnrpc.Payment_FAILED, + ) + require.Equal(ht, lnrpc.Payment_FAILED, payUpdate.Status, "expected payment failed") - require.Equal(t, lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, + 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(ctx context.Context, t *testing.T, idx int, sender, - receiver *lntest.HarnessNode) *holdSubscription { +func acceptHoldInvoice(ht *lntemp.HarnessTest, idx int, sender, + receiver *node.HarnessNode) *holdSubscription { hash := [lntypes.HashSize]byte{byte(idx + 1)} - invoice, err := receiver.AddHoldInvoice( - ctx, &invoicesrpc.AddHoldInvoiceRequest{ - ValueMsat: 10000, - Hash: hash[:], - }, - ) - require.NoError(t, err, "couldn't add invoice") + req := &invoicesrpc.AddHoldInvoiceRequest{ + ValueMsat: 10000, + Hash: hash[:], + } + invoice := receiver.RPC.AddHoldInvoice(req) - invStream, err := receiver.InvoicesClient.SubscribeSingleInvoice( - ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ - RHash: hash[:], - }, - ) - require.NoError(t, err, "could not subscribe to invoice") + invStream := receiver.RPC.SubscribeSingleInvoice(hash[:]) + inv := ht.ReceiveSingleInvoice(invStream) + require.Equal(ht, lnrpc.Invoice_OPEN, inv.State, "expect open") - inv, err := invStream.Recv() - require.NoError(t, err, "invoice open stream failed") - require.Equal(t, lnrpc.Invoice_OPEN, inv.State, - "expected open") - - payStream, err := sender.RouterClient.SendPaymentV2( - ctx, &routerrpc.SendPaymentRequest{ - PaymentRequest: invoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitSat: 1000000, - }, - ) - require.NoError(t, err, "send payment failed") + 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, err := payStream.Recv() - require.NoError(t, err, "payment in flight stream failed") - require.Equal(t, lnrpc.Payment_IN_FLIGHT, payment.Status) - require.Len(t, payment.Htlcs, 0) + payment := ht.AssertPaymentStatusFromStream( + payStream, lnrpc.Payment_IN_FLIGHT, + ) + require.Empty(ht, payment.Htlcs) - payment, err = payStream.Recv() - require.NoError(t, err, "payment in flight stream failed") - require.Equal(t, lnrpc.Payment_IN_FLIGHT, payment.Status) - require.Len(t, payment.Htlcs, 1) + payment = ht.AssertPaymentStatusFromStream( + payStream, lnrpc.Payment_IN_FLIGHT, + ) + require.Len(ht, payment.Htlcs, 1) - inv, err = invStream.Recv() - require.NoError(t, err, "invoice accepted stream failed") - require.Equal(t, lnrpc.Invoice_ACCEPTED, inv.State, - "expected accepted invoice") + inv = ht.ReceiveSingleInvoice(invStream) + require.Equal(ht, lnrpc.Invoice_ACCEPTED, inv.State, + "expected accepted") return &holdSubscription{ - recipient: receiver.InvoicesClient, + recipient: receiver, hash: hash, invSubscription: invStream, paymentSubscription: payStream, diff --git a/lntest/itest/lnd_multi-hop-error-propagation_test.go b/lntest/itest/lnd_multi-hop-error-propagation_test.go index 53ded769a..a5cca06aa 100644 --- a/lntest/itest/lnd_multi-hop-error-propagation_test.go +++ b/lntest/itest/lnd_multi-hop-error-propagation_test.go @@ -1,47 +1,65 @@ package itest import ( - "context" "math" - "strings" - "time" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) -func testHtlcErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +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 := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointAlice := ht.OpenChannel( + alice, bob, + lntemp.OpenChannelParams{Amt: chanAmt}, ) - if err := net.Alice.WaitForNetworkChannelOpen(chanPointAlice); err != nil { - t.Fatalf("channel not seen by alice before timeout: %v", err) - } - cType, err := channelCommitType(net.Alice, chanPointAlice) - if err != nil { - t.Fatalf("unable to get channel type: %v", err) - } + // 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. + // 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), @@ -57,7 +75,7 @@ func testHtlcErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) { // Deprecated fields. Balance: int64(chanAmt - commitFee), } - assertChannelBalanceResp(t, net.Alice, expBalanceAlice) + 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. @@ -81,52 +99,21 @@ func testHtlcErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) { // Deprecated fields. Balance: int64(chanAmt - commitFee), } - assertChannelBalanceResp(t, net.Bob, expBalanceBob) + ht.AssertChannelBalanceResp(bob, expBalanceBob) } - // Since we'd like to test some multi-hop failure scenarios, we'll - // introduce another node into our test network: Carol. - carol := net.NewNode(t.t, "Carol", nil) + // assertLinkFailure checks that the stream provided has a single link + // failure the failure detail provided. + assertLinkFailure := func(event *routerrpc.HtlcEvent, + failureDetail routerrpc.FailureDetail) { - // 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. - net.ConnectNodes(t.t, net.Bob, carol) - const bobChanAmt = funding.MaxBtcFundingAmount - chanPointBob := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) + linkFail, ok := event.Event.(*routerrpc.HtlcEvent_LinkFailEvent) + require.Truef(ht, ok, "expected forwarding failure, got: %T", + linkFail) - // Ensure that Alice has Carol in her routing table before proceeding. - nodeInfoReq := &lnrpc.NodeInfoRequest{ - PubKey: carol.PubKeyStr, - } - checkTableTimeout := time.After(time.Second * 10) - checkTableTicker := time.NewTicker(100 * time.Millisecond) - defer checkTableTicker.Stop() - -out: - // TODO(roasbeef): make into async hook for node announcements - for { - select { - case <-checkTableTicker.C: - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - _, err := net.Alice.GetNodeInfo(ctxt, nodeInfoReq) - if err != nil && strings.Contains(err.Error(), - "unable to find") { - - continue - } - - break out - case <-checkTableTimeout: - t.Fatalf("carol's node announcement didn't propagate within " + - "the timeout period") - } + 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 @@ -137,61 +124,14 @@ out: Memo: "kek99", Value: payAmt, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - carolInvoice, err := carol.AddInvoice(ctxt, invoiceReq) - if err != nil { - t.Fatalf("unable to generate carol invoice: %v", err) - } - carolPayReq, err := carol.DecodePayReq(ctxb, - &lnrpc.PayReqString{ - PayReq: carolInvoice.PaymentRequest, - }) - if err != nil { - t.Fatalf("unable to decode generated payment request: %v", err) - } - - // Before we send the payment, ensure that the announcement of the new - // channel has been processed by Alice. - if err := net.Alice.WaitForNetworkChannelOpen(chanPointBob); err != nil { - t.Fatalf("channel not seen by alice before timeout: %v", err) - } - - // Before we start sending payments, subscribe to htlc events for each - // node. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - - aliceEvents, err := net.Alice.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, aliceEvents) - - bobEvents, err := net.Bob.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, bobEvents) - - carolEvents, err := carol.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, carolEvents) + 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. - // TODO(roasbeef): return failure response rather than failing entire - // stream on payment error. sendReq := &routerrpc.SendPaymentRequest{ - PaymentHash: makeFakePayHash(t), + PaymentHash: ht.Random32Bytes(), Dest: carol.PubKey[:], Amt: payAmt, FinalCltvDelta: int32(carolPayReq.CltvExpiry), @@ -199,25 +139,56 @@ out: FeeLimitMsat: noFeeLimitMsat, MaxParts: 1, } - sendAndAssertFailure( - t, net.Alice, - sendReq, lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, + ht.SendPaymentAssertFail( + alice, sendReq, + lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll ) - assertLastHTLCError( - t, net.Alice, - lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, + 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. - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents) - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_FORWARD, bobEvents) + assertAliceAndBob() // Carol should have a link failure because the htlc failed on her // incoming link. - assertLinkFailure( - t, routerrpc.HtlcEvent_RECEIVE, - routerrpc.FailureDetail_UNKNOWN_INVOICE, carolEvents, + 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 @@ -228,33 +199,36 @@ out: // value on the extended HTLC. htlcAmt := lnwire.NewMSatFromSatoshis(1000) sendReq = &routerrpc.SendPaymentRequest{ - PaymentHash: carolInvoice.RHash, - Dest: carol.PubKey[:], - Amt: int64(htlcAmt.ToSatoshis()), // 10k satoshis are expected. + PaymentHash: carolInvoice.RHash, + Dest: carol.PubKey[:], + // 10k satoshis are expected. + Amt: int64(htlcAmt.ToSatoshis()), FinalCltvDelta: int32(carolPayReq.CltvExpiry), TimeoutSeconds: 60, FeeLimitMsat: noFeeLimitMsat, MaxParts: 1, } - sendAndAssertFailure( - t, net.Alice, - sendReq, lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, + ht.SendPaymentAssertFail( + alice, sendReq, + lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS, //nolint:lll ) - assertLastHTLCError( - t, net.Alice, - lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, + 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. - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents) - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_FORWARD, bobEvents) + assertAliceAndBob() // Carol should have a link failure because the htlc failed on her // incoming link. - assertLinkFailure( - t, routerrpc.HtlcEvent_RECEIVE, - routerrpc.FailureDetail_INVOICE_UNDERPAID, carolEvents, + 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 @@ -284,29 +258,34 @@ out: invoiceReq = &lnrpc.Invoice{ Value: toSend, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolInvoice2, err := carol.AddInvoice(ctxt, invoiceReq) - if err != nil { - t.Fatalf("unable to generate carol invoice: %v", err) - } + carolInvoice2 := carol.RPC.AddInvoice(invoiceReq) - sendAndAssertSuccess( - t, net.Bob, &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice2.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - MaxParts: 1, - }, - ) + 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 for her receive. - assertHtlcEvents( - t, 1, 0, 1, routerrpc.HtlcEvent_SEND, bobEvents, + // settle event and a final htlc event for her receive. + ht.AssertHtlcEventTypes( + bobEvents, routerrpc.HtlcEvent_SEND, + lntemp.HtlcEventForward, ) - assertHtlcEvents( - t, 0, 0, 1, routerrpc.HtlcEvent_RECEIVE, carolEvents, + 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 @@ -318,11 +297,7 @@ out: invoiceReq = &lnrpc.Invoice{ Value: 100000, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolInvoice3, err := carol.AddInvoice(ctxt, invoiceReq) - if err != nil { - t.Fatalf("unable to generate carol invoice: %v", err) - } + carolInvoice3 := carol.RPC.AddInvoice(invoiceReq) sendReq = &routerrpc.SendPaymentRequest{ PaymentRequest: carolInvoice3.PaymentRequest, @@ -330,30 +305,37 @@ out: FeeLimitMsat: noFeeLimitMsat, MaxParts: 1, } - sendAndAssertFailure( - t, net.Alice, - sendReq, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, + ht.SendPaymentAssertFail( + alice, sendReq, + lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, ) - assertLastHTLCError( - t, net.Alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE, + ht.AssertLastHTLCError( + alice, lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE, ) // Alice should have a forwarding event and a forwarding failure. - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents) + 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. - assertLinkFailure( - t, routerrpc.HtlcEvent_FORWARD, - routerrpc.FailureDetail_INSUFFICIENT_BALANCE, bobEvents, + 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. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - carolInvoice, err = carol.AddInvoice(ctxt, invoiceReq) - if err != nil { - t.Fatalf("unable to generate carol invoice: %v", err) - } + 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. @@ -361,77 +343,48 @@ out: // 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. - shutdownAndAssert(net, t, carol) + ht.Shutdown(carol) // Reset mission control to forget the temporary channel failure above. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - _, err = net.Alice.RouterClient.ResetMissionControl( - ctxt, &routerrpc.ResetMissionControlRequest{}, - ) - if err != nil { - t.Fatalf("unable to reset mission control: %v", err) - } + alice.RPC.ResetMissionControl() - sendAndAssertFailure( - t, net.Alice, - &routerrpc.SendPaymentRequest{ - PaymentRequest: carolInvoice.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - MaxParts: 1, - }, - lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: carolInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + MaxParts: 1, + } + ht.SendPaymentAssertFail( + alice, req, lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE, ) - assertLastHTLCError(t, net.Alice, lnrpc.Failure_UNKNOWN_NEXT_PEER) + ht.AssertLastHTLCError(alice, lnrpc.Failure_UNKNOWN_NEXT_PEER) // Alice should have a forwarding event and subsequent fail. - assertHtlcEvents(t, 1, 1, 0, routerrpc.HtlcEvent_SEND, aliceEvents) + 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. - assertLinkFailure( - t, routerrpc.HtlcEvent_FORWARD, - routerrpc.FailureDetail_NO_DETAIL, bobEvents, + 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. - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) + ht.CloseChannel(alice, chanPointAlice) // Force close Bob's final channel. - closeChannelAndAssert(t, net, net.Bob, chanPointBob, true) - - // Cleanup by mining the force close and sweep transaction. - cleanupForceClose(t, net, net.Bob, chanPointBob) -} - -// assertLinkFailure checks that the stream provided has a single link failure -// the the failure detail provided. -func assertLinkFailure(t *harnessTest, - eventType routerrpc.HtlcEvent_EventType, - failureDetail routerrpc.FailureDetail, - client routerrpc.Router_SubscribeHtlcEventsClient) { - - event := assertEventAndType(t, eventType, client) - - linkFail, ok := event.Event.(*routerrpc.HtlcEvent_LinkFailEvent) - if !ok { - t.Fatalf("expected forwarding failure, got: %T", linkFail) - } - - if linkFail.LinkFailEvent.FailureDetail != failureDetail { - t.Fatalf("expected: %v, got: %v", failureDetail, - linkFail.LinkFailEvent.FailureDetail) - } - - event = assertEventAndType(t, routerrpc.HtlcEvent_UNKNOWN, client) - finalHtlc, ok := event.Event.(*routerrpc.HtlcEvent_FinalHtlcEvent) - if !ok { - t.Fatalf("expected final htlc, got: %T", event.Event) - } - - if finalHtlc.FinalHtlcEvent.Settled { - t.Fatalf("expected final fail") - } + ht.ForceCloseChannel(bob, chanPointBob) } diff --git a/lntest/itest/lnd_multi-hop-payments_test.go b/lntest/itest/lnd_multi-hop-payments_test.go index 09a5f6b78..28c032061 100644 --- a/lntest/itest/lnd_multi-hop-payments_test.go +++ b/lntest/itest/lnd_multi-hop-payments_test.go @@ -1,146 +1,70 @@ package itest import ( - "context" - "time" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntemp" + "github.com/lightningnetwork/lnd/lntemp/node" "github.com/stretchr/testify/require" ) -func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testMultiHopPayments(ht *lntemp.HarnessTest) { const chanAmt = btcutil.Amount(100000) - var networkChans []*lnrpc.ChannelPoint - - // Open a channel with 100k satoshis between Alice and Bob with Alice - // being the sole funder of the channel. - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointAlice) - - aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - aliceFundPoint := wire.OutPoint{ - Hash: *aliceChanTXID, - Index: chanPointAlice.OutputIndex, - } // 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 will - // be running an older node that requires the legacy onion payload. + alice, bob := ht.Alice, ht.Bob + daveArgs := []string{"--protocol.legacy.onion"} - dave := net.NewNode(t.t, "Dave", daveArgs) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("Dave", daveArgs) + carol := ht.NewNode("Carol", nil) - net.ConnectNodes(t.t, dave, net.Alice) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) + // 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() - chanPointDave := openChannelAndAssert( - t, net, dave, net.Alice, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + // 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}, ) - networkChans = append(networkChans, chanPointDave) - daveChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointDave) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - daveFundPoint := wire.OutPoint{ - Hash: *daveChanTXID, - Index: chanPointDave.OutputIndex, - } // Next, we'll create Carol and establish a channel to from her to // Dave. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) - - net.ConnectNodes(t.t, carol, dave) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) - - chanPointCarol := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + chanPointCarol := ht.OpenChannel( + carol, dave, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointCarol) - - carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - carolFundPoint := wire.OutPoint{ - Hash: *carolChanTXID, - Index: chanPointCarol.OutputIndex, - } - - // Wait for all nodes to have seen all channels. - nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave} - nodeNames := []string{"Alice", "Bob", "Carol", "Dave"} - for _, chanPoint := range networkChans { - for i, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - point := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d): timeout waiting for "+ - "channel(%s) open: %v", nodeNames[i], - node.NodeID, point, err) - } - } - } // 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, _, _, err := createPayReqs( - net.Bob, paymentAmt, numPayments, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // We'll wait for all parties to recognize the new channels within the - // network. - err = dave.WaitForNetworkChannelOpen(chanPointDave) - if err != nil { - t.Fatalf("dave didn't advertise his channel: %v", err) - } - err = carol.WaitForNetworkChannelOpen(chanPointCarol) - if err != nil { - t.Fatalf("carol didn't advertise her channel in time: %v", - err) - } - - time.Sleep(time.Millisecond * 50) + 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 @@ -149,61 +73,21 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { const aliceBaseFeeSat = 1 const aliceFeeRatePPM = 100000 updateChannelPolicy( - t, net.Alice, chanPointAlice, aliceBaseFeeSat*1000, - aliceFeeRatePPM, chainreg.DefaultBitcoinTimeLockDelta, maxHtlc, - carol, + ht, alice, chanPointAlice, aliceBaseFeeSat*1000, + aliceFeeRatePPM, chainreg.DefaultBitcoinTimeLockDelta, + maxHtlc, carol, ) const daveBaseFeeSat = 5 const daveFeeRatePPM = 150000 updateChannelPolicy( - t, dave, chanPointDave, daveBaseFeeSat*1000, daveFeeRatePPM, + ht, dave, chanPointDave, daveBaseFeeSat*1000, daveFeeRatePPM, chainreg.DefaultBitcoinTimeLockDelta, maxHtlc, carol, ) - // Before we start sending payments, subscribe to htlc events for each - // node. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - - aliceEvents, err := net.Alice.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, aliceEvents) - - bobEvents, err := net.Bob.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, bobEvents) - - carolEvents, err := carol.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, carolEvents) - - daveEvents, err := dave.RouterClient.SubscribeHtlcEvents( - ctxt, &routerrpc.SubscribeHtlcEventsRequest{}, - ) - if err != nil { - t.Fatalf("could not subscribe events: %v", err) - } - assertSubscribed(t, daveEvents) - // Using Carol as the source, pay to the 5 invoices from Bob created // above. - err = completePaymentRequests(carol, carol.RouterClient, payReqs, true) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } + 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 @@ -215,10 +99,10 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // The final node bob expects to get paid five times 1000 sat. expectedAmountPaidAtoB := int64(numPayments * paymentAmt) - assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob, - aliceFundPoint, int64(0), expectedAmountPaidAtoB) - assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice, - aliceFundPoint, expectedAmountPaidAtoB, int64(0)) + 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. @@ -229,10 +113,10 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // Dave needs to pay what Alice pays plus Alice's fee. expectedAmountPaidDtoA := expectedAmountPaidAtoB + expectedFeeAlice - assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice, - daveFundPoint, int64(0), expectedAmountPaidDtoA) - assertAmountPaid(t, "Dave(local) => Alice(remote)", dave, - daveFundPoint, expectedAmountPaidDtoA, int64(0)) + 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. @@ -244,10 +128,10 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // Carol needs to pay what Dave pays plus Dave's fee. expectedAmountPaidCtoD := expectedAmountPaidDtoA + expectedFeeDave - assertAmountPaid(t, "Carol(local) => Dave(remote)", dave, - carolFundPoint, int64(0), expectedAmountPaidCtoD) - assertAmountPaid(t, "Carol(local) => Dave(remote)", carol, - carolFundPoint, expectedAmountPaidCtoD, int64(0)) + 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 @@ -256,206 +140,92 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { // 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. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - feeReport, err := dave.FeeReport(ctxt, &lnrpc.FeeReportRequest{}) - require.NoError(t.t, err) - require.EqualValues(t.t, expectedFeeDave, feeReport.DayFeeSum) - require.EqualValues(t.t, expectedFeeDave, feeReport.WeekFeeSum) - require.EqualValues(t.t, expectedFeeDave, feeReport.MonthFeeSum) + 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, err := carol.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) - require.NoError(t.t, err) + info := carol.RPC.GetInfo() carolAlias := info.Alias - info, err = net.Alice.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) - require.NoError(t.t, err) + info = alice.RPC.GetInfo() aliceAlias := info.Alias - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - fwdingHistory, err := dave.ForwardingHistory( - ctxt, &lnrpc.ForwardingHistoryRequest{}, - ) - require.NoError(t.t, err) - require.Len(t.t, fwdingHistory.ForwardingEvents, numPayments) + 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(t.t, expectedForwardingFee, event.Fee) + require.Equal(ht, expectedForwardingFee, event.Fee) // Check that peer aliases are empty since the // ForwardingHistoryRequest did not specify the PeerAliasLookup // flag. - require.Empty(t.t, event.PeerAliasIn) - require.Empty(t.t, event.PeerAliasOut) + 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, err = dave.ForwardingHistory( - ctxt, &lnrpc.ForwardingHistoryRequest{ + fwdingHistory = dave.RPC.ForwardingHistory( + &lnrpc.ForwardingHistoryRequest{ PeerAliasLookup: true, }, ) - require.NoError(t.t, err) - require.Len(t.t, fwdingHistory.ForwardingEvents, numPayments) + require.Len(ht, fwdingHistory.ForwardingEvents, numPayments) for _, event := range fwdingHistory.ForwardingEvents { // Each event should show a fee of 170 satoshi. - require.Equal(t.t, expectedForwardingFee, event.Fee) + require.Equal(ht, expectedForwardingFee, event.Fee) // Check that peer aliases adhere to payment flow, namely // Carol->Dave->Alice. - require.Equal(t.t, carolAlias, event.PeerAliasIn) - require.Equal(t.t, aliceAlias, event.PeerAliasOut) + require.Equal(ht, carolAlias, event.PeerAliasIn) + require.Equal(ht, aliceAlias, event.PeerAliasOut) } // We expect Carol to have successful forwards and settles for // her sends. - assertHtlcEvents( - t, numPayments, 0, numPayments, routerrpc.HtlcEvent_SEND, - carolEvents, + ht.AssertHtlcEvents( + carolEvents, numPayments, 0, numPayments, + routerrpc.HtlcEvent_SEND, ) // Dave and Alice should both have forwards and settles for // their role as forwarding nodes. - assertHtlcEvents( - t, numPayments, 0, numPayments, routerrpc.HtlcEvent_FORWARD, - daveEvents, + ht.AssertHtlcEvents( + daveEvents, numPayments, 0, numPayments, + routerrpc.HtlcEvent_FORWARD, ) - assertHtlcEvents( - t, numPayments, 0, numPayments, routerrpc.HtlcEvent_FORWARD, - aliceEvents, + ht.AssertHtlcEvents( + aliceEvents, numPayments, 0, numPayments, + routerrpc.HtlcEvent_FORWARD, ) // Bob should only have settle events for his receives. - assertHtlcEvents( - t, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE, bobEvents, + ht.AssertHtlcEvents( + bobEvents, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE, ) - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - closeChannelAndAssert(t, net, dave, chanPointDave, false) - closeChannelAndAssert(t, net, carol, chanPointCarol, false) + // Finally, close all channels. + ht.CloseChannel(alice, chanPointAlice) + ht.CloseChannel(dave, chanPointDave) + ht.CloseChannel(carol, chanPointCarol) } -// assertHtlcEvents consumes events from a client and ensures that they are of -// the expected type and contain the expected number of forwards, forward -// failures and settles. -func assertHtlcEvents(t *harnessTest, fwdCount, fwdFailCount, settleCount int, - userType routerrpc.HtlcEvent_EventType, - client routerrpc.Router_SubscribeHtlcEventsClient) { - - var forwards, forwardFails, settles, finalSettles, finalFails int - - var finalFailCount, finalSettleCount int - if userType != routerrpc.HtlcEvent_SEND { - finalFailCount = fwdFailCount - finalSettleCount = settleCount - } - - numEvents := fwdCount + fwdFailCount + settleCount + - finalFailCount + finalSettleCount - - for i := 0; i < numEvents; i++ { - event, err := client.Recv() - if err != nil { - t.Fatalf("could not get event") - } - - expectedEventType := userType - - switch e := event.Event.(type) { - case *routerrpc.HtlcEvent_ForwardEvent: - forwards++ - - case *routerrpc.HtlcEvent_ForwardFailEvent: - forwardFails++ - - case *routerrpc.HtlcEvent_SettleEvent: - settles++ - - case *routerrpc.HtlcEvent_FinalHtlcEvent: - if e.FinalHtlcEvent.Settled { - finalSettles++ - } else { - finalFails++ - } - - expectedEventType = routerrpc.HtlcEvent_UNKNOWN - - default: - t.Fatalf("unexpected event: %T", event.Event) - } - - if event.EventType != expectedEventType { - t.Fatalf("expected: %v, got: %v", expectedEventType, - event.EventType) - } - } - - if forwards != fwdCount { - t.Fatalf("expected: %v forwards, got: %v", fwdCount, forwards) - } - - if forwardFails != fwdFailCount { - t.Fatalf("expected: %v forward fails, got: %v", fwdFailCount, - forwardFails) - } - - if finalFails != finalFailCount { - t.Fatalf("expected: %v final fails, got: %v", finalFailCount, - finalFails) - } - - if settles != settleCount { - t.Fatalf("expected: %v settles, got: %v", settleCount, settles) - } - - if finalSettles != finalSettleCount { - t.Fatalf("expected: %v settles, got: %v", finalSettleCount, - finalSettles) - } -} - -// assertEventAndType reads an event from the stream provided and ensures that -// it is associated with the correct user related type - a user initiated send, -// a receive to our node or a forward through our node. Note that this event -// type is different from the htlc event type (forward, link failure etc). -func assertEventAndType(t *harnessTest, eventType routerrpc.HtlcEvent_EventType, - client routerrpc.Router_SubscribeHtlcEventsClient) *routerrpc.HtlcEvent { - - event, err := client.Recv() - if err != nil { - t.Fatalf("could not get event") - } - - if event.EventType != eventType { - t.Fatalf("expected: %v, got: %v", eventType, - event.EventType) - } - - return event -} - -func assertSubscribed(t *harnessTest, - client routerrpc.Router_SubscribeHtlcEventsClient) { - - event, err := client.Recv() - require.NoError(t.t, err) - require.NotNil(t.t, event.GetSubscribedEvent()) -} - -// updateChannelPolicy updates the channel policy of node to the -// given fees and timelock delta. This function blocks until -// listenerNode has received the policy update. -func updateChannelPolicy(t *harnessTest, node *lntest.HarnessNode, - chanPoint *lnrpc.ChannelPoint, baseFee int64, feeRate int64, - timeLockDelta uint32, maxHtlc uint64, listenerNode *lntest.HarnessNode) { - - ctxb := context.Background() +// 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, @@ -475,14 +245,10 @@ func updateChannelPolicy(t *harnessTest, node *lntest.HarnessNode, MaxHtlcMsat: maxHtlc, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - if _, err := node.UpdateChannelPolicy(ctxt, updateFeeReq); err != nil { - t.Fatalf("unable to update chan policy: %v", err) - } + hn.RPC.UpdateChannelPolicy(updateFeeReq) // Wait for listener node to receive the channel update from node. - assertChannelPolicyUpdate( - t.t, listenerNode, node.PubKeyStr, - expectedPolicy, chanPoint, false, + ht.AssertChannelPolicyUpdate( + listenerNode, hn, expectedPolicy, chanPoint, false, ) } diff --git a/lntest/itest/lnd_no_etcd_dummy_failover_test.go b/lntest/itest/lnd_no_etcd_dummy_failover_test.go index c2a9a188b..721b60cf6 100644 --- a/lntest/itest/lnd_no_etcd_dummy_failover_test.go +++ b/lntest/itest/lnd_no_etcd_dummy_failover_test.go @@ -3,10 +3,8 @@ package itest -import ( - "github.com/lightningnetwork/lnd/lntest" -) +import "github.com/lightningnetwork/lnd/lntemp" // testEtcdFailover is an empty itest when LND is not compiled with etcd // support. -func testEtcdFailover(net *lntest.NetworkHarness, ht *harnessTest) {} +func testEtcdFailover(ht *lntemp.HarnessTest) {} diff --git a/lntest/itest/lnd_onchain_test.go b/lntest/itest/lnd_onchain_test.go index 4f94b7e0f..a2d024aa6 100644 --- a/lntest/itest/lnd_onchain_test.go +++ b/lntest/itest/lnd_onchain_test.go @@ -4,8 +4,6 @@ import ( "bytes" "context" "fmt" - "strings" - "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -15,6 +13,7 @@ import ( "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" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwallet" @@ -233,17 +232,17 @@ func runCPFP(net *lntest.NetworkHarness, t *harnessTest, // 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(net *lntest.NetworkHarness, t *harnessTest) { +func testAnchorReservedValue(ht *lntemp.HarnessTest) { // Start two nodes supporting anchor channels. args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) - alice := net.NewNode(t.t, "Alice", args) - defer shutdownAndAssert(net, t, alice) - bob := net.NewNode(t.t, "Bob", args) - defer shutdownAndAssert(net, t, bob) + // 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) - ctxb := context.Background() - net.ConnectNodes(t.t, alice, bob) + ht.ConnectNodes(alice, bob) // Send just enough coins for Alice to open a channel without a change // output. @@ -252,100 +251,73 @@ func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) { feeEst = 8000 ) - net.SendCoins(t.t, chanAmt+feeEst, alice) + ht.FundCoins(chanAmt+feeEst, alice) // wallet, without a change output. This should not be allowed. - resErr := lnwallet.ErrReservedValueInvalidated.Error() - - _, err := net.OpenChannel( - alice, bob, lntest.OpenChannelParams{ + ht.OpenChannelAssertErr( + alice, bob, lntemp.OpenChannelParams{ Amt: chanAmt, - }, + }, lnwallet.ErrReservedValueInvalidated, ) - if err == nil || !strings.Contains(err.Error(), resErr) { - t.Fatalf("expected failure, got: %v", err) - } // Alice opens a smaller channel. This works since it will have a // change output. - aliceChanPoint1 := openChannelAndAssert( - t, net, alice, bob, lntest.OpenChannelParams{ - Amt: chanAmt / 4, - }, + 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. - aliceChanPoint2 := openChannelAndAssert( - t, net, alice, bob, lntest.OpenChannelParams{ - Amt: chanAmt / 4, - }, + 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. - err = net.RestartNode(bob, nil) - require.NoError(t.t, err) - aliceChanPoint3 := openChannelAndAssert( - t, net, alice, bob, lntest.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{ - aliceChanPoint1, aliceChanPoint2, aliceChanPoint3, - } - for _, chanPoint := range chanPoints { - err = alice.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err) - - err = bob.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err) - } + 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. - addrReq := &lnrpc.NewAddressRequest{ + req := &lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - resp, err := alice.NewAddress(ctxt, addrReq) - require.NoError(t.t, err) + resp := alice.RPC.NewAddress(req) sweepReq := &lnrpc.SendCoinsRequest{ Addr: resp.Address, SendAll: true, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - _, err = alice.SendCoins(ctxt, sweepReq) - require.NoError(t.t, err) + alice.RPC.SendCoins(sweepReq) - block := mineBlocks(t, net, 1, 1)[0] + 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] - if len(sweepTx.TxIn) != 1 { - t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn)) - } // It should have a single output. - if len(sweepTx.TxOut) != 1 { - t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut)) - } + assertNumTxInAndTxOut(sweepTx, 1, 1) // Wait for Alice to see her balance as confirmed. waitForConfirmedBalance := func() int64 { var balance int64 err := wait.NoError(func() error { - req := &lnrpc.WalletBalanceRequest{} - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - resp, err := alice.WalletBalance(ctxt, req) - if err != nil { - return err - } + resp := alice.RPC.WalletBalance() if resp.TotalBalance == 0 { return fmt.Errorf("no balance") @@ -358,93 +330,51 @@ func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) { balance = resp.TotalBalance return nil }, defaultTimeout) - require.NoError(t.t, err) + require.NoError(ht, err, "timeout checking alice's balance") return balance } - _ = waitForConfirmedBalance() + waitForConfirmedBalance() // Alice tries to send all funds to an external address, the reserved // value must stay in her wallet. - minerAddr, err := net.Miner.NewAddress() - require.NoError(t.t, err) + minerAddr := ht.Miner.NewMinerAddress() sweepReq = &lnrpc.SendCoinsRequest{ Addr: minerAddr.String(), SendAll: true, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - _, err = alice.SendCoins(ctxt, sweepReq) - require.NoError(t.t, err) + alice.RPC.SendCoins(sweepReq) // We'll mine a block which should include the sweep transaction we // generated above. - block = mineBlocks(t, net, 1, 1)[0] + 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] - if len(sweepTx.TxIn) != 1 { - t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn)) - } // It should have two outputs, one being the miner address, the other // one being the reserve going back to our wallet. - if len(sweepTx.TxOut) != 2 { - t.Fatalf("expected 2 outputs instead have %v", len(sweepTx.TxOut)) - } + assertNumTxInAndTxOut(sweepTx, 1, 2) // The reserved value is now back in Alice's wallet. aliceBalance := waitForConfirmedBalance() - // The reserved value should be equal to the required reserve for anchor - // channels. - walletBalanceResp, err := alice.WalletBalance( - ctxb, &lnrpc.WalletBalanceRequest{}, - ) - require.NoError(t.t, err) - require.Equal( - t.t, aliceBalance, walletBalanceResp.ReservedBalanceAnchorChan, - ) - - additionalChannels := int64(1) - - // Required reserve when additional channels are provided. - requiredReserveResp, err := alice.WalletKitClient.RequiredReserve( - ctxb, &walletrpc.RequiredReserveRequest{ - AdditionalPublicChannels: uint32(additionalChannels), - }, - ) - require.NoError(t.t, err) - - additionalReservedValue := btcutil.Amount(additionalChannels * - int64(lnwallet.AnchorChanReservedValue)) - totalReserved := btcutil.Amount(aliceBalance) + additionalReservedValue - - // The total reserved value should not exceed the maximum value reserved - // for anchor channels. - if totalReserved > lnwallet.MaxAnchorChanReservedValue { - totalReserved = lnwallet.MaxAnchorChanReservedValue - } - require.Equal( - t.t, int64(totalReserved), requiredReserveResp.RequiredReserve, - ) - // Alice closes channel, should now be allowed to send everything to an // external address. for _, chanPoint := range chanPoints { - closeChannelAndAssert(t, net, alice, chanPoint, false) + ht.CloseChannel(alice, chanPoint) } newBalance := waitForConfirmedBalance() - if newBalance <= aliceBalance { - t.Fatalf("Alice's balance did not increase after channel close") - } + require.Greater(ht, newBalance, aliceBalance, + "Alice's balance did not increase after channel close") // Assert there are no open or pending channels anymore. - assertNumPendingChannels(t, alice, 0, 0) - assertNodeNumChannels(t, alice, 0) + 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. @@ -452,32 +382,205 @@ func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) { Addr: minerAddr.String(), SendAll: true, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - _, err = alice.SendCoins(ctxt, sweepReq) - require.NoError(t.t, err) + alice.RPC.SendCoins(sweepReq) // We'll mine a block which should include the sweep transaction we // generated above. - block = mineBlocks(t, net, 1, 1)[0] + 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] - if len(sweepTx.TxIn) != 4 { - t.Fatalf("expected 4 inputs instead have %v", len(sweepTx.TxIn)) - } // It should have a single output. - if len(sweepTx.TxOut) != 1 { - t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut)) + 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(t *harnessTest, net *lntest.NetworkHarness, - aliceAnchor *sweptOutput, anchorCsv uint32) *btcutil.Tx { +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 @@ -491,7 +594,8 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness, } } - t.Fatalf("anchor op not found") + require.FailNow(ht, "anchor op not found") + return wire.TxIn{} }() @@ -500,14 +604,9 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness, aliceAnchorTxIn.Witness[0] = nil aliceAnchorTxIn.Sequence = anchorCsv - minerAddr, err := net.Miner.NewAddress() - if err != nil { - t.Fatalf("unable to get miner addr: %v", err) - } + minerAddr := ht.Miner.NewMinerAddress() addrScript, err := txscript.PayToAddrScript(minerAddr) - if err != nil { - t.Fatalf("unable to gen addr script: %v", err) - } + 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. @@ -520,191 +619,3 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness, return btcutil.NewTx(tx) } - -// 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(net *lntest.NetworkHarness, t *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 := net.NewNode(t.t, "Alice", args) - defer shutdownAndAssert(net, t, alice) - - bob := net.NewNode(t.t, "Bob", args) - defer shutdownAndAssert(net, t, bob) - - ctxb := context.Background() - net.ConnectNodes(t.t, 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. - const ( - firstChanSize = 1_000_000 - anchorFeeBuffer = 500_000 - ) - net.SendCoins(t.t, firstChanSize, alice) - - // We'll give Alice another spare UTXO as well so she can use it to - // help sweep all coins. - net.SendCoins(t.t, anchorFeeBuffer, alice) - - // Open the channel between the two nodes and wait for it to confirm - // fully. - aliceChanPoint1 := openChannelAndAssert( - t, net, alice, bob, lntest.OpenChannelParams{ - Amt: firstChanSize, - }, - ) - - // 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. - _, _, err := net.CloseChannel(alice, aliceChanPoint1, true) - if err != nil { - t.Fatalf("unable to execute force channel closure: %v", err) - } - - // Now that the channel has been force closed, it should show up in the - // PendingChannels RPC under the waiting close section. - pendingChansRequest := &lnrpc.PendingChannelsRequest{} - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - pendingChanResp, err := alice.PendingChannels(ctxt, pendingChansRequest) - if err != nil { - t.Fatalf("unable to query for pending channels: %v", err) - } - err = checkNumWaitingCloseChannels(pendingChanResp, 1) - if err != nil { - t.Fatalf(err.Error()) - } - - // Get the normal channel outpoint so we can track it in the set of - // channels that are waiting to be closed. - fundingTxID, err := lnrpc.GetChanPointFundingTxid(aliceChanPoint1) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - chanPoint := wire.OutPoint{ - Hash: *fundingTxID, - Index: aliceChanPoint1.OutputIndex, - } - waitingClose, err := findWaitingCloseChannel(pendingChanResp, &chanPoint) - if err != nil { - t.Fatalf(err.Error()) - } - - // 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, err := getNTxsFromMempool( - net.Miner.Client, expectedTxns, minerMempoolTimeout, - ) - require.NoError(t.t, err, "no sweep txns in miner mempool") - aliceCloseTx := waitingClose.Commitments.LocalTxid - _, aliceAnchor := findCommitAndAnchor(t, net, sweepTxns, aliceCloseTx) - - // We'll now mine _only_ the commitment force close transaction, as we - // want the anchor sweep to stay unconfirmed. - var emptyTime time.Time - forceCloseTxID, _ := chainhash.NewHashFromStr(aliceCloseTx) - commitTxn, err := net.Miner.Client.GetRawTransaction( - forceCloseTxID, - ) - if err != nil { - t.Fatalf("unable to get transaction: %v", err) - } - _, err = net.Miner.GenerateAndSubmitBlock( - []*btcutil.Tx{commitTxn}, -1, emptyTime, - ) - if err != nil { - t.Fatalf("unable to generate block: %v", err) - } - - // 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, err := net.Miner.NewAddress() - if err != nil { - t.Fatalf("unable to create new miner addr: %v", err) - } - sweepReq := &lnrpc.SendCoinsRequest{ - Addr: minerAddr.String(), - SendAll: true, - MinConfs: 0, - SpendUnconfirmed: true, - } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - sweepAllResp, err := alice.SendCoins(ctxt, sweepReq) - if err != nil { - t.Fatalf("unable to sweep coins: %v", err) - } - - // 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) - assertTransactionInWallet(t.t, alice, aliceAnchor.SweepTx.TxHash()) - assertTransactionInWallet(t.t, 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, err := net.SuspendNode(alice) - if err != nil { - t.Fatalf("unable to shutdown alice: %v", err) - } - const anchorCsv = 16 - for i := 0; i < anchorCsv; i++ { - _, err := net.Miner.GenerateAndSubmitBlock(nil, -1, emptyTime) - if err != nil { - t.Fatalf("unable to generate block: %v", err) - } - } - - // Before we sweep the anchor, we'll restart Alice. - if err := aliceRestart(); err != nil { - t.Fatalf("unable to restart alice: %v", err) - } - - // 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(t, net, aliceAnchor, anchorCsv) - _, err = net.Miner.GenerateAndSubmitBlock( - []*btcutil.Tx{thirdPartyAnchorSweep}, -1, emptyTime, - ) - if err != nil { - t.Fatalf("unable to generate block: %v", err) - } - - // At this point, we should no longer find Alice's transaction that - // tried to sweep the anchor in her wallet. - assertTransactionNotInWallet(t.t, alice, aliceAnchor.SweepTx.TxHash()) - - // In addition, the transaction she sent to sweep all her coins to the - // miner also should no longer be found. - assertTransactionNotInWallet(t.t, alice, *sweepAllTxID) - - // The anchor should now show as being "lost", while the force close - // response is still present. - assertAnchorOutputLost(t, alice, chanPoint) - - // 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, - } - assertSpendingTxInMempool( - t, net.Miner.Client, minerMempoolTimeout, commitSweepOp, - ) - _, err = net.Miner.Client.Generate(1) - if err != nil { - t.Fatalf("unable to generate block: %v", err) - } - assertNumPendingChannels(t, alice, 0, 0) -} diff --git a/lntest/itest/lnd_open_channel_test.go b/lntest/itest/lnd_open_channel_test.go index 0a2d0b90b..51e9a02d3 100644 --- a/lntest/itest/lnd_open_channel_test.go +++ b/lntest/itest/lnd_open_channel_test.go @@ -6,12 +6,12 @@ import ( "time" "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcutil" "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/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" @@ -20,112 +20,83 @@ import ( // 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(net *lntest.NetworkHarness, t *harnessTest) { +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 net.BackendCfg.Name() == lntest.NeutrinoBackendName { - t.Skipf("skipping reorg test for neutrino backend") + if ht.IsNeutrinoBackend() { + ht.Skipf("skipping reorg test for neutrino backend") } - var ( - ctxb = context.Background() - temp = "temp" - ) + 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, err := lntest.NewTempMiner(tempLogDir, logFilename) - require.NoError(t.t, err, "failed to create temp miner") - defer func() { - require.NoError( - t.t, tempMiner.Stop(), - "failed to clean up temp miner", - ) - }() + tempMiner := lntemp.NewTempMiner( + ht.Context(), ht.T, tempLogDir, logFilename, + ) + defer tempMiner.Stop() // Setup the temp miner - require.NoError( - t.t, tempMiner.SetUp(false, 0), "unable to set up mining node", - ) + 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 = net.Miner.Client.Node( + err := miner.Client.Node( btcjson.NConnect, tempMiner.P2PAddress(), &temp, ) - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } - nodeSlice := []*rpctest.Harness{net.Miner.Harness, tempMiner.Harness} - if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil { - t.Fatalf("unable to join node on blocks: %v", err) - } + 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(t, net.Miner, tempMiner, 0) + 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 = net.Miner.Client.Node( + err = miner.Client.Node( btcjson.NDisconnect, tempMiner.P2PAddress(), &temp, ) - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } + 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 - chanAmt := funding.MaxBtcFundingAmount - pushAmt := btcutil.Amount(0) - pendingUpdate, err := net.OpenPendingChannel( - net.Alice, net.Bob, chanAmt, pushAmt, - ) - if err != nil { - t.Fatalf("unable to open channel: %v", err) + 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. - _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("failed to find funding tx in mempool: %v", err) - } + 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. - assertNumOpenChannelsPending(t, net.Alice, net.Bob, 1) + ht.AssertNodesNumPendingOpenChannels(alice, bob, 1) fundingTxID, err := chainhash.NewHash(pendingUpdate.Txid) - if err != nil { - t.Fatalf("unable to convert funding txid into chainhash.Hash:"+ - " %v", err) - } + 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 := mineBlocks(t, net, 10, 1)[0] - assertTxInBlock(t, block, fundingTxID) - if _, err := tempMiner.Client.Generate(15); err != nil { - t.Fatalf("unable to generate blocks: %v", err) - } + 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(t, net.Miner, tempMiner, 5) - - // Wait for Alice to sync to the original miner's chain. - _, minerHeight, err := net.Miner.Client.GetBestBlock() - if err != nil { - t.Fatalf("unable to get current blockheight %v", err) - } - err = waitForNodeBlockHeight(net.Alice, minerHeight) - if err != nil { - t.Fatalf("unable to sync to chain: %v", err) - } + assertMinerBlockHeightDelta(ht, miner, tempMiner, 5) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ @@ -135,121 +106,57 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { } // Ensure channel is no longer pending. - assertNumOpenChannelsPending(t, net.Alice, net.Bob, 0) + ht.AssertNodesNumPendingOpenChannels(alice, bob, 0) // Wait for Alice and Bob to recognize and advertise the new channel // generated above. - err = net.Alice.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("alice didn't advertise channel before "+ - "timeout: %v", err) - } - err = net.Bob.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("bob didn't advertise channel before "+ - "timeout: %v", err) - } + ht.AssertTopologyChannelOpen(alice, chanPoint) + ht.AssertTopologyChannelOpen(bob, chanPoint) // Alice should now have 1 edge in her graph. - req := &lnrpc.ChannelGraphRequest{ - IncludeUnannounced: true, - } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - chanGraph, err := net.Alice.DescribeGraph(ctxt, req) - if err != nil { - t.Fatalf("unable to query for alice's routing table: %v", err) - } - - numEdges := len(chanGraph.Edges) - if numEdges != 1 { - t.Fatalf("expected to find one edge in the graph, found %d", - numEdges) - } + 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. - err = net.BackendCfg.DisconnectMiner() - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } + ht.DisconnectMiner() // Connecting to the temporary miner should now cause our original // chain to be re-orged out. - err = net.Miner.Client.Node( - btcjson.NConnect, tempMiner.P2PAddress(), &temp, - ) - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } + err = miner.Client.Node(btcjson.NConnect, tempMiner.P2PAddress(), &temp) + require.NoError(ht, err, "unable to connect temp miner") - nodes := []*rpctest.Harness{tempMiner.Harness, net.Miner.Harness} - if err := rpctest.JoinNodes(nodes, rpctest.Blocks); err != nil { - t.Fatalf("unable to join node on blocks: %v", err) - } + 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(t, net.Miner, tempMiner, 0) + assertMinerBlockHeightDelta(ht, miner, tempMiner, 0) // Now we disconnect the two miners, and connect our original miner to // our chain backend once again. - err = net.Miner.Client.Node( + err = miner.Client.Node( btcjson.NDisconnect, tempMiner.P2PAddress(), &temp, ) - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } + require.NoError(ht, err, "unable to disconnect temp miner") - err = net.BackendCfg.ConnectMiner() - if err != nil { - t.Fatalf("unable to remove node: %v", err) - } + 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() - if err != nil { - t.Fatalf("unable to get current blockheight %v", err) - } - err = waitForNodeBlockHeight(net.Alice, tempMinerHeight) - if err != nil { - t.Fatalf("unable to sync to chain: %v", err) - } + 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. - req = &lnrpc.ChannelGraphRequest{ - IncludeUnannounced: true, - } - - var predErr error - err = wait.Predicate(func() bool { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - chanGraph, err = net.Alice.DescribeGraph(ctxt, req) - if err != nil { - predErr = fmt.Errorf("unable to query for "+ - "alice's routing table: %v", err) - return false - } - - numEdges = len(chanGraph.Edges) - if numEdges != 0 { - predErr = fmt.Errorf("expected to find "+ - "no edge in the graph, found %d", - numEdges) - return false - } - return true - }, defaultTimeout) - if err != nil { - t.Fatalf(predErr.Error()) - } + ht.AssertNumEdges(alice, 0, true) // Cleanup by mining the funding tx again, then closing the channel. - block = mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, fundingTxID) + block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] + ht.Miner.AssertTxInBlock(block, fundingTxID) - closeReorgedChannelAndAssert(t, net, net.Alice, chanPoint, false) + ht.CloseChannel(alice, chanPoint) } // testOpenChannelFeePolicy checks if different channel fee scenarios @@ -579,3 +486,33 @@ func runBasicChannelCreationAndUpdates(net *lntest.NetworkHarness, ), "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") +} diff --git a/lntest/itest/lnd_payment_test.go b/lntest/itest/lnd_payment_test.go index e965b7c3c..65343825e 100644 --- a/lntest/itest/lnd_payment_test.go +++ b/lntest/itest/lnd_payment_test.go @@ -1,7 +1,6 @@ package itest import ( - "bytes" "context" "crypto/sha256" "encoding/hex" @@ -31,11 +30,18 @@ func testListPayments(ht *lntemp.HarnessTest) { 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 := bytes.Repeat([]byte("B"), 32) + preimage := ht.Random32Bytes() invoice := &lnrpc.Invoice{ Memo: "testing", RPreimage: preimage, @@ -43,6 +49,10 @@ func testListPayments(ht *lntemp.HarnessTest) { } 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} @@ -113,8 +123,8 @@ func testListPayments(ht *lntemp.HarnessTest) { invReq := &lnrpc.ListInvoiceRequest{ CreationDateStart: 1227035905, } - invResp := bob.RPC.ListInvoices(invReq) - require.Len(ht, invResp.Invoices, 1) + 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{ @@ -135,7 +145,7 @@ func testListPayments(ht *lntemp.HarnessTest) { CreationDateEnd: 5392552705, } invResp = bob.RPC.ListInvoices(invReq) - require.Len(ht, invResp.Invoices, 1) + require.Len(ht, invResp.Invoices, numInvoices) // Delete all payments from Alice. DB should have no payments. alice.RPC.DeleteAllPayments() @@ -152,7 +162,7 @@ func testListPayments(ht *lntemp.HarnessTest) { time.Sleep(2 * time.Second) // Close the channel. - defer ht.CloseChannel(alice, chanPoint) + ht.CloseChannel(alice, chanPoint) } // testPaymentFollowingChannelOpen tests that the channel transition from diff --git a/lntest/itest/lnd_psbt_test.go b/lntest/itest/lnd_psbt_test.go index 4644d54bc..656a7361c 100644 --- a/lntest/itest/lnd_psbt_test.go +++ b/lntest/itest/lnd_psbt_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "fmt" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" @@ -20,6 +21,7 @@ import ( "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/lntest" "github.com/stretchr/testify/require" ) @@ -255,69 +257,56 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // and dave by using a Partially Signed Bitcoin Transaction that funds the // channel multisig funding output and is fully funded by an external third // party. -func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() +func testPsbtChanFundingExternal(ht *lntemp.HarnessTest) { const chanSize = funding.MaxBtcFundingAmount - // Everything we do here should be done within a second or two, so we - // can just keep a single timeout context around for all calls. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - // First, we'll create two new nodes that we'll use to open channels // between for this test. Both these nodes have an empty wallet as Alice // will be funding the channel. - carol := net.NewNode(t.t, "carol", nil) - defer shutdownAndAssert(net, t, carol) - - dave := net.NewNode(t.t, "dave", nil) - defer shutdownAndAssert(net, t, dave) + carol := ht.NewNode("carol", nil) + dave := ht.NewNode("dave", nil) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. - net.EnsureConnected(t.t, carol, dave) - net.EnsureConnected(t.t, carol, net.Alice) + alice := ht.Alice + ht.EnsureConnected(carol, dave) + ht.EnsureConnected(carol, alice) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. - var pendingChanID [32]byte - _, err := rand.Read(pendingChanID[:]) - require.NoError(t.t, err) + pendingChanID := ht.Random32Bytes() // We'll also test batch funding of two channels so we need another ID. - var pendingChanID2 [32]byte - _, err = rand.Read(pendingChanID2[:]) - require.NoError(t.t, err) + pendingChanID2 := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We use the NoPublish flag here to avoid // publishing the whole batch TX too early. - chanUpdates, tempPsbt, err := openChannelPsbt( - ctxt, carol, dave, lntest.OpenChannelParams{ + chanUpdates, tempPsbt := ht.OpenChannelPsbt( + carol, dave, lntemp.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ - PendingChanId: pendingChanID[:], + PendingChanId: pendingChanID, NoPublish: true, }, }, }, }, ) - require.NoError(t.t, err) // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. - chanUpdates2, psbtBytes2, err := openChannelPsbt( - ctxt, carol, net.Alice, lntest.OpenChannelParams{ + chanUpdates2, psbtBytes2 := ht.OpenChannelPsbt( + carol, alice, lntemp.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ - PendingChanId: pendingChanID2[:], + PendingChanId: pendingChanID2, NoPublish: true, BasePsbt: tempPsbt, }, @@ -325,7 +314,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { }, }, ) - require.NoError(t.t, err) // We'll now ask Alice's wallet to fund the PSBT for us. This will // return a packet with inputs and outputs set but without any witness @@ -338,8 +326,7 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { SatPerVbyte: 2, }, } - fundResp, err := net.Alice.WalletKitClient.FundPsbt(ctxt, fundReq) - require.NoError(t.t, err) + fundResp := alice.RPC.FundPsbt(fundReq) // We have a PSBT that has no witness data yet, which is exactly what we // need for the next step: Verify the PSBT with the funding intents. @@ -349,92 +336,77 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // direct communication with Carol and won't send the signed TX to her // before broadcasting it. So we cannot call the finalize step but // instead just tell lnd to wait for a TX to be published/confirmed. - _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ - PendingChanId: pendingChanID[:], + PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, SkipFinalize: true, }, }, }) - require.NoError(t.t, err) - _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ - PendingChanId: pendingChanID2[:], + PendingChanId: pendingChanID2, FundedPsbt: fundResp.FundedPsbt, SkipFinalize: true, }, }, }) - require.NoError(t.t, err) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled for both channels. - updateResp, err := receiveChanUpdate(ctxt, chanUpdates) - require.NoError(t.t, err) + updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) - require.True(t.t, ok) + require.True(ht, ok) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, }, OutputIndex: upd.ChanPending.OutputIndex, } - updateResp2, err := receiveChanUpdate(ctxt, chanUpdates2) - require.NoError(t.t, err) + updateResp2 := ht.ReceiveOpenChannelUpdate(chanUpdates2) upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending) - require.True(t.t, ok) + require.True(ht, ok) chanPoint2 := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd2.ChanPending.Txid, }, OutputIndex: upd2.ChanPending.OutputIndex, } - numPending, err := numOpenChannelsPending(ctxt, carol) - require.NoError(t.t, err) - require.Equal(t.t, 2, numPending) + ht.AssertNumPendingOpenChannels(carol, 2) // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } - finalizeRes, err := net.Alice.WalletKitClient.FinalizePsbt( - ctxt, finalizeReq, - ) - require.NoError(t.t, err) + finalizeRes := alice.RPC.FinalizePsbt(finalizeReq) // No transaction should have been published yet. - mempool, err := net.Miner.Client.GetRawMempool() - require.NoError(t.t, err) - require.Equal(t.t, 0, len(mempool)) + ht.Miner.AssertNumTxsInMempool(0) // Great, now let's publish the final raw transaction. var finalTx wire.MsgTx - err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) - require.NoError(t.t, err) + err := finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) + require.NoError(ht, err) txHash := finalTx.TxHash() - _, err = net.Miner.Client.SendRawTransaction(&finalTx, false) - require.NoError(t.t, err) + _, err = ht.Miner.Client.SendRawTransaction(&finalTx, false) + require.NoError(ht, err) // Now we can mine a block to get the transaction confirmed, then wait // for the new channel to be propagated through the network. - block := mineBlocks(t, net, 6, 1)[0] - assertTxInBlock(t, block, &txHash) - err = carol.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err) - err = carol.WaitForNetworkChannelOpen(chanPoint2) - require.NoError(t.t, err) + block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] + ht.Miner.AssertTxInBlock(block, &txHash) + ht.AssertTopologyChannelOpen(carol, chanPoint) + ht.AssertTopologyChannelOpen(carol, chanPoint2) // With the channel open, ensure that it is counted towards Carol's // total channel balance. - balReq := &lnrpc.ChannelBalanceRequest{} - balRes, err := carol.ChannelBalance(ctxt, balReq) - require.NoError(t.t, err) - require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat) + balRes := carol.RPC.ChannelBalance() + require.NotZero(ht, balRes.LocalBalance.Sat) // Next, to make sure the channel functions as normal, we'll make some // payments within the channel. @@ -443,88 +415,88 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { Memo: "new chans", Value: int64(payAmt), } - resp, err := dave.AddInvoice(ctxt, invoice) - require.NoError(t.t, err) - err = completePaymentRequests( - carol, carol.RouterClient, []string{resp.PaymentRequest}, true, - ) - require.NoError(t.t, err) + resp := dave.RPC.AddInvoice(invoice) + ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) + + // TODO(yy): remove the sleep once the following bug is fixed. When the + // payment is reported as settled by Carol, 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) // To conclude, we'll close the newly created channel between Carol and // Dave. This function will also block until the channels are closed and // will additionally assert the relevant channel closing post // conditions. - closeChannelAndAssert(t, net, carol, chanPoint, false) - closeChannelAndAssert(t, net, carol, chanPoint2, false) + ht.CloseChannel(carol, chanPoint) + ht.CloseChannel(carol, chanPoint2) } -// testPsbtChanFundingSingleStep checks whether PSBT funding works also when the -// wallet of both nodes are empty and one of them uses PSBT and an external +// testPsbtChanFundingSingleStep checks whether PSBT funding works also when +// the wallet of both nodes are empty and one of them uses PSBT and an external // wallet to fund the channel while creating reserve output in the same // transaction. -func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() +func testPsbtChanFundingSingleStep(ht *lntemp.HarnessTest) { const chanSize = funding.MaxBtcFundingAmount - // Everything we do here should be done within a second or two, so we - // can just keep a single timeout context around for all calls. - ctxt, cancel := context.WithTimeout(ctxb, 2*defaultTimeout) - defer cancel() - args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) // First, we'll create two new nodes that we'll use to open channels // between for this test. But in this case both nodes have an empty // wallet. - carol := net.NewNode(t.t, "carol", args) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("carol", args) + defer ht.Shutdown(carol) - dave := net.NewNode(t.t, "dave", args) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("dave", args) + defer ht.Shutdown(dave) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice) + alice := ht.Alice + ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) // Get new address for anchor reserve. - reserveAddrReq := &lnrpc.NewAddressRequest{ + req := &lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, } - addrResp, err := carol.NewAddress(ctxb, reserveAddrReq) - require.NoError(t.t, err) - reserveAddr, err := btcutil.DecodeAddress(addrResp.Address, harnessNetParams) - require.NoError(t.t, err) + addrResp := carol.RPC.NewAddress(req) + reserveAddr, err := btcutil.DecodeAddress( + addrResp.Address, harnessNetParams, + ) + require.NoError(ht, err) reserveAddrScript, err := txscript.PayToAddrScript(reserveAddr) - require.NoError(t.t, err) + require.NoError(ht, err) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. - net.EnsureConnected(t.t, carol, dave) + ht.EnsureConnected(carol, dave) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. - var pendingChanID [32]byte - _, err = rand.Read(pendingChanID[:]) - require.NoError(t.t, err) + pendingChanID := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. - chanUpdates, tempPsbt, err := openChannelPsbt( - ctxt, carol, dave, lntest.OpenChannelParams{ + chanUpdates, tempPsbt := ht.OpenChannelPsbt( + carol, dave, lntemp.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ - PendingChanId: pendingChanID[:], + PendingChanId: pendingChanID, NoPublish: false, }, }, }, }, ) - require.NoError(t.t, err) - decodedPsbt, err := psbt.NewFromRawBytes(bytes.NewReader(tempPsbt), false) - require.NoError(t.t, err) + decodedPsbt, err := psbt.NewFromRawBytes( + bytes.NewReader(tempPsbt), false, + ) + require.NoError(ht, err) reserveTxOut := wire.TxOut{ Value: 10000, @@ -538,7 +510,7 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { var psbtBytes bytes.Buffer err = decodedPsbt.Serialize(&psbtBytes) - require.NoError(t.t, err) + require.NoError(ht, err) fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ @@ -548,55 +520,45 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { SatPerVbyte: 2, }, } - fundResp, err := net.Alice.WalletKitClient.FundPsbt(ctxt, fundReq) - require.NoError(t.t, err) + fundResp := alice.RPC.FundPsbt(fundReq) // Make sure the wallets are actually empty - unspentCarol, err := carol.ListUnspent(ctxb, &lnrpc.ListUnspentRequest{}) - require.NoError(t.t, err) - require.Len(t.t, unspentCarol.Utxos, 0) - - unspentDave, err := dave.ListUnspent(ctxb, &lnrpc.ListUnspentRequest{}) - require.NoError(t.t, err) - require.Len(t.t, unspentDave.Utxos, 0) + ht.AssertNumUTXOsUnconfirmed(alice, 0) + ht.AssertNumUTXOsUnconfirmed(dave, 0) // We have a PSBT that has no witness data yet, which is exactly what we // need for the next step: Verify the PSBT with the funding intents. - _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ - PendingChanId: pendingChanID[:], + PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, }, }, }) - require.NoError(t.t, err) // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } - finalizeRes, err := net.Alice.WalletKitClient.FinalizePsbt(ctxt, finalizeReq) - require.NoError(t.t, err) + finalizeRes := alice.RPC.FinalizePsbt(finalizeReq) // We've signed our PSBT now, let's pass it to the intent again. - _, err = carol.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{ + carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ - PendingChanId: pendingChanID[:], + PendingChanId: pendingChanID, SignedPsbt: finalizeRes.SignedPsbt, }, }, }) - require.NoError(t.t, err) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. - updateResp, err := receiveChanUpdate(ctxt, chanUpdates) - require.NoError(t.t, err) + updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) - require.True(t.t, ok) + require.True(ht, ok) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, @@ -606,13 +568,12 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { var finalTx wire.MsgTx err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) - require.NoError(t.t, err) + require.NoError(ht, err) txHash := finalTx.TxHash() - block := mineBlocks(t, net, 6, 1)[0] - assertTxInBlock(t, block, &txHash) - err = carol.WaitForNetworkChannelOpen(chanPoint) - require.NoError(t.t, err) + block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] + ht.Miner.AssertTxInBlock(block, &txHash) + ht.AssertTopologyChannelOpen(carol, chanPoint) // Next, to make sure the channel functions as normal, we'll make some // payments within the channel. @@ -621,19 +582,23 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { Memo: "new chans", Value: int64(payAmt), } - resp, err := dave.AddInvoice(ctxt, invoice) - require.NoError(t.t, err) - err = completePaymentRequests( - carol, carol.RouterClient, []string{resp.PaymentRequest}, - true, - ) - require.NoError(t.t, err) + resp := dave.RPC.AddInvoice(invoice) + ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) + + // TODO(yy): remove the sleep once the following bug is fixed. When the + // payment is reported as settled by Carol, 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) // 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. - closeChannelAndAssert(t, net, carol, chanPoint, false) + ht.CloseChannel(carol, chanPoint) } // testSignPsbt tests that the SignPsbt RPC works correctly. diff --git a/lntest/itest/lnd_res_handoff_test.go b/lntest/itest/lnd_res_handoff_test.go index 36d74527c..a18d2fce0 100644 --- a/lntest/itest/lnd_res_handoff_test.go +++ b/lntest/itest/lnd_res_handoff_test.go @@ -5,131 +5,69 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lntest" - "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntemp" "github.com/stretchr/testify/require" - "golang.org/x/net/context" ) // testResHandoff tests that the contractcourt is able to properly hand-off // resolution messages to the switch. -func testResHandoff(net *lntest.NetworkHarness, t *harnessTest) { +func testResHandoff(ht *lntemp.HarnessTest) { const ( chanAmt = btcutil.Amount(1000000) paymentAmt = 50000 ) - ctxb := context.Background() + alice, bob := ht.Alice, ht.Bob // First we'll create a channel between Alice and Bob. - net.EnsureConnected(t.t, net.Alice, net.Bob) + ht.EnsureConnected(alice, bob) - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - defer closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - - // Wait for Alice and Bob to receive the channel edge from the funding - // manager. - err := net.Alice.WaitForNetworkChannelOpen(chanPointAlice) - require.NoError(t.t, err) - - err = net.Bob.WaitForNetworkChannelOpen(chanPointAlice) - require.NoError(t.t, err) + 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 := net.NewNode(t.t, "Carol", []string{"--hodl.commit"}) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", []string{"--hodl.commit"}) + defer ht.Shutdown(carol) // Connect Bob to Carol. - net.ConnectNodes(t.t, net.Bob, carol) + ht.ConnectNodes(bob, carol) // Open a channel between Bob and Carol. - chanPointCarol := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - - // Wait for Bob and Carol to receive the channel edge from the funding - // manager. - err = net.Bob.WaitForNetworkChannelOpen(chanPointCarol) - require.NoError(t.t, err) - - err = carol.WaitForNetworkChannelOpen(chanPointCarol) - require.NoError(t.t, err) + chanPointCarol := ht.OpenChannel(bob, carol, params) // Wait for Alice to see the channel edge in the graph. - err = net.Alice.WaitForNetworkChannelOpen(chanPointCarol) - require.NoError(t.t, err) + 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, _, _, err := createPayReqs( - carol, paymentAmt, 1, - ) - require.NoError(t.t, err) + carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, 1) // Alice will now attempt to fulfill the invoice. - err = completePaymentRequests( - net.Alice, net.Alice.RouterClient, carolPayReqs, false, - ) - require.NoError(t.t, err) + 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. - err = wait.Predicate(func() bool { - carolInfo, err := getChanInfo(carol) - if err != nil { - return false - } - - return carolInfo.NumUpdates == 1 - }, defaultTimeout) - require.NoError(t.t, err) + ht.AssertChannelCommitHeight(carol, chanPointCarol, 1) // Before we shutdown Alice, we'll assert that she only has 1 update. - err = wait.Predicate(func() bool { - aliceInfo, err := getChanInfo(net.Alice) - if err != nil { - return false - } - - return aliceInfo.NumUpdates == 1 - }, defaultTimeout) - require.NoError(t.t, err) + ht.AssertChannelCommitHeight(alice, chanPointAlice, 1) // We'll shutdown Alice so that Bob can't connect to her. - restartAlice, err := net.SuspendNode(net.Alice) - require.NoError(t.t, err) + restartAlice := ht.SuspendNode(alice) // Bob will now force close his channel with Carol such that resolution // messages are created and forwarded backwards to Alice. - _, _, err = net.CloseChannel(net.Bob, chanPointCarol, true) - require.NoError(t.t, err) + ht.CloseChannelAssertPending(bob, chanPointCarol, true) // The channel should be listed in the PendingChannels result. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() + ht.AssertNumWaitingClose(bob, 1) - pendingChansRequest := &lnrpc.PendingChannelsRequest{} - pendingChanResp, err := net.Bob.PendingChannels( - ctxt, pendingChansRequest, - ) - require.NoError(t.t, err) - require.NoError(t.t, checkNumWaitingCloseChannels(pendingChanResp, 1)) - - // We'll mine a block to confirm the force close transaction and to - // advance Bob's contract state with Carol to StateContractClosed. - mineBlocks(t, net, 1, 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 @@ -137,64 +75,25 @@ func testResHandoff(net *lntest.NetworkHarness, t *harnessTest) { // 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 + // Mine blocks until Bob has no waiting close channels. This tests that + // the circuit-deletion logic is skipped if a resolution message // exists. - for { - _, err = net.Miner.Client.Generate(1) - require.NoError(t.t, err) - - pendingChanResp, err = net.Bob.PendingChannels( - ctxt, pendingChansRequest, - ) - require.NoError(t.t, err) - - isErr := checkNumForceClosedChannels(pendingChanResp, 0) - if isErr == nil { - break - } - - time.Sleep(150 * time.Millisecond) - } + ht.CleanupForceClose(bob, chanPointCarol) // We will now restart Bob so that we can test whether the resolution // messages are re-forwarded on start-up. - restartBob, err := net.SuspendNode(net.Bob) - require.NoError(t.t, err) - - err = restartBob() - require.NoError(t.t, err) + ht.RestartNode(bob) // We'll now also restart Alice and connect her with Bob. - err = restartAlice() - require.NoError(t.t, err) + require.NoError(ht, restartAlice()) - net.EnsureConnected(t.t, net.Alice, net.Bob) + ht.EnsureConnected(alice, bob) - // We'll assert that Alice has received the failure resolution - // message. - err = wait.Predicate(func() bool { - aliceInfo, err := getChanInfo(net.Alice) - if err != nil { - return false - } - - return aliceInfo.NumUpdates == 2 - }, defaultTimeout) - require.NoError(t.t, err) + // We'll assert that Alice has received the failure resolution message. + ht.AssertChannelCommitHeight(alice, chanPointAlice, 2) // Assert that Alice's payment failed. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - paymentsResp, err := net.Alice.ListPayments( - ctxt, &lnrpc.ListPaymentsRequest{ - IncludeIncomplete: true, - }, - ) - require.NoError(t.t, err) - require.Equal(t.t, 1, len(paymentsResp.Payments)) + ht.AssertFirstHTLCError(alice, lnrpc.Failure_PERMANENT_CHANNEL_FAILURE) - htlcs := paymentsResp.Payments[0].Htlcs - - require.Equal(t.t, 1, len(htlcs)) - require.Equal(t.t, lnrpc.HTLCAttempt_FAILED, htlcs[0].Status) + ht.CloseChannel(alice, chanPointAlice) } diff --git a/lntest/itest/lnd_rest_api_test.go b/lntest/itest/lnd_rest_api_test.go index 16f7c6c17..43aff6110 100644 --- a/lntest/itest/lnd_rest_api_test.go +++ b/lntest/itest/lnd_rest_api_test.go @@ -2,7 +2,6 @@ package itest import ( "bytes" - "context" "crypto/tls" "encoding/base64" "encoding/hex" @@ -24,8 +23,8 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/verrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" - "github.com/lightningnetwork/lnd/lntest" - "github.com/stretchr/testify/assert" + "github.com/lightningnetwork/lnd/lntemp" + "github.com/lightningnetwork/lnd/lntemp/node" "github.com/stretchr/testify/require" ) @@ -57,19 +56,21 @@ var ( // testRestAPI tests that the most important features of the REST API work // correctly. -func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { +func testRestAPI(ht *lntemp.HarnessTest) { testCases := []struct { name string - run func(*testing.T, *lntest.HarnessNode, *lntest.HarnessNode) + run func(*testing.T, *node.HarnessNode, *node.HarnessNode) }{{ name: "simple GET", - run: func(t *testing.T, a, b *lntest.HarnessNode) { + 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") - assert.Equal(t, "#3399ff", resp.Color, "node color") + require.Equal(t, "#3399ff", resp.Color, "node color") // Make sure we get the correct field names (snake // case). @@ -77,20 +78,22 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { a, "/v1/getinfo", "GET", nil, nil, ) require.Nil(t, err, "getinfo") - assert.Contains( + require.Contains( t, string(resp2), "best_header_timestamp", "getinfo", ) }, }, { name: "simple POST and GET with query param", - run: func(t *testing.T, a, b *lntest.HarnessNode) { + 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") - assert.Equal(t, 32, len(resp.RHash), "invoice rhash") + require.Equal(t, 32, len(resp.RHash), "invoice rhash") // Make sure we can call a GET endpoint with a hex // encoded URL part. @@ -98,11 +101,14 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { resp2 := &lnrpc.Invoice{} err = invokeGET(a, url, resp2) require.Nil(t, err, "query invoice") - assert.Equal(t, int64(1234), resp2.Value, "invoice amt") + 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 *lntest.HarnessNode) { + 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[:]), @@ -115,38 +121,37 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { }, }, { name: "GET with map type query param", - run: func(t *testing.T, a, b *lntest.HarnessNode) { - // Get a new wallet address from Alice. - ctxb := context.Background() - newAddrReq := &lnrpc.NewAddressRequest{ - Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, - } - addrRes, err := a.NewAddress(ctxb, newAddrReq) - require.Nil(t, err, "get address") + 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, addrRes.Address, 50000) + url = fmt.Sprintf(url, 2, addr, 50000) resp := &lnrpc.EstimateFeeResponse{} - err = invokeGET(a, url, resp) + err := invokeGET(a, url, resp) require.Nil(t, err, "estimate fee") - assert.Greater(t, resp.FeeSat, int64(253), "fee") + require.Greater(t, resp.FeeSat, int64(253), "fee") }, }, { name: "sub RPC servers REST support", - run: func(t *testing.T, a, b *lntest.HarnessNode) { + 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") - assert.Equal(t, false, res1.Active, "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") - assert.Greater( + require.Greater( t, res2.AppMinor, uint32(0), "lnd minor version", ) @@ -157,11 +162,13 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { a, "/v2/wallet/address/next", req1, res3, ) require.Nil(t, err, "address") - assert.NotEmpty(t, res3.Addr, "address") + require.NotEmpty(t, res3.Addr, "address") }, }, { name: "CORS headers", - run: func(t *testing.T, a, b *lntest.HarnessNode) { + 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. @@ -171,12 +178,12 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { a, "/v1/getinfo", "OPTIONS", nil, reqHeaders, ) require.Nil(t, err, "getinfo") - assert.Equal( + require.Equal( t, "https://foo.bar:9999", resHeaders.Get("Access-Control-Allow-Origin"), "CORS header", ) - assert.Equal(t, 0, len(body)) + 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. @@ -184,17 +191,17 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { b, "/v1/getinfo", "OPTIONS", nil, reqHeaders, ) require.Nil(t, err, "getinfo") - assert.Equal( + require.Equal( t, "", resHeaders.Get("Access-Control-Allow-Origin"), "CORS header", ) - assert.Equal(t, 0, len(body)) + require.Equal(t, 0, len(body)) }, }} wsTestCases := []struct { name string - run func(ht *harnessTest, net *lntest.NetworkHarness) + run func(ht *lntemp.HarnessTest) }{{ name: "websocket subscription", run: wsTestCaseSubscription, @@ -212,39 +219,33 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { // 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. - net.Alice.Cfg.ExtraArgs = append( - net.Alice.Cfg.ExtraArgs, "--restcors=\"*\"", + 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), ) - err := net.RestartNode(net.Alice, nil) - if err != nil { - ht.t.Fatalf("Could not restart Alice to set CORS config: %v", - err) - } + ht.RestartNode(alice) for _, tc := range testCases { tc := tc - ht.t.Run(tc.name, func(t *testing.T) { - tc.run(t, net.Alice, net.Bob) + ht.Run(tc.name, func(t *testing.T) { + tc.run(t, alice, bob) }) } for _, tc := range wsTestCases { tc := tc - ht.t.Run(tc.name, func(t *testing.T) { - ht := &harnessTest{ - t: t, testCase: ht.testCase, lndHarness: net, - } - tc.run(ht, net) + ht.Run(tc.name, func(t *testing.T) { + st := ht.Subtest(t) + tc.run(st) }) } } -func wsTestCaseSubscription(ht *harnessTest, net *lntest.NetworkHarness) { +func wsTestCaseSubscription(ht *lntemp.HarnessTest) { // Find out the current best block so we can subscribe to the next one. - hash, height, err := net.Miner.Client.GetBestBlock() - require.Nil(ht.t, err, "get best block") + hash, height := ht.Miner.GetBestBlock() // Create a new subscription to get block epoch events. req := &chainrpc.BlockEpoch{ @@ -252,11 +253,11 @@ func wsTestCaseSubscription(ht *harnessTest, net *lntest.NetworkHarness) { Height: uint32(height), } url := "/v2/chainnotifier/register/blocks" - c, err := openWebSocket(net.Alice, url, "POST", req, nil) - require.Nil(ht.t, err, "websocket") + 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.t, err) + require.NoError(ht, err) _ = c.Close() }() @@ -300,30 +301,25 @@ func wsTestCaseSubscription(ht *harnessTest, net *lntest.NetworkHarness) { }() // Mine a block and make sure we get a message for it. - blockHashes, err := net.Miner.Client.Generate(1) - require.Nil(ht.t, err, "generate blocks") - assert.Equal(ht.t, 1, len(blockHashes), "num blocks") + blockHashes := ht.Miner.GenerateBlocks(1) select { case msg := <-msgChan: - assert.Equal( - ht.t, blockHashes[0].CloneBytes(), msg.Hash, + require.Equal( + ht, blockHashes[0].CloneBytes(), msg.Hash, "block hash", ) case err := <-errChan: - ht.t.Fatalf("Received error from WS: %v", err) + ht.Fatalf("Received error from WS: %v", err) case <-timeout: - ht.t.Fatalf("Timeout before message was received") + ht.Fatalf("Timeout before message was received") } } -func wsTestCaseSubscriptionMacaroon(ht *harnessTest, - net *lntest.NetworkHarness) { - +func wsTestCaseSubscriptionMacaroon(ht *lntemp.HarnessTest) { // Find out the current best block so we can subscribe to the next one. - hash, height, err := net.Miner.Client.GetBestBlock() - require.Nil(ht.t, err, "get best block") + hash, height := ht.Miner.GetBestBlock() // Create a new subscription to get block epoch events. req := &chainrpc.BlockEpoch{ @@ -335,22 +331,23 @@ func wsTestCaseSubscriptionMacaroon(ht *harnessTest, // 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. - mac, err := net.Alice.ReadMacaroon( - net.Alice.AdminMacPath(), defaultTimeout, + alice := ht.Alice + mac, err := alice.ReadMacaroon( + alice.Cfg.AdminMacPath, defaultTimeout, ) - require.NoError(ht.t, err, "read admin mac") + require.NoError(ht, err, "read admin mac") macBytes, err := mac.MarshalBinary() - require.NoError(ht.t, err, "marshal admin mac") + 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(net.Alice, url, "POST", req, customHeader) - require.Nil(ht.t, err, "websocket") + c, err := openWebSocket(alice, url, "POST", req, customHeader) + require.Nil(ht, err, "websocket") defer func() { err := c.WriteMessage(websocket.CloseMessage, closeMsg) - require.NoError(ht.t, err) + require.NoError(ht, err) _ = c.Close() }() @@ -394,52 +391,49 @@ func wsTestCaseSubscriptionMacaroon(ht *harnessTest, }() // Mine a block and make sure we get a message for it. - blockHashes, err := net.Miner.Client.Generate(1) - require.Nil(ht.t, err, "generate blocks") - assert.Equal(ht.t, 1, len(blockHashes), "num blocks") + blockHashes := ht.Miner.GenerateBlocks(1) select { case msg := <-msgChan: - assert.Equal( - ht.t, blockHashes[0].CloneBytes(), msg.Hash, + require.Equal( + ht, blockHashes[0].CloneBytes(), msg.Hash, "block hash", ) case err := <-errChan: - ht.t.Fatalf("Received error from WS: %v", err) + ht.Fatalf("Received error from WS: %v", err) case <-timeout: - ht.t.Fatalf("Timeout before message was received") + ht.Fatalf("Timeout before message was received") } } -func wsTestCaseBiDirectionalSubscription(ht *harnessTest, - net *lntest.NetworkHarness) { - +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. - mac, err := net.Alice.ReadMacaroon( - net.Alice.AdminMacPath(), defaultTimeout, + alice := ht.Alice + mac, err := alice.ReadMacaroon( + alice.Cfg.AdminMacPath, defaultTimeout, ) - require.NoError(ht.t, err, "read admin mac") + require.NoError(ht, err, "read admin mac") macBytes, err := mac.MarshalBinary() - require.NoError(ht.t, err, "marshal admin mac") + 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( - net.Alice, url, "POST", initialRequest, customHeader, + alice, url, "POST", initialRequest, customHeader, ) - require.Nil(ht.t, err, "websocket") + require.Nil(ht, err, "websocket") defer func() { err := conn.WriteMessage(websocket.CloseMessage, closeMsg) _ = conn.Close() - require.NoError(ht.t, err) + require.NoError(ht, err) }() // Buffer the message channel to make sure we're always blocking on @@ -528,29 +522,30 @@ func wsTestCaseBiDirectionalSubscription(ht *harnessTest, // Before we start opening channels, make sure the two nodes are // connected. - net.EnsureConnected(ht.t, net.Alice, net.Bob) + 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++ { - openChannelAndAssert( - ht, net, net.Bob, net.Alice, - lntest.OpenChannelParams{Amt: 500000}, + chanPoint := ht.OpenChannel( + bob, alice, lntemp.OpenChannelParams{Amt: 500000}, ) + defer ht.CloseChannel(bob, chanPoint) select { case <-msgChan: case err := <-errChan: - ht.t.Fatalf("Received error from WS: %v", err) + ht.Fatalf("Received error from WS: %v", err) case <-timeout: - ht.t.Fatalf("Timeout before message was received") + ht.Fatalf("Timeout before message was received") } } } -func wsTestPingPongTimeout(ht *harnessTest, net *lntest.NetworkHarness) { +func wsTestPingPongTimeout(ht *lntemp.HarnessTest) { initialRequest := &lnrpc.InvoiceSubscription{ AddIndex: 1, SettleIndex: 1, } @@ -559,25 +554,26 @@ func wsTestPingPongTimeout(ht *harnessTest, net *lntest.NetworkHarness) { // 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. - mac, err := net.Alice.ReadMacaroon( - net.Alice.AdminMacPath(), defaultTimeout, + alice := ht.Alice + mac, err := alice.ReadMacaroon( + alice.Cfg.AdminMacPath, defaultTimeout, ) - require.NoError(ht.t, err, "read admin mac") + require.NoError(ht, err, "read admin mac") macBytes, err := mac.MarshalBinary() - require.NoError(ht.t, err, "marshal admin mac") + 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( - net.Alice, url, "GET", initialRequest, customHeader, + alice, url, "GET", initialRequest, customHeader, ) - require.Nil(ht.t, err, "websocket") + require.Nil(ht, err, "websocket") defer func() { err := conn.WriteMessage(websocket.CloseMessage, closeMsg) _ = conn.Close() - require.NoError(ht.t, err) + require.NoError(ht, err) }() // We want to be able to read invoices for a long time, making sure we @@ -650,27 +646,26 @@ func wsTestPingPongTimeout(ht *harnessTest, net *lntest.NetworkHarness) { // Let's create five invoices and wait for them to arrive. We'll wait // for at least one ping/pong cycle between each invoice. - ctxb := context.Background() const numInvoices = 5 const value = 123 const memo = "websocket" for i := 0; i < numInvoices; i++ { - _, err := net.Alice.AddInvoice(ctxb, &lnrpc.Invoice{ + invoice := &lnrpc.Invoice{ Value: value, Memo: memo, - }) - require.NoError(ht.t, err) + } + alice.RPC.AddInvoice(invoice) select { case streamMsg := <-invoices: - require.Equal(ht.t, int64(value), streamMsg.Value) - require.Equal(ht.t, memo, streamMsg.Memo) + require.Equal(ht, int64(value), streamMsg.Value) + require.Equal(ht, memo, streamMsg.Memo) case err := <-errChan: - require.Fail(ht.t, "Error reading invoice: %v", err) + require.Fail(ht, "Error reading invoice: %v", err) case <-timeout: - require.Fail(ht.t, "No invoice msg received in time") + require.Fail(ht, "No invoice msg received in time") } // Let's wait for at least a whole ping/pong cycle to happen, so @@ -683,7 +678,7 @@ func wsTestPingPongTimeout(ht *harnessTest, net *lntest.NetworkHarness) { // 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 *lntest.HarnessNode, url string, resp proto.Message) error { +func invokeGET(node *node.HarnessNode, url string, resp proto.Message) error { _, rawResp, err := makeRequest(node, url, "GET", nil, nil) if err != nil { return err @@ -695,7 +690,7 @@ func invokeGET(node *lntest.HarnessNode, url string, resp proto.Message) error { // 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 *lntest.HarnessNode, url string, req, +func invokePOST(node *node.HarnessNode, url string, req, resp proto.Message) error { // Marshal the request to JSON using the jsonpb marshaler to get correct @@ -715,7 +710,7 @@ func invokePOST(node *lntest.HarnessNode, url string, req, // 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 *lntest.HarnessNode, url, method string, +func makeRequest(node *node.HarnessNode, url, method string, request io.Reader, additionalHeaders http.Header) (http.Header, []byte, error) { @@ -748,7 +743,7 @@ func makeRequest(node *lntest.HarnessNode, url, method string, // 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 *lntest.HarnessNode, url, method string, +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 @@ -785,8 +780,8 @@ func openWebSocket(node *lntest.HarnessNode, url, method string, // addAdminMacaroon reads the admin macaroon from the node and appends it to // the HTTP header fields. -func addAdminMacaroon(node *lntest.HarnessNode, header http.Header) error { - mac, err := node.ReadMacaroon(node.AdminMacPath(), defaultTimeout) +func addAdminMacaroon(node *node.HarnessNode, header http.Header) error { + mac, err := node.ReadMacaroon(node.Cfg.AdminMacPath, defaultTimeout) if err != nil { return err } diff --git a/lntest/itest/lnd_revocation_test.go b/lntest/itest/lnd_revocation_test.go index a562c88d9..bff6e6760 100644 --- a/lntest/itest/lnd_revocation_test.go +++ b/lntest/itest/lnd_revocation_test.go @@ -2,9 +2,7 @@ package itest import ( "bytes" - "context" "fmt" - "strings" "testing" "time" @@ -13,8 +11,8 @@ import ( "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" "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" @@ -23,7 +21,7 @@ import ( // 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(net *lntest.NetworkHarness, t *harnessTest) { +func testRevokedCloseRetribution(ht *lntemp.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 @@ -35,77 +33,38 @@ func testRevokedCloseRetribution(net *lntest.NetworkHarness, t *harnessTest) { // 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 := net.NewNode( - t.t, "Carol", + carol := ht.NewNode( + "Carol", []string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"}, ) - defer shutdownAndAssert(net, t, carol) // We must let Bob communicate with Carol before they are able to open // channel, so we connect Bob and Carol, - net.ConnectNodes(t.t, carol, net.Bob) + 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. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + 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 := openChannelAndAssert( - t, net, carol, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + 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, _, _, err := createPayReqs( - net.Bob, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // Wait for Carol to receive the channel edge from the funding manager. - err = carol.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("carol didn't see the carol->bob channel before "+ - "timeout: %v", err) - } + bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numInvoices) // Send payments from Carol to Bob using 3 of Bob's payment hashes // generated above. - err = completePaymentRequests( - carol, carol.RouterClient, bobPayReqs[:numInvoices/2], true, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } + 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. - var bobChan *lnrpc.Channel - var predErr error - err = wait.Predicate(func() bool { - bChan, err := getChanInfo(net.Bob) - if err != nil { - t.Fatalf("unable to get bob's channel info: %v", err) - } - if bChan.LocalBalance != 30000 { - predErr = fmt.Errorf("bob's balance is incorrect, "+ - "got %v, expected %v", bChan.LocalBalance, - 30000) - return false - } - - bobChan = bChan - return true - }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + 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 @@ -115,149 +74,99 @@ func testRevokedCloseRetribution(net *lntest.NetworkHarness, t *harnessTest) { // 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. - if err := net.BackupDb(net.Bob); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(bob) // Reconnect the peers after the restart that was needed for the db // backup. - net.EnsureConnected(t.t, carol, net.Bob) + 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. - err = completePaymentRequests( - carol, carol.RouterClient, bobPayReqs[numInvoices/2:], true, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } - - bobChan, err = getChanInfo(net.Bob) - if err != nil { - t.Fatalf("unable to get bob chan info: %v", err) - } + 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. - if err = net.RestartNode(net.Bob, func() error { - return net.RestoreDb(net.Bob) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } + 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. - bobChan, err = getChanInfo(net.Bob) - if err != nil { - t.Fatalf("unable to get bob chan info: %v", err) - } - if bobChan.NumUpdates != bobStateNumPreCopy { - t.Fatalf("db copy failed: %v", bobChan.NumUpdates) - } + 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. - force := true - closeUpdates, _, err := net.CloseChannel( - net.Bob, chanPoint, force, - ) - require.NoError(t.t, err, "unable to close channel") + _, breachTXID := ht.CloseChannelAssertPending(bob, chanPoint, true) - // Wait for Bob's breach transaction to show up in the mempool to ensure - // that Carol's node has started waiting for confirmations. - _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Bob's breach tx in mempool: %v", err) - } - - // 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. - if err := net.RestartNode(carol, nil); err != nil { - t.Fatalf("unable to restart Carol's node: %v", err) - } + // 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 := mineBlocks(t, net, 1, 1)[0] - - breachTXID, err := net.WaitForChannelClose(closeUpdates) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) - } - assertTxInBlock(t, block, breachTXID) + 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, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's justice tx in mempool: %v", err) - } - time.Sleep(100 * time.Millisecond) + 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, err := net.Miner.Client.GetRawTransaction(justiceTXID) - if err != nil { - t.Fatalf("unable to query for justice tx: %v", err) - } + justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { - if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { - t.Fatalf("justice tx not spending commitment utxo "+ - "instead is: %v", txIn.PreviousOutPoint) - } + 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. - if err := net.RestartNode(carol, nil); err != nil { - t.Fatalf("unable to restart Carol's node: %v", err) - } + // 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 = mineBlocks(t, net, 1, 1)[0] + block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. - if len(block.Transactions) != 2 { - t.Fatalf("transaction wasn't mined") - } - justiceSha := block.Transactions[1].TxHash() - if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { - t.Fatalf("justice tx wasn't mined") - } + require.Len(ht, block.Transactions, 2, "transaction wasn't mined") - assertNodeNumChannels(t, carol, 0) + 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. - mineBlocks(t, net, lntest.DefaultCSV-2, 0) + ht.MineBlocks(lntest.DefaultCSV - 2) - assertNumPendingChannels(t, net.Bob, 0, 0) + 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(net *lntest.NetworkHarness, - t *harnessTest) { - +func testRevokedCloseRetributionZeroValueRemoteOutput(ht *lntemp.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 @@ -266,177 +175,103 @@ func testRevokedCloseRetributionZeroValueRemoteOutput(net *lntest.NetworkHarness // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. - carol := net.NewNode(t.t, "Carol", []string{"--hodl.exit-settle"}) - defer shutdownAndAssert(net, t, 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 := net.NewNode( - t.t, "Dave", + dave := ht.NewNode( + "Dave", []string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"}, ) - defer shutdownAndAssert(net, t, dave) // We must let Dave have an open channel before he can send a node // announcement, so we open a channel with Carol, - net.ConnectNodes(t.t, dave, carol) + ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) + 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 := openChannelAndAssert( - t, net, dave, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + 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, _, _, err := createPayReqs( - carol, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // Wait for Dave to receive the channel edge from the funding manager. - err = dave.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("dave didn't see the dave->carol channel before "+ - "timeout: %v", err) - } + 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, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } - if carolChan.LocalBalance != 0 { - t.Fatalf("carol's balance is incorrect, got %v, expected %v", - carolChan.LocalBalance, 0) - } + 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 := carolChan.NumUpdates + 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. - if err := net.BackupDb(carol); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. - net.EnsureConnected(t.t, dave, carol) + ht.EnsureConnected(dave, carol) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. - err = completePaymentRequests( - dave, dave.RouterClient, carolPayReqs, false, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } - - _, err = getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol chan info: %v", err) - } + 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. - if err = net.RestartNode(carol, func() error { - return net.RestoreDb(carol) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } + 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. - carolChan, err = getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol chan info: %v", err) - } - if carolChan.NumUpdates != carolStateNumPreCopy { - t.Fatalf("db copy failed: %v", carolChan.NumUpdates) - } + 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. - force := true - closeUpdates, closeTxID, closeErr := net.CloseChannel( - carol, chanPoint, force, + stream, closeTxID := ht.CloseChannelAssertPending( + carol, chanPoint, true, ) - require.NoError(t.t, closeErr, "unable to close channel") - - // Query the mempool for the breaching closing transaction, this should - // be broadcast by Carol when she force closes the channel above. - txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's force close tx in mempool: %v", - err) - } - if *txid != *closeTxID { - t.Fatalf("expected closeTx(%v) in mempool, instead found %v", - closeTxID, txid) - } // Finally, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. - block := mineBlocks(t, net, 1, 1)[0] + 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. - if err := net.RestartNode(dave, nil); err != nil { - t.Fatalf("unable to stop Dave's node: %v", err) - } + ht.RestartNode(dave) - breachTXID, err := net.WaitForChannelClose(closeUpdates) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) - } - assertTxInBlock(t, block, breachTXID) + // 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, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Dave's justice tx in mempool: %v", - err) - } - time.Sleep(100 * time.Millisecond) + 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, err := net.Miner.Client.GetRawTransaction(justiceTXID) - if err != nil { - t.Fatalf("unable to query for justice tx: %v", err) - } + justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { - if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { - t.Fatalf("justice tx not spending commitment utxo "+ - "instead is: %v", txIn.PreviousOutPoint) - } + 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 @@ -444,33 +279,26 @@ func testRevokedCloseRetributionZeroValueRemoteOutput(net *lntest.NetworkHarness // 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. - if err := net.RestartNode(dave, nil); err != nil { - t.Fatalf("unable to restart Dave's node: %v", err) - } + ht.RestartNode(dave) // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. - block = mineBlocks(t, net, 1, 1)[0] + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. - if len(block.Transactions) != 2 { - t.Fatalf("transaction wasn't mined") - } + require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() - if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { - t.Fatalf("justice tx wasn't mined") - } + require.Equal(ht, justiceTx.Hash()[:], justiceSha[:], + "justice tx wasn't mined") - assertNodeNumChannels(t, dave, 0) + 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(net *lntest.NetworkHarness, - t *harnessTest) { - +func testRevokedCloseRetributionRemoteHodl(ht *lntemp.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount pushAmt = 200000 @@ -481,34 +309,31 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, // 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 := net.NewNode(t.t, "Carol", []string{"--hodl.exit-settle"}) - defer shutdownAndAssert(net, t, 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 := net.NewNode( - t.t, "Dave", + dave := ht.NewNode( + "Dave", []string{"--hodl.exit-settle", "--nolisten"}, ) - defer shutdownAndAssert(net, t, dave) // We must let Dave communicate with Carol before they are able to open // channel, so we connect Dave and Carol, - net.ConnectNodes(t.t, dave, carol) + ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) + 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 := openChannelAndAssert( - t, net, dave, carol, - lntest.OpenChannelParams{ + chanPoint := ht.OpenChannel( + dave, carol, lntemp.OpenChannelParams{ Amt: chanAmt, PushAmt: pushAmt, }, @@ -516,206 +341,122 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, // 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, _, _, err := createPayReqs( - carol, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } + carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices) - // We'll introduce a closure to validate that Carol's current balance - // matches the given expected amount. - checkCarolBalance := func(expectedAmt int64) { - carolChan, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } - if carolChan.LocalBalance != expectedAmt { - t.Fatalf("carol's balance is incorrect, "+ - "got %v, expected %v", carolChan.LocalBalance, - expectedAmt) - } - } - - // We'll introduce another closure to validate that Carol's current + // 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(minimum uint64) { - carolChan, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } - if carolChan.NumUpdates < minimum { - t.Fatalf("carol's numupdates is incorrect, want %v "+ - "to be at least %v", carolChan.NumUpdates, - minimum) - } - } + checkCarolNumUpdatesAtLeast := func(carolChan *lnrpc.Channel, + minimum int) { - // Wait for Dave to receive the channel edge from the funding manager. - err = dave.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("dave didn't see the dave->carol channel before "+ - "timeout: %v", err) + 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. - checkCarolBalance(pushAmt) + ht.AssertChannelLocalBalance(carol, chanPoint, pushAmt) // Send payments from Dave to Carol using 3 of Carol's payment hashes // generated above. - err = completePaymentRequests( - dave, dave.RouterClient, carolPayReqs[:numInvoices/2], false, + ht.CompletePaymentRequestsNoWait( + dave, carolPayReqs[:numInvoices/2], chanPoint, ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } // 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, _, _, err := createPayReqs( - dave, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } + davePayReqs, _, _ := ht.CreatePayReqs(dave, paymentAmt, numInvoices) // Send payments from Carol to Dave using 3 of Dave's payment hashes // generated above. - err = completePaymentRequests( - carol, carol.RouterClient, davePayReqs[:numInvoices/2], false, + ht.CompletePaymentRequestsNoWait( + carol, davePayReqs[:numInvoices/2], chanPoint, ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } // 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. - carolChan, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } + + // 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 := carolChan.NumUpdates - - // Ensure that carol's balance still reflects the original amount we - // pushed to her, minus the HTLCs she just sent to Dave. - checkCarolBalance(pushAmt - 3*paymentAmt) + carolStateNumPreCopy := int(carolChan.NumUpdates) // Since Carol has not settled, she should only see at least one update // to her channel. - checkCarolNumUpdatesAtLeast(1) + 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. - if err := net.BackupDb(carol); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. - net.EnsureConnected(t.t, dave, carol) + ht.EnsureConnected(dave, carol) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. - err = completePaymentRequests( - dave, dave.RouterClient, carolPayReqs[numInvoices/2:], false, + ht.CompletePaymentRequestsNoWait( + dave, carolPayReqs[numInvoices/2:], chanPoint, ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } // 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. - time.Sleep(500 * time.Millisecond) - checkCarolBalance(pushAmt - 3*paymentAmt) - checkCarolNumUpdatesAtLeast(carolStateNumPreCopy + 1) + 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, err := net.SuspendNode(dave) - if err != nil { - t.Fatalf("unable to suspend Dave: %v", err) - } + 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. - if err = net.RestartNode(carol, func() error { - return net.RestoreDb(carol) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } - - time.Sleep(200 * time.Millisecond) + ht.RestartNodeAndRestoreDB(carol) // Ensure that Carol's view of the channel is consistent with the state // of the channel just before it was snapshotted. - checkCarolBalance(pushAmt - 3*paymentAmt) - checkCarolNumUpdatesAtLeast(1) + 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. - carolChan, err = getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol chan info: %v", err) - } - if carolChan.NumUpdates != carolStateNumPreCopy { - t.Fatalf("db copy failed: %v", carolChan.NumUpdates) - } + 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. - force := true - closeUpdates, closeTxID, err := net.CloseChannel( - carol, chanPoint, force, + closeUpdates, closeTxID := ht.CloseChannelAssertPending( + carol, chanPoint, true, ) - if err != nil { - t.Fatalf("unable to close channel: %v", err) - } - - // Query the mempool for the breaching closing transaction, this should - // be broadcast by Carol when she force closes the channel above. - txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's force close tx in mempool: %v", - err) - } - if *txid != *closeTxID { - t.Fatalf("expected closeTx(%v) in mempool, instead found %v", - closeTxID, txid) - } // Generate a single block to mine the breach transaction. - block := mineBlocks(t, net, 1, 1)[0] + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] // We resurrect Dave to ensure he will be exacting justice after his // node restarts. - if err := restartDave(); err != nil { - t.Fatalf("unable to stop Dave's node: %v", err) - } + 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, err := net.WaitForChannelClose(closeUpdates) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) - } - if *breachTXID != *closeTxID { - t.Fatalf("expected breach ID(%v) to be equal to close ID (%v)", - breachTXID, closeTxID) - } - assertTxInBlock(t, block, breachTXID) + 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 @@ -723,24 +464,15 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, // 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 predErr error var justiceTxid *chainhash.Hash errNotFound := errors.New("justice tx not found") findJusticeTx := func() (*chainhash.Hash, error) { - mempool, err := net.Miner.Client.GetRawMempool() - if err != nil { - return nil, fmt.Errorf("unable to get mempool from "+ - "miner: %v", err) - } + mempool := ht.Miner.GetRawMempool() for _, txid := range mempool { // Check that the justice tx has the appropriate number // of inputs. - tx, err := net.Miner.Client.GetRawTransaction(txid) - if err != nil { - return nil, fmt.Errorf("unable to query for "+ - "txs: %v", err) - } + tx := ht.Miner.GetRawTransaction(txid) exNumInputs := 2 + numInvoices if len(tx.MsgTx().TxIn) == exNumInputs { @@ -750,17 +482,17 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, return nil, errNotFound } - err = wait.Predicate(func() bool { + err := wait.NoError(func() error { txid, err := findJusticeTx() if err != nil { - predErr = err - return false + return err } - justiceTxid = txid - return true + + return nil }, defaultTimeout) - if err != nil && predErr == errNotFound { + + 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 @@ -769,35 +501,28 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, // 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. - mineBlocks(t, net, 1, 0) - err = wait.Predicate(func() bool { + ht.MineBlocks(1) + err = wait.NoError(func() error { txid, err := findJusticeTx() if err != nil { - predErr = err - return false + return err } justiceTxid = txid - return true + + return nil }, defaultTimeout) } - if err != nil { - t.Fatalf(predErr.Error()) - } + require.NoError(ht, err, "timeout finding justice tx") - justiceTx, err := net.Miner.Client.GetRawTransaction(justiceTxid) - if err != nil { - t.Fatalf("unable to query for justice tx: %v", err) - } + 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, err := net.Miner.Client.GetRawTransaction( - secondLevelTxid) - if err != nil { - t.Fatalf("unable to query for tx: %v", err) - } + isSecondLevelSpend := func(commitTxid, + secondLevelTxid *chainhash.Hash) bool { + + secondLevel := ht.Miner.GetRawTransaction(secondLevelTxid) // A second level spend should have only one input, and one // output. @@ -828,27 +553,23 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, if isSecondLevelSpend(breachTXID, &txIn.PreviousOutPoint.Hash) { continue } - t.Fatalf("justice tx not spending commitment utxo "+ + require.Fail(ht, "justice tx not spending commitment utxo "+ "instead is: %v", txIn.PreviousOutPoint) } - time.Sleep(100 * time.Millisecond) // 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. - if err := net.RestartNode(dave, nil); err != nil { - t.Fatalf("unable to restart Dave's node: %v", err) - } + ht.RestartNode(dave) // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. - block = mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, justiceTxid) + ht.MineBlocksAndAssertNumTxes(1, 1) // Dave should have no open channels. - assertNodeNumChannels(t, dave, 0) + ht.AssertNodeNumChannels(dave, 0) } // testRevokedCloseRetributionAltruistWatchtower establishes a channel between @@ -857,9 +578,7 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness, // 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(net *lntest.NetworkHarness, - t *harnessTest) { - +func testRevokedCloseRetributionAltruistWatchtower(ht *lntemp.HarnessTest) { testCases := []struct { name string anchors bool @@ -873,23 +592,25 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, for _, tc := range testCases { tc := tc + testFunc := func(ht *lntemp.HarnessTest) { + testRevokedCloseRetributionAltruistWatchtowerCase( + ht, tc.anchors, + ) + } - success := t.t.Run(tc.name, func(tt *testing.T) { - ht := newHarnessTest(tt, net) - ht.RunTestCase(&testCase{ - name: tc.name, - test: func(net1 *lntest.NetworkHarness, t1 *harnessTest) { - testRevokedCloseRetributionAltruistWatchtowerCase( - net1, t1, 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. - t.Logf("Failure time: %v", time.Now().Format( + ht.Logf("Failure time: %v", time.Now().Format( "2006-01-02 15:04:05.000", )) @@ -898,10 +619,9 @@ func testRevokedCloseRetributionAltruistWatchtower(net *lntest.NetworkHarness, } } -func testRevokedCloseRetributionAltruistWatchtowerCase( - net *lntest.NetworkHarness, t *harnessTest, anchors bool) { +func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntemp.HarnessTest, + anchors bool) { - ctxb := context.Background() const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 @@ -915,49 +635,32 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( if anchors { carolArgs = append(carolArgs, "--protocol.anchors") } - carol := net.NewNode(t.t, "Carol", carolArgs) - defer shutdownAndAssert(net, t, carol) + 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 := net.NewNode(t.t, "Willy", []string{ - "--watchtower.active", - "--watchtower.externalip=" + externalIP, - }) - defer shutdownAndAssert(net, t, willy) - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - willyInfo, err := willy.Watchtower.GetInfo( - ctxt, &watchtowerrpc.GetInfoRequest{}, + willy := ht.NewNode( + "Willy", []string{"--watchtower.active", + "--watchtower.externalip=" + externalIP}, ) - if err != nil { - t.Fatalf("unable to getinfo from willy: %v", err) - } + + 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. - if len(willyInfo.Listeners) != 1 { - t.Fatalf("Willy should have 1 listener, has %d", - len(willyInfo.Listeners)) - } + require.Len(ht, willyInfo.Listeners, 1, "Willy should have 1 listener") listener := willyInfo.Listeners[0] if listener != "0.0.0.0:9911" && listener != "[::]:9911" { - t.Fatalf("expected listener on 0.0.0.0:9911 or [::]: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. - if len(willyInfo.Uris) != 1 { - t.Fatalf("Willy should have 1 uri, has %d", - len(willyInfo.Uris)) - } - if !strings.Contains(willyInfo.Uris[0], externalIP) { - t.Fatalf("expected uri with %v, got %v", - externalIP, willyInfo.Uris[0]) - } + 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 @@ -969,110 +672,72 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( if anchors { daveArgs = append(daveArgs, "--protocol.anchors") } - dave := net.NewNode(t.t, "Dave", daveArgs) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("Dave", daveArgs) - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) addTowerReq := &wtclientrpc.AddTowerRequest{ Pubkey: willyInfo.Pubkey, Address: listener, } - if _, err := dave.WatchtowerClient.AddTower(ctxt, addTowerReq); err != nil { - t.Fatalf("unable to add willy's watchtower: %v", err) - } + 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, - net.ConnectNodes(t.t, dave, carol) + ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) + 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. - chanPoint := openChannelAndAssert( - t, net, dave, carol, - lntest.OpenChannelParams{ - Amt: 3 * (chanAmt / 4), - PushAmt: chanAmt / 4, - }, - ) + 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, _, _, err := createPayReqs( - carol, paymentAmt, numInvoices, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - // Wait for Dave to receive the channel edge from the funding manager. - err = dave.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("dave didn't see the dave->carol channel before "+ - "timeout: %v", err) - } + 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, err := getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol's channel info: %v", err) - } - if carolChan.LocalBalance != int64(chanAmt/4) { - t.Fatalf("carol's balance is incorrect, got %v, expected %v", - carolChan.LocalBalance, chanAmt/4) - } + 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 := carolChan.NumUpdates + 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. - if err := net.BackupDb(carol); err != nil { - t.Fatalf("unable to copy database files: %v", err) - } + ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. - net.EnsureConnected(t.t, dave, carol) + ht.EnsureConnected(dave, carol) - // Finally, send payments from Dave to Carol, consuming Carol's remaining - // payment hashes. - err = completePaymentRequests( - dave, dave.RouterClient, carolPayReqs, false, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } - - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + // 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 { - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - bkpStats, err := dave.WatchtowerClient.Stats(ctxt, - &wtclientrpc.StatsRequest{}, - ) - if err != nil { - return err - } + err := wait.NoError(func() error { + bkpStats := dave.RPC.WatchtowerStats() if bkpStats == nil { return errors.New("no active backup sessions") } @@ -1082,197 +747,133 @@ func testRevokedCloseRetributionAltruistWatchtowerCase( return nil }, defaultTimeout) - if err != nil { - t.Fatalf("unable to verify backup task completed: %v", err) - } + 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, err := net.SuspendNode(dave) - if err != nil { - t.Fatalf("unable to suspend Dave: %v", err) - } + 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. - if err = net.RestartNode(carol, func() error { - return net.RestoreDb(carol) - }); err != nil { - t.Fatalf("unable to restart node: %v", err) - } + 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. - carolChan, err = getChanInfo(carol) - if err != nil { - t.Fatalf("unable to get carol chan info: %v", err) - } - if carolChan.NumUpdates != carolStateNumPreCopy { - t.Fatalf("db copy failed: %v", carolChan.NumUpdates) - } + 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, err := net.CloseChannel(carol, chanPoint, true) - if err != nil { - t.Fatalf("unable to close channel: %v", err) - } - - // Query the mempool for the breaching closing transaction, this should - // be broadcast by Carol when she force closes the channel above. - txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Carol's force close tx in mempool: %v", - err) - } - if *txid != *closeTxID { - t.Fatalf("expected closeTx(%v) in mempool, instead found %v", - closeTxID, txid) - } + 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 := mineBlocks(t, net, 1, 1)[0] + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] - breachTXID, err := net.WaitForChannelClose(closeUpdates) - if err != nil { - t.Fatalf("error while waiting for channel close: %v", err) - } - assertTxInBlock(t, block, breachTXID) + 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, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - if err != nil { - t.Fatalf("unable to find Dave's justice tx in mempool: %v", - err) - } - time.Sleep(100 * time.Millisecond) + 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, err := net.Miner.Client.GetRawTransaction(justiceTXID) - if err != nil { - t.Fatalf("unable to query for justice tx: %v", err) - } + justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { - if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { - t.Fatalf("justice tx not spending commitment utxo "+ - "instead is: %v", txIn.PreviousOutPoint) - } + require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], + "justice tx not spending commitment utxo") } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - willyBalReq := &lnrpc.WalletBalanceRequest{} - willyBalResp, err := willy.WalletBalance(ctxt, willyBalReq) - if err != nil { - t.Fatalf("unable to get willy's balance: %v", err) - } - - if willyBalResp.ConfirmedBalance != 0 { - t.Fatalf("willy should have 0 balance before mining "+ - "justice transaction, instead has %d", - willyBalResp.ConfirmedBalance) - } + 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 = mineBlocks(t, net, 1, 1)[0] + block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. - if len(block.Transactions) != 2 { - t.Fatalf("transaction wasn't mined") - } + require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() - if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { - t.Fatalf("justice tx wasn't mined") - } + 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. - var predErr error - err = wait.Invariant(func() bool { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - willyBalReq := &lnrpc.WalletBalanceRequest{} - willyBalResp, err := willy.WalletBalance(ctxt, willyBalReq) - if err != nil { - t.Fatalf("unable to get willy's balance: %v", err) - } + err = wait.NoError(func() error { + willyBalResp := willy.RPC.WalletBalance() if willyBalResp.ConfirmedBalance != 0 { - predErr = fmt.Errorf("Expected Willy to have no funds "+ + return fmt.Errorf("Expected Willy to have no funds "+ "after justice transaction was mined, found %v", willyBalResp) - return false } - return true + return nil }, time.Second*5) - if err != nil { - t.Fatalf("%v", predErr) - } + 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. - err = restart() - if err != nil { - t.Fatalf("unable to restart dave: %v", err) - } - - err = wait.Predicate(func() bool { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.ChannelBalanceRequest{} - daveBalResp, err := dave.ChannelBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + require.NoError(ht, restart(), "unable to restart dave") + err = wait.NoError(func() error { + daveBalResp := dave.RPC.ChannelBalance() if daveBalResp.LocalBalance.Sat != 0 { - predErr = fmt.Errorf("Dave should end up with zero "+ + return fmt.Errorf("Dave should end up with zero "+ "channel balance, instead has %d", daveBalResp.LocalBalance.Sat) - return false } - return true + return nil }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) + 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) } - assertNumPendingChannels(t, dave, 0, 0) - - err = wait.Predicate(func() bool { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - daveBalReq := &lnrpc.WalletBalanceRequest{} - daveBalResp, err := dave.WalletBalance(ctxt, daveBalReq) - if err != nil { - t.Fatalf("unable to get dave's balance: %v", err) - } + // Check that Dave's wallet balance is increased. + err = wait.NoError(func() error { + daveBalResp := dave.RPC.WalletBalance() if daveBalResp.ConfirmedBalance <= davePreSweepBalance { - predErr = fmt.Errorf("Dave should have more than %d "+ + return fmt.Errorf("Dave should have more than %d "+ "after sweep, instead has %d", davePreSweepBalance, daveBalResp.ConfirmedBalance) - return false } - return true + return nil }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + require.NoError(ht, err, "timeout checking dave's wallet balance") // Dave should have no open channels. - assertNodeNumChannels(t, dave, 0) + ht.AssertNodeNumChannels(dave, 0) } diff --git a/lntest/itest/lnd_routing_test.go b/lntest/itest/lnd_routing_test.go index caf5320af..b94e3b488 100644 --- a/lntest/itest/lnd_routing_test.go +++ b/lntest/itest/lnd_routing_test.go @@ -1,20 +1,17 @@ package itest import ( - "bytes" - "context" "encoding/hex" "fmt" - "strings" "testing" "time" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntemp" + "github.com/lightningnetwork/lnd/lntemp/node" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -68,23 +65,18 @@ var singleHopSendToRouteCases = []singleHopSendToRouteCase{ // by feeding the route back into the various SendToRoute RPC methods. Here we // test all three SendToRoute endpoints, forcing each to perform both a regular // payment and an MPP payment. -func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) { +func testSingleHopSendToRoute(ht *lntemp.HarnessTest) { for _, test := range singleHopSendToRouteCases { test := test - t.t.Run(test.name, func(t1 *testing.T) { - ht := newHarnessTest(t1, t.lndHarness) - ht.RunTestCase(&testCase{ - name: test.name, - test: func(_ *lntest.NetworkHarness, tt *harnessTest) { - testSingleHopSendToRouteCase(net, tt, test) - }, - }) + ht.Run(test.name, func(t1 *testing.T) { + st := ht.Subtest(t1) + testSingleHopSendToRouteCase(st, test) }) } } -func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, +func testSingleHopSendToRouteCase(ht *lntemp.HarnessTest, test singleHopSendToRouteCase) { const chanAmt = btcutil.Amount(100000) @@ -92,96 +84,41 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, const numPayments = 5 const amountPaid = int64(numPayments * paymentAmtSat) - ctxb := context.Background() - var networkChans []*lnrpc.ChannelPoint - // Create Carol and Dave, then establish a channel between them. Carol // is the sole funder of the channel with 100k satoshis. The network // topology should look like: // Carol -> 100k -> Dave - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", nil) + dave := ht.NewNode("Dave", nil) - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) - - net.ConnectNodes(t.t, carol, dave) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + ht.ConnectNodes(carol, dave) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // Open a channel with 100k satoshis between Carol and Dave with Carol // being the sole funder of the channel. - chanPointCarol := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointCarol := ht.OpenChannel( + carol, dave, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointCarol) - - carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - carolFundPoint := wire.OutPoint{ - Hash: *carolChanTXID, - Index: chanPointCarol.OutputIndex, - } - - // Wait for all nodes to have seen all channels. - nodes := []*lntest.HarnessNode{carol, dave} - for _, chanPoint := range networkChans { - for _, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - point := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d): timeout waiting for "+ - "channel(%s) open: %v", node.Name(), - node.NodeID, point, err) - } - } - } + defer ht.CloseChannel(carol, chanPointCarol) // Create invoices for Dave, which expect a payment from Carol. - payReqs, rHashes, _, err := createPayReqs( + payReqs, rHashes, _ := ht.CreatePayReqs( dave, paymentAmtSat, numPayments, ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } // Reconstruct payment addresses. var payAddrs [][]byte for _, payReq := range payReqs { - ctx, _ := context.WithTimeout( - context.Background(), defaultTimeout, - ) - resp, err := dave.DecodePayReq( - ctx, - &lnrpc.PayReqString{PayReq: payReq}, - ) - if err != nil { - t.Fatalf("decode pay req: %v", err) - } + resp := dave.RPC.DecodePayReq(payReq) payAddrs = append(payAddrs, resp.PaymentAddr) } // Assert Carol and Dave are synced to the chain before proceeding, to // ensure the queried route will have a valid final CLTV once the HTLC // reaches Dave. - _, minerHeight, err := net.Miner.Client.GetBestBlock() - if err != nil { - t.Fatalf("unable to get best height: %v", err) - } - require.NoError(t.t, waitForNodeBlockHeight(carol, minerHeight)) - require.NoError(t.t, waitForNodeBlockHeight(dave, minerHeight)) + _, minerHeight := ht.Miner.GetBestBlock() + ht.WaitForNodeBlockHeight(carol, minerHeight) + ht.WaitForNodeBlockHeight(dave, minerHeight) // Query for routes to pay from Carol to Dave using the default CLTV // config. @@ -189,12 +126,7 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, PubKey: dave.PubKeyStr, Amt: paymentAmtSat, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - routes, err := carol.QueryRoutes(ctxt, routesReq) - if err != nil { - t.Fatalf("unable to get route from %s: %v", - carol.Name(), err) - } + routes := carol.RPC.QueryRoutes(routesReq) // There should only be one route to try, so take the first item. r := routes.Routes[0] @@ -222,27 +154,14 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, PaymentHash: rHash, Route: r, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - resp, err := carol.SendToRouteSync( - ctxt, sendReq, - ) - if err != nil { - t.Fatalf("unable to send to route for "+ - "%s: %v", carol.Name(), err) - } - if resp.PaymentError != "" { - t.Fatalf("received payment error from %s: %v", - carol.Name(), resp.PaymentError) - } + resp := carol.RPC.SendToRouteSync(sendReq) + require.Emptyf(ht, resp.PaymentError, + "received payment error from %s: %v", + carol.Name(), resp.PaymentError) } } sendToRouteStream := func() { - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - alicePayStream, err := carol.SendToRoute(ctxt) // nolint:staticcheck - if err != nil { - t.Fatalf("unable to create payment stream for "+ - "carol: %v", err) - } + alicePayStream := carol.RPC.SendToRoute() for i, rHash := range rHashes { setMPPFields(i) @@ -252,19 +171,13 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, Route: routes.Routes[0], } err := alicePayStream.Send(sendReq) + require.NoError(ht, err, "unable to send payment") - if err != nil { - t.Fatalf("unable to send payment: %v", err) - } - - resp, err := alicePayStream.Recv() - if err != nil { - t.Fatalf("unable to send payment: %v", err) - } - if resp.PaymentError != "" { - t.Fatalf("received payment error: %v", - resp.PaymentError) - } + resp, err := ht.ReceiveSendToRouteUpdate(alicePayStream) + require.NoError(ht, err, "unable to receive stream") + require.Emptyf(ht, resp.PaymentError, + "received payment error from %s: %v", + carol.Name(), resp.PaymentError) } } sendToRouteRouterRPC := func() { @@ -275,23 +188,14 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, PaymentHash: rHash, Route: r, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - resp, err := carol.RouterClient.SendToRouteV2( - ctxt, sendReq, - ) - if err != nil { - t.Fatalf("unable to send to route for "+ - "%s: %v", carol.Name(), err) - } - if resp.Failure != nil { - t.Fatalf("received payment error from %s: %v", - carol.Name(), resp.Failure) - } + resp := carol.RPC.SendToRouteV2(sendReq) + require.Nilf(ht, resp.Failure, "received payment "+ + "error from %s", carol.Name()) } } // Using Carol as the node as the source, send the payments - // synchronously via the the routerrpc's SendToRoute, or via the main RPC + // synchronously via the routerrpc's SendToRoute, or via the main RPC // server's SendToRoute streaming or sync calls. switch { case !test.routerrpc && test.streaming: @@ -301,118 +205,70 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, case test.routerrpc && !test.streaming: sendToRouteRouterRPC() default: - t.Fatalf("routerrpc does not support streaming send_to_route") + require.Fail(ht, "routerrpc does not support "+ + "streaming send_to_route") } // Verify that the payment's from Carol's PoV have the correct payment // hash and amount. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - paymentsResp, err := carol.ListPayments( - ctxt, &lnrpc.ListPaymentsRequest{}, - ) - if err != nil { - t.Fatalf("error when obtaining %s payments: %v", - carol.Name(), err) - } - if len(paymentsResp.Payments) != numPayments { - t.Fatalf("incorrect number of payments, got %v, want %v", - len(paymentsResp.Payments), numPayments) - } + payments := ht.AssertNumPayments(carol, numPayments) - for i, p := range paymentsResp.Payments { + for i, p := range payments { // Assert that the payment hashes for each payment match up. rHashHex := hex.EncodeToString(rHashes[i]) - if p.PaymentHash != rHashHex { - t.Fatalf("incorrect payment hash for payment %d, "+ - "want: %s got: %s", - i, rHashHex, p.PaymentHash) - } + require.Equalf(ht, rHashHex, p.PaymentHash, + "incorrect payment hash for payment %d", i) - // Assert that each payment has no invoice since the payment was - // completed using SendToRoute. - if p.PaymentRequest != "" { - t.Fatalf("incorrect payment request for payment: %d, "+ - "want: \"\", got: %s", - i, p.PaymentRequest) - } + // Assert that each payment has no invoice since the payment + // was completed using SendToRoute. + require.Emptyf(ht, p.PaymentRequest, + "incorrect payment request for payment: %d", i) // Assert the payment amount is correct. - if p.ValueSat != paymentAmtSat { - t.Fatalf("incorrect payment amt for payment %d, "+ - "want: %d, got: %d", - i, paymentAmtSat, p.ValueSat) - } + require.EqualValues(ht, paymentAmtSat, p.ValueSat, + "incorrect payment amt for payment %d, ", i) // Assert exactly one htlc was made. - if len(p.Htlcs) != 1 { - t.Fatalf("expected 1 htlc for payment %d, got: %d", - i, len(p.Htlcs)) - } + require.Lenf(ht, p.Htlcs, 1, + "expected 1 htlc for payment %d", i) // Assert the htlc's route is populated. htlc := p.Htlcs[0] - if htlc.Route == nil { - t.Fatalf("expected route for payment %d", i) - } + require.NotNilf(ht, htlc.Route, + "expected route for payment %d", i) // Assert the hop has exactly one hop. - if len(htlc.Route.Hops) != 1 { - t.Fatalf("expected 1 hop for payment %d, got: %d", - i, len(htlc.Route.Hops)) - } + require.Lenf(ht, htlc.Route.Hops, 1, + "expected 1 hop for payment %d", i) // If this is an MPP test, assert the MPP record's fields are // properly populated. Otherwise the hop should not have an MPP // record. hop := htlc.Route.Hops[0] - if hop.MppRecord == nil { - t.Fatalf("expected mpp record for mpp payment") - } + require.NotNilf(ht, hop.MppRecord, + "expected mpp record for mpp payment %d", i) - if hop.MppRecord.TotalAmtMsat != paymentAmtSat*1000 { - t.Fatalf("incorrect mpp total msat for payment %d "+ - "want: %d, got: %d", - i, paymentAmtSat*1000, - hop.MppRecord.TotalAmtMsat) - } + require.EqualValues(ht, paymentAmtSat*1000, + hop.MppRecord.TotalAmtMsat, + "incorrect mpp total msat for payment %d", i) expAddr := payAddrs[i] - if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr) { - t.Fatalf("incorrect mpp payment addr for payment %d "+ - "want: %x, got: %x", - i, expAddr, hop.MppRecord.PaymentAddr) - } + require.Equal(ht, expAddr, hop.MppRecord.PaymentAddr, + "incorrect mpp payment addr for payment %d ", i) } // Verify that the invoices's from Dave's PoV have the correct payment // hash and amount. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - invoicesResp, err := dave.ListInvoices( - ctxt, &lnrpc.ListInvoiceRequest{}, - ) - if err != nil { - t.Fatalf("error when obtaining %s payments: %v", - dave.Name(), err) - } - if len(invoicesResp.Invoices) != numPayments { - t.Fatalf("incorrect number of invoices, got %v, want %v", - len(invoicesResp.Invoices), numPayments) - } + invoices := ht.AssertNumInvoices(dave, numPayments) - for i, inv := range invoicesResp.Invoices { + for i, inv := range invoices { // Assert that the payment hashes match up. - if !bytes.Equal(inv.RHash, rHashes[i]) { - t.Fatalf("incorrect payment hash for invoice %d, "+ - "want: %x got: %x", - i, rHashes[i], inv.RHash) - } + require.Equal(ht, rHashes[i], inv.RHash, + "incorrect payment hash for invoice %d", i) // Assert that the amount paid to the invoice is correct. - if inv.AmtPaidSat != paymentAmtSat { - t.Fatalf("incorrect payment amt for invoice %d, "+ - "want: %d, got %d", - i, paymentAmtSat, inv.AmtPaidSat) - } + require.EqualValues(ht, paymentAmtSat, inv.AmtPaidSat, + "incorrect payment amt for invoice %d, ", i) } // At this point all the channels within our proto network should be @@ -420,12 +276,10 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, // 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->Dave, order is Dave and then Carol. - assertAmountPaid(t, "Carol(local) => Dave(remote)", dave, - carolFundPoint, int64(0), amountPaid) - assertAmountPaid(t, "Carol(local) => Dave(remote)", carol, - carolFundPoint, amountPaid, int64(0)) - - closeChannelAndAssert(t, net, carol, chanPointCarol, false) + ht.AssertAmountPaid("Carol(local) => Dave(remote)", dave, + chanPointCarol, int64(0), amountPaid) + ht.AssertAmountPaid("Carol(local) => Dave(remote)", carol, + chanPointCarol, amountPaid, int64(0)) } // testMultiHopSendToRoute tests that payments are properly processed @@ -435,15 +289,15 @@ func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest, // // We'll query the daemon for routes from Alice to Carol and then // send payments through the routes. -func testMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) { - t.t.Run("with cache", func(tt *testing.T) { - ht := newHarnessTest(tt, t.lndHarness) - runMultiHopSendToRoute(net, ht, true) +func testMultiHopSendToRoute(ht *lntemp.HarnessTest) { + ht.Run("with cache", func(tt *testing.T) { + st := ht.Subtest(tt) + runMultiHopSendToRoute(st, true) }) if *dbBackendFlag == "bbolt" { - t.t.Run("without cache", func(tt *testing.T) { - ht := newHarnessTest(tt, t.lndHarness) - runMultiHopSendToRoute(net, ht, false) + ht.Run("without cache", func(tt *testing.T) { + st := ht.Subtest(tt) + runMultiHopSendToRoute(st, false) }) } } @@ -455,104 +309,45 @@ func testMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) { // // We'll query the daemon for routes from Alice to Carol and then // send payments through the routes. -func runMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest, - useGraphCache bool) { - - ctxb := context.Background() - +func runMultiHopSendToRoute(ht *lntemp.HarnessTest, useGraphCache bool) { var opts []string if !useGraphCache { opts = append(opts, "--db.no-graph-cache") } - alice := net.NewNode(t.t, "Alice", opts) - defer shutdownAndAssert(net, t, alice) + alice, bob := ht.Alice, ht.Bob + ht.RestartNodeWithExtraArgs(alice, opts) - net.ConnectNodes(t.t, alice, net.Bob) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, alice) + ht.EnsureConnected(alice, bob) const chanAmt = btcutil.Amount(100000) - var networkChans []*lnrpc.ChannelPoint // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. - chanPointAlice := openChannelAndAssert( - t, net, alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointAlice := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointAlice) + defer ht.CloseChannel(alice, chanPointAlice) - aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - aliceFundPoint := wire.OutPoint{ - Hash: *aliceChanTXID, - Index: chanPointAlice.OutputIndex, - } - - // Create Carol and establish a channel from Bob. Bob is the sole funder - // of the channel with 100k satoshis. The network topology should look like: + // Create Carol and establish a channel from Bob. Bob is the sole + // funder of the channel with 100k satoshis. The network topology + // should look like: // Alice -> Bob -> Carol - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", nil) + ht.ConnectNodes(carol, bob) - net.ConnectNodes(t.t, carol, net.Bob) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Bob) - - chanPointBob := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointBob := ht.OpenChannel( + bob, carol, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointBob) - bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - bobFundPoint := wire.OutPoint{ - Hash: *bobChanTXID, - Index: chanPointBob.OutputIndex, - } + defer ht.CloseChannel(carol, chanPointBob) - // Wait for all nodes to have seen all channels. - nodes := []*lntest.HarnessNode{alice, net.Bob, carol} - nodeNames := []string{"Alice", "Bob", "Carol"} - for _, chanPoint := range networkChans { - for i, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - point := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d): timeout waiting for "+ - "channel(%s) open: %v", nodeNames[i], - node.NodeID, point, err) - } - } - } - - // Create 5 invoices for Carol, which expect a payment from Alice for 1k - // satoshis with a different preimage each time. + // Create 5 invoices for Carol, which expect a payment from Alice for + // 1k satoshis with a different preimage each time. const ( numPayments = 5 paymentAmt = 1000 ) - _, rHashes, invoices, err := createPayReqs( - carol, paymentAmt, numPayments, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } + _, rHashes, invoices := ht.CreatePayReqs(carol, paymentAmt, numPayments) // Construct a route from Alice to Carol for each of the invoices // created above. We set FinalCltvDelta to 40 since by default @@ -563,30 +358,15 @@ func runMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest, Amt: paymentAmt, FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - routes, err := alice.QueryRoutes(ctxt, routesReq) - if err != nil { - t.Fatalf("unable to get route: %v", err) - } - - // We'll wait for all parties to recognize the new channels within the - // network. - err = carol.WaitForNetworkChannelOpen(chanPointBob) - if err != nil { - t.Fatalf("bob didn't advertise his channel in time: %v", err) - } - - time.Sleep(time.Millisecond * 50) + routes := alice.RPC.QueryRoutes(routesReq) // Using Alice as the source, pay to the 5 invoices from Carol created // above. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - for i, rHash := range rHashes { // 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 := routes.Routes[0] route.Hops[len(route.Hops)-1].TlvPayload = true route.Hops[len(route.Hops)-1].MppRecord = &lnrpc.MPPRecord{ PaymentAddr: invoices[i].PaymentAddr, @@ -597,16 +377,10 @@ func runMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest, sendReq := &routerrpc.SendToRouteRequest{ PaymentHash: rHash, - Route: &route, - } - resp, err := alice.RouterClient.SendToRouteV2(ctxt, sendReq) - if err != nil { - t.Fatalf("unable to send payment: %v", err) - } - - if resp.Failure != nil { - t.Fatalf("received payment error: %v", resp.Failure) + Route: route, } + resp := alice.RPC.SendToRouteV2(sendReq) + require.Nil(ht, resp.Failure, "received payment error") } // When asserting the amount of satoshis moved, we'll factor in the @@ -615,75 +389,50 @@ func runMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest, 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 + // 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 Alice->Bob->Carol, order is Carol, Bob, // Alice. const amountPaid = int64(5000) - assertAmountPaid(t, "Bob(local) => Carol(remote)", carol, - bobFundPoint, int64(0), amountPaid) - assertAmountPaid(t, "Bob(local) => Carol(remote)", net.Bob, - bobFundPoint, amountPaid, int64(0)) - assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob, - aliceFundPoint, int64(0), amountPaid+(baseFee*numPayments)) - assertAmountPaid(t, "Alice(local) => Bob(remote)", alice, - aliceFundPoint, amountPaid+(baseFee*numPayments), int64(0)) - - closeChannelAndAssert(t, net, alice, chanPointAlice, false) - closeChannelAndAssert(t, net, carol, chanPointBob, false) + ht.AssertAmountPaid("Bob(local) => Carol(remote)", carol, + chanPointBob, int64(0), amountPaid) + ht.AssertAmountPaid("Bob(local) => Carol(remote)", bob, + chanPointBob, amountPaid, int64(0)) + ht.AssertAmountPaid("Alice(local) => Bob(remote)", bob, + chanPointAlice, int64(0), amountPaid+(baseFee*numPayments)) + ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice, + chanPointAlice, amountPaid+(baseFee*numPayments), int64(0)) } // testSendToRouteErrorPropagation tests propagation of errors that occur // while processing a multi-hop payment through an unknown route. -func testSendToRouteErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testSendToRouteErrorPropagation(ht *lntemp.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Open a channel with 100k satoshis between Alice and Bob with Alice // being the sole funder of the channel. - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + alice, bob := ht.Alice, ht.Bob + chanPointAlice := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: chanAmt}, ) - err := net.Alice.WaitForNetworkChannelOpen(chanPointAlice) - if err != nil { - t.Fatalf("alice didn't advertise her channel: %v", err) - } - // Create a new nodes (Carol and Charlie), load her with some funds, // then establish a connection between Carol and Charlie with a channel // that has identical capacity to the one created above.Then we will // get route via queryroutes call which will be fake route for Alice -> // Bob graph. // - // The network topology should now look like: Alice -> Bob; Carol -> Charlie. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + // The network topology should now look like: + // Alice -> Bob; Carol -> Charlie. + carol := ht.NewNode("Carol", nil) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) + charlie := ht.NewNode("Charlie", nil) + ht.FundCoins(btcutil.SatoshiPerBitcoin, charlie) - charlie := net.NewNode(t.t, "Charlie", nil) - defer shutdownAndAssert(net, t, charlie) - - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, charlie) - - net.ConnectNodes(t.t, carol, charlie) - - chanPointCarol := openChannelAndAssert( - t, net, carol, charlie, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - err = carol.WaitForNetworkChannelOpen(chanPointCarol) - if err != nil { - t.Fatalf("carol didn't advertise her channel: %v", err) - } + ht.ConnectNodes(carol, charlie) + ht.OpenChannel(carol, charlie, lntemp.OpenChannelParams{Amt: chanAmt}) // Query routes from Carol to Charlie which will be an invalid route // for Alice -> Bob. @@ -691,64 +440,44 @@ func testSendToRouteErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) PubKey: charlie.PubKeyStr, Amt: int64(1), } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - fakeRoute, err := carol.QueryRoutes(ctxt, fakeReq) - if err != nil { - t.Fatalf("unable get fake route: %v", err) - } + fakeRoute := carol.RPC.QueryRoutes(fakeReq) - // Create 1 invoices for Bob, which expect a payment from Alice for 1k - // satoshis + // Create 1 invoice for Bob, which expect a payment from Alice for 1k + // satoshis. const paymentAmt = 1000 invoice := &lnrpc.Invoice{ Memo: "testing", Value: paymentAmt, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - resp, err := net.Bob.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - + resp := bob.RPC.AddInvoice(invoice) rHash := resp.RHash - // Using Alice as the source, pay to the 5 invoices from Bob created above. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - alicePayStream, err := net.Alice.SendToRoute(ctxt) // nolint:staticcheck - if err != nil { - t.Fatalf("unable to create payment stream for alice: %v", err) - } + // Using Alice as the source, pay to the invoice from Bob. + alicePayStream := alice.RPC.SendToRoute() sendReq := &lnrpc.SendToRouteRequest{ PaymentHash: rHash, Route: fakeRoute.Routes[0], } - - if err := alicePayStream.Send(sendReq); err != nil { - t.Fatalf("unable to send payment: %v", err) - } + err := alicePayStream.Send(sendReq) + require.NoError(ht, err, "unable to send payment") // At this place we should get an rpc error with notification // that edge is not found on hop(0) - _, err = alicePayStream.Recv() - if err != nil && strings.Contains(err.Error(), "edge not found") { - } else if err != nil { - t.Fatalf("payment stream has been closed but fake route has consumed: %v", err) - } + event, err := ht.ReceiveSendToRouteUpdate(alicePayStream) + require.NoError(ht, err, "payment stream has been closed but fake "+ + "route has consumed") + require.Contains(ht, event.PaymentError, "UnknownNextPeer") - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - closeChannelAndAssert(t, net, carol, chanPointCarol, false) + ht.CloseChannel(alice, chanPointAlice) } // testPrivateChannels tests that a private channel can be used for // routing by the two endpoints of the channel, but is not known by // the rest of the nodes in the graph. -func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testPrivateChannels(ht *lntemp.HarnessTest) { const chanAmt = btcutil.Amount(100000) - var networkChans []*lnrpc.ChannelPoint // We create the following topology: // @@ -762,136 +491,40 @@ func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) { // where the 100k channel between Carol and Alice is private. // Open a channel with 200k satoshis between Alice and Bob. - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt * 2, - }, + alice, bob := ht.Alice, ht.Bob + chanPointAlice := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: chanAmt * 2}, ) - networkChans = append(networkChans, chanPointAlice) - - aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - aliceFundPoint := wire.OutPoint{ - Hash: *aliceChanTXID, - Index: chanPointAlice.OutputIndex, - } // Create Dave, and a channel to Alice of 100k. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) + dave := ht.NewNode("Dave", nil) + ht.ConnectNodes(dave, alice) + ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) - net.ConnectNodes(t.t, dave, net.Alice) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) - - chanPointDave := openChannelAndAssert( - t, net, dave, net.Alice, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointDave := ht.OpenChannel( + dave, alice, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointDave) - daveChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointDave) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - daveFundPoint := wire.OutPoint{ - Hash: *daveChanTXID, - Index: chanPointDave.OutputIndex, - } // Next, we'll create Carol and establish a channel from her to // Dave of 100k. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", nil) + ht.ConnectNodes(carol, dave) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - net.ConnectNodes(t.t, carol, dave) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) - - chanPointCarol := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointCarol := ht.OpenChannel( + carol, dave, lntemp.OpenChannelParams{Amt: chanAmt}, ) - networkChans = append(networkChans, chanPointCarol) - carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - carolFundPoint := wire.OutPoint{ - Hash: *carolChanTXID, - Index: chanPointCarol.OutputIndex, - } - - // Wait for all nodes to have seen all these channels, as they - // are all public. - nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave} - nodeNames := []string{"Alice", "Bob", "Carol", "Dave"} - for _, chanPoint := range networkChans { - for i, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - point := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d): timeout waiting for "+ - "channel(%s) open: %v", nodeNames[i], - node.NodeID, point, err) - } - } - } // Now create a _private_ channel directly between Carol and // Alice of 100k. - net.ConnectNodes(t.t, carol, net.Alice) - chanOpenUpdate := openChannelStream( - t, net, carol, net.Alice, - lntest.OpenChannelParams{ + ht.ConnectNodes(carol, alice) + chanPointPrivate := ht.OpenChannel( + carol, alice, lntemp.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) - if err != nil { - t.Fatalf("unable to open channel: %v", err) - } - - // One block is enough to make the channel ready for use, since the - // nodes have defaultNumConfs=1 set. - block := mineBlocks(t, net, 1, 1)[0] - - chanPointPrivate, err := net.WaitForChannelOpen(chanOpenUpdate) - if err != nil { - t.Fatalf("error while waiting for channel open: %v", err) - } - fundingTxID, err := lnrpc.GetChanPointFundingTxid(chanPointPrivate) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - assertTxInBlock(t, block, fundingTxID) - - // The channel should be listed in the peer information returned by - // both peers. - privateFundPoint := wire.OutPoint{ - Hash: *fundingTxID, - Index: chanPointPrivate.OutputIndex, - } - err = net.AssertChannelExists(carol, &privateFundPoint) - if err != nil { - t.Fatalf("unable to assert channel existence: %v", err) - } - err = net.AssertChannelExists(net.Alice, &privateFundPoint) - if err != nil { - t.Fatalf("unable to assert channel existence: %v", err) - } + defer ht.CloseChannel(carol, chanPointPrivate) // The channel should be available for payments between Carol and Alice. // We check this by sending payments from Carol to Bob, that @@ -902,20 +535,10 @@ func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) { // by only using one of the channels. const numPayments = 2 const paymentAmt = 70000 - payReqs, _, _, err := createPayReqs( - net.Bob, paymentAmt, numPayments, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - time.Sleep(time.Millisecond * 50) + payReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numPayments) // Let Carol pay the invoices. - err = completePaymentRequests(carol, carol.RouterClient, payReqs, true) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } + ht.CompletePaymentRequests(carol, payReqs) // When asserting the amount of satoshis moved, we'll factor in the // default base fee, as we didn't modify the fee structure when @@ -923,135 +546,63 @@ func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) { const baseFee = 1 // Bob should have received 140k satoshis from Alice. - assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob, - aliceFundPoint, int64(0), 2*paymentAmt) + ht.AssertAmountPaid("Alice(local) => Bob(remote)", bob, + chanPointAlice, int64(0), 2*paymentAmt) // Alice sent 140k to Bob. - assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice, - aliceFundPoint, 2*paymentAmt, int64(0)) + ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice, + chanPointAlice, 2*paymentAmt, int64(0)) // Alice received 70k + fee from Dave. - assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice, - daveFundPoint, int64(0), paymentAmt+baseFee) + ht.AssertAmountPaid("Dave(local) => Alice(remote)", alice, + chanPointDave, int64(0), paymentAmt+baseFee) // Dave sent 70k+fee to Alice. - assertAmountPaid(t, "Dave(local) => Alice(remote)", dave, - daveFundPoint, paymentAmt+baseFee, int64(0)) + ht.AssertAmountPaid("Dave(local) => Alice(remote)", dave, + chanPointDave, paymentAmt+baseFee, int64(0)) // Dave received 70k+fee of two hops from Carol. - assertAmountPaid(t, "Carol(local) => Dave(remote)", dave, - carolFundPoint, int64(0), paymentAmt+baseFee*2) + ht.AssertAmountPaid("Carol(local) => Dave(remote)", dave, + chanPointCarol, int64(0), paymentAmt+baseFee*2) // Carol sent 70k+fee of two hops to Dave. - assertAmountPaid(t, "Carol(local) => Dave(remote)", carol, - carolFundPoint, paymentAmt+baseFee*2, int64(0)) + ht.AssertAmountPaid("Carol(local) => Dave(remote)", carol, + chanPointCarol, paymentAmt+baseFee*2, int64(0)) // Alice received 70k+fee from Carol. - assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)", - net.Alice, privateFundPoint, int64(0), paymentAmt+baseFee) + ht.AssertAmountPaid("Carol(local) [private=>] Alice(remote)", + alice, chanPointPrivate, int64(0), paymentAmt+baseFee) // Carol sent 70k+fee to Alice. - assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)", - carol, privateFundPoint, paymentAmt+baseFee, int64(0)) + ht.AssertAmountPaid("Carol(local) [private=>] Alice(remote)", + carol, chanPointPrivate, paymentAmt+baseFee, int64(0)) // Alice should also be able to route payments using this channel, // so send two payments of 60k back to Carol. const paymentAmt60k = 60000 - payReqs, _, _, err = createPayReqs( - carol, paymentAmt60k, numPayments, - ) - if err != nil { - t.Fatalf("unable to create pay reqs: %v", err) - } - - time.Sleep(time.Millisecond * 50) + payReqs, _, _ = ht.CreatePayReqs(carol, paymentAmt60k, numPayments) // Let Bob pay the invoices. - err = completePaymentRequests( - net.Alice, net.Alice.RouterClient, payReqs, true, - ) - if err != nil { - t.Fatalf("unable to send payments: %v", err) - } + ht.CompletePaymentRequests(alice, payReqs) - // Finally, we make sure Dave and Bob does not know about the - // private channel between Carol and Alice. We first mine - // plenty of blocks, such that the channel would have been - // announced in case it was public. - mineBlocks(t, net, 10, 0) - - // We create a helper method to check how many edges each of the - // nodes know about. Carol and Alice should know about 4, while - // Bob and Dave should only know about 3, since one channel is - // private. - numChannels := func(node *lntest.HarnessNode, includeUnannounced bool) int { - req := &lnrpc.ChannelGraphRequest{ - IncludeUnannounced: includeUnannounced, - } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - chanGraph, err := node.DescribeGraph(ctxt, req) - if err != nil { - t.Fatalf("unable go describegraph: %v", err) - } - return len(chanGraph.Edges) - } - - var predErr error - err = wait.Predicate(func() bool { - aliceChans := numChannels(net.Alice, true) - if aliceChans != 4 { - predErr = fmt.Errorf("expected Alice to know 4 edges, "+ - "had %v", aliceChans) - return false - } - alicePubChans := numChannels(net.Alice, false) - if alicePubChans != 3 { - predErr = fmt.Errorf("expected Alice to know 3 public edges, "+ - "had %v", alicePubChans) - return false - } - bobChans := numChannels(net.Bob, true) - if bobChans != 3 { - predErr = fmt.Errorf("expected Bob to know 3 edges, "+ - "had %v", bobChans) - return false - } - carolChans := numChannels(carol, true) - if carolChans != 4 { - predErr = fmt.Errorf("expected Carol to know 4 edges, "+ - "had %v", carolChans) - return false - } - carolPubChans := numChannels(carol, false) - if carolPubChans != 3 { - predErr = fmt.Errorf("expected Carol to know 3 public edges, "+ - "had %v", carolPubChans) - return false - } - daveChans := numChannels(dave, true) - if daveChans != 3 { - predErr = fmt.Errorf("expected Dave to know 3 edges, "+ - "had %v", daveChans) - return false - } - return true - }, defaultTimeout) - if err != nil { - t.Fatalf("%v", predErr) - } + // Carol and Alice should know about 4, while Bob and Dave should only + // know about 3, since one channel is private. + ht.AssertNumEdges(alice, 4, true) + ht.AssertNumEdges(alice, 3, false) + ht.AssertNumEdges(bob, 3, true) + ht.AssertNumEdges(carol, 4, true) + ht.AssertNumEdges(carol, 3, false) + ht.AssertNumEdges(dave, 3, true) // Close all channels. - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - closeChannelAndAssert(t, net, dave, chanPointDave, false) - closeChannelAndAssert(t, net, carol, chanPointCarol, false) - closeChannelAndAssert(t, net, carol, chanPointPrivate, false) + ht.CloseChannel(alice, chanPointAlice) + ht.CloseChannel(dave, chanPointDave) + ht.CloseChannel(carol, chanPointCarol) } // testInvoiceRoutingHints tests that the routing hints for an invoice are // created properly. -func testInvoiceRoutingHints(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testInvoiceRoutingHints(ht *lntemp.HarnessTest) { const chanAmt = btcutil.Amount(100000) // Throughout this test, we'll be opening a channel between Alice and @@ -1060,169 +611,106 @@ func testInvoiceRoutingHints(net *lntest.NetworkHarness, t *harnessTest) { // First, we'll create a private channel between Alice and Bob. This // will be the only channel that will be considered as a routing hint // throughout this test. We'll include a push amount since we currently - // require channels to have enough remote balance to cover the invoice's - // payment. - chanPointBob := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ + // require channels to have enough remote balance to cover the + // invoice's payment. + alice, bob := ht.Alice, ht.Bob + chanPointBob := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, Private: true, }, ) - // Then, we'll create Carol's node and open a public channel between her - // and Alice. This channel will not be considered as a routing hint due - // to it being public. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + // Then, we'll create Carol's node and open a public channel between + // her and Alice. This channel will not be considered as a routing hint + // due to it being public. + carol := ht.NewNode("Carol", nil) - net.ConnectNodes(t.t, net.Alice, carol) - chanPointCarol := openChannelAndAssert( - t, net, net.Alice, carol, - lntest.OpenChannelParams{ + ht.ConnectNodes(alice, carol) + chanPointCarol := ht.OpenChannel( + alice, carol, lntemp.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }, ) // We'll also create a public channel between Bob and Carol to ensure - // that Bob gets selected as the only routing hint. We do this as - // we should only include routing hints for nodes that are publicly + // that Bob gets selected as the only routing hint. We do this as we + // should only include routing hints for nodes that are publicly // advertised, otherwise we'd end up leaking information about nodes // that wish to stay unadvertised. - net.ConnectNodes(t.t, net.Bob, carol) - chanPointBobCarol := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ + ht.ConnectNodes(bob, carol) + chanPointBobCarol := ht.OpenChannel( + bob, carol, lntemp.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, }, ) - // Then, we'll create Dave's node and open a private channel between him - // and Alice. We will not include a push amount in order to not consider - // this channel as a routing hint as it will not have enough remote - // balance for the invoice's amount. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) + // Then, we'll create Dave's node and open a private channel between + // him and Alice. We will not include a push amount in order to not + // consider this channel as a routing hint as it will not have enough + // remote balance for the invoice's amount. + dave := ht.NewNode("Dave", nil) - net.ConnectNodes(t.t, net.Alice, dave) - chanPointDave := openChannelAndAssert( - t, net, net.Alice, dave, - lntest.OpenChannelParams{ + ht.ConnectNodes(alice, dave) + chanPointDave := ht.OpenChannel( + alice, dave, lntemp.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) // Finally, we'll create Eve's node and open a private channel between - // her and Alice. This time though, we'll take Eve's node down after the - // channel has been created to avoid populating routing hints for + // her and Alice. This time though, we'll take Eve's node down after + // the channel has been created to avoid populating routing hints for // inactive channels. - eve := net.NewNode(t.t, "Eve", nil) - net.ConnectNodes(t.t, net.Alice, eve) - chanPointEve := openChannelAndAssert( - t, net, net.Alice, eve, - lntest.OpenChannelParams{ + eve := ht.NewNode("Eve", nil) + ht.ConnectNodes(alice, eve) + chanPointEve := ht.OpenChannel( + alice, eve, lntemp.OpenChannelParams{ Amt: chanAmt, PushAmt: chanAmt / 2, Private: true, }, ) - // Make sure all the channels have been opened. - chanNames := []string{ - "alice-bob", "alice-carol", "bob-carol", "alice-dave", - "alice-eve", - } - aliceChans := []*lnrpc.ChannelPoint{ - chanPointBob, chanPointCarol, chanPointBobCarol, chanPointDave, - chanPointEve, - } - for i, chanPoint := range aliceChans { - err := net.Alice.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("timed out waiting for channel open %s: %v", - chanNames[i], err) - } - } - - // Now that the channels are open, we'll take down Eve's node. - shutdownAndAssert(net, t, eve) + // Now that the channels are open, we'll disconnect the connection + // between Alice and Eve and then take down Eve's node. + ht.DisconnectNodes(alice, eve) + ht.Shutdown(eve) // We'll need the short channel ID of the channel between Alice and Bob // to make sure the routing hint is for this channel. - listReq := &lnrpc.ListChannelsRequest{} - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - listResp, err := net.Alice.ListChannels(ctxt, listReq) - require.NoError(t.t, err, "unable to retrieve alice's channels") - - var aliceBobChanID uint64 - for _, channel := range listResp.Channels { - if channel.RemotePubkey == net.Bob.PubKeyStr { - aliceBobChanID = channel.ChanId - } - } - - require.NotZero(t.t, aliceBobChanID, - "channel between alice and bob not found") + aliceBobChanID := ht.QueryChannelByChanPoint(alice, chanPointBob).ChanId checkInvoiceHints := func(invoice *lnrpc.Invoice) { // Due to the way the channels were set up above, the channel // between Alice and Bob should be the only channel used as a // routing hint. - var predErr error var decoded *lnrpc.PayReq - err := wait.Predicate(func() bool { - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - resp, err := net.Alice.AddInvoice(ctxt, invoice) - if err != nil { - predErr = fmt.Errorf( - "unable to add invoice: %w", err) - return false - } + err := wait.NoError(func() error { + resp := alice.RPC.AddInvoice(invoice) // We'll decode the invoice's payment request to // determine which channels were used as routing hints. - payReq := &lnrpc.PayReqString{ - PayReq: resp.PaymentRequest, - } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - decoded, err = net.Alice.DecodePayReq(ctxt, payReq) - if err != nil { - predErr = fmt.Errorf( - "unable to decode payment "+ - "request: %w", err) - - return false - } + decoded = alice.RPC.DecodePayReq(resp.PaymentRequest) if len(decoded.RouteHints) != 1 { - predErr = fmt.Errorf( - "expected one route hint, got %d", - len(decoded.RouteHints)) - - return false + return fmt.Errorf("expected one route hint, "+ + "got %d", len(decoded.RouteHints)) } - return true + return nil }, defaultTimeout) - if err != nil { - t.t.Fatalf(predErr.Error()) - } + require.NoError(ht, err, "timeout checking invoice hints") hops := decoded.RouteHints[0].HopHints - if len(hops) != 1 { - t.t.Fatalf("expected one hop in route hint, got %d", - len(hops)) - } - chanID := hops[0].ChanId + require.Len(ht, hops, 1, "expected one hop in route hint") - if chanID != aliceBobChanID { - t.t.Fatalf("expected channel ID %d, got %d", - aliceBobChanID, chanID) - } + chanID := hops[0].ChanId + require.Equal(ht, aliceBobChanID, chanID, "chanID mismatch") } // Create an invoice for Alice that will populate the routing hints. @@ -1244,24 +732,19 @@ func testInvoiceRoutingHints(net *lntest.NetworkHarness, t *harnessTest) { // Now that we've confirmed the routing hints were added correctly, we // can close all the channels and shut down all the nodes created. - closeChannelAndAssert(t, net, net.Alice, chanPointBob, false) - closeChannelAndAssert(t, net, net.Alice, chanPointCarol, false) - closeChannelAndAssert(t, net, net.Bob, chanPointBobCarol, false) - closeChannelAndAssert(t, net, net.Alice, chanPointDave, false) + ht.CloseChannel(alice, chanPointBob) + ht.CloseChannel(alice, chanPointCarol) + ht.CloseChannel(bob, chanPointBobCarol) + ht.CloseChannel(alice, chanPointDave) // The channel between Alice and Eve should be force closed since Eve // is offline. - closeChannelAndAssert(t, net, net.Alice, chanPointEve, true) - - // Cleanup by mining the force close and sweep transaction. - cleanupForceClose(t, net, net.Alice, chanPointEve) + ht.ForceCloseChannel(alice, chanPointEve) } // testMultiHopOverPrivateChannels tests that private channels can be used as // intermediate hops in a route for payments. -func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testMultiHopOverPrivateChannels(ht *lntemp.HarnessTest) { // We'll test that multi-hop payments over private channels work as // intended. To do so, we'll create the following topology: // private public private @@ -1270,115 +753,42 @@ func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) // First, we'll open a private channel between Alice and Bob with Alice // being the funder. - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ + alice, bob := ht.Alice, ht.Bob + chanPointAlice := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) - err := net.Alice.WaitForNetworkChannelOpen(chanPointAlice) - if err != nil { - t.Fatalf("alice didn't see the channel alice <-> bob before "+ - "timeout: %v", err) - } - err = net.Bob.WaitForNetworkChannelOpen(chanPointAlice) - if err != nil { - t.Fatalf("bob didn't see the channel alice <-> bob before "+ - "timeout: %v", err) - } - - // Retrieve Alice's funding outpoint. - aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - aliceFundPoint := wire.OutPoint{ - Hash: *aliceChanTXID, - Index: chanPointAlice.OutputIndex, - } - // Next, we'll create Carol's node and open a public channel between // her and Bob with Bob being the funder. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) - - net.ConnectNodes(t.t, net.Bob, carol) - chanPointBob := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ + carol := ht.NewNode("Carol", nil) + ht.ConnectNodes(bob, carol) + chanPointBob := ht.OpenChannel( + bob, carol, lntemp.OpenChannelParams{ Amt: chanAmt, }, ) - err = net.Bob.WaitForNetworkChannelOpen(chanPointBob) - if err != nil { - t.Fatalf("bob didn't see the channel bob <-> carol before "+ - "timeout: %v", err) - } - err = carol.WaitForNetworkChannelOpen(chanPointBob) - if err != nil { - t.Fatalf("carol didn't see the channel bob <-> carol before "+ - "timeout: %v", err) - } - err = net.Alice.WaitForNetworkChannelOpen(chanPointBob) - if err != nil { - t.Fatalf("alice didn't see the channel bob <-> carol before "+ - "timeout: %v", err) - } + // Alice should know the new channel from Bob. + ht.AssertTopologyChannelOpen(alice, chanPointBob) - // Retrieve Bob's funding outpoint. - bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - bobFundPoint := wire.OutPoint{ - Hash: *bobChanTXID, - Index: chanPointBob.OutputIndex, - } + // Next, we'll create Dave's node and open a private channel between + // him and Carol with Carol being the funder. + dave := ht.NewNode("Dave", nil) + ht.ConnectNodes(carol, dave) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - // Next, we'll create Dave's node and open a private channel between him - // and Carol with Carol being the funder. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) - - net.ConnectNodes(t.t, carol, dave) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) - - chanPointCarol := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ + chanPointCarol := ht.OpenChannel( + carol, dave, lntemp.OpenChannelParams{ Amt: chanAmt, Private: true, }, ) - err = carol.WaitForNetworkChannelOpen(chanPointCarol) - if err != nil { - t.Fatalf("carol didn't see the channel carol <-> dave before "+ - "timeout: %v", err) - } - err = dave.WaitForNetworkChannelOpen(chanPointCarol) - if err != nil { - t.Fatalf("dave didn't see the channel carol <-> dave before "+ - "timeout: %v", err) - } - err = dave.WaitForNetworkChannelOpen(chanPointBob) - if err != nil { - t.Fatalf("dave didn't see the channel bob <-> carol before "+ - "timeout: %v", err) - } - - // Retrieve Carol's funding point. - carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - carolFundPoint := wire.OutPoint{ - Hash: *carolChanTXID, - Index: chanPointCarol.OutputIndex, - } + // Dave should know the channel[Bob<->Carol] from Carol. + ht.AssertTopologyChannelOpen(dave, chanPointBob) // Now that all the channels are set up according to the topology from // above, we can proceed to test payments. We'll create an invoice for @@ -1393,21 +803,11 @@ func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) Value: paymentAmt, Private: true, } - - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - resp, err := dave.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to add invoice for dave: %v", err) - } + resp := dave.RPC.AddInvoice(invoice) // Let Alice pay the invoice. payReqs := []string{resp.PaymentRequest} - err = completePaymentRequests( - net.Alice, net.Alice.RouterClient, payReqs, true, - ) - if err != nil { - t.Fatalf("unable to send payments from alice to dave: %v", err) - } + ht.CompletePaymentRequests(alice, payReqs) // When asserting the amount of satoshis moved, we'll factor in the // default base fee, as we didn't modify the fee structure when opening @@ -1415,39 +815,34 @@ func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) const baseFee = 1 // Dave should have received 20k satoshis from Carol. - assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)", - dave, carolFundPoint, 0, paymentAmt) + ht.AssertAmountPaid("Carol(local) [private=>] Dave(remote)", + dave, chanPointCarol, 0, paymentAmt) // Carol should have sent 20k satoshis to Dave. - assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)", - carol, carolFundPoint, paymentAmt, 0) + ht.AssertAmountPaid("Carol(local) [private=>] Dave(remote)", + carol, chanPointCarol, paymentAmt, 0) // Carol should have received 20k satoshis + fee for one hop from Bob. - assertAmountPaid(t, "Bob(local) => Carol(remote)", - carol, bobFundPoint, 0, paymentAmt+baseFee) + ht.AssertAmountPaid("Bob(local) => Carol(remote)", + carol, chanPointBob, 0, paymentAmt+baseFee) // Bob should have sent 20k satoshis + fee for one hop to Carol. - assertAmountPaid(t, "Bob(local) => Carol(remote)", - net.Bob, bobFundPoint, paymentAmt+baseFee, 0) + ht.AssertAmountPaid("Bob(local) => Carol(remote)", + bob, chanPointBob, paymentAmt+baseFee, 0) // Bob should have received 20k satoshis + fee for two hops from Alice. - assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Bob, - aliceFundPoint, 0, paymentAmt+baseFee*2) + ht.AssertAmountPaid("Alice(local) [private=>] Bob(remote)", bob, + chanPointAlice, 0, paymentAmt+baseFee*2) // Alice should have sent 20k satoshis + fee for two hops to Bob. - assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Alice, - aliceFundPoint, paymentAmt+baseFee*2, 0) + ht.AssertAmountPaid("Alice(local) [private=>] Bob(remote)", alice, + chanPointAlice, paymentAmt+baseFee*2, 0) // At this point, the payment was successful. We can now close all the // channels and shutdown the nodes created throughout this test. - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - closeChannelAndAssert(t, net, net.Bob, chanPointBob, false) - closeChannelAndAssert(t, net, carol, chanPointCarol, false) -} - -// computeFee calculates the payment fee as specified in BOLT07. -func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { - return baseFee + amt*feeRate/1000000 + ht.CloseChannel(alice, chanPointAlice) + ht.CloseChannel(bob, chanPointBob) + ht.CloseChannel(carol, chanPointCarol) } // testQueryRoutes checks the response of queryroutes. @@ -1456,73 +851,41 @@ func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { // Alice --> Bob --> Carol --> Dave // // and query the daemon for routes from Alice to Dave. -func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testQueryRoutes(ht *lntemp.HarnessTest) { const chanAmt = btcutil.Amount(100000) - var networkChans []*lnrpc.ChannelPoint - // Open a channel between Alice and Bob. - chanPointAlice := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointAlice) + // Grab Alice and Bob from the standby nodes. + alice, bob := ht.Alice, ht.Bob - // Create Carol and establish a channel from Bob. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + // Create Carol and connect her to Bob. We also send her some coins for + // channel opening. + carol := ht.NewNode("Carol", nil) + ht.ConnectNodes(carol, bob) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - net.ConnectNodes(t.t, carol, net.Bob) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Bob) + // Create Dave and connect him to Carol. + dave := ht.NewNode("Dave", nil) + ht.ConnectNodes(dave, carol) - chanPointBob := openChannelAndAssert( - t, net, net.Bob, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointBob) - - // Create Dave and establish a channel from Carol. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) - - net.ConnectNodes(t.t, dave, carol) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) - - chanPointCarol := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - networkChans = append(networkChans, chanPointCarol) - - // Wait for all nodes to have seen all channels. - nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave} - nodeNames := []string{"Alice", "Bob", "Carol", "Dave"} - for _, chanPoint := range networkChans { - for i, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - point := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d): timeout waiting for "+ - "channel(%s) open: %v", nodeNames[i], - node.NodeID, point, err) - } - } + // We now proceed to open channels: + // Alice=>Bob, Bob=>Carol and Carol=>Dave. + p := lntemp.OpenChannelParams{Amt: chanAmt} + reqs := []*lntemp.OpenChannelRequest{ + {Local: alice, Remote: bob, Param: p}, + {Local: bob, Remote: carol, Param: p}, + {Local: carol, Remote: dave, Param: p}, } + resp := ht.OpenMultiChannelsAsync(reqs) + + // Extract channel points from the response. + chanPointAlice := resp[0] + chanPointBob := resp[1] + chanPointCarol := resp[2] + + // Before we continue, give Alice some time to catch up with the newly + // opened channels. + ht.AssertTopologyChannelOpen(alice, chanPointBob) + ht.AssertTopologyChannelOpen(alice, chanPointCarol) // Query for routes to pay from Alice to Dave. const paymentAmt = 1000 @@ -1530,11 +893,7 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) { PubKey: dave.PubKeyStr, Amt: paymentAmt, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - routesRes, err := net.Alice.QueryRoutes(ctxt, routesReq) - if err != nil { - t.Fatalf("unable to get route: %v", err) - } + routesRes := ht.QueryRoutesAndRetry(alice, routesReq) const mSat = 1000 feePerHopMSat := computeFee(1000, 1, paymentAmt*mSat) @@ -1542,100 +901,109 @@ func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) { for i, route := range routesRes.Routes { expectedTotalFeesMSat := lnwire.MilliSatoshi(len(route.Hops)-1) * feePerHopMSat - expectedTotalAmtMSat := (paymentAmt * mSat) + expectedTotalFeesMSat - if route.TotalFees != route.TotalFeesMsat/mSat { // nolint:staticcheck - t.Fatalf("route %v: total fees %v (msat) does not "+ + expectedTotalAmtMSat := + (paymentAmt * mSat) + expectedTotalFeesMSat + + if route.TotalFees != route.TotalFeesMsat/mSat { + ht.Fatalf("route %v: total fees %v (msat) does not "+ "round down to %v (sat)", - i, route.TotalFeesMsat, route.TotalFees) // nolint:staticcheck + i, route.TotalFeesMsat, route.TotalFees) } if route.TotalFeesMsat != int64(expectedTotalFeesMSat) { - t.Fatalf("route %v: total fees in msat expected %v got %v", - i, expectedTotalFeesMSat, route.TotalFeesMsat) + ht.Fatalf("route %v: total fees in msat expected %v "+ + "got %v", i, expectedTotalFeesMSat, + route.TotalFeesMsat) } - if route.TotalAmt != route.TotalAmtMsat/mSat { // nolint:staticcheck - t.Fatalf("route %v: total amt %v (msat) does not "+ + if route.TotalAmt != route.TotalAmtMsat/mSat { + ht.Fatalf("route %v: total amt %v (msat) does not "+ "round down to %v (sat)", - i, route.TotalAmtMsat, route.TotalAmt) // nolint:staticcheck + i, route.TotalAmtMsat, route.TotalAmt) } if route.TotalAmtMsat != int64(expectedTotalAmtMSat) { - t.Fatalf("route %v: total amt in msat expected %v got %v", - i, expectedTotalAmtMSat, route.TotalAmtMsat) + ht.Fatalf("route %v: total amt in msat expected %v "+ + "got %v", i, expectedTotalAmtMSat, + route.TotalAmtMsat) } - // For all hops except the last, we check that fee equals feePerHop - // and amount to forward deducts feePerHop on each hop. + // For all hops except the last, we check that fee equals + // feePerHop and amount to forward deducts feePerHop on each + // hop. expectedAmtToForwardMSat := expectedTotalAmtMSat for j, hop := range route.Hops[:len(route.Hops)-1] { expectedAmtToForwardMSat -= feePerHopMSat - if hop.Fee != hop.FeeMsat/mSat { // nolint:staticcheck - t.Fatalf("route %v hop %v: fee %v (msat) does not "+ - "round down to %v (sat)", - i, j, hop.FeeMsat, hop.Fee) // nolint:staticcheck + if hop.Fee != hop.FeeMsat/mSat { + ht.Fatalf("route %v hop %v: fee %v (msat) "+ + "does not round down to %v (sat)", + i, j, hop.FeeMsat, hop.Fee) } if hop.FeeMsat != int64(feePerHopMSat) { - t.Fatalf("route %v hop %v: fee in msat expected %v got %v", + ht.Fatalf("route %v hop %v: fee in msat "+ + "expected %v got %v", i, j, feePerHopMSat, hop.FeeMsat) } - if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck - t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+ - "round down to %v (sat)", - i, j, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck + if hop.AmtToForward != hop.AmtToForwardMsat/mSat { + ht.Fatalf("route %v hop %v: amt to forward %v"+ + "(msat) does not round down to %v(sat)", + i, j, hop.AmtToForwardMsat, + hop.AmtToForward) } - if hop.AmtToForwardMsat != int64(expectedAmtToForwardMSat) { - t.Fatalf("route %v hop %v: amt to forward in msat "+ - "expected %v got %v", - i, j, expectedAmtToForwardMSat, hop.AmtToForwardMsat) + if hop.AmtToForwardMsat != + int64(expectedAmtToForwardMSat) { + + ht.Fatalf("route %v hop %v: amt to forward "+ + "in msat expected %v got %v", + i, j, expectedAmtToForwardMSat, + hop.AmtToForwardMsat) } } - // Last hop should have zero fee and amount to forward should equal - // payment amount. + + // Last hop should have zero fee and amount to forward should + // equal payment amount. hop := route.Hops[len(route.Hops)-1] - if hop.Fee != 0 || hop.FeeMsat != 0 { // nolint:staticcheck - t.Fatalf("route %v hop %v: fee expected 0 got %v (sat) %v (msat)", - i, len(route.Hops)-1, hop.Fee, hop.FeeMsat) // nolint:staticcheck + if hop.Fee != 0 || hop.FeeMsat != 0 { + ht.Fatalf("route %v hop %v: fee expected 0 got %v "+ + "(sat) %v (msat)", i, len(route.Hops)-1, + hop.Fee, hop.FeeMsat) } - if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck - t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+ - "round down to %v (sat)", - i, len(route.Hops)-1, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck + if hop.AmtToForward != hop.AmtToForwardMsat/mSat { + ht.Fatalf("route %v hop %v: amt to forward %v (msat) "+ + "does not round down to %v (sat)", i, + len(route.Hops)-1, hop.AmtToForwardMsat, + hop.AmtToForward) } if hop.AmtToForwardMsat != paymentAmt*mSat { - t.Fatalf("route %v hop %v: amt to forward in msat "+ - "expected %v got %v", - i, len(route.Hops)-1, paymentAmt*mSat, hop.AmtToForwardMsat) + ht.Fatalf("route %v hop %v: amt to forward in msat "+ + "expected %v got %v", i, len(route.Hops)-1, + paymentAmt*mSat, hop.AmtToForwardMsat) } } // While we're here, we test updating mission control's config values // and assert that they are correctly updated and check that our mission // control import function updates appropriately. - testMissionControlCfg(t.t, net.Alice) - testMissionControlImport( - t.t, net.Alice, net.Bob.PubKey[:], carol.PubKey[:], - ) + testMissionControlCfg(ht.T, alice) + testMissionControlImport(ht, alice, bob.PubKey[:], carol.PubKey[:]) // We clean up the test case by closing channels that were created for // the duration of the tests. - closeChannelAndAssert(t, net, net.Alice, chanPointAlice, false) - closeChannelAndAssert(t, net, net.Bob, chanPointBob, false) - closeChannelAndAssert(t, net, carol, chanPointCarol, false) + ht.CloseChannel(alice, chanPointAlice) + ht.CloseChannel(bob, chanPointBob) + ht.CloseChannel(carol, chanPointCarol) } // testMissionControlCfg tests getting and setting of a node's mission control // config, resetting to the original values after testing so that no other // tests are affected. -func testMissionControlCfg(t *testing.T, node *lntest.HarnessNode) { - ctxb := context.Background() - startCfg, err := node.RouterClient.GetMissionControlConfig( - ctxb, &routerrpc.GetMissionControlConfigRequest{}, - ) - require.NoError(t, err) +func testMissionControlCfg(t *testing.T, hn *node.HarnessNode) { + t.Helper() + + startCfg := hn.RPC.GetMissionControlConfig() cfg := &routerrpc.MissionControlConfig{ HalfLifeSeconds: 8000, @@ -1645,40 +1013,25 @@ func testMissionControlCfg(t *testing.T, node *lntest.HarnessNode) { MinimumFailureRelaxInterval: 60, } - _, err = node.RouterClient.SetMissionControlConfig( - ctxb, &routerrpc.SetMissionControlConfigRequest{ - Config: cfg, - }, - ) - require.NoError(t, err) + hn.RPC.SetMissionControlConfig(cfg) - resp, err := node.RouterClient.GetMissionControlConfig( - ctxb, &routerrpc.GetMissionControlConfigRequest{}, - ) - require.NoError(t, err) + resp := hn.RPC.GetMissionControlConfig() require.True(t, proto.Equal(cfg, resp.Config)) - _, err = node.RouterClient.SetMissionControlConfig( - ctxb, &routerrpc.SetMissionControlConfigRequest{ - Config: startCfg.Config, - }, - ) - require.NoError(t, err) + hn.RPC.SetMissionControlConfig(startCfg.Config) + + resp = hn.RPC.GetMissionControlConfig() + require.True(t, proto.Equal(startCfg.Config, resp.Config)) } // testMissionControlImport tests import of mission control results from an // external source. -func testMissionControlImport(t *testing.T, node *lntest.HarnessNode, +func testMissionControlImport(ht *lntemp.HarnessTest, hn *node.HarnessNode, fromNode, toNode []byte) { - ctxb := context.Background() - // Reset mission control so that our query will return the default // probability for our first request. - _, err := node.RouterClient.ResetMissionControl( - ctxb, &routerrpc.ResetMissionControlRequest{}, - ) - require.NoError(t, err, "could not reset mission control") + hn.RPC.ResetMissionControl() // Get our baseline probability for a 10 msat hop between our target // nodes. @@ -1696,10 +1049,9 @@ func testMissionControlImport(t *testing.T, node *lntest.HarnessNode, // Assert that our history is not already equal to the value we want to // set. This should not happen because we have just cleared our state. - resp1, err := node.RouterClient.QueryProbability(ctxb, probReq) - require.NoError(t, err, "query probability failed") - require.Zero(t, resp1.History.FailTime) - require.Zero(t, resp1.History.FailAmtMsat) + resp1 := hn.RPC.QueryProbability(probReq) + require.Zero(ht, resp1.History.FailTime) + require.Zero(ht, resp1.History.FailAmtMsat) // Now, we import a single entry which tracks a failure of the amount // we want to query between our nodes. @@ -1712,27 +1064,21 @@ func testMissionControlImport(t *testing.T, node *lntest.HarnessNode, }, }, } + hn.RPC.XImportMissionControl(req) - _, err = node.RouterClient.XImportMissionControl(ctxb, req) - require.NoError(t, err, "could not import config") - - resp2, err := node.RouterClient.QueryProbability(ctxb, probReq) - require.NoError(t, err, "query probability failed") - require.Equal(t, importHistory.FailTime, resp2.History.FailTime) - require.Equal(t, importHistory.FailAmtMsat, resp2.History.FailAmtMsat) + resp2 := hn.RPC.QueryProbability(probReq) + require.Equal(ht, importHistory.FailTime, resp2.History.FailTime) + require.Equal(ht, importHistory.FailAmtMsat, resp2.History.FailAmtMsat) // Finally, check that we will fail if inconsistent sat/msat values are // set. importHistory.FailAmtSat = amount * 2 - _, err = node.RouterClient.XImportMissionControl(ctxb, req) - require.Error(t, err, "mismatched import amounts succeeded") + hn.RPC.XImportMissionControlAssertErr(req) } // testRouteFeeCutoff tests that we are able to prevent querying routes and // sending payments that incur a fee higher than the fee limit. -func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - +func testRouteFeeCutoff(ht *lntemp.HarnessTest) { // For this test, we'll create the following topology: // // --- Bob --- @@ -1747,75 +1093,45 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { const chanAmt = btcutil.Amount(100000) // Open a channel between Alice and Bob. - chanPointAliceBob := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + alice, bob := ht.Alice, ht.Bob + chanPointAliceBob := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: chanAmt}, ) // Create Carol's node and open a channel between her and Alice with // Alice being the funder. - carol := net.NewNode(t.t, "Carol", nil) - defer shutdownAndAssert(net, t, carol) + carol := ht.NewNode("Carol", nil) + ht.ConnectNodes(carol, alice) + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) - net.ConnectNodes(t.t, carol, net.Alice) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol) - - chanPointAliceCarol := openChannelAndAssert( - t, net, net.Alice, carol, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointAliceCarol := ht.OpenChannel( + alice, carol, lntemp.OpenChannelParams{Amt: chanAmt}, ) // Create Dave's node and open a channel between him and Bob with Bob // being the funder. - dave := net.NewNode(t.t, "Dave", nil) - defer shutdownAndAssert(net, t, dave) - - net.ConnectNodes(t.t, dave, net.Bob) - chanPointBobDave := openChannelAndAssert( - t, net, net.Bob, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + dave := ht.NewNode("Dave", nil) + ht.ConnectNodes(dave, bob) + chanPointBobDave := ht.OpenChannel( + bob, dave, lntemp.OpenChannelParams{Amt: chanAmt}, ) // Open a channel between Carol and Dave. - net.ConnectNodes(t.t, carol, dave) - chanPointCarolDave := openChannelAndAssert( - t, net, carol, dave, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + ht.ConnectNodes(carol, dave) + chanPointCarolDave := ht.OpenChannel( + carol, dave, lntemp.OpenChannelParams{Amt: chanAmt}, ) // Now that all the channels were set up, we'll wait for all the nodes // to have seen all the channels. - nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave} - nodeNames := []string{"alice", "bob", "carol", "dave"} + nodes := []*node.HarnessNode{alice, bob, carol, dave} networkChans := []*lnrpc.ChannelPoint{ chanPointAliceBob, chanPointAliceCarol, chanPointBobDave, chanPointCarolDave, } for _, chanPoint := range networkChans { - for i, node := range nodes { - txid, err := lnrpc.GetChanPointFundingTxid(chanPoint) - if err != nil { - t.Fatalf("unable to get txid: %v", err) - } - outpoint := wire.OutPoint{ - Hash: *txid, - Index: chanPoint.OutputIndex, - } - - err = node.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("%s(%d) timed out waiting for "+ - "channel(%s) open: %v", nodeNames[i], - node.NodeID, outpoint, err) - } + for _, node := range nodes { + ht.AssertTopologyChannelOpen(node, chanPoint) } } @@ -1846,56 +1162,35 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { ChanPoint: chanPointCarolDave, }, } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - if _, err := carol.UpdateChannelPolicy(ctxt, updateFeeReq); err != nil { - t.Fatalf("unable to update chan policy: %v", err) - } + carol.RPC.UpdateChannelPolicy(updateFeeReq) // Wait for Alice to receive the channel update from Carol. - assertChannelPolicyUpdate( - t.t, net.Alice, carol.PubKeyStr, - expectedPolicy, chanPointCarolDave, false, + ht.AssertChannelPolicyUpdate( + alice, carol, expectedPolicy, chanPointCarolDave, false, ) // We'll also need the channel IDs for Bob's channels in order to // confirm the route of the payments. - listReq := &lnrpc.ListChannelsRequest{} - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - listResp, err := net.Bob.ListChannels(ctxt, listReq) - if err != nil { - t.Fatalf("unable to retrieve bob's channels: %v", err) - } + channel := ht.QueryChannelByChanPoint(bob, chanPointAliceBob) + aliceBobChanID := channel.ChanId + require.NotZero(ht, aliceBobChanID, + "channel between alice and bob not found") - var aliceBobChanID, bobDaveChanID uint64 - for _, channel := range listResp.Channels { - switch channel.RemotePubkey { - case net.Alice.PubKeyStr: - aliceBobChanID = channel.ChanId - case dave.PubKeyStr: - bobDaveChanID = channel.ChanId - } - } + channel = ht.QueryChannelByChanPoint(bob, chanPointBobDave) + bobDaveChanID := channel.ChanId + require.NotZero(ht, bobDaveChanID, + "channel between bob and dave not found") - if aliceBobChanID == 0 { - t.Fatalf("channel between alice and bob not found") - } - if bobDaveChanID == 0 { - t.Fatalf("channel between bob and dave not found") - } hopChanIDs := []uint64{aliceBobChanID, bobDaveChanID} // checkRoute is a helper closure to ensure the route contains the // correct intermediate hops. checkRoute := func(route *lnrpc.Route) { - if len(route.Hops) != 2 { - t.Fatalf("expected two hops, got %d", len(route.Hops)) - } + require.Len(ht, route.Hops, 2, "expected two hops") for i, hop := range route.Hops { - if hop.ChanId != hopChanIDs[i] { - t.Fatalf("expected chan id %d, got %d", - hopChanIDs[i], hop.ChanId) - } + require.Equal(ht, hopChanIDs[i], hop.ChanId, + "hop chan id not match") } } @@ -1914,20 +1209,12 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { Amt: paymentAmt, FeeLimit: feeLimit, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - routesResp, err := net.Alice.QueryRoutes(ctxt, queryRoutesReq) - if err != nil { - t.Fatalf("unable to get routes: %v", err) - } + routesResp := alice.RPC.QueryRoutes(queryRoutesReq) checkRoute(routesResp.Routes[0]) invoice := &lnrpc.Invoice{Value: paymentAmt} - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - invoiceResp, err := dave.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to create invoice: %v", err) - } + invoiceResp := dave.RPC.AddInvoice(invoice) sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: invoiceResp.PaymentRequest, @@ -1941,7 +1228,7 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { sendReq.FeeLimitMsat = 1000 * paymentAmt * limit.Percent / 100 } - result := sendAndAssertSuccess(t, net.Alice, sendReq) + result := ht.SendPaymentAssertSettled(alice, sendReq) checkRoute(result.Htlcs[0].Route) } @@ -1966,10 +1253,24 @@ func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) { } testFeeCutoff(feeLimitFixed) + // TODO(yy): remove the sleep once the following bug is fixed. When the + // payment is reported as settled by Carol, 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) + // Once we're done, close the channels and shut down the nodes created // throughout this test. - closeChannelAndAssert(t, net, net.Alice, chanPointAliceBob, false) - closeChannelAndAssert(t, net, net.Alice, chanPointAliceCarol, false) - closeChannelAndAssert(t, net, net.Bob, chanPointBobDave, false) - closeChannelAndAssert(t, net, carol, chanPointCarolDave, false) + ht.CloseChannel(alice, chanPointAliceBob) + ht.CloseChannel(alice, chanPointAliceCarol) + ht.CloseChannel(bob, chanPointBobDave) + ht.CloseChannel(carol, chanPointCarolDave) +} + +// computeFee calculates the payment fee as specified in BOLT07. +func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { + return baseFee + amt*feeRate/1000000 } diff --git a/lntest/itest/lnd_rpc_middleware_interceptor_test.go b/lntest/itest/lnd_rpc_middleware_interceptor_test.go index 6aaa37642..f9c1d7b5c 100644 --- a/lntest/itest/lnd_rpc_middleware_interceptor_test.go +++ b/lntest/itest/lnd_rpc_middleware_interceptor_test.go @@ -8,7 +8,8 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lntest" + "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" @@ -18,133 +19,149 @@ import ( // testRPCMiddlewareInterceptor tests that the RPC middleware interceptor can // be used correctly and in a safe way. -func testRPCMiddlewareInterceptor(net *lntest.NetworkHarness, t *harnessTest) { +func testRPCMiddlewareInterceptor(ht *lntemp.HarnessTest) { // Let's first enable the middleware interceptor. - net.Alice.Cfg.ExtraArgs = append( - net.Alice.Cfg.ExtraArgs, "--rpcmiddleware.enable", - ) - err := net.RestartNode(net.Alice, nil) - require.NoError(t.t, err) + // + // 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. - net.EnsureConnected(t.t, net.Alice, net.Bob) - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice) - _ = openChannelAndAssert( - t, net, net.Alice, net.Bob, lntest.OpenChannelParams{ - Amt: 1_234_567, - }, - ) + 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 := net.Alice.ReadMacaroon( - net.Alice.ReadMacPath(), defaultTimeout, + readonlyMac, err := alice.ReadMacaroon( + alice.Cfg.ReadMacPath, defaultTimeout, ) - require.NoError(t.t, err) - adminMac, err := net.Alice.ReadMacaroon( - net.Alice.AdminMacPath(), defaultTimeout, + require.NoError(ht, err) + adminMac, err := alice.ReadMacaroon( + alice.Cfg.AdminMacPath, defaultTimeout, ) - require.NoError(t.t, err) + require.NoError(ht, err) customCaveatReadonlyMac, err := macaroons.SafeCopyMacaroon(readonlyMac) - require.NoError(t.t, err) + require.NoError(ht, err) addConstraint := macaroons.CustomConstraint( "itest-caveat", "itest-value", ) - require.NoError(t.t, addConstraint(customCaveatReadonlyMac)) + require.NoError(ht, addConstraint(customCaveatReadonlyMac)) customCaveatAdminMac, err := macaroons.SafeCopyMacaroon(adminMac) - require.NoError(t.t, err) - require.NoError(t.t, addConstraint(customCaveatAdminMac)) + 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. - t.t.Run("registration restrictions", func(tt *testing.T) { - middlewareRegistrationRestrictionTests(tt, net.Alice) + ht.Run("registration restrictions", func(tt *testing.T) { + middlewareRegistrationRestrictionTests(tt, alice) }) - t.t.Run("read-only intercept", func(tt *testing.T) { + + ht.Run("read-only intercept", func(tt *testing.T) { registration := registerMiddleware( - tt, net.Alice, &lnrpc.MiddlewareRegistration{ - MiddlewareName: "itest-interceptor", + tt, alice, &lnrpc.MiddlewareRegistration{ + MiddlewareName: "itest-interceptor-1", ReadOnlyMode: true, }, true, ) defer registration.cancel() middlewareInterceptionTest( - tt, net.Alice, net.Bob, registration, readonlyMac, + tt, alice, bob, registration, readonlyMac, customCaveatReadonlyMac, true, ) }) // We've manually disconnected Bob from Alice in the previous test, make // sure they're connected again. - net.EnsureConnected(t.t, net.Alice, net.Bob) - t.t.Run("encumbered macaroon intercept", func(tt *testing.T) { + // + // 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, net.Alice, &lnrpc.MiddlewareRegistration{ - MiddlewareName: "itest-interceptor", + tt, alice, &lnrpc.MiddlewareRegistration{ + MiddlewareName: "itest-interceptor-2", CustomMacaroonCaveatName: "itest-caveat", }, true, ) defer registration.cancel() middlewareInterceptionTest( - tt, net.Alice, net.Bob, registration, + tt, alice, bob, registration, customCaveatReadonlyMac, readonlyMac, false, ) }) // Next, run the response manipulation tests. - net.EnsureConnected(t.t, net.Alice, net.Bob) - t.t.Run("read-only not allowed to manipulate", func(tt *testing.T) { + // + // 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, net.Alice, &lnrpc.MiddlewareRegistration{ - MiddlewareName: "itest-interceptor", + tt, alice, &lnrpc.MiddlewareRegistration{ + MiddlewareName: "itest-interceptor-3", ReadOnlyMode: true, }, true, ) defer registration.cancel() middlewareRequestManipulationTest( - tt, net.Alice, registration, adminMac, true, + tt, alice, registration, adminMac, true, ) middlewareResponseManipulationTest( - tt, net.Alice, net.Bob, registration, readonlyMac, true, + tt, alice, bob, registration, readonlyMac, true, ) }) - net.EnsureConnected(t.t, net.Alice, net.Bob) - t.t.Run("encumbered macaroon manipulate", func(tt *testing.T) { + + // 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, net.Alice, &lnrpc.MiddlewareRegistration{ - MiddlewareName: "itest-interceptor", + tt, alice, &lnrpc.MiddlewareRegistration{ + MiddlewareName: "itest-interceptor-4", CustomMacaroonCaveatName: "itest-caveat", }, true, ) defer registration.cancel() middlewareRequestManipulationTest( - tt, net.Alice, registration, customCaveatAdminMac, - false, + tt, alice, registration, customCaveatAdminMac, false, ) middlewareResponseManipulationTest( - tt, net.Alice, net.Bob, registration, + tt, alice, bob, registration, customCaveatReadonlyMac, false, ) }) // And finally make sure mandatory middleware is always checked for any // RPC request. - t.t.Run("mandatory middleware", func(tt *testing.T) { - middlewareMandatoryTest(tt, net.Alice, net) + 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 *lntest.HarnessNode) { + node *node.HarnessNode) { testCases := []struct { registration *lnrpc.MiddlewareRegistration @@ -189,10 +206,12 @@ func middlewareRegistrationRestrictionTests(t *testing.T, // 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 *lntest.HarnessNode, - peer *lntest.HarnessNode, registration *middlewareHarness, - userMac *macaroon.Macaroon, disallowedMac *macaroon.Macaroon, - readOnly bool) { +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. @@ -253,10 +272,7 @@ func middlewareInterceptionTest(t *testing.T, node *lntest.HarnessNode, // Disconnect Bob to trigger a peer event without using Alice's RPC // interface itself. - _, err = peer.DisconnectPeer(ctxc, &lnrpc.DisconnectPeerRequest{ - PubKey: node.PubKeyStr, - }) - require.NoError(t, err) + peer.RPC.DisconnectPeer(node.PubKeyStr) peerEvent, err := resp2.Recv() require.NoError(t, err) require.Equal(t, lnrpc.PeerEvent_PEER_OFFLINE, peerEvent.GetType()) @@ -330,10 +346,12 @@ func middlewareInterceptionTest(t *testing.T, node *lntest.HarnessNode, // 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 *lntest.HarnessNode, - peer *lntest.HarnessNode, registration *middlewareHarness, +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() @@ -421,10 +439,7 @@ func middlewareResponseManipulationTest(t *testing.T, node *lntest.HarnessNode, // Disconnect Bob to trigger a peer event without using Alice's RPC // interface itself. - _, err = peer.DisconnectPeer(ctxc, &lnrpc.DisconnectPeerRequest{ - PubKey: node.PubKeyStr, - }) - require.NoError(t, err) + peer.RPC.DisconnectPeer(node.PubKeyStr) peerEvent, err := resp2.Recv() require.NoError(t, err) @@ -448,10 +463,12 @@ func middlewareResponseManipulationTest(t *testing.T, node *lntest.HarnessNode, // 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 *lntest.HarnessNode, +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() @@ -528,54 +545,44 @@ func middlewareRequestManipulationTest(t *testing.T, node *lntest.HarnessNode, // middlewareMandatoryTest tests that all RPC requests are blocked if there is // a mandatory middleware declared that's currently not registered. -func middlewareMandatoryTest(t *testing.T, node *lntest.HarnessNode, - net *lntest.NetworkHarness) { - +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.ExtraArgs = append( - node.Cfg.ExtraArgs, + node.Cfg.SkipUnlock = true + ht.RestartNodeWithExtraArgs(node, []string{ + "--noseedbackup", "--rpcmiddleware.enable", "--rpcmiddleware.addmandatory=itest-interceptor", - ) - err := net.RestartNodeNoUnlock(node, nil, false) - require.NoError(t, err) + }) // 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(true) - require.NoError(t, err) + conn, err := node.ConnectRPC() + require.NoError(ht, err) node.InitRPCClients(conn) - err = node.WaitUntilStateReached(lnrpc.WalletState_RPC_ACTIVE) - require.NoError(t, err) - node.LightningClient = lnrpc.NewLightningClient(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.ListChannels(ctxc, &lnrpc.ListChannelsRequest{}) - require.Error(t, err) - require.Contains( - t, err.Error(), "middleware 'itest-interceptor' is "+ - "currently not registered", - ) + _, 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, err := node.SubscribeInvoices(ctxc, &lnrpc.InvoiceSubscription{}) - require.NoError(t, err) + stream := node.RPC.SubscribeInvoices(&lnrpc.InvoiceSubscription{}) _, err = stream.Recv() - require.Error(t, err) - require.Contains( - t, err.Error(), "middleware 'itest-interceptor' is "+ - "currently not registered", - ) + 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( - t, node, &lnrpc.MiddlewareRegistration{ + ht.T, node, &lnrpc.MiddlewareRegistration{ MiddlewareName: "itest-interceptor", CustomMacaroonCaveatName: "itest-caveat", }, true, @@ -584,16 +591,13 @@ func middlewareMandatoryTest(t *testing.T, node *lntest.HarnessNode, // Both the unary and streaming requests should now be allowed. time.Sleep(500 * time.Millisecond) - _, err = node.ListChannels(ctxc, &lnrpc.ListChannelsRequest{}) - require.NoError(t, err) - _, err = node.SubscribeInvoices(ctxc, &lnrpc.InvoiceSubscription{}) - require.NoError(t, err) + 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. - err = net.ShutdownNode(node) - require.NoError(t, err) + // 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 @@ -648,35 +652,62 @@ type middlewareHarness struct { // registerMiddleware creates a new middleware harness and sends the initial // register message to the RPC server. -func registerMiddleware(t *testing.T, node *lntest.HarnessNode, +func registerMiddleware(t *testing.T, node *node.HarnessNode, registration *lnrpc.MiddlewareRegistration, waitForRegister bool) *middlewareHarness { - ctxc, cancel := context.WithCancel(context.Background()) + t.Helper() - middlewareStream, err := node.RegisterRPCMiddleware(ctxc) - require.NoError(t, err) + middlewareStream, cancel := node.RPC.RegisterRPCMiddleware() - err = middlewareStream.Send(&lnrpc.RPCMiddlewareResponse{ - MiddlewareMessage: &lnrpc.RPCMiddlewareResponse_Register{ + errChan := make(chan error) + go func() { + msg := &lnrpc.RPCMiddlewareResponse_Register{ Register: registration, - }, - }) - require.NoError(t, err) + } + err := middlewareStream.Send(&lnrpc.RPCMiddlewareResponse{ + MiddlewareMessage: msg, + }) - if waitForRegister { - // Wait for the registration complete message. - regCompleteMsg, err := middlewareStream.Recv() - require.NoError(t, err) - require.True(t, regCompleteMsg.GetRegComplete()) + errChan <- err + }() + + select { + case <-time.After(defaultTimeout): + require.Fail(t, "registerMiddleware send timeout") + case err := <-errChan: + require.NoError(t, err, "registerMiddleware send failed") } - return &middlewareHarness{ + 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 diff --git a/lntest/itest/lnd_single_hop_invoice_test.go b/lntest/itest/lnd_single_hop_invoice_test.go index 74f377458..ce75c42d0 100644 --- a/lntest/itest/lnd_single_hop_invoice_test.go +++ b/lntest/itest/lnd_single_hop_invoice_test.go @@ -2,35 +2,34 @@ package itest import ( "bytes" - "context" "encoding/hex" - "time" "github.com/btcsuite/btcd/btcutil" - "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" - "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntemp" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/stretchr/testify/require" ) -func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { - ctxb := context.Background() - - // Open a channel with 100k satoshis between Alice and Bob with Alice being - // the sole funder of the channel. +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) - chanPoint := openChannelAndAssert( - t, net, net.Alice, net.Bob, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + 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. @@ -41,61 +40,20 @@ func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { RPreimage: preimage, Value: paymentAmt, } - invoiceResp, err := net.Bob.AddInvoice(ctxb, invoice) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - - // Wait for Alice to recognize and advertise the new channel generated - // above. - err = net.Alice.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("alice didn't advertise channel before "+ - "timeout: %v", err) - } - err = net.Bob.WaitForNetworkChannelOpen(chanPoint) - if err != nil { - t.Fatalf("bob didn't advertise channel before "+ - "timeout: %v", err) - } + invoiceResp := bob.RPC.AddInvoice(invoice) // With the invoice for Bob added, send a payment towards Alice paying // to the above generated invoice. - resp := sendAndAssertSuccess( - t, net.Alice, &routerrpc.SendPaymentRequest{ - PaymentRequest: invoiceResp.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - }, - ) - if hex.EncodeToString(preimage) != resp.PaymentPreimage { - t.Fatalf("preimage mismatch: expected %v, got %v", preimage, - resp.PaymentPreimage) - } + ht.CompletePaymentRequests(alice, []string{invoiceResp.PaymentRequest}) // Bob's invoice should now be found and marked as settled. - payHash := &lnrpc.PaymentHash{ - RHash: invoiceResp.RHash, - } - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - dbInvoice, err := net.Bob.LookupInvoice(ctxt, payHash) - if err != nil { - t.Fatalf("unable to lookup invoice: %v", err) - } - if !dbInvoice.Settled { // nolint:staticcheck - t.Fatalf("bob's invoice should be marked as settled: %v", - spew.Sdump(dbInvoice)) - } + 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. - err = wait.NoError( - assertAmountSent(paymentAmt, net.Alice, net.Bob), - 3*time.Second, - ) - if err != nil { - t.Fatalf(err.Error()) - } + assertAmountPaid(paymentAmt) // Create another invoice for Bob, this time leaving off the preimage // to one will be randomly generated. We'll test the proper @@ -104,92 +62,58 @@ func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { Memo: "test3", Value: paymentAmt, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - invoiceResp, err = net.Bob.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } + invoiceResp = bob.RPC.AddInvoice(invoice) // Next send another payment, but this time using a zpay32 encoded // invoice rather than manually specifying the payment details. - sendAndAssertSuccess( - t, net.Alice, &routerrpc.SendPaymentRequest{ - PaymentRequest: invoiceResp.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, - }, - ) + ht.CompletePaymentRequests(alice, []string{invoiceResp.PaymentRequest}) // The second payment should also have succeeded, with the balances // being update accordingly. - err = wait.NoError( - assertAmountSent(2*paymentAmt, net.Alice, net.Bob), - 3*time.Second, - ) - if err != nil { - t.Fatalf(err.Error()) - } + assertAmountPaid(paymentAmt * 2) // Next send a keysend payment. keySendPreimage := lntypes.Preimage{3, 4, 5, 11} keySendHash := keySendPreimage.Hash() - sendAndAssertSuccess( - t, net.Alice, &routerrpc.SendPaymentRequest{ - Dest: net.Bob.PubKey[:], - Amt: paymentAmt, - FinalCltvDelta: 40, - PaymentHash: keySendHash[:], - DestCustomRecords: map[uint64][]byte{ - record.KeySendType: keySendPreimage[:], - }, - TimeoutSeconds: 60, - FeeLimitMsat: noFeeLimitMsat, + 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. - err = wait.NoError( - assertAmountSent(3*paymentAmt, net.Alice, net.Bob), - 3*time.Second, - ) - if err != nil { - t.Fatalf(err.Error()) - } + 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. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - keysendInvoice, err := net.Bob.LookupInvoice( - ctxt, &lnrpc.PaymentHash{ - RHash: keySendHash[:], - }, - ) - require.NoError(t.t, err) - require.Equal(t.t, 1, len(keysendInvoice.Htlcs)) + keysendInvoice := bob.RPC.LookupInvoice(keySendHash[:]) + require.Len(ht, keysendInvoice.Htlcs, 1) htlc := keysendInvoice.Htlcs[0] - require.Equal(t.t, uint64(0), htlc.MppTotalAmtMsat) - require.Nil(t.t, htlc.Amp) + 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(net.Bob.PubKey[:]) - hints := []*lnrpc.RouteHint{ - { - HopHints: []*lnrpc.HopHint{ - { - NodeId: bobPubKey, - ChanId: hintChannel.ToUint64(), - FeeBaseMsat: 1, - FeeProportionalMillionths: 1000000, - CltvExpiryDelta: 20, - }, - }, - }, + 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", @@ -197,43 +121,21 @@ func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { RouteHints: hints, } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - invoiceResp, err = net.Bob.AddInvoice(ctxt, invoice) - if err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - payreq, err := net.Bob.DecodePayReq(ctxt, &lnrpc.PayReqString{PayReq: invoiceResp.PaymentRequest}) - if err != nil { - t.Fatalf("failed to decode payment request %v", err) - } - if len(payreq.RouteHints) != 1 { - t.Fatalf("expected one routing hint") - } + 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] - if len(routingHint.HopHints) != 1 { - t.Fatalf("expected one hop hint") - } - hopHint := routingHint.HopHints[0] - if hopHint.FeeProportionalMillionths != 1000000 { - t.Fatalf("wrong FeeProportionalMillionths %v", - hopHint.FeeProportionalMillionths) - } - if hopHint.NodeId != bobPubKey { - t.Fatalf("wrong NodeId %v", - hopHint.NodeId) - } - if hopHint.ChanId != hintChannel.ToUint64() { - t.Fatalf("wrong ChanId %v", - hopHint.ChanId) - } - if hopHint.FeeBaseMsat != 1 { - t.Fatalf("wrong FeeBaseMsat %v", - hopHint.FeeBaseMsat) - } - if hopHint.CltvExpiryDelta != 20 { - t.Fatalf("wrong CltvExpiryDelta %v", - hopHint.CltvExpiryDelta) - } + require.Len(ht, routingHint.HopHints, 1, "expected one hop hint") - closeChannelAndAssert(t, net, net.Alice, chanPoint, false) + 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) } diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index fe56fcf0d..5d26440ac 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -4,50 +4,10 @@ package itest var allTestCases = []*testCase{ - { - name: "open channel reorg test", - test: testOpenChannelAfterReorg, - }, - { - name: "single hop invoice", - test: testSingleHopInvoice, - }, - { - name: "multi-hop payments", - test: testMultiHopPayments, - }, - { - name: "single-hop send to route", - test: testSingleHopSendToRoute, - }, - { - name: "multi-hop send to route", - test: testMultiHopSendToRoute, - }, - { - name: "send to route error propagation", - test: testSendToRouteErrorPropagation, - }, - { - name: "private channels", - test: testPrivateChannels, - }, - { - name: "invoice routing hints", - test: testInvoiceRoutingHints, - }, - { - name: "multi-hop payments over private channels", - test: testMultiHopOverPrivateChannels, - }, { name: "multiple channel creation and update subscription", test: testBasicChannelCreationAndUpdates, }, - { - name: "multi-hop htlc error propagation", - test: testHtlcErrorPropagation, - }, { name: "derive shared key", test: testDeriveSharedKey, @@ -84,76 +44,18 @@ var allTestCases = []*testCase{ name: "switch offline delivery outgoing offline", test: testSwitchOfflineDeliveryOutgoingOffline, }, - { - // TODO(roasbeef): test always needs to be last as Bob's state - // is borked since we trick him into attempting to cheat Alice? - name: "revoked uncooperative close retribution", - test: testRevokedCloseRetribution, - }, - { - name: "revoked uncooperative close retribution zero value remote output", - test: testRevokedCloseRetributionZeroValueRemoteOutput, - }, - { - name: "revoked uncooperative close retribution remote hodl", - test: testRevokedCloseRetributionRemoteHodl, - }, - { - name: "revoked uncooperative close retribution altruist watchtower", - test: testRevokedCloseRetributionAltruistWatchtower, - }, - { - name: "query routes", - test: testQueryRoutes, - }, - { - name: "route fee cutoff", - test: testRouteFeeCutoff, - }, - { - name: "hold invoice sender persistence", - test: testHoldInvoicePersistence, - }, - { - name: "hold invoice force close", - test: testHoldInvoiceForceClose, - }, { name: "cpfp", test: testCPFP, }, - { - name: "anchors reserved value", - test: testAnchorReservedValue, - }, - { - name: "macaroon authentication", - test: testMacaroonAuthentication, - }, - { - name: "bake macaroon", - test: testBakeMacaroon, - }, - { - name: "delete macaroon id", - test: testDeleteMacaroonID, - }, { name: "psbt channel funding", test: testPsbtChanFunding, }, - { - name: "psbt channel funding external", - test: testPsbtChanFundingExternal, - }, { name: "sign psbt", test: testSignPsbt, }, - { - name: "psbt channel funding single step", - test: testPsbtChanFundingSingleStep, - }, { name: "sendtoroute multi path payment", test: testSendToRouteMultiPath, @@ -178,10 +80,6 @@ var allTestCases = []*testCase{ name: "send multi path payment", test: testSendMultiPathPayment, }, - { - name: "REST API", - test: testRestAPI, - }, { name: "forward interceptor", test: testForwardInterceptorBasic, @@ -190,18 +88,6 @@ var allTestCases = []*testCase{ name: "forward interceptor dedup htlcs", test: testForwardInterceptorDedupHtlc, }, - { - name: "wumbo channels", - test: testWumboChannels, - }, - { - name: "maximum channel size", - test: testMaxChannelSize, - }, - { - name: "stateless init", - test: testStatelessInit, - }, { name: "wallet import account", test: testWalletImportAccount, @@ -210,38 +96,14 @@ var allTestCases = []*testCase{ name: "wallet import pubkey", test: testWalletImportPubKey, }, - { - name: "etcd_failover", - test: testEtcdFailover, - }, - { - name: "max htlc pathfind", - test: testMaxHtlcPathfind, - }, - { - name: "rpc middleware interceptor", - test: testRPCMiddlewareInterceptor, - }, - { - name: "wipe forwarding packages", - test: testWipeForwardingPackages, - }, { name: "remote signer", test: testRemoteSigner, }, - { - name: "3rd party anchor spend", - test: testAnchorThirdPartySpend, - }, { name: "taproot", test: testTaproot, }, - { - name: "resolution handoff", - test: testResHandoff, - }, { name: "zero conf channel open", test: testZeroConfChannelOpen, diff --git a/lntest/itest/lnd_wipe_fwdpkgs_test.go b/lntest/itest/lnd_wipe_fwdpkgs_test.go index 878211ed8..0f74d4fab 100644 --- a/lntest/itest/lnd_wipe_fwdpkgs_test.go +++ b/lntest/itest/lnd_wipe_fwdpkgs_test.go @@ -1,14 +1,11 @@ package itest import ( - "context" - "testing" "time" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/routerrpc" - "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntemp" "github.com/stretchr/testify/require" ) @@ -22,91 +19,9 @@ type pendingChan *lnrpc.PendingChannelsResponse_PendingChannel // - 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(net *lntest.NetworkHarness, - t *harnessTest) { - - // Setup the test and get the channel points. - pointAB, pointBC, carol, cleanUp := setupFwdPkgTest(net, t) - defer cleanUp() - - // Firstly, Bob force closes the channel. - _, _, err := net.CloseChannel(net.Bob, pointAB, true) - require.NoError(t.t, err, "unable to force close channel") - - // Now that the channel has been force closed, it should show up in - // bob's PendingChannels RPC under the waiting close section. - pendingChan := assertWaitingCloseChannel(t.t, net.Bob) - - // 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(t.t, pendingChan.NumForwardingPackages) - - // Mine 1 block to get the closing transaction confirmed. - _, err = net.Miner.Client.Generate(1) - require.NoError(t.t, err, "unable to mine blocks") - - // Now that the closing transaction is confirmed, the above waiting - // close channel should now become pending force closed channel. - pendingChan = assertPendingForceClosedChannel(t.t, net.Bob) - - // Check the forwarding packages are deleted. - require.Zero(t.t, pendingChan.NumForwardingPackages) - - // For Alice, the forwarding packages should have been wiped too. - pendingChanAlice := assertPendingForceClosedChannel(t.t, net.Alice) - require.Zero(t.t, pendingChanAlice.NumForwardingPackages) - - // Secondly, Bob coop closes the channel. - _, _, err = net.CloseChannel(net.Bob, pointBC, false) - require.NoError(t.t, err, "unable to coop close channel") - - // Now that the channel has been coop closed, it should show up in - // bob's PendingChannels RPC under the waiting close section. - pendingChan = assertWaitingCloseChannel(t.t, net.Bob) - - // 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(t.t, pendingChan.NumForwardingPackages) - - // Since it's a coop close, Carol should see the waiting close channel - // too. - pendingChanCarol := assertWaitingCloseChannel(t.t, carol) - require.NotZero(t.t, pendingChanCarol.NumForwardingPackages) - - // Mine 1 block to get the closing transaction confirmed. - _, err = net.Miner.Client.Generate(1) - require.NoError(t.t, err, "unable to mine blocks") - - // Now that the closing transaction is confirmed, the above waiting - // close channel should now become pending closed channel. Note that - // the name PendingForceClosingChannels is a bit confusing, what it - // really contains is channels whose closing tx has been broadcast. - pendingChan = assertPendingForceClosedChannel(t.t, net.Bob) - - // Check the forwarding packages are deleted. - require.Zero(t.t, pendingChan.NumForwardingPackages) - - // Mine a block to confirm sweep transactions such that they - // don't remain in the mempool for any subsequent tests. - _, err = net.Miner.Client.Generate(1) - require.NoError(t.t, err, "unable to mine blocks") -} - -// setupFwdPkgTest prepares the wipe forwarding packages tests. It creates a -// network topology that has a channel direction: Alice -> Bob -> Carol, sends -// several payments from Alice to Carol, and returns the two channel points(one -// for Alice and Bob, the other for Bob and Carol), the node Carol, and a -// cleanup function to be used when the test finishes. -func setupFwdPkgTest(net *lntest.NetworkHarness, - t *harnessTest) (*lnrpc.ChannelPoint, *lnrpc.ChannelPoint, - *lntest.HarnessNode, func()) { - - ctxb := context.Background() - +// - 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 @@ -114,114 +29,96 @@ func setupFwdPkgTest(net *lntest.NetworkHarness, numInvoices = 3 ) - // Grab Alice and Bob from harness net. - alice, bob := net.Alice, net.Bob + // 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 := net.NewNode(t.t, "Carol", nil) + carol := ht.NewNode("Carol", nil) // Connect Bob to Carol. - net.ConnectNodes(t.t, bob, carol) + ht.ConnectNodes(bob, carol) // Open a channel between Alice and Bob. - chanPointAB := openChannelAndAssert( - t, net, alice, bob, lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointAB := ht.OpenChannel( + alice, bob, lntemp.OpenChannelParams{Amt: chanAmt}, ) // Open a channel between Bob and Carol. - chanPointBC := openChannelAndAssert( - t, net, bob, carol, lntest.OpenChannelParams{ - Amt: chanAmt, - }, + chanPointBC := ht.OpenChannel( + bob, carol, lntemp.OpenChannelParams{Amt: chanAmt}, ) - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() + // 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} - invoiceResp, err := carol.AddInvoice(ctxt, invoice) - require.NoError(t.t, err, "unable to add invoice") + resp := carol.RPC.AddInvoice(invoice) // Alice sends a payment to Carol through Bob. - sendAndAssertSuccess( - t, net.Alice, &routerrpc.SendPaymentRequest{ - PaymentRequest: invoiceResp.PaymentRequest, - TimeoutSeconds: 60, - FeeLimitSat: noFeeLimitMsat, - }, - ) + ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest}) } - return chanPointAB, chanPointBC, carol, func() { - shutdownAndAssert(net, t, alice) - shutdownAndAssert(net, t, bob) - shutdownAndAssert(net, t, carol) - } -} - -// assertWaitingCloseChannel checks there is a single channel that is waiting -// for close and returns the channel found. -func assertWaitingCloseChannel(t *testing.T, - node *lntest.HarnessNode) pendingChan { - - ctxb := context.Background() - - var channel pendingChan - require.Eventually(t, func() bool { - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - - req := &lnrpc.PendingChannelsRequest{} - resp, err := node.PendingChannels(ctxt, req) - - // We require the RPC call to be succeeded and won't retry upon - // an error. - require.NoError(t, err, "unable to query for pending channels") - - if err := checkNumWaitingCloseChannels(resp, 1); err != nil { - return false - } - - channel = resp.WaitingCloseChannels[0].Channel - return true - }, defaultTimeout, 200*time.Millisecond) - - return channel -} - -// assertForceClosedChannel checks there is a single channel that is pending -// force closed and returns the channel found. -func assertPendingForceClosedChannel(t *testing.T, - node *lntest.HarnessNode) pendingChan { - - ctxb := context.Background() - - var channel pendingChan - require.Eventually(t, func() bool { - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() - - req := &lnrpc.PendingChannelsRequest{} - resp, err := node.PendingChannels(ctxt, req) - - // We require the RPC call to be succeeded and won't retry upon - // an error. - require.NoError(t, err, "unable to query for pending channels") - - if err := checkNumForceClosedChannels(resp, 1); err != nil { - return false - } - - channel = resp.PendingForceClosingChannels[0].Channel - return true - }, defaultTimeout, 200*time.Millisecond) - - return channel + // 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) } diff --git a/lntest/itest/lnd_wumbo_channels_test.go b/lntest/itest/lnd_wumbo_channels_test.go index ddfd6d677..5a5ba6627 100644 --- a/lntest/itest/lnd_wumbo_channels_test.go +++ b/lntest/itest/lnd_wumbo_channels_test.go @@ -1,70 +1,52 @@ package itest import ( - "strings" - "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/funding" - "github.com/lightningnetwork/lnd/lntest" + "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(net *lntest.NetworkHarness, t *harnessTest) { +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 := net.NewNode( - t.t, "wumbo", []string{"--protocol.wumbo-channels"}, - ) - defer shutdownAndAssert(net, t, wumboNode) - miniNode := net.NewNode(t.t, "mini", nil) - defer shutdownAndAssert(net, t, miniNode) + 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. - net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, wumboNode) + 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. - net.EnsureConnected(t.t, wumboNode, miniNode) + ht.EnsureConnected(wumboNode, miniNode) chanAmt := funding.MaxBtcFundingAmount + 1 - _, err := net.OpenChannel( - wumboNode, miniNode, lntest.OpenChannelParams{ - Amt: chanAmt, - }, - ) - if err == nil { - t.Fatalf("expected wumbo channel funding to fail") - } - // The test should indicate a failure due to the channel being too // large. - if !strings.Contains(err.Error(), "exceeds maximum chan size") { - t.Fatalf("channel should be rejected due to size, instead "+ - "error was: %v", err) - } + 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 := net.NewNode( - t.t, "wumbo2", []string{"--protocol.wumbo-channels"}, + wumboNode2 := ht.NewNode( + "wumbo2", []string{"--protocol.wumbo-channels"}, ) - defer shutdownAndAssert(net, t, wumboNode2) // Creating a wumbo channel between these two nodes should succeed. - net.EnsureConnected(t.t, wumboNode, wumboNode2) - chanPoint := openChannelAndAssert( - t, net, wumboNode, wumboNode2, - lntest.OpenChannelParams{ - Amt: chanAmt, - }, + ht.EnsureConnected(wumboNode, wumboNode2) + chanPoint := ht.OpenChannel( + wumboNode, wumboNode2, lntemp.OpenChannelParams{Amt: chanAmt}, ) - closeChannelAndAssert(t, net, wumboNode, chanPoint, false) + ht.CloseChannel(wumboNode, chanPoint) } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 49c6410be..6b1400fc6 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -798,6 +798,9 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg return } + walletLog.Debugf("Registered funding intent for "+ + "PendingChanID: %x", req.PendingChanID) + localFundingAmt = fundingIntent.LocalFundingAmt() remoteFundingAmt = fundingIntent.RemoteFundingAmt() } @@ -891,6 +894,10 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // completed, or canceled. req.resp <- reservation req.err <- nil + + walletLog.Debugf("Successfully handled funding reservation with "+ + "pendingChanID: %x, reservationID: %v", + reservation.pendingChanID, reservation.reservationID) } // enforceReservedValue enforces that the wallet, upon a new channel being diff --git a/routing/bandwidth.go b/routing/bandwidth.go index 8abf025ab..19c608701 100644 --- a/routing/bandwidth.go +++ b/routing/bandwidth.go @@ -77,6 +77,7 @@ func (b *bandwidthManager) getBandwidth(cid lnwire.ShortChannelID, if err != nil { // If the link isn't online, then we'll report that it has // zero bandwidth. + log.Warnf("ShortChannelID=%v: link not found: %v", cid, err) return 0 } @@ -84,12 +85,15 @@ func (b *bandwidthManager) getBandwidth(cid lnwire.ShortChannelID, // to forward any HTLCs, then we'll treat it as if it isn't online in // the first place. if !link.EligibleToForward() { + log.Warnf("ShortChannelID=%v: not eligible to forward", cid) return 0 } // If our link isn't currently in a state where it can add another // outgoing htlc, treat the link as unusable. if err := link.MayAddOutgoingHtlc(amount); err != nil { + log.Warnf("ShortChannelID=%v: cannot add outgoing htlc: %v", + cid, err) return 0 }