itest: move channel force close test into one file
This commit is contained in:
parent
e3f2cf1711
commit
3f78a5da10
@ -1,13 +1,18 @@
|
|||||||
package itest
|
package itest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/blockchain"
|
"github.com/btcsuite/btcd/blockchain"
|
||||||
"github.com/btcsuite/btcd/integration/rpctest"
|
"github.com/btcsuite/btcd/integration/rpctest"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/go-errors/errors"
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
@ -18,8 +23,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(yy): move channel force closed related tests into this file.
|
|
||||||
|
|
||||||
// testCommitmentTransactionDeadline tests that the anchor sweep transaction is
|
// testCommitmentTransactionDeadline tests that the anchor sweep transaction is
|
||||||
// taking account of the deadline of the commitment transaction. It tests two
|
// taking account of the deadline of the commitment transaction. It tests two
|
||||||
// scenarios:
|
// scenarios:
|
||||||
@ -220,3 +223,1369 @@ func calculateTxnsFeeRate(t *testing.T,
|
|||||||
|
|
||||||
return feeRate
|
return feeRate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testChannelForceClosure performs a test to exercise the behavior of "force"
|
||||||
|
// closing a channel or unilaterally broadcasting the latest local commitment
|
||||||
|
// state on-chain. The test creates a new channel between Alice and Carol, then
|
||||||
|
// force closes the channel after some cursory assertions. Within the test, a
|
||||||
|
// total of 3 + n transactions will be broadcast, representing the commitment
|
||||||
|
// transaction, a transaction sweeping the local CSV delayed output, a
|
||||||
|
// transaction sweeping the CSV delayed 2nd-layer htlcs outputs, and n
|
||||||
|
// htlc timeout transactions, where n is the number of payments Alice attempted
|
||||||
|
// to send to Carol. This test includes several restarts to ensure that the
|
||||||
|
// transaction output states are persisted throughout the forced closure
|
||||||
|
// process.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): also add an unsettled HTLC before force closing.
|
||||||
|
func testChannelForceClosure(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
// We'll test the scenario for some of the commitment types, to ensure
|
||||||
|
// outputs can be swept.
|
||||||
|
commitTypes := []commitType{
|
||||||
|
commitTypeLegacy,
|
||||||
|
commitTypeAnchors,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channelType := range commitTypes {
|
||||||
|
testName := fmt.Sprintf("committype=%v", channelType)
|
||||||
|
logLine := fmt.Sprintf(
|
||||||
|
"---- channel force close subtest %s ----\n",
|
||||||
|
testName,
|
||||||
|
)
|
||||||
|
AddToNodeLog(t.t, net.Alice, logLine)
|
||||||
|
|
||||||
|
channelType := channelType
|
||||||
|
success := t.t.Run(testName, func(t *testing.T) {
|
||||||
|
ht := newHarnessTest(t, net)
|
||||||
|
|
||||||
|
args := channelType.Args()
|
||||||
|
alice := net.NewNode(ht.t, "Alice", args)
|
||||||
|
defer shutdownAndAssert(net, ht, alice)
|
||||||
|
|
||||||
|
// Since we'd like to test failure scenarios with
|
||||||
|
// outstanding htlcs, we'll introduce another node into
|
||||||
|
// our test network: Carol.
|
||||||
|
carolArgs := []string{"--hodl.exit-settle"}
|
||||||
|
carolArgs = append(carolArgs, args...)
|
||||||
|
carol := net.NewNode(ht.t, "Carol", carolArgs)
|
||||||
|
defer shutdownAndAssert(net, ht, carol)
|
||||||
|
|
||||||
|
// Each time, we'll send Alice new set of coins in
|
||||||
|
// order to fund the channel.
|
||||||
|
ctxt, _ := context.WithTimeout(
|
||||||
|
context.Background(), defaultTimeout,
|
||||||
|
)
|
||||||
|
net.SendCoins(ctxt, t, btcutil.SatoshiPerBitcoin, alice)
|
||||||
|
|
||||||
|
// Also give Carol some coins to allow her to sweep her
|
||||||
|
// anchor.
|
||||||
|
net.SendCoins(ctxt, t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
channelForceClosureTest(
|
||||||
|
net, ht, alice, carol, channelType,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
||||||
|
alice, carol *lntest.HarnessNode, channelType commitType) {
|
||||||
|
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
chanAmt = btcutil.Amount(10e6)
|
||||||
|
pushAmt = btcutil.Amount(5e6)
|
||||||
|
paymentAmt = 100000
|
||||||
|
numInvoices = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
const commitFeeRate = 20000
|
||||||
|
net.SetFeeEstimate(commitFeeRate)
|
||||||
|
|
||||||
|
// TODO(roasbeef): should check default value in config here
|
||||||
|
// instead, or make delay a param
|
||||||
|
defaultCLTV := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
||||||
|
|
||||||
|
// We must let Alice have an open channel before she can send a node
|
||||||
|
// announcement, so we open a channel with Carol,
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, alice, carol)
|
||||||
|
|
||||||
|
// Before we start, obtain Carol's current wallet balance, we'll check
|
||||||
|
// to ensure that at the end of the force closure by Alice, Carol
|
||||||
|
// recognizes his new on-chain output.
|
||||||
|
carolBalReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalResp, err := carol.WalletBalance(ctxt, carolBalReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
carolStartingBalance := carolBalResp.ConfirmedBalance
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: pushAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait for Alice and Carol to receive the channel edge from the
|
||||||
|
// funding manager.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the alice->carol channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the alice->carol channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send payments from Alice to Carol, since Carol is htlchodl mode, the
|
||||||
|
// htlc outputs should be left unsettled, and should be swept by the
|
||||||
|
// utxo nursery.
|
||||||
|
carolPubKey := carol.PubKey[:]
|
||||||
|
for i := 0; i < numInvoices; i++ {
|
||||||
|
ctx, cancel := context.WithCancel(ctxb)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := alice.RouterClient.SendPaymentV2(
|
||||||
|
ctx,
|
||||||
|
&routerrpc.SendPaymentRequest{
|
||||||
|
Dest: carolPubKey,
|
||||||
|
Amt: int64(paymentAmt),
|
||||||
|
PaymentHash: makeFakePayHash(t),
|
||||||
|
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send alice htlc: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the HTLC has cleared, all the nodes n our mini network should
|
||||||
|
// show that the HTLC has been locked in.
|
||||||
|
nodes := []*lntest.HarnessNode{alice, carol}
|
||||||
|
var predErr error
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
predErr = assertNumActiveHtlcs(nodes, numInvoices)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("htlc mismatch: %v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch starting height of this test so we can compute the block
|
||||||
|
// heights we expect certain events to take place.
|
||||||
|
_, curHeight, err := net.Miner.Client.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get best block height")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using the current height of the chain, derive the relevant heights
|
||||||
|
// for incubating two-stage htlcs.
|
||||||
|
var (
|
||||||
|
startHeight = uint32(curHeight)
|
||||||
|
commCsvMaturityHeight = startHeight + 1 + defaultCSV
|
||||||
|
htlcExpiryHeight = padCLTV(startHeight + defaultCLTV)
|
||||||
|
htlcCsvMaturityHeight = padCLTV(startHeight + defaultCLTV + 1 + defaultCSV)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we are dealing with an anchor channel type, the sweeper will
|
||||||
|
// sweep the HTLC second level output one block earlier (than the
|
||||||
|
// nursery that waits an additional block, and handles non-anchor
|
||||||
|
// channels). So we set a maturity height that is one less.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
htlcCsvMaturityHeight = padCLTV(
|
||||||
|
startHeight + defaultCLTV + defaultCSV,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceChan, err := getChanInfo(ctxt, alice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get alice's channel info: %v", err)
|
||||||
|
}
|
||||||
|
if aliceChan.NumUpdates == 0 {
|
||||||
|
t.Fatalf("alice should see at least one update to her channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel is open and we have unsettled htlcs, immediately
|
||||||
|
// execute a force closure of the channel. This will also assert that
|
||||||
|
// the commitment transaction was immediately broadcast in order to
|
||||||
|
// fulfill the force closure request.
|
||||||
|
const actualFeeRate = 30000
|
||||||
|
net.SetFeeEstimate(actualFeeRate)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
_, closingTxID, err := net.CloseChannel(ctxt, alice, chanPoint, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to execute force channel closure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel has been force closed, it should show up in the
|
||||||
|
// PendingChannels RPC under the waiting close section.
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(ctxt, pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for pending channels: %v", err)
|
||||||
|
}
|
||||||
|
err = checkNumWaitingCloseChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the outpoint of the channel, which we will use repeatedly to
|
||||||
|
// locate the pending channel information in the rpc responses.
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
op := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingClose, err := findWaitingCloseChannel(pendingChanResp, &op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately after force closing, all of the funds should be in limbo.
|
||||||
|
if waitingClose.LimboBalance == 0 {
|
||||||
|
t.Fatalf("all funds should still be in limbo")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of outpoints to expected resolutions for alice and carol
|
||||||
|
// which we will add reports to as we sweep outputs.
|
||||||
|
var (
|
||||||
|
aliceReports = make(map[string]*lnrpc.Resolution)
|
||||||
|
carolReports = make(map[string]*lnrpc.Resolution)
|
||||||
|
)
|
||||||
|
|
||||||
|
// The several restarts in this test are intended to ensure that when a
|
||||||
|
// channel is force-closed, the UTXO nursery has persisted the state of
|
||||||
|
// the channel in the closure process and will recover the correct state
|
||||||
|
// when the system comes back on line. This restart tests state
|
||||||
|
// persistence at the beginning of the process, when the commitment
|
||||||
|
// transaction has been broadcast but not yet confirmed in a block.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a block which should confirm the commitment transaction
|
||||||
|
// broadcast as a result of the force closure. If there are anchors, we
|
||||||
|
// also expect the anchor sweep tx to be in the mempool.
|
||||||
|
expectedTxes := 1
|
||||||
|
expectedFeeRate := commitFeeRate
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
expectedTxes = 2
|
||||||
|
expectedFeeRate = actualFeeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTxns, err := getNTxsFromMempool(
|
||||||
|
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find commitment in miner mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify fee rate of the commitment tx plus anchor if present.
|
||||||
|
var totalWeight, totalFee int64
|
||||||
|
for _, tx := range sweepTxns {
|
||||||
|
utx := btcutil.NewTx(tx)
|
||||||
|
totalWeight += blockchain.GetTransactionWeight(utx)
|
||||||
|
|
||||||
|
fee, err := getTxFee(net.Miner.Client, tx)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
totalFee += int64(fee)
|
||||||
|
}
|
||||||
|
feeRate := totalFee * 1000 / totalWeight
|
||||||
|
|
||||||
|
// Allow some deviation because weight estimates during tx generation
|
||||||
|
// are estimates.
|
||||||
|
require.InEpsilon(t.t, expectedFeeRate, feeRate, 0.005)
|
||||||
|
|
||||||
|
// Find alice's commit sweep and anchor sweep (if present) in the
|
||||||
|
// mempool.
|
||||||
|
aliceCloseTx := waitingClose.Commitments.LocalTxid
|
||||||
|
_, aliceAnchor := findCommitAndAnchor(
|
||||||
|
t, net, sweepTxns, aliceCloseTx,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we expect anchors, add alice's anchor to our expected set of
|
||||||
|
// reports.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
aliceReports[aliceAnchor.OutPoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||||
|
SweepTxid: aliceAnchor.SweepTx,
|
||||||
|
Outpoint: &lnrpc.OutPoint{
|
||||||
|
TxidBytes: aliceAnchor.OutPoint.Hash[:],
|
||||||
|
TxidStr: aliceAnchor.OutPoint.Hash.String(),
|
||||||
|
OutputIndex: aliceAnchor.OutPoint.Index,
|
||||||
|
},
|
||||||
|
AmountSat: uint64(anchorSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := net.Miner.Client.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the commitment has been confirmed, the channel should be
|
||||||
|
// marked as force closed.
|
||||||
|
err = wait.NoError(func() error {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel has been force closed, it should now
|
||||||
|
// have the height and number of blocks to confirm populated.
|
||||||
|
err = checkCommitmentMaturity(
|
||||||
|
forceClose, commCsvMaturityHeight, int32(defaultCSV),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// None of our outputs have been swept, so they should all be in
|
||||||
|
// limbo. For anchors, we expect the anchor amount to be
|
||||||
|
// recovered.
|
||||||
|
if forceClose.LimboBalance == 0 {
|
||||||
|
return errors.New("all funds should still be in " +
|
||||||
|
"limbo")
|
||||||
|
}
|
||||||
|
expectedRecoveredBalance := int64(0)
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
expectedRecoveredBalance = anchorSize
|
||||||
|
}
|
||||||
|
if forceClose.RecoveredBalance != expectedRecoveredBalance {
|
||||||
|
return errors.New("no funds should yet be shown " +
|
||||||
|
"as recovered")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following restart is intended to ensure that outputs from the
|
||||||
|
// force close commitment transaction have been persisted once the
|
||||||
|
// transaction has been confirmed, but before the outputs are spendable
|
||||||
|
// (the "kindergarten" bucket.)
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol's sweep tx should be in the mempool already, as her output is
|
||||||
|
// not timelocked. If there are anchors, we also expect Carol's anchor
|
||||||
|
// sweep now.
|
||||||
|
sweepTxns, err = getNTxsFromMempool(
|
||||||
|
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find Carol's sweep in miner mempool: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the total fee Carol paid.
|
||||||
|
var totalFeeCarol btcutil.Amount
|
||||||
|
for _, tx := range sweepTxns {
|
||||||
|
fee, err := getTxFee(net.Miner.Client, tx)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
totalFeeCarol += fee
|
||||||
|
}
|
||||||
|
|
||||||
|
// We look up the sweep txns we have found in mempool and create
|
||||||
|
// expected resolutions for carol.
|
||||||
|
carolCommit, carolAnchor := findCommitAndAnchor(
|
||||||
|
t, net, sweepTxns, aliceCloseTx,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we have anchors, add an anchor resolution for carol.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
carolReports[carolAnchor.OutPoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||||
|
SweepTxid: carolAnchor.SweepTx,
|
||||||
|
AmountSat: anchorSize,
|
||||||
|
Outpoint: &lnrpc.OutPoint{
|
||||||
|
TxidBytes: carolAnchor.OutPoint.Hash[:],
|
||||||
|
TxidStr: carolAnchor.OutPoint.Hash.String(),
|
||||||
|
OutputIndex: carolAnchor.OutPoint.Index,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently within the codebase, the default CSV is 4 relative blocks.
|
||||||
|
// For the persistence test, we generate two blocks, then trigger
|
||||||
|
// a restart and then generate the final block that should trigger
|
||||||
|
// the creation of the sweep transaction.
|
||||||
|
if _, err := net.Miner.Client.Generate(defaultCSV - 2); err != nil {
|
||||||
|
t.Fatalf("unable to mine blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following restart checks to ensure that outputs in the
|
||||||
|
// kindergarten bucket are persisted while waiting for the required
|
||||||
|
// number of confirmations to be reported.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice should see the channel in her set of pending force closed
|
||||||
|
// channels with her funds still in limbo.
|
||||||
|
var aliceBalance int64
|
||||||
|
err = wait.NoError(func() error {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
forceClose, err := findForceClosedChannel(
|
||||||
|
pendingChanResp, &op,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a record of the balances we expect for alice and carol.
|
||||||
|
aliceBalance = forceClose.Channel.LocalBalance
|
||||||
|
|
||||||
|
// At this point, the nursery should show that the commitment
|
||||||
|
// output has 2 block left before its CSV delay expires. In
|
||||||
|
// total, we have mined exactly defaultCSV blocks, so the htlc
|
||||||
|
// outputs should also reflect that this many blocks have
|
||||||
|
// passed.
|
||||||
|
err = checkCommitmentMaturity(
|
||||||
|
forceClose, commCsvMaturityHeight, 2,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// All funds should still be shown in limbo.
|
||||||
|
if forceClose.LimboBalance == 0 {
|
||||||
|
return errors.New("all funds should still be in " +
|
||||||
|
"limbo")
|
||||||
|
}
|
||||||
|
expectedRecoveredBalance := int64(0)
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
expectedRecoveredBalance = anchorSize
|
||||||
|
}
|
||||||
|
if forceClose.RecoveredBalance != expectedRecoveredBalance {
|
||||||
|
return errors.New("no funds should yet be shown " +
|
||||||
|
"as recovered")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an additional block, which should cause the CSV delayed
|
||||||
|
// output from the commitment txn to expire.
|
||||||
|
if _, err := net.Miner.Client.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to mine blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the CSV will expire in the next block, meaning that
|
||||||
|
// the sweeping transaction should now be broadcast. So we fetch the
|
||||||
|
// node's mempool to ensure it has been properly broadcast.
|
||||||
|
sweepingTXID, err := waitForTxInMempool(
|
||||||
|
net.Miner.Client, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the sweep transaction, all input it's spending should be from
|
||||||
|
// the commitment transaction which was broadcast on-chain.
|
||||||
|
sweepTx, err := net.Miner.Client.GetRawTransaction(sweepingTXID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch sweep tx: %v", err)
|
||||||
|
}
|
||||||
|
for _, txIn := range sweepTx.MsgTx().TxIn {
|
||||||
|
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
||||||
|
t.Fatalf("sweep transaction not spending from commit "+
|
||||||
|
"tx %v, instead spending %v",
|
||||||
|
closingTxID, txIn.PreviousOutPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect a resolution which spends our commit output.
|
||||||
|
output := sweepTx.MsgTx().TxIn[0].PreviousOutPoint
|
||||||
|
aliceReports[output.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_COMMIT,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||||
|
SweepTxid: sweepingTXID.String(),
|
||||||
|
Outpoint: &lnrpc.OutPoint{
|
||||||
|
TxidBytes: output.Hash[:],
|
||||||
|
TxidStr: output.Hash.String(),
|
||||||
|
OutputIndex: output.Index,
|
||||||
|
},
|
||||||
|
AmountSat: uint64(aliceBalance),
|
||||||
|
}
|
||||||
|
|
||||||
|
carolReports[carolCommit.OutPoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_COMMIT,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
||||||
|
Outpoint: &lnrpc.OutPoint{
|
||||||
|
TxidBytes: carolCommit.OutPoint.Hash[:],
|
||||||
|
TxidStr: carolCommit.OutPoint.Hash.String(),
|
||||||
|
OutputIndex: carolCommit.OutPoint.Index,
|
||||||
|
},
|
||||||
|
AmountSat: uint64(pushAmt),
|
||||||
|
SweepTxid: carolCommit.SweepTx,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we can find the commitment sweep in our set of known
|
||||||
|
// sweeps, using the simple transaction id ListSweeps output.
|
||||||
|
assertSweepFound(ctxb, t.t, alice, sweepingTXID.String(), false)
|
||||||
|
|
||||||
|
// Restart Alice to ensure that she resumes watching the finalized
|
||||||
|
// commitment sweep txid.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we mine an additional block which should include the sweep
|
||||||
|
// transaction as the input scripts and the sequence locks on the
|
||||||
|
// inputs should be properly met.
|
||||||
|
blockHash, err := net.Miner.Client.Generate(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
block, err := net.Miner.Client.GetBlock(blockHash[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTxInBlock(t, block, sweepTx.Hash())
|
||||||
|
|
||||||
|
// Update current height
|
||||||
|
_, curHeight, err = net.Miner.Client.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get best block height")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
// Now that the commit output has been fully swept, check to see
|
||||||
|
// that the channel remains open for the pending htlc outputs.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The commitment funds will have been recovered after the
|
||||||
|
// commit txn was included in the last block. The htlc funds
|
||||||
|
// will be shown in limbo.
|
||||||
|
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
predErr = checkPendingHtlcStageAndMaturity(
|
||||||
|
forceClose, 1, htlcExpiryHeight,
|
||||||
|
int32(htlcExpiryHeight)-curHeight,
|
||||||
|
)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if forceClose.LimboBalance == 0 {
|
||||||
|
predErr = fmt.Errorf("expected funds in limbo, found 0")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the height preceding that which will cause the htlc CLTV
|
||||||
|
// timeouts will expire. The outputs entered at the same height as the
|
||||||
|
// output spending from the commitment txn, so we must deduct the number
|
||||||
|
// of blocks we have generated since adding it to the nursery, and take
|
||||||
|
// an additional block off so that we end up one block shy of the expiry
|
||||||
|
// height, and add the block padding.
|
||||||
|
cltvHeightDelta := padCLTV(defaultCLTV - defaultCSV - 1 - 1)
|
||||||
|
|
||||||
|
// Advance the blockchain until just before the CLTV expires, nothing
|
||||||
|
// exciting should have happened during this time.
|
||||||
|
if _, err := net.Miner.Client.Generate(cltvHeightDelta); err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now restart Alice, to ensure that she will broadcast the presigned
|
||||||
|
// htlc timeout txns after the delay expires after experiencing a while
|
||||||
|
// waiting for the htlc outputs to incubate.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice should now see the channel in her set of pending force closed
|
||||||
|
// channels with one pending HTLC.
|
||||||
|
err = wait.NoError(func() error {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
forceClose, err := findForceClosedChannel(
|
||||||
|
pendingChanResp, &op,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should now be at the block just before the utxo nursery
|
||||||
|
// will attempt to broadcast the htlc timeout transactions.
|
||||||
|
err = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = checkPendingHtlcStageAndMaturity(
|
||||||
|
forceClose, 1, htlcExpiryHeight, 1,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that our commitment confirmation depth has been
|
||||||
|
// surpassed, we should now see a non-zero recovered balance.
|
||||||
|
// All htlc outputs are still left in limbo, so it should be
|
||||||
|
// non-zero as well.
|
||||||
|
if forceClose.LimboBalance == 0 {
|
||||||
|
return errors.New("htlc funds should still be in " +
|
||||||
|
"limbo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, generate the block which will cause Alice to broadcast the
|
||||||
|
// presigned htlc timeout txns.
|
||||||
|
if _, err = net.Miner.Client.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since Alice had numInvoices (6) htlcs extended to Carol before force
|
||||||
|
// closing, we expect Alice to broadcast an htlc timeout txn for each
|
||||||
|
// one.
|
||||||
|
expectedTxes = numInvoices
|
||||||
|
|
||||||
|
// In case of anchors, the timeout txs will be aggregated into one.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
expectedTxes = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for them all to show up in the mempool.
|
||||||
|
htlcTxIDs, err := waitForNTxsInMempool(
|
||||||
|
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve each htlc timeout txn from the mempool, and ensure it is
|
||||||
|
// well-formed. This entails verifying that each only spends from
|
||||||
|
// output, and that that output is from the commitment txn. In case
|
||||||
|
// this is an anchor channel, the transactions are aggregated by the
|
||||||
|
// sweeper into one.
|
||||||
|
numInputs := 1
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
numInputs = numInvoices + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a map of the already confirmed htlc timeout outpoints,
|
||||||
|
// that will count the number of times each is spent by the sweep txn.
|
||||||
|
// We prepopulate it in this way so that we can later detect if we are
|
||||||
|
// spending from an output that was not a confirmed htlc timeout txn.
|
||||||
|
var htlcTxOutpointSet = make(map[wire.OutPoint]int)
|
||||||
|
|
||||||
|
var htlcLessFees uint64
|
||||||
|
for _, htlcTxID := range htlcTxIDs {
|
||||||
|
// Fetch the sweep transaction, all input it's spending should
|
||||||
|
// be from the commitment transaction which was broadcast
|
||||||
|
// on-chain. In case of an anchor type channel, we expect one
|
||||||
|
// extra input that is not spending from the commitment, that
|
||||||
|
// is added for fees.
|
||||||
|
htlcTx, err := net.Miner.Client.GetRawTransaction(htlcTxID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch sweep tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the htlc transaction has the expected number of
|
||||||
|
// inputs.
|
||||||
|
inputs := htlcTx.MsgTx().TxIn
|
||||||
|
if len(inputs) != numInputs {
|
||||||
|
t.Fatalf("htlc transaction should only have %d txin, "+
|
||||||
|
"has %d", numInputs, len(htlcTx.MsgTx().TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The number of outputs should be the same.
|
||||||
|
outputs := htlcTx.MsgTx().TxOut
|
||||||
|
if len(outputs) != numInputs {
|
||||||
|
t.Fatalf("htlc transaction should only have %d"+
|
||||||
|
"txout, has: %v", numInputs, len(outputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all the htlc transaction inputs are spending from the
|
||||||
|
// commitment transaction, except if this is an extra input
|
||||||
|
// added to pay for fees for anchor channels.
|
||||||
|
nonCommitmentInputs := 0
|
||||||
|
for i, txIn := range inputs {
|
||||||
|
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
||||||
|
nonCommitmentInputs++
|
||||||
|
|
||||||
|
if nonCommitmentInputs > 1 {
|
||||||
|
t.Fatalf("htlc transaction not "+
|
||||||
|
"spending from commit "+
|
||||||
|
"tx %v, instead spending %v",
|
||||||
|
closingTxID,
|
||||||
|
txIn.PreviousOutPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This was an extra input added to pay fees,
|
||||||
|
// continue to the next one.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each htlc timeout transaction, we expect a
|
||||||
|
// resolver report recording this on chain resolution
|
||||||
|
// for both alice and carol.
|
||||||
|
outpoint := txIn.PreviousOutPoint
|
||||||
|
resolutionOutpoint := &lnrpc.OutPoint{
|
||||||
|
TxidBytes: outpoint.Hash[:],
|
||||||
|
TxidStr: outpoint.Hash.String(),
|
||||||
|
OutputIndex: outpoint.Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect alice to have a timeout tx resolution with
|
||||||
|
// an amount equal to the payment amount.
|
||||||
|
aliceReports[outpoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
|
||||||
|
SweepTxid: htlcTx.Hash().String(),
|
||||||
|
Outpoint: resolutionOutpoint,
|
||||||
|
AmountSat: uint64(paymentAmt),
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect carol to have a resolution with an
|
||||||
|
// incoming htlc timeout which reflects the full amount
|
||||||
|
// of the htlc. It has no spend tx, because carol stops
|
||||||
|
// monitoring the htlc once it has timed out.
|
||||||
|
carolReports[outpoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
||||||
|
SweepTxid: "",
|
||||||
|
Outpoint: resolutionOutpoint,
|
||||||
|
AmountSat: uint64(paymentAmt),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recorf the HTLC outpoint, such that we can later
|
||||||
|
// check whether it gets swept
|
||||||
|
op := wire.OutPoint{
|
||||||
|
Hash: *htlcTxID,
|
||||||
|
Index: uint32(i),
|
||||||
|
}
|
||||||
|
htlcTxOutpointSet[op] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// We record the htlc amount less fees here, so that we know
|
||||||
|
// what value to expect for the second stage of our htlc
|
||||||
|
// htlc resolution.
|
||||||
|
htlcLessFees = uint64(outputs[0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the htlc timeout txns still in the mempool, we restart Alice to
|
||||||
|
// verify that she can resume watching the htlc txns she broadcasted
|
||||||
|
// before crashing.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a block that mines the htlc timeout txns. Doing so now
|
||||||
|
// activates the 2nd-stage CSV delayed outputs.
|
||||||
|
if _, err = net.Miner.Client.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice is restarted here to ensure that she promptly moved the crib
|
||||||
|
// outputs to the kindergarten bucket after the htlc timeout txns were
|
||||||
|
// confirmed.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the chain until just before the 2nd-layer CSV delays expire.
|
||||||
|
// For anchor channels thhis is one block earlier.
|
||||||
|
numBlocks := uint32(defaultCSV - 1)
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
numBlocks = defaultCSV - 2
|
||||||
|
|
||||||
|
}
|
||||||
|
_, err = net.Miner.Client.Generate(numBlocks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart Alice to ensure that she can recover from a failure before
|
||||||
|
// having graduated the htlc outputs in the kindergarten bucket.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel has been fully swept, it should no longer show
|
||||||
|
// incubated, check to see that Alice's node still reports the channel
|
||||||
|
// as pending force closed.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err = alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceClose.LimboBalance == 0 {
|
||||||
|
predErr = fmt.Errorf("htlc funds should still be in limbo")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a block that causes Alice to sweep the htlc outputs in the
|
||||||
|
// kindergarten bucket.
|
||||||
|
if _, err := net.Miner.Client.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the single sweep txn to appear in the mempool.
|
||||||
|
htlcSweepTxID, err := waitForTxInMempool(
|
||||||
|
net.Miner.Client, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the htlc sweep transaction from the mempool.
|
||||||
|
htlcSweepTx, err := net.Miner.Client.GetRawTransaction(htlcSweepTxID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch sweep tx: %v", err)
|
||||||
|
}
|
||||||
|
// Ensure the htlc sweep transaction only has one input for each htlc
|
||||||
|
// Alice extended before force closing.
|
||||||
|
if len(htlcSweepTx.MsgTx().TxIn) != numInvoices {
|
||||||
|
t.Fatalf("htlc transaction should have %d txin, "+
|
||||||
|
"has %d", numInvoices, len(htlcSweepTx.MsgTx().TxIn))
|
||||||
|
}
|
||||||
|
outputCount := len(htlcSweepTx.MsgTx().TxOut)
|
||||||
|
if outputCount != 1 {
|
||||||
|
t.Fatalf("htlc sweep transaction should have one output, has: "+
|
||||||
|
"%v", outputCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that each output spends from exactly one htlc timeout output.
|
||||||
|
for _, txIn := range htlcSweepTx.MsgTx().TxIn {
|
||||||
|
outpoint := txIn.PreviousOutPoint
|
||||||
|
// Check that the input is a confirmed htlc timeout txn.
|
||||||
|
if _, ok := htlcTxOutpointSet[outpoint]; !ok {
|
||||||
|
t.Fatalf("htlc sweep output not spending from htlc "+
|
||||||
|
"tx, instead spending output %v", outpoint)
|
||||||
|
}
|
||||||
|
// Increment our count for how many times this output was spent.
|
||||||
|
htlcTxOutpointSet[outpoint]++
|
||||||
|
|
||||||
|
// Check that each is only spent once.
|
||||||
|
if htlcTxOutpointSet[outpoint] > 1 {
|
||||||
|
t.Fatalf("htlc sweep tx has multiple spends from "+
|
||||||
|
"outpoint %v", outpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we have now swept our htlc timeout tx, we expect to
|
||||||
|
// have timeout resolutions for each of our htlcs.
|
||||||
|
output := txIn.PreviousOutPoint
|
||||||
|
aliceReports[output.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
||||||
|
SweepTxid: htlcSweepTx.Hash().String(),
|
||||||
|
Outpoint: &lnrpc.OutPoint{
|
||||||
|
TxidBytes: output.Hash[:],
|
||||||
|
TxidStr: output.Hash.String(),
|
||||||
|
OutputIndex: output.Index,
|
||||||
|
},
|
||||||
|
AmountSat: htlcLessFees,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that each HTLC output was spent exactly onece.
|
||||||
|
for op, num := range htlcTxOutpointSet {
|
||||||
|
if num != 1 {
|
||||||
|
t.Fatalf("HTLC outpoint %v was spent %v times", op, num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we can find the htlc sweep in our set of sweeps using
|
||||||
|
// the verbose output of the listsweeps output.
|
||||||
|
assertSweepFound(ctxb, t.t, alice, htlcSweepTx.Hash().String(), true)
|
||||||
|
|
||||||
|
// The following restart checks to ensure that the nursery store is
|
||||||
|
// storing the txid of the previously broadcast htlc sweep txn, and that
|
||||||
|
// it begins watching that txid after restarting.
|
||||||
|
if err := net.RestartNode(alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel has been fully swept, it should no longer show
|
||||||
|
// incubated, check to see that Alice's node still reports the channel
|
||||||
|
// as pending force closed.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// All htlcs should show zero blocks until maturity, as
|
||||||
|
// evidenced by having checked the sweep transaction in the
|
||||||
|
// mempool.
|
||||||
|
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = checkPendingHtlcStageAndMaturity(
|
||||||
|
forceClose, 2, htlcCsvMaturityHeight, 0,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the final block that sweeps all htlc funds into the user's
|
||||||
|
// wallet, and make sure the sweep is in this block.
|
||||||
|
block = mineBlocks(t, net, 1, 1)[0]
|
||||||
|
assertTxInBlock(t, block, htlcSweepTxID)
|
||||||
|
|
||||||
|
// Now that the channel has been fully swept, it should no longer show
|
||||||
|
// up within the pending channels RPC.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// In addition to there being no pending channels, we verify
|
||||||
|
// that pending channels does not report any money still in
|
||||||
|
// limbo.
|
||||||
|
if pendingChanResp.TotalLimboBalance != 0 {
|
||||||
|
predErr = errors.New("no user funds should be left " +
|
||||||
|
"in limbo after incubation")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, Carol should now be aware of her new immediately
|
||||||
|
// spendable on-chain balance, as it was Alice who broadcast the
|
||||||
|
// commitment transaction.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalResp, err = carol.WalletBalance(ctxt, carolBalReq)
|
||||||
|
require.NoError(t.t, err, "unable to get carol's balance")
|
||||||
|
|
||||||
|
// Carol's expected balance should be its starting balance plus the
|
||||||
|
// push amount sent by Alice and minus the miner fee paid.
|
||||||
|
carolExpectedBalance := btcutil.Amount(carolStartingBalance) +
|
||||||
|
pushAmt - totalFeeCarol
|
||||||
|
|
||||||
|
// In addition, if this is an anchor-enabled channel, further add the
|
||||||
|
// anchor size.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
carolExpectedBalance += btcutil.Amount(anchorSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
t.t, carolExpectedBalance,
|
||||||
|
btcutil.Amount(carolBalResp.ConfirmedBalance),
|
||||||
|
"carol's balance is incorrect",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, we check that alice and carol have the set of resolutions
|
||||||
|
// we expect.
|
||||||
|
assertReports(ctxb, t, alice, op, aliceReports)
|
||||||
|
assertReports(ctxb, t, carol, op, carolReports)
|
||||||
|
}
|
||||||
|
|
||||||
|
// padCLTV is a small helper function that pads a cltv value with a block
|
||||||
|
// padding.
|
||||||
|
func padCLTV(cltv uint32) uint32 {
|
||||||
|
return cltv + uint32(routing.BlockPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sweptOutput struct {
|
||||||
|
OutPoint wire.OutPoint
|
||||||
|
SweepTx string
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCommitAndAnchor looks for a commitment sweep and anchor sweep in the
|
||||||
|
// mempool. Our anchor output is identified by having multiple inputs, because
|
||||||
|
// we have to bring another input to add fees to the anchor. Note that the
|
||||||
|
// anchor swept output may be nil if the channel did not have anchors.
|
||||||
|
func findCommitAndAnchor(t *harnessTest, net *lntest.NetworkHarness,
|
||||||
|
sweepTxns []*wire.MsgTx, closeTx string) (*sweptOutput, *sweptOutput) {
|
||||||
|
|
||||||
|
var commitSweep, anchorSweep *sweptOutput
|
||||||
|
|
||||||
|
for _, tx := range sweepTxns {
|
||||||
|
txHash := tx.TxHash()
|
||||||
|
sweepTx, err := net.Miner.Client.GetRawTransaction(&txHash)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// We expect our commitment sweep to have a single input, and,
|
||||||
|
// our anchor sweep to have more inputs (because the wallet
|
||||||
|
// needs to add balance to the anchor amount). We find their
|
||||||
|
// sweep txids here to setup appropriate resolutions. We also
|
||||||
|
// need to find the outpoint for our resolution, which we do by
|
||||||
|
// matching the inputs to the sweep to the close transaction.
|
||||||
|
inputs := sweepTx.MsgTx().TxIn
|
||||||
|
if len(inputs) == 1 {
|
||||||
|
commitSweep = &sweptOutput{
|
||||||
|
OutPoint: inputs[0].PreviousOutPoint,
|
||||||
|
SweepTx: txHash.String(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Since we have more than one input, we run through
|
||||||
|
// them to find the outpoint that spends from the close
|
||||||
|
// tx. This will be our anchor output.
|
||||||
|
for _, txin := range inputs {
|
||||||
|
outpointStr := txin.PreviousOutPoint.Hash.String()
|
||||||
|
if outpointStr == closeTx {
|
||||||
|
anchorSweep = &sweptOutput{
|
||||||
|
OutPoint: txin.PreviousOutPoint,
|
||||||
|
SweepTx: txHash.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitSweep, anchorSweep
|
||||||
|
}
|
||||||
|
|
||||||
|
// testFailingChannel tests that we will fail the channel by force closing ii
|
||||||
|
// in the case where a counterparty tries to settle an HTLC with the wrong
|
||||||
|
// preimage.
|
||||||
|
func testFailingChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
paymentAmt = 10000
|
||||||
|
)
|
||||||
|
|
||||||
|
chanAmt := lnd.MaxFundingAmount
|
||||||
|
|
||||||
|
// We'll introduce Carol, which will settle any incoming invoice with a
|
||||||
|
// totally unrelated preimage.
|
||||||
|
carol := net.NewNode(t.t, "Carol", []string{"--hodl.bogus-settle"})
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Let Alice connect and open a channel to Carol,
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// With the channel open, we'll create a invoice for Carol that Alice
|
||||||
|
// will attempt to pay.
|
||||||
|
preimage := bytes.Repeat([]byte{byte(192)}, 32)
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: paymentAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := carol.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
carolPayReqs := []string{resp.PaymentRequest}
|
||||||
|
|
||||||
|
// Wait for Alice to receive the channel edge from the funding manager.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the alice->carol channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the payment from Alice to Carol. We expect Carol to attempt to
|
||||||
|
// settle this payment with the wrong preimage.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient, carolPayReqs, false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since Alice detects that Carol is trying to trick her by providing a
|
||||||
|
// fake preimage, she should fail and force close the channel.
|
||||||
|
var predErr error
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
||||||
|
pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pendingChanResp.WaitingCloseChannels)
|
||||||
|
if n != 1 {
|
||||||
|
predErr = fmt.Errorf("Expected to find %d channels "+
|
||||||
|
"waiting close, found %d", 1, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a block to confirm the broadcasted commitment.
|
||||||
|
block := mineBlocks(t, net, 1, 1)[0]
|
||||||
|
if len(block.Transactions) != 2 {
|
||||||
|
t.Fatalf("transaction wasn't mined")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel should now show up as force closed both for Alice and
|
||||||
|
// Carol.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
||||||
|
pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pendingChanResp.WaitingCloseChannels)
|
||||||
|
if n != 0 {
|
||||||
|
predErr = fmt.Errorf("Expected to find %d channels "+
|
||||||
|
"waiting close, found %d", 0, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n = len(pendingChanResp.PendingForceClosingChannels)
|
||||||
|
if n != 1 {
|
||||||
|
predErr = fmt.Errorf("expected to find %d channel "+
|
||||||
|
"pending force close, found %d", 1, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := carol.PendingChannels(ctxt,
|
||||||
|
pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pendingChanResp.PendingForceClosingChannels)
|
||||||
|
if n != 1 {
|
||||||
|
predErr = fmt.Errorf("expected to find %d channel "+
|
||||||
|
"pending force close, found %d", 1, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol will use the correct preimage to resolve the HTLC on-chain.
|
||||||
|
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Carol's resolve tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine enough blocks for Alice to sweep her funds from the force
|
||||||
|
// closed channel.
|
||||||
|
_, err = net.Miner.Client.Generate(defaultCSV - 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the sweeping tx to be broadcast.
|
||||||
|
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Alice's sweep tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine the sweep.
|
||||||
|
_, err = net.Miner.Client.Generate(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No pending channels should be left.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
||||||
|
pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pendingChanResp.PendingForceClosingChannels)
|
||||||
|
if n != 0 {
|
||||||
|
predErr = fmt.Errorf("expected to find %d channel "+
|
||||||
|
"pending force close, found %d", 0, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,7 +14,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/blockchain"
|
|
||||||
"github.com/btcsuite/btcd/btcjson"
|
"github.com/btcsuite/btcd/btcjson"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/integration/rpctest"
|
"github.com/btcsuite/btcd/integration/rpctest"
|
||||||
@ -24,7 +23,6 @@ import (
|
|||||||
"github.com/btcsuite/btcwallet/wallet"
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/lightningnetwork/lnd"
|
|
||||||
"github.com/lightningnetwork/lnd/chainreg"
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
"github.com/lightningnetwork/lnd/funding"
|
"github.com/lightningnetwork/lnd/funding"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
@ -39,7 +37,6 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2112,1179 +2109,6 @@ func testChannelUnsettledBalance(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
cleanupForceClose(t, net, net.Alice, chanPointAlice)
|
cleanupForceClose(t, net, net.Alice, chanPointAlice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// padCLTV is a small helper function that pads a cltv value with a block
|
|
||||||
// padding.
|
|
||||||
func padCLTV(cltv uint32) uint32 {
|
|
||||||
return cltv + uint32(routing.BlockPadding)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testChannelForceClosure performs a test to exercise the behavior of "force"
|
|
||||||
// closing a channel or unilaterally broadcasting the latest local commitment
|
|
||||||
// state on-chain. The test creates a new channel between Alice and Carol, then
|
|
||||||
// force closes the channel after some cursory assertions. Within the test, a
|
|
||||||
// total of 3 + n transactions will be broadcast, representing the commitment
|
|
||||||
// transaction, a transaction sweeping the local CSV delayed output, a
|
|
||||||
// transaction sweeping the CSV delayed 2nd-layer htlcs outputs, and n
|
|
||||||
// htlc timeout transactions, where n is the number of payments Alice attempted
|
|
||||||
// to send to Carol. This test includes several restarts to ensure that the
|
|
||||||
// transaction output states are persisted throughout the forced closure
|
|
||||||
// process.
|
|
||||||
//
|
|
||||||
// TODO(roasbeef): also add an unsettled HTLC before force closing.
|
|
||||||
func testChannelForceClosure(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
// We'll test the scenario for some of the commitment types, to ensure
|
|
||||||
// outputs can be swept.
|
|
||||||
commitTypes := []commitType{
|
|
||||||
commitTypeLegacy,
|
|
||||||
commitTypeAnchors,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channelType := range commitTypes {
|
|
||||||
testName := fmt.Sprintf("committype=%v", channelType)
|
|
||||||
logLine := fmt.Sprintf(
|
|
||||||
"---- channel force close subtest %s ----\n",
|
|
||||||
testName,
|
|
||||||
)
|
|
||||||
AddToNodeLog(t.t, net.Alice, logLine)
|
|
||||||
|
|
||||||
channelType := channelType
|
|
||||||
success := t.t.Run(testName, func(t *testing.T) {
|
|
||||||
ht := newHarnessTest(t, net)
|
|
||||||
|
|
||||||
args := channelType.Args()
|
|
||||||
alice := net.NewNode(ht.t, "Alice", args)
|
|
||||||
defer shutdownAndAssert(net, ht, alice)
|
|
||||||
|
|
||||||
// Since we'd like to test failure scenarios with
|
|
||||||
// outstanding htlcs, we'll introduce another node into
|
|
||||||
// our test network: Carol.
|
|
||||||
carolArgs := []string{"--hodl.exit-settle"}
|
|
||||||
carolArgs = append(carolArgs, args...)
|
|
||||||
carol := net.NewNode(ht.t, "Carol", carolArgs)
|
|
||||||
defer shutdownAndAssert(net, ht, carol)
|
|
||||||
|
|
||||||
// Each time, we'll send Alice new set of coins in
|
|
||||||
// order to fund the channel.
|
|
||||||
ctxt, _ := context.WithTimeout(
|
|
||||||
context.Background(), defaultTimeout,
|
|
||||||
)
|
|
||||||
net.SendCoins(ctxt, t, btcutil.SatoshiPerBitcoin, alice)
|
|
||||||
|
|
||||||
// Also give Carol some coins to allow her to sweep her
|
|
||||||
// anchor.
|
|
||||||
net.SendCoins(ctxt, t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
channelForceClosureTest(
|
|
||||||
net, ht, alice, carol, channelType,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
if !success {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|
||||||
alice, carol *lntest.HarnessNode, channelType commitType) {
|
|
||||||
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const (
|
|
||||||
chanAmt = btcutil.Amount(10e6)
|
|
||||||
pushAmt = btcutil.Amount(5e6)
|
|
||||||
paymentAmt = 100000
|
|
||||||
numInvoices = 6
|
|
||||||
)
|
|
||||||
|
|
||||||
const commitFeeRate = 20000
|
|
||||||
net.SetFeeEstimate(commitFeeRate)
|
|
||||||
|
|
||||||
// TODO(roasbeef): should check default value in config here
|
|
||||||
// instead, or make delay a param
|
|
||||||
defaultCLTV := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
|
||||||
|
|
||||||
// We must let Alice have an open channel before she can send a node
|
|
||||||
// announcement, so we open a channel with Carol,
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, alice, carol)
|
|
||||||
|
|
||||||
// Before we start, obtain Carol's current wallet balance, we'll check
|
|
||||||
// to ensure that at the end of the force closure by Alice, Carol
|
|
||||||
// recognizes his new on-chain output.
|
|
||||||
carolBalReq := &lnrpc.WalletBalanceRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalResp, err := carol.WalletBalance(ctxt, carolBalReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get carol's balance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
carolStartingBalance := carolBalResp.ConfirmedBalance
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: pushAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for Alice and Carol to receive the channel edge from the
|
|
||||||
// funding manager.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the alice->carol channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the alice->carol channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send payments from Alice to Carol, since Carol is htlchodl mode, the
|
|
||||||
// htlc outputs should be left unsettled, and should be swept by the
|
|
||||||
// utxo nursery.
|
|
||||||
carolPubKey := carol.PubKey[:]
|
|
||||||
for i := 0; i < numInvoices; i++ {
|
|
||||||
ctx, cancel := context.WithCancel(ctxb)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := alice.RouterClient.SendPaymentV2(
|
|
||||||
ctx,
|
|
||||||
&routerrpc.SendPaymentRequest{
|
|
||||||
Dest: carolPubKey,
|
|
||||||
Amt: int64(paymentAmt),
|
|
||||||
PaymentHash: makeFakePayHash(t),
|
|
||||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
|
||||||
TimeoutSeconds: 60,
|
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send alice htlc: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the HTLC has cleared, all the nodes n our mini network should
|
|
||||||
// show that the HTLC has been locked in.
|
|
||||||
nodes := []*lntest.HarnessNode{alice, carol}
|
|
||||||
var predErr error
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
predErr = assertNumActiveHtlcs(nodes, numInvoices)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("htlc mismatch: %v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch starting height of this test so we can compute the block
|
|
||||||
// heights we expect certain events to take place.
|
|
||||||
_, curHeight, err := net.Miner.Client.GetBestBlock()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get best block height")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using the current height of the chain, derive the relevant heights
|
|
||||||
// for incubating two-stage htlcs.
|
|
||||||
var (
|
|
||||||
startHeight = uint32(curHeight)
|
|
||||||
commCsvMaturityHeight = startHeight + 1 + defaultCSV
|
|
||||||
htlcExpiryHeight = padCLTV(startHeight + defaultCLTV)
|
|
||||||
htlcCsvMaturityHeight = padCLTV(startHeight + defaultCLTV + 1 + defaultCSV)
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we are dealing with an anchor channel type, the sweeper will
|
|
||||||
// sweep the HTLC second level output one block earlier (than the
|
|
||||||
// nursery that waits an additional block, and handles non-anchor
|
|
||||||
// channels). So we set a maturity height that is one less.
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
htlcCsvMaturityHeight = padCLTV(
|
|
||||||
startHeight + defaultCLTV + defaultCSV,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceChan, err := getChanInfo(ctxt, alice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get alice's channel info: %v", err)
|
|
||||||
}
|
|
||||||
if aliceChan.NumUpdates == 0 {
|
|
||||||
t.Fatalf("alice should see at least one update to her channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel is open and we have unsettled htlcs, immediately
|
|
||||||
// execute a force closure of the channel. This will also assert that
|
|
||||||
// the commitment transaction was immediately broadcast in order to
|
|
||||||
// fulfill the force closure request.
|
|
||||||
const actualFeeRate = 30000
|
|
||||||
net.SetFeeEstimate(actualFeeRate)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
_, closingTxID, err := net.CloseChannel(ctxt, alice, chanPoint, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to execute force channel closure: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel has been force closed, it should show up in the
|
|
||||||
// PendingChannels RPC under the waiting close section.
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(ctxt, pendingChansRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for pending channels: %v", err)
|
|
||||||
}
|
|
||||||
err = checkNumWaitingCloseChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the outpoint of the channel, which we will use repeatedly to
|
|
||||||
// locate the pending channel information in the rpc responses.
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
op := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
waitingClose, err := findWaitingCloseChannel(pendingChanResp, &op)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediately after force closing, all of the funds should be in limbo.
|
|
||||||
if waitingClose.LimboBalance == 0 {
|
|
||||||
t.Fatalf("all funds should still be in limbo")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map of outpoints to expected resolutions for alice and carol
|
|
||||||
// which we will add reports to as we sweep outputs.
|
|
||||||
var (
|
|
||||||
aliceReports = make(map[string]*lnrpc.Resolution)
|
|
||||||
carolReports = make(map[string]*lnrpc.Resolution)
|
|
||||||
)
|
|
||||||
|
|
||||||
// The several restarts in this test are intended to ensure that when a
|
|
||||||
// channel is force-closed, the UTXO nursery has persisted the state of
|
|
||||||
// the channel in the closure process and will recover the correct state
|
|
||||||
// when the system comes back on line. This restart tests state
|
|
||||||
// persistence at the beginning of the process, when the commitment
|
|
||||||
// transaction has been broadcast but not yet confirmed in a block.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine a block which should confirm the commitment transaction
|
|
||||||
// broadcast as a result of the force closure. If there are anchors, we
|
|
||||||
// also expect the anchor sweep tx to be in the mempool.
|
|
||||||
expectedTxes := 1
|
|
||||||
expectedFeeRate := commitFeeRate
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
expectedTxes = 2
|
|
||||||
expectedFeeRate = actualFeeRate
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepTxns, err := getNTxsFromMempool(
|
|
||||||
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to find commitment in miner mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify fee rate of the commitment tx plus anchor if present.
|
|
||||||
var totalWeight, totalFee int64
|
|
||||||
for _, tx := range sweepTxns {
|
|
||||||
utx := btcutil.NewTx(tx)
|
|
||||||
totalWeight += blockchain.GetTransactionWeight(utx)
|
|
||||||
|
|
||||||
fee, err := getTxFee(net.Miner.Client, tx)
|
|
||||||
require.NoError(t.t, err)
|
|
||||||
totalFee += int64(fee)
|
|
||||||
}
|
|
||||||
feeRate := totalFee * 1000 / totalWeight
|
|
||||||
|
|
||||||
// Allow some deviation because weight estimates during tx generation
|
|
||||||
// are estimates.
|
|
||||||
require.InEpsilon(t.t, expectedFeeRate, feeRate, 0.005)
|
|
||||||
|
|
||||||
// Find alice's commit sweep and anchor sweep (if present) in the
|
|
||||||
// mempool.
|
|
||||||
aliceCloseTx := waitingClose.Commitments.LocalTxid
|
|
||||||
_, aliceAnchor := findCommitAndAnchor(
|
|
||||||
t, net, sweepTxns, aliceCloseTx,
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we expect anchors, add alice's anchor to our expected set of
|
|
||||||
// reports.
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
aliceReports[aliceAnchor.OutPoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
||||||
SweepTxid: aliceAnchor.SweepTx,
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: aliceAnchor.OutPoint.Hash[:],
|
|
||||||
TxidStr: aliceAnchor.OutPoint.Hash.String(),
|
|
||||||
OutputIndex: aliceAnchor.OutPoint.Index,
|
|
||||||
},
|
|
||||||
AmountSat: uint64(anchorSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := net.Miner.Client.Generate(1); err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the commitment has been confirmed, the channel should be
|
|
||||||
// marked as force closed.
|
|
||||||
err = wait.NoError(func() error {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel has been force closed, it should now
|
|
||||||
// have the height and number of blocks to confirm populated.
|
|
||||||
err = checkCommitmentMaturity(
|
|
||||||
forceClose, commCsvMaturityHeight, int32(defaultCSV),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// None of our outputs have been swept, so they should all be in
|
|
||||||
// limbo. For anchors, we expect the anchor amount to be
|
|
||||||
// recovered.
|
|
||||||
if forceClose.LimboBalance == 0 {
|
|
||||||
return errors.New("all funds should still be in " +
|
|
||||||
"limbo")
|
|
||||||
}
|
|
||||||
expectedRecoveredBalance := int64(0)
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
expectedRecoveredBalance = anchorSize
|
|
||||||
}
|
|
||||||
if forceClose.RecoveredBalance != expectedRecoveredBalance {
|
|
||||||
return errors.New("no funds should yet be shown " +
|
|
||||||
"as recovered")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following restart is intended to ensure that outputs from the
|
|
||||||
// force close commitment transaction have been persisted once the
|
|
||||||
// transaction has been confirmed, but before the outputs are spendable
|
|
||||||
// (the "kindergarten" bucket.)
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol's sweep tx should be in the mempool already, as her output is
|
|
||||||
// not timelocked. If there are anchors, we also expect Carol's anchor
|
|
||||||
// sweep now.
|
|
||||||
sweepTxns, err = getNTxsFromMempool(
|
|
||||||
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to find Carol's sweep in miner mempool: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the total fee Carol paid.
|
|
||||||
var totalFeeCarol btcutil.Amount
|
|
||||||
for _, tx := range sweepTxns {
|
|
||||||
fee, err := getTxFee(net.Miner.Client, tx)
|
|
||||||
require.NoError(t.t, err)
|
|
||||||
|
|
||||||
totalFeeCarol += fee
|
|
||||||
}
|
|
||||||
|
|
||||||
// We look up the sweep txns we have found in mempool and create
|
|
||||||
// expected resolutions for carol.
|
|
||||||
carolCommit, carolAnchor := findCommitAndAnchor(
|
|
||||||
t, net, sweepTxns, aliceCloseTx,
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we have anchors, add an anchor resolution for carol.
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
carolReports[carolAnchor.OutPoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
||||||
SweepTxid: carolAnchor.SweepTx,
|
|
||||||
AmountSat: anchorSize,
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: carolAnchor.OutPoint.Hash[:],
|
|
||||||
TxidStr: carolAnchor.OutPoint.Hash.String(),
|
|
||||||
OutputIndex: carolAnchor.OutPoint.Index,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently within the codebase, the default CSV is 4 relative blocks.
|
|
||||||
// For the persistence test, we generate two blocks, then trigger
|
|
||||||
// a restart and then generate the final block that should trigger
|
|
||||||
// the creation of the sweep transaction.
|
|
||||||
if _, err := net.Miner.Client.Generate(defaultCSV - 2); err != nil {
|
|
||||||
t.Fatalf("unable to mine blocks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following restart checks to ensure that outputs in the
|
|
||||||
// kindergarten bucket are persisted while waiting for the required
|
|
||||||
// number of confirmations to be reported.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice should see the channel in her set of pending force closed
|
|
||||||
// channels with her funds still in limbo.
|
|
||||||
var aliceBalance int64
|
|
||||||
err = wait.NoError(func() error {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
forceClose, err := findForceClosedChannel(
|
|
||||||
pendingChanResp, &op,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a record of the balances we expect for alice and carol.
|
|
||||||
aliceBalance = forceClose.Channel.LocalBalance
|
|
||||||
|
|
||||||
// At this point, the nursery should show that the commitment
|
|
||||||
// output has 2 block left before its CSV delay expires. In
|
|
||||||
// total, we have mined exactly defaultCSV blocks, so the htlc
|
|
||||||
// outputs should also reflect that this many blocks have
|
|
||||||
// passed.
|
|
||||||
err = checkCommitmentMaturity(
|
|
||||||
forceClose, commCsvMaturityHeight, 2,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// All funds should still be shown in limbo.
|
|
||||||
if forceClose.LimboBalance == 0 {
|
|
||||||
return errors.New("all funds should still be in " +
|
|
||||||
"limbo")
|
|
||||||
}
|
|
||||||
expectedRecoveredBalance := int64(0)
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
expectedRecoveredBalance = anchorSize
|
|
||||||
}
|
|
||||||
if forceClose.RecoveredBalance != expectedRecoveredBalance {
|
|
||||||
return errors.New("no funds should yet be shown " +
|
|
||||||
"as recovered")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate an additional block, which should cause the CSV delayed
|
|
||||||
// output from the commitment txn to expire.
|
|
||||||
if _, err := net.Miner.Client.Generate(1); err != nil {
|
|
||||||
t.Fatalf("unable to mine blocks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, the CSV will expire in the next block, meaning that
|
|
||||||
// the sweeping transaction should now be broadcast. So we fetch the
|
|
||||||
// node's mempool to ensure it has been properly broadcast.
|
|
||||||
sweepingTXID, err := waitForTxInMempool(
|
|
||||||
net.Miner.Client, minerMempoolTimeout,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the sweep transaction, all input it's spending should be from
|
|
||||||
// the commitment transaction which was broadcast on-chain.
|
|
||||||
sweepTx, err := net.Miner.Client.GetRawTransaction(sweepingTXID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to fetch sweep tx: %v", err)
|
|
||||||
}
|
|
||||||
for _, txIn := range sweepTx.MsgTx().TxIn {
|
|
||||||
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
|
||||||
t.Fatalf("sweep transaction not spending from commit "+
|
|
||||||
"tx %v, instead spending %v",
|
|
||||||
closingTxID, txIn.PreviousOutPoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect a resolution which spends our commit output.
|
|
||||||
output := sweepTx.MsgTx().TxIn[0].PreviousOutPoint
|
|
||||||
aliceReports[output.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_COMMIT,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
||||||
SweepTxid: sweepingTXID.String(),
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: output.Hash[:],
|
|
||||||
TxidStr: output.Hash.String(),
|
|
||||||
OutputIndex: output.Index,
|
|
||||||
},
|
|
||||||
AmountSat: uint64(aliceBalance),
|
|
||||||
}
|
|
||||||
|
|
||||||
carolReports[carolCommit.OutPoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_COMMIT,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: carolCommit.OutPoint.Hash[:],
|
|
||||||
TxidStr: carolCommit.OutPoint.Hash.String(),
|
|
||||||
OutputIndex: carolCommit.OutPoint.Index,
|
|
||||||
},
|
|
||||||
AmountSat: uint64(pushAmt),
|
|
||||||
SweepTxid: carolCommit.SweepTx,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we can find the commitment sweep in our set of known
|
|
||||||
// sweeps, using the simple transaction id ListSweeps output.
|
|
||||||
assertSweepFound(ctxb, t.t, alice, sweepingTXID.String(), false)
|
|
||||||
|
|
||||||
// Restart Alice to ensure that she resumes watching the finalized
|
|
||||||
// commitment sweep txid.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we mine an additional block which should include the sweep
|
|
||||||
// transaction as the input scripts and the sequence locks on the
|
|
||||||
// inputs should be properly met.
|
|
||||||
blockHash, err := net.Miner.Client.Generate(1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
block, err := net.Miner.Client.GetBlock(blockHash[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTxInBlock(t, block, sweepTx.Hash())
|
|
||||||
|
|
||||||
// Update current height
|
|
||||||
_, curHeight, err = net.Miner.Client.GetBestBlock()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get best block height")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
// Now that the commit output has been fully swept, check to see
|
|
||||||
// that the channel remains open for the pending htlc outputs.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The commitment funds will have been recovered after the
|
|
||||||
// commit txn was included in the last block. The htlc funds
|
|
||||||
// will be shown in limbo.
|
|
||||||
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
predErr = checkPendingHtlcStageAndMaturity(
|
|
||||||
forceClose, 1, htlcExpiryHeight,
|
|
||||||
int32(htlcExpiryHeight)-curHeight,
|
|
||||||
)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if forceClose.LimboBalance == 0 {
|
|
||||||
predErr = fmt.Errorf("expected funds in limbo, found 0")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the height preceding that which will cause the htlc CLTV
|
|
||||||
// timeouts will expire. The outputs entered at the same height as the
|
|
||||||
// output spending from the commitment txn, so we must deduct the number
|
|
||||||
// of blocks we have generated since adding it to the nursery, and take
|
|
||||||
// an additional block off so that we end up one block shy of the expiry
|
|
||||||
// height, and add the block padding.
|
|
||||||
cltvHeightDelta := padCLTV(defaultCLTV - defaultCSV - 1 - 1)
|
|
||||||
|
|
||||||
// Advance the blockchain until just before the CLTV expires, nothing
|
|
||||||
// exciting should have happened during this time.
|
|
||||||
if _, err := net.Miner.Client.Generate(cltvHeightDelta); err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We now restart Alice, to ensure that she will broadcast the presigned
|
|
||||||
// htlc timeout txns after the delay expires after experiencing a while
|
|
||||||
// waiting for the htlc outputs to incubate.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice should now see the channel in her set of pending force closed
|
|
||||||
// channels with one pending HTLC.
|
|
||||||
err = wait.NoError(func() error {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
forceClose, err := findForceClosedChannel(
|
|
||||||
pendingChanResp, &op,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should now be at the block just before the utxo nursery
|
|
||||||
// will attempt to broadcast the htlc timeout transactions.
|
|
||||||
err = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = checkPendingHtlcStageAndMaturity(
|
|
||||||
forceClose, 1, htlcExpiryHeight, 1,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that our commitment confirmation depth has been
|
|
||||||
// surpassed, we should now see a non-zero recovered balance.
|
|
||||||
// All htlc outputs are still left in limbo, so it should be
|
|
||||||
// non-zero as well.
|
|
||||||
if forceClose.LimboBalance == 0 {
|
|
||||||
return errors.New("htlc funds should still be in " +
|
|
||||||
"limbo")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, generate the block which will cause Alice to broadcast the
|
|
||||||
// presigned htlc timeout txns.
|
|
||||||
if _, err = net.Miner.Client.Generate(1); err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since Alice had numInvoices (6) htlcs extended to Carol before force
|
|
||||||
// closing, we expect Alice to broadcast an htlc timeout txn for each
|
|
||||||
// one.
|
|
||||||
expectedTxes = numInvoices
|
|
||||||
|
|
||||||
// In case of anchors, the timeout txs will be aggregated into one.
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
expectedTxes = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for them all to show up in the mempool.
|
|
||||||
htlcTxIDs, err := waitForNTxsInMempool(
|
|
||||||
net.Miner.Client, expectedTxes, minerMempoolTimeout,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve each htlc timeout txn from the mempool, and ensure it is
|
|
||||||
// well-formed. This entails verifying that each only spends from
|
|
||||||
// output, and that that output is from the commitment txn. In case
|
|
||||||
// this is an anchor channel, the transactions are aggregated by the
|
|
||||||
// sweeper into one.
|
|
||||||
numInputs := 1
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
numInputs = numInvoices + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a map of the already confirmed htlc timeout outpoints,
|
|
||||||
// that will count the number of times each is spent by the sweep txn.
|
|
||||||
// We prepopulate it in this way so that we can later detect if we are
|
|
||||||
// spending from an output that was not a confirmed htlc timeout txn.
|
|
||||||
var htlcTxOutpointSet = make(map[wire.OutPoint]int)
|
|
||||||
|
|
||||||
var htlcLessFees uint64
|
|
||||||
for _, htlcTxID := range htlcTxIDs {
|
|
||||||
// Fetch the sweep transaction, all input it's spending should
|
|
||||||
// be from the commitment transaction which was broadcast
|
|
||||||
// on-chain. In case of an anchor type channel, we expect one
|
|
||||||
// extra input that is not spending from the commitment, that
|
|
||||||
// is added for fees.
|
|
||||||
htlcTx, err := net.Miner.Client.GetRawTransaction(htlcTxID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to fetch sweep tx: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the htlc transaction has the expected number of
|
|
||||||
// inputs.
|
|
||||||
inputs := htlcTx.MsgTx().TxIn
|
|
||||||
if len(inputs) != numInputs {
|
|
||||||
t.Fatalf("htlc transaction should only have %d txin, "+
|
|
||||||
"has %d", numInputs, len(htlcTx.MsgTx().TxIn))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The number of outputs should be the same.
|
|
||||||
outputs := htlcTx.MsgTx().TxOut
|
|
||||||
if len(outputs) != numInputs {
|
|
||||||
t.Fatalf("htlc transaction should only have %d"+
|
|
||||||
"txout, has: %v", numInputs, len(outputs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all the htlc transaction inputs are spending from the
|
|
||||||
// commitment transaction, except if this is an extra input
|
|
||||||
// added to pay for fees for anchor channels.
|
|
||||||
nonCommitmentInputs := 0
|
|
||||||
for i, txIn := range inputs {
|
|
||||||
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
|
||||||
nonCommitmentInputs++
|
|
||||||
|
|
||||||
if nonCommitmentInputs > 1 {
|
|
||||||
t.Fatalf("htlc transaction not "+
|
|
||||||
"spending from commit "+
|
|
||||||
"tx %v, instead spending %v",
|
|
||||||
closingTxID,
|
|
||||||
txIn.PreviousOutPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This was an extra input added to pay fees,
|
|
||||||
// continue to the next one.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each htlc timeout transaction, we expect a
|
|
||||||
// resolver report recording this on chain resolution
|
|
||||||
// for both alice and carol.
|
|
||||||
outpoint := txIn.PreviousOutPoint
|
|
||||||
resolutionOutpoint := &lnrpc.OutPoint{
|
|
||||||
TxidBytes: outpoint.Hash[:],
|
|
||||||
TxidStr: outpoint.Hash.String(),
|
|
||||||
OutputIndex: outpoint.Index,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect alice to have a timeout tx resolution with
|
|
||||||
// an amount equal to the payment amount.
|
|
||||||
aliceReports[outpoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
|
|
||||||
SweepTxid: htlcTx.Hash().String(),
|
|
||||||
Outpoint: resolutionOutpoint,
|
|
||||||
AmountSat: uint64(paymentAmt),
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect carol to have a resolution with an
|
|
||||||
// incoming htlc timeout which reflects the full amount
|
|
||||||
// of the htlc. It has no spend tx, because carol stops
|
|
||||||
// monitoring the htlc once it has timed out.
|
|
||||||
carolReports[outpoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
|
||||||
SweepTxid: "",
|
|
||||||
Outpoint: resolutionOutpoint,
|
|
||||||
AmountSat: uint64(paymentAmt),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recorf the HTLC outpoint, such that we can later
|
|
||||||
// check whether it gets swept
|
|
||||||
op := wire.OutPoint{
|
|
||||||
Hash: *htlcTxID,
|
|
||||||
Index: uint32(i),
|
|
||||||
}
|
|
||||||
htlcTxOutpointSet[op] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// We record the htlc amount less fees here, so that we know
|
|
||||||
// what value to expect for the second stage of our htlc
|
|
||||||
// htlc resolution.
|
|
||||||
htlcLessFees = uint64(outputs[0].Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the htlc timeout txns still in the mempool, we restart Alice to
|
|
||||||
// verify that she can resume watching the htlc txns she broadcasted
|
|
||||||
// before crashing.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a block that mines the htlc timeout txns. Doing so now
|
|
||||||
// activates the 2nd-stage CSV delayed outputs.
|
|
||||||
if _, err = net.Miner.Client.Generate(1); err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice is restarted here to ensure that she promptly moved the crib
|
|
||||||
// outputs to the kindergarten bucket after the htlc timeout txns were
|
|
||||||
// confirmed.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance the chain until just before the 2nd-layer CSV delays expire.
|
|
||||||
// For anchor channels thhis is one block earlier.
|
|
||||||
numBlocks := uint32(defaultCSV - 1)
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
numBlocks = defaultCSV - 2
|
|
||||||
|
|
||||||
}
|
|
||||||
_, err = net.Miner.Client.Generate(numBlocks)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart Alice to ensure that she can recover from a failure before
|
|
||||||
// having graduated the htlc outputs in the kindergarten bucket.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel has been fully swept, it should no longer show
|
|
||||||
// incubated, check to see that Alice's node still reports the channel
|
|
||||||
// as pending force closed.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err = alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if forceClose.LimboBalance == 0 {
|
|
||||||
predErr = fmt.Errorf("htlc funds should still be in limbo")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a block that causes Alice to sweep the htlc outputs in the
|
|
||||||
// kindergarten bucket.
|
|
||||||
if _, err := net.Miner.Client.Generate(1); err != nil {
|
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the single sweep txn to appear in the mempool.
|
|
||||||
htlcSweepTxID, err := waitForTxInMempool(
|
|
||||||
net.Miner.Client, minerMempoolTimeout,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the htlc sweep transaction from the mempool.
|
|
||||||
htlcSweepTx, err := net.Miner.Client.GetRawTransaction(htlcSweepTxID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to fetch sweep tx: %v", err)
|
|
||||||
}
|
|
||||||
// Ensure the htlc sweep transaction only has one input for each htlc
|
|
||||||
// Alice extended before force closing.
|
|
||||||
if len(htlcSweepTx.MsgTx().TxIn) != numInvoices {
|
|
||||||
t.Fatalf("htlc transaction should have %d txin, "+
|
|
||||||
"has %d", numInvoices, len(htlcSweepTx.MsgTx().TxIn))
|
|
||||||
}
|
|
||||||
outputCount := len(htlcSweepTx.MsgTx().TxOut)
|
|
||||||
if outputCount != 1 {
|
|
||||||
t.Fatalf("htlc sweep transaction should have one output, has: "+
|
|
||||||
"%v", outputCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that each output spends from exactly one htlc timeout output.
|
|
||||||
for _, txIn := range htlcSweepTx.MsgTx().TxIn {
|
|
||||||
outpoint := txIn.PreviousOutPoint
|
|
||||||
// Check that the input is a confirmed htlc timeout txn.
|
|
||||||
if _, ok := htlcTxOutpointSet[outpoint]; !ok {
|
|
||||||
t.Fatalf("htlc sweep output not spending from htlc "+
|
|
||||||
"tx, instead spending output %v", outpoint)
|
|
||||||
}
|
|
||||||
// Increment our count for how many times this output was spent.
|
|
||||||
htlcTxOutpointSet[outpoint]++
|
|
||||||
|
|
||||||
// Check that each is only spent once.
|
|
||||||
if htlcTxOutpointSet[outpoint] > 1 {
|
|
||||||
t.Fatalf("htlc sweep tx has multiple spends from "+
|
|
||||||
"outpoint %v", outpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we have now swept our htlc timeout tx, we expect to
|
|
||||||
// have timeout resolutions for each of our htlcs.
|
|
||||||
output := txIn.PreviousOutPoint
|
|
||||||
aliceReports[output.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
|
||||||
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
|
||||||
SweepTxid: htlcSweepTx.Hash().String(),
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: output.Hash[:],
|
|
||||||
TxidStr: output.Hash.String(),
|
|
||||||
OutputIndex: output.Index,
|
|
||||||
},
|
|
||||||
AmountSat: htlcLessFees,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that each HTLC output was spent exactly onece.
|
|
||||||
for op, num := range htlcTxOutpointSet {
|
|
||||||
if num != 1 {
|
|
||||||
t.Fatalf("HTLC outpoint %v was spent %v times", op, num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we can find the htlc sweep in our set of sweeps using
|
|
||||||
// the verbose output of the listsweeps output.
|
|
||||||
assertSweepFound(ctxb, t.t, alice, htlcSweepTx.Hash().String(), true)
|
|
||||||
|
|
||||||
// The following restart checks to ensure that the nursery store is
|
|
||||||
// storing the txid of the previously broadcast htlc sweep txn, and that
|
|
||||||
// it begins watching that txid after restarting.
|
|
||||||
if err := net.RestartNode(alice, nil); err != nil {
|
|
||||||
t.Fatalf("Node restart failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel has been fully swept, it should no longer show
|
|
||||||
// incubated, check to see that Alice's node still reports the channel
|
|
||||||
// as pending force closed.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
err = checkNumForceClosedChannels(pendingChanResp, 1)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// All htlcs should show zero blocks until maturity, as
|
|
||||||
// evidenced by having checked the sweep transaction in the
|
|
||||||
// mempool.
|
|
||||||
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
err = checkPendingHtlcStageAndMaturity(
|
|
||||||
forceClose, 2, htlcCsvMaturityHeight, 0,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the final block that sweeps all htlc funds into the user's
|
|
||||||
// wallet, and make sure the sweep is in this block.
|
|
||||||
block = mineBlocks(t, net, 1, 1)[0]
|
|
||||||
assertTxInBlock(t, block, htlcSweepTxID)
|
|
||||||
|
|
||||||
// Now that the channel has been fully swept, it should no longer show
|
|
||||||
// up within the pending channels RPC.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// In addition to there being no pending channels, we verify
|
|
||||||
// that pending channels does not report any money still in
|
|
||||||
// limbo.
|
|
||||||
if pendingChanResp.TotalLimboBalance != 0 {
|
|
||||||
predErr = errors.New("no user funds should be left " +
|
|
||||||
"in limbo after incubation")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, Carol should now be aware of her new immediately
|
|
||||||
// spendable on-chain balance, as it was Alice who broadcast the
|
|
||||||
// commitment transaction.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalResp, err = carol.WalletBalance(ctxt, carolBalReq)
|
|
||||||
require.NoError(t.t, err, "unable to get carol's balance")
|
|
||||||
|
|
||||||
// Carol's expected balance should be its starting balance plus the
|
|
||||||
// push amount sent by Alice and minus the miner fee paid.
|
|
||||||
carolExpectedBalance := btcutil.Amount(carolStartingBalance) +
|
|
||||||
pushAmt - totalFeeCarol
|
|
||||||
|
|
||||||
// In addition, if this is an anchor-enabled channel, further add the
|
|
||||||
// anchor size.
|
|
||||||
if channelType == commitTypeAnchors {
|
|
||||||
carolExpectedBalance += btcutil.Amount(anchorSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(
|
|
||||||
t.t, carolExpectedBalance,
|
|
||||||
btcutil.Amount(carolBalResp.ConfirmedBalance),
|
|
||||||
"carol's balance is incorrect",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Finally, we check that alice and carol have the set of resolutions
|
|
||||||
// we expect.
|
|
||||||
assertReports(ctxb, t, alice, op, aliceReports)
|
|
||||||
assertReports(ctxb, t, carol, op, carolReports)
|
|
||||||
}
|
|
||||||
|
|
||||||
type sweptOutput struct {
|
|
||||||
OutPoint wire.OutPoint
|
|
||||||
SweepTx string
|
|
||||||
}
|
|
||||||
|
|
||||||
// findCommitAndAnchor looks for a commitment sweep and anchor sweep in the
|
|
||||||
// mempool. Our anchor output is identified by having multiple inputs, because
|
|
||||||
// we have to bring another input to add fees to the anchor. Note that the
|
|
||||||
// anchor swept output may be nil if the channel did not have anchors.
|
|
||||||
func findCommitAndAnchor(t *harnessTest, net *lntest.NetworkHarness,
|
|
||||||
sweepTxns []*wire.MsgTx, closeTx string) (*sweptOutput, *sweptOutput) {
|
|
||||||
|
|
||||||
var commitSweep, anchorSweep *sweptOutput
|
|
||||||
|
|
||||||
for _, tx := range sweepTxns {
|
|
||||||
txHash := tx.TxHash()
|
|
||||||
sweepTx, err := net.Miner.Client.GetRawTransaction(&txHash)
|
|
||||||
require.NoError(t.t, err)
|
|
||||||
|
|
||||||
// We expect our commitment sweep to have a single input, and,
|
|
||||||
// our anchor sweep to have more inputs (because the wallet
|
|
||||||
// needs to add balance to the anchor amount). We find their
|
|
||||||
// sweep txids here to setup appropriate resolutions. We also
|
|
||||||
// need to find the outpoint for our resolution, which we do by
|
|
||||||
// matching the inputs to the sweep to the close transaction.
|
|
||||||
inputs := sweepTx.MsgTx().TxIn
|
|
||||||
if len(inputs) == 1 {
|
|
||||||
commitSweep = &sweptOutput{
|
|
||||||
OutPoint: inputs[0].PreviousOutPoint,
|
|
||||||
SweepTx: txHash.String(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Since we have more than one input, we run through
|
|
||||||
// them to find the outpoint that spends from the close
|
|
||||||
// tx. This will be our anchor output.
|
|
||||||
for _, txin := range inputs {
|
|
||||||
outpointStr := txin.PreviousOutPoint.Hash.String()
|
|
||||||
if outpointStr == closeTx {
|
|
||||||
anchorSweep = &sweptOutput{
|
|
||||||
OutPoint: txin.PreviousOutPoint,
|
|
||||||
SweepTx: txHash.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return commitSweep, anchorSweep
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSphinxReplayPersistence verifies that replayed onion packets are rejected
|
// testSphinxReplayPersistence verifies that replayed onion packets are rejected
|
||||||
// by a remote peer after a restart. We use a combination of unsafe
|
// by a remote peer after a restart. We use a combination of unsafe
|
||||||
// configuration arguments to force Carol to replay the same sphinx packet after
|
// configuration arguments to force Carol to replay the same sphinx packet after
|
||||||
@ -4428,199 +3252,6 @@ func getTxFee(miner *rpcclient.Client, tx *wire.MsgTx) (btcutil.Amount, error) {
|
|||||||
return balance, nil
|
return balance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// testFailingChannel tests that we will fail the channel by force closing ii
|
|
||||||
// in the case where a counterparty tries to settle an HTLC with the wrong
|
|
||||||
// preimage.
|
|
||||||
func testFailingChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const (
|
|
||||||
paymentAmt = 10000
|
|
||||||
)
|
|
||||||
|
|
||||||
chanAmt := lnd.MaxFundingAmount
|
|
||||||
|
|
||||||
// We'll introduce Carol, which will settle any incoming invoice with a
|
|
||||||
// totally unrelated preimage.
|
|
||||||
carol := net.NewNode(t.t, "Carol", []string{"--hodl.bogus-settle"})
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
// Let Alice connect and open a channel to Carol,
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// With the channel open, we'll create a invoice for Carol that Alice
|
|
||||||
// will attempt to pay.
|
|
||||||
preimage := bytes.Repeat([]byte{byte(192)}, 32)
|
|
||||||
invoice := &lnrpc.Invoice{
|
|
||||||
Memo: "testing",
|
|
||||||
RPreimage: preimage,
|
|
||||||
Value: paymentAmt,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := carol.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
carolPayReqs := []string{resp.PaymentRequest}
|
|
||||||
|
|
||||||
// Wait for Alice to receive the channel edge from the funding manager.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the alice->carol channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the payment from Alice to Carol. We expect Carol to attempt to
|
|
||||||
// settle this payment with the wrong preimage.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, net.Alice, net.Alice.RouterClient, carolPayReqs, false,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since Alice detects that Carol is trying to trick her by providing a
|
|
||||||
// fake preimage, she should fail and force close the channel.
|
|
||||||
var predErr error
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
|
||||||
pendingChansRequest)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n := len(pendingChanResp.WaitingCloseChannels)
|
|
||||||
if n != 1 {
|
|
||||||
predErr = fmt.Errorf("Expected to find %d channels "+
|
|
||||||
"waiting close, found %d", 1, n)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine a block to confirm the broadcasted commitment.
|
|
||||||
block := mineBlocks(t, net, 1, 1)[0]
|
|
||||||
if len(block.Transactions) != 2 {
|
|
||||||
t.Fatalf("transaction wasn't mined")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The channel should now show up as force closed both for Alice and
|
|
||||||
// Carol.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
|
||||||
pendingChansRequest)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n := len(pendingChanResp.WaitingCloseChannels)
|
|
||||||
if n != 0 {
|
|
||||||
predErr = fmt.Errorf("Expected to find %d channels "+
|
|
||||||
"waiting close, found %d", 0, n)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n = len(pendingChanResp.PendingForceClosingChannels)
|
|
||||||
if n != 1 {
|
|
||||||
predErr = fmt.Errorf("expected to find %d channel "+
|
|
||||||
"pending force close, found %d", 1, n)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := carol.PendingChannels(ctxt,
|
|
||||||
pendingChansRequest)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n := len(pendingChanResp.PendingForceClosingChannels)
|
|
||||||
if n != 1 {
|
|
||||||
predErr = fmt.Errorf("expected to find %d channel "+
|
|
||||||
"pending force close, found %d", 1, n)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol will use the correct preimage to resolve the HTLC on-chain.
|
|
||||||
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to find Carol's resolve tx in mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine enough blocks for Alice to sweep her funds from the force
|
|
||||||
// closed channel.
|
|
||||||
_, err = net.Miner.Client.Generate(defaultCSV - 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate blocks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the sweeping tx to be broadcast.
|
|
||||||
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to find Alice's sweep tx in mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine the sweep.
|
|
||||||
_, err = net.Miner.Client.Generate(1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate blocks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// No pending channels should be left.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := net.Alice.PendingChannels(ctxt,
|
|
||||||
pendingChansRequest)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n := len(pendingChanResp.PendingForceClosingChannels)
|
|
||||||
if n != 0 {
|
|
||||||
predErr = fmt.Errorf("expected to find %d channel "+
|
|
||||||
"pending force close, found %d", 0, n)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testGarbageCollectLinkNodes tests that we properly garbase collect link nodes
|
// testGarbageCollectLinkNodes tests that we properly garbase collect link nodes
|
||||||
// from the database and the set of persistent connections within the server.
|
// from the database and the set of persistent connections within the server.
|
||||||
func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) {
|
func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
Loading…
Reference in New Issue
Block a user