Skip to content

Commit f193ea8

Browse files
hugohnachow101
andcommitted
add importdescriptors RPC and tests for native descriptor wallets
Co-authored-by: Andrew Chow <[email protected]>
1 parent ce24a94 commit f193ea8

File tree

10 files changed

+905
-0
lines changed

10 files changed

+905
-0
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
131131
{ "importpubkey", 2, "rescan" },
132132
{ "importmulti", 0, "requests" },
133133
{ "importmulti", 1, "options" },
134+
{ "importdescriptors", 0, "requests" },
134135
{ "verifychain", 0, "checklevel" },
135136
{ "verifychain", 1, "nblocks" },
136137
{ "getblockstats", 0, "hash_or_height" },

src/wallet/rpcdump.cpp

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,3 +1458,297 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
14581458

14591459
return response;
14601460
}
1461+
1462+
static UniValue ProcessDescriptorImport(CWallet * const pwallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
1463+
{
1464+
UniValue warnings(UniValue::VARR);
1465+
UniValue result(UniValue::VOBJ);
1466+
1467+
try {
1468+
if (!data.exists("desc")) {
1469+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
1470+
}
1471+
1472+
const std::string& descriptor = data["desc"].get_str();
1473+
const bool active = data.exists("active") ? data["active"].get_bool() : false;
1474+
const bool internal = data.exists("internal") ? data["internal"].get_bool() : false;
1475+
const std::string& label = data.exists("label") ? data["label"].get_str() : "";
1476+
1477+
// Parse descriptor string
1478+
FlatSigningProvider keys;
1479+
std::string error;
1480+
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
1481+
if (!parsed_desc) {
1482+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
1483+
}
1484+
1485+
// Range check
1486+
int64_t range_start = 0, range_end = 1, next_index = 0;
1487+
if (!parsed_desc->IsRange() && data.exists("range")) {
1488+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
1489+
} else if (parsed_desc->IsRange()) {
1490+
if (data.exists("range")) {
1491+
auto range = ParseDescriptorRange(data["range"]);
1492+
range_start = range.first;
1493+
range_end = range.second + 1; // Specified range end is inclusive, but we need range end as exclusive
1494+
} else {
1495+
warnings.push_back("Range not given, using default keypool range");
1496+
range_start = 0;
1497+
range_end = gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE);
1498+
}
1499+
next_index = range_start;
1500+
1501+
if (data.exists("next_index")) {
1502+
next_index = data["next_index"].get_int64();
1503+
// bound checks
1504+
if (next_index < range_start || next_index >= range_end) {
1505+
throw JSONRPCError(RPC_INVALID_PARAMETER, "next_index is out of range");
1506+
}
1507+
}
1508+
}
1509+
1510+
// Active descriptors must be ranged
1511+
if (active && !parsed_desc->IsRange()) {
1512+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Active descriptors must be ranged");
1513+
}
1514+
1515+
// Ranged descriptors should not have a label
1516+
if (data.exists("range") && data.exists("label")) {
1517+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptors should not have a label");
1518+
}
1519+
1520+
// Internal addresses should not have a label either
1521+
if (internal && data.exists("label")) {
1522+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Internal addresses should not have a label");
1523+
}
1524+
1525+
// Combo descriptor check
1526+
if (active && !parsed_desc->IsSingleType()) {
1527+
throw JSONRPCError(RPC_WALLET_ERROR, "Combo descriptors cannot be set to active");
1528+
}
1529+
1530+
// If the wallet disabled private keys, abort if private keys exist
1531+
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
1532+
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled");
1533+
}
1534+
1535+
// Need to ExpandPrivate to check if private keys are available for all pubkeys
1536+
FlatSigningProvider expand_keys;
1537+
std::vector<CScript> scripts;
1538+
parsed_desc->Expand(0, keys, scripts, expand_keys);
1539+
parsed_desc->ExpandPrivate(0, keys, expand_keys);
1540+
1541+
// Check if all private keys are provided
1542+
bool have_all_privkeys = !expand_keys.keys.empty();
1543+
for (const auto& entry : expand_keys.origins) {
1544+
const CKeyID& key_id = entry.first;
1545+
CKey key;
1546+
if (!expand_keys.GetKey(key_id, key)) {
1547+
have_all_privkeys = false;
1548+
break;
1549+
}
1550+
}
1551+
1552+
// If private keys are enabled, check some things.
1553+
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
1554+
if (keys.keys.empty()) {
1555+
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled");
1556+
}
1557+
if (!have_all_privkeys) {
1558+
warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
1559+
}
1560+
}
1561+
1562+
WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
1563+
1564+
// Check if the wallet already contains the descriptor
1565+
auto existing_spk_manager = pwallet->GetDescriptorScriptPubKeyMan(w_desc);
1566+
if (existing_spk_manager) {
1567+
LOCK(existing_spk_manager->cs_desc_man);
1568+
if (range_start > existing_spk_manager->GetWalletDescriptor().range_start) {
1569+
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));
1570+
}
1571+
}
1572+
1573+
// Add descriptor to the wallet
1574+
auto spk_manager = pwallet->AddWalletDescriptor(w_desc, keys, label);
1575+
if (spk_manager == nullptr) {
1576+
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s'", descriptor));
1577+
}
1578+
1579+
// Set descriptor as active if necessary
1580+
if (active) {
1581+
if (!w_desc.descriptor->GetOutputType()) {
1582+
warnings.push_back("Unknown output type, cannot set descriptor to active.");
1583+
} else {
1584+
pwallet->SetActiveScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), internal);
1585+
}
1586+
}
1587+
1588+
result.pushKV("success", UniValue(true));
1589+
} catch (const UniValue& e) {
1590+
result.pushKV("success", UniValue(false));
1591+
result.pushKV("error", e);
1592+
} catch (...) {
1593+
result.pushKV("success", UniValue(false));
1594+
1595+
result.pushKV("error", JSONRPCError(RPC_MISC_ERROR, "Missing required fields"));
1596+
}
1597+
if (warnings.size()) result.pushKV("warnings", warnings);
1598+
return result;
1599+
}
1600+
1601+
UniValue importdescriptors(const JSONRPCRequest& main_request) {
1602+
// Acquire the wallet
1603+
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(main_request);
1604+
CWallet* const pwallet = wallet.get();
1605+
if (!EnsureWalletIsAvailable(pwallet, main_request.fHelp)) {
1606+
return NullUniValue;
1607+
}
1608+
1609+
RPCHelpMan{"importdescriptors",
1610+
"\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"
1611+
"\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n"
1612+
"may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n",
1613+
{
1614+
{"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported",
1615+
{
1616+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
1617+
{
1618+
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "Descriptor to import."},
1619+
{"active", RPCArg::Type::BOOL, /* default */ "false", "Set this descriptor to be the active descriptor for the corresponding output type/externality"},
1620+
{"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"},
1621+
{"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"},
1622+
{"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, "Time from which to start rescanning the blockchain for this descriptor, in " + UNIX_EPOCH_TIME + "\n"
1623+
" Use the string \"now\" to substitute the current synced blockchain time.\n"
1624+
" \"now\" can be specified to bypass scanning, for outputs which are known to never have been used, and\n"
1625+
" 0 can be specified to scan the entire blockchain. Blocks up to 2 hours before the earliest timestamp\n"
1626+
" of all descriptors being imported will be scanned.",
1627+
/* oneline_description */ "", {"timestamp | \"now\"", "integer / string"}
1628+
},
1629+
{"internal", RPCArg::Type::BOOL, /* default */ "false", "Whether matching outputs should be treated as not incoming payments (e.g. change)"},
1630+
{"label", RPCArg::Type::STR, /* default */ "''", "Label to assign to the address, only allowed with internal=false"},
1631+
},
1632+
},
1633+
},
1634+
"\"requests\""},
1635+
},
1636+
RPCResult{
1637+
RPCResult::Type::ARR, "", "Response is an array with the same size as the input that has the execution result",
1638+
{
1639+
{RPCResult::Type::OBJ, "", "",
1640+
{
1641+
{RPCResult::Type::BOOL, "success", ""},
1642+
{RPCResult::Type::ARR, "warnings", /* optional */ true, "",
1643+
{
1644+
{RPCResult::Type::STR, "", ""},
1645+
}},
1646+
{RPCResult::Type::OBJ, "error", /* optional */ true, "",
1647+
{
1648+
{RPCResult::Type::ELISION, "", "JSONRPC error"},
1649+
}},
1650+
}},
1651+
}
1652+
},
1653+
RPCExamples{
1654+
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"internal\": true }, "
1655+
"{ \"desc\": \"<my desccriptor 2>\", \"label\": \"example 2\", \"timestamp\": 1455191480 }]'") +
1656+
HelpExampleCli("importdescriptors", "'[{ \"desc\": \"<my descriptor>\", \"timestamp\":1455191478, \"active\": true, \"range\": [0,100], \"label\": \"<my bech32 wallet>\" }]'")
1657+
},
1658+
}.Check(main_request);
1659+
1660+
// Make sure wallet is a descriptor wallet
1661+
if (!pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
1662+
throw JSONRPCError(RPC_WALLET_ERROR, "importdescriptors is not available for non-descriptor wallets");
1663+
}
1664+
1665+
RPCTypeCheck(main_request.params, {UniValue::VARR, UniValue::VOBJ});
1666+
1667+
WalletRescanReserver reserver(*pwallet);
1668+
if (!reserver.reserve()) {
1669+
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
1670+
}
1671+
1672+
const UniValue& requests = main_request.params[0];
1673+
const int64_t minimum_timestamp = 1;
1674+
int64_t now = 0;
1675+
int64_t lowest_timestamp = 0;
1676+
bool rescan = false;
1677+
UniValue response(UniValue::VARR);
1678+
{
1679+
auto locked_chain = pwallet->chain().lock();
1680+
LOCK(pwallet->cs_wallet);
1681+
EnsureWalletIsUnlocked(pwallet);
1682+
1683+
CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now)));
1684+
1685+
// Get all timestamps and extract the lowest timestamp
1686+
for (const UniValue& request : requests.getValues()) {
1687+
// This throws an error if "timestamp" doesn't exist
1688+
const int64_t timestamp = std::max(GetImportTimestamp(request, now), minimum_timestamp);
1689+
const UniValue result = ProcessDescriptorImport(pwallet, request, timestamp);
1690+
response.push_back(result);
1691+
1692+
if (lowest_timestamp > timestamp ) {
1693+
lowest_timestamp = timestamp;
1694+
}
1695+
1696+
// If we know the chain tip, and at least one request was successful then allow rescan
1697+
if (!rescan && result["success"].get_bool()) {
1698+
rescan = true;
1699+
}
1700+
}
1701+
pwallet->ConnectScriptPubKeyManNotifiers();
1702+
}
1703+
1704+
// Rescan the blockchain using the lowest timestamp
1705+
if (rescan) {
1706+
int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */);
1707+
{
1708+
auto locked_chain = pwallet->chain().lock();
1709+
LOCK(pwallet->cs_wallet);
1710+
pwallet->ReacceptWalletTransactions();
1711+
}
1712+
1713+
if (pwallet->IsAbortingRescan()) {
1714+
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
1715+
}
1716+
1717+
if (scanned_time > lowest_timestamp) {
1718+
std::vector<UniValue> results = response.getValues();
1719+
response.clear();
1720+
response.setArray();
1721+
1722+
// Compose the response
1723+
for (unsigned int i = 0; i < requests.size(); ++i) {
1724+
const UniValue& request = requests.getValues().at(i);
1725+
1726+
// If the descriptor timestamp is within the successfully scanned
1727+
// range, or if the import result already has an error set, let
1728+
// the result stand unmodified. Otherwise replace the result
1729+
// with an error message.
1730+
if (scanned_time <= GetImportTimestamp(request, now) || results.at(i).exists("error")) {
1731+
response.push_back(results.at(i));
1732+
} else {
1733+
UniValue result = UniValue(UniValue::VOBJ);
1734+
result.pushKV("success", UniValue(false));
1735+
result.pushKV(
1736+
"error",
1737+
JSONRPCError(
1738+
RPC_MISC_ERROR,
1739+
strprintf("Rescan failed for descriptor with timestamp %d. There was an error reading a "
1740+
"block from time %d, which is after or within %d seconds of key creation, and "
1741+
"could contain transactions pertaining to the desc. As a result, transactions "
1742+
"and coins using this desc may not appear in the wallet. This error could be "
1743+
"caused by pruning or data corruption (see bitcoind log for details) and could "
1744+
"be dealt with by downloading and rescanning the relevant blocks (see -reindex "
1745+
"and -rescan options).",
1746+
GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW)));
1747+
response.push_back(std::move(result));
1748+
}
1749+
}
1750+
}
1751+
}
1752+
1753+
return response;
1754+
}

src/wallet/rpcwallet.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4289,6 +4289,7 @@ UniValue importwallet(const JSONRPCRequest& request);
42894289
UniValue importprunedfunds(const JSONRPCRequest& request);
42904290
UniValue removeprunedfunds(const JSONRPCRequest& request);
42914291
UniValue importmulti(const JSONRPCRequest& request);
4292+
UniValue importdescriptors(const JSONRPCRequest& request);
42924293

42934294
void RegisterWalletRPCCommands(interfaces::Chain& chain, std::vector<std::unique_ptr<interfaces::Handler>>& handlers)
42944295
{
@@ -4318,6 +4319,7 @@ static const CRPCCommand commands[] =
43184319
{ "wallet", "getbalances", &getbalances, {} },
43194320
{ "wallet", "getwalletinfo", &getwalletinfo, {} },
43204321
{ "wallet", "importaddress", &importaddress, {"address","label","rescan","p2sh"} },
4322+
{ "wallet", "importdescriptors", &importdescriptors, {"requests"} },
43214323
{ "wallet", "importmulti", &importmulti, {"requests","options"} },
43224324
{ "wallet", "importprivkey", &importprivkey, {"privkey","label","rescan"} },
43234325
{ "wallet", "importprunedfunds", &importprunedfunds, {"rawtransaction","txoutproof"} },

src/wallet/scriptpubkeyman.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,15 @@ void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript& script)
17451745
}
17461746
}
17471747

1748+
void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey &pubkey)
1749+
{
1750+
LOCK(cs_desc_man);
1751+
WalletBatch batch(m_storage.GetDatabase());
1752+
if (!AddDescriptorKeyWithDB(batch, key, pubkey)) {
1753+
throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed");
1754+
}
1755+
}
1756+
17481757
bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey)
17491758
{
17501759
AssertLockHeld(cs_desc_man);
@@ -2121,3 +2130,35 @@ bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKe
21212130
m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
21222131
return true;
21232132
}
2133+
2134+
bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const
2135+
{
2136+
LOCK(cs_desc_man);
2137+
return m_wallet_descriptor.descriptor != nullptr && desc.descriptor != nullptr && m_wallet_descriptor.descriptor->ToString() == desc.descriptor->ToString();
2138+
}
2139+
2140+
void DescriptorScriptPubKeyMan::WriteDescriptor()
2141+
{
2142+
LOCK(cs_desc_man);
2143+
WalletBatch batch(m_storage.GetDatabase());
2144+
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
2145+
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
2146+
}
2147+
}
2148+
2149+
const WalletDescriptor DescriptorScriptPubKeyMan::GetWalletDescriptor() const
2150+
{
2151+
return m_wallet_descriptor;
2152+
}
2153+
2154+
const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
2155+
{
2156+
LOCK(cs_desc_man);
2157+
std::vector<CScript> script_pub_keys;
2158+
script_pub_keys.reserve(m_map_script_pub_keys.size());
2159+
2160+
for (auto const& script_pub_key: m_map_script_pub_keys) {
2161+
script_pub_keys.push_back(script_pub_key.first);
2162+
}
2163+
return script_pub_keys;
2164+
}

src/wallet/scriptpubkeyman.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,13 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
580580

581581
bool AddKey(const CKeyID& key_id, const CKey& key);
582582
bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key);
583+
584+
bool HasWalletDescriptor(const WalletDescriptor& desc) const;
585+
void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
586+
void WriteDescriptor();
587+
588+
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
589+
const std::vector<CScript> GetScriptPubKeys() const;
583590
};
584591

585592
#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H

0 commit comments

Comments
 (0)