Skip to content

Instantly share code, notes, and snippets.

@blocktraveler
Last active October 4, 2025 00:42
Show Gist options
  • Select an option

  • Save blocktraveler/3e6198c698a272bd8b13b16e0f13d390 to your computer and use it in GitHub Desktop.

Select an option

Save blocktraveler/3e6198c698a272bd8b13b16e0f13d390 to your computer and use it in GitHub Desktop.
Proposal: Add importprivkeys RPC for WIF to descriptor imports in Bitcoin Core

Proposal: Add importprivkeys RPC (helper for WIF → descriptor import)

On-chain Archive

nft/importprivkeys_rpc_proposal.md


Motivation

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.


Proposed RPC: importprivkeys

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: Uses combo(<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.
    • 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"}
  ]
}

Implementation Concept

This RPC would act as a light-weight wrapper over existing functionality like getdescriptorinfo and importdescriptors.

Internally:

  1. Parse the WIF private key(s) and validate them.
  2. For each key, wrap it in the chosen descriptor template.
  3. Call getdescriptorinfo to append checksum.
  4. To optimize for batch imports and avoid redundant rescans:
    • If timestamp is "now", import all descriptors in a single importdescriptors call with timestamp: "now".
    • If timestamp is 0 or 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.
  5. 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.


Benefits

  • 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.

Possible Objections

  • 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.

Appendix: Batch Import Logic (Python Example)

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 importprivkeys as 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 label be 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

@blocktraveler
Copy link
Copy Markdown
Author

See related issue #30175.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment