diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 4afd334df..878d74b05 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -590,4 +590,8 @@ var allTestCases = []*lntest.TestCase{ Name: "sweep anchor cpfp local force close", TestFunc: testSweepAnchorCPFPLocalForceClose, }, + { + Name: "sweep htlcs", + TestFunc: testSweepHTLCs, + }, } diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 430b001c6..e2d1ec033 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -785,6 +785,6 @@ func testForwardBlindedRoute(ht *lntest.HarnessTest) { // Assert that the HTLC has settled before test cleanup runs so that // we can cooperatively close all channels. - ht.AssertHLTCNotActive(ht.Bob, testCase.channels[1], hash[:]) - ht.AssertHLTCNotActive(ht.Alice, testCase.channels[0], hash[:]) + ht.AssertHTLCNotActive(ht.Bob, testCase.channels[1], hash[:]) + ht.AssertHTLCNotActive(ht.Alice, testCase.channels[0], hash[:]) } diff --git a/itest/lnd_sweep_test.go b/itest/lnd_sweep_test.go index aaf52357e..a9e838088 100644 --- a/itest/lnd_sweep_test.go +++ b/itest/lnd_sweep_test.go @@ -4,11 +4,16 @@ import ( "fmt" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/contractcourt" + "github.com/lightningnetwork/lnd/fn" + "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/lntest/node" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/routing" "github.com/stretchr/testify/require" @@ -305,6 +310,493 @@ func testSweepAnchorCPFPLocalForceClose(ht *lntest.HarnessTest) { ht.AssertNumPendingSweeps(alice, 1) } +// testSweepHTLCs checks the sweeping behavior for HTLC outputs. Since HTLCs +// are time-sensitive, we expect to see both the incoming and outgoing HTLCs +// are fee bumped properly based on their budgets and deadlines. +// +// Setup: +// 1. Fund Alice with 1 UTXOs - she only needs one for the funding process, +// 2. Fund Bob with 3 UTXOs - he needs one for the funding process, one for +// his CPFP anchor sweeping, and one for sweeping his outgoing HTLC. +// 3. Create a linear network from Alice -> Bob -> Carol. +// 4. Alice pays two invoices to Carol, with Carol holding the settlement. +// 5. Alice goes offline. +// 7. Carol settles one of the invoices with Bob, so Bob has an incoming HTLC +// that he can claim onchain since he has the preimage. +// 8. Carol goes offline. +// 9. Assert Bob sweeps his incoming and outgoing HTLCs with the expected fee +// rates. +// +// Test: +// 1. Bob's outgoing HTLC is swept and fee bumped based on its deadline and +// budget. +// 2. Bob's incoming HTLC is swept and fee bumped based on its deadline and +// budget. +func testSweepHTLCs(ht *lntest.HarnessTest) { + // Setup testing params. + // + // Invoice is 100k sats. + invoiceAmt := btcutil.Amount(100_000) + + // Use the smallest CLTV so we can mine fewer blocks. + cltvDelta := routing.MinCLTVDelta + + // Start tracking the deadline delta of Bob's HTLCs. We need one block + // for the CSV lock, and another block to trigger the sweeper to sweep. + outgoingHTLCDeadline := int32(cltvDelta - 2) + incomingHTLCDeadline := int32(lncfg.DefaultIncomingBroadcastDelta - 2) + + // startFeeRate1 and startFeeRate2 are returned by the fee estimator in + // sat/kw. They will be used as the starting fee rate for the linear + // fee func used by Bob. The values are chosen from calling the cli in + // bitcoind: + // - `estimatesmartfee 18 conservative`. + // - `estimatesmartfee 10 conservative`. + startFeeRate1 := chainfee.SatPerKWeight(2500) + startFeeRate2 := chainfee.SatPerKWeight(3000) + + // Set up the fee estimator to return the testing fee rate when the + // conf target is the deadline. + ht.SetFeeEstimateWithConf(startFeeRate1, uint32(outgoingHTLCDeadline)) + ht.SetFeeEstimateWithConf(startFeeRate2, uint32(incomingHTLCDeadline)) + + // Create two preimages, one that will be settled, the other be hold. + var preimageSettled, preimageHold lntypes.Preimage + copy(preimageSettled[:], ht.Random32Bytes()) + copy(preimageHold[:], ht.Random32Bytes()) + payHashSettled := preimageSettled.Hash() + payHashHold := preimageHold.Hash() + + // We now set up the force close scenario. We will create a network + // from Alice -> Bob -> Carol, where Alice will send two payments to + // Carol via Bob, Alice goes offline, then Carol settles the first + // payment, goes offline. We expect Bob to sweep his incoming and + // outgoing HTLCs. + // + // Prepare params. + cfg := []string{ + "--protocol.anchors", + // Use a small CLTV to mine less blocks. + fmt.Sprintf("--bitcoin.timelockdelta=%d", cltvDelta), + // Use a very large CSV, this way to_local outputs are never + // swept so we can focus on testing HTLCs. + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", cltvDelta*10), + } + openChannelParams := lntest.OpenChannelParams{ + Amt: invoiceAmt * 10, + } + + // Create a three hop network: Alice -> Bob -> Carol. + chanPoints, nodes := createSimpleNetwork(ht, cfg, 3, openChannelParams) + + // Unwrap the results. + abChanPoint, bcChanPoint := chanPoints[0], chanPoints[1] + alice, bob, carol := nodes[0], nodes[1], nodes[2] + + // Bob needs two more wallet utxos: + // - when sweeping anchors, he needs one utxo for each sweep. + // - when sweeping HTLCs, he needs one utxo for each sweep. + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + + // Subscribe the invoices. + stream1 := carol.RPC.SubscribeSingleInvoice(payHashSettled[:]) + stream2 := carol.RPC.SubscribeSingleInvoice(payHashHold[:]) + + // With the network active, we'll now add two hodl invoices at Carol's + // end. + invoiceReqSettle := &invoicesrpc.AddHoldInvoiceRequest{ + Value: int64(invoiceAmt), + CltvExpiry: finalCltvDelta, + Hash: payHashSettled[:], + } + invoiceSettle := carol.RPC.AddHoldInvoice(invoiceReqSettle) + + invoiceReqHold := &invoicesrpc.AddHoldInvoiceRequest{ + Value: int64(invoiceAmt), + CltvExpiry: finalCltvDelta, + Hash: payHashHold[:], + } + invoiceHold := carol.RPC.AddHoldInvoice(invoiceReqHold) + + // Let Alice pay the invoices. + req1 := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoiceSettle.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + req2 := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoiceHold.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + + // Assert the payments are inflight. + ht.SendPaymentAndAssertStatus(alice, req1, lnrpc.Payment_IN_FLIGHT) + ht.SendPaymentAndAssertStatus(alice, req2, lnrpc.Payment_IN_FLIGHT) + + // Wait for Carol to mark invoice as accepted. There is a small gap to + // bridge between adding the htlc to the channel and executing the exit + // hop logic. + ht.AssertInvoiceState(stream1, lnrpc.Invoice_ACCEPTED) + ht.AssertInvoiceState(stream2, lnrpc.Invoice_ACCEPTED) + + // At this point, all 3 nodes should now have an active channel with + // the created HTLCs pending on all of them. + // + // Alice should have two outgoing HTLCs on channel Alice -> Bob. + ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHashSettled[:]) + ht.AssertOutgoingHTLCActive(alice, abChanPoint, payHashHold[:]) + + // Bob should have two incoming HTLCs on channel Alice -> Bob, and two + // outgoing HTLCs on channel Bob -> Carol. + ht.AssertIncomingHTLCActive(bob, abChanPoint, payHashSettled[:]) + ht.AssertIncomingHTLCActive(bob, abChanPoint, payHashHold[:]) + ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHashSettled[:]) + ht.AssertOutgoingHTLCActive(bob, bcChanPoint, payHashHold[:]) + + // Carol should have two incoming HTLCs on channel Bob -> Carol. + ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHashSettled[:]) + ht.AssertIncomingHTLCActive(carol, bcChanPoint, payHashHold[:]) + + // Let Alice go offline. Once Bob later learns the preimage, he + // couldn't settle it with Alice so he has to go onchain to collect it. + ht.Shutdown(alice) + + // Carol settles the first invoice. + carol.RPC.SettleInvoice(preimageSettled[:]) + + // Let Carol go offline so we can focus on testing Bob's sweeping + // behavior. + ht.Shutdown(carol) + + // Bob should have settled his outgoing HTLC with Carol. + ht.AssertHTLCNotActive(bob, bcChanPoint, payHashSettled[:]) + + // We'll now mine enough blocks to trigger Bob to force close channel + // Bob->Carol due to his outgoing HTLC is about to timeout. With the + // default outgoing broadcast delta of zero, this will be the same + // height as the htlc expiry height. + numBlocks := padCLTV(uint32( + invoiceReqHold.CltvExpiry - lncfg.DefaultOutgoingBroadcastDelta, + )) + ht.MineBlocks(numBlocks) + + // Bob force closes the channel. + // ht.CloseChannelAssertPending(bob, bcChanPoint, true) + + // Before we mine empty blocks to check the RBF behavior, we need to be + // aware that Bob's incoming HTLC will expire before his outgoing HTLC + // deadline is reached. This happens because the incoming HTLC is sent + // onchain at CLTVDelta-BroadcastDelta=18-10=8, which means after 8 + // blocks are mined, we expect Bob force closes the channel Alice->Bob. + blocksTillIncomingSweep := cltvDelta - + lncfg.DefaultIncomingBroadcastDelta + + // Bob should now have two pending sweeps, one for the anchor on the + // local commitment, the other on the remote commitment. + ht.AssertNumPendingSweeps(bob, 2) + + // Assert Bob's force closing tx has been broadcast. + ht.Miner.AssertNumTxsInMempool(1) + + // Mine the force close tx, which triggers Bob's contractcourt to offer + // his outgoing HTLC to his sweeper. + // + // NOTE: HTLC outputs are only offered to sweeper when the force close + // tx is confirmed and the CSV has reached. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Update the blocks left till Bob force closes Alice->Bob. + blocksTillIncomingSweep-- + + // Bob should have two pending sweeps, one for the anchor sweeping, the + // other for the outgoing HTLC. + ht.AssertNumPendingSweeps(bob, 2) + + // Mine one block to confirm Bob's anchor sweeping tx, which will + // trigger his sweeper to publish the HTLC sweeping tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Update the blocks left till Bob force closes Alice->Bob. + blocksTillIncomingSweep-- + + // Bob should now have one sweep and one sweeping tx in the mempool. + ht.AssertNumPendingSweeps(bob, 1) + outgoingSweep := ht.Miner.GetNumTxsFromMempool(1)[0] + + // Check the shape of the sweeping tx - we expect it to be + // 2-input-2-output as a wallet utxo is used and a required output is + // made. + require.Len(ht, outgoingSweep.TxIn, 2) + require.Len(ht, outgoingSweep.TxOut, 2) + + // Calculate the ending fee rate. + // + // TODO(yy): the budget we use to sweep the first-level outgoing HTLC + // is twice its value. This is a temporary mitigation to prevent + // cascading FCs and the test should be updated once it's properly + // fixed. + outgoingBudget := 2 * invoiceAmt + outgoingTxSize := ht.CalculateTxWeight(outgoingSweep) + outgoingEndFeeRate := chainfee.NewSatPerKWeight( + outgoingBudget, uint64(outgoingTxSize), + ) + + // Assert the initial sweeping tx is using the start fee rate. + outgoingStartFeeRate := ht.CalculateTxFeeRate(outgoingSweep) + require.InEpsilonf(ht, uint64(startFeeRate1), + uint64(outgoingStartFeeRate), 0.01, "want %d, got %d", + startFeeRate1, outgoingStartFeeRate) + + // Now the start fee rate is checked, we can calculate the fee rate + // delta. + outgoingFeeRateDelta := (outgoingEndFeeRate - outgoingStartFeeRate) / + chainfee.SatPerKWeight(outgoingHTLCDeadline) + + // outgoingFuncPosition records the position of Bob's fee function used + // for his outgoing HTLC sweeping tx. + outgoingFuncPosition := int32(0) + + // assertSweepFeeRate is a helper closure that asserts the expected fee + // rate is used at the given position for a sweeping tx. + assertSweepFeeRate := func(sweepTx *wire.MsgTx, + startFeeRate, delta chainfee.SatPerKWeight, txSize int64, + deadline, position int32, desc string) { + + // Bob's HTLC sweeping tx should be fee bumped. + feeRate := ht.CalculateTxFeeRate(sweepTx) + expectedFeeRate := startFeeRate + delta*chainfee.SatPerKWeight( + position, + ) + + ht.Logf("Bob's %s HTLC (deadline=%v): txWeight=%v, want "+ + "feerate=%v, got feerate=%v, delta=%v", desc, + deadline-position, txSize, expectedFeeRate, + feeRate, delta) + + require.InEpsilonf(ht, uint64(expectedFeeRate), uint64(feeRate), + 0.01, "want %v, got %v in tx=%v", expectedFeeRate, + feeRate, sweepTx.TxHash()) + } + + // We now mine enough blocks to trigger Bob to force close channel + // Alice->Bob. Along the way, we will check his outgoing HTLC sweeping + // tx is RBFed as expected. + for i := 0; i < blocksTillIncomingSweep-1; i++ { + // Mine an empty block. Since the sweeping tx is not confirmed, + // Bob's fee bumper should increase its fees. + ht.MineEmptyBlocks(1) + + // Update Bob's fee function position. + outgoingFuncPosition++ + + // We should see Bob's sweeping tx in the mempool. + ht.Miner.AssertNumTxsInMempool(1) + + // Make sure Bob's old sweeping tx has been removed from the + // mempool. + ht.Miner.AssertTxNotInMempool(outgoingSweep.TxHash()) + + // Bob should still have the outgoing HTLC sweep. + ht.AssertNumPendingSweeps(bob, 1) + + // We should see Bob's replacement tx in the mempool. + outgoingSweep = ht.Miner.GetNumTxsFromMempool(1)[0] + + // Bob's outgoing HTLC sweeping tx should be fee bumped. + assertSweepFeeRate( + outgoingSweep, outgoingStartFeeRate, + outgoingFeeRateDelta, outgoingTxSize, + outgoingHTLCDeadline, outgoingFuncPosition, "Outgoing", + ) + } + + // Once exited the above loop and mine one more block, we'd have mined + // enough blocks to trigger Bob to force close his channel with Alice. + ht.MineEmptyBlocks(1) + + // Update Bob's fee function position. + outgoingFuncPosition++ + + // Bob should now have three pending sweeps: + // 1. the outgoing HTLC output. + // 2. the anchor output from his local commitment. + // 3. the anchor output from his remote commitment. + ht.AssertNumPendingSweeps(bob, 3) + + // We should see two txns in the mempool: + // 1. Bob's outgoing HTLC sweeping tx. + // 2. Bob's force close tx for Alice->Bob. + txns := ht.Miner.GetNumTxsFromMempool(2) + + // Find the force close tx - we expect it to have a single input. + closeTx := txns[0] + if len(closeTx.TxIn) != 1 { + closeTx = txns[1] + } + + // We don't care the behavior of the anchor sweep in this test, so we + // mine the force close tx to trigger Bob's contractcourt to offer his + // incoming HTLC to his sweeper. + ht.Miner.MineBlockWithTx(closeTx) + + // Update Bob's fee function position. + outgoingFuncPosition++ + + // Bob should now have three pending sweeps: + // 1. the outgoing HTLC output on Bob->Carol. + // 2. the incoming HTLC output on Alice->Bob. + // 3. the anchor sweeping on Alice-> Bob. + ht.AssertNumPendingSweeps(bob, 3) + + // Mine one block, which will trigger his sweeper to publish his + // incoming HTLC sweeping tx. + ht.MineEmptyBlocks(1) + + // Update the fee function's positions. + outgoingFuncPosition++ + + // We should see three txns in the mempool: + // 1. the outgoing HTLC sweeping tx. + // 2. the incoming HTLC sweeping tx. + // 3. the anchor sweeping tx. + txns = ht.Miner.GetNumTxsFromMempool(3) + + abCloseTxid := closeTx.TxHash() + + // Identify the sweeping txns spent from Alice->Bob. + txns = ht.FindSweepingTxns(txns, 2, abCloseTxid) + + // Identify the anchor and incoming HTLC sweeps - if the tx has 1 + // output, then it's the anchor sweeping tx. + var incomingSweep, anchorSweep = txns[0], txns[1] + if len(anchorSweep.TxOut) != 1 { + incomingSweep, anchorSweep = anchorSweep, incomingSweep + } + + // Calculate the ending fee rate for the incoming HTLC sweep. + incomingBudget := invoiceAmt.MulF64(contractcourt.DefaultBudgetRatio) + incomingTxSize := ht.CalculateTxWeight(incomingSweep) + incomingEndFeeRate := chainfee.NewSatPerKWeight( + incomingBudget, uint64(incomingTxSize), + ) + + // Assert the initial sweeping tx is using the start fee rate. + incomingStartFeeRate := ht.CalculateTxFeeRate(incomingSweep) + require.InEpsilonf(ht, uint64(startFeeRate2), + uint64(incomingStartFeeRate), 0.01, "want %d, got %d in tx=%v", + startFeeRate2, incomingStartFeeRate, incomingSweep.TxHash()) + + // Now the start fee rate is checked, we can calculate the fee rate + // delta. + incomingFeeRateDelta := (incomingEndFeeRate - incomingStartFeeRate) / + chainfee.SatPerKWeight(incomingHTLCDeadline) + + // incomingFuncPosition records the position of Bob's fee function used + // for his incoming HTLC sweeping tx. + incomingFuncPosition := int32(0) + + // Mine the anchor sweeping tx to reduce noise in this test. + ht.Miner.MineBlockWithTxes([]*btcutil.Tx{btcutil.NewTx(anchorSweep)}) + + // Update the fee function's positions. + outgoingFuncPosition++ + incomingFuncPosition++ + + // identifySweepTxns is a helper closure that identifies the incoming + // and outgoing HTLC sweeping txns. It always assumes there are two + // sweeping txns in the mempool, and returns the incoming HTLC sweep + // first. + identifySweepTxns := func() (*wire.MsgTx, *wire.MsgTx) { + // We should see two txns in the mempool: + // 1. the outgoing HTLC sweeping tx. + // 2. the incoming HTLC sweeping tx. + txns = ht.Miner.GetNumTxsFromMempool(2) + + var incoming, outgoing *wire.MsgTx + + // The sweeping tx has two inputs, one from wallet, the other + // from the force close tx. We now check whether the first tx + // spends from the force close tx of Alice->Bob. + found := fn.Any(func(inp *wire.TxIn) bool { + return inp.PreviousOutPoint.Hash == abCloseTxid + }, txns[0].TxIn) + + // If the first tx spends an outpoint from the force close tx + // of Alice->Bob, then it must be the incoming HTLC sweeping + // tx. + if found { + incoming, outgoing = txns[0], txns[1] + } else { + // Otherwise the second tx must be the incoming HTLC + // sweep. + incoming, outgoing = txns[1], txns[0] + } + + return incoming, outgoing + } + + // We should see Bob's sweeping txns in the mempool. + incomingSweep, outgoingSweep = identifySweepTxns() + + // We now mine enough blocks till we reach the end of the outgoing + // HTLC's deadline. Along the way, we check the expected fee rates are + // used for both incoming and outgoing HTLC sweeping txns. + blocksLeft := outgoingHTLCDeadline - outgoingFuncPosition + for i := int32(0); i < blocksLeft; i++ { + // Mine an empty block. + ht.MineEmptyBlocks(1) + + // Update Bob's fee function position. + outgoingFuncPosition++ + incomingFuncPosition++ + + // We should see two txns in the mempool, + // - the incoming HTLC sweeping tx. + // - the outgoing HTLC sweeping tx. + ht.Miner.AssertNumTxsInMempool(2) + + // Make sure Bob's old sweeping txns have been removed from the + // mempool. + ht.Miner.AssertTxNotInMempool(outgoingSweep.TxHash()) + ht.Miner.AssertTxNotInMempool(incomingSweep.TxHash()) + + // Bob should have two pending sweeps: + // 1. the outgoing HTLC output on Bob->Carol. + // 2. the incoming HTLC output on Alice->Bob. + ht.AssertNumPendingSweeps(bob, 2) + + // We should see Bob's replacement txns in the mempool. + incomingSweep, outgoingSweep = identifySweepTxns() + + // Bob's outgoing HTLC sweeping tx should be fee bumped. + assertSweepFeeRate( + outgoingSweep, outgoingStartFeeRate, + outgoingFeeRateDelta, outgoingTxSize, + outgoingHTLCDeadline, outgoingFuncPosition, "Outgoing", + ) + + // Bob's incoming HTLC sweeping tx should be fee bumped. + assertSweepFeeRate( + incomingSweep, incomingStartFeeRate, + incomingFeeRateDelta, incomingTxSize, + incomingHTLCDeadline, incomingFuncPosition, "Incoming", + ) + } + + // Mine an empty block. + ht.MineEmptyBlocks(1) + + // We should see Bob's old txns in the mempool. + currentIncomingSweep, currentOutgoingSweep := identifySweepTxns() + require.Equal(ht, incomingSweep.TxHash(), currentIncomingSweep.TxHash()) + require.Equal(ht, outgoingSweep.TxHash(), currentOutgoingSweep.TxHash()) + + // Mine a block to confirm the HTLC sweeps. + 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. diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index c64373d33..b6805a7a7 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -1328,7 +1328,7 @@ func (h *HarnessTest) AssertActiveHtlcs(hn *node.HarnessNode, func (h *HarnessTest) AssertIncomingHTLCActive(hn *node.HarnessNode, cp *lnrpc.ChannelPoint, payHash []byte) *lnrpc.HTLC { - return h.assertHLTCActive(hn, cp, payHash, true) + return h.assertHTLCActive(hn, cp, payHash, true) } // AssertOutgoingHTLCActive asserts the node has a pending outgoing HTLC in the @@ -1336,12 +1336,12 @@ func (h *HarnessTest) AssertIncomingHTLCActive(hn *node.HarnessNode, func (h *HarnessTest) AssertOutgoingHTLCActive(hn *node.HarnessNode, cp *lnrpc.ChannelPoint, payHash []byte) *lnrpc.HTLC { - return h.assertHLTCActive(hn, cp, payHash, false) + return h.assertHTLCActive(hn, cp, payHash, false) } // assertHLTCActive asserts the node has a pending HTLC in the given channel. // Returns the HTLC if found and active. -func (h *HarnessTest) assertHLTCActive(hn *node.HarnessNode, +func (h *HarnessTest) assertHTLCActive(hn *node.HarnessNode, cp *lnrpc.ChannelPoint, payHash []byte, incoming bool) *lnrpc.HTLC { var result *lnrpc.HTLC @@ -1378,7 +1378,7 @@ func (h *HarnessTest) assertHLTCActive(hn *node.HarnessNode, "have: %s", hn.Name(), payHash, want, have) } - return fmt.Errorf("node [%s:%x] didn't have: the payHash %v", + return fmt.Errorf("node [%s:%x] didn't have: the payHash %x", hn.Name(), hn.PubKey[:], payHash) }, DefaultTimeout) require.NoError(h, err, "timeout checking pending HTLC") @@ -1392,7 +1392,7 @@ func (h *HarnessTest) assertHLTCActive(hn *node.HarnessNode, // // NOTE: to check a pending HTLC becoming settled, first use AssertHLTCActive // then follow this check. -func (h *HarnessTest) AssertHLTCNotActive(hn *node.HarnessNode, +func (h *HarnessTest) AssertHTLCNotActive(hn *node.HarnessNode, cp *lnrpc.ChannelPoint, payHash []byte) *lnrpc.HTLC { var result *lnrpc.HTLC diff --git a/lntest/harness_miner.go b/lntest/harness_miner.go index 90c7418cf..751f48aff 100644 --- a/lntest/harness_miner.go +++ b/lntest/harness_miner.go @@ -476,6 +476,30 @@ func (h *HarnessMiner) MineBlockWithTxes(txes []*btcutil.Tx) *wire.MsgBlock { block, err := h.Client.GetBlock(b.Hash()) require.NoError(h, err, "unable to get block") + // Make sure the mempool has been updated. + for _, tx := range txes { + h.AssertTxNotInMempool(*tx.Hash()) + } + + return block +} + +// MineBlocksWithTx mines a single block to include the specifies tx only. +func (h *HarnessMiner) MineBlockWithTx(tx *wire.MsgTx) *wire.MsgBlock { + var emptyTime time.Time + + txes := []*btcutil.Tx{btcutil.NewTx(tx)} + + // 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") + + // Make sure the mempool has been updated. + h.AssertTxNotInMempool(tx.TxHash()) + return block }