Merge 27034 via rpc_importaddr_for_descwallet-27+k

This commit is contained in:
Luke Dashjr 2025-03-05 03:27:08 +00:00
commit 3422369f44
8 changed files with 92 additions and 9 deletions

View File

@ -148,8 +148,6 @@ std::string DescriptorChecksum(const Span<const char>& span)
return ret; return ret;
} }
std::string AddChecksum(const std::string& str) { return str + "#" + DescriptorChecksum(str); }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Internal representation // // Internal representation //
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -2086,6 +2084,8 @@ std::string GetDescriptorChecksum(const std::string& descriptor)
return ret; return ret;
} }
std::string AddChecksum(const std::string& str) { return str + "#" + DescriptorChecksum(str); }
std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider) std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider)
{ {
return InferScript(script, ParseScriptContext::TOP, provider); return InferScript(script, ParseScriptContext::TOP, provider);

View File

@ -185,6 +185,11 @@ std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProv
*/ */
std::string GetDescriptorChecksum(const std::string& descriptor); std::string GetDescriptorChecksum(const std::string& descriptor);
/**
* Simple wrapper to add the checksum at the end of the descriptor
*/
std::string AddChecksum(const std::string& str);
/** Find a descriptor for the specified `script`, using information from `provider` where possible. /** Find a descriptor for the specified `script`, using information from `provider` where possible.
* *
* A non-ranged descriptor which only generates the specified script will be returned in all * A non-ranged descriptor which only generates the specified script will be returned in all

View File

@ -216,6 +216,8 @@ RPCHelpMan importprivkey()
}; };
} }
UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
RPCHelpMan importaddress() RPCHelpMan importaddress()
{ {
return RPCHelpMan{"importaddress", return RPCHelpMan{"importaddress",
@ -229,7 +231,7 @@ RPCHelpMan importaddress()
"\nNote: If you import a non-standard raw script in hex form, outputs sending to it will be treated\n" "\nNote: If you import a non-standard raw script in hex form, outputs sending to it will be treated\n"
"as change, and not show up in many RPCs.\n" "as change, and not show up in many RPCs.\n"
"Note: Use \"getwalletinfo\" to query the scanning progress.\n" "Note: Use \"getwalletinfo\" to query the scanning progress.\n"
"Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" for descriptor wallets.\n", "Note: For descriptor wallets, this command will create new descriptor/s, and only works if the wallet has private keys disabled.\n",
{ {
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The Bitcoin address (or hex-encoded script)"}, {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The Bitcoin address (or hex-encoded script)"},
{"label", RPCArg::Type::STR, RPCArg::Default{""}, "An optional label"}, {"label", RPCArg::Type::STR, RPCArg::Default{""}, "An optional label"},
@ -250,7 +252,18 @@ RPCHelpMan importaddress()
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request); std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL; if (!pwallet) return UniValue::VNULL;
// Use legacy spkm only if the wallet does not support descriptors.
bool use_legacy = !pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS);
if (use_legacy) {
// In case the wallet is blank
EnsureLegacyScriptPubKeyMan(*pwallet, true); EnsureLegacyScriptPubKeyMan(*pwallet, true);
} else {
// We don't allow mixing watch-only descriptors with spendable ones.
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import address in wallet with private keys enabled. "
"Create wallet with no private keys to watch specific addresses/scripts");
}
}
const std::string strLabel{LabelFromValue(request.params[1])}; const std::string strLabel{LabelFromValue(request.params[1])};
@ -276,23 +289,41 @@ RPCHelpMan importaddress()
if (!request.params[3].isNull()) if (!request.params[3].isNull())
fP2SH = request.params[3].get_bool(); fP2SH = request.params[3].get_bool();
// Import descriptor helper function
const auto& import_descriptor = [pwallet](const std::string& desc, const std::string label) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) {
UniValue data(UniValue::VType::VOBJ);
data.pushKV("desc", AddChecksum(desc));
if (!label.empty()) data.pushKV("label", label);
const UniValue& ret = ProcessDescriptorImport(*pwallet, data, /*timestamp=*/1);
if (ret.exists("error")) throw ret["error"];
};
{ {
LOCK(pwallet->cs_wallet); LOCK(pwallet->cs_wallet);
CTxDestination dest = DecodeDestination(request.params[0].get_str()); const std::string& address = request.params[0].get_str();
CTxDestination dest = DecodeDestination(address);
if (IsValidDestination(dest)) { if (IsValidDestination(dest)) {
if (fP2SH) { if (fP2SH) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot use the p2sh flag with an address - use a script instead"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot use the p2sh flag with an address - use a script instead");
} }
if (OutputTypeFromDestination(dest) == OutputType::BECH32M) { if (OutputTypeFromDestination(dest) == OutputType::BECH32M) {
if (use_legacy)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m addresses cannot be imported into legacy wallets"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m addresses cannot be imported into legacy wallets");
} }
pwallet->MarkDirty(); pwallet->MarkDirty();
if (use_legacy) {
pwallet->ImportScriptPubKeys(strLabel, {GetScriptForDestination(dest)}, /*have_solving_data=*/false, /*apply_label=*/true, /*timestamp=*/1); pwallet->ImportScriptPubKeys(strLabel, {GetScriptForDestination(dest)}, /*have_solving_data=*/false, /*apply_label=*/true, /*timestamp=*/1);
} else {
import_descriptor("addr(" + address + ")", strLabel);
}
} else if (IsHex(request.params[0].get_str())) { } else if (IsHex(request.params[0].get_str())) {
std::vector<unsigned char> data(ParseHex(request.params[0].get_str())); const std::string& hex = request.params[0].get_str();
if (use_legacy) {
std::vector<unsigned char> data(ParseHex(hex));
CScript redeem_script(data.begin(), data.end()); CScript redeem_script(data.begin(), data.end());
std::set<CScript> scripts = {redeem_script}; std::set<CScript> scripts = {redeem_script};
@ -303,6 +334,14 @@ RPCHelpMan importaddress()
} }
pwallet->ImportScriptPubKeys(strLabel, scripts, /*have_solving_data=*/false, /*apply_label=*/true, /*timestamp=*/1); pwallet->ImportScriptPubKeys(strLabel, scripts, /*have_solving_data=*/false, /*apply_label=*/true, /*timestamp=*/1);
} else {
// P2SH Not allowed. Can't detect inner P2SH function from a raw hex.
if (fP2SH) throw JSONRPCError(RPC_WALLET_ERROR, "P2SH import feature disabled for descriptors' wallet. "
"Use 'importdescriptors' to specify inner P2SH function");
// Import descriptors
import_descriptor("raw(" + hex + ")", strLabel);
}
} else { } else {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script");
} }

View File

@ -984,7 +984,7 @@ class RPCOverloadWrapper():
if not import_res[0]['success']: if not import_res[0]['success']:
raise JSONRPCException(import_res[0]['error']) raise JSONRPCException(import_res[0]['error'])
def importaddress(self, address, label=None, rescan=None, p2sh=None): def _deleted_importaddress(self, address, label=None, rescan=None, p2sh=None):
wallet_info = self.getwalletinfo() wallet_info = self.getwalletinfo()
if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']):
return self.__getattr__('importaddress')(address, label, rescan, p2sh) return self.__getattr__('importaddress')(address, label, rescan, p2sh)

View File

@ -61,6 +61,24 @@ class WalletTest(BitcoinTestFramework):
def get_vsize(self, txn): def get_vsize(self, txn):
return self.nodes[0].decoderawtransaction(txn)['vsize'] return self.nodes[0].decoderawtransaction(txn)['vsize']
def test_legacy_importaddress(self):
if self.options.descriptors:
return
addr = self.nodes[1].getnewaddress()
self.nodes[1].sendtoaddress(addr, 10)
self.sync_mempools(self.nodes[0:2])
self.log.info("Test 'importaddress' on a blank, private keys disabled, wallet with no descriptors support")
self.nodes[0].createwallet(wallet_name="watch-only-legacy", disable_private_keys=False, descriptors=False, blank=True)
wallet_watch_only = self.nodes[0].get_wallet_rpc("watch-only-legacy")
wallet_watch_only.importaddress(addr)
assert_equal(wallet_watch_only.getaddressinfo(addr)['ismine'], False)
assert_equal(wallet_watch_only.getaddressinfo(addr)['iswatchonly'], True)
assert_equal(wallet_watch_only.getaddressinfo(addr)['solvable'], False)
assert_equal(wallet_watch_only.getbalances()["watchonly"]['untrusted_pending'], 10)
self.nodes[0].unloadwallet("watch-only-legacy")
def run_test(self): def run_test(self):
# Check that there's no UTXO on none of the nodes # Check that there's no UTXO on none of the nodes
@ -558,6 +576,9 @@ class WalletTest(BitcoinTestFramework):
{"address": address_to_import}, {"address": address_to_import},
{"spendable": True}) {"spendable": True})
# Test importaddress on a blank, private keys disabled, legacy wallet with no descriptors support
self.test_legacy_importaddress()
# Mine a block from node0 to an address from node1 # Mine a block from node0 to an address from node1
coinbase_addr = self.nodes[1].getnewaddress() coinbase_addr = self.nodes[1].getnewaddress()
block_hash = self.generatetoaddress(self.nodes[0], 1, coinbase_addr, sync_fun=lambda: self.sync_all(self.nodes[0:3]))[0] block_hash = self.generatetoaddress(self.nodes[0], 1, coinbase_addr, sync_fun=lambda: self.sync_all(self.nodes[0:3]))[0]

View File

@ -153,7 +153,6 @@ class WalletDescriptorTest(BitcoinTestFramework):
self.log.info("Test disabled RPCs") self.log.info("Test disabled RPCs")
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importprivkey, "cVpF924EspNh8KjYsfhgY96mmxvT6DgdWiTYMtMjuM74hJaU5psW") assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importprivkey, "cVpF924EspNh8KjYsfhgY96mmxvT6DgdWiTYMtMjuM74hJaU5psW")
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importpubkey, send_wrpc.getaddressinfo(send_wrpc.getnewaddress())["pubkey"]) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importpubkey, send_wrpc.getaddressinfo(send_wrpc.getnewaddress())["pubkey"])
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importaddress, recv_wrpc.getnewaddress())
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importmulti, []) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importmulti, [])
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.addmultisigaddress, 1, [recv_wrpc.getnewaddress()]) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.addmultisigaddress, 1, [recv_wrpc.getnewaddress()])
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.dumpprivkey, recv_wrpc.getnewaddress()) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.dumpprivkey, recv_wrpc.getnewaddress())
@ -161,6 +160,16 @@ class WalletDescriptorTest(BitcoinTestFramework):
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importwallet, 'wallet.dump') assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importwallet, 'wallet.dump')
assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.sethdseed) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.sethdseed)
# Test importaddress
self.log.info("Test watch-only descriptor wallet")
self.nodes[0].createwallet(wallet_name="watch-only-desc", disable_private_keys=True, descriptors=True, blank=True)
wallet_watch_only = self.nodes[0].get_wallet_rpc("watch-only-desc")
wallet_watch_only.importaddress(addr)
assert_equal(wallet_watch_only.getaddressinfo(addr)['ismine'], True)
assert_equal(wallet_watch_only.getaddressinfo(addr)['solvable'], False)
assert_equal(wallet_watch_only.getbalances()["mine"]['untrusted_pending'], 10)
self.nodes[0].unloadwallet("watch-only-desc")
self.log.info("Test encryption") self.log.info("Test encryption")
# Get the master fingerprint before encrypt # Get the master fingerprint before encrypt
info1 = send_wrpc.getaddressinfo(send_wrpc.getnewaddress()) info1 = send_wrpc.getaddressinfo(send_wrpc.getnewaddress())

View File

@ -212,7 +212,7 @@ class WalletLabelsTest(BitcoinTestFramework):
ad = BECH32_INVALID[l] ad = BECH32_INVALID[l]
assert_raises_rpc_error( assert_raises_rpc_error(
-5, -5,
"Address is not valid" if self.options.descriptors else "Invalid Bitcoin address or script", "Invalid Bitcoin address or script",
lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad), lambda: wallet_watch_only.importaddress(label=l, rescan=False, address=ad),
) )

View File

@ -8,6 +8,7 @@
import time import time
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
from test_framework.descriptors import descsum_create
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
@ -52,7 +53,15 @@ class WalletReindexTest(BitcoinTestFramework):
# For a descriptors wallet: Import address with timestamp=now. # For a descriptors wallet: Import address with timestamp=now.
# For legacy wallet: There is no way of importing a script/address with a custom time. The wallet always imports it with birthtime=1. # For legacy wallet: There is no way of importing a script/address with a custom time. The wallet always imports it with birthtime=1.
# In both cases, disable rescan to not detect the transaction. # In both cases, disable rescan to not detect the transaction.
wallet_watch_only.importaddress(wallet_addr, rescan=False) if self.options.descriptors:
import_res = wallet_watch_only.importdescriptors([{
'desc': descsum_create('addr(' + wallet_addr + ')'),
'timestamp': 'now',
}])
assert len(import_res) == 1
assert import_res[0]['success']
else:
wallet_watch_only.importaddress(wallet_addr, rescan=False)
assert_equal(len(wallet_watch_only.listtransactions()), 0) assert_equal(len(wallet_watch_only.listtransactions()), 0)
# Depending on the wallet type, the birth time changes. # Depending on the wallet type, the birth time changes.