lntest/itest: add new itest for 3rd party anchor spends

In this commit, we add a new integration tests to exercise the fix
introduced in the prior commit. In this test, we reconstruct a scenario
for a 3rd party to sweep an anchor spend after force closing, causing a
prior spend we had to be invalidated. Without the prior commit, this test
fails as the original anchor sweep is still found in the wallet.
This commit is contained in:
Olaoluwa Osuntokun 2022-03-03 17:03:54 -08:00
parent e1e9de24df
commit e5625878e9
No known key found for this signature in database
GPG Key ID: 3BBD59E99B280306
4 changed files with 316 additions and 6 deletions

View File

@ -1789,3 +1789,70 @@ func assertChannelPolicyUpdate(t *testing.T, node *lntest.HarnessNode,
), "error while waiting for channel update",
)
}
func transactionInWallet(node *lntest.HarnessNode, txid chainhash.Hash) bool {
txStr := txid.String()
txResp, err := node.GetTransactions(
context.Background(), &lnrpc.GetTransactionsRequest{},
)
if err != nil {
return false
}
for _, txn := range txResp.Transactions {
if txn.TxHash == txStr {
return true
}
}
return false
}
func assertTransactionInWallet(t *testing.T, node *lntest.HarnessNode, txID chainhash.Hash) {
t.Helper()
err := wait.Predicate(func() bool {
return transactionInWallet(node, txID)
}, defaultTimeout)
require.NoError(
t, err, fmt.Sprintf("transaction %v not found in wallet", txID),
)
}
func assertTransactionNotInWallet(t *testing.T, node *lntest.HarnessNode,
txID chainhash.Hash) {
t.Helper()
err := wait.Predicate(func() bool {
return !transactionInWallet(node, txID)
}, defaultTimeout)
require.NoError(
t, err, fmt.Sprintf("transaction %v found in wallet", txID),
)
}
func assertAnchorOutputLost(t *harnessTest, node *lntest.HarnessNode,
chanPoint wire.OutPoint) {
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
err := wait.Predicate(func() bool {
resp, pErr := node.PendingChannels(
context.Background(), pendingChansRequest,
)
if pErr != nil {
return false
}
for _, pendingChan := range resp.PendingForceClosingChannels {
if pendingChan.Channel.ChannelPoint == chanPoint.String() {
return (pendingChan.Anchor ==
lnrpc.PendingChannelsResponse_ForceClosedChannel_LOST)
}
}
return false
}, defaultTimeout)
require.NoError(t.t, err, "anchor doesn't show as being lost")
}

View File

@ -526,7 +526,7 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
aliceReports[aliceAnchor.OutPoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_ANCHOR,
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
SweepTxid: aliceAnchor.SweepTx,
SweepTxid: aliceAnchor.SweepTx.TxHash().String(),
Outpoint: &lnrpc.OutPoint{
TxidBytes: aliceAnchor.OutPoint.Hash[:],
TxidStr: aliceAnchor.OutPoint.Hash.String(),
@ -632,7 +632,7 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
carolReports[carolAnchor.OutPoint.String()] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_ANCHOR,
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
SweepTxid: carolAnchor.SweepTx,
SweepTxid: carolAnchor.SweepTx.TxHash().String(),
AmountSat: anchorSize,
Outpoint: &lnrpc.OutPoint{
TxidBytes: carolAnchor.OutPoint.Hash[:],
@ -770,7 +770,7 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
OutputIndex: carolCommit.OutPoint.Index,
},
AmountSat: uint64(pushAmt),
SweepTxid: carolCommit.SweepTx,
SweepTxid: carolCommit.SweepTx.TxHash().String(),
}
// Check that we can find the commitment sweep in our set of known
@ -1337,7 +1337,7 @@ func padCLTV(cltv uint32) uint32 {
type sweptOutput struct {
OutPoint wire.OutPoint
SweepTx string
SweepTx *wire.MsgTx
}
// findCommitAndAnchor looks for a commitment sweep and anchor sweep in the
@ -1364,7 +1364,7 @@ func findCommitAndAnchor(t *harnessTest, net *lntest.NetworkHarness,
if len(inputs) == 1 {
commitSweep = &sweptOutput{
OutPoint: inputs[0].PreviousOutPoint,
SweepTx: txHash.String(),
SweepTx: tx,
}
} else {
// Since we have more than one input, we run through
@ -1375,7 +1375,7 @@ func findCommitAndAnchor(t *harnessTest, net *lntest.NetworkHarness,
if outpointStr == closeTx {
anchorSweep = &sweptOutput{
OutPoint: txin.PreviousOutPoint,
SweepTx: txHash.String(),
SweepTx: tx,
}
}
}

View File

@ -5,9 +5,12 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
@ -379,3 +382,239 @@ func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) {
t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut))
}
}
// genAnchorSweep generates a "3rd party" anchor sweeping from an existing one.
// In practice, we just re-use the existing witness, and track on our own
// output producing a 1-in-1-out transaction.
func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness,
aliceAnchor *sweptOutput, anchorCsv uint32) *btcutil.Tx {
// At this point, we have the transaction that Alice used to try to
// sweep her anchor. As this is actually just something anyone can
// spend, just need to find the input spending the anchor output, then
// we can swap the output address.
aliceAnchorTxIn := func() wire.TxIn {
sweepCopy := aliceAnchor.SweepTx.Copy()
for _, txIn := range sweepCopy.TxIn {
if txIn.PreviousOutPoint == aliceAnchor.OutPoint {
return *txIn
}
}
t.Fatalf("anchor op not found")
return wire.TxIn{}
}()
// We'll set the signature on the input to nil, and then set the
// sequence to 16 (the anchor CSV period).
aliceAnchorTxIn.Witness[0] = nil
aliceAnchorTxIn.Sequence = anchorCsv
minerAddr, err := net.Miner.NewAddress()
if err != nil {
t.Fatalf("unable to get miner addr: %v", err)
}
addrScript, err := txscript.PayToAddrScript(minerAddr)
if err != nil {
t.Fatalf("unable to gen addr script: %v", err)
}
// Now that we have the txIn, we can just make a new transaction that
// uses a different script for the output.
tx := wire.NewMsgTx(2)
tx.AddTxIn(&aliceAnchorTxIn)
tx.AddTxOut(&wire.TxOut{
PkScript: addrScript,
Value: anchorSize - 1,
})
return btcutil.NewTx(tx)
}
// testAnchorThirdPartySpend tests that if we force close a channel, but then
// don't sweep the anchor in time and a 3rd party spends it, that we remove any
// transactions that are a descendent of that sweep.
func testAnchorThirdPartySpend(net *lntest.NetworkHarness, t *harnessTest) {
// First, we'll create two new nodes that both default to anchor
// channels.
//
// NOTE: The itests differ here as anchors is default off vs the normal
// lnd binary.
args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
alice := net.NewNode(t.t, "Alice", args)
defer shutdownAndAssert(net, t, alice)
bob := net.NewNode(t.t, "Bob", args)
defer shutdownAndAssert(net, t, bob)
ctxb := context.Background()
net.ConnectNodes(t.t, alice, bob)
// We'll fund our Alice with coins, as she'll be opening the channel.
// We'll fund her with *just* enough coins to open the channel.
const (
firstChanSize = 1_000_000
anchorFeeBuffer = 500_000
)
net.SendCoins(t.t, firstChanSize, alice)
// We'll give Alice another spare UTXO as well so she can use it to
// help sweep all coins.
net.SendCoins(t.t, anchorFeeBuffer, alice)
// Open the channel between the two nodes and wait for it to confirm
// fully.
aliceChanPoint1 := openChannelAndAssert(
t, net, alice, bob, lntest.OpenChannelParams{
Amt: firstChanSize,
},
)
// With the channel open, we'll actually immediately force close it. We
// don't care about network announcements here since there's no routing
// in this test.
_, _, err := net.CloseChannel(alice, aliceChanPoint1, 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())
}
// Get the normal channel outpoint so we can track it in the set of
// channels that are waiting to be closed.
fundingTxID, err := lnrpc.GetChanPointFundingTxid(aliceChanPoint1)
if err != nil {
t.Fatalf("unable to get txid: %v", err)
}
chanPoint := wire.OutPoint{
Hash: *fundingTxID,
Index: aliceChanPoint1.OutputIndex,
}
waitingClose, err := findWaitingCloseChannel(pendingChanResp, &chanPoint)
if err != nil {
t.Fatalf(err.Error())
}
// At this point, the channel is waiting close, and we have both the
// commitment transaction and anchor sweep in the mempool.
const expectedTxns = 2
sweepTxns, err := getNTxsFromMempool(
net.Miner.Client, expectedTxns, minerMempoolTimeout,
)
require.NoError(t.t, err, "no sweep txns in miner mempool")
aliceCloseTx := waitingClose.Commitments.LocalTxid
_, aliceAnchor := findCommitAndAnchor(t, net, sweepTxns, aliceCloseTx)
// We'll now mine _only_ the commitment force close transaction, as we
// want the anchor sweep to stay unconfirmed.
var emptyTime time.Time
forceCloseTxID, _ := chainhash.NewHashFromStr(aliceCloseTx)
commitTxn, err := net.Miner.Client.GetRawTransaction(
forceCloseTxID,
)
if err != nil {
t.Fatalf("unable to get transaction: %v", err)
}
_, err = net.Miner.GenerateAndSubmitBlock(
[]*btcutil.Tx{commitTxn}, -1, emptyTime,
)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// With the anchor output located, and the main commitment mined we'll
// instruct the wallet to send all coins in the wallet to a new address
// (to the miner), including unconfirmed change.
minerAddr, err := net.Miner.NewAddress()
if err != nil {
t.Fatalf("unable to create new miner addr: %v", err)
}
sweepReq := &lnrpc.SendCoinsRequest{
Addr: minerAddr.String(),
SendAll: true,
MinConfs: 0,
SpendUnconfirmed: true,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
sweepAllResp, err := alice.SendCoins(ctxt, sweepReq)
if err != nil {
t.Fatalf("unable to sweep coins: %v", err)
}
// Both the original anchor sweep transaction, as well as the
// transaction we created to sweep all the coins from Alice's wallet
// should be found in her transaction store.
sweepAllTxID, _ := chainhash.NewHashFromStr(sweepAllResp.Txid)
assertTransactionInWallet(t.t, alice, aliceAnchor.SweepTx.TxHash())
assertTransactionInWallet(t.t, alice, *sweepAllTxID)
// Next, we'll shutdown Alice, and allow 16 blocks to pass so that the
// anchor output can be swept by anyone. Rather than use the normal API
// call, we'll generate a series of _empty_ blocks here.
aliceRestart, err := net.SuspendNode(alice)
if err != nil {
t.Fatalf("unable to shutdown alice: %v", err)
}
const anchorCsv = 16
for i := 0; i < anchorCsv; i++ {
_, err := net.Miner.GenerateAndSubmitBlock(nil, -1, emptyTime)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
}
// Before we sweep the anchor, we'll restart Alice.
if err := aliceRestart(); err != nil {
t.Fatalf("unable to restart alice: %v", err)
}
// Now that the channel has been closed, and Alice has an unconfirmed
// transaction spending the output produced by her anchor sweep, we'll
// mine a transaction that double spends the output.
thirdPartyAnchorSweep := genAnchorSweep(t, net, aliceAnchor, anchorCsv)
_, err = net.Miner.GenerateAndSubmitBlock(
[]*btcutil.Tx{thirdPartyAnchorSweep}, -1, emptyTime,
)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// At this point, we should no longer find Alice's transaction that
// tried to sweep the anchor in her wallet.
assertTransactionNotInWallet(t.t, alice, aliceAnchor.SweepTx.TxHash())
// In addition, the transaction she sent to sweep all her coins to the
// miner also should no longer be found.
assertTransactionNotInWallet(t.t, alice, *sweepAllTxID)
// The anchor should now show as being "lost", while the force close
// response is still present.
assertAnchorOutputLost(t, alice, chanPoint)
// At this point Alice's CSV output should already be fully spent and
// the channel marked as being resolved. We mine a block first, as so
// far we've been generating custom blocks this whole time..
commitSweepOp := wire.OutPoint{
Hash: *forceCloseTxID,
Index: 1,
}
assertSpendingTxInMempool(
t, net.Miner.Client, minerMempoolTimeout, commitSweepOp,
)
_, err = net.Miner.Client.Generate(1)
if err != nil {
t.Fatalf("unable to generate block: %v", err)
}
assertNumPendingChannels(t, alice, 0, 0)
}

View File

@ -379,4 +379,8 @@ var allTestCases = []*testCase{
name: "remote signer",
test: testRemoteSigner,
},
{
name: "3rd party anchor spend",
test: testAnchorThirdPartySpend,
},
}