You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
11 KiB
362 lines
11 KiB
package itest |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"fmt" |
|
"strings" |
|
|
|
"github.com/btcsuite/btcd/txscript" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/lightningnetwork/lnd/lnrpc" |
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" |
|
"github.com/lightningnetwork/lnd/lntest" |
|
"github.com/lightningnetwork/lnd/lntest/wait" |
|
"github.com/lightningnetwork/lnd/lnwallet" |
|
"github.com/lightningnetwork/lnd/sweep" |
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
// testCPFP ensures that the daemon can bump an unconfirmed transaction's fee |
|
// rate by broadcasting a Child-Pays-For-Parent (CPFP) transaction. |
|
// |
|
// TODO(wilmer): Add RBF case once btcd supports it. |
|
func testCPFP(net *lntest.NetworkHarness, t *harnessTest) { |
|
// Skip this test for neutrino, as it's not aware of mempool |
|
// transactions. |
|
if net.BackendCfg.Name() == "neutrino" { |
|
t.Skipf("skipping CPFP test for neutrino backend") |
|
} |
|
|
|
// We'll start the test by sending Alice some coins, which she'll use to |
|
// send to Bob. |
|
ctxb := context.Background() |
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) |
|
err := net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, net.Alice) |
|
if err != nil { |
|
t.Fatalf("unable to send coins to alice: %v", err) |
|
} |
|
|
|
// Create an address for Bob to send the coins to. |
|
addrReq := &lnrpc.NewAddressRequest{ |
|
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
resp, err := net.Bob.NewAddress(ctxt, addrReq) |
|
if err != nil { |
|
t.Fatalf("unable to get new address for bob: %v", err) |
|
} |
|
|
|
// Send the coins from Alice to Bob. We should expect a transaction to |
|
// be broadcast and seen in the mempool. |
|
sendReq := &lnrpc.SendCoinsRequest{ |
|
Addr: resp.Address, |
|
Amount: btcutil.SatoshiPerBitcoin, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
if _, err = net.Alice.SendCoins(ctxt, sendReq); err != nil { |
|
t.Fatalf("unable to send coins to bob: %v", err) |
|
} |
|
|
|
txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) |
|
if err != nil { |
|
t.Fatalf("expected one mempool transaction: %v", err) |
|
} |
|
|
|
// We'll then extract the raw transaction from the mempool in order to |
|
// determine the index of Bob's output. |
|
tx, err := net.Miner.Client.GetRawTransaction(txid) |
|
if err != nil { |
|
t.Fatalf("unable to extract raw transaction from mempool: %v", |
|
err) |
|
} |
|
bobOutputIdx := -1 |
|
for i, txOut := range tx.MsgTx().TxOut { |
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs( |
|
txOut.PkScript, net.Miner.ActiveNet, |
|
) |
|
if err != nil { |
|
t.Fatalf("unable to extract address from pkScript=%x: "+ |
|
"%v", txOut.PkScript, err) |
|
} |
|
if addrs[0].String() == resp.Address { |
|
bobOutputIdx = i |
|
} |
|
} |
|
if bobOutputIdx == -1 { |
|
t.Fatalf("bob's output was not found within the transaction") |
|
} |
|
|
|
// Wait until bob has seen the tx and considers it as owned. |
|
op := &lnrpc.OutPoint{ |
|
TxidBytes: txid[:], |
|
OutputIndex: uint32(bobOutputIdx), |
|
} |
|
assertWalletUnspent(t, net.Bob, op) |
|
|
|
// We'll attempt to bump the fee of this transaction by performing a |
|
// CPFP from Alice's point of view. |
|
bumpFeeReq := &walletrpc.BumpFeeRequest{ |
|
Outpoint: op, |
|
SatPerVbyte: uint64( |
|
sweep.DefaultMaxFeeRate.FeePerKVByte() / 2000, |
|
), |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
_, err = net.Bob.WalletKitClient.BumpFee(ctxt, bumpFeeReq) |
|
if err != nil { |
|
t.Fatalf("unable to bump fee: %v", err) |
|
} |
|
|
|
// We should now expect to see two transactions within the mempool, a |
|
// parent and its child. |
|
_, err = waitForNTxsInMempool(net.Miner.Client, 2, minerMempoolTimeout) |
|
if err != nil { |
|
t.Fatalf("expected two mempool transactions: %v", err) |
|
} |
|
|
|
// We should also expect to see the output being swept by the |
|
// UtxoSweeper. We'll ensure it's using the fee rate specified. |
|
pendingSweepsReq := &walletrpc.PendingSweepsRequest{} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
pendingSweepsResp, err := net.Bob.WalletKitClient.PendingSweeps( |
|
ctxt, pendingSweepsReq, |
|
) |
|
if err != nil { |
|
t.Fatalf("unable to retrieve pending sweeps: %v", err) |
|
} |
|
if len(pendingSweepsResp.PendingSweeps) != 1 { |
|
t.Fatalf("expected to find %v pending sweep(s), found %v", 1, |
|
len(pendingSweepsResp.PendingSweeps)) |
|
} |
|
pendingSweep := pendingSweepsResp.PendingSweeps[0] |
|
if !bytes.Equal(pendingSweep.Outpoint.TxidBytes, op.TxidBytes) { |
|
t.Fatalf("expected output txid %x, got %x", op.TxidBytes, |
|
pendingSweep.Outpoint.TxidBytes) |
|
} |
|
if pendingSweep.Outpoint.OutputIndex != op.OutputIndex { |
|
t.Fatalf("expected output index %v, got %v", op.OutputIndex, |
|
pendingSweep.Outpoint.OutputIndex) |
|
} |
|
if pendingSweep.SatPerVbyte != bumpFeeReq.SatPerVbyte { |
|
t.Fatalf("expected sweep sat per vbyte %v, got %v", |
|
bumpFeeReq.SatPerVbyte, pendingSweep.SatPerVbyte) |
|
} |
|
|
|
// Mine a block to clean up the unconfirmed transactions. |
|
mineBlocks(t, net, 1, 2) |
|
|
|
// The input used to CPFP should no longer be pending. |
|
err = wait.NoError(func() error { |
|
req := &walletrpc.PendingSweepsRequest{} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
resp, err := net.Bob.WalletKitClient.PendingSweeps(ctxt, req) |
|
if err != nil { |
|
return fmt.Errorf("unable to retrieve bob's pending "+ |
|
"sweeps: %v", err) |
|
} |
|
if len(resp.PendingSweeps) != 0 { |
|
return fmt.Errorf("expected 0 pending sweeps, found %d", |
|
len(resp.PendingSweeps)) |
|
} |
|
return nil |
|
}, defaultTimeout) |
|
if err != nil { |
|
t.Fatalf(err.Error()) |
|
} |
|
} |
|
|
|
// testAnchorReservedValue tests that we won't allow sending transactions when |
|
// that would take the value we reserve for anchor fee bumping out of our |
|
// wallet. |
|
func testAnchorReservedValue(net *lntest.NetworkHarness, t *harnessTest) { |
|
// Start two nodes supporting anchor channels. |
|
args := commitTypeAnchors.Args() |
|
alice, err := net.NewNode("Alice", args) |
|
require.NoError(t.t, err) |
|
|
|
defer shutdownAndAssert(net, t, alice) |
|
|
|
bob, err := net.NewNode("Bob", args) |
|
require.NoError(t.t, err) |
|
|
|
defer shutdownAndAssert(net, t, bob) |
|
|
|
ctxb := context.Background() |
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) |
|
err = net.ConnectNodes(ctxt, alice, bob) |
|
require.NoError(t.t, err) |
|
|
|
// Send just enough coins for Alice to open a channel without a change output. |
|
const ( |
|
chanAmt = 1000000 |
|
feeEst = 8000 |
|
) |
|
|
|
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout) |
|
err = net.SendCoins(ctxt, chanAmt+feeEst, alice) |
|
require.NoError(t.t, err) |
|
|
|
// Alice opens a channel that would consume all the funds in her |
|
// wallet, without a change output. This should not be allowed. |
|
resErr := lnwallet.ErrReservedValueInvalidated.Error() |
|
|
|
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout) |
|
_, err = net.OpenChannel( |
|
ctxt, alice, bob, |
|
lntest.OpenChannelParams{ |
|
Amt: chanAmt, |
|
}, |
|
) |
|
if err == nil || !strings.Contains(err.Error(), resErr) { |
|
t.Fatalf("expected failure, got: %v", err) |
|
} |
|
|
|
// Alice opens a smaller channel. This works since it will have a |
|
// change output. |
|
ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout) |
|
aliceChanPoint := openChannelAndAssert( |
|
ctxt, t, net, alice, bob, |
|
lntest.OpenChannelParams{ |
|
Amt: chanAmt / 2, |
|
}, |
|
) |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
err = alice.WaitForNetworkChannelOpen(ctxt, aliceChanPoint) |
|
require.NoError(t.t, err) |
|
|
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
err = bob.WaitForNetworkChannelOpen(ctxt, aliceChanPoint) |
|
require.NoError(t.t, err) |
|
|
|
// Alice tries to send all coins to an internal address. This is |
|
// allowed, since the final wallet balance will still be above the |
|
// reserved value. |
|
addrReq := &lnrpc.NewAddressRequest{ |
|
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
resp, err := alice.NewAddress(ctxt, addrReq) |
|
require.NoError(t.t, err) |
|
|
|
sweepReq := &lnrpc.SendCoinsRequest{ |
|
Addr: resp.Address, |
|
SendAll: true, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
_, err = alice.SendCoins(ctxt, sweepReq) |
|
require.NoError(t.t, err) |
|
|
|
block := mineBlocks(t, net, 1, 1)[0] |
|
|
|
// The sweep transaction should have exactly one input, the change from |
|
// the previous SendCoins call. |
|
sweepTx := block.Transactions[1] |
|
if len(sweepTx.TxIn) != 1 { |
|
t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn)) |
|
} |
|
|
|
// It should have a single output. |
|
if len(sweepTx.TxOut) != 1 { |
|
t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut)) |
|
} |
|
|
|
// Wait for Alice to see her balance as confirmed. |
|
waitForConfirmedBalance := func() int64 { |
|
var balance int64 |
|
err := wait.NoError(func() error { |
|
req := &lnrpc.WalletBalanceRequest{} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
resp, err := alice.WalletBalance(ctxt, req) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if resp.TotalBalance == 0 { |
|
return fmt.Errorf("no balance") |
|
} |
|
|
|
if resp.UnconfirmedBalance > 0 { |
|
return fmt.Errorf("unconfirmed balance") |
|
} |
|
|
|
balance = resp.TotalBalance |
|
return nil |
|
}, defaultTimeout) |
|
require.NoError(t.t, err) |
|
|
|
return balance |
|
} |
|
|
|
_ = waitForConfirmedBalance() |
|
|
|
// Alice tries to send all funds to an external address, the reserved |
|
// value must stay in her wallet. |
|
minerAddr, err := net.Miner.NewAddress() |
|
require.NoError(t.t, err) |
|
|
|
sweepReq = &lnrpc.SendCoinsRequest{ |
|
Addr: minerAddr.String(), |
|
SendAll: true, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
_, err = alice.SendCoins(ctxt, sweepReq) |
|
require.NoError(t.t, err) |
|
|
|
// We'll mine a block which should include the sweep transaction we |
|
// generated above. |
|
block = mineBlocks(t, net, 1, 1)[0] |
|
|
|
// The sweep transaction should have exactly one inputs as we only had |
|
// the the single output from above in the wallet. |
|
sweepTx = block.Transactions[1] |
|
if len(sweepTx.TxIn) != 1 { |
|
t.Fatalf("expected 1 inputs instead have %v", len(sweepTx.TxIn)) |
|
} |
|
|
|
// It should have two outputs, one being the miner address, the other |
|
// one being the reserve going back to our wallet. |
|
if len(sweepTx.TxOut) != 2 { |
|
t.Fatalf("expected 2 outputs instead have %v", len(sweepTx.TxOut)) |
|
} |
|
|
|
// The reserved value is now back in Alice's wallet. |
|
aliceBalance := waitForConfirmedBalance() |
|
|
|
// Alice closes channel, should now be allowed to send everything to an |
|
// external address. |
|
closeChannelAndAssert(ctxt, t, net, alice, aliceChanPoint, false) |
|
|
|
newBalance := waitForConfirmedBalance() |
|
if newBalance <= aliceBalance { |
|
t.Fatalf("Alice's balance did not increase after channel close") |
|
} |
|
|
|
// We'll wait for the balance to reflect that the channel has been |
|
// closed and the funds are in the wallet. |
|
|
|
sweepReq = &lnrpc.SendCoinsRequest{ |
|
Addr: minerAddr.String(), |
|
SendAll: true, |
|
} |
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) |
|
_, err = alice.SendCoins(ctxt, sweepReq) |
|
require.NoError(t.t, err) |
|
|
|
// We'll mine a block which should include the sweep transaction we |
|
// generated above. |
|
block = mineBlocks(t, net, 1, 1)[0] |
|
|
|
// The sweep transaction should have two inputs, the change output from |
|
// the previous sweep, and the output from the coop closed channel. |
|
sweepTx = block.Transactions[1] |
|
if len(sweepTx.TxIn) != 2 { |
|
t.Fatalf("expected 2 inputs instead have %v", len(sweepTx.TxIn)) |
|
} |
|
|
|
// It should have a single output. |
|
if len(sweepTx.TxOut) != 1 { |
|
t.Fatalf("expected 1 output instead have %v", len(sweepTx.TxOut)) |
|
} |
|
}
|
|
|