diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index 942952d9c6..152917ac60 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -464,6 +464,7 @@ class WalletImpl : public Wallet { RemoveWallet(m_wallet); } + bool isLegacy() override { return m_wallet->IsLegacy(); } std::unique_ptr handleUnload(UnloadFn fn) override { return MakeHandler(m_wallet->NotifyUnload.connect(fn)); diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 0157e9cf95..5d870c5e3d 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -266,6 +266,9 @@ class Wallet // Remove wallet. virtual void remove() = 0; + //! Return whether is a legacy wallet + virtual bool isLegacy() = 0; + //! Register handler for unload message. using UnloadFn = std::function; virtual std::unique_ptr handleUnload(UnloadFn fn) = 0; diff --git a/src/pubkey.h b/src/pubkey.h index 2fc92c9bc6..261842b7f7 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -219,6 +219,11 @@ struct CExtPubKey { a.pubkey == b.pubkey; } + friend bool operator!=(const CExtPubKey &a, const CExtPubKey &b) + { + return !(a == b); + } + void Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const; void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]); bool Derive(CExtPubKey& out, unsigned int nChild) const; diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 8e6474b0d4..5056e487fc 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -60,3 +60,8 @@ bool CreateWalletDialog::isMakeBlankWalletChecked() const { return ui->blank_wallet_checkbox->isChecked(); } + +bool CreateWalletDialog::isDescriptorWalletChecked() const +{ + return ui->descriptor_checkbox->isChecked(); +} diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 30766107b9..20cce937c8 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -27,6 +27,7 @@ class CreateWalletDialog : public QDialog bool isEncryptWalletChecked() const; bool isDisablePrivateKeysChecked() const; bool isMakeBlankWalletChecked() const; + bool isDescriptorWalletChecked() const; private: Ui::CreateWalletDialog *ui; diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index e49bab8f3b..b592140dd7 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -7,7 +7,7 @@ 0 0 364 - 185 + 213 @@ -17,7 +17,7 @@ 10 - 140 + 170 341 32 @@ -106,6 +106,22 @@ Make Blank Wallet + + + + 20 + 140 + 171 + 22 + + + + Use descriptors for scriptPubKey management + + + Descriptor Wallet + + wallet_name_line_edit diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index c376921b72..e0ae1f9e92 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -161,20 +161,27 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances) { int unit = walletModel->getOptionsModel()->getDisplayUnit(); m_balances = balances; - if (walletModel->wallet().privateKeysDisabled()) { - ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + if (walletModel->wallet().isLegacy()) { + if (walletModel->wallet().privateKeysDisabled()) { + ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + } else { + ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways)); + ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways)); + ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways)); + ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + } } else { ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways)); ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways)); ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways)); ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); } // only show immature (newly mined) balance if it's non-zero, so as not to complicate things // for the non-mining users diff --git a/src/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp index 180550c5ae..5debded4ea 100644 --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -157,17 +157,40 @@ void ReceiveCoinsDialog::on_receiveButton_clicked() } } address = model->getAddressTableModel()->addRow(AddressTableModel::Receive, label, "", address_type); - SendCoinsRecipient info(address, label, - ui->reqAmount->value(), ui->reqMessage->text()); - ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->setModel(model); - dialog->setInfo(info); - dialog->show(); - clear(); - /* Store request for later reference */ - model->getRecentRequestsTableModel()->addNewRequest(info); + switch(model->getAddressTableModel()->getEditStatus()) + { + case AddressTableModel::EditStatus::OK: { + // Success + SendCoinsRecipient info(address, label, + ui->reqAmount->value(), ui->reqMessage->text()); + ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setModel(model); + dialog->setInfo(info); + dialog->show(); + + /* Store request for later reference */ + model->getRecentRequestsTableModel()->addNewRequest(info); + break; + } + case AddressTableModel::EditStatus::WALLET_UNLOCK_FAILURE: + QMessageBox::critical(this, windowTitle(), + tr("Could not unlock wallet."), + QMessageBox::Ok, QMessageBox::Ok); + break; + case AddressTableModel::EditStatus::KEY_GENERATION_FAILURE: + QMessageBox::critical(this, windowTitle(), + tr("Could not generate new %1 address").arg(QString::fromStdString(FormatOutputType(address_type))), + QMessageBox::Ok, QMessageBox::Ok); + break; + // These aren't valid return values for our action + case AddressTableModel::EditStatus::INVALID_ADDRESS: + case AddressTableModel::EditStatus::DUPLICATE_ADDRESS: + case AddressTableModel::EditStatus::NO_CHANGES: + assert(false); + } + clear(); } void ReceiveCoinsDialog::on_recentRequestsView_doubleClicked(const QModelIndex &index) diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 8e422fa962..9c1488fb0e 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -226,6 +226,9 @@ void CreateWalletActivity::createWallet() if (m_create_wallet_dialog->isMakeBlankWalletChecked()) { flags |= WALLET_FLAG_BLANK_WALLET; } + if (m_create_wallet_dialog->isDescriptorWalletChecked()) { + flags |= WALLET_FLAG_DESCRIPTORS; + } QTimer::singleShot(500, worker(), [this, name, flags] { WalletCreationStatus status; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 5d0f2fd703..3045a74d7a 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importpubkey", 2, "rescan" }, { "importmulti", 0, "requests" }, { "importmulti", 1, "options" }, + { "importdescriptors", 0, "requests" }, { "verifychain", 0, "checklevel" }, { "verifychain", 1, "nblocks" }, { "getblockstats", 0, "hash_or_height" }, @@ -170,6 +171,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createwallet", 1, "disable_private_keys"}, { "createwallet", 2, "blank"}, { "createwallet", 4, "avoid_reuse"}, + { "createwallet", 5, "descriptors"}, { "getnodeaddresses", 0, "count"}, { "stop", 0, "wait" }, }; diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 68d83299ee..5279f40506 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -575,6 +575,7 @@ class AddressDescriptor final : public DescriptorImpl default: return nullopt; } } + bool IsSingleType() const final { return true; } }; /** A parsed raw(H) descriptor. */ @@ -602,6 +603,7 @@ class RawDescriptor final : public DescriptorImpl default: return nullopt; } } + bool IsSingleType() const final { return true; } }; /** A parsed pk(P) descriptor. */ @@ -611,6 +613,7 @@ class PKDescriptor final : public DescriptorImpl std::vector MakeScripts(const std::vector& keys, const CScript*, FlatSigningProvider&) const override { return Vector(GetScriptForRawPubKey(keys[0])); } public: PKDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pk") {} + bool IsSingleType() const final { return true; } }; /** A parsed pkh(P) descriptor. */ @@ -626,6 +629,7 @@ class PKHDescriptor final : public DescriptorImpl public: PKHDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pkh") {} Optional GetOutputType() const override { return OutputType::LEGACY; } + bool IsSingleType() const final { return true; } }; /** A parsed wpkh(P) descriptor. */ @@ -641,6 +645,7 @@ class WPKHDescriptor final : public DescriptorImpl public: WPKHDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "wpkh") {} Optional GetOutputType() const override { return OutputType::BECH32; } + bool IsSingleType() const final { return true; } }; /** A parsed combo(P) descriptor. */ @@ -664,6 +669,7 @@ class ComboDescriptor final : public DescriptorImpl } public: ComboDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "combo") {} + bool IsSingleType() const final { return false; } }; /** A parsed multi(...) or sortedmulti(...) descriptor */ @@ -683,6 +689,7 @@ class MultisigDescriptor final : public DescriptorImpl } public: MultisigDescriptor(int threshold, std::vector> providers, bool sorted = false) : DescriptorImpl(std::move(providers), {}, sorted ? "sortedmulti" : "multi"), m_threshold(threshold), m_sorted(sorted) {} + bool IsSingleType() const final { return true; } }; /** A parsed sh(...) descriptor. */ @@ -699,6 +706,7 @@ class SHDescriptor final : public DescriptorImpl if (m_subdescriptor_arg->GetOutputType() == OutputType::BECH32) return OutputType::P2SH_SEGWIT; return OutputType::LEGACY; } + bool IsSingleType() const final { return true; } }; /** A parsed wsh(...) descriptor. */ @@ -709,6 +717,7 @@ class WSHDescriptor final : public DescriptorImpl public: WSHDescriptor(std::unique_ptr desc) : DescriptorImpl({}, std::move(desc), "wsh") {} Optional GetOutputType() const override { return OutputType::BECH32; } + bool IsSingleType() const final { return true; } }; //////////////////////////////////////////////////////////////////////////// diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 69850eb670..17b43e7c81 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -87,6 +87,9 @@ struct Descriptor { /** Convert the descriptor back to a string, undoing parsing. */ virtual std::string ToString() const = 0; + /** Whether this descriptor will return one scriptPubKey or multiple (aka is or is not combo) */ + virtual bool IsSingleType() const = 0; + /** Convert the descriptor to a private string. This fails if the provided provider does not have the relevant private keys. */ virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0; diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 86e4e06673..128de52b58 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1458,3 +1458,297 @@ UniValue importmulti(const JSONRPCRequest& mainRequest) return response; } + +static UniValue ProcessDescriptorImport(CWallet * const pwallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) +{ + UniValue warnings(UniValue::VARR); + UniValue result(UniValue::VOBJ); + + try { + if (!data.exists("desc")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found."); + } + + const std::string& descriptor = data["desc"].get_str(); + const bool active = data.exists("active") ? data["active"].get_bool() : false; + const bool internal = data.exists("internal") ? data["internal"].get_bool() : false; + const std::string& label = data.exists("label") ? data["label"].get_str() : ""; + + // Parse descriptor string + FlatSigningProvider keys; + std::string error; + auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true); + if (!parsed_desc) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); + } + + // Range check + int64_t range_start = 0, range_end = 1, next_index = 0; + if (!parsed_desc->IsRange() && data.exists("range")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor"); + } else if (parsed_desc->IsRange()) { + if (data.exists("range")) { + auto range = ParseDescriptorRange(data["range"]); + range_start = range.first; + range_end = range.second + 1; // Specified range end is inclusive, but we need range end as exclusive + } else { + warnings.push_back("Range not given, using default keypool range"); + range_start = 0; + range_end = gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE); + } + next_index = range_start; + + if (data.exists("next_index")) { + next_index = data["next_index"].get_int64(); + // bound checks + if (next_index < range_start || next_index >= range_end) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "next_index is out of range"); + } + } + } + + // Active descriptors must be ranged + if (active && !parsed_desc->IsRange()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Active descriptors must be ranged"); + } + + // Ranged descriptors should not have a label + if (data.exists("range") && data.exists("label")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptors should not have a label"); + } + + // Internal addresses should not have a label either + if (internal && data.exists("label")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label"); + } + + // Combo descriptor check + if (active && !parsed_desc->IsSingleType()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Combo descriptors cannot be set to active"); + } + + // If the wallet disabled private keys, abort if private keys exist + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled"); + } + + // Need to ExpandPrivate to check if private keys are available for all pubkeys + FlatSigningProvider expand_keys; + std::vector scripts; + parsed_desc->Expand(0, keys, scripts, expand_keys); + parsed_desc->ExpandPrivate(0, keys, expand_keys); + + // Check if all private keys are provided + bool have_all_privkeys = !expand_keys.keys.empty(); + for (const auto& entry : expand_keys.origins) { + const CKeyID& key_id = entry.first; + CKey key; + if (!expand_keys.GetKey(key_id, key)) { + have_all_privkeys = false; + break; + } + } + + // If private keys are enabled, check some things. + if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + if (keys.keys.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled"); + } + if (!have_all_privkeys) { + warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors"); + } + } + + WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index); + + // Check if the wallet already contains the descriptor + auto existing_spk_manager = pwallet->GetDescriptorScriptPubKeyMan(w_desc); + if (existing_spk_manager) { + LOCK(existing_spk_manager->cs_desc_man); + if (range_start > existing_spk_manager->GetWalletDescriptor().range_start) { + throw JSONRPCError(RPC_INVALID_PARAMS, strprintf("range_start can only decrease; current range = [%d,%d]", existing_spk_manager->GetWalletDescriptor().range_start, existing_spk_manager->GetWalletDescriptor().range_end)); + } + } + + // Add descriptor to the wallet + auto spk_manager = pwallet->AddWalletDescriptor(w_desc, keys, label); + if (spk_manager == nullptr) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s'", descriptor)); + } + + // Set descriptor as active if necessary + if (active) { + if (!w_desc.descriptor->GetOutputType()) { + warnings.push_back("Unknown output type, cannot set descriptor to active."); + } else { + pwallet->SetActiveScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), internal); + } + } + + result.pushKV("success", UniValue(true)); + } catch (const UniValue& e) { + result.pushKV("success", UniValue(false)); + result.pushKV("error", e); + } catch (...) { + result.pushKV("success", UniValue(false)); + + result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, "Missing required fields")); + } + if (warnings.size()) result.pushKV("warnings", warnings); + return result; +} + +UniValue importdescriptors(const JSONRPCRequest& main_request) { + // Acquire the wallet + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(main_request); + CWallet* const pwallet = wallet.get(); + if (!EnsureWalletIsAvailable(pwallet, main_request.fHelp)) { + return NullUniValue; + } + + RPCHelpMan{"importdescriptors", + "\nImport descriptors. This will trigger a rescan of the blockchain based on the earliest timestamp of all descriptors being imported. Requires a new wallet backup.\n" + "\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n" + "may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n", + { + {"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "Descriptor to import."}, + {"active", RPCArg::Type::BOOL, /* default */ "false", "Set this descriptor to be the active descriptor for the corresponding output type/externality"}, + {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in the form [begin,end]) to import"}, + {"next_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "If a ranged descriptor is set to active, this specifies the next index to generate addresses from"}, + {"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, "Time from which to start rescanning the blockchain for this descriptor, in " + UNIX_EPOCH_TIME + "\n" + " Use the string \"now\" to substitute the current synced blockchain time.\n" + " \"now\" can be specified to bypass scanning, for outputs which are known to never have been used, and\n" + " 0 can be specified to scan the entire blockchain. Blocks up to 2 hours before the earliest timestamp\n" + " of all descriptors being imported will be scanned.", + /* oneline_description */ "", {"timestamp | \"now\"", "integer / string"} + }, + {"internal", RPCArg::Type::BOOL, /* default */ "false", "Whether matching outputs should be treated as not incoming payments (e.g. change)"}, + {"label", RPCArg::Type::STR, /* default */ "''", "Label to assign to the address, only allowed with internal=false"}, + }, + }, + }, + "\"requests\""}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::BOOL, "success", ""}, + {RPCResult::Type::ARR, "warnings", /* optional */ true, "", + { + {RPCResult::Type::STR, "", ""}, + }}, + {RPCResult::Type::OBJ, "error", /* optional */ true, "", + { + {RPCResult::Type::ELISION, "", "JSONRPC error"}, + }}, + }}, + } + }, + RPCExamples{ + HelpExampleCli("importdescriptors", "'[{ \"desc\": \"\", \"timestamp\":1455191478, \"internal\": true }, " + "{ \"desc\": \"\", \"label\": \"example 2\", \"timestamp\": 1455191480 }]'") + + HelpExampleCli("importdescriptors", "'[{ \"desc\": \"\", \"timestamp\":1455191478, \"active\": true, \"range\": [0,100], \"label\": \"\" }]'") + }, + }.Check(main_request); + + // Make sure wallet is a descriptor wallet + if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "importdescriptors is not available for non-descriptor wallets"); + } + + RPCTypeCheck(main_request.params, {UniValue::VARR, UniValue::VOBJ}); + + WalletRescanReserver reserver(*pwallet); + if (!reserver.reserve()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait."); + } + + const UniValue& requests = main_request.params[0]; + const int64_t minimum_timestamp = 1; + int64_t now = 0; + int64_t lowest_timestamp = 0; + bool rescan = false; + UniValue response(UniValue::VARR); + { + auto locked_chain = pwallet->chain().lock(); + LOCK(pwallet->cs_wallet); + EnsureWalletIsUnlocked(pwallet); + + CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now))); + + // Get all timestamps and extract the lowest timestamp + for (const UniValue& request : requests.getValues()) { + // This throws an error if "timestamp" doesn't exist + const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp); + const UniValue result = ProcessDescriptorImport(pwallet, request, timestamp); + response.push_back(result); + + if (lowest_timestamp > timestamp ) { + lowest_timestamp = timestamp; + } + + // If we know the chain tip, and at least one request was successful then allow rescan + if (!rescan && result["success"].get_bool()) { + rescan = true; + } + } + pwallet->ConnectScriptPubKeyManNotifiers(); + } + + // Rescan the blockchain using the lowest timestamp + if (rescan) { + int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */); + { + auto locked_chain = pwallet->chain().lock(); + LOCK(pwallet->cs_wallet); + pwallet->ReacceptWalletTransactions(); + } + + if (pwallet->IsAbortingRescan()) { + throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user."); + } + + if (scanned_time > lowest_timestamp) { + std::vector results = response.getValues(); + response.clear(); + response.setArray(); + + // Compose the response + for (unsigned int i = 0; i < requests.size(); ++i) { + const UniValue& request = requests.getValues().at(i); + + // If the descriptor timestamp is within the successfully scanned + // range, or if the import result already has an error set, let + // the result stand unmodified. Otherwise replace the result + // with an error message. + if (scanned_time <= GetImportTimestamp(request, now) || results.at(i).exists("error")) { + response.push_back(results.at(i)); + } else { + UniValue result = UniValue(UniValue::VOBJ); + result.pushKV("success", UniValue(false)); + result.pushKV( + "error", + JSONRPCError( + RPC_MISC_ERROR, + strprintf("Rescan failed for descriptor with timestamp %d. There was an error reading a " + "block from time %d, which is after or within %d seconds of key creation, and " + "could contain transactions pertaining to the desc. As a result, transactions " + "and coins using this desc may not appear in the wallet. This error could be " + "caused by pruning or data corruption (see bitcoind log for details) and could " + "be dealt with by downloading and rescanning the relevant blocks (see -reindex " + "and -rescan options).", + GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW))); + response.push_back(std::move(result)); + } + } + } + } + + return response; +} diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 46efb18793..49c583c422 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -1851,7 +1851,7 @@ static UniValue keypoolrefill(const JSONRPCRequest& request) }, }.Check(request); - if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + if (pwallet->IsLegacy() && pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); } @@ -2439,7 +2439,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) {RPCResult::Type::STR_AMOUNT, "unconfirmed_balance", "DEPRECATED. Identical to getbalances().mine.untrusted_pending"}, {RPCResult::Type::STR_AMOUNT, "immature_balance", "DEPRECATED. Identical to getbalances().mine.immature"}, {RPCResult::Type::NUM, "txcount", "the total number of transactions in the wallet"}, - {RPCResult::Type::NUM_TIME, "keypoololdest", "the " + UNIX_EPOCH_TIME + " of the oldest pre-generated key in the key pool"}, + {RPCResult::Type::NUM_TIME, "keypoololdest", "the " + UNIX_EPOCH_TIME + " of the oldest pre-generated key in the key pool. Legacy wallets only."}, {RPCResult::Type::NUM, "keypoolsize", "how many new keys are pre-generated (only counts external keys)"}, {RPCResult::Type::NUM, "keypoolsize_hd_internal", "how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used)"}, {RPCResult::Type::NUM_TIME, "unlocked_until", "the " + UNIX_EPOCH_TIME + " until which the wallet is unlocked for transfers, or 0 if the wallet is locked"}, @@ -2452,6 +2452,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) {RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"}, {RPCResult::Type::NUM, "progress", "scanning progress percentage [0.0, 1.0]"}, }}, + {RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"}, }}, }, RPCExamples{ @@ -2471,13 +2472,16 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) size_t kpExternalSize = pwallet->KeypoolCountExternalKeys(); const auto bal = pwallet->GetBalance(); + int64_t kp_oldest = pwallet->GetOldestKeyPoolTime(); obj.pushKV("walletname", pwallet->GetName()); obj.pushKV("walletversion", pwallet->GetVersion()); obj.pushKV("balance", ValueFromAmount(bal.m_mine_trusted)); obj.pushKV("unconfirmed_balance", ValueFromAmount(bal.m_mine_untrusted_pending)); obj.pushKV("immature_balance", ValueFromAmount(bal.m_mine_immature)); obj.pushKV("txcount", (int)pwallet->mapWallet.size()); - obj.pushKV("keypoololdest", pwallet->GetOldestKeyPoolTime()); + if (kp_oldest > 0) { + obj.pushKV("keypoololdest", kp_oldest); + } obj.pushKV("keypoolsize", (int64_t)kpExternalSize); LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan(); @@ -2505,6 +2509,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) } else { obj.pushKV("scanning", false); } + obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); return obj; } @@ -2701,6 +2706,7 @@ static UniValue createwallet(const JSONRPCRequest& request) {"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."}, {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, {"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, + {"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -2737,6 +2743,9 @@ static UniValue createwallet(const JSONRPCRequest& request) if (!request.params[4].isNull() && request.params[4].get_bool()) { flags |= WALLET_FLAG_AVOID_REUSE; } + if (!request.params[5].isNull() && request.params[5].get_bool()) { + flags |= WALLET_FLAG_DESCRIPTORS; + } std::string error; std::shared_ptr wallet; @@ -3815,7 +3824,7 @@ UniValue getaddressinfo(const JSONRPCRequest& request) ScriptPubKeyMan* spk_man = pwallet->GetScriptPubKeyMan(scriptPubKey); if (spk_man) { - if (const CKeyMetadata* meta = spk_man->GetMetadata(dest)) { + if (const std::unique_ptr meta = spk_man->GetMetadata(dest)) { ret.pushKV("timestamp", meta->nCreateTime); if (meta->has_key_origin) { ret.pushKV("hdkeypath", WriteHDKeypath(meta->key_origin.path)); @@ -4280,6 +4289,7 @@ UniValue importwallet(const JSONRPCRequest& request); UniValue importprunedfunds(const JSONRPCRequest& request); UniValue removeprunedfunds(const JSONRPCRequest& request); UniValue importmulti(const JSONRPCRequest& request); +UniValue importdescriptors(const JSONRPCRequest& request); void RegisterWalletRPCCommands(interfaces::Chain& chain, std::vector>& handlers) { @@ -4293,7 +4303,7 @@ static const CRPCCommand commands[] = { "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} }, { "wallet", "backupwallet", &backupwallet, {"destination"} }, { "wallet", "bumpfee", &bumpfee, {"txid", "options"} }, - { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} }, + { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors"} }, { "wallet", "dumpprivkey", &dumpprivkey, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, {"passphrase"} }, @@ -4309,6 +4319,7 @@ static const CRPCCommand commands[] = { "wallet", "getbalances", &getbalances, {} }, { "wallet", "getwalletinfo", &getwalletinfo, {} }, { "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} }, + { "wallet", "importdescriptors", &importdescriptors, {"requests"} }, { "wallet", "importmulti", &importmulti, {"requests","options"} }, { "wallet", "importprivkey", &importprivkey, {"privkey","label","rescan"} }, { "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} }, diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index ec75f06e5e..ecb95d599d 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -568,7 +568,7 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb return TransactionError::OK; } -const CKeyMetadata* LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const +std::unique_ptr LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const { LOCK(cs_KeyStore); @@ -576,14 +576,14 @@ const CKeyMetadata* LegacyScriptPubKeyMan::GetMetadata(const CTxDestination& des if (!key_id.IsNull()) { auto it = mapKeyMetadata.find(key_id); if (it != mapKeyMetadata.end()) { - return &it->second; + return MakeUnique(it->second); } } CScript scriptPubKey = GetScriptForDestination(dest); auto it = m_script_metadata.find(CScriptID(scriptPubKey)); if (it != m_script_metadata.end()) { - return &it->second; + return MakeUnique(it->second); } return nullptr; @@ -1497,3 +1497,668 @@ std::set LegacyScriptPubKeyMan::GetKeys() const } return set_address; } + +void LegacyScriptPubKeyMan::SetType(OutputType type, bool internal) {} + +bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) +{ + // Returns true if this descriptor supports getting new addresses. Conditions where we may be unable to fetch them (e.g. locked) are caught later + if (!CanGetAddresses(m_internal)) { + error = "No addresses available"; + return false; + } + { + LOCK(cs_desc_man); + assert(m_wallet_descriptor.descriptor->IsSingleType()); // This is a combo descriptor which should not be an active descriptor + if (type != m_address_type) { + throw std::runtime_error(std::string(__func__) + ": Types are inconsistent"); + } + + TopUp(); + + // Get the scriptPubKey from the descriptor + FlatSigningProvider out_keys; + std::vector scripts_temp; + if (m_wallet_descriptor.range_end <= m_max_cached_index && !TopUp(1)) { + // We can't generate anymore keys + error = "Error: Keypool ran out, please call keypoolrefill first"; + return false; + } + if (!m_wallet_descriptor.descriptor->ExpandFromCache(m_wallet_descriptor.next_index, m_wallet_descriptor.cache, scripts_temp, out_keys)) { + // We can't generate anymore keys + error = "Error: Keypool ran out, please call keypoolrefill first"; + return false; + } + + Optional out_script_type = m_wallet_descriptor.descriptor->GetOutputType(); + if (out_script_type && out_script_type == type) { + ExtractDestination(scripts_temp[0], dest); + } else { + throw std::runtime_error(std::string(__func__) + ": Types are inconsistent. Stored type does not match type of newly generated address"); + } + m_wallet_descriptor.next_index++; + WalletBatch(m_storage.GetDatabase()).WriteDescriptor(GetID(), m_wallet_descriptor); + return true; + } +} + +isminetype DescriptorScriptPubKeyMan::IsMine(const CScript& script) const +{ + LOCK(cs_desc_man); + if (m_map_script_pub_keys.count(script) > 0) { + return ISMINE_SPENDABLE; + } + return ISMINE_NO; +} + +bool DescriptorScriptPubKeyMan::CheckDecryptionKey(const CKeyingMaterial& master_key, bool accept_no_keys) +{ + LOCK(cs_desc_man); + if (!m_map_keys.empty()) { + return false; + } + + bool keyPass = m_map_crypted_keys.empty(); // Always pass when there are no encrypted keys + bool keyFail = false; + for (const auto& mi : m_map_crypted_keys) { + const CPubKey &pubkey = mi.second.first; + const std::vector &crypted_secret = mi.second.second; + CKey key; + if (!DecryptKey(master_key, crypted_secret, pubkey, key)) { + keyFail = true; + break; + } + keyPass = true; + if (m_decryption_thoroughly_checked) + break; + } + if (keyPass && keyFail) { + LogPrintf("The wallet is probably corrupted: Some keys decrypt but not all.\n"); + throw std::runtime_error("Error unlocking wallet: some keys decrypt but not all. Your wallet file may be corrupt."); + } + if (keyFail || (!keyPass && !accept_no_keys)) { + return false; + } + m_decryption_thoroughly_checked = true; + return true; +} + +bool DescriptorScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBatch* batch) +{ + LOCK(cs_desc_man); + if (!m_map_crypted_keys.empty()) { + return false; + } + + for (const KeyMap::value_type& key_in : m_map_keys) + { + const CKey &key = key_in.second; + CPubKey pubkey = key.GetPubKey(); + CKeyingMaterial secret(key.begin(), key.end()); + std::vector crypted_secret; + if (!EncryptSecret(master_key, secret, pubkey.GetHash(), crypted_secret)) { + return false; + } + m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret); + batch->WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret); + } + m_map_keys.clear(); + return true; +} + +bool DescriptorScriptPubKeyMan::GetReservedDestination(const OutputType type, bool internal, CTxDestination& address, int64_t& index, CKeyPool& keypool) +{ + LOCK(cs_desc_man); + std::string error; + bool result = GetNewDestination(type, address, error); + index = m_wallet_descriptor.next_index - 1; + return result; +} + +void DescriptorScriptPubKeyMan::ReturnDestination(int64_t index, bool internal, const CTxDestination& addr) +{ + LOCK(cs_desc_man); + // Only return when the index was the most recent + if (m_wallet_descriptor.next_index - 1 == index) { + m_wallet_descriptor.next_index--; + } + WalletBatch(m_storage.GetDatabase()).WriteDescriptor(GetID(), m_wallet_descriptor); + NotifyCanGetAddressesChanged(); +} + +std::map DescriptorScriptPubKeyMan::GetKeys() const +{ + AssertLockHeld(cs_desc_man); + if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked()) { + KeyMap keys; + for (auto key_pair : m_map_crypted_keys) { + const CPubKey& pubkey = key_pair.second.first; + const std::vector& crypted_secret = key_pair.second.second; + CKey key; + DecryptKey(m_storage.GetEncryptionKey(), crypted_secret, pubkey, key); + keys[pubkey.GetID()] = key; + } + return keys; + } + return m_map_keys; +} + +bool DescriptorScriptPubKeyMan::TopUp(unsigned int size) +{ + LOCK(cs_desc_man); + unsigned int target_size; + if (size > 0) { + target_size = size; + } else { + target_size = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 1); + } + + // Calculate the new range_end + int32_t new_range_end = std::max(m_wallet_descriptor.next_index + (int32_t)target_size, m_wallet_descriptor.range_end); + + // If the descriptor is not ranged, we actually just want to fill the first cache item + if (!m_wallet_descriptor.descriptor->IsRange()) { + new_range_end = 1; + m_wallet_descriptor.range_end = 1; + m_wallet_descriptor.range_start = 0; + } + + FlatSigningProvider provider; + provider.keys = GetKeys(); + + WalletBatch batch(m_storage.GetDatabase()); + uint256 id = GetID(); + for (int32_t i = m_max_cached_index + 1; i < new_range_end; ++i) { + FlatSigningProvider out_keys; + std::vector scripts_temp; + DescriptorCache temp_cache; + // Maybe we have a cached xpub and we can expand from the cache first + if (!m_wallet_descriptor.descriptor->ExpandFromCache(i, m_wallet_descriptor.cache, scripts_temp, out_keys)) { + if (!m_wallet_descriptor.descriptor->Expand(i, provider, scripts_temp, out_keys, &temp_cache)) return false; + } + // Add all of the scriptPubKeys to the scriptPubKey set + for (const CScript& script : scripts_temp) { + m_map_script_pub_keys[script] = i; + } + for (const auto& pk_pair : out_keys.pubkeys) { + const CPubKey& pubkey = pk_pair.second; + if (m_map_pubkeys.count(pubkey) != 0) { + // We don't need to give an error here. + // It doesn't matter which of many valid indexes the pubkey has, we just need an index where we can derive it and it's private key + continue; + } + m_map_pubkeys[pubkey] = i; + } + // Write the cache + for (const auto& parent_xpub_pair : temp_cache.GetCachedParentExtPubKeys()) { + CExtPubKey xpub; + if (m_wallet_descriptor.cache.GetCachedParentExtPubKey(parent_xpub_pair.first, xpub)) { + if (xpub != parent_xpub_pair.second) { + throw std::runtime_error(std::string(__func__) + ": New cached parent xpub does not match already cached parent xpub"); + } + continue; + } + if (!batch.WriteDescriptorParentCache(parent_xpub_pair.second, id, parent_xpub_pair.first)) { + throw std::runtime_error(std::string(__func__) + ": writing cache item failed"); + } + m_wallet_descriptor.cache.CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second); + } + for (const auto& derived_xpub_map_pair : temp_cache.GetCachedDerivedExtPubKeys()) { + for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) { + CExtPubKey xpub; + if (m_wallet_descriptor.cache.GetCachedDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, xpub)) { + if (xpub != derived_xpub_pair.second) { + throw std::runtime_error(std::string(__func__) + ": New cached derived xpub does not match already cached derived xpub"); + } + continue; + } + if (!batch.WriteDescriptorDerivedCache(derived_xpub_pair.second, id, derived_xpub_map_pair.first, derived_xpub_pair.first)) { + throw std::runtime_error(std::string(__func__) + ": writing cache item failed"); + } + m_wallet_descriptor.cache.CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second); + } + } + m_max_cached_index++; + } + m_wallet_descriptor.range_end = new_range_end; + batch.WriteDescriptor(GetID(), m_wallet_descriptor); + + // By this point, the cache size should be the size of the entire range + assert(m_wallet_descriptor.range_end - 1 == m_max_cached_index); + + NotifyCanGetAddressesChanged(); + return true; +} + +void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script) +{ + LOCK(cs_desc_man); + if (IsMine(script)) { + int32_t index = m_map_script_pub_keys[script]; + if (index >= m_wallet_descriptor.next_index) { + WalletLogPrintf("%s: Detected a used keypool item at index %d, mark all keypool items up to this item as used\n", __func__, index); + m_wallet_descriptor.next_index = index + 1; + } + if (!TopUp()) { + WalletLogPrintf("%s: Topping up keypool failed (locked wallet)\n", __func__); + } + } +} + +void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey &pubkey) +{ + LOCK(cs_desc_man); + WalletBatch batch(m_storage.GetDatabase()); + if (!AddDescriptorKeyWithDB(batch, key, pubkey)) { + throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed"); + } +} + +bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) +{ + AssertLockHeld(cs_desc_man); + assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); + + if (m_storage.HasEncryptionKeys()) { + if (m_storage.IsLocked()) { + return false; + } + + std::vector crypted_secret; + CKeyingMaterial secret(key.begin(), key.end()); + if (!EncryptSecret(m_storage.GetEncryptionKey(), secret, pubkey.GetHash(), crypted_secret)) { + return false; + } + + m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret); + return batch.WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret); + } else { + m_map_keys[pubkey.GetID()] = key; + return batch.WriteDescriptorKey(GetID(), pubkey, key.GetPrivKey()); + } +} + +bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key) +{ + LOCK(cs_desc_man); + assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); + + // Ignore when there is already a descriptor + if (m_wallet_descriptor.descriptor) { + return false; + } + + int64_t creation_time = GetTime(); + + std::string xpub = EncodeExtPubKey(master_key.Neuter()); + + // Build descriptor string + std::string desc_prefix; + std::string desc_suffix = "/*)"; + switch (m_address_type) { + case OutputType::LEGACY: { + desc_prefix = "pkh(" + xpub + "/44'"; + break; + } + case OutputType::P2SH_SEGWIT: { + desc_prefix = "sh(wpkh(" + xpub + "/49'"; + desc_suffix += ")"; + break; + } + case OutputType::BECH32: { + desc_prefix = "wpkh(" + xpub + "/84'"; + break; + } + default: assert(false); + } + + // Mainnet derives at 0', testnet and regtest derive at 1' + if (Params().IsTestChain()) { + desc_prefix += "/1'"; + } else { + desc_prefix += "/0'"; + } + + std::string internal_path = m_internal ? "/1" : "/0"; + std::string desc_str = desc_prefix + "/0'" + internal_path + desc_suffix; + + // Make the descriptor + FlatSigningProvider keys; + std::string error; + std::unique_ptr desc = Parse(desc_str, keys, error, false); + WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0); + m_wallet_descriptor = w_desc; + + // Store the master private key, and descriptor + WalletBatch batch(m_storage.GetDatabase()); + if (!AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey())) { + throw std::runtime_error(std::string(__func__) + ": writing descriptor master private key failed"); + } + if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) { + throw std::runtime_error(std::string(__func__) + ": writing descriptor failed"); + } + + // TopUp + TopUp(); + + m_storage.UnsetBlankWalletFlag(batch); + return true; +} + +bool DescriptorScriptPubKeyMan::IsHDEnabled() const +{ + LOCK(cs_desc_man); + return m_wallet_descriptor.descriptor->IsRange(); +} + +bool DescriptorScriptPubKeyMan::CanGetAddresses(bool internal) const +{ + // We can only give out addresses from descriptors that are single type (not combo), ranged, + // and either have cached keys or can generate more keys (ignoring encryption) + LOCK(cs_desc_man); + return m_wallet_descriptor.descriptor->IsSingleType() && + m_wallet_descriptor.descriptor->IsRange() && + (HavePrivateKeys() || m_wallet_descriptor.next_index < m_wallet_descriptor.range_end); +} + +bool DescriptorScriptPubKeyMan::HavePrivateKeys() const +{ + LOCK(cs_desc_man); + return m_map_keys.size() > 0 || m_map_crypted_keys.size() > 0; +} + +int64_t DescriptorScriptPubKeyMan::GetOldestKeyPoolTime() const +{ + // This is only used for getwalletinfo output and isn't relevant to descriptor wallets. + // The magic number 0 indicates that it shouldn't be displayed so that's what we return. + return 0; +} + +size_t DescriptorScriptPubKeyMan::KeypoolCountExternalKeys() const +{ + if (m_internal) { + return 0; + } + return GetKeyPoolSize(); +} + +unsigned int DescriptorScriptPubKeyMan::GetKeyPoolSize() const +{ + LOCK(cs_desc_man); + return m_wallet_descriptor.range_end - m_wallet_descriptor.next_index; +} + +int64_t DescriptorScriptPubKeyMan::GetTimeFirstKey() const +{ + LOCK(cs_desc_man); + return m_wallet_descriptor.creation_time; +} + +std::unique_ptr DescriptorScriptPubKeyMan::GetSigningProvider(const CScript& script, bool include_private) const +{ + LOCK(cs_desc_man); + + // Find the index of the script + auto it = m_map_script_pub_keys.find(script); + if (it == m_map_script_pub_keys.end()) { + return nullptr; + } + int32_t index = it->second; + + return GetSigningProvider(index, include_private); +} + +std::unique_ptr DescriptorScriptPubKeyMan::GetSigningProvider(const CPubKey& pubkey) const +{ + LOCK(cs_desc_man); + + // Find index of the pubkey + auto it = m_map_pubkeys.find(pubkey); + if (it == m_map_pubkeys.end()) { + return nullptr; + } + int32_t index = it->second; + + // Always try to get the signing provider with private keys. This function should only be called during signing anyways + return GetSigningProvider(index, true); +} + +std::unique_ptr DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const +{ + AssertLockHeld(cs_desc_man); + // Get the scripts, keys, and key origins for this script + std::unique_ptr out_keys = MakeUnique(); + std::vector scripts_temp; + if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr; + + if (HavePrivateKeys() && include_private) { + FlatSigningProvider master_provider; + master_provider.keys = GetKeys(); + m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys); + } + + return out_keys; +} + +std::unique_ptr DescriptorScriptPubKeyMan::GetSolvingProvider(const CScript& script) const +{ + return GetSigningProvider(script, false); +} + +bool DescriptorScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sigdata) +{ + return IsMine(script); +} + +bool DescriptorScriptPubKeyMan::SignTransaction(CMutableTransaction& tx, const std::map& coins, int sighash, std::map& input_errors) const +{ + std::unique_ptr keys = MakeUnique(); + for (const auto& coin_pair : coins) { + std::unique_ptr coin_keys = GetSigningProvider(coin_pair.second.out.scriptPubKey, true); + if (!coin_keys) { + continue; + } + *keys = Merge(*keys, *coin_keys); + } + + return ::SignTransaction(tx, keys.get(), coins, sighash, input_errors); +} + +SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const +{ + std::unique_ptr keys = GetSigningProvider(GetScriptForDestination(pkhash), true); + if (!keys) { + return SigningResult::PRIVATE_KEY_NOT_AVAILABLE; + } + + CKeyID key_id(pkhash); + CKey key; + if (!keys->GetKey(key_id, key)) { + return SigningResult::PRIVATE_KEY_NOT_AVAILABLE; + } + + if (!MessageSign(key, message, str_sig)) { + return SigningResult::SIGNING_FAILED; + } + return SigningResult::OK; +} + +TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const +{ + for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) { + const CTxIn& txin = psbtx.tx->vin[i]; + PSBTInput& input = psbtx.inputs.at(i); + + if (PSBTInputSigned(input)) { + continue; + } + + // Verify input looks sane. This will check that we have at most one uxto, witness or non-witness. + if (!input.IsSane()) { + return TransactionError::INVALID_PSBT; + } + + // Get the Sighash type + if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) { + return TransactionError::SIGHASH_MISMATCH; + } + + // Get the scriptPubKey to know which SigningProvider to use + CScript script; + if (!input.witness_utxo.IsNull()) { + script = input.witness_utxo.scriptPubKey; + } else if (input.non_witness_utxo) { + if (txin.prevout.n >= input.non_witness_utxo->vout.size()) { + return TransactionError::MISSING_INPUTS; + } + script = input.non_witness_utxo->vout[txin.prevout.n].scriptPubKey; + } else { + // There's no UTXO so we can just skip this now + continue; + } + SignatureData sigdata; + input.FillSignatureData(sigdata); + + std::unique_ptr keys = MakeUnique(); + std::unique_ptr script_keys = GetSigningProvider(script, sign); + if (script_keys) { + *keys = Merge(*keys, *script_keys); + } else { + // Maybe there are pubkeys listed that we can sign for + script_keys = MakeUnique(); + for (const auto& pk_pair : input.hd_keypaths) { + const CPubKey& pubkey = pk_pair.first; + std::unique_ptr pk_keys = GetSigningProvider(pubkey); + if (pk_keys) { + *keys = Merge(*keys, *pk_keys); + } + } + } + + SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, sighash_type); + } + + // Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change + for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) { + std::unique_ptr keys = GetSolvingProvider(psbtx.tx->vout.at(i).scriptPubKey); + if (!keys) { + continue; + } + UpdatePSBTOutput(HidingSigningProvider(keys.get(), true, !bip32derivs), psbtx, i); + } + + return TransactionError::OK; +} + +std::unique_ptr DescriptorScriptPubKeyMan::GetMetadata(const CTxDestination& dest) const +{ + std::unique_ptr provider = GetSigningProvider(GetScriptForDestination(dest)); + if (provider) { + KeyOriginInfo orig; + CKeyID key_id = GetKeyForDestination(*provider, dest); + if (provider->GetKeyOrigin(key_id, orig)) { + LOCK(cs_desc_man); + std::unique_ptr meta = MakeUnique(); + meta->key_origin = orig; + meta->has_key_origin = true; + meta->nCreateTime = m_wallet_descriptor.creation_time; + return meta; + } + } + return nullptr; +} + +uint256 DescriptorScriptPubKeyMan::GetID() const +{ + LOCK(cs_desc_man); + std::string desc_str = m_wallet_descriptor.descriptor->ToString(); + uint256 id; + CSHA256().Write((unsigned char*)desc_str.data(), desc_str.size()).Finalize(id.begin()); + return id; +} + +void DescriptorScriptPubKeyMan::SetType(OutputType type, bool internal) +{ + this->m_address_type = type; + this->m_internal = internal; +} + +void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache) +{ + LOCK(cs_desc_man); + m_wallet_descriptor.cache = cache; + for (int32_t i = m_wallet_descriptor.range_start; i < m_wallet_descriptor.range_end; ++i) { + FlatSigningProvider out_keys; + std::vector scripts_temp; + if (!m_wallet_descriptor.descriptor->ExpandFromCache(i, m_wallet_descriptor.cache, scripts_temp, out_keys)) { + throw std::runtime_error("Error: Unable to expand wallet descriptor from cache"); + } + // Add all of the scriptPubKeys to the scriptPubKey set + for (const CScript& script : scripts_temp) { + if (m_map_script_pub_keys.count(script) != 0) { + throw std::runtime_error(strprintf("Error: Already loaded script at index %d as being at index %d", i, m_map_script_pub_keys[script])); + } + m_map_script_pub_keys[script] = i; + } + for (const auto& pk_pair : out_keys.pubkeys) { + const CPubKey& pubkey = pk_pair.second; + if (m_map_pubkeys.count(pubkey) != 0) { + // We don't need to give an error here. + // It doesn't matter which of many valid indexes the pubkey has, we just need an index where we can derive it and it's private key + continue; + } + m_map_pubkeys[pubkey] = i; + } + m_max_cached_index++; + } +} + +bool DescriptorScriptPubKeyMan::AddKey(const CKeyID& key_id, const CKey& key) +{ + LOCK(cs_desc_man); + m_map_keys[key_id] = key; + return true; +} + +bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector& crypted_key) +{ + LOCK(cs_desc_man); + if (!m_map_keys.empty()) { + return false; + } + + m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key); + return true; +} + +bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const +{ + LOCK(cs_desc_man); + return m_wallet_descriptor.descriptor != nullptr && desc.descriptor != nullptr && m_wallet_descriptor.descriptor->ToString() == desc.descriptor->ToString(); +} + +void DescriptorScriptPubKeyMan::WriteDescriptor() +{ + LOCK(cs_desc_man); + WalletBatch batch(m_storage.GetDatabase()); + if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) { + throw std::runtime_error(std::string(__func__) + ": writing descriptor failed"); + } +} + +const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const +{ + return m_wallet_descriptor; +} + +const std::vector DescriptorScriptPubKeyMan::GetScriptPubKeys() const +{ + LOCK(cs_desc_man); + std::vector script_pub_keys; + script_pub_keys.reserve(m_map_script_pub_keys.size()); + + for (auto const& script_pub_key: m_map_script_pub_keys) { + script_pub_keys.push_back(script_pub_key.first); + } + return script_pub_keys; +} diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 8512eadf31..3117b13d35 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -6,6 +6,7 @@ #define BITCOIN_WALLET_SCRIPTPUBKEYMAN_H #include +#include