lnrpc: ability to spend unconfirmed coins

This commit is contained in:
Tom Kirkpatrick committed 2020-10-01 13:27:03 +01:00
1 parent fd962d322a
commit 73a5f325b6
15 files changed
+1030 -764

No files matched your search

+7 -2
View File
@@ -297,7 +297,7 @@ func (b *BtcWallet) IsOurAddress(a btcutil.Address) bool {
//
// This is a part of the WalletController interface.
func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight, label string) (*wire.MsgTx, error) {
feeRate chainfee.SatPerKWeight, minconf int32, label string) (*wire.MsgTx, error) {
// Convert our fee rate from sat/kw to sat/kb since it's required by
// SendOutputs.
@@ -308,8 +308,13 @@ func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut,
return nil, lnwallet.ErrNoOutputs
}
// Sanity check minconf.
if minconf < 0 {
return nil, lnwallet.ErrInvalidMinconf
}
return b.wallet.SendOutputs(
outputs, defaultAccount, 1, feeSatPerKB, label,
outputs, defaultAccount, minconf, feeSatPerKB, label,
)
}
+6 -1
View File
@@ -56,6 +56,11 @@ var (
// or send coins to a set of outputs that is empty.
var ErrNoOutputs = errors.New("no outputs")
// ErrInvalidMinconf is returned if we try to create a transaction with
// invalid minconf value.
var ErrInvalidMinconf = errors.New("minimum number of confirmations must " +
"be a non-negative number")
// Utxo is an unspent output denoted by its outpoint, and output value of the
// original output.
type Utxo struct {
@@ -181,7 +186,7 @@ type WalletController interface {
//
// NOTE: This method requires the global coin selection lock to be held.
SendOutputs(outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight, label string) (*wire.MsgTx, error)
feeRate chainfee.SatPerKWeight, minconf int32, label string) (*wire.MsgTx, error)
// CreateSimpleTx creates a Bitcoin transaction paying to the specified
// outputs. The transaction is not broadcasted to the network. In the
+120 -21
View File
@@ -171,18 +171,20 @@ func newPkScript(t *testing.T, w *lnwallet.LightningWallet,
// parties to send on-chain funds to each other.
func sendCoins(t *testing.T, miner *rpctest.Harness,
sender, receiver *lnwallet.LightningWallet, output *wire.TxOut,
feeRate chainfee.SatPerKWeight) *wire.MsgTx { //nolint:unparam
feeRate chainfee.SatPerKWeight, mineBlock bool, minConf int32) *wire.MsgTx { //nolint:unparam
t.Helper()
tx, err := sender.SendOutputs(
[]*wire.TxOut{output}, 2500, labels.External,
[]*wire.TxOut{output}, feeRate, minConf, labels.External,
)
if err != nil {
t.Fatalf("unable to send transaction: %v", err)
}
mineAndAssertTxInBlock(t, miner, tx.TxHash())
if mineBlock {
mineAndAssertTxInBlock(t, miner, tx.TxHash())
}
if err := waitForWalletSync(miner, sender); err != nil {
t.Fatalf("unable to sync alice: %v", err)
@@ -201,12 +203,6 @@ func assertTxInWallet(t *testing.T, w *lnwallet.LightningWallet,
t.Helper()
// If the backend is Neutrino, then we can't determine unconfirmed
// transactions since it's not aware of the mempool.
if !confirmed && w.BackEnd() == "neutrino" {
return
}
// We'll fetch all of our transaction and go through each one until
// finding the expected transaction with its expected confirmation
// status.
@@ -1269,7 +1265,7 @@ func testListTransactionDetails(miner *rpctest.Harness,
}
burnOutput := wire.NewTxOut(outputAmt, outputScript)
burnTX, err := alice.SendOutputs(
[]*wire.TxOut{burnOutput}, 2500, labels.External,
[]*wire.TxOut{burnOutput}, 2500, 1, labels.External,
)
if err != nil {
t.Fatalf("unable to create burn tx: %v", err)
@@ -1544,7 +1540,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
}
burnOutput := wire.NewTxOut(outputAmt, outputScript)
tx, err := alice.SendOutputs(
[]*wire.TxOut{burnOutput}, 2500, labels.External,
[]*wire.TxOut{burnOutput}, 2500, 1, labels.External,
)
if err != nil {
t.Fatalf("unable to create burn tx: %v", err)
@@ -1737,7 +1733,7 @@ func newTx(t *testing.T, r *rpctest.Harness, pubKey *btcec.PublicKey,
PkScript: keyScript,
}
tx, err := alice.SendOutputs(
[]*wire.TxOut{newOutput}, 2500, labels.External,
[]*wire.TxOut{newOutput}, 2500, 1, labels.External,
)
if err != nil {
t.Fatalf("unable to create output: %v", err)
@@ -2028,7 +2024,7 @@ func testSignOutputUsingTweaks(r *rpctest.Harness,
PkScript: keyScript,
}
tx, err := alice.SendOutputs(
[]*wire.TxOut{newOutput}, 2500, labels.External,
[]*wire.TxOut{newOutput}, 2500, 1, labels.External,
)
if err != nil {
t.Fatalf("unable to create output: %v", err)
@@ -2152,7 +2148,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
PkScript: script,
}
tx, err := w.SendOutputs(
[]*wire.TxOut{output}, 2500, labels.External,
[]*wire.TxOut{output}, 2500, 1, labels.External,
)
if err != nil {
t.Fatalf("unable to send outputs: %v", err)
@@ -2311,7 +2307,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
}
bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey)
// We'll use a transaction fee of 13020 satoshis, which will allow us to
// We'll use a transaction fee of 14380 satoshis, which will allow us to
// sweep all of Alice's balance in one transaction containing 1 input
// and 1 output.
//
@@ -2323,7 +2319,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: int64(aliceBalance - txFee),
PkScript: bobPkScript,
}
tx := sendCoins(t, r, alice, bob, output, txFeeRate)
tx := sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash := tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
@@ -2345,7 +2341,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: btcutil.SatoshiPerBitcoin,
PkScript: alicePkScript,
}
tx = sendCoins(t, r, bob, alice, output, txFeeRate)
tx = sendCoins(t, r, bob, alice, output, txFeeRate, true, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
@@ -2357,14 +2353,14 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: btcutil.SatoshiPerBitcent,
PkScript: bobPkScript,
}
tx = sendCoins(t, r, alice, bob, output, txFeeRate)
tx = sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
// Then, we'll spend the change output and ensure we see its
// confirmation come in.
tx = sendCoins(t, r, alice, bob, output, txFeeRate)
tx = sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
@@ -2376,6 +2372,101 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
}
}
// testSpendUnconfirmed ensures that when can spend unconfirmed outputs.
func testSpendUnconfirmed(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) {
bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey)
alicePkScript := newPkScript(t, alice, lnwallet.WitnessPubKey)
txFeeRate := chainfee.SatPerKWeight(2500)
// First we will empty out bob's wallet, sending the entire balance
// to alice.
bobBalance, err := bob.ConfirmedBalance(0)
if err != nil {
t.Fatalf("unable to retrieve bob's balance: %v", err)
}
txFee := btcutil.Amount(28760)
output := &wire.TxOut{
Value: int64(bobBalance - txFee),
PkScript: alicePkScript,
}
tx := sendCoins(t, miner, bob, alice, output, txFeeRate, true, 1)
txHash := tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
// Verify that bob doesn't have enough balance to send coins.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin * 0.5,
PkScript: alicePkScript,
}
_, err = bob.SendOutputs(
[]*wire.TxOut{output}, txFeeRate, 0, labels.External,
)
if err == nil {
t.Fatalf("should have not been able to pay due to insufficient balance: %v", err)
}
// Next we will send a transaction to bob but leave it in an
// unconfirmed state.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin,
PkScript: bobPkScript,
}
tx = sendCoins(t, miner, alice, bob, output, txFeeRate, false, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, false)
assertTxInWallet(t, bob, txHash, false)
// Now, try to spend some of the unconfirmed funds from bob's wallet.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin * 0.5,
PkScript: alicePkScript,
}
// First, verify that we don't have enough balance to send the coins
// using confirmed outputs only.
_, err = bob.SendOutputs(
[]*wire.TxOut{output}, txFeeRate, 1, labels.External,
)
if err == nil {
t.Fatalf("should have not been able to pay due to insufficient balance: %v", err)
}
// Now try the send again using unconfirmed outputs.
tx = sendCoins(t, miner, bob, alice, output, txFeeRate, false, 0)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, false)
assertTxInWallet(t, bob, txHash, false)
// Mine the unconfirmed transactions.
err = waitForMempoolTx(miner, &txHash)
if err != nil {
t.Fatalf("tx not relayed to miner: %v", err)
}
if _, err := miner.Node.Generate(1); err != nil {
t.Fatalf("unable to generate block: %v", err)
}
if err := waitForWalletSync(miner, alice); err != nil {
t.Fatalf("unable to sync alice: %v", err)
}
if err := waitForWalletSync(miner, bob); err != nil {
t.Fatalf("unable to sync bob: %v", err)
}
// Finally, send the remainder of bob's wallet balance back to him so
// that these money movements dont mess up later tests.
output = &wire.TxOut{
Value: int64(bobBalance) - (btcutil.SatoshiPerBitcoin * 0.4),
PkScript: bobPkScript,
}
tx = sendCoins(t, miner, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
}
// testLastUnusedAddr tests that the LastUnusedAddress returns the address if
// it isn't used, and also that once the address becomes used, then it's
// properly rotated.
@@ -2418,7 +2509,7 @@ func testLastUnusedAddr(miner *rpctest.Harness,
Value: 1000000,
PkScript: addrScript,
}
sendCoins(t, miner, bob, alice, output, feeRate)
sendCoins(t, miner, bob, alice, output, feeRate, true, 1)
// If we make a new address, then it should be brand new, as
// the prior address has been used.
@@ -2534,7 +2625,7 @@ func testCreateSimpleTx(r *rpctest.Harness, w *lnwallet.LightningWallet,
// _very_ similar to the one we just created being sent. The
// only difference is that the dry run tx is not signed, and
// that the change output position might be different.
tx, sendErr := w.SendOutputs(outputs, feeRate, labels.External)
tx, sendErr := w.SendOutputs(outputs, feeRate, 1, labels.External)
switch {
case test.valid && sendErr != nil:
t.Fatalf("got unexpected error when sending tx: %v",
@@ -2678,6 +2769,10 @@ var walletTests = []walletTestCase{
name: "change output spend confirmation",
test: testChangeOutputSpendConfirmation,
},
{
name: "spend unconfirmed outputs",
test: testSpendUnconfirmed,
},
{
name: "insane fee reject",
test: testReservationInitiatorBalanceBelowDustCancel,
@@ -3310,6 +3405,10 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
strings.Contains(walletTest.name, "dual funder") {
t.Skip("skipping dual funder tests for neutrino")
}
if backEnd == "neutrino" &&
strings.Contains(walletTest.name, "spend unconfirmed") {
t.Skip("skipping spend unconfirmed tests for neutrino")
}
walletTest.test(miningNode, alice, bob, t)
})