Skip to content

Keys

Keys are the most fundamental primitive in Minsc, as well as in Bitcoin in general. Most of everything else uses keys in one way or another.

Minsc has two runtime types used to represent keys: PubKey for public keys and SecKey for secret (private) keys.

Both public and secret keys come in two variants: SingleKey and BIP32 extended Xpub/Xpriv keys.

Resources:

Key Construction

Xpub, Xpriv and single secret key WIF strings can be used as-is as a Minsc expression. They're detected in the parse stage and evaluated straight into a PubKey/SecKey. For example:

// Xpub
$descriptor = wpkh(xpub661MyMwAqRbcFvXwqbgwigDczeocqeBEibKMCkoc31RiyB464Ybc1z8sWMnR38JdeCBJPPkSM7mKahcBX2nPX9KYVTz3cotpLmSkMxrp99L);
// Xpriv
$psbt_signed = psbt::sign($psbt, xprv9s21ZrQH143K37CBUxq5bSXTyHwKc5KwYWsm6onCYDmiPBBDyiz3bYwvHYH9NXzsY6mDiXmhf77Ym2EJkGreLHB3s6MH5tRkfKQT9uDQ4r2);
// WIF
$signature = ecdsa::sign(L4oC7AMPJkKPwuVipfFaKcJKBhsR3kmXy89f7oAFhSQSPWLNXocJ, hash::sha256("Hello World"));

Single PubKeys can be constructed from bytes using pubkey(Bytes). For example:

pubkey(0x03b2123025f45648c3f31fd4b7d3e1ec3344769ab3f53dec5af9b8a9a95385cbd5)

Explicit pubkey() is needed in order to construct a PubKey<SingleKey> rather than Bytes.

However, for compatibility with the native Descriptor/Policy syntax, Bytes are automatically coerced into a PubKey when used as one. Also for compatibility, the 0x prefix is optional.

Together, this makes the following two equivalent:

wpkh(pubkey(0x03b2123025f45648c3f31fd4b7d3e1ec3344769ab3f53dec5af9b8a9a95385cbd5))
wpkh(03b2123025f45648c3f31fd4b7d3e1ec3344769ab3f53dec5af9b8a9a95385cbd5)

Single SecKeys can be constructed from bytes using seckey(Bytes), as an alternative to WIF.

Apart from the key material itself, keys can also optionally include an origin, derivation path and the wildcard modifier. See the full Key Structure below.

SecKeys can be converted into a PubKey using pubkey(SecKey). They will also coerce automatically when used where a PubKey is expected, e.g. for wpkh(SecKey) calls.

Key Generation

Use seckey::rand() to generate a random single key, or xpriv::rand() to generate a random Xpriv.

>>> xpriv::rand()

Key Structure

The PubKey/SecKey structures consist of four components:

  1. Key Origin — master fingerprint and derivation path leading to the literal key
  2. The literal key material (single or extended)
  3. Derivation Stepsfrom the literal key (extended keys only)
  4. Wildcard Modifier (/*) — off/on/hardened (extended keys only)

In the example key below, [1522f2ac/1'/9] is the origin leading to the literal xpub6AdiCgLUU… key. The literal key is then derived with the m/2/100 path, and the /* wildcard modifier is enabled.

[1522f2ac/1'/9]xpub6AdiCgLUUAawwmyzpiLvG8AStHk6Q41BDGNJEa6DgyrskDDfVeEP8MXzJGCdWkm9UPc9Mgi4GHaY5NtHoaafsN3cXa8o6egDVWQMDjT7TnH/2/100/*

Key Origin

Both single keys and Xpubs/Xprivs can have a BIP32 key origin associated with them, consisting of the master key's fingerprint and the derivation path leading from it to the literal key.

The key origin metadata is made available to signing devices (through the PSBT bip32_derivation and tap_key_origins fields) and enables them to derive child keys for signing and to verify change outputs.

It appears to the left of the literal key, enclosed in [] brackets. For example:

[01aa02bb/1'/0]xprv9s21ZrQH143K37CBUxq5bSX…zhf77Ym2EJkGreLHB3s6MH5tRkfKQT9uDQ4r2
[99ee88dd/2'/1]03b2123025f45648c3f31fd4b7d3e1ec3344769ab3f53dec5af9b8a9a95385cbd5 // single public key
[ab01cd34/1001]L4oC7AMPJkKPwuVipfFaKcJKBhsR3kmXy89f7oAFhSQSPWLNXocJ // single secret key (WIF)

Single public keys with an origin do not require an explicit pubkey() — they are detected during parsing and evaluate into a PubKey<SingleKey>.

Key Derivation

Xpubs and Xprivs can also have additional derivation path steps applied from the literal key, using the / operator to the right of it. For example:

xpubD6NzVbkrYhZ4YJ35iWLKa78…Gjka8jRFk7qh65hzW549vaTZg7ozhgAcEV1r/0/100

To derive hardened child keys from an Xpriv, use the ' or h modifier:

xprv9s21ZrQH143K2XoYcWBjRLx…NJTK2fz4JkdXkZa8MFrwdVKfXBtNXAQp/86'/0'/0'/0/5
xprv9s21ZrQH143K2XoYcWBjRLx…NJTK2fz4JkdXkZa8MFrwdVKfXBtNXAQp/86h/0h/0h/0/5

Note that Xprivs are coerced into an Xpub when used to construct a Descriptor, so any hardened derivations must be performed before the Xpriv is wrapped in a descriptor.

The / operator does not immediately compute the child key. Instead, it simply extends the derivation path associated with the key instance, making it a cheap operation. The actual derivation is only applied when the tweaked child key is actually needed — for example to construct a scriptPubKey, compute the key fingerprint, or sign with it — or explicitly through derived()/xderived().

🔍 This can be seen by inspecting the inner debug representation. In the following example, $a, $b and $c all internally hold the same public_key/chain_code material, the only difference being the extended derivation_path.

When [x]derived($c) is called, the m/1/100 derivation path is applied, moved to the key origin, and the internal public_key/chain_code material is updated to the child key's.

$a = [01aa02bb/5]xpub661MyMwAqRbcFvXwqbgwigDczeocqeBEibKMCkoc31RiyB464Ybc1z8sWMnR38JdeCBJPPkSM7mKahcBX2nPX9KYVTz3cotpLmSkMxrp99L;
$b = $a/1;         // [01aa02bb/5]xpub661MyM…xrp99L/1
$c = $b/100;       // [01aa02bb/5]xpub661MyM…xrp99L/1/100
$d = derived($c);  // [01aa02bb/5/1/100]035466fe2f…2b21dd
$x = xderived($c); // [01aa02bb/5/1/100]xpub6A3gge…bfugB9
// Note `/1/100` was moved from the right-hand side derivation path in $c,
// to the bracketed left-hand side key origin in $d/$x

env::debug()
Note that accessing public PubKey/SecKey fields like chain_code will apply the necessary derivations to return the correct value for the child key, not the internal chain_code seen in the debug representation.

Wildcard Modifier

Using /* as the final derivation step enables the wildcard modifier, signifying that the key is expected to be derived one more time before being used as a definite key on-chain.

Wildcard keys can be used to construct a wildcard Descriptor/Policy, which are themselves derivable using / and also considered indefinite — meaning they can't be represented as a definite scriptPubKey — until they are derived (see Definite vs Indefinite).

Deriving a wildcard Descriptor/Policy will derive each of the contained keys using the same child index. For example, wsh($alice/0/* && $bob/1/*)/5 == wsh($alice/0/5 && $bob/1/5).

Arrays of wildcard keys, descriptors or policies can similarly be derived. For example, [ $alice/0/*, $bob/1/* ]/5 == [ $alice/0/5, $bob/1/5 ].

If only some of the contained keys have wildcards then only they will be derived, leaving the non-wildcard ones unchanged. However if all keys are non-wildcard, an error is raised.

Hardened wildcard on Xprivs is supported via /*' or /*h.

Multi-Path Keys

Minsc supports BIP-389 multi-path keys, allowing a single PubKey/SecKey instance to represent multiple derivation paths. A multi-path key can then be used to construct a multi-path Descriptor/Policy.

The typical use-case for multi-path is representing both the wallet's external (receive) and internal (change) chains using a single descriptor.

For example, wpkh($alice/84'/0'/0'/<0;1>/*) is a multi-path descriptor representing both wpkh($alice/84'/0'/0'/0/*) and wpkh($alice/84'/0'/0'/1/*) (BIP 84's derivation paths).

The <M;N;K> syntax specified in BIP-389 is supported in Minsc, internally handled as an alternative array construction syntax. You may also use standard arrays instead, e.g. $alice/[0,1] is equivalent to $alice/<0;1>.

Multi-path keys/descriptors/policies can be transformed into an array of single-path instances using singles(). For example, singles($alice/<0;1>) would return an array of [ $alice/0, $alice/1 ].

However this is typically not necessary, since multi-path instances are already Array-like — can be used with . array access, unpacked using array destructuring assignment, have a len(), and work with array utilities like map(). For example:

$wallet = wpkh($alice_sk/84'/0'/0'/<0;1>/*);
$receive = $wallet.0, $change = $wallet.1;

$recv_address = address($receive/15); // m/84'/0'/0'/0/15

// With array destructuring
[$receive_, $change_] = wpkh($alice_sk/84'/0'/0'/<0;1>/*);

// With both the multi-path and wildcard resolved together
$recv_address_ = address($wallet.0/15); // m/84'/0'/0'/0/15

Multi-path keys, descriptors and policies are considered indefinite — meaning they can't be represented as a definite key or scriptPubKey — until a single-path is extracted from them. If both multi-path and wildcard are used, both must be resolved before becoming definite. Also see Definite vs Indefinite.

Key Range

Multi-path can also be used to represent a specific range of keys/descriptors, with a range() of child derivation numbers used as the final derivation step.

For example, this will generate the first 15 addresses in the wpkh($alice/1/014) range:

$descriptor = wpkh($alice/1/range(0, 14));
$addresses = map($descriptor, address);

This also works with multiple chains derived with a range. This will generate the first 15 receive addresses plus the first 15 change addresses:

map(wpkh($alice/<0;1>/range(0, 14)), address) // 30 total addresses

Definite vs Indefinite

When a PubKey/SecKey instance corresponds to one specific ECDSA key, it is considered definite. SingleKeys are always definite. Xpubs and Xprivs are only definite if they're non-multi-path and non-wildcard.

When an indefinite key is used to construct a Descriptor/Policy, they are considered indefinite too.

Indefinite keys/descriptors/policies cannot be used to generate scriptPubKeys or addresses, to sign, or to do anything useful really — apart from being derived into a definite instance that can.

Consider the following example:

// Indefinite Descriptor - wildcard key must be derived once more
$desc_i = wpkh($alice/1/*);
$addr1i = address($desc_i);   // ERROR: "Expected a definite descriptor with no underived wildcards"
$addr2i = address($desc_i/5); // Works 👍

// Definite Descriptor - represents a final definite key, cannot be derived
$desc_d = wpkh($alice/1);
$addr1d = address($desc_d);   // Works 👍
$addr2d = address($desc_d/5); // ERROR: "No inner wildcard xpubs to derive"

Multi-path must similarly be resolved into a definite single-path. See Multi-Path Keys for an example.

The PubKey->is_definite, SecKey->is_definite, Descriptor->is_definite and Policy->is_definite fields can be used to determine whether an instance is definite or not.

X-Only Keys

Taproot uses X-only public keys, encoded with only the X coordinate.

When using tr() descriptors with Miniscript, X-only encoding is handled automatically for you.

⚠ However, when writing raw Script for use with Taproot, care must be taken to explicitly encode public keys as X-only — risking the lose of funds if not done correctly.

Explicit conversion can be done using xonly(). For example:

tr(NUMS, `xonly($alice/0/5) OP_CHECKSIG`)

Note that xonly() always returns a PubKey<SingleKey>, even when called with an Xpub. The underlying miniscript::DescriptorPublicKey structure cannot represent X-only Xpubs.

Digital Signatures

See ecdsa::sign(), ecdsa::verify(), schnorr::sign() and schnorr::verify().

$msg = hash::sha256("Hello Minsc");
$signature = ecdsa::sign($alice_sk, $msg);
assert(ecdsa::verify($alice, $msg, $signature));

Hash Derivation

Minsc supports key derivation tweaked using a 256-bit hash, also known as the pay-to-contract-hash construct.

For interoperability with signing devices, the hash is split up and represented as nine u32s used as a standard BIP 32 derivation path. This enables BIP32-enabled signers to derive contract keys and sign with them. See hash_to_child_vec for a complete description of the algorithm.

Example use:

$contract_hash = hash::sha256("Alice is selling the FOO.COM domain to Bob for 0.5 BTC");
$alice_wpkh = wpkh($alice/$contract_hash);
  // == wpkh($alice/812713858/1226840367/1508216297/336464386/799898706/1008189013/1682778198/203561018/210)
$alice_addr = address($alice_wpkh);

Or with a multi-party descriptor, deriving each participant's key along the same path:

$contract_hash = hash::sha256("Alice is selling the FOO.COM domain to Bob for 0.5 BTC");
;;;
$escrow_abc = wsh(2 of [ $alice/*, $bob/*, $charlie/* ]);
$escrow_contract = $escrow_abc/$contract_hash;
$escrow_addr = address($escrow_contract);

Bonus: Sapio CTV Emulator Oracle using hash derivation

The Sapio CTV Emulator Oracle uses the same hash derivation method to generate oracle child keys that correspond to and are cryptographically tied to a specific CTV template hash.

Here's an example of how this could be used in Minsc:

```minsc-exec id="ctv-oracle-1" linenums="1" [$cold_pk, _, $hot_pk, $hot_sk] = dummy::kpairs(2); ;;;

ORACLE = pk(xpub661MyMwAqRbcEwGHZf9BN5accEwzg51XoNsWtqbXrjGiyW1zWapP1Hkof9Dks7ruCqrQotunxMgdpvwgiPh24jPwSDeQKRxAXdyWB54CYTN/*); // Returns a Policy requiring a signature by the Oracle's CTV hash child key fn ctv::oracle($tx, $input_index=0) = ORACLE/ctv::hash($tx, $input_index);

// Vault Timelock enforced by a CTV Emulator Oracle $txo_amount = 0.1 BTC; $vault_deposit = wsh($cold_pk || ($hot_pk && ctv::oracle[ "output": wsh($cold_pk || ($hot_pk && older(3 weeks))): $txo_amount - 1000 sat, ])); $vault_addr = address($vault_deposit);

> The `#!m ORACLE` can easily be made an M-of-N federation,
for example by setting `#!m ORACLE = 2 of [ $alice, $bob, $charlie ]`.

The Oracle would provide a public API (e.g. over HTTP) that is essentially this function:

```minsc linenums="11"
ORACLE_SK = xprv9s21ZrQH143K2TBpTdcAzwdt4D7WGcHgS9wv6TBvJPjk6hgqy3W8TVSKos4kq9vZqj94f1dwnTT5ezqjw9WJfATKxfAYiEhupoCrj6B2VVX;

fn oracle_sign($psbt, $input_index) {
  $child_sk = ORACLE_SK/ctv::hash($psbt, $input_index);
  psbt::sign($psbt, derived($child_sk)) // why derived()? (1)
}

  1. Passing $child_sk directly would work too, however the PSBT signer will also attempt to use the parent ORACLE_SK secret key material still internally held by it. Using explicit derived() ensures that the signer only has access to the SingleKey child corresponding to the specific CTV hash.

Which can then be used for CTV emulation:

```minsc-exec continues="ctv-oracle-1" linenums="17" ORACLE_SK = xprv9s21ZrQH143K2TBpTdcAzwdt4D7WGcHgS9wv6TBvJPjk6hgqy3W8TVSKos4kq9vZqj94f1dwnTT5ezqjw9WJfATKxfAYiEhupoCrj6B2VVX; fn oracle_sign($psbt, $input_index) = psbt::sign($psbt, derived(ORACLE_SK/ctv::hash($psbt, $input_index))); ;;; // Construct PSBT withdrawing from vault $psbt = psbt[ "input": [ "prevout": f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1, "utxo": $vault_deposit:$txo_amount, ], "output": wsh($cold_pk || ($hot_pk && older(3 weeks))): $txo_amount - 1000 sat, ];

// Add CTV Emulator Oracle signature $oracle_signed = oracle_sign($psbt, 0);

// Add the user's final signature and finalize the tx $tx_final = psbt::sign_extract($oracle_signed, $hot_sk); ```

Bytes Encoding

Keys can be encoded to bytes using bytes(PubKey|SecKey) and decoded using pubkey(Bytes) or seckey(Bytes).

⚠ The byte encoding does not preserve the key origin information associated with the PubKey/SecKey instance. For Xpubs/Xprivs, only the immediate parent is preserved in the BIP 32 serialization format. For SingleKeys, nothing is.

To preserve key origin information, use the str()/repr() encoding instead of bytes.

$pk = [11ff22ee/0/8]027d023ea4e2ef6d74360c3c9e588f6df2c886db0631aa147e320914dee9aa5883;
$px = [96873aca/5/2]xpub6AUxv5HeUG94BAw1sGbWpXN1KwBDbmfoQgzLEumXVsahr9BKwgSFfXYyn2CkNHwoEcE4no8AK7oPQ6QDqn5V6zi2kx51sP6Rb1ks2hQoe8Q;

$pk_bytes = bytes($pk);
$px_bytes = bytes($px);

$pk_rt = pubkey($pk_bytes); // no origin
$px_rt = pubkey($px_bytes); // [60f39d44/2]xpub6AUxv5HeU… (60f39d44 is 96873aca/5)