lnd/lntest/itest/lnd_wallet_import_test.go
Olaoluwa Osuntokun 2482de9cab
lnwallet/chancloser: properly compute initial fee of cop close txn
In this commit, we modify the way we compute the starting ideal fee for
the co-op close transaction. Before thsi commit, channel.CalcFee was
used, which'll compute the fee based on the commitment transaction
itself, rathern than the co-op close transaction. As the co-op close
transaction is potentailly bigger (two P2TR outputs) than the commitment
transaction, this can cause us to under estimate the fee, which can
result in the fee rate being too low to propagate.

To remedy this, we now compute a fee estimate from scratch, based on the
delivery fees of the two parties.

We also add a bug fix in the chancloser unit tests that wasn't caught
due to loop variable shadowing.

The wallet import itest has been updated as well, since we'll now pay
600 extra saothis to close the channel, since we're accounting for the
added weight of the P2TR outputs.

Fixes #6953
2022-10-10 14:50:45 -07:00

929 lines
29 KiB
Go

package itest
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"math"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/funding"
"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/stretchr/testify/require"
)
const (
defaultAccount = lnwallet.DefaultAccountName
defaultImportedAccount = waddrmgr.ImportedAddrAccountName
)
// walletToLNAddrType maps walletrpc.AddressType to lnrpc.AddressType.
func walletToLNAddrType(t *testing.T,
addrType walletrpc.AddressType) lnrpc.AddressType {
switch addrType {
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
return lnrpc.AddressType_NESTED_PUBKEY_HASH
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
return lnrpc.AddressType_WITNESS_PUBKEY_HASH
case walletrpc.AddressType_TAPROOT_PUBKEY:
return lnrpc.AddressType_TAPROOT_PUBKEY
default:
t.Fatalf("unhandled addr type %v", addrType)
return 0
}
}
// newExternalAddr generates a new external address of an imported account for a
// pair of nodes, where one acts as the funder and the other as the signer.
func newExternalAddr(t *testing.T, funder, signer *lntest.HarnessNode,
importedAccount string, addrType walletrpc.AddressType) string {
// We'll generate a new address for Carol from Dave's node to receive
// and fund a new channel.
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
funderResp, err := funder.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: walletToLNAddrType(t, addrType),
Account: importedAccount,
})
require.NoError(t, err)
// Carol also needs to generate the address for the sake of this test to
// be able to sign the channel funding input.
signerResp, err := signer.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: walletToLNAddrType(t, addrType),
})
require.NoError(t, err)
// Sanity check that the generated addresses match.
require.Equal(t, funderResp.Address, signerResp.Address)
assertExternalAddrType(t, funderResp.Address, addrType)
return funderResp.Address
}
// assertExternalAddrType asserts that an external address generated for an
// imported account is of the expected type.
func assertExternalAddrType(t *testing.T, addrStr string,
accountAddrType walletrpc.AddressType) {
addr, err := btcutil.DecodeAddress(addrStr, harnessNetParams)
require.NoError(t, err)
switch accountAddrType {
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
require.IsType(t, addr, &btcutil.AddressWitnessPubKeyHash{})
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
require.IsType(t, addr, &btcutil.AddressScriptHash{})
case walletrpc.AddressType_TAPROOT_PUBKEY:
require.IsType(t, addr, &btcutil.AddressTaproot{})
default:
t.Fatalf("unsupported account addr type %v", accountAddrType)
}
}
// assertOutputScriptType asserts that a transaction's output, indicated by the
// output with the given amount, has a script of the expected type. This assumes
// all transaction outputs have unique amounts.
func assertOutputScriptType(t *testing.T, expType txscript.ScriptClass,
tx *wire.MsgTx, outputAmt int64) {
for _, txOut := range tx.TxOut {
if txOut.Value != outputAmt {
continue
}
pkScript, err := txscript.ParsePkScript(txOut.PkScript)
require.NoError(t, err)
require.Equal(t, expType, pkScript.Class())
return
}
// No output with the given amount was found.
t.Fatalf("output with amount %v not found in transaction %v", outputAmt,
spew.Sdump(tx))
}
// assertAccountBalance asserts that the unconfirmed and confirmed balance for
// the given account is satisfied by the WalletBalance and ListUnspent RPCs. The
// unconfirmed balance is not checked for neutrino nodes.
func assertAccountBalance(t *testing.T, node *lntest.HarnessNode, account string,
confirmedBalance, unconfirmedBalance int64) {
err := wait.NoError(func() error {
balanceResp, err := node.WalletBalance(
context.Background(), &lnrpc.WalletBalanceRequest{},
)
if err != nil {
return err
}
require.Contains(t, balanceResp.AccountBalance, account)
accountBalance := balanceResp.AccountBalance[account]
// Check confirmed balance.
if accountBalance.ConfirmedBalance != confirmedBalance {
return fmt.Errorf("expected confirmed balance %v, "+
"got %v", confirmedBalance,
accountBalance.ConfirmedBalance)
}
listUtxosReq := &lnrpc.ListUnspentRequest{
MinConfs: 1,
MaxConfs: math.MaxInt32,
Account: account,
}
confirmedUtxosResp, err := node.ListUnspent(
context.Background(), listUtxosReq,
)
if err != nil {
return err
}
var totalConfirmedVal int64
for _, utxo := range confirmedUtxosResp.Utxos {
totalConfirmedVal += utxo.AmountSat
}
if totalConfirmedVal != confirmedBalance {
return fmt.Errorf("expected total confirmed utxo "+
"balance %v, got %v", confirmedBalance,
totalConfirmedVal)
}
// Skip unconfirmed balance checks for neutrino nodes.
if node.Cfg.BackendCfg.Name() == lntest.NeutrinoBackendName {
return nil
}
// Check unconfirmed balance.
if accountBalance.UnconfirmedBalance != unconfirmedBalance {
return fmt.Errorf("expected unconfirmed balance %v, "+
"got %v", unconfirmedBalance,
accountBalance.UnconfirmedBalance)
}
listUtxosReq.MinConfs = 0
listUtxosReq.MaxConfs = 0
unconfirmedUtxosResp, err := node.ListUnspent(
context.Background(), listUtxosReq,
)
require.NoError(t, err)
var totalUnconfirmedVal int64
for _, utxo := range unconfirmedUtxosResp.Utxos {
totalUnconfirmedVal += utxo.AmountSat
}
if totalUnconfirmedVal != unconfirmedBalance {
return fmt.Errorf("expected total unconfirmed utxo "+
"balance %v, got %v", unconfirmedBalance,
totalUnconfirmedVal)
}
return nil
}, defaultTimeout)
require.NoError(t, err)
}
// psbtSendFromImportedAccount attempts to fund a PSBT from the given imported
// account, originating from the source node to the destination.
func psbtSendFromImportedAccount(t *harnessTest, srcNode, destNode,
signer *lntest.HarnessNode, account string,
accountAddrType walletrpc.AddressType) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
balanceResp, err := srcNode.WalletBalance(
ctxt, &lnrpc.WalletBalanceRequest{},
)
require.NoError(t.t, err)
require.Contains(t.t, balanceResp.AccountBalance, account)
confBalance := balanceResp.AccountBalance[account].ConfirmedBalance
destAmt := confBalance - 10000
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
destAddrResp, err := destNode.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
})
require.NoError(t.t, err)
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
fundReq := &walletrpc.FundPsbtRequest{
Template: &walletrpc.FundPsbtRequest_Raw{
Raw: &walletrpc.TxTemplate{
Outputs: map[string]uint64{
destAddrResp.Address: uint64(destAmt),
},
},
},
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
SatPerVbyte: 1,
},
Account: account,
}
fundResp, err := srcNode.WalletKitClient.FundPsbt(ctxt, fundReq)
require.NoError(t.t, err)
// Have Carol sign the PSBT input since Dave doesn't have any private
// key information.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
finalizeReq := &walletrpc.FinalizePsbtRequest{
FundedPsbt: fundResp.FundedPsbt,
}
finalizeResp, err := signer.WalletKitClient.FinalizePsbt(
ctxt, finalizeReq,
)
require.NoError(t.t, err)
// With the PSBT signed, we can broadcast the resulting transaction.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
publishReq := &walletrpc.Transaction{
TxHex: finalizeResp.RawFinalTx,
}
_, err = srcNode.WalletKitClient.PublishTransaction(ctxt, publishReq)
require.NoError(t.t, err)
// Carol's balance from Dave's perspective should update accordingly.
var (
expTxFee int64
expChangeScriptType txscript.ScriptClass
)
switch accountAddrType {
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
expTxFee = 141
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH:
if account != defaultImportedAccount {
expTxFee = 165
expChangeScriptType = txscript.ScriptHashTy
break
}
// Spends from the default NP2WKH imported account have the same
// fee rate as the hybrid address type since a NP2WKH input is
// spent and a P2WKH change output is created.
fallthrough
case walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
expTxFee = 164
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
case walletrpc.AddressType_TAPROOT_PUBKEY:
if account != defaultImportedAccount {
expTxFee = 143
expChangeScriptType = txscript.WitnessV1TaprootTy
break
}
// Spends from the default imported account fall back to a P2WKH
// change. We'll want to change that, but in a separate PR.
expTxFee = 131
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
default:
t.Fatalf("unsupported addr type %v", accountAddrType)
}
changeUtxoAmt := confBalance - destAmt - expTxFee
// If the transaction was created from the default imported account,
// then any change produced is moved to the default wallet account.
accountWithBalance := account
if account == defaultImportedAccount {
accountWithBalance = defaultAccount
}
assertAccountBalance(t.t, srcNode, accountWithBalance, 0, changeUtxoAmt)
_ = mineBlocks(t, t.lndHarness, 1, 1)
assertAccountBalance(t.t, srcNode, accountWithBalance, changeUtxoAmt, 0)
// Finally, assert that the transaction has the expected change address
// type based on the account.
var tx wire.MsgTx
err = tx.Deserialize(bytes.NewReader(finalizeResp.RawFinalTx))
require.NoError(t.t, err)
assertOutputScriptType(t.t, expChangeScriptType, &tx, changeUtxoAmt)
}
// fundChanAndCloseFromImportedAccount attempts to a fund a channel from the
// given imported account, originating from the source node to the destination
// node. To ensure the channel is operational before closing it, a test payment
// is made. Several balance assertions are made along the way for the sake of
// correctness.
func fundChanAndCloseFromImportedAccount(t *harnessTest, srcNode, destNode,
signer *lntest.HarnessNode, account string,
accountAddrType walletrpc.AddressType, utxoAmt, chanSize int64) {
ctxb := context.Background()
// Retrieve the current confirmed balance to make some assertions later
// on.
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
balanceResp, err := srcNode.WalletBalance(
ctxt, &lnrpc.WalletBalanceRequest{},
)
require.NoError(t.t, err)
require.Contains(t.t, balanceResp.AccountBalance, account)
accountConfBalance := balanceResp.
AccountBalance[account].ConfirmedBalance
defaultAccountConfBalance := balanceResp.
AccountBalance[defaultAccount].ConfirmedBalance
// Now, start the channel funding process. We'll need to connect both
// nodes first.
t.lndHarness.EnsureConnected(t.t, srcNode, destNode)
// The source node will then fund the channel through a PSBT shim.
var pendingChanID [32]byte
_, err = rand.Read(pendingChanID[:])
require.NoError(t.t, err)
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
chanUpdates, rawPsbt, err := openChannelPsbt(
ctxt, srcNode, destNode, lntest.OpenChannelParams{
Amt: btcutil.Amount(chanSize),
FundingShim: &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_PsbtShim{
PsbtShim: &lnrpc.PsbtShim{
PendingChanId: pendingChanID[:],
},
},
},
},
)
require.NoError(t.t, err)
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
fundReq := &walletrpc.FundPsbtRequest{
Template: &walletrpc.FundPsbtRequest_Psbt{
Psbt: rawPsbt,
},
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
SatPerVbyte: 1,
},
Account: account,
}
fundResp, err := srcNode.WalletKitClient.FundPsbt(ctxt, fundReq)
require.NoError(t.t, err)
_, err = srcNode.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
PsbtVerify: &lnrpc.FundingPsbtVerify{
PendingChanId: pendingChanID[:],
FundedPsbt: fundResp.FundedPsbt,
},
},
})
require.NoError(t.t, err)
// Now that we have a PSBT to fund the channel, our signer needs to sign
// it.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
finalizeReq := &walletrpc.FinalizePsbtRequest{
FundedPsbt: fundResp.FundedPsbt,
}
finalizeResp, err := signer.WalletKitClient.FinalizePsbt(ctxt, finalizeReq)
require.NoError(t.t, err)
// The source node can then submit the signed PSBT and complete the
// channel funding process.
_, err = srcNode.FundingStateStep(ctxb, &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
PendingChanId: pendingChanID[:],
SignedPsbt: finalizeResp.SignedPsbt,
},
},
})
require.NoError(t.t, err)
// We should receive a notification of the channel funding transaction
// being broadcast.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
updateResp, err := receiveChanUpdate(ctxt, chanUpdates)
require.NoError(t.t, err)
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
require.True(t.t, ok)
// Mine enough blocks to announce the channel to the network, making
// balance assertions along the way.
var (
expChanTxFee int64
expChangeScriptType txscript.ScriptClass
)
switch accountAddrType {
case walletrpc.AddressType_WITNESS_PUBKEY_HASH:
expChanTxFee = 153
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
case walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH:
if account != defaultImportedAccount {
expChanTxFee = 177
expChangeScriptType = txscript.ScriptHashTy
break
}
// Spends from the default NP2WKH imported account have the same
// fee rate as the hybrid address type since a NP2WKH input is
// spent and a P2WKH change output is created.
fallthrough
case walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH:
expChanTxFee = 176
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
case walletrpc.AddressType_TAPROOT_PUBKEY:
if account != defaultImportedAccount {
expChanTxFee = 155
expChangeScriptType = txscript.WitnessV1TaprootTy
break
}
// Spends from the default imported account fall back to a P2WKH
// change. We'll want to change that, but in a separate PR.
expChanTxFee = 143
expChangeScriptType = txscript.WitnessV0PubKeyHashTy
default:
t.Fatalf("unsupported addr type %v", accountAddrType)
}
chanChangeUtxoAmt := utxoAmt - chanSize - expChanTxFee
txHash, err := chainhash.NewHash(upd.ChanPending.Txid)
require.NoError(t.t, err)
// If we're spending from the default imported account, then any change
// outputs produced are moved to the default wallet account, so we
// should expect to see balances there.
var confBalanceAfterChan int64
if account == defaultImportedAccount {
confBalanceAfterChan = defaultAccountConfBalance
assertAccountBalance(t.t, srcNode, account, 0, 0)
assertAccountBalance(
t.t, srcNode, defaultAccount, defaultAccountConfBalance,
chanChangeUtxoAmt,
)
block := mineBlocks(t, t.lndHarness, 6, 1)[0]
assertTxInBlock(t, block, txHash)
confBalanceAfterChan += chanChangeUtxoAmt
assertAccountBalance(t.t, srcNode, account, 0, 0)
assertAccountBalance(
t.t, srcNode, defaultAccount, confBalanceAfterChan, 0,
)
} else {
// Otherwise, all interactions remain within Carol's imported
// account.
confBalanceAfterChan = accountConfBalance - utxoAmt
assertAccountBalance(
t.t, srcNode, account, confBalanceAfterChan,
chanChangeUtxoAmt,
)
block := mineBlocks(t, t.lndHarness, 6, 1)[0]
assertTxInBlock(t, block, txHash)
confBalanceAfterChan += chanChangeUtxoAmt
assertAccountBalance(
t.t, srcNode, account, confBalanceAfterChan, 0,
)
}
// Assert that the transaction has the expected change address type
// based on the account.
var tx wire.MsgTx
err = tx.Deserialize(bytes.NewReader(finalizeResp.RawFinalTx))
require.NoError(t.t, err)
assertOutputScriptType(t.t, expChangeScriptType, &tx, chanChangeUtxoAmt)
// Wait for the channel to be announced by both parties.
chanPoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: upd.ChanPending.Txid,
},
OutputIndex: upd.ChanPending.OutputIndex,
}
err = srcNode.WaitForNetworkChannelOpen(chanPoint)
require.NoError(t.t, err)
err = destNode.WaitForNetworkChannelOpen(chanPoint)
require.NoError(t.t, err)
// Send a test payment to ensure the channel is operating as normal.
const invoiceAmt = 100000
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
resp, err := destNode.AddInvoice(ctxt, &lnrpc.Invoice{
Memo: "psbt import chan",
Value: invoiceAmt,
})
require.NoError(t.t, err)
err = completePaymentRequests(
srcNode, srcNode.RouterClient,
[]string{resp.PaymentRequest}, true,
)
require.NoError(t.t, err)
// Now that we've confirmed the opened channel works, we'll close it.
closeChannelAndAssert(t, t.lndHarness, srcNode, chanPoint, false)
// Since the channel still had funds left on the source node's side,
// they must've been redeemed after the close. Without a pre-negotiated
// close address, the funds will go into the source node's wallet
// instead of the imported account.
const chanCloseTxFee = 9650
balanceFromClosedChan := chanSize - invoiceAmt - chanCloseTxFee
if account == defaultImportedAccount {
assertAccountBalance(t.t, srcNode, account, 0, 0)
assertAccountBalance(
t.t, srcNode, defaultAccount,
confBalanceAfterChan+balanceFromClosedChan, 0,
)
} else {
assertAccountBalance(
t.t, srcNode, account, confBalanceAfterChan, 0,
)
assertAccountBalance(
t.t, srcNode, defaultAccount, balanceFromClosedChan, 0,
)
}
}
// testWalletImportAccount tests that an imported account can fund transactions
// and channels through PSBTs, by having one node (the one with the imported
// account) craft the transactions and another node act as the signer.
func testWalletImportAccount(net *lntest.NetworkHarness, t *harnessTest) {
testCases := []struct {
name string
addrType walletrpc.AddressType
}{
{
name: "standard BIP-0049",
addrType: walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
},
{
name: "lnd BIP-0049 variant",
addrType: walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH,
},
{
name: "standard BIP-0084",
addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH,
},
{
name: "standard BIP-0086",
addrType: walletrpc.AddressType_TAPROOT_PUBKEY,
},
}
for _, tc := range testCases {
tc := tc
success := t.t.Run(tc.name, func(tt *testing.T) {
ht := newHarnessTest(tt, net)
ht.RunTestCase(&testCase{
name: tc.name,
test: func(net1 *lntest.NetworkHarness,
t1 *harnessTest) {
testWalletImportAccountScenario(
net, t, tc.addrType,
)
},
})
})
if !success {
// Log failure time to help relate the lnd logs to the
// failure.
t.Logf("Failure time: %v", time.Now().Format(
"2006-01-02 15:04:05.000",
))
break
}
}
}
func testWalletImportAccountScenario(net *lntest.NetworkHarness, t *harnessTest,
addrType walletrpc.AddressType) {
// We'll start our test by having two nodes, Carol and Dave. Carol's
// default wallet account will be imported into Dave's node.
carol := net.NewNode(t.t, "carol", nil)
defer shutdownAndAssert(net, t, carol)
dave := net.NewNode(t.t, "dave", nil)
defer shutdownAndAssert(net, t, dave)
runWalletImportAccountScenario(net, t, addrType, carol, dave)
}
func runWalletImportAccountScenario(net *lntest.NetworkHarness, t *harnessTest,
addrType walletrpc.AddressType, carol, dave *lntest.HarnessNode) {
ctxb := context.Background()
const utxoAmt int64 = btcutil.SatoshiPerBitcoin
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
listReq := &walletrpc.ListAccountsRequest{
Name: "default",
AddressType: addrType,
}
listResp, err := carol.WalletKitClient.ListAccounts(ctxt, listReq)
require.NoError(t.t, err)
require.Equal(t.t, len(listResp.Accounts), 1)
carolAccount := listResp.Accounts[0]
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
const importedAccount = "carol"
importReq := &walletrpc.ImportAccountRequest{
Name: importedAccount,
ExtendedPublicKey: carolAccount.ExtendedPublicKey,
AddressType: addrType,
}
_, err = dave.WalletKitClient.ImportAccount(ctxt, importReq)
require.NoError(t.t, err)
// We'll generate an address for Carol from Dave's node to receive some
// funds.
externalAddr := newExternalAddr(
t.t, dave, carol, importedAccount, addrType,
)
// Send coins to Carol's address and confirm them, making sure the
// balance updates accordingly.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: externalAddr,
Amount: utxoAmt,
SatPerByte: 1,
})
require.NoError(t.t, err)
assertAccountBalance(t.t, dave, importedAccount, 0, utxoAmt)
_ = mineBlocks(t, net, 1, 1)
assertAccountBalance(t.t, dave, importedAccount, utxoAmt, 0)
// To ensure that Dave can use Carol's account as watch-only, we'll
// construct a PSBT that sends funds to Alice, which we'll then hand
// over to Carol to sign.
psbtSendFromImportedAccount(
t, dave, net.Alice, carol, importedAccount, addrType,
)
// We'll generate a new address for Carol from Dave's node to receive
// and fund a new channel.
externalAddr = newExternalAddr(
t.t, dave, carol, importedAccount, addrType,
)
// Retrieve the current confirmed balance of the imported account for
// some assertions we'll make later on.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
balanceResp, err := dave.WalletBalance(
ctxt, &lnrpc.WalletBalanceRequest{},
)
require.NoError(t.t, err)
require.Contains(t.t, balanceResp.AccountBalance, importedAccount)
confBalance := balanceResp.AccountBalance[importedAccount].ConfirmedBalance
// Send coins to Carol's address and confirm them, making sure the
// balance updates accordingly.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: externalAddr,
Amount: utxoAmt,
SatPerByte: 1,
})
require.NoError(t.t, err)
assertAccountBalance(t.t, dave, importedAccount, confBalance, utxoAmt)
_ = mineBlocks(t, net, 1, 1)
assertAccountBalance(
t.t, dave, importedAccount, confBalance+utxoAmt, 0,
)
// Now that we have enough funds, it's time to fund the channel, make a
// test payment, and close it. This contains several balance assertions
// along the way.
fundChanAndCloseFromImportedAccount(
t, dave, net.Alice, carol, importedAccount, addrType, utxoAmt,
int64(funding.MaxBtcFundingAmount),
)
}
// testWalletImportPubKey tests that an imported public keys can fund
// transactions and channels through PSBTs, by having one node (the one with the
// imported account) craft the transactions and another node act as the signer.
func testWalletImportPubKey(net *lntest.NetworkHarness, t *harnessTest) {
testCases := []struct {
name string
addrType walletrpc.AddressType
}{
{
name: "BIP-0049",
addrType: walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH,
},
{
name: "BIP-0084",
addrType: walletrpc.AddressType_WITNESS_PUBKEY_HASH,
},
{
name: "BIP-0086",
addrType: walletrpc.AddressType_TAPROOT_PUBKEY,
},
}
for _, tc := range testCases {
tc := tc
success := t.t.Run(tc.name, func(tt *testing.T) {
ht := newHarnessTest(tt, net)
ht.RunTestCase(&testCase{
name: tc.name,
test: func(net1 *lntest.NetworkHarness,
t1 *harnessTest) {
testWalletImportPubKeyScenario(
net, t, tc.addrType,
)
},
})
})
if !success {
// Log failure time to help relate the lnd logs to the
// failure.
t.Logf("Failure time: %v", time.Now().Format(
"2006-01-02 15:04:05.000",
))
break
}
}
}
func testWalletImportPubKeyScenario(net *lntest.NetworkHarness, t *harnessTest,
addrType walletrpc.AddressType) {
ctxb := context.Background()
const utxoAmt int64 = btcutil.SatoshiPerBitcoin
// We'll start our test by having two nodes, Carol and Dave.
carol := net.NewNode(t.t, "carol", nil)
defer shutdownAndAssert(net, t, carol)
dave := net.NewNode(t.t, "dave", nil)
defer shutdownAndAssert(net, t, dave)
// We'll define a helper closure that we'll use throughout the test to
// generate a new address of the given type from Carol's perspective,
// import it into Dave's wallet, and fund it.
importPubKey := func(keyIndex uint32, prevConfBalance,
prevUnconfBalance int64) {
// Retrieve Carol's account public key for the corresponding
// address type.
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
listReq := &walletrpc.ListAccountsRequest{
Name: "default",
AddressType: addrType,
}
listResp, err := carol.WalletKitClient.ListAccounts(
ctxt, listReq,
)
require.NoError(t.t, err)
require.Equal(t.t, len(listResp.Accounts), 1)
p2wkhAccount := listResp.Accounts[0]
// Derive the external address at the given index.
accountPubKey, err := hdkeychain.NewKeyFromString(
p2wkhAccount.ExtendedPublicKey,
)
require.NoError(t.t, err)
externalAccountExtKey, err := accountPubKey.Derive(0)
require.NoError(t.t, err)
externalAddrExtKey, err := externalAccountExtKey.Derive(keyIndex)
require.NoError(t.t, err)
externalAddrPubKey, err := externalAddrExtKey.ECPubKey()
require.NoError(t.t, err)
// Serialize as 32-byte x-only pubkey for Taproot addresses.
serializedPubKey := externalAddrPubKey.SerializeCompressed()
if addrType == walletrpc.AddressType_TAPROOT_PUBKEY {
serializedPubKey = schnorr.SerializePubKey(
externalAddrPubKey,
)
}
// Import the public key into Dave.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
importReq := &walletrpc.ImportPublicKeyRequest{
PublicKey: serializedPubKey,
AddressType: addrType,
}
_, err = dave.WalletKitClient.ImportPublicKey(ctxt, importReq)
require.NoError(t.t, err)
// We'll also generate the same address for Carol, as it'll be
// required later when signing.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
carolAddrResp, err := carol.NewAddress(
ctxt, &lnrpc.NewAddressRequest{
Type: walletToLNAddrType(t.t, addrType),
},
)
require.NoError(t.t, err)
// Send coins to Carol's address and confirm them, making sure
// the balance updates accordingly.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: carolAddrResp.Address,
Amount: utxoAmt,
SatPerByte: 1,
})
require.NoError(t.t, err)
assertAccountBalance(
t.t, dave, defaultImportedAccount, prevConfBalance,
prevUnconfBalance+utxoAmt,
)
_ = mineBlocks(t, net, 1, 1)
assertAccountBalance(
t.t, dave, defaultImportedAccount,
prevConfBalance+utxoAmt, prevUnconfBalance,
)
}
// We'll have Carol generate a new external address, which we'll import
// into Dave.
importPubKey(0, 0, 0)
// To ensure that Dave can use Carol's public key as watch-only, we'll
// construct a PSBT that sends funds to Alice, which we'll then hand
// over to Carol to sign.
psbtSendFromImportedAccount(
t, dave, net.Alice, carol, defaultImportedAccount, addrType,
)
// We'll now attempt to fund a channel.
//
// We'll have Carol generate another external address, which we'll
// import into Dave again.
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
balanceResp, err := dave.WalletBalance(
ctxt, &lnrpc.WalletBalanceRequest{},
)
require.NoError(t.t, err)
require.Contains(
t.t, balanceResp.AccountBalance, defaultImportedAccount,
)
confBalance := balanceResp.
AccountBalance[defaultImportedAccount].ConfirmedBalance
importPubKey(1, confBalance, 0)
// Now that we have enough funds, it's time to fund the channel, make a
// test payment, and close it. This contains several balance assertions
// along the way.
fundChanAndCloseFromImportedAccount(
t, dave, net.Alice, carol, defaultImportedAccount, addrType,
utxoAmt, int64(funding.MaxBtcFundingAmount),
)
}