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:
- Minsc — Keys API Reference
- Learn Me a Bitcoin — Keys Guide
-
Bitcoin Core — Descriptor Keys Reference
💡 Minsc's key expression syntax is fully compatible with the standard Descriptor
KEYexpression described in the Bitcoin Core reference.
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 aPubKey<SingleKey>rather thanBytes.However, for compatibility with the native Descriptor/Policy syntax,
Bytesare automatically coerced into aPubKeywhen used as one. Also for compatibility, the0xprefix is optional.Together, this makes the following two equivalent:
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.
Key Structure¶
The PubKey/SecKey structures consist of four components:
- Key Origin — master fingerprint and derivation path leading to the literal key
- The literal key material (single or extended)
- Derivation Steps — from the literal key (extended keys only)
- 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 aPubKey<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:
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
Xprivsare coerced into anXpubwhen used to construct aDescriptor, so any hardened derivations must be performed before theXprivis 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
debugrepresentation. In the following example,$a,$band$call internally hold the samepublic_key/chain_codematerial, the only difference being the extendedderivation_path.When
[x]derived($c)is called, them/1/100derivation path is applied, moved to the key origin, and the internalpublic_key/chain_codematerial is updated to the child key's.Note that accessing public$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()PubKey/SecKeyfields likechain_codewill apply the necessary derivations to return the correct value for the child key, not the internalchain_codeseen in thedebugrepresentation.
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/0…14) range:
This also works with multiple chains derived with a range. This will generate the first 15 receive addresses plus the first 15 change 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_definiteandPolicy->is_definitefields 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 aPubKey<SingleKey>, even when called with anXpub. The underlyingminiscript::DescriptorPublicKeystructure 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)
}
- Passing
$child_skdirectly would work too, however the PSBT signer will also attempt to use the parentORACLE_SKsecret key material still internally held by it. Using explicitderived()ensures that the signer only has access to theSingleKeychild 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).
-
SingleKeysare encoded as the32/33raw key bytes -
XpubsandXprivsare encoded using the BIP32 serialization format (78bytes)
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)