lnd/itest/lnd_htlc_timeout_resolver_test.go
2024-12-20 17:54:12 +08:00

382 lines
13 KiB
Go

package itest
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/chainreg"
"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/routing"
"github.com/stretchr/testify/require"
)
const (
finalCltvDelta = routing.MinCLTVDelta // 18.
thawHeightDelta = finalCltvDelta * 2 // 36.
)
// makeRouteHints creates a route hints that will allow Carol to be reached
// using an unadvertised channel created by Bob (Bob -> Carol). If the zeroConf
// bool is set, then the scid alias of Bob will be used in place.
func makeRouteHints(bob, carol *node.HarnessNode,
zeroConf bool) []*lnrpc.RouteHint {
carolChans := carol.RPC.ListChannels(
&lnrpc.ListChannelsRequest{},
)
carolChan := carolChans.Channels[0]
hopHint := &lnrpc.HopHint{
NodeId: carolChan.RemotePubkey,
ChanId: carolChan.ChanId,
FeeBaseMsat: uint32(
chainreg.DefaultBitcoinBaseFeeMSat,
),
FeeProportionalMillionths: uint32(
chainreg.DefaultBitcoinFeeRate,
),
CltvExpiryDelta: chainreg.DefaultBitcoinTimeLockDelta,
}
if zeroConf {
bobChans := bob.RPC.ListChannels(
&lnrpc.ListChannelsRequest{},
)
// Now that we have Bob's channels, scan for the channel he has
// open to Carol so we can use the proper scid.
var found bool
for _, bobChan := range bobChans.Channels {
if bobChan.RemotePubkey == carol.PubKeyStr {
hopHint.ChanId = bobChan.AliasScids[0]
found = true
break
}
}
if !found {
bob.Fatalf("unable to create route hint")
}
}
return []*lnrpc.RouteHint{
{
HopHints: []*lnrpc.HopHint{hopHint},
},
}
}
// 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. The 2nd level
// success tx is broadcast by Carol and spends the outpoint on her commit tx.
func testHtlcTimeoutResolverExtractPreimageRemote(ht *lntest.HarnessTest) {
// For neutrino backend there's no mempool source so we skip it. The
// test of extracting preimage from blocks has already been covered in
// other tests.
if ht.IsNeutrinoBackend() {
ht.Skip("skipping neutrino")
}
// Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor
// is never swept.
//
// TODO(yy): delete this line once the normal anchor sweeping is
// removed.
ht.SetMinRelayFeerate(10_000)
// Create a three hop network: Alice -> Bob -> Carol, using
// anchor channels.
//
// Prepare params.
params := lntest.OpenChannelParams{Amt: chanAmt}
cfg := node.CfgAnchor
cfgs := [][]string{cfg, cfg, cfg}
// First, we'll create a three hop network: Alice -> Bob -> Carol, with
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params)
alice, bob, carol := nodes[0], nodes[1], nodes[2]
aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1]
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
// 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,
}
ht.SendPaymentAssertInflight(alice, 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 - incomingBroadcastDelta,
))
ht.MineBlocks(int(numBlocks))
// Mine the two txns made from Carol,
// - the force close tx.
// - the anchor sweeping tx.
ht.MineBlocksAndAssertNumTxes(1, 2)
// With the closing transaction confirmed, we should expect Carol's
// HTLC success transaction to be offered to the sweeper. along with her
// anchor output. Note that the anchor output is uneconomical to sweep.
ht.AssertNumPendingSweeps(carol, 1)
// We should now have Carol's htlc success tx in the mempool.
ht.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.CurrentHeight()
// We'll now mine enough blocks to trigger Bob's timeout resolver.
numBlocks = htlc.ExpirationHeight - 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))
// 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)
// Mine a block to clean the mempool.
ht.MineBlocksAndAssertNumTxes(1, 2)
// 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. The direct spend tx is broadcast by Carol and
// spends the outpoint on Bob's commit tx.
func testHtlcTimeoutResolverExtractPreimageLocal(ht *lntest.HarnessTest) {
// For neutrino backend there's no mempool source so we skip it. The
// test of extracting preimage from blocks has already been covered in
// other tests.
if ht.IsNeutrinoBackend() {
ht.Skip("skipping neutrino")
}
// Set the min relay feerate to be 10 sat/vbyte so the non-CPFP anchor
// is never swept.
//
// TODO(yy): delete this line once the normal anchor sweeping is
// removed.
ht.SetMinRelayFeerate(10_000)
// Create a three hop network: Alice -> Bob -> Carol, using
// anchor channels.
//
// Prepare params.
params := lntest.OpenChannelParams{Amt: chanAmt}
cfg := node.CfgAnchor
cfgs := [][]string{cfg, cfg, cfg}
// First, we'll create a three hop network: Alice -> Bob -> Carol, with
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
// Create a three hop network: Alice -> Bob -> Carol.
chanPoints, nodes := ht.CreateSimpleNetwork(cfgs, params)
alice, bob, carol := nodes[0], nodes[1], nodes[2]
aliceChanPoint, bobChanPoint := chanPoints[0], chanPoints[1]
// 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[:],
}
carolInvoice := carol.RPC.AddHoldInvoice(invoiceReq)
// Record the height which the invoice will expire.
invoiceExpiry := ht.CurrentHeight() + uint32(invoiceReq.CltvExpiry)
// 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: carolInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
}
ht.SendPaymentAssertInflight(alice, 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[:])
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.
ht.MineClosingTx(bobChanPoint)
// Once Bob's force closing tx is confirmed, he will re-offer the
// anchor output to his sweeper, which won't be swept due to it being
// uneconomical.
ht.AssertNumPendingSweeps(bob, 1)
// Mine 3 blocks so the output will be offered to the sweeper.
ht.MineBlocks(defaultCSV - 1)
// Bob should have two pending sweeps now,
// - the commit output.
// - the anchor output, uneconomical.
ht.AssertNumPendingSweeps(bob, 2)
// Mine a block to confirm Bob's sweeping tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
ht.Logf("Invoice expire height: %d, current: %d", invoiceExpiry,
ht.CurrentHeight())
// We'll now mine enough blocks to trigger Carol's sweeping of the htlc
// via the direct spend.
numBlocks := padCLTV(
invoiceExpiry - ht.CurrentHeight() - incomingBroadcastDelta,
)
ht.MineBlocks(int(numBlocks))
// Restart Carol to sweep the htlc output.
require.NoError(ht, restartCarol())
// With the above blocks mined, we should expect Carol's to offer the
// htlc output on Bob's commitment to the sweeper.
//
// Carol should two pending sweeps,
// - htlc output.
// - anchor output, uneconomical.
ht.AssertNumPendingSweeps(carol, 2)
// Check the current mempool state and we should see,
// - Carol's direct spend tx, which contains the preimage.
// - Carol's anchor sweep tx cannot be broadcast as it's uneconomical.
ht.AssertNumTxsInMempool(1)
// 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.
resp := ht.AssertNumPendingForceClose(bob, 1)[0]
require.Equal(ht, 1, len(resp.PendingHtlcs))
ht.Logf("Bob's timelock to_local output=%v, timelock on second stage "+
"htlc=%v", resp.BlocksTilMaturity,
resp.PendingHtlcs[0].BlocksTilMaturity)
// 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.
//
// TODO(yy): there's no need to wait till the HTLC's CLTV is reached,
// Bob's outgoing contest resolver can also monitor the mempool and
// resolve the payment even earlier.
ht.MineEmptyBlocks(int(resp.PendingHtlcs[0].BlocksTilMaturity))
// 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()
}