mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-06-10 06:39:15 +02:00
Merge bitcoin/bitcoin#29136: wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor
a39cc16b43doc: Release note for addhdkey (Ava Chow)89b9a01b4ewallet, rpc: Disallow importing unused() to wallets without privkeys (Ava Chow)35bbee6374wallet, rpc: Disallow import of unused() if key already exists (Ava Chow)f3f8bcbd1dwallet: Add addhdkey RPC (Ava Chow)82bc280de4test: Simple test for importing unused(KEY) (Ava Chow)80c29bc6f1descriptor: Add unused(KEY) descriptor (Ava Chow) Pull request description: It is sometimes useful for the wallet to have keys that it can sign with but are not (initially) involved in any scripts, e.g. for setting up a multisig. Ryanofsky [suggested](https://github.com/bitcoin/bitcoin/pull/26728#issuecomment-1867721948) A `unused(KEY)` descriptor which allows for a key to be specified, but produces no scripts. These can be imported into the wallet, and subsequently retrieved with `gethdkeys`. Additionally, `listdescriptors` will output these descriptors so that they can be easily backed up. In order to make it easier for people to add HD keys to their wallet, and to generate a new one if they want to rotate their descriptors, an `addhdkey` RPC is also added. Without arguments, it will generate a new HD key and add it to the wallet via a `unused(KEY)` descriptor. If provided a private key, it will construct the descriptor and add it to the wallet. See also: https://github.com/bitcoin/bitcoin/pull/26728#issuecomment-1866961865 Based on #29130 as `gethdkeys` is useful for testing this. ACKs for top commit: Sjors: utACKa39cc16rkrux: lgtm ACKa39cc16b43Tree-SHA512: c1288c792ab01ca2eaddd24b0e7d11c259cd59e79042465d0d1eb656fd559c1200dc19750b4d84acc762b5b599935a06df214c18226e662087842ea91ec3011b
This commit is contained in:
6
doc/release-notes-29136.md
Normal file
6
doc/release-notes-29136.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Wallet
|
||||
------
|
||||
- A new RPC `addhdkey` is added which allows a BIP 32 extended key to be added to the wallet without
|
||||
needing to import it as part of a separate descriptor. This key will not be used to produce any
|
||||
output scripts unless it is explicitly imported as part of a separate descriptor independent of
|
||||
the `addhdkey` RPC.
|
||||
@@ -1038,6 +1038,8 @@ public:
|
||||
|
||||
virtual std::unique_ptr<DescriptorImpl> Clone() const = 0;
|
||||
|
||||
bool HasScripts() const override { return true; }
|
||||
|
||||
// NOLINTNEXTLINE(misc-no-recursion)
|
||||
std::vector<std::string> Warnings() const override {
|
||||
std::vector<std::string> all = m_warnings;
|
||||
@@ -1738,6 +1740,23 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/** A parsed unused(KEY) descriptor */
|
||||
class UnusedDescriptor final : public DescriptorImpl
|
||||
{
|
||||
protected:
|
||||
std::vector<CScript> MakeScripts(const std::vector<CPubKey>& keys, std::span<const CScript> scripts, FlatSigningProvider& out) const override { return {}; }
|
||||
public:
|
||||
UnusedDescriptor(std::unique_ptr<PubkeyProvider> prov) : DescriptorImpl(Vector(std::move(prov)), "unused") {}
|
||||
bool IsSingleType() const final { return true; }
|
||||
bool HasScripts() const override { return false; }
|
||||
|
||||
std::unique_ptr<DescriptorImpl> Clone() const override
|
||||
{
|
||||
return std::make_unique<UnusedDescriptor>(m_pubkey_args.at(0)->Clone());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Parser //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -2575,6 +2594,27 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
|
||||
error = "Can only have rawtr at top level";
|
||||
return {};
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("unused", expr)) {
|
||||
// Check for only one expression, should not find commas, brackets, or parentheses
|
||||
auto arg = Expr(expr);
|
||||
if (expr.size()) {
|
||||
error = strprintf("unused(): only one key expected");
|
||||
return {};
|
||||
}
|
||||
auto keys = ParsePubkey(key_exp_index, arg, ctx, out, error);
|
||||
if (keys.empty()) return {};
|
||||
for (auto& pubkey : keys) {
|
||||
if (pubkey->IsRange()) {
|
||||
error = "unused(): key cannot be ranged";
|
||||
return {};
|
||||
}
|
||||
ret.emplace_back(std::make_unique<UnusedDescriptor>(std::move(pubkey)));
|
||||
}
|
||||
return ret;
|
||||
} else if (Func("unused", expr)) {
|
||||
error = "Can only have unused at top level";
|
||||
return {};
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("raw", expr)) {
|
||||
std::string str(expr.begin(), expr.end());
|
||||
if (!IsHex(str)) {
|
||||
|
||||
@@ -108,7 +108,7 @@ struct Descriptor {
|
||||
/** Convert the descriptor back to a string, undoing parsing. */
|
||||
virtual std::string ToString(bool compat_format=false) const = 0;
|
||||
|
||||
/** Whether this descriptor will return one scriptPubKey or multiple (aka is or is not combo) */
|
||||
/** Whether this descriptor will return at most one scriptPubKey or multiple (aka is or is not combo) */
|
||||
virtual bool IsSingleType() const = 0;
|
||||
|
||||
/** Whether the given provider has all private keys required by this descriptor.
|
||||
@@ -179,6 +179,9 @@ struct Descriptor {
|
||||
*/
|
||||
virtual void GetPubKeys(std::set<CPubKey>& pubkeys, std::set<CExtPubKey>& ext_pubs) const = 0;
|
||||
|
||||
/** Whether this descriptor produces any scripts with the Expand functions */
|
||||
virtual bool HasScripts() const = 0;
|
||||
|
||||
/** Semantic/safety warnings (includes subdescriptors). */
|
||||
virtual std::vector<std::string> Warnings() const = 0;
|
||||
|
||||
|
||||
@@ -1334,4 +1334,65 @@ BOOST_AUTO_TEST_CASE(descriptor_older_warnings)
|
||||
}
|
||||
}
|
||||
|
||||
void CheckSingleUnparsable(const std::string& desc, const std::string& expected_error)
|
||||
{
|
||||
FlatSigningProvider keys;
|
||||
std::string error;
|
||||
auto parsed = Parse(desc, keys, error);
|
||||
BOOST_CHECK_MESSAGE(parsed.empty(), desc);
|
||||
BOOST_CHECK_EQUAL(error, expected_error);
|
||||
}
|
||||
|
||||
void CheckUnused(const std::string& prv, const std::string& pub)
|
||||
{
|
||||
FlatSigningProvider keys_priv, keys_pub;
|
||||
std::string error;
|
||||
|
||||
std::unique_ptr<Descriptor> parse_priv;
|
||||
std::unique_ptr<Descriptor> parse_pub;
|
||||
parse_priv = std::move(Parse(prv, keys_priv, error).at(0));
|
||||
parse_pub = std::move(Parse(pub, keys_pub, error).at(0));
|
||||
BOOST_CHECK_MESSAGE(parse_priv, error);
|
||||
BOOST_CHECK_MESSAGE(parse_pub, error);
|
||||
|
||||
BOOST_CHECK(parse_priv->GetOutputType() == std::nullopt);
|
||||
BOOST_CHECK(parse_pub->GetOutputType() == std::nullopt);
|
||||
|
||||
// Check private keys are extracted from the private version but not the public one.
|
||||
BOOST_CHECK(keys_priv.keys.size());
|
||||
BOOST_CHECK(!keys_pub.keys.size());
|
||||
|
||||
// Check that both versions serialize back to the public version.
|
||||
std::string pub1 = parse_priv->ToString();
|
||||
std::string pub2 = parse_pub->ToString();
|
||||
BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub1), "Private ser: " + pub1 + " Public desc: " + pub);
|
||||
BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub);
|
||||
|
||||
// Check both only have one pubkey
|
||||
std::set<CPubKey> prv_pubkeys;
|
||||
std::set<CExtPubKey> prv_extpubs;
|
||||
parse_pub->GetPubKeys(prv_pubkeys, prv_extpubs);
|
||||
BOOST_CHECK_EQUAL(prv_pubkeys.size() + prv_extpubs.size(), 1);
|
||||
std::set<CPubKey> pub_pubkeys;
|
||||
std::set<CExtPubKey> pub_extpubs;
|
||||
parse_pub->GetPubKeys(pub_pubkeys, pub_extpubs);
|
||||
BOOST_CHECK_EQUAL(pub_pubkeys.size() + pub_extpubs.size(), 1);
|
||||
}
|
||||
|
||||
// unused() descriptors don't produce scripts, so these need to be tested separately
|
||||
BOOST_AUTO_TEST_CASE(unused_descriptor_test)
|
||||
{
|
||||
CheckUnparsable("unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "unused(): only one key expected");
|
||||
CheckUnparsable("wsh(unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Can only have unused at top level");
|
||||
CheckUnparsable("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/*)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/*)", "unused(): key cannot be ranged");
|
||||
CheckUnparsable("unused()", "unused()", "No key provided");
|
||||
|
||||
// x-only keys cannot be used in unused()
|
||||
CheckSingleUnparsable("unused(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "Pubkey 'a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd' is invalid");
|
||||
|
||||
CheckUnused("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)");
|
||||
CheckUnused("unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)");
|
||||
CheckUnused("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/0h/0h/1)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0h/0h/1)");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -265,6 +265,26 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an unused(KEY) descriptor, check that the wallet doesn't already have other descriptors with this key
|
||||
if (!parsed_desc->HasScripts()) {
|
||||
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import unused() to wallet without private keys enabled");
|
||||
}
|
||||
// Unused descriptors must contain a single key.
|
||||
// Earlier checks will have enforced that this key is either a private key when private keys are enabled,
|
||||
// or that this key is a public key when private keys are disabled.
|
||||
// If we can retrieve the corresponding private key from the wallet, then this key is already in the wallet
|
||||
// and we should not import it.
|
||||
std::set<CPubKey> pubkeys;
|
||||
std::set<CExtPubKey> extpubs;
|
||||
parsed_desc->GetPubKeys(pubkeys, extpubs);
|
||||
std::transform(extpubs.begin(), extpubs.end(), std::inserter(pubkeys, pubkeys.begin()), [](const CExtPubKey& xpub) { return xpub.pubkey; });
|
||||
CHECK_NONFATAL(pubkeys.size() == 1);
|
||||
if (wallet.GetKey(pubkeys.begin()->GetID())) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import an unused() descriptor when its private key is already in the wallet");
|
||||
}
|
||||
}
|
||||
|
||||
WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
|
||||
|
||||
// Add descriptor to the wallet
|
||||
|
||||
@@ -840,6 +840,82 @@ static RPCMethod createwalletdescriptor()
|
||||
};
|
||||
}
|
||||
|
||||
RPCMethod addhdkey()
|
||||
{
|
||||
return RPCMethod{
|
||||
"addhdkey",
|
||||
"Add a BIP 32 HD key to the wallet that can be used with 'createwalletdescriptor'\n",
|
||||
{
|
||||
{"hdkey", RPCArg::Type::STR, RPCArg::DefaultHint{"Automatically generated new key"}, "The BIP 32 extended private key to add. If none is provided, a randomly generated one will be added."},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
{
|
||||
{RPCResult::Type::STR, "xpub", "The xpub of the HD key that was added to the wallet"}
|
||||
},
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("addhdkey", "xprv") + HelpExampleRpc("addhdkey", "xprv")
|
||||
},
|
||||
[&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
|
||||
if (!wallet) return UniValue::VNULL;
|
||||
|
||||
if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "addhdkey is not available for wallets without private keys");
|
||||
}
|
||||
|
||||
EnsureWalletIsUnlocked(*wallet);
|
||||
|
||||
CExtKey hdkey;
|
||||
if (request.params[0].isNull()) {
|
||||
CKey seed_key = GenerateRandomKey();
|
||||
hdkey.SetSeed(seed_key);
|
||||
} else {
|
||||
hdkey = DecodeExtKey(request.params[0].get_str());
|
||||
if (!hdkey.key.IsValid()) {
|
||||
// Check if the user gave us an xpub and give a more descriptive error if so
|
||||
CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str());
|
||||
if (xpub.pubkey.IsValid()) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extended public key (xpub) provided, but extended private key (xprv) is required");
|
||||
} else {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOCK(wallet->cs_wallet);
|
||||
std::string desc_str = "unused(" + EncodeExtKey(hdkey) + ")";
|
||||
FlatSigningProvider keys;
|
||||
std::string error;
|
||||
std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_str, keys, error, false);
|
||||
CHECK_NONFATAL(!descs.empty());
|
||||
WalletDescriptor w_desc(std::move(descs.at(0)), GetTime(), 0, 0, 0);
|
||||
if (wallet->GetDescriptorScriptPubKeyMan(w_desc) != nullptr) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists");
|
||||
}
|
||||
|
||||
auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false);
|
||||
if (!spkm) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(spkm).original);
|
||||
}
|
||||
|
||||
UniValue response(UniValue::VOBJ);
|
||||
const DescriptorScriptPubKeyMan& desc_spkm = spkm->get();
|
||||
LOCK(desc_spkm.cs_desc_man);
|
||||
std::set<CPubKey> pubkeys;
|
||||
std::set<CExtPubKey> extpubs;
|
||||
desc_spkm.GetWalletDescriptor().descriptor->GetPubKeys(pubkeys, extpubs);
|
||||
CHECK_NONFATAL(pubkeys.size() == 0);
|
||||
CHECK_NONFATAL(extpubs.size() == 1);
|
||||
response.pushKV("xpub", EncodeExtPubKey(*extpubs.begin()));
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// addresses
|
||||
RPCMethod getaddressinfo();
|
||||
RPCMethod getnewaddress();
|
||||
@@ -907,6 +983,7 @@ std::span<const CRPCCommand> GetWalletRPCCommands()
|
||||
{"rawtransactions", &fundrawtransaction},
|
||||
{"wallet", &abandontransaction},
|
||||
{"wallet", &abortrescan},
|
||||
{"wallet", &addhdkey},
|
||||
{"wallet", &backupwallet},
|
||||
{"wallet", &bumpfee},
|
||||
{"wallet", &psbtbumpfee},
|
||||
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
std::optional<int64_t> MaxSatisfactionWeight(bool) const override { return {}; }
|
||||
std::optional<int64_t> MaxSatisfactionElems() const override { return {}; }
|
||||
void GetPubKeys(std::set<CPubKey>& pubkeys, std::set<CExtPubKey>& ext_pubs) const override {}
|
||||
bool HasScripts() const override { return true; }
|
||||
std::vector<std::string> Warnings() const override { return {}; }
|
||||
uint32_t GetMaxKeyExpr() const override { return 0; }
|
||||
size_t GetKeyCount() const override { return 0; }
|
||||
|
||||
@@ -3829,8 +3829,8 @@ util::Result<std::reference_wrapper<DescriptorScriptPubKeyMan>> CWallet::AddWall
|
||||
}
|
||||
|
||||
// Apply the label if necessary
|
||||
// Note: we disable labels for ranged descriptors
|
||||
if (!desc.descriptor->IsRange()) {
|
||||
// Note: we disable labels for descriptors that are ranged or that don't produce output scripts (i.e. unused())
|
||||
if (!desc.descriptor->IsRange() && desc.descriptor->HasScripts()) {
|
||||
auto script_pub_keys = spk_man->GetScriptPubKeys();
|
||||
if (script_pub_keys.empty()) {
|
||||
return util::Error{_("Could not generate scriptPubKeys (cache is empty)")};
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
import shutil
|
||||
|
||||
from test_framework.blocktools import COINBASE_MATURITY
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
wallet_importprivkey,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +27,56 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def test_addhdkey(self):
|
||||
self.log.info("Test addhdkey")
|
||||
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||
self.nodes[0].createwallet("hdkey")
|
||||
wallet = self.nodes[0].get_wallet_rpc("hdkey")
|
||||
|
||||
assert_equal(len(wallet.gethdkeys()), 1)
|
||||
|
||||
wallet.addhdkey()
|
||||
xpub_info = wallet.gethdkeys()
|
||||
assert_equal(len(xpub_info), 2)
|
||||
for x in xpub_info:
|
||||
if len(x["descriptors"]) == 1 and x["descriptors"][0]["desc"].startswith("unused("):
|
||||
break
|
||||
else:
|
||||
assert False, "Did not find HD key with no descriptors"
|
||||
|
||||
imp_xpub_info = def_wallet.gethdkeys(private=True)[0]
|
||||
imp_xpub = imp_xpub_info["xpub"]
|
||||
imp_xprv = imp_xpub_info["xprv"]
|
||||
|
||||
assert_raises_rpc_error(-5, "Extended public key (xpub) provided, but extended private key (xprv) is required", wallet.addhdkey, imp_xpub)
|
||||
add_res = wallet.addhdkey(imp_xprv)
|
||||
expected_unused_desc = descsum_create(f"unused({imp_xpub})")
|
||||
assert_equal(add_res["xpub"], imp_xpub)
|
||||
xpub_info = wallet.gethdkeys()
|
||||
assert_equal(len(xpub_info), 3)
|
||||
for x in xpub_info:
|
||||
if x["xpub"] == imp_xpub:
|
||||
assert_equal(len(x["descriptors"]), 1)
|
||||
assert_equal(x["descriptors"][0]["desc"], expected_unused_desc)
|
||||
break
|
||||
else:
|
||||
assert False, "Added HD key was not found in wallet"
|
||||
|
||||
for d in wallet.listdescriptors()["descriptors"]:
|
||||
if d["desc"] == expected_unused_desc:
|
||||
assert_equal(d["active"], False)
|
||||
break
|
||||
else:
|
||||
assert False, "Added HD key's descriptor was not found in wallet"
|
||||
|
||||
assert_raises_rpc_error(-4, "HD key already exists", wallet.addhdkey, imp_xprv)
|
||||
|
||||
def test_addhdkey_noprivs(self):
|
||||
self.log.info("Test addhdkey is not available for wallets without privkeys")
|
||||
self.nodes[0].createwallet("hdkey_noprivs", disable_private_keys=True)
|
||||
wallet = self.nodes[0].get_wallet_rpc("hdkey_noprivs")
|
||||
assert_raises_rpc_error(-4, "addhdkey is not available for wallets without private keys", wallet.addhdkey)
|
||||
|
||||
def run_test(self):
|
||||
# Make sure we use hd, keep masterkeyid
|
||||
hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
|
||||
@@ -124,6 +176,8 @@ class WalletHDTest(BitcoinTestFramework):
|
||||
|
||||
assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
|
||||
|
||||
self.test_addhdkey()
|
||||
self.test_addhdkey_noprivs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletHDTest(__file__).main()
|
||||
|
||||
@@ -65,6 +65,59 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||
assert_equal(result[0]['error']['code'], error_code)
|
||||
assert_equal(result[0]['error']['message'], error_message)
|
||||
|
||||
def test_import_unused_key(self):
|
||||
self.log.info("Test import of unused(KEY)")
|
||||
self.nodes[0].createwallet(wallet_name="import_unused", blank=True)
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_unused")
|
||||
|
||||
assert_equal(len(wallet.gethdkeys()), 0)
|
||||
|
||||
xprv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
|
||||
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||
self.test_importdesc({"desc":descsum_create(f"unused({xpub})"),
|
||||
"timestamp": "now"},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message='Cannot import descriptor without private keys to a wallet with private keys enabled',
|
||||
wallet=wallet)
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xprv})")},
|
||||
success=True,
|
||||
wallet=wallet)
|
||||
hdkeys = wallet.gethdkeys()
|
||||
assert_equal(len(hdkeys), 1)
|
||||
assert_equal(hdkeys[0]["xpub"], xpub)
|
||||
wallet.unloadwallet()
|
||||
|
||||
def test_import_unused_key_existing(self):
|
||||
self.log.info("Test import of unused(KEY) with existing KEY")
|
||||
self.nodes[0].createwallet(wallet_name="import_existing_unused")
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_existing_unused")
|
||||
|
||||
hdkeys = wallet.gethdkeys(private=True)
|
||||
assert_equal(len(hdkeys), 1)
|
||||
xprv = hdkeys[0]["xprv"]
|
||||
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xprv})")},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message="Cannot import an unused() descriptor when its private key is already in the wallet",
|
||||
wallet=wallet)
|
||||
wallet.unloadwallet()
|
||||
|
||||
def test_import_unused_noprivs(self):
|
||||
self.log.info("Test import of unused(KEY) to wallet without privkeys")
|
||||
self.nodes[0].createwallet(wallet_name="import_unused_noprivs", disable_private_keys=True)
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_unused_noprivs")
|
||||
|
||||
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xpub})")},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message="Cannot import unused() to wallet without private keys enabled",
|
||||
wallet=wallet)
|
||||
wallet.unloadwallet()
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.log.info('Setting up wallets')
|
||||
self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)
|
||||
@@ -822,5 +875,9 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||
)
|
||||
|
||||
|
||||
self.test_import_unused_key()
|
||||
self.test_import_unused_key_existing()
|
||||
self.test_import_unused_noprivs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
ImportDescriptorsTest(__file__).main()
|
||||
|
||||
Reference in New Issue
Block a user