lntemp+itest: refactor testAnchorThirdPartySpend
This commit is contained in:
8 files changed
+278
-279
No files matched your search
@@ -1391,6 +1391,19 @@ func (h *HarnessTest) MineBlocksAndAssertNumTxes(num uint32,
|
|||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MineEmptyBlocks mines a given number of empty blocks.
|
||||||
|
//
|
||||||
|
// NOTE: this differs from miner's `MineEmptyBlocks` as it requires the nodes
|
||||||
|
// to be synced.
|
||||||
|
func (h *HarnessTest) MineEmptyBlocks(num int) []*wire.MsgBlock {
|
||||||
|
blocks := h.Miner.MineEmptyBlocks(num)
|
||||||
|
|
||||||
|
// Finally, make sure all the active nodes are synced.
|
||||||
|
h.AssertActiveNodesSynced()
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
// QueryChannelByChanPoint tries to find a channel matching the channel point
|
// QueryChannelByChanPoint tries to find a channel matching the channel point
|
||||||
// and asserts. It returns the channel found.
|
// and asserts. It returns the channel found.
|
||||||
func (h *HarnessTest) QueryChannelByChanPoint(hn *node.HarnessNode,
|
func (h *HarnessTest) QueryChannelByChanPoint(hn *node.HarnessNode,
|
||||||
|
|||||||
@@ -1927,3 +1927,45 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
|
|||||||
|
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertTransactionInWallet asserts a given txid can be found in the node's
|
||||||
|
// wallet.
|
||||||
|
func (h *HarnessTest) AssertTransactionInWallet(hn *node.HarnessNode,
|
||||||
|
txid chainhash.Hash) {
|
||||||
|
|
||||||
|
req := &lnrpc.GetTransactionsRequest{}
|
||||||
|
err := wait.NoError(func() error {
|
||||||
|
txResp := hn.RPC.GetTransactions(req)
|
||||||
|
for _, txn := range txResp.Transactions {
|
||||||
|
if txn.TxHash == txid.String() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s: expected txid=%v not found in wallet",
|
||||||
|
hn.Name(), txid)
|
||||||
|
}, DefaultTimeout)
|
||||||
|
|
||||||
|
require.NoError(h, err, "failed to find tx")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertTransactionNotInWallet asserts a given txid can NOT be found in the
|
||||||
|
// node's wallet.
|
||||||
|
func (h *HarnessTest) AssertTransactionNotInWallet(hn *node.HarnessNode,
|
||||||
|
txid chainhash.Hash) {
|
||||||
|
|
||||||
|
req := &lnrpc.GetTransactionsRequest{}
|
||||||
|
err := wait.NoError(func() error {
|
||||||
|
txResp := hn.RPC.GetTransactions(req)
|
||||||
|
for _, txn := range txResp.Transactions {
|
||||||
|
if txn.TxHash == txid.String() {
|
||||||
|
return fmt.Errorf("expected txid=%v to be "+
|
||||||
|
"not found", txid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, DefaultTimeout)
|
||||||
|
|
||||||
|
require.NoErrorf(h, err, "%s: failed to assert tx not found", hn.Name())
|
||||||
|
}
|
||||||
@@ -383,3 +383,35 @@ func (h *HarnessMiner) NewMinerAddress() btcutil.Address {
|
|||||||
require.NoError(h, err, "failed to create new miner address")
|
require.NoError(h, err, "failed to create new miner address")
|
||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MineBlocksWithTxes mines a single block to include the specifies
|
||||||
|
// transactions only.
|
||||||
|
func (h *HarnessMiner) MineBlockWithTxes(txes []*btcutil.Tx) *wire.MsgBlock {
|
||||||
|
var emptyTime time.Time
|
||||||
|
|
||||||
|
// Generate a block.
|
||||||
|
b, err := h.GenerateAndSubmitBlock(txes, -1, emptyTime)
|
||||||
|
require.NoError(h, err, "unable to mine block")
|
||||||
|
|
||||||
|
block, err := h.Client.GetBlock(b.Hash())
|
||||||
|
require.NoError(h, err, "unable to get block")
|
||||||
|
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
// MineEmptyBlocks mines a given number of empty blocks.
|
||||||
|
func (h *HarnessMiner) MineEmptyBlocks(num int) []*wire.MsgBlock {
|
||||||
|
var emptyTime time.Time
|
||||||
|
|
||||||
|
blocks := make([]*wire.MsgBlock, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
// Generate an empty block.
|
||||||
|
b, err := h.GenerateAndSubmitBlock(nil, -1, emptyTime)
|
||||||
|
require.NoError(h, err, "unable to mine empty block")
|
||||||
|
|
||||||
|
block := h.GetBlock(b.Hash())
|
||||||
|
blocks[i] = block
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
@@ -1418,30 +1418,6 @@ func assertTransactionNotInWallet(t *testing.T, node *lntest.HarnessNode,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// assertNodeAnnouncement compares that two node announcements match.
|
// assertNodeAnnouncement compares that two node announcements match.
|
||||||
func assertNodeAnnouncement(t *harnessTest, n1, n2 *lnrpc.NodeUpdate) {
|
func assertNodeAnnouncement(t *harnessTest, n1, n2 *lnrpc.NodeUpdate) {
|
||||||
// Alias should match.
|
// Alias should match.
|
||||||
|
|||||||
@@ -267,4 +267,8 @@ var allTestCasesTemp = []*lntemp.TestCase{
|
|||||||
Name: "anchors reserved value",
|
Name: "anchors reserved value",
|
||||||
TestFunc: testAnchorReservedValue,
|
TestFunc: testAnchorReservedValue,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "3rd party anchor spend",
|
||||||
|
TestFunc: testAnchorThirdPartySpend,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntemp"
|
"github.com/lightningnetwork/lnd/lntemp"
|
||||||
"github.com/lightningnetwork/lnd/lntemp/node"
|
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
|
||||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
@@ -984,57 +983,6 @@ func padCLTV(cltv uint32) uint32 {
|
|||||||
return cltv + uint32(routing.BlockPadding)
|
return cltv + uint32(routing.BlockPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sweptOutput struct {
|
|
||||||
OutPoint wire.OutPoint
|
|
||||||
SweepTx *wire.MsgTx
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// TODO(yy): delete.
|
|
||||||
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: tx,
|
|
||||||
}
|
|
||||||
} 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: tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return commitSweep, anchorSweep
|
|
||||||
}
|
|
||||||
|
|
||||||
// testFailingChannel tests that we will fail the channel by force closing it
|
// testFailingChannel tests that we will fail the channel by force closing it
|
||||||
// in the case where a counterparty tries to settle an HTLC with the wrong
|
// in the case where a counterparty tries to settle an HTLC with the wrong
|
||||||
// preimage.
|
// preimage.
|
||||||
|
|||||||
+187
-199
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntemp"
|
"github.com/lightningnetwork/lnd/lntemp"
|
||||||
|
"github.com/lightningnetwork/lnd/lntemp/node"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
@@ -396,11 +396,191 @@ func testAnchorReservedValue(ht *lntemp.HarnessTest) {
|
|||||||
assertNumTxInAndTxOut(sweepTx, 4, 1)
|
assertNumTxInAndTxOut(sweepTx, 4, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(ht *lntemp.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 := ht.NewNode("Alice", args)
|
||||||
|
defer ht.Shutdown(alice)
|
||||||
|
|
||||||
|
bob := ht.NewNode("Bob", args)
|
||||||
|
defer ht.Shutdown(bob)
|
||||||
|
|
||||||
|
ht.EnsureConnected(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 and
|
||||||
|
// sweep the anchor.
|
||||||
|
const (
|
||||||
|
firstChanSize = 1_000_000
|
||||||
|
anchorFeeBuffer = 500_000
|
||||||
|
)
|
||||||
|
ht.FundCoins(firstChanSize+anchorFeeBuffer, alice)
|
||||||
|
|
||||||
|
// Open the channel between the two nodes and wait for it to confirm
|
||||||
|
// fully.
|
||||||
|
aliceChanPoint1 := ht.OpenChannel(
|
||||||
|
alice, bob, lntemp.OpenChannelParams{
|
||||||
|
Amt: firstChanSize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send another UTXO if this is a neutrino backend. When sweeping
|
||||||
|
// anchors, there are two transactions created, `local_sweep_tx` for
|
||||||
|
// sweeping Alice's anchor on the local commitment, `remote_sweep_tx`
|
||||||
|
// for sweeping her anchor on the remote commitment. Whenever the force
|
||||||
|
// close transaction is published, Alice will always create these two
|
||||||
|
// transactions to sweep her anchor.
|
||||||
|
// On the other hand, when creating the sweep txes, the anchor itself
|
||||||
|
// is not able to cover the fee, so another wallet UTXO is needed. In
|
||||||
|
// our test case, there's a change output that can be used from the
|
||||||
|
// above funding process. And it's used by both sweep txes - when `lnd`
|
||||||
|
// happens to create the `remote_sweep_tx` first, it will receive an
|
||||||
|
// error since its parent tx, the remote commitment, is not known,
|
||||||
|
// hence freeing the change output to be used by `local_sweep_tx`.
|
||||||
|
// For neutrino client, however, it will consider the transaction which
|
||||||
|
// sweeps the remote anchor as an orphan tx, and it will neither send
|
||||||
|
// it to the mempool nor return an error to free the change output.
|
||||||
|
// Thus, if the change output is already used in `remote_sweep_tx`, we
|
||||||
|
// won't have UTXO to create `local_sweep_tx`.
|
||||||
|
//
|
||||||
|
// NOTE: the order of the sweep requests for the two anchors cannot be
|
||||||
|
// guaranteed. If the sweeper happens to sweep the remote anchor first,
|
||||||
|
// then the test won't pass without the extra UTXO, which is the source
|
||||||
|
// of the flakeness.
|
||||||
|
//
|
||||||
|
// TODO(yy): make a RPC server for sweeper so we can explicitly check
|
||||||
|
// and control its state.
|
||||||
|
if ht.IsNeutrinoBackend() {
|
||||||
|
ht.FundCoins(anchorFeeBuffer, alice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
ht.CloseChannelAssertPending(alice, aliceChanPoint1, true)
|
||||||
|
|
||||||
|
// Now that the channel has been force closed, it should show up in the
|
||||||
|
// PendingChannels RPC under the waiting close section.
|
||||||
|
waitingClose := ht.AssertChannelWaitingClose(alice, aliceChanPoint1)
|
||||||
|
|
||||||
|
// 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 := ht.Miner.GetNumTxsFromMempool(expectedTxns)
|
||||||
|
aliceCloseTx := waitingClose.Commitments.LocalTxid
|
||||||
|
_, aliceAnchor := ht.FindCommitAndAnchor(sweepTxns, aliceCloseTx)
|
||||||
|
|
||||||
|
// We'll now mine _only_ the commitment force close transaction, as we
|
||||||
|
// want the anchor sweep to stay unconfirmed.
|
||||||
|
forceCloseTxID, _ := chainhash.NewHashFromStr(aliceCloseTx)
|
||||||
|
commitTxn := ht.Miner.GetRawTransaction(forceCloseTxID)
|
||||||
|
ht.Miner.MineBlockWithTxes([]*btcutil.Tx{commitTxn})
|
||||||
|
|
||||||
|
// 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 := ht.Miner.NewMinerAddress()
|
||||||
|
sweepReq := &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: minerAddr.String(),
|
||||||
|
SendAll: true,
|
||||||
|
MinConfs: 0,
|
||||||
|
SpendUnconfirmed: true,
|
||||||
|
}
|
||||||
|
sweepAllResp := alice.RPC.SendCoins(sweepReq)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
ht.AssertTransactionInWallet(alice, aliceAnchor.SweepTx.TxHash())
|
||||||
|
ht.AssertTransactionInWallet(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 := ht.SuspendNode(alice)
|
||||||
|
const anchorCsv = 16
|
||||||
|
ht.MineEmptyBlocks(anchorCsv)
|
||||||
|
|
||||||
|
// Before we sweep the anchor, we'll restart Alice.
|
||||||
|
require.NoErrorf(ht, aliceRestart(), "unable to restart alice")
|
||||||
|
|
||||||
|
// 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(ht, aliceAnchor, anchorCsv)
|
||||||
|
ht.Miner.MineBlockWithTxes([]*btcutil.Tx{thirdPartyAnchorSweep})
|
||||||
|
|
||||||
|
// At this point, we should no longer find Alice's transaction that
|
||||||
|
// tried to sweep the anchor in her wallet.
|
||||||
|
ht.AssertTransactionNotInWallet(alice, aliceAnchor.SweepTx.TxHash())
|
||||||
|
|
||||||
|
// In addition, the transaction she sent to sweep all her coins to the
|
||||||
|
// miner also should no longer be found.
|
||||||
|
ht.AssertTransactionNotInWallet(alice, *sweepAllTxID)
|
||||||
|
|
||||||
|
// The anchor should now show as being "lost", while the force close
|
||||||
|
// response is still present.
|
||||||
|
assertAnchorOutputLost(ht, alice, aliceChanPoint1)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
ht.Miner.AssertOutpointInMempool(commitSweepOp)
|
||||||
|
ht.MineBlocks(1)
|
||||||
|
|
||||||
|
ht.AssertNumWaitingClose(alice, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertAnchorOutputLost asserts that the anchor output for the given channel
|
||||||
|
// has the state of being lost.
|
||||||
|
func assertAnchorOutputLost(ht *lntemp.HarnessTest, hn *node.HarnessNode,
|
||||||
|
chanPoint *lnrpc.ChannelPoint) {
|
||||||
|
|
||||||
|
cp := ht.OutPointFromChannelPoint(chanPoint)
|
||||||
|
|
||||||
|
expected := lnrpc.PendingChannelsResponse_ForceClosedChannel_LOST
|
||||||
|
|
||||||
|
err := wait.NoError(func() error {
|
||||||
|
resp := hn.RPC.PendingChannels()
|
||||||
|
channels := resp.PendingForceClosingChannels
|
||||||
|
|
||||||
|
for _, c := range channels {
|
||||||
|
// Not the wanted channel, skipped.
|
||||||
|
if c.Channel.ChannelPoint != cp.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found the channel, check the anchor state.
|
||||||
|
if c.Anchor == expected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unexpected anchor state, want %v, "+
|
||||||
|
"got %v", expected, c.Anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("channel not found using cp=%v", cp)
|
||||||
|
}, defaultTimeout)
|
||||||
|
require.NoError(ht, err, "anchor doesn't show as being lost")
|
||||||
|
}
|
||||||
|
|
||||||
// genAnchorSweep generates a "3rd party" anchor sweeping from an existing one.
|
// 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
|
// In practice, we just re-use the existing witness, and track on our own
|
||||||
// output producing a 1-in-1-out transaction.
|
// output producing a 1-in-1-out transaction.
|
||||||
func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness,
|
func genAnchorSweep(ht *lntemp.HarnessTest,
|
||||||
aliceAnchor *sweptOutput, anchorCsv uint32) *btcutil.Tx {
|
aliceAnchor *lntemp.SweptOutput, anchorCsv uint32) *btcutil.Tx {
|
||||||
|
|
||||||
// At this point, we have the transaction that Alice used to try to
|
// At this point, we have the transaction that Alice used to try to
|
||||||
// sweep her anchor. As this is actually just something anyone can
|
// sweep her anchor. As this is actually just something anyone can
|
||||||
@@ -414,7 +594,8 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Fatalf("anchor op not found")
|
require.FailNow(ht, "anchor op not found")
|
||||||
|
|
||||||
return wire.TxIn{}
|
return wire.TxIn{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -423,14 +604,9 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness,
|
|||||||
aliceAnchorTxIn.Witness[0] = nil
|
aliceAnchorTxIn.Witness[0] = nil
|
||||||
aliceAnchorTxIn.Sequence = anchorCsv
|
aliceAnchorTxIn.Sequence = anchorCsv
|
||||||
|
|
||||||
minerAddr, err := net.Miner.NewAddress()
|
minerAddr := ht.Miner.NewMinerAddress()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get miner addr: %v", err)
|
|
||||||
}
|
|
||||||
addrScript, err := txscript.PayToAddrScript(minerAddr)
|
addrScript, err := txscript.PayToAddrScript(minerAddr)
|
||||||
if err != nil {
|
require.NoError(ht, err, "unable to gen addr script")
|
||||||
t.Fatalf("unable to gen addr script: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the txIn, we can just make a new transaction that
|
// Now that we have the txIn, we can just make a new transaction that
|
||||||
// uses a different script for the output.
|
// uses a different script for the output.
|
||||||
@@ -443,191 +619,3 @@ func genAnchorSweep(t *harnessTest, net *lntest.NetworkHarness,
|
|||||||
|
|
||||||
return btcutil.NewTx(tx)
|
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)
|
|
||||||
}
|
|
||||||
@@ -194,10 +194,6 @@ var allTestCases = []*testCase{
|
|||||||
name: "remote signer",
|
name: "remote signer",
|
||||||
test: testRemoteSigner,
|
test: testRemoteSigner,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "3rd party anchor spend",
|
|
||||||
test: testAnchorThirdPartySpend,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "taproot",
|
name: "taproot",
|
||||||
test: testTaproot,
|
test: testTaproot,
|
||||||
|
|||||||
Reference in new issue
Block a user