Wallets & ScriptPubKeys
ScriptPubKeys, script types, and the three ways to populate a pubkeys_set
blockd tracks Bitcoin ownership at the level of ScriptPubKeys — the locking scripts that control outputs. “Wallet” is a convenient shorthand for a collection of those scripts along with their transaction history, and blockd uses it throughout, but the model underneath is always ScriptPubKeys.
blockd tracks two things for any set of ScriptPubKeys: the complete transaction history — every transaction that ever created or spent an output locked to those scripts — and the current UTXO set derived from that history.
Script types
Section titled “Script types”blockd derives scripthashes for all standard Bitcoin output types that can receive and spend funds:
| Script type | Discrete prefix | Address format | Era |
|---|---|---|---|
| P2PK | p2pk | none (raw pubkey) | Pre-address Bitcoin (early chain) |
| P2PKH | p2pkh | 1... (Base58Check) | Legacy — original address format |
| P2SH-P2WPKH | p2sh-p2wpkh | 3... (Base58Check) | Wrapped SegWit — transitional single-key |
| P2SH | — | 3... (Base58Check) | Generic script hash; wraps P2WPKH, P2WSH, P2MS, and arbitrary redeem scripts |
| P2SH-P2WSH | — | 3... (Base58Check) | P2SH envelope around P2WSH; used by legacy wallets funding SegWit scripts |
| P2SH-P2MS | — | 3... (Base58Check) | Dominant pre-Taproot multisig pattern |
| P2WPKH | p2wpkh | bc1q... (Bech32, 20-byte program) | Native SegWit — single key |
| P2WSH | — | bc1q... (Bech32, 32-byte program) | Native SegWit — script hash; underlies Lightning and collaborative multisig |
| P2MS | — | none (bare multisig) | Pre-P2SH era multisig |
| P2TR | p2tr | bc1p... (Bech32m) | Taproot — key-path and script-path spends |
A few notes on the table:
P2PK outputs predate Bitcoin’s address conventions entirely. They lock directly to a
public key rather than a hash of one. There is no address to derive — the public key
itself is the identifier. blockd constructs the P2PK script (<pubkey> OP_CHECKSIG) and computes
the scripthash from that script, so P2PK outputs from the early chain are fully trackable
alongside modern script types.
P2MS (bare multisig) similarly has no address representation. Providing it requires
the full descriptor via pubkeys.descriptor.import.
P2WPKH and P2WSH both produce bc1q... addresses, but are distinct script types:
P2WPKH encodes a 20-byte key hash while P2WSH encodes a 32-byte script hash.
The five types with a discrete prefix (p2pk, p2pkh, p2wpkh, p2sh-p2wpkh, p2tr)
can be imported via pubkeys.pubkey.import. All other types require
pubkeys.descriptor.import.
Three ways to populate a pubkeys_set
Section titled “Three ways to populate a pubkeys_set”All script types converge on the same internal representation — a scripthash. How you provide the source material to blockd determines which workflow step you use.
Extended public key derivation
Section titled “Extended public key derivation”For single-key HD wallets, provide an xpub/ypub/zpub and blockd derives the full address
set according to a scanspec. blockd interprets the key encoding as a script type hint —
zpub as P2WPKH, ypub as P2SH-P2WPKH, and xpub as P2PKH. This follows the SLIP-0132
convention. blockd derives both external (receive) and internal (change) chains.
P2TR HD wallets have no SLIP-0132 encoding, so they cannot be imported via this step.
Use pubkeys.descriptor.import with a tr() descriptor instead.
Three scanspec forms are supported:
Gap limit — derive addresses starting at index from until gap_limit consecutive
unused indexes are found. The standard approach for HD wallet imports:
{"from": 0, "gap_limit": 20}Count — derive count addresses starting at index from, regardless of usage:
{"from": 0, "count": 4000}Range — derive addresses from index from through index to inclusive. to must be
greater than or equal to from:
{"from": 500, "to": 1000}Count and range are useful when gap limit scanning is impractical — large wallets with known index ranges, archival imports, or cases where you need a deterministic address count regardless of chain activity.
Full step example using gap limit:
{ "step": "pubkeys.extpubkey.import", "args": { "extpubkey": "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", "scanspec": {"from": 0, "gap_limit": 20} }}Discrete import
Section titled “Discrete import”For non-HD contexts — individual addresses, watch-only outputs, or P2PK outputs from early blocks — provide source material directly. Two formats are accepted.
Plain addresses:
{ "step": "pubkeys.pubkey.import", "args": { "addresses": [ "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", "bc1qp7lsyxsf0p9049dgea2t0cynx6d3ama0xe3pdv" ] }}Raw public keys with explicit script type prefix:
{ "step": "pubkeys.pubkey.import", "args": { "pubkeys": [ "p2pk:04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", "p2pkh:02d8e638a311b13f6a097b33fead3fe9b02b1e04c9a28a87e80b3598fc7a2c4d90", "p2wpkh:02d8e638a311b13f6a097b33fead3fe9b02b1e04c9a28a87e80b3598fc7a2c4d90", "p2sh-p2wpkh:02d8e638a311b13f6a097b33fead3fe9b02b1e04c9a28a87e80b3598fc7a2c4d90", "p2tr:02d8e638a311b13f6a097b33fead3fe9b02b1e04c9a28a87e80b3598fc7a2c4d90" ] }}p2sh-p2wpkh and p2shwpkh are both accepted as the prefix for wrapped SegWit keys.
The pubkeys format is necessary when tracking a key across multiple script types, or
when handling P2PK outputs that have no address representation. addresses and pubkeys
can be combined in a single step.
This step covers the five single-key types that support a discrete prefix. For multisig
arrangements (P2MS, P2SH-P2MS, P2WSH, P2SH-P2WSH), P2TR HD wallets, and generic P2SH,
use pubkeys.descriptor.import.
Descriptor import
Section titled “Descriptor import”For wallets that export output descriptors, provide the descriptor string directly.
{ "step": "pubkeys.descriptor.import", "args": { "descriptor": "wpkh([fingerprint/84h/0h/0h]xpub6CatWdiZyn.../<0;1>/*)", "scanspec": {"from": 0, "gap_limit": 20} }}Descriptors fall into two categories that affect how scanspec is used:
Ranged descriptors contain a wildcard (*) and describe an unbounded sequence of
scripts — for example wpkh(xpub.../<0;1>/*) or tr(xpub.../*). A scanspec is
required to bound the derivation.
Fixed descriptors contain only concrete keys and describe a specific, finite set of
scripts — for example wsh(multi(2,key1,key2,key3)). No scanspec is needed or expected.
A multipath descriptor (/<0;1>/) covers both the receive and change chains in a single
step. A single-path descriptor covers only the chain encoded in its derivation path.
Some examples of what pubkeys.descriptor.import handles that the other two steps do not:
{ "descriptor": "tr([fingerprint/86h/0h/0h]xpub6.../<0;1>/*)" }{ "descriptor": "wsh(multi(2,xpub1.../0/*,xpub2.../0/*,xpub3.../0/*))" }{ "descriptor": "sh(wsh(multi(2,key1,key2,key3)))" }{ "descriptor": "tr(key,{pk(key2),pk(key3)})" }For the full descriptor language — syntax, key expressions, derivation paths, script functions, and checksums — see the Bitcoin Core descriptor documentation. blockd accepts any descriptor that conforms to that specification. The Bitcoin Optech topic page is a useful entry point covering the background and the relevant BIPs (380–386, 389).
What this means in practice
Section titled “What this means in practice”Most developers working with modern single-key HD wallets will use
pubkeys.extpubkey.import and rarely think about script types directly — the xpub, ypub,
or zpub encodes the intent.
pubkeys.pubkey.import exists for the cases where HD derivation doesn’t apply: archived
early-chain keys, individual watch addresses, or keys that were used across multiple script
types over their lifetime.
pubkeys.descriptor.import handles everything more complex: Taproot HD wallets, all
multisig arrangements, and any wallet that exports a descriptor string. When in doubt about
which step to use, if your wallet software can export a descriptor, use it.
From blockd’s perspective, all three paths produce the same thing: a set of scripthashes to query against the indexer, subscribe to for new activity, and use to identify inputs and outputs in the transaction history. The pubkeys_set is the named container for that set. The next page covers how pubkeys_sets are named, managed, and secured.
Derived and discrete addresses cannot be mixed in the same pubkeys_set. A pubkeys_set is either xpub-derived, discretely composed, or descriptor-derived. If you need to combine them, use separate pubkeys_sets and query them independently.