nft/importprivkeys_rpc_proposal.md
With descriptor wallets now the default in Bitcoin Core, users can no longer use importprivkey to import raw WIF keys. Instead, they must manually construct descriptors and call importdescriptors. While this is technically correct and explicit, it poses challenges for users who:
- are migrating from legacy wallets or paper wallets,
- have backup WIF private keys,
- simply want a quick way to get funds into Core.
Over the years, users frequently asked on various platforms how to import private keys, both legacy and Bech32/SegWit, into a Bitcoin Core descriptor wallet. Currently, they rely on external scripts like the Core-Wallet-Migration-Tools to handle this. A built-in RPC in Core could streamline this common migration task without compromising the descriptor model.
This proposal suggests a minimal RPC helper inside Core that supports importing single or multiple WIF keys efficiently, including the optimized rescan strategy of the existing importdescriptors call to minimize overhead during batch imports.
The call importprivkeys (plural) distinguishes the import routine from the legacy importprivkey call (singular), which only allows the import of one private key at a time.
Usage
For a single legacy key:
bitcoin-cli importprivkeys "5K...WIF..." "pkh" 1356998400
For a single SegWit key:
bitcoin-cli importprivkeys "cQ1...WIFPRIVATEKEY..." "wpkh" 1356998400
For multiple keys:
bitcoin-cli importprivkeys '["cQ1...WIF1...", "cQ2...WIF2..."]' "wpkh" 1356998400
Parameters
-
privkeys(string or array of strings, required): One or more private keys in WIF format.Accepts a single WIF string or a JSON array for batch imports.
-
template(string, optional, default=auto): Descriptor type. Supported values:auto: Usescombo(<key>)to generate all applicable descriptor types for the private key:- For compressed keys:
pkh(<key>),sh(wpkh(<key>)),wpkh(<key>). - For uncompressed keys:
pkh(<key>). - Taproot (
tr(<key>)) shall be excluded to avoid unnecessary complexity for typical WIF migrations.
- For compressed keys:
pkh: Legacy P2PKH (1xxx addresses).wpkh: Native SegWit P2WPKH (bc1q addresses).sh-wpkh: Nested SegWit P2SH-P2WPKH (3xxx addresses).
-
timestamp(int or string, optional, default=0): Blockchain rescan starting point:0: Full blockchain rescan."now": No rescan (most recent transactions only).<unix_timestamp>: Rescan from a specific UNIX timestamp (e.g., 1356998400 for Jan 1, 2013).
Invalid timestamps (e.g., negative or future values) return an error.
Return
For a single key:
{
"success": true,
"imported_count": 1,
"descriptor": "wpkh(cQ1...WIF)#abcd1234",
"address": "bc1qxyz..."
}For multiple keys:
{
"success": true,
"imported_count": 2,
"descriptors": [
"wpkh(cQ1...WIF1)#abcd1234",
"wpkh(cQ2...WIF2)#efgh5678"
],
"addresses": [
"bc1qabc...",
"bc1qdef..."
]
}If any imports fail (e.g., invalid WIF), the return includes an "errors" array with details (e.g., invalid WIF format, unsupported template, checksum failure). The array should clearly indicate which key failed (by index and/or truncated string). In such cases, "success" will be false:
{
"success": false,
"imported_count": 1,
"descriptors": ["wpkh(cQ1...WIF1)#abcd1234"],
"addresses": ["bc1qabc..."],
"errors": [
{"index": 1, "key": "cQ2...WIF2...", "error": "Invalid WIF format"}
]
}This RPC would act as a light-weight wrapper over existing functionality like getdescriptorinfo and importdescriptors.
Internally:
- Parse the WIF private key(s) and validate them.
- For each key, wrap it in the chosen descriptor template.
- Call
getdescriptorinfoto append checksum. - To optimize for batch imports and avoid redundant rescans:
- If timestamp is
"now", import all descriptors in a singleimportdescriptorscall withtimestamp: "now". - If timestamp is
0or a past UNIX time:- Import all but the last descriptor with
timestamp: "now"(no rescan triggered). - Import the final descriptor with the user-specified timestamp, triggering one wallet-wide rescan that covers all newly added descriptors.
- Import all but the last descriptor with
- If timestamp is
- Compute and return the generated address(es) in the same order as the input keys.
This approach ensures efficiency for large batches, as the rescan happens only once at the end using the existing logic of the importdescriptors call. The RPC would set "active": true for all imports. This won't introduce a new wallet logic, just a light-weight wrapper over existing RPCs.
- Provides an intuitive, built-in command for importing single or multiple WIF keys.
- Supports efficient batch imports with a single rescan, ideal for migrating large sets of keys.
- Maintains consistency with the descriptor model and outputs.
- Reduces dependency on third-party scripts for a frequent user need.
- Makes transitioning to descriptor wallets more accessible and less error-prone.
- Users can already achieve the same by manually constructing descriptors or using external scripts. While true, WIF imports remain a common issue.
- However, given how widespread WIF key usage remains, an onboard helper beside the existing migration routine for entire wallets seems reasonable and could be implemented in a small PR.
def importprivkeys(keys, template="wpkh", timestamp=0):
descs = []
for i, key in enumerate(keys):
if template == "auto":
desc = f"combo({key})"
else:
desc = f"{template}({key})"
checksum = getdescriptorinfo(desc)["checksum"]
full_desc = f"{desc}#{checksum}"
ts = "now" if i < len(keys) - 1 else timestamp
descs.append({"desc": full_desc, "timestamp": ts, "active": True})
return importdescriptors(descs)Question for maintainers:
- Would you consider accepting a PR to add
importprivkeysas described, primarily as a migration aid for users with WIF keys? If so, are there preferences for the name, parameters, or implementation details (e.g., handling very large batches)? - Should e.g. a parameter
labelbe added for imported addresses to support additional metadata? - What would be the preferred batch size limits and should there be a cap on the number of keys to prevent resource exhaustion?
External tool for context: Core-Wallet-Migration-Tools
See related issue #30175.