lnd.xprv/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go
carla d0d0d21573
itest/test: remove hold force close dependency
Our aggregate htlc test depends on our previous behavior
where recipients would allow channels with pending hold
invoice htlcs to force close. Now that we have an expiry
watcher to prevent these force closes, we can't rely on
this for tests because the recipient will cancel the htlcs
back before they expire.
2021-04-23 08:19:58 +02:00

436 lines
13 KiB
Go

package itest
import (
"context"
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"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/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// testMultiHopHtlcAggregation tests that in a multi-hop HTLC scenario, if we
// force close a channel with both incoming and outgoing HTLCs, we can properly
// resolve them using the second level timeout and success transactions. In
// case of anchor channels, the second-level spends can also be aggregated and
// properly feebumped, so we'll check that as well.
func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c commitType) {
const finalCltvDelta = 40
ctxb := context.Background()
// First, we'll create a three hop network: Alice -> Bob -> Carol.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
)
defer shutdownAndAssert(net, t, carol)
// To ensure we have capacity in both directions of the route, we'll
// make a fairly large payment Alice->Carol and settle it.
const reBalanceAmt = 500_000
invoice := &lnrpc.Invoice{
Value: reBalanceAmt,
}
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
resp, err := carol.AddInvoice(ctxt, invoice)
require.NoError(t.t, err)
sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: resp.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
stream, err := alice.RouterClient.SendPaymentV2(ctxt, sendReq)
require.NoError(t.t, err)
result, err := getPaymentResult(stream)
require.NoError(t.t, err)
require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED)
// With the network active, we'll now add a new hodl invoices at both
// Alice's and Carol's end. Make sure the cltv expiry delta is large
// enough, otherwise Bob won't send out the outgoing htlc.
const numInvoices = 5
const invoiceAmt = 50_000
var (
carolInvoices []*invoicesrpc.AddHoldInvoiceResp
aliceInvoices []*invoicesrpc.AddHoldInvoiceResp
alicePreimages []lntypes.Preimage
payHashes [][]byte
alicePayHashes [][]byte
carolPayHashes [][]byte
)
// Add Carol invoices.
for i := 0; i < numInvoices; i++ {
preimage := lntypes.Preimage{1, 1, 1, byte(i)}
payHash := preimage.Hash()
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
Value: invoiceAmt,
CltvExpiry: finalCltvDelta,
Hash: payHash[:],
}
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
carolInvoice, err := carol.AddHoldInvoice(ctxt, invoiceReq)
require.NoError(t.t, err)
carolInvoices = append(carolInvoices, carolInvoice)
payHashes = append(payHashes, payHash[:])
carolPayHashes = append(carolPayHashes, payHash[:])
}
// We'll give Alice's invoices a longer CLTV expiry, to ensure the
// channel Bob<->Carol will be closed first.
for i := 0; i < numInvoices; i++ {
preimage := lntypes.Preimage{2, 2, 2, byte(i)}
payHash := preimage.Hash()
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
Value: invoiceAmt,
CltvExpiry: 2 * finalCltvDelta,
Hash: payHash[:],
}
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
aliceInvoice, err := alice.AddHoldInvoice(ctxt, invoiceReq)
require.NoError(t.t, err)
aliceInvoices = append(aliceInvoices, aliceInvoice)
alicePreimages = append(alicePreimages, preimage)
payHashes = append(payHashes, payHash[:])
alicePayHashes = append(alicePayHashes, payHash[:])
}
// Now that we've created the invoices, we'll pay them all from
// Alice<->Carol, going through Bob. We won't wait for the response
// however, as neither will immediately settle the payment.
ctx, cancel := context.WithCancel(ctxb)
defer cancel()
// Alice will pay all of Carol's invoices.
for _, carolInvoice := range carolInvoices {
_, err = alice.RouterClient.SendPaymentV2(
ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: carolInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
},
)
require.NoError(t.t, err)
}
// And Carol will pay Alice's.
for _, aliceInvoice := range aliceInvoices {
_, err = carol.RouterClient.SendPaymentV2(
ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: aliceInvoice.PaymentRequest,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
},
)
require.NoError(t.t, err)
}
// At this point, all 3 nodes should now the HTLCs active on their
// channels.
nodes := []*lntest.HarnessNode{alice, bob, carol}
err = wait.NoError(func() error {
return assertActiveHtlcs(nodes, payHashes...)
}, defaultTimeout)
require.NoError(t.t, err)
// Wait for Alice and Carol to mark the invoices as accepted. There is
// a small gap to bridge between adding the htlc to the channel and
// executing the exit hop logic.
for _, payHash := range carolPayHashes {
h := lntypes.Hash{}
copy(h[:], payHash)
waitForInvoiceAccepted(t, carol, h)
}
for _, payHash := range alicePayHashes {
h := lntypes.Hash{}
copy(h[:], payHash)
waitForInvoiceAccepted(t, alice, h)
}
// Increase the fee estimate so that the following force close tx will
// be cpfp'ed.
net.SetFeeEstimate(30000)
// We want Carol's htlcs to expire off-chain to demonstrate bob's force
// close. However, Carol will cancel her invoices to prevent force
// closes, so we shut her down for now.
restartCarol, err := net.SuspendNode(carol)
require.NoError(t.t, err)
// We'll now mine enough blocks to trigger Bob's broadcast of his
// commitment transaction due to the fact that the Carol's HTLCs are
// 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(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta),
)
_, err = net.Miner.Client.Generate(numBlocks)
require.NoError(t.t, err)
// 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 == commitTypeAnchors {
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,
},
)
closeTxid := closeTx.TxHash()
// Go through the closing transaction outputs, and make an index for the HTLC outputs.
successOuts := make(map[wire.OutPoint]struct{})
timeoutOuts := make(map[wire.OutPoint]struct{})
for i, txOut := range closeTx.TxOut {
op := wire.OutPoint{
Hash: closeTxid,
Index: uint32(i),
}
switch txOut.Value {
// If this HTLC goes towards Carol, Bob will claim it with a
// timeout Tx. In this case the value will be the invoice
// amount.
case invoiceAmt:
timeoutOuts[op] = struct{}{}
// If the HTLC has direction towards Alice, Bob will
// claim it with the success TX when he learns the preimage. In
// this case one extra sat will be on the output, because of
// the routing fee.
case invoiceAmt + 1:
successOuts[op] = struct{}{}
}
}
// Once bob has force closed, we can restart carol.
require.NoError(t.t, restartCarol())
// Mine a block to confirm the closing transaction.
mineBlocks(t, net, 1, expectedTxes)
time.Sleep(1 * time.Second)
// Let Alice settle her invoices. When Bob now gets the preimages, he
// has no other option than to broadcast his second-level transactions
// to claim the money.
for _, preimage := range alicePreimages {
ctx, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
_, err = alice.SettleInvoice(ctx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
require.NoError(t.t, err)
}
// 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
// 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 == commitTypeAnchors {
expectedTxes = 4
}
txes, err := getNTxsFromMempool(
net.Miner.Client, expectedTxes, minerMempoolTimeout,
)
require.NoError(t.t, err)
// Since Bob can aggregate the transactions, we expect a single
// transaction, that have multiple spends from the commitment.
var (
timeoutTxs []*chainhash.Hash
successTxs []*chainhash.Hash
)
for _, tx := range txes {
txid := tx.TxHash()
for i := range tx.TxIn {
prevOp := tx.TxIn[i].PreviousOutPoint
if _, ok := successOuts[prevOp]; ok {
successTxs = append(successTxs, &txid)
break
}
if _, ok := timeoutOuts[prevOp]; ok {
timeoutTxs = append(timeoutTxs, &txid)
break
}
}
}
// 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 == commitTypeAnchors {
require.Len(t.t, timeoutTxs, 1)
require.Len(t.t, successTxs, 1)
} else {
require.Len(t.t, timeoutTxs, numInvoices)
require.Len(t.t, successTxs, numInvoices)
}
// All mempool transactions should be spending from the commitment
// transaction.
assertAllTxesSpendFrom(t, txes, closeTxid)
// Mine a block to confirm the transactions.
block := mineBlocks(t, net, 1, expectedTxes)[0]
require.Len(t.t, block.Transactions, expectedTxes+1)
// At this point, Bob should have broadcast his second layer success
// transaction, and should have sent it to the nursery for incubation,
// or to the sweeper for sweeping.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = waitForNumChannelPendingForceClose(
ctxt, bob, 1, func(c *lnrpcForceCloseChannel) error {
if c.Channel.LocalBalance != 0 {
return nil
}
if len(c.PendingHtlcs) != 1 {
return fmt.Errorf("bob should have pending " +
"htlc but doesn't")
}
if c.PendingHtlcs[0].Stage != 1 {
return fmt.Errorf("bob's htlc should have "+
"advanced to the first stage but was "+
"stage: %v", c.PendingHtlcs[0].Stage)
}
return nil
},
)
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)
// 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,
)
// 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")
}
}
}
switch {
// Mining one additional block, Bob's second level tx is mature, and he
// can sweep the output.
case c == commitTypeAnchors:
_ = 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.
default:
_ = mineBlocks(t, net, 2, 1)
}
bobSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
// Make sure it spends from the second level tx.
secondLevelSweep, err := net.Miner.Client.GetRawTransaction(bobSweep)
require.NoError(t.t, err)
// It should be sweeping all the second-level outputs.
var secondLvlSpends int
for _, txin := range secondLevelSweep.MsgTx().TxIn {
for _, timeoutTx := range timeoutTxs {
if *timeoutTx == txin.PreviousOutPoint.Hash {
secondLvlSpends++
}
}
for _, successTx := range successTxs {
if *successTx == txin.PreviousOutPoint.Hash {
secondLvlSpends++
}
}
}
require.Equal(t.t, 2*numInvoices, secondLvlSpends)
// When we mine one additional block, that will confirm Bob's second
// level sweep. Now Bob should have no pending channels anymore, as
// this just resolved it by the confirmation of the sweep transaction.
block = mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, bobSweep)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = waitForNumChannelPendingForceClose(ctxt, bob, 0, nil)
require.NoError(t.t, err)
// THe channel with Alice is still open.
assertNodeNumChannels(t, bob, 1)
// Carol should have no channels left (open nor pending).
err = waitForNumChannelPendingForceClose(ctxt, carol, 0, nil)
require.NoError(t.t, err)
assertNodeNumChannels(t, carol, 0)
// Coop close channel, expect no anchors.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssertType(
ctxt, t, net, alice, aliceChanPoint, false, false,
)
}