diff --git a/lntest/itest/assertions.go b/lntest/itest/assertions.go index 18c385164..5f0f79a38 100644 --- a/lntest/itest/assertions.go +++ b/lntest/itest/assertions.go @@ -1529,17 +1529,18 @@ func assertNumActiveHtlcs(nodes []*lntest.HarnessNode, numHtlcs int) error { } func assertSpendingTxInMempool(t *harnessTest, miner *rpcclient.Client, - timeout time.Duration, chanPoint wire.OutPoint) chainhash.Hash { + timeout time.Duration, inputs ...wire.OutPoint) chainhash.Hash { - tx := getSpendingTxInMempool(t, miner, timeout, chanPoint) + tx := getSpendingTxInMempool(t, miner, timeout, inputs...) return tx.TxHash() } // getSpendingTxInMempool waits for a transaction spending the given outpoint to // appear in the mempool and returns that tx in full. func getSpendingTxInMempool(t *harnessTest, miner *rpcclient.Client, - timeout time.Duration, chanPoint wire.OutPoint) *wire.MsgTx { + timeout time.Duration, inputs ...wire.OutPoint) *wire.MsgTx { + inputSet := make(map[wire.OutPoint]struct{}, len(inputs)) breakTimeout := time.After(timeout) ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() @@ -1559,13 +1560,30 @@ func getSpendingTxInMempool(t *harnessTest, miner *rpcclient.Client, for _, txid := range mempool { tx, err := miner.GetRawTransaction(txid) require.NoError(t.t, err, "unable to fetch tx") - msgTx := tx.MsgTx() + + // Include the inputs again in case they were + // removed in a previous iteration. + for _, input := range inputs { + inputSet[input] = struct{}{} + } + for _, txIn := range msgTx.TxIn { - if txIn.PreviousOutPoint == chanPoint { - return msgTx + input := txIn.PreviousOutPoint + if _, ok := inputSet[input]; ok { + delete(inputSet, input) } } + + if len(inputSet) > 0 { + // Missing input, check next transaction + // or try again. + continue + } + + // Transaction spends all expected inputs, + // return. + return msgTx } } } diff --git a/lntest/itest/lnd_funding_test.go b/lntest/itest/lnd_funding_test.go index 17bb01b08..8c8cda653 100644 --- a/lntest/itest/lnd_funding_test.go +++ b/lntest/itest/lnd_funding_test.go @@ -14,7 +14,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnrpc/signrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" @@ -393,7 +393,7 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) { const thawHeight uint32 = 10 const chanSize = funding.MaxBtcFundingAmount fundingShim1, chanPoint1, _ := deriveFundingShim( - net, t, carol, dave, chanSize, thawHeight, 1, false, + net, t, carol, dave, chanSize, thawHeight, false, ) _ = openChannelStream( t, net, carol, dave, lntest.OpenChannelParams{ @@ -409,7 +409,7 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) { // do exactly that now. For this one we publish the transaction so we // can mine it later. fundingShim2, chanPoint2, _ := deriveFundingShim( - net, t, carol, dave, chanSize, thawHeight, 2, true, + net, t, carol, dave, chanSize, thawHeight, true, ) // At this point, we'll now carry out the normal basic channel funding @@ -637,17 +637,14 @@ func testChannelFundingPersistence(net *lntest.NetworkHarness, t *harnessTest) { // keys on both sides. func deriveFundingShim(net *lntest.NetworkHarness, t *harnessTest, carol, dave *lntest.HarnessNode, chanSize btcutil.Amount, - thawHeight uint32, keyIndex int32, publish bool) (*lnrpc.FundingShim, + thawHeight uint32, publish bool) (*lnrpc.FundingShim, *lnrpc.ChannelPoint, *chainhash.Hash) { ctxb := context.Background() - keyLoc := &signrpc.KeyLocator{ - KeyFamily: 9999, - KeyIndex: keyIndex, - } - carolFundingKey, err := carol.WalletKitClient.DeriveKey(ctxb, keyLoc) + keyLoc := &walletrpc.KeyReq{KeyFamily: 9999} + carolFundingKey, err := carol.WalletKitClient.DeriveNextKey(ctxb, keyLoc) require.NoError(t.t, err) - daveFundingKey, err := dave.WalletKitClient.DeriveKey(ctxb, keyLoc) + daveFundingKey, err := dave.WalletKitClient.DeriveNextKey(ctxb, keyLoc) require.NoError(t.t, err) // Now that we have the multi-sig keys for each party, we can manually diff --git a/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go index f487293ef..f17db42fe 100644 --- a/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go @@ -187,8 +187,9 @@ func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest, // Bob's force close transaction should now be found in the mempool. If // there are anchors, we also expect Bob's anchor sweep. + hasAnchors := commitTypeHasAnchors(c) expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { + if hasAnchors { expectedTxes = 2 } @@ -251,19 +252,25 @@ func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest, require.NoError(t.t, err) } + switch c { // With the closing transaction confirmed, we should expect Bob's HTLC // timeout transactions to be broadcast due to the expiry being reached. // We will also expect the success transactions, since he learnt the // preimages from Alice. We also expect Carol to sweep her commitment // output. - expectedTxes = 2*numInvoices + 1 + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 2*numInvoices + 1 // In case of anchors, all success transactions will be aggregated into // one, the same is the case for the timeout transactions. In this case - // Carol will also sweep her anchor output in a separate tx (since it - // will be low fee). - if c == lnrpc.CommitmentType_ANCHORS { + // Carol will also sweep her commitment and anchor output as separate + // txs (since it will be low fee). + case lnrpc.CommitmentType_ANCHORS, + lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: expectedTxes = 4 + + default: + t.Fatalf("unhandled commitment type %v", c) } txes, err := getNTxsFromMempool( @@ -298,7 +305,7 @@ func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest, // In case of anchor we expect all the timeout and success second // levels to be aggregated into one tx. For earlier channel types, they // will be separate transactions. - if c == lnrpc.CommitmentType_ANCHORS { + if hasAnchors { require.Len(t.t, timeoutTxs, 1) require.Len(t.t, successTxs, 1) } else { @@ -340,47 +347,75 @@ func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest, ) require.NoError(t.t, err) - // If we then mine additional blocks, Bob can sweep his commitment - // output. - _, err = net.Miner.Client.Generate(defaultCSV - 2) - require.NoError(t.t, err) + if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // If we then mine additional blocks, Bob can sweep his commitment + // output. + _, err = net.Miner.Client.Generate(defaultCSV - 2) + require.NoError(t.t, err) - // Find the commitment sweep. - bobCommitSweepHash, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - require.NoError(t.t, err) - bobCommitSweep, err := net.Miner.Client.GetRawTransaction(bobCommitSweepHash) - require.NoError(t.t, err) + // Find the commitment sweep. + bobCommitSweepHash, err := waitForTxInMempool( + net.Miner.Client, minerMempoolTimeout, + ) + require.NoError(t.t, err) + bobCommitSweep, err := net.Miner.Client.GetRawTransaction( + bobCommitSweepHash, + ) + require.NoError(t.t, err) - require.Equal( - t.t, closeTxid, bobCommitSweep.MsgTx().TxIn[0].PreviousOutPoint.Hash, - ) + require.Equal( + t.t, closeTxid, + bobCommitSweep.MsgTx().TxIn[0].PreviousOutPoint.Hash, + ) - // Also ensure it is not spending from any of the HTLC output. - for _, txin := range bobCommitSweep.MsgTx().TxIn { - for _, timeoutTx := range timeoutTxs { - if *timeoutTx == txin.PreviousOutPoint.Hash { - t.Fatalf("found unexpected spend of timeout tx") + // Also ensure it is not spending from any of the HTLC output. + for _, txin := range bobCommitSweep.MsgTx().TxIn { + for _, timeoutTx := range timeoutTxs { + if *timeoutTx == txin.PreviousOutPoint.Hash { + t.Fatalf("found unexpected spend of " + + "timeout tx") + } } - } - for _, successTx := range successTxs { - if *successTx == txin.PreviousOutPoint.Hash { - t.Fatalf("found unexpected spend of success tx") + for _, successTx := range successTxs { + if *successTx == txin.PreviousOutPoint.Hash { + t.Fatalf("found unexpected spend of " + + "success tx") + } } } } - switch { + switch c { + // In case this is a non-anchor channel type, we must mine 2 blocks, as + // the nursery waits an extra block before sweeping. Before the blocks + // are mined, we should expect to see Bob's commit sweep in the mempool. + case lnrpc.CommitmentType_LEGACY: + _ = mineBlocks(t, net, 2, 1) // Mining one additional block, Bob's second level tx is mature, and he - // can sweep the output. - case c == lnrpc.CommitmentType_ANCHORS: + // can sweep the output. Before the blocks are mined, we should expect + // to see Bob's commit sweep in the mempool. + case lnrpc.CommitmentType_ANCHORS: _ = mineBlocks(t, net, 1, 1) - // In case this is a non-anchor channel type, we must mine 2 blocks, as - // the nursery waits an extra block before sweeping. + // Since Bob is the initiator of the Bob-Carol script-enforced leased + // channel, he incurs an additional CLTV when sweeping outputs back to + // his wallet. We'll need to mine enough blocks for the timelock to + // expire to prompt his broadcast. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + resp, err := bob.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, resp.PendingForceClosingChannels, 1) + forceCloseChan := resp.PendingForceClosingChannels[0] + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + _ = mineBlocks(t, net, uint32(forceCloseChan.BlocksTilMaturity), 0) + default: - _ = mineBlocks(t, net, 2, 1) + t.Fatalf("unhandled commitment type %v", c) } bobSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go index 4af71b526..60e9aa7f8 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go @@ -85,16 +85,29 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, // At this point, Bob decides that he wants to exit the channel // immediately, so he force closes his commitment transaction. + hasAnchors := commitTypeHasAnchors(c) bobForceClose := closeChannelAndAssertType( - t, net, bob, aliceChanPoint, - c == lnrpc.CommitmentType_ANCHORS, true, + t, net, bob, aliceChanPoint, hasAnchors, true, ) - // Alice will sweep her commitment output immediately. If there are - // anchors, Alice will also sweep hers. - expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { + var expectedTxes int + switch c { + // Alice will sweep her commitment output immediately. + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 1 + + // Alice will sweep her commitment and anchor output immediately. + case lnrpc.CommitmentType_ANCHORS: expectedTxes = 2 + + // Alice will sweep her anchor output immediately. Her commitment output + // cannot be swept yet as it has incurred an additional CLTV due to + // being the initiator of a script-enforced leased channel. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + expectedTxes = 1 + + default: + t.Fatalf("unhandled commitment type %v", c) } _, err = waitForNTxsInMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, @@ -155,15 +168,28 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, err = restartBob() require.NoError(t.t, err) - // After the force close transacion is mined, Carol should broadcast her - // second level HTLC transacion. Bob will broadcast a sweep tx to sweep - // his output in the channel with Carol. He can do this immediately, as - // the output is not timelocked since Carol was the one force closing. - // If there are anchors on the commitment, Bob will also sweep his - // anchor. - expectedTxes = 2 - if c == lnrpc.CommitmentType_ANCHORS { + // After the force close transacion is mined, transactions will be + // broadcast by both Bob and Carol. + switch c { + // Carol will broadcast her second level HTLC transaction and Bob will + // sweep his commitment output. + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 2 + + // Carol will broadcast her second level HTLC transaction and Bob will + // sweep his commitment and anchor output. + case lnrpc.CommitmentType_ANCHORS: expectedTxes = 3 + + // Carol will broadcast her second level HTLC transaction and anchor + // sweep, and Bob will sweep his anchor output. Bob can't sweep his + // commitment output yet as it has incurred an additional CLTV due to + // being the initiator of a script-enforced leased channel. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + expectedTxes = 3 + + default: + t.Fatalf("unhandled commitment type %v", c) } txes, err := getNTxsFromMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, @@ -179,24 +205,20 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, restartAlice, err := net.SuspendNode(alice) require.NoError(t.t, err) - // Mine a block to confirm the two transactions (+ the coinbase). + // Mine a block to confirm the expected transactions (+ the coinbase). block = mineBlocks(t, net, 1, expectedTxes)[0] require.Len(t.t, block.Transactions, expectedTxes+1) - var secondLevelMaturity uint32 - switch c { - - // If this is a channel of the anchor type, we will subtract one block - // from the default CSV, as the Sweeper will handle the input, and the Sweeper - // sweeps the input as soon as the lock expires. - case lnrpc.CommitmentType_ANCHORS: - secondLevelMaturity = defaultCSV - 1 - // For non-anchor channel types, the nursery will handle sweeping the // second level output, and it will wait one extra block before // sweeping it. - default: - secondLevelMaturity = defaultCSV + secondLevelMaturity := uint32(defaultCSV) + + // If this is a channel of the anchor type, we will subtract one block + // from the default CSV, as the Sweeper will handle the input, and the + // Sweeper sweeps the input as soon as the lock expires. + if hasAnchors { + secondLevelMaturity = defaultCSV - 1 } // Keep track of the second level tx maturity. @@ -220,8 +242,12 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, // At this point, Bob should have broadcast his second layer success // transaction, and should have sent it to the nursery for incubation. + numPendingChans := 1 + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + numPendingChans++ + } err = waitForNumChannelPendingForceClose( - bob, 1, func(c *lnrpcForceCloseChannel) error { + bob, numPendingChans, func(c *lnrpcForceCloseChannel) error { if c.Channel.LocalBalance != 0 { return nil } @@ -288,14 +314,57 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest, block = mineBlocks(t, net, 1, 1)[0] assertTxInBlock(t, block, bobSweep) - err = waitForNumChannelPendingForceClose(bob, 0, nil) - require.NoError(t.t, err) - assertNodeNumChannels(t, bob, 0) + // With the script-enforced lease commitment type, Alice and Bob still + // haven't been able to sweep their respective commit outputs due to the + // additional CLTV. We'll need to mine enough blocks for the timelock to + // expire and prompt their sweep. + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + for _, node := range []*lntest.HarnessNode{alice, bob} { + err = waitForNumChannelPendingForceClose(node, 1, nil) + require.NoError(t.t, err) + } - // Also Carol should have no channels left (open nor pending). - err = waitForNumChannelPendingForceClose(carol, 0, nil) - require.NoError(t.t, err) - assertNodeNumChannels(t, carol, 0) + // Due to the way the test is set up, Alice and Bob share the + // same CLTV for their commit outputs even though it's enforced + // on different channels (Alice-Bob and Bob-Carol). + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := alice.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, resp.PendingForceClosingChannels, 1) + forceCloseChan := resp.PendingForceClosingChannels[0] + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + + // Mine enough blocks for the timelock to expire. + numBlocks := uint32(forceCloseChan.BlocksTilMaturity) + _, err = net.Miner.Client.Generate(numBlocks) + require.NoError(t.t, err) + + // Both Alice and Bob show broadcast their commit sweeps. + aliceCommitOutpoint := wire.OutPoint{Hash: *bobForceClose, Index: 3} + aliceCommitSweep := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + aliceCommitOutpoint, + ) + bobCommitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} + bobCommitSweep := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + bobCommitOutpoint, + ) + + // Confirm their sweeps. + block := mineBlocks(t, net, 1, 2)[0] + assertTxInBlock(t, block, &aliceCommitSweep) + assertTxInBlock(t, block, &bobCommitSweep) + } + + // All nodes should show zero pending and open channels. + for _, node := range []*lntest.HarnessNode{alice, bob, carol} { + err = waitForNumChannelPendingForceClose(node, 0, nil) + require.NoError(t.t, err) + assertNodeNumChannels(t, node, 0) + } // Finally, check that the Alice's payment is correctly marked // succeeded. diff --git a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go index 542483cfc..34a595a7b 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lncfg" @@ -104,23 +103,24 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest, // Bob's force close transaction should now be found in the mempool. If // there are anchors, we also expect Bob's anchor sweep. expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { + hasAnchors := commitTypeHasAnchors(c) + if hasAnchors { expectedTxes = 2 } - - bobFundingTxid, err := lnrpc.GetChanPointFundingTxid(bobChanPoint) - require.NoError(t.t, err) _, err = waitForNTxsInMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, ) require.NoError(t.t, err) - closeTx := getSpendingTxInMempool( - t, net.Miner.Client, minerMempoolTimeout, wire.OutPoint{ - Hash: *bobFundingTxid, - Index: bobChanPoint.OutputIndex, - }, + + bobFundingTxid, err := lnrpc.GetChanPointFundingTxid(bobChanPoint) + require.NoError(t.t, err) + bobChanOutpoint := wire.OutPoint{ + Hash: *bobFundingTxid, + Index: bobChanPoint.OutputIndex, + } + closeTxid := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, bobChanOutpoint, ) - closeTxid := closeTx.TxHash() // Mine a block to confirm the closing transaction. mineBlocks(t, net, 1, expectedTxes) @@ -137,87 +137,86 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest, // With the closing transaction confirmed, we should expect Bob's HTLC // timeout transaction to be broadcast due to the expiry being reached. // If there are anchors, we also expect Carol's anchor sweep now. - txes, err := getNTxsFromMempool( + _, err = getNTxsFromMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, ) require.NoError(t.t, err) - // Lookup the timeout transaction that is expected to spend from the - // closing tx. We distinguish it from a possibly anchor sweep by value. - var htlcTimeout *chainhash.Hash - for _, tx := range txes { - prevOp := tx.TxIn[0].PreviousOutPoint - require.Equal(t.t, closeTxid, prevOp.Hash) - - // Assume that the timeout tx doesn't spend an output of exactly - // the size of the anchor. - if closeTx.TxOut[prevOp.Index].Value != anchorSize { - hash := tx.TxHash() - htlcTimeout = &hash - } + // We'll also obtain the expected HTLC timeout transaction hash. + htlcOutpoint := wire.OutPoint{Hash: closeTxid, Index: 0} + commitOutpoint := wire.OutPoint{Hash: closeTxid, Index: 1} + if hasAnchors { + htlcOutpoint.Index = 2 + commitOutpoint.Index = 3 } - require.NotNil(t.t, htlcTimeout) - - // We'll mine the remaining blocks in order to generate the sweep - // transaction of Bob's commitment output. The commitment was just - // mined at the current tip and the sweep will be broadcast so it can - // be mined at the tip+defaultCSV'th block, so mine one less to be able - // to make mempool assertions. - mineBlocks(t, net, defaultCSV-1, expectedTxes) - - // Check that the sweep spends from the mined commitment. - txes, err = getNTxsFromMempool(net.Miner.Client, 1, minerMempoolTimeout) - require.NoError(t.t, err) - assertAllTxesSpendFrom(t, txes, closeTxid) - - // Bob's pending channel report should show that he has a commitment - // output awaiting sweeping, and also that there's an outgoing HTLC - // output pending. - pendingChansRequest := &lnrpc.PendingChannelsRequest{} - ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) - pendingChanResp, err := bob.PendingChannels(ctxt, pendingChansRequest) - require.NoError(t.t, err) - - require.NotZero(t.t, len(pendingChanResp.PendingForceClosingChannels)) - forceCloseChan := pendingChanResp.PendingForceClosingChannels[0] - require.NotZero(t.t, forceCloseChan.LimboBalance) - require.NotZero(t.t, len(forceCloseChan.PendingHtlcs)) - - // Mine a block to confirm Bob's commit sweep tx and assert it was in - // fact mined. - block := mineBlocks(t, net, 1, 1)[0] - commitSweepTxid := txes[0].TxHash() - assertTxInBlock(t, block, &commitSweepTxid) - - // Mine an additional block to prompt Bob to broadcast their second - // layer sweep due to the CSV on the HTLC timeout output. - mineBlocks(t, net, 1, 0) - assertSpendingTxInMempool( - t, net.Miner.Client, minerMempoolTimeout, wire.OutPoint{ - Hash: *htlcTimeout, - Index: 0, - }, + htlcTimeoutTxid := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, htlcOutpoint, ) - // The block should have confirmed Bob's HTLC timeout transaction. - // Therefore, at this point, there should be no active HTLC's on the - // commitment transaction from Alice -> Bob. - nodes = []*lntest.HarnessNode{alice} + // Mine a block to confirm the expected transactions. + _ = mineBlocks(t, net, 1, expectedTxes) + + // With Bob's HTLC timeout transaction confirmed, there should be no + // active HTLC's on the commitment transaction from Alice -> Bob. err = wait.NoError(func() error { - return assertNumActiveHtlcs(nodes, 0) + return assertNumActiveHtlcs([]*lntest.HarnessNode{alice}, 0) }, defaultTimeout) require.NoError(t.t, err) // At this point, Bob should show that the pending HTLC has advanced to - // the second stage and is to be swept. - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) - pendingChanResp, err = bob.PendingChannels(ctxt, pendingChansRequest) + // the second stage and is ready to be swept once the timelock is up. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + pendingChansRequest := &lnrpc.PendingChannelsRequest{} + pendingChanResp, err := bob.PendingChannels(ctxt, pendingChansRequest) require.NoError(t.t, err) - forceCloseChan = pendingChanResp.PendingForceClosingChannels[0] + require.Equal(t.t, 1, len(pendingChanResp.PendingForceClosingChannels)) + forceCloseChan := pendingChanResp.PendingForceClosingChannels[0] + require.NotZero(t.t, forceCloseChan.LimboBalance) + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + require.Equal(t.t, 1, len(forceCloseChan.PendingHtlcs)) require.Equal(t.t, uint32(2), forceCloseChan.PendingHtlcs[0].Stage) - // Next, we'll mine a final block that should confirm the second-layer - // sweeping transaction. + htlcTimeoutOutpoint := wire.OutPoint{Hash: htlcTimeoutTxid, Index: 0} + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // Since Bob is the initiator of the script-enforced leased + // channel between him and Carol, he will incur an additional + // CLTV on top of the usual CSV delay on any outputs that he can + // sweep back to his wallet. + blocksTilMaturity := uint32(forceCloseChan.BlocksTilMaturity) + mineBlocks(t, net, blocksTilMaturity, 0) + + // Check that the sweep spends the expected inputs. + _ = assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + commitOutpoint, htlcTimeoutOutpoint, + ) + } else { + // Since Bob force closed the channel between him and Carol, he + // will incur the usual CSV delay on any outputs that he can + // sweep back to his wallet. We'll subtract one block from our + // current maturity period to assert on the mempool. + mineBlocks(t, net, uint32(forceCloseChan.BlocksTilMaturity-1), 0) + + // Check that the sweep spends from the mined commitment. + _ = assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, commitOutpoint, + ) + + // Mine a block to confirm Bob's commit sweep tx and assert it + // was in fact mined. + _ = mineBlocks(t, net, 1, 1)[0] + + // Mine an additional block to prompt Bob to broadcast their + // second layer sweep due to the CSV on the HTLC timeout output. + mineBlocks(t, net, 1, 0) + _ = assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + htlcTimeoutOutpoint, + ) + } + + // Next, we'll mine a final block that should confirm the sweeping + // transactions left. _, err = net.Miner.Client.Generate(1) require.NoError(t.t, err) diff --git a/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go index 120f508ef..8ce72a639 100644 --- a/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_receiver_chain_claim_test.go @@ -113,7 +113,8 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, // transaction in order to go to the chain and sweep her HTLC. If there // are anchors, Carol also sweeps hers. expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { + hasAnchors := commitTypeHasAnchors(c) + if hasAnchors { expectedTxes = 2 } _, err = getNTxsFromMempool( @@ -143,15 +144,32 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, err = restartBob() require.NoError(t.t, err) - // After the force close transaction is mined, Carol should broadcast - // her second level HTLC transaction. Bob will broadcast a sweep tx to - // sweep his output in the channel with Carol. When Bob notices Carol's - // second level transaction in the mempool, he will extract the preimage - // and settle the HTLC back off-chain. Bob will also sweep his anchor, - // if present. - expectedTxes = 2 - if c == lnrpc.CommitmentType_ANCHORS { + // After the force close transaction is mined, a series of transactions + // should be broadcast by Bob and Carol. When Bob notices Carol's second + // level transaction in the mempool, he will extract the preimage and + // settle the HTLC back off-chain. + switch c { + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a sweep tx to sweep his output in the channel with + // Carol. + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 2 + + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a sweep tx to sweep his output in the channel with + // Carol, and another sweep tx to sweep his anchor output. + case lnrpc.CommitmentType_ANCHORS: expectedTxes = 3 + + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a sweep tx to sweep his anchor output. Bob's commit + // output can't be swept yet as he's incurring an additional CLTV from + // being the channel initiator of a script-enforced leased channel. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + expectedTxes = 2 + + default: + t.Fatalf("unhandled commitment type %v", c) } txes, err := getNTxsFromMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, @@ -195,8 +213,8 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, }, defaultTimeout) require.NoError(t.t, err) - // If we mine 4 additional blocks, then both outputs should now be - // mature. + // If we mine 4 additional blocks, then Carol can sweep the second level + // HTLC output. _, err = net.Miner.Client.Generate(defaultCSV) require.NoError(t.t, err) @@ -227,6 +245,41 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest, err = checkPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) require.NoError(t.t, err) + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // Bob still has his commit output to sweep to since he incurred + // an additional CLTV from being the channel initiator of a + // script-enforced leased channel, regardless of whether he + // forced closed the channel or not. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + pendingChanResp, err := bob.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t.t, err) + + require.Len(t.t, pendingChanResp.PendingForceClosingChannels, 1) + forceCloseChan := pendingChanResp.PendingForceClosingChannels[0] + require.Positive(t.t, forceCloseChan.LimboBalance) + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + + // TODO: Bob still shows a pending HTLC at this point when he + // shouldn't, as he already extracted the preimage from Carol's + // claim. + // require.Len(t.t, forceCloseChan.PendingHtlcs, 0) + + // Mine enough blocks for Bob's commit output's CLTV to expire + // and sweep it. + _ = mineBlocks(t, net, uint32(forceCloseChan.BlocksTilMaturity), 0) + commitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} + assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, commitOutpoint, + ) + _, err = net.Miner.Client.Generate(1) + require.NoError(t.t, err) + } + + err = waitForNumChannelPendingForceClose(bob, 0, nil) + require.NoError(t.t, err) + // We'll close out the channel between Alice and Bob, then shutdown // carol to conclude the test. closeChannelAndAssertType(t, net, alice, aliceChanPoint, false, false) diff --git a/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go index 784065e38..f1dee3435 100644 --- a/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_remote_chain_claim_test.go @@ -85,9 +85,9 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest // Next, Alice decides that she wants to exit the channel, so she'll // immediately force close the channel by broadcast her commitment // transaction. + hasAnchors := commitTypeHasAnchors(c) aliceForceClose := closeChannelAndAssertType( - t, net, alice, aliceChanPoint, - c == lnrpc.CommitmentType_ANCHORS, true, + t, net, alice, aliceChanPoint, hasAnchors, true, ) // Wait for the channel to be marked pending force close. @@ -97,29 +97,29 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest // After closeChannelAndAssertType returns, it has mined a block so now // bob will attempt to redeem his anchor commitment (if the channel // type is of that type). - if c == lnrpc.CommitmentType_ANCHORS { + if hasAnchors { _, err = waitForNTxsInMempool( net.Miner.Client, 1, minerMempoolTimeout, ) - if err != nil { - t.Fatalf("unable to find bob's anchor commit sweep: %v", - err) - } + require.NoError(t.t, err) } - // Mine enough blocks for Alice to sweep her funds from the force - // closed channel. closeChannelAndAssertType() already mined a block - // containing the commitment tx and the commit sweep tx will be - // broadcast immediately before it can be included in a block, so mine - // one less than defaultCSV in order to perform mempool assertions. - _, err = net.Miner.Client.Generate(defaultCSV - 1) - require.NoError(t.t, err) + if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // Mine enough blocks for Alice to sweep her funds from the + // force closed channel. closeChannelAndAssertType() already + // mined a block containing the commitment tx and the commit + // sweep tx will be broadcast immediately before it can be + // included in a block, so mine one less than defaultCSV in + // order to perform mempool assertions. + _, err = net.Miner.Client.Generate(defaultCSV - 1) + require.NoError(t.t, err) - // Alice should now sweep her funds. - _, err = waitForNTxsInMempool( - net.Miner.Client, 1, minerMempoolTimeout, - ) - require.NoError(t.t, err) + // Alice should now sweep her funds. + _, err = waitForNTxsInMempool( + net.Miner.Client, 1, minerMempoolTimeout, + ) + require.NoError(t.t, err) + } // Suspend bob, so Carol is forced to go on chain. restartBob, err := net.SuspendNode(bob) @@ -140,14 +140,17 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest // We'll now mine enough blocks so Carol decides that she needs to go // on-chain to claim the HTLC as Bob has been inactive. numBlocks := padCLTV(uint32( - invoiceReq.CltvExpiry-lncfg.DefaultIncomingBroadcastDelta, - ) - defaultCSV) + invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, + )) + if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + numBlocks -= defaultCSV + } _, err = net.Miner.Client.Generate(numBlocks) require.NoError(t.t, err) expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { + if hasAnchors { expectedTxes = 2 } @@ -181,14 +184,31 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest err = restartBob() require.NoError(t.t, err) - // After the force close transacion is mined, Carol should broadcast her - // second level HTLC transacion. Bob will broadcast a sweep tx to sweep - // his output in the channel with Carol. He can do this immediately, as - // the output is not timelocked since Carol was the one force closing. - // If there are anchors, Bob should also sweep his. - expectedTxes = 2 - if c == lnrpc.CommitmentType_ANCHORS { + // After the force close transacion is mined, we should expect Bob and + // Carol to broadcast some transactions depending on the channel + // commitment type. + switch c { + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a transaction to sweep his commitment output. + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 2 + + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a transaction to sweep his commitment output and + // another to sweep his anchor output. + case lnrpc.CommitmentType_ANCHORS: expectedTxes = 3 + + // Carol should broadcast her second level HTLC transaction and Bob + // should broadcast a transaction to sweep his anchor output. Bob can't + // sweep his commitment output yet as he has incurred an additional CLTV + // due to being the channel initiator of a force closed script-enforced + // leased channel. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + expectedTxes = 2 + + default: + t.Fatalf("unhandled commitment type %v", c) } txes, err := getNTxsFromMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, @@ -228,10 +248,18 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest carolSecondLevelCSV-- // Now that the sweeping transaction has been confirmed, Bob should now - // recognize that all contracts have been fully resolved, and show no - // pending close channels. - err = waitForNumChannelPendingForceClose(bob, 0, nil) - require.NoError(t.t, err) + // recognize that all contracts for the Bob-Carol channel have been + // fully resolved + aliceBobPendingChansLeft := 0 + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + aliceBobPendingChansLeft = 1 + } + for _, node := range []*lntest.HarnessNode{alice, bob} { + err = waitForNumChannelPendingForceClose( + node, aliceBobPendingChansLeft, nil, + ) + require.NoError(t.t, err) + } // If we then mine 3 additional blocks, Carol's second level tx will // mature, and she should pull the funds. @@ -251,6 +279,53 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest err = waitForNumChannelPendingForceClose(carol, 0, nil) require.NoError(t.t, err) + // With the script-enforced lease commitment type, Alice and Bob still + // haven't been able to sweep their respective commit outputs due to the + // additional CLTV. We'll need to mine enough blocks for the timelock to + // expire and prompt their sweep. + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // Due to the way the test is set up, Alice and Bob share the + // same CLTV for their commit outputs even though it's enforced + // on different channels (Alice-Bob and Bob-Carol). + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := alice.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, resp.PendingForceClosingChannels, 1) + forceCloseChan := resp.PendingForceClosingChannels[0] + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + + // Mine enough blocks for the timelock to expire. + numBlocks := uint32(forceCloseChan.BlocksTilMaturity) + _, err = net.Miner.Client.Generate(numBlocks) + require.NoError(t.t, err) + + // Both Alice and Bob show broadcast their commit sweeps. + aliceCommitOutpoint := wire.OutPoint{Hash: *aliceForceClose, Index: 3} + aliceCommitSweep := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + aliceCommitOutpoint, + ) + bobCommitOutpoint := wire.OutPoint{Hash: closingTxid, Index: 3} + bobCommitSweep := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + bobCommitOutpoint, + ) + + // Confirm their sweeps. + block := mineBlocks(t, net, 1, 2)[0] + assertTxInBlock(t, block, &aliceCommitSweep) + assertTxInBlock(t, block, &bobCommitSweep) + + // Alice and Bob should not show any pending channels anymore as + // they have been fully resolved. + for _, node := range []*lntest.HarnessNode{alice, bob} { + err = waitForNumChannelPendingForceClose(node, 0, nil) + require.NoError(t.t, err) + } + } + // The invoice should show as settled for Carol, indicating that it was // swept on-chain. invoicesReq := &lnrpc.ListInvoiceRequest{} diff --git a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go index c94147304..591aefeb7 100644 --- a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" @@ -72,9 +73,9 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, // Now that all parties have the HTLC locked in, we'll immediately // force close the Bob -> Carol channel. This should trigger contract // resolution mode for both of them. - closeChannelAndAssertType( - t, net, bob, bobChanPoint, - c == lnrpc.CommitmentType_ANCHORS, true, + hasAnchors := commitTypeHasAnchors(c) + closeTx := closeChannelAndAssertType( + t, net, bob, bobChanPoint, hasAnchors, true, ) // At this point, Bob should have a pending force close channel as he @@ -92,27 +93,45 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, ) require.NoError(t.t, err) - // We'll mine defaultCSV blocks in order to generate the sweep - // transaction of Bob's funding output. If there are anchors, mine - // Carol's anchor sweep too. - if c == lnrpc.CommitmentType_ANCHORS { + // If the channel closed has anchors, we should expect to see a sweep + // transaction for Carol's anchor. + htlcOutpoint := wire.OutPoint{Hash: *closeTx, Index: 0} + bobCommitOutpoint := wire.OutPoint{Hash: *closeTx, Index: 1} + if hasAnchors { + htlcOutpoint.Index = 2 + bobCommitOutpoint.Index = 3 _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) require.NoError(t.t, err) } - // The sweep is broadcast on the block immediately before the CSV - // expires and the commitment was already mined inside - // closeChannelAndAssertType(), so mine one block less than defaultCSV - // in order to perform mempool assertions. - _, err = net.Miner.Client.Generate(defaultCSV - 1) - require.NoError(t.t, err) + // Before the HTLC times out, we'll need to assert that Bob broadcasts a + // sweep transaction for his commit output. Note that if the channel has + // a script-enforced lease, then Bob will have to wait for an additional + // CLTV before sweeping it. + if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // The sweep is broadcast on the block immediately before the + // CSV expires and the commitment was already mined inside + // closeChannelAndAssertType(), so mine one block less than + // defaultCSV in order to perform mempool assertions. + _, err = net.Miner.Client.Generate(defaultCSV - 1) + require.NoError(t.t, err) - _, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - require.NoError(t.t, err) + commitSweepTx := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + bobCommitOutpoint, + ) + blocks := mineBlocks(t, net, 1, 1) + assertTxInBlock(t, blocks[0], &commitSweepTx) + } // We'll now mine enough blocks for the HTLC to expire. After this, Bob // should hand off the now expired HTLC output to the utxo nursery. - numBlocks := padCLTV(uint32(finalCltvDelta - defaultCSV)) + numBlocks := padCLTV(finalCltvDelta) + if c != lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + // Subtract the number of blocks already mined to confirm Bob's + // commit sweep. + numBlocks -= defaultCSV + } _, err = net.Miner.Client.Generate(numBlocks) require.NoError(t.t, err) @@ -137,13 +156,14 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, // We should also now find a transaction in the mempool, as Bob should // have broadcast his second layer timeout transaction. - timeoutTx, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) - require.NoError(t.t, err) + timeoutTx := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, htlcOutpoint, + ) // Next, we'll mine an additional block. This should serve to confirm // the second layer timeout transaction. block := mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, timeoutTx) + assertTxInBlock(t, block, &timeoutTx) // With the second layer timeout transaction confirmed, Bob should have // canceled backwards the HTLC that carol sent. @@ -172,19 +192,37 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, ) require.NoError(t.t, err) - // We'll now mine 4 additional blocks. This should be enough for Bob's - // CSV timelock to expire and the sweeping transaction of the HTLC to be - // broadcast. - _, err = net.Miner.Client.Generate(defaultCSV) + // Bob should now broadcast a transaction that sweeps certain inputs + // depending on the commitment type. We'll need to mine some blocks + // before the broadcast is possible. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + resp, err := bob.PendingChannels(ctxt, &lnrpc.PendingChannelsRequest{}) require.NoError(t.t, err) - sweepTx, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) + require.Len(t.t, resp.PendingForceClosingChannels, 1) + forceCloseChan := resp.PendingForceClosingChannels[0] + require.Len(t.t, forceCloseChan.PendingHtlcs, 1) + pendingHtlc := forceCloseChan.PendingHtlcs[0] + require.Positive(t.t, pendingHtlc.BlocksTilMaturity) + numBlocks = uint32(pendingHtlc.BlocksTilMaturity) + + _, err = net.Miner.Client.Generate(numBlocks) require.NoError(t.t, err) - // We'll then mine a final block which should confirm this second layer - // sweep transaction. + // Now that the CSV/CLTV timelock has expired, the transaction should + // either only sweep the HTLC timeout transaction, or sweep both the + // HTLC timeout transaction and Bob's commit output depending on the + // commitment type. + htlcTimeoutOutpoint := wire.OutPoint{Hash: timeoutTx, Index: 0} + expectedInputsSwept := []wire.OutPoint{htlcTimeoutOutpoint} + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + expectedInputsSwept = append(expectedInputsSwept, bobCommitOutpoint) + } + sweepTx := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, expectedInputsSwept..., + ) block = mineBlocks(t, net, 1, 1)[0] - assertTxInBlock(t, block, sweepTx) + assertTxInBlock(t, block, &sweepTx) // At this point, Bob should no longer show any channels as pending // close. diff --git a/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go b/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go index 86b79c1a0..a88368a03 100644 --- a/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_remote_force_close_on_chain_htlc_timeout_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" @@ -83,9 +84,9 @@ func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, // transaction. This will let us exercise that Bob is able to sweep the // expired HTLC on Carol's version of the commitment transaction. If // Carol has an anchor, it will be swept too. - closeChannelAndAssertType( - t, net, carol, bobChanPoint, - c == lnrpc.CommitmentType_ANCHORS, true, + hasAnchors := commitTypeHasAnchors(c) + closeTx := closeChannelAndAssertType( + t, net, carol, bobChanPoint, hasAnchors, true, ) // At this point, Bob should have a pending force close channel as @@ -93,13 +94,25 @@ func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, err = waitForNumChannelPendingForceClose(bob, 1, nil) require.NoError(t.t, err) - // Bob can sweep his output immediately. If there is an anchor, Bob will - // sweep that as well. - expectedTxes := 1 - if c == lnrpc.CommitmentType_ANCHORS { - expectedTxes = 2 - } + var expectedTxes int + switch c { + // Bob can sweep his commit output immediately. + case lnrpc.CommitmentType_LEGACY: + expectedTxes = 1 + // Bob can sweep his commit and anchor outputs immediately. + case lnrpc.CommitmentType_ANCHORS: + expectedTxes = 2 + + // Bob can't sweep his commit output yet as he was the initiator of a + // script-enforced leased channel, so he'll always incur the additional + // CLTV. He can still sweep his anchor output however. + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + expectedTxes = 1 + + default: + t.Fatalf("unhandled commitment type %v", c) + } _, err = waitForNTxsInMempool( net.Miner.Client, expectedTxes, minerMempoolTimeout, ) @@ -169,8 +182,32 @@ func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness, require.NoError(t.t, err) // Now we'll check Bob's pending channel report. Since this was Carol's - // commitment, he doesn't have to wait for any CSV delays. As a result, - // he should show no additional pending transactions. + // commitment, he doesn't have to wait for any CSV delays, but he may + // still need to wait for a CLTV on his commit output to expire + // depending on the commitment type. + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + resp, err := bob.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t.t, err) + + require.Len(t.t, resp.PendingForceClosingChannels, 1) + forceCloseChan := resp.PendingForceClosingChannels[0] + require.Positive(t.t, forceCloseChan.BlocksTilMaturity) + + numBlocks := uint32(forceCloseChan.BlocksTilMaturity) + _, err = net.Miner.Client.Generate(numBlocks) + require.NoError(t.t, err) + + bobCommitOutpoint := wire.OutPoint{Hash: *closeTx, Index: 3} + bobCommitSweep := assertSpendingTxInMempool( + t, net.Miner.Client, minerMempoolTimeout, + bobCommitOutpoint, + ) + block := mineBlocks(t, net, 1, 1)[0] + assertTxInBlock(t, block, &bobCommitSweep) + } err = waitForNumChannelPendingForceClose(bob, 0, nil) require.NoError(t.t, err) diff --git a/lntest/itest/lnd_multi-hop_test.go b/lntest/itest/lnd_multi-hop_test.go index 70154366b..a2b11ab8a 100644 --- a/lntest/itest/lnd_multi-hop_test.go +++ b/lntest/itest/lnd_multi-hop_test.go @@ -12,6 +12,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" ) func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) { @@ -71,6 +72,7 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) { commitTypes := []lnrpc.CommitmentType{ lnrpc.CommitmentType_LEGACY, lnrpc.CommitmentType_ANCHORS, + lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE, } for _, commitType := range commitTypes { @@ -221,11 +223,22 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, // We'll start the test by creating a channel between Alice and Bob, // which will act as the first leg for out multi-hop HTLC. const chanAmt = 1000000 + var aliceFundingShim *lnrpc.FundingShim + var thawHeight uint32 + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + _, minerHeight, err := net.Miner.Client.GetBestBlock() + require.NoError(t.t, err) + thawHeight = uint32(minerHeight + 144) + aliceFundingShim, _, _ = deriveFundingShim( + net, t, alice, bob, chanAmt, thawHeight, true, + ) + } aliceChanPoint := openChannelAndAssert( t, net, alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, CommitmentType: c, + FundingShim: aliceFundingShim, }, ) @@ -262,11 +275,18 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness, // We'll then create a channel from Bob to Carol. After this channel is // open, our topology looks like: A -> B -> C. + var bobFundingShim *lnrpc.FundingShim + if c == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE { + bobFundingShim, _, _ = deriveFundingShim( + net, t, bob, carol, chanAmt, thawHeight, true, + ) + } bobChanPoint := openChannelAndAssert( t, net, bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, CommitmentType: c, + FundingShim: bobFundingShim, }, ) ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) diff --git a/lntest/itest/utils.go b/lntest/itest/utils.go index b0c3da420..84c3d776e 100644 --- a/lntest/itest/utils.go +++ b/lntest/itest/utils.go @@ -208,6 +208,17 @@ func getChanInfo(node *lntest.HarnessNode) (*lnrpc.Channel, error) { return channelInfo.Channels[0], nil } +// commitTypeHasAnchors returns whether commitType uses anchor outputs. +func commitTypeHasAnchors(commitType lnrpc.CommitmentType) bool { + switch commitType { + case lnrpc.CommitmentType_ANCHORS, + lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + return true + default: + return false + } +} + // nodeArgsForCommitType returns the command line flag to supply to enable this // commitment type. func nodeArgsForCommitType(commitType lnrpc.CommitmentType) []string { @@ -218,6 +229,11 @@ func nodeArgsForCommitType(commitType lnrpc.CommitmentType) []string { return []string{} case lnrpc.CommitmentType_ANCHORS: return []string{"--protocol.anchors"} + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + return []string{ + "--protocol.anchors", + "--protocol.script-enforced-lease", + } } return nil @@ -238,7 +254,7 @@ func calcStaticFee(c lnrpc.CommitmentType, numHTLCs int) btcutil.Amount { // the value of the two anchors to the resulting fee the initiator // pays. In addition the fee rate is capped at 10 sat/vbyte for anchor // channels. - if c == lnrpc.CommitmentType_ANCHORS { + if commitTypeHasAnchors(c) { feePerKw = chainfee.SatPerKVByte( lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte * 1000, ).FeePerKWeight()