From e715b8ed1fd6d34dcdaae0b4087d6640a5e88ac1 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 7 Apr 2023 19:10:27 +0800 Subject: [PATCH] itest+lntest: add itest for mempool preimage watch --- itest/list_on_test.go | 8 + itest/lnd_multi-hop_test.go | 288 ++++++++++++++++++++++++++++++++++++ lntest/node/config.go | 4 + 3 files changed, 300 insertions(+) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 1814d6292..ade6a80f6 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -523,4 +523,12 @@ var allTestCases = []*lntest.TestCase{ Name: "channel fundmax", TestFunc: testChannelFundMax, }, + { + Name: "htlc timeout resolver extract preimage remote", + TestFunc: testHtlcTimeoutResolverExtractPreimageRemote, + }, + { + Name: "htlc timeout resolver extract preimage local", + TestFunc: testHtlcTimeoutResolverExtractPreimageLocal, + }, } diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go index 0f562ed9b..61b5cf9fa 100644 --- a/itest/lnd_multi-hop_test.go +++ b/itest/lnd_multi-hop_test.go @@ -1838,3 +1838,291 @@ func createThreeHopNetwork(ht *lntest.HarnessTest, return aliceChanPoint, bobChanPoint, carol } + +// testHtlcTimeoutResolverExtractPreimageRemote tests that in the multi-hop +// setting, Alice->Bob->Carol, when Bob's outgoing HTLC is swept by Carol using +// the 2nd level success tx2nd level success tx, Bob's timeout resolver will +// extract the preimage from the sweep tx found in mempool or blocks(for +// neutrino). The 2nd level success tx is broadcast by Carol and spends the +// outpoint on her commit tx. +func testHtlcTimeoutResolverExtractPreimageRemote(ht *lntest.HarnessTest) { + runMultiHopHtlcClaimTest(ht, runExtraPreimageFromRemoteCommit) +} + +// runExtraPreimageFromRemoteCommit checks that Bob's htlc timeout resolver +// will extract the preimage from the 2nd level success tx broadcast by Carol +// which spends the htlc output on her commitment tx. +func runExtraPreimageFromRemoteCommit(ht *lntest.HarnessTest, + alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { + + // First, we'll create a three hop network: Alice -> Bob -> Carol, with + // Carol refusing to actually settle or directly cancel any HTLC's + // self. + aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( + ht, alice, bob, false, c, zeroConf, + ) + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: 100_000, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + } + eveInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: eveInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // Once the payment sent, Alice should have one outgoing HTLC active. + ht.AssertOutgoingHTLCActive(alice, aliceChanPoint, payHash[:]) + + // Bob should have two HTLCs active. One incoming HTLC from Alice, and + // one outgoing to Carol. + ht.AssertIncomingHTLCActive(bob, aliceChanPoint, payHash[:]) + htlc := ht.AssertOutgoingHTLCActive(bob, bobChanPoint, payHash[:]) + + // Carol should have one incoming HTLC from Bob. + ht.AssertIncomingHTLCActive(carol, bobChanPoint, payHash[:]) + + // 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(stream, lnrpc.Invoice_ACCEPTED) + + // Bob now goes offline so the link between Bob and Carol is broken. + restartBob := ht.SuspendNode(bob) + + // Carol now settles the invoice, since her link with Bob is broken, + // Bob won't know the preimage. + carol.RPC.SettleInvoice(preimage[:]) + + // We'll now mine enough blocks to trigger Carol's broadcast of her + // commitment transaction due to the fact that the HTLC is about to + // timeout. With the default incoming broadcast delta of 10, this + // will be the htlc expiry height minus 10. + numBlocks := padCLTV(uint32( + invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta, + )) + ht.MineBlocks(numBlocks) + + // Carol's force close transaction should now be found in the mempool. + // If there are anchors, we also expect Carol's anchor sweep. We now + // mine a block to confirm Carol's closing transaction. + ht.MineClosingTx(bobChanPoint, c) + + // With the closing transaction confirmed, we should expect Carol's + // HTLC success transaction to be broadcast. + ht.Miner.AssertNumTxsInMempool(1) + + // Restart Bob. Once he finishes syncing the channel state, he should + // notice the force close from Carol. + require.NoError(ht, restartBob()) + + // Get the current height to compute number of blocks to mine to + // trigger the htlc timeout resolver from Bob. + _, height := ht.Miner.GetBestBlock() + + // We'll now mine enough blocks to trigger Bob's timeout resolver. + numBlocks = htlc.ExpirationHeight - uint32(height) - + lncfg.DefaultOutgoingBroadcastDelta + + // Mine empty blocks so Carol's htlc success tx stays in mempool. Once + // the height is reached, Bob's timeout resolver will resolve the htlc + // by extracing the preimage from the mempool. + ht.MineEmptyBlocks(int(numBlocks)) + + // For neutrino backend, the timeout resolver needs to extract the + // preimage from the blocks. + if ht.IsNeutrinoBackend() { + // Mine a block to confirm Carol's 2nd level success tx. + ht.MineBlocksAndAssertNumTxes(1, 1) + } + + // Finally, check that the Alice's payment is marked as succeeded as + // Bob has settled the htlc using the preimage extracted from Carol's + // 2nd level success tx. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) + + // NOTE: for non-standby nodes there's no need to clean up the force + // close as long as the mempool is cleaned. + ht.CleanShutDown() +} + +// testHtlcTimeoutResolverExtractPreimage tests that in the multi-hop setting, +// Alice->Bob->Carol, when Bob's outgoing HTLC is swept by Carol using the +// direct preimage spend, Bob's timeout resolver will extract the preimage from +// the sweep tx found in mempool or blocks(for neutrino). The direct spend tx +// is broadcast by Carol and spends the outpoint on Bob's commit tx. +func testHtlcTimeoutResolverExtractPreimageLocal(ht *lntest.HarnessTest) { + runMultiHopHtlcClaimTest(ht, runExtraPreimageFromLocalCommit) +} + +// runExtraPreimageFromLocalCommit checks that Bob's htlc timeout resolver will +// extract the preimage from the direct spend broadcast by Carol which spends +// the htlc output on Bob's commitment tx. +func runExtraPreimageFromLocalCommit(ht *lntest.HarnessTest, + alice, bob *node.HarnessNode, c lnrpc.CommitmentType, zeroConf bool) { + + // First, we'll create a three hop network: Alice -> Bob -> Carol, with + // Carol refusing to actually settle or directly cancel any HTLC's + // self. + aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork( + ht, alice, bob, false, c, zeroConf, + ) + + // With the network active, we'll now add a new hodl invoice at Carol's + // end. Make sure the cltv expiry delta is large enough, otherwise Bob + // won't send out the outgoing htlc. + preimage := ht.RandomPreimage() + payHash := preimage.Hash() + invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{ + Value: 100_000, + CltvExpiry: finalCltvDelta, + Hash: payHash[:], + } + eveInvoice := carol.RPC.AddHoldInvoice(invoiceReq) + + // Subscribe the invoice. + stream := carol.RPC.SubscribeSingleInvoice(payHash[:]) + + // Now that we've created the invoice, we'll send a single payment from + // Alice to Carol. We won't wait for the response however, as Carol + // will not immediately settle the payment. + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: eveInvoice.PaymentRequest, + TimeoutSeconds: 60, + FeeLimitMsat: noFeeLimitMsat, + } + alice.RPC.SendPayment(req) + + // Once the payment sent, Alice should have one outgoing HTLC active. + ht.AssertOutgoingHTLCActive(alice, aliceChanPoint, payHash[:]) + + // Bob should have two HTLCs active. One incoming HTLC from Alice, and + // one outgoing to Carol. + ht.AssertIncomingHTLCActive(bob, aliceChanPoint, payHash[:]) + htlc := ht.AssertOutgoingHTLCActive(bob, bobChanPoint, payHash[:]) + + // Carol should have one incoming HTLC from Bob. + ht.AssertIncomingHTLCActive(carol, bobChanPoint, payHash[:]) + + // 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(stream, lnrpc.Invoice_ACCEPTED) + + // Bob now goes offline so the link between Bob and Carol is broken. + restartBob := ht.SuspendNode(bob) + + // Carol now settles the invoice, since her link with Bob is broken, + // Bob won't know the preimage. + carol.RPC.SettleInvoice(preimage[:]) + + // Stop Carol so it's easier to check the mempool's state since she + // will broadcast the anchor sweeping once Bob force closes. + restartCarol := ht.SuspendNode(carol) + + // Restart Bob to force close the channel. + require.NoError(ht, restartBob()) + + // Bob force closes the channel, which gets his commitment tx into the + // mempool. + ht.CloseChannelAssertPending(bob, bobChanPoint, true) + + // Mine Bob's force close tx. + closeTx := ht.MineClosingTx(bobChanPoint, c) + + // We'll now mine enough blocks to trigger Carol's sweeping of the htlc + // via the direct spend. With the default incoming broadcast delta of + // 10, this will be the htlc expiry height minus 10. + // + // NOTE: we need to mine 1 fewer block as we've already mined one to + // confirm Bob's force close tx. + numBlocks := padCLTV(uint32( + invoiceReq.CltvExpiry - lncfg.DefaultIncomingBroadcastDelta - 1, + )) + + // Mine empty blocks so it's easier to check Bob's sweeping txes below. + ht.MineEmptyBlocks(int(numBlocks)) + + // Increase the fee rate used by the sweeper so Carol's direct spend tx + // won't be replaced by Bob's timeout tx. + ht.SetFeeEstimate(30000) + + // Restart Carol to sweep the htlc output. + require.NoError(ht, restartCarol()) + + // Construct the htlc output on Bob's commitment tx, and decide its + // index based on the commit type below. + htlcOutpoint := wire.OutPoint{Hash: closeTx.TxHash()} + + // Check the current mempool state and we should see, + // - Carol's direct spend tx. + // - Bob's local output sweep tx, if this is NOT script enforced lease. + // - Carol's anchor sweep tx, if the commitment type is anchor. + switch c { + case lnrpc.CommitmentType_LEGACY: + htlcOutpoint.Index = 0 + ht.Miner.AssertNumTxsInMempool(2) + + case lnrpc.CommitmentType_ANCHORS: + htlcOutpoint.Index = 2 + ht.Miner.AssertNumTxsInMempool(3) + + case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE: + htlcOutpoint.Index = 2 + ht.Miner.AssertNumTxsInMempool(2) + } + + // Get the current height to compute number of blocks to mine to + // trigger the timeout resolver from Bob. + _, height := ht.Miner.GetBestBlock() + + // We'll now mine enough blocks to trigger Bob's htlc timeout resolver + // to act. Once his timeout resolver starts, it will extract the + // preimage from Carol's direct spend tx found in the mempool. + numBlocks = htlc.ExpirationHeight - uint32(height) - + lncfg.DefaultOutgoingBroadcastDelta + + // Decrease the fee rate used by the sweeper so Bob's timeout tx will + // not replace Carol's direct spend tx. + ht.SetFeeEstimate(1000) + + // Mine empty blocks so Carol's direct spend tx stays in mempool. Once + // the height is reached, Bob's timeout resolver will resolve the htlc + // by extracing the preimage from the mempool. + ht.MineEmptyBlocks(int(numBlocks)) + + // For neutrino backend, the timeout resolver needs to extract the + // preimage from the blocks. + if ht.IsNeutrinoBackend() { + // Make sure the direct spend tx is still in the mempool. + ht.Miner.AssertOutpointInMempool(htlcOutpoint) + + // Mine a block to confirm Carol's direct spend tx. + ht.MineBlocks(1) + } + + // Finally, check that the Alice's payment is marked as succeeded as + // Bob has settled the htlc using the preimage extracted from Carol's + // direct spend tx. + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) + + // NOTE: for non-standby nodes there's no need to clean up the force + // close as long as the mempool is cleaned. + ht.CleanShutDown() +} diff --git a/lntest/node/config.go b/lntest/node/config.go index d69c7f80d..f044475e7 100644 --- a/lntest/node/config.go +++ b/lntest/node/config.go @@ -236,6 +236,10 @@ func (cfg *BaseNodeConfig) GenArgs() []string { // Use a small cache duration so the `DescribeGraph` can be // updated quicker. "--caches.rpc-graph-cache-duration=100ms", + + // Speed up the tests for bitcoind backend. + "--bitcoind.blockpollinginterval=100ms", + "--bitcoind.txpollinginterval=100ms", } args = append(args, nodeArgs...)