diff --git a/itest/list_on_test.go b/itest/list_on_test.go index c8fa5674d..b4f586fa2 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -590,4 +590,8 @@ var allTestCases = []*lntest.TestCase{ Name: "sweep htlcs", TestFunc: testSweepHTLCs, }, + { + Name: "sweep commit output and anchor", + TestFunc: testSweepCommitOutputAndAnchor, + }, } diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index 3747b9b82..87184bc3d 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -2,6 +2,7 @@ package itest import ( "fmt" + "math" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" @@ -800,6 +801,580 @@ func testSweepHTLCs(ht *lntest.HarnessTest) { ht.MineBlocksAndAssertNumTxes(1, 2) } +// testSweepCommitOutputAndAnchor checks when a channel is force closed without +// any time-sensitive HTLCs, the anchor output is swept without any CPFP +// attempts. In addition, the to_local output should be swept using the +// specified deadline and budget. +// +// Setup: +// 1. Fund Alice with 1 UTXOs - she only needs one for the funding process, +// and no wallet utxos are needed for her sweepings. +// 2. Fund Bob with no UTXOs - his sweeping txns don't need wallet utxos as he +// doesn't need to sweep any time-sensitive outputs. +// 3. Alice opens a channel with Bob, and successfully sends him an HTLC. +// 4. Alice force closes the channel. +// +// Test: +// 1. Alice's anchor sweeping is not attempted, instead, it should be swept +// together with her to_local output using the no deadline path. +// 2. Bob would also sweep his anchor and to_local outputs in a single +// sweeping tx using the no deadline path. +// 3. Both Alice and Bob's RBF attempts are using the fee rates calculated +// from the deadline and budget. +// 4. Wallet UTXOs requirements are met - neither Alice nor Bob needs wallet +// utxos to finish their sweeps. +func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) { + // Setup testing params for Alice. + // + // deadline is the expected deadline when sweeping the anchor and + // to_local output. We will use a customized deadline to test the + // config. + deadline := uint32(1000) + + // The actual deadline used by the fee function will be one block off + // from the deadline configured as we require one block to be mined to + // trigger the sweep. + deadlineA, deadlineB := deadline-1, deadline-1 + + // startFeeRate is returned by the fee estimator in sat/kw. This + // will be used as the starting fee rate for the linear fee func used + // by Alice. Since there are no time-sensitive HTLCs, Alice's sweeper + // should start with the above default deadline, which will result in + // the min relay fee rate being used since it's >= MaxBlockTarget. + startFeeRate := chainfee.FeePerKwFloor + + // Set up the fee estimator to return the testing fee rate when the + // conf target is the deadline. + ht.SetFeeEstimateWithConf(startFeeRate, deadlineA) + + // toLocalCSV is the CSV delay for Alice's to_local output. We use a + // small value to save us from mining blocks. + // + // NOTE: once the force close tx is confirmed, we expect anchor + // sweeping starts. Then two more block later the commit output + // sweeping starts. + // + // NOTE: The CSV value is chosen to be 3 instead of 2, to reduce the + // possibility of flakes as there is a race between the two goroutines: + // G1 - Alice's sweeper receives the commit output. + // G2 - Alice's sweeper receives the new block mined. + // G1 is triggered by the same block being received by Alice's + // contractcourt, deciding the commit output is mature and offering it + // to her sweeper. Normally, we'd expect G2 to be finished before G1 + // because it's the same block processed by both contractcourt and + // sweeper. However, if G2 is delayed (maybe the sweeper is slow in + // finishing its previous round), G1 may finish before G2. This will + // cause the sweeper to add the commit output to its pending inputs, + // and once G2 fires, it will then start sweeping this output, + // resulting a valid sweep tx being created using her commit and anchor + // outputs. + // + // TODO(yy): fix the above issue by making sure subsystems share the + // same view on current block height. + toLocalCSV := 3 + + // htlcAmt is the amount of the HTLC in sats, this should be Alice's + // to_remote amount that goes to Bob. + htlcAmt := int64(100_000) + + // fundAmt is the funding amount. + fundAmt := btcutil.Amount(1_000_000) + + // We now set up testing params for Bob. + // + // bobBalance is the push amount when Alice opens the channel with Bob. + // We will use zero here so Bob's balance equals to the htlc amount by + // the time Alice force closes. + bobBalance := btcutil.Amount(0) + + // We now set up the force close scenario. Alice will open a channel + // with Bob, send an HTLC, Bob settles it, and then Alice force closes + // the channel without any pending HTLCs. + // + // Prepare node params. + cfg := []string{ + "--protocol.anchors", + fmt.Sprintf("--sweeper.nodeadlineconftarget=%v", deadline), + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), + } + openChannelParams := lntest.OpenChannelParams{ + Amt: fundAmt, + PushAmt: bobBalance, + } + + // Create a two hop network: Alice -> Bob. + chanPoints, nodes := createSimpleNetwork(ht, cfg, 2, openChannelParams) + + // Unwrap the results. + chanPoint := chanPoints[0] + alice, bob := nodes[0], nodes[1] + + invoice := &lnrpc.Invoice{ + Memo: "bob", + Value: htlcAmt, + CltvExpiry: finalCltvDelta, + } + resp := bob.RPC.AddInvoice(invoice) + + // Send a payment with a specified finalCTLVDelta, and assert it's + // succeeded. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: resp.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + ht.SendPaymentAssertSettled(alice, req) + + // Assert Alice's to_remote (Bob's to_local) output is the htlc amount. + ht.AssertChannelLocalBalance(bob, chanPoint, htlcAmt) + bobToLocal := htlcAmt + + // Get Alice's channel to calculate Alice's to_local output amount. + aliceChan := ht.GetChannelByChanPoint(alice, chanPoint) + expectedToLocal := int64(fundAmt) - aliceChan.CommitFee - htlcAmt - + 330*2 + + // Assert Alice's to_local output is correct. + aliceToLocal := aliceChan.LocalBalance + require.EqualValues(ht, expectedToLocal, aliceToLocal) + + // Alice force closes the channel. + ht.CloseChannelAssertPending(alice, chanPoint, true) + + // Now that the channel has been force closed, it should show up in the + // PendingChannels RPC under the waiting close section. + ht.AssertChannelWaitingClose(alice, chanPoint) + + // We should see neither Alice or Bob has any pending sweeps as there + // are no time-sensitive HTLCs. + ht.AssertNumPendingSweeps(alice, 0) + ht.AssertNumPendingSweeps(bob, 0) + + // Mine a block to confirm Alice's force closing tx. Once it's + // confirmed, we should see both Alice and Bob's anchors being offered + // to their sweepers. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Alice should have one pending sweep, + // - anchor sweeping from her local commitment. + ht.AssertNumPendingSweeps(alice, 1) + + // Bob should have two pending sweeps, + // - anchor sweeping from the remote anchor on Alice's commit tx. + // - commit sweeping from the to_remote on Alice's commit tx. + ht.AssertNumPendingSweeps(bob, 2) + + // Mine one more empty block should trigger Bob's sweeping. Since we + // use a CSV of 3, this means Alice's to_local output is one block away + // from being mature. + ht.MineEmptyBlocks(1) + + // We expect to see one sweeping tx in the mempool: + // - Alice's anchor sweeping tx must have been failed due to the fee + // rate chosen in this test - the anchor sweep tx has no output. + // - Bob's sweeping tx, which sweeps both his anchor and commit outputs. + bobSweepTx := ht.Miner.GetNumTxsFromMempool(1)[0] + + // We expect two pending sweeps for Bob - anchor and commit outputs. + pendingSweepBob := ht.AssertNumPendingSweeps(bob, 2)[0] + + // The sweeper may be one block behind contractcourt, so we double + // check the actual deadline. + // + // TODO(yy): assert they are equal once blocks are synced via + // `blockbeat`. + _, currentHeight := ht.Miner.GetBestBlock() + actualDeadline := int32(pendingSweepBob.DeadlineHeight) - currentHeight + if actualDeadline != int32(deadlineB) { + ht.Logf("!!! Found unsynced block between sweeper and "+ + "contractcourt, expected deadline=%v, got=%v", + deadlineB, actualDeadline) + + deadlineB = uint32(actualDeadline) + } + + // Alice should still have one pending sweep - the anchor output. + ht.AssertNumPendingSweeps(alice, 1) + + // We now check Bob's sweeping tx. + // + // Bob's sweeping tx should have 2 inputs, one from his commit output, + // the other from his anchor output. + require.Len(ht, bobSweepTx.TxIn, 2) + + // Because Bob is sweeping without deadline pressure, the starting fee + // rate should be the min relay fee rate. + bobStartFeeRate := ht.CalculateTxFeeRate(bobSweepTx) + require.InEpsilonf(ht, uint64(chainfee.FeePerKwFloor), + uint64(bobStartFeeRate), 0.01, "want %v, got %v", + chainfee.FeePerKwFloor, bobStartFeeRate) + + // With Bob's starting fee rate being validated, we now calculate his + // ending fee rate and fee rate delta. + // + // Bob sweeps two inputs - anchor and commit, so the starting budget + // should come from the sum of these two. + bobValue := btcutil.Amount(bobToLocal + 330) + bobBudget := bobValue.MulF64(contractcourt.DefaultBudgetRatio) + + // Calculate the ending fee rate and fee rate delta used in his fee + // function. + bobTxWeight := uint64(ht.CalculateTxWeight(bobSweepTx)) + bobEndingFeeRate := chainfee.NewSatPerKWeight(bobBudget, bobTxWeight) + bobFeeRateDelta := (bobEndingFeeRate - bobStartFeeRate) / + chainfee.SatPerKWeight(deadlineB) + + // Mine an empty block, which should trigger Alice's contractcourt to + // offer her commit output to the sweeper. + ht.MineEmptyBlocks(1) + + // Alice should have both anchor and commit as the pending sweep + // requests. + aliceSweeps := ht.AssertNumPendingSweeps(alice, 2) + aliceAnchor, aliceCommit := aliceSweeps[0], aliceSweeps[1] + if aliceAnchor.AmountSat > aliceCommit.AmountSat { + aliceAnchor, aliceCommit = aliceCommit, aliceAnchor + } + + // The sweeper may be one block behind contractcourt, so we double + // check the actual deadline. + // + // TODO(yy): assert they are equal once blocks are synced via + // `blockbeat`. + _, currentHeight = ht.Miner.GetBestBlock() + actualDeadline = int32(aliceCommit.DeadlineHeight) - currentHeight + if actualDeadline != int32(deadlineA) { + ht.Logf("!!! Found unsynced block between Alice's sweeper and "+ + "contractcourt, expected deadline=%v, got=%v", + deadlineA, actualDeadline) + + deadlineA = uint32(actualDeadline) + } + + // We now wait for 30 seconds to overcome the flake - there's a block + // race between contractcourt and sweeper, causing the sweep to be + // broadcast earlier. + // + // TODO(yy): remove this once `blockbeat` is in place. + aliceStartPosition := 0 + var aliceFirstSweepTx *wire.MsgTx + err := wait.NoError(func() error { + mem := ht.Miner.GetRawMempool() + if len(mem) != 2 { + return fmt.Errorf("want 2, got %v in mempool: %v", + len(mem), mem) + } + + // If there are two txns, it means Alice's sweep tx has been + // created and published. + aliceStartPosition = 1 + + txns := ht.Miner.GetNumTxsFromMempool(2) + aliceFirstSweepTx = txns[0] + + // Reassign if the second tx is larger. + if txns[1].TxOut[0].Value > aliceFirstSweepTx.TxOut[0].Value { + aliceFirstSweepTx = txns[1] + } + + return nil + }, wait.DefaultTimeout) + ht.Logf("Checking mempool got: %v", err) + + // Mine an empty block, which should trigger Alice's sweeper to publish + // her commit sweep along with her anchor output. + ht.MineEmptyBlocks(1) + + // If Alice has already published her initial sweep tx, the above mined + // block would trigger an RBF. We now need to assert the mempool has + // removed the replaced tx. + if aliceFirstSweepTx != nil { + ht.Miner.AssertTxNotInMempool(aliceFirstSweepTx.TxHash()) + } + + // We also remember the positions of fee functions used by Alice and + // Bob. They will be used to calculate the expected fee rates later. + // + // Alice's sweeping tx has just been created, so she is at the starting + // position. For Bob, due to the above mined blocks, his fee function + // is now at position 2. + alicePosition, bobPosition := uint32(aliceStartPosition), uint32(2) + + // We should see two txns in the mempool: + // - Alice's sweeping tx, which sweeps her commit output at the + // starting fee rate - Alice's anchor output won't be swept with her + // commit output together because they have different deadlines. + // - Bob's previous sweeping tx, which sweeps both his anchor and + // commit outputs, at the starting fee rate. + txns := ht.Miner.GetNumTxsFromMempool(2) + + // Assume the first tx is Alice's sweeping tx, if the second tx has a + // larger output value, then that's Alice's as her to_local value is + // much gearter. + aliceSweepTx := txns[0] + bobSweepTx = txns[1] + + // Swap them if bobSweepTx is smaller. + if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { + aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx + } + + // We now check Alice's sweeping tx. + // + // Alice's sweeping tx should have a shape of 1-in-1-out since it's not + // used for CPFP, so it shouldn't take any wallet utxos. + require.Len(ht, aliceSweepTx.TxIn, 1) + require.Len(ht, aliceSweepTx.TxOut, 1) + + // We now check Alice's sweeping tx to see if it's already published. + // + // TODO(yy): remove this check once we have better block control. + aliceSweeps = ht.AssertNumPendingSweeps(alice, 2) + aliceCommit = aliceSweeps[0] + if aliceCommit.AmountSat < aliceSweeps[1].AmountSat { + aliceCommit = aliceSweeps[1] + } + if aliceCommit.BroadcastAttempts > 1 { + ht.Logf("!!! Alice's commit sweep has already been broadcast, "+ + "broadcast_attempts=%v", aliceCommit.BroadcastAttempts) + alicePosition = aliceCommit.BroadcastAttempts + } + + // Alice's sweeping tx should use the min relay fee rate as there's no + // deadline pressure. + aliceStartingFeeRate := chainfee.FeePerKwFloor + + // With Alice's starting fee rate being validated, we now calculate her + // ending fee rate and fee rate delta. + // + // Alice sweeps two inputs - anchor and commit, so the starting budget + // should come from the sum of these two. However, due to the value + // being too large, the actual ending fee rate used should be the + // sweeper's max fee rate configured. + aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx)) + aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight() + aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) / + chainfee.SatPerKWeight(deadlineA) + + aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) + expectedFeeRateAlice := aliceStartingFeeRate + + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) + require.InEpsilonf(ht, uint64(expectedFeeRateAlice), + uint64(aliceFeeRate), 0.02, "want %v, got %v", + expectedFeeRateAlice, aliceFeeRate) + + // We now check Bob' sweeping tx. + // + // The above mined block will trigger Bob's sweeper to RBF his previous + // sweeping tx, which will fail due to RBF rule#4 - the additional fees + // paid are not sufficient. This happens as our default incremental + // relay fee rate is 1 sat/vb, with the tx size of 771 weight units, or + // 192 vbytes, we need to pay at least 192 sats more to be able to RBF. + // However, since Bob's budget delta is (100_000 + 330) * 0.5 / 1008 = + // 49.77 sats, it means Bob can only perform a successful RBF every 4 + // blocks. + // + // Assert Bob's sweeping tx is not RBFed. + bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx) + expectedFeeRateBob := bobStartFeeRate + require.InEpsilonf(ht, uint64(expectedFeeRateBob), uint64(bobFeeRate), + 0.01, "want %d, got %d", expectedFeeRateBob, bobFeeRate) + + // reloclateAlicePosition is a temp hack to find the actual fee + // function position used for Alice. Due to block sync issue among the + // subsystems, we can end up having this situation: + // - sweeper is at block 2, starts sweeping an input with deadline 100. + // - fee bumper is at block 1, and thinks the conf target is 99. + // - new block 3 arrives, the func now is at position 2. + // + // TODO(yy): fix it using `blockbeat`. + reloclateAlicePosition := func() { + // Mine an empty block to trigger the possible RBF attempts. + ht.MineEmptyBlocks(1) + + // Increase the positions for both fee functions. + alicePosition++ + bobPosition++ + + // We expect two pending sweeps for both nodes as we are mining + // empty blocks. + ht.AssertNumPendingSweeps(alice, 2) + ht.AssertNumPendingSweeps(bob, 2) + + // We expect to see both Alice's and Bob's sweeping txns in the + // mempool. + ht.Miner.AssertNumTxsInMempool(2) + + // Make sure Alice's old sweeping tx has been removed from the + // mempool. + ht.Miner.AssertTxNotInMempool(aliceSweepTx.TxHash()) + + // We should see two txns in the mempool: + // - Alice's sweeping tx, which sweeps both her anchor and + // commit outputs, using the increased fee rate. + // - Bob's previous sweeping tx, which sweeps both his anchor + // and commit outputs, at the possible increased fee rate. + txns = ht.Miner.GetNumTxsFromMempool(2) + + // Assume the first tx is Alice's sweeping tx, if the second tx + // has a larger output value, then that's Alice's as her + // to_local value is much gearter. + aliceSweepTx = txns[0] + bobSweepTx = txns[1] + + // Swap them if bobSweepTx is smaller. + if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { + aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx + } + + // Alice's sweeping tx should be increased. + aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) + expectedFeeRate := aliceStartingFeeRate + + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) + + ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ + "got feerate=%v, delta=%v", deadlineA-alicePosition, + aliceTxWeight, expectedFeeRate, aliceFeeRate, + aliceFeeRateDelta) + + nextPosition := alicePosition + 1 + nextFeeRate := aliceStartingFeeRate + + aliceFeeRateDelta*chainfee.SatPerKWeight(nextPosition) + + // Calculate the distances. + delta := math.Abs(float64(aliceFeeRate - expectedFeeRate)) + deltaNext := math.Abs(float64(aliceFeeRate - nextFeeRate)) + + // Exit early if the first distance is smaller - it means we + // are at the right fee func position. + if delta < deltaNext { + require.InEpsilonf(ht, uint64(expectedFeeRate), + uint64(aliceFeeRate), 0.02, "want %v, got %v "+ + "in tx=%v", expectedFeeRate, + aliceFeeRate, aliceSweepTx.TxHash()) + + return + } + + alicePosition++ + ht.Logf("Jump position for Alice(deadline=%v): txWeight=%v, "+ + "want feerate=%v, got feerate=%v, delta=%v", + deadlineA-alicePosition, aliceTxWeight, nextFeeRate, + aliceFeeRate, aliceFeeRateDelta) + + require.InEpsilonf(ht, uint64(nextFeeRate), + uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", + nextFeeRate, aliceFeeRate, aliceSweepTx.TxHash()) + } + + reloclateAlicePosition() + + // We now mine 7 empty blocks. For each block mined, we'd see Alice's + // sweeping tx being RBFed. For Bob, he performs a fee bump every + // block, but will only publish a tx every 4 blocks mined as some of + // the fee bumps is not sufficient to meet the fee requirements + // enforced by RBF. Since his fee function is already at position 1, + // mining 7 more blocks means he will RBF his sweeping tx twice. + for i := 1; i < 7; i++ { + // Mine an empty block to trigger the possible RBF attempts. + ht.MineEmptyBlocks(1) + + // Increase the positions for both fee functions. + alicePosition++ + bobPosition++ + + // We expect two pending sweeps for both nodes as we are mining + // empty blocks. + ht.AssertNumPendingSweeps(alice, 2) + ht.AssertNumPendingSweeps(bob, 2) + + // We expect to see both Alice's and Bob's sweeping txns in the + // mempool. + ht.Miner.AssertNumTxsInMempool(2) + + // Make sure Alice's old sweeping tx has been removed from the + // mempool. + ht.Miner.AssertTxNotInMempool(aliceSweepTx.TxHash()) + + // Make sure Bob's old sweeping tx has been removed from the + // mempool. Since Bob's sweeping tx will only be successfully + // RBFed every 4 blocks, his old sweeping tx only will be + // removed when there are 4 blocks increased. + if bobPosition%4 == 0 { + ht.Miner.AssertTxNotInMempool(bobSweepTx.TxHash()) + } + + // We should see two txns in the mempool: + // - Alice's sweeping tx, which sweeps both her anchor and + // commit outputs, using the increased fee rate. + // - Bob's previous sweeping tx, which sweeps both his anchor + // and commit outputs, at the possible increased fee rate. + txns := ht.Miner.GetNumTxsFromMempool(2) + + // Assume the first tx is Alice's sweeping tx, if the second tx + // has a larger output value, then that's Alice's as her + // to_local value is much gearter. + aliceSweepTx = txns[0] + bobSweepTx = txns[1] + + // Swap them if bobSweepTx is smaller. + if bobSweepTx.TxOut[0].Value > aliceSweepTx.TxOut[0].Value { + aliceSweepTx, bobSweepTx = bobSweepTx, aliceSweepTx + } + + // We now check Alice's sweeping tx. + // + // Alice's sweeping tx should have a shape of 1-in-1-out since + // it's not used for CPFP, so it shouldn't take any wallet + // utxos. + require.Len(ht, aliceSweepTx.TxIn, 1) + require.Len(ht, aliceSweepTx.TxOut, 1) + + // Alice's sweeping tx should be increased. + aliceFeeRate := ht.CalculateTxFeeRate(aliceSweepTx) + expectedFeeRateAlice := aliceStartingFeeRate + + aliceFeeRateDelta*chainfee.SatPerKWeight(alicePosition) + + ht.Logf("Alice(deadline=%v): txWeight=%v, want feerate=%v, "+ + "got feerate=%v, delta=%v", deadlineA-alicePosition, + aliceTxWeight, expectedFeeRateAlice, aliceFeeRate, + aliceFeeRateDelta) + + require.InEpsilonf(ht, uint64(expectedFeeRateAlice), + uint64(aliceFeeRate), 0.02, "want %v, got %v in tx=%v", + expectedFeeRateAlice, aliceFeeRate, + aliceSweepTx.TxHash()) + + // We now check Bob' sweeping tx. + bobFeeRate := ht.CalculateTxFeeRate(bobSweepTx) + + // accumulatedDelta is the delta that Bob has accumulated so + // far. This will only be added when there's a successful RBF + // attempt. + accumulatedDelta := bobFeeRateDelta * + chainfee.SatPerKWeight(bobPosition) + + // Bob's sweeping tx will only be successfully RBFed every 4 + // blocks. + if bobPosition%4 == 0 { + expectedFeeRateBob = bobStartFeeRate + accumulatedDelta + } + + ht.Logf("Bob(deadline=%v): txWeight=%v, want feerate=%v, "+ + "got feerate=%v, delta=%v", deadlineB-bobPosition, + bobTxWeight, expectedFeeRateBob, bobFeeRate, + bobFeeRateDelta) + + require.InEpsilonf(ht, uint64(expectedFeeRateBob), + uint64(bobFeeRate), 0.02, "want %d, got %d in tx=%v", + expectedFeeRateBob, bobFeeRate, bobSweepTx.TxHash()) + } + + // Mine a block to confirm both sweeping txns, this is needed to clean + // up the mempool. + ht.MineBlocksAndAssertNumTxes(1, 2) +} + // createSimpleNetwork creates the specified number of nodes and makes a // topology of `node1 -> node2 -> node3...`. Each node is created using the // specified config, the neighbors are connected, and the channels are opened.