diff --git a/lntest/itest/lnd_onchain_test.go b/lntest/itest/lnd_onchain_test.go index d999f214b..fc9f38ab2 100644 --- a/lntest/itest/lnd_onchain_test.go +++ b/lntest/itest/lnd_onchain_test.go @@ -24,7 +24,7 @@ import ( 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" { + if net.BackendCfg.Name() == lntest.NeutrinoBackendName { t.Skipf("skipping CPFP test for neutrino backend") } diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 7f6eaa880..c54fa4d29 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -2531,7 +2531,7 @@ func testOpenChannelAfterReorg(net *lntest.NetworkHarness, t *harnessTest) { // Skip test for neutrino, as we cannot disconnect the miner at will. // TODO(halseth): remove when either can disconnect at will, or restart // node with connection to new miner. - if net.BackendCfg.Name() == "neutrino" { + if net.BackendCfg.Name() == lntest.NeutrinoBackendName { t.Skipf("skipping reorg test for neutrino backend") } diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 17008187a..216851e25 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -294,4 +294,12 @@ var allTestCases = []*testCase{ name: "stateless init", test: testStatelessInit, }, + { + name: "wallet import account", + test: testWalletImportAccount, + }, + { + name: "wallet import pubkey", + test: testWalletImportPubKey, + }, } diff --git a/lntest/itest/lnd_wallet_import_test.go b/lntest/itest/lnd_wallet_import_test.go new file mode 100644 index 000000000..64decb137 --- /dev/null +++ b/lntest/itest/lnd_wallet_import_test.go @@ -0,0 +1,866 @@ +package itest + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "math" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "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 + + 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. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + 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{}) + + 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 + + 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. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = t.lndHarness.EnsureConnected(ctxt, srcNode, destNode) + require.NoError(t.t, err) + + // 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 + + 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, + } + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = srcNode.WaitForNetworkChannelOpen(ctxt, chanPoint) + require.NoError(t.t, err) + err = destNode.WaitForNetworkChannelOpen(ctxt, 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) + + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + err = completePaymentRequests( + ctxt, 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. + ctxt, cancel = context.WithTimeout(ctxb, channelCloseTimeout) + defer cancel() + closeChannelAndAssert(ctxt, 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 = 9050 + 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, + }, + } + + 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) { + + ctxb := context.Background() + const utxoAmt int64 = btcutil.SatoshiPerBitcoin + + // 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, err := net.NewNode("carol", nil) + require.NoError(t.t, err) + defer shutdownAndAssert(net, t, carol) + + dave, err := net.NewNode("dave", nil) + require.NoError(t.t, err) + defer shutdownAndAssert(net, t, dave) + + 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, + }, + } + + 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, err := net.NewNode("carol", nil) + require.NoError(t.t, err) + defer shutdownAndAssert(net, t, carol) + + dave, err := net.NewNode("dave", nil) + require.NoError(t.t, err) + 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) + + // Import the public key into Dave. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + importReq := &walletrpc.ImportPublicKeyRequest{ + PublicKey: externalAddrPubKey.SerializeCompressed(), + 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), + ) +} diff --git a/lntest/neutrino.go b/lntest/neutrino.go index d8e4596ca..dadb2c26a 100644 --- a/lntest/neutrino.go +++ b/lntest/neutrino.go @@ -39,7 +39,7 @@ func (b NeutrinoBackendConfig) DisconnectMiner() error { // Name returns the name of the backend type. func (b NeutrinoBackendConfig) Name() string { - return "neutrino" + return NeutrinoBackendName } // NewBackend starts and returns a NeutrinoBackendConfig for the node. diff --git a/lntest/node.go b/lntest/node.go index 89afca736..1024406ab 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -57,6 +57,9 @@ const ( // listenerFormat is the format string that is used to generate local // listener addresses. listenerFormat = "127.0.0.1:%d" + + // NeutrinoBackendName is the name of the neutrino backend. + NeutrinoBackendName = "neutrino" ) var (