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:

Minsc's key expression syntax is fully compatible with the standard Descriptor KEY expression. If you're familiar with it, you already know how to construct keys in Minsc!

Key Construction

Xpub, Xpriv and WIF-encoded single secret keys can be written directly in Minsc code. They're detected during parsing and evaluate into a PubKey/SecKey.

For example:

// Xpub (used to construct a WPKH descriptor)
$descriptor = wpkh(xpub661MyMwAqRbcFvXwqbgwigDczeocqeBEibKMCkoc31RiyB464Ybc1z8sWMnR38JdeCBJPPkSM7mKahcBX2nPX9KYVTz3cotpLmSkMxrp99L);

// Xpriv (converted into an Xpub)
$xpub = pubkey(xprv9s21ZrQH143K37CBUxq5bSXTyHwKc5KwYWsm6onCYDmiPBBDyiz3bYwvHYH9NXzsY6mDiXmhf77Ym2EJkGreLHB3s6MH5tRkfKQT9uDQ4r2);

// WIF (used to sign)
$signature = ecdsa::sign(L4oC7AMPJkKPwuVipfFaKcJKBhsR3kmXy89f7oAFhSQSPWLNXocJ, hash::sha256("Hello World"));
$descriptor = wpkh(xpub661MyMwAqRbcFvXwqbgwigDczeocqeBEibKMCkoc31RiyB464Ybc1z8sWMnR38JdeCBJPPkSM7mKahcBX2nPX9KYVTz3cotpLmSkMxrp99L) // descriptor

$xpub = xpub661MyMwAqRbcFbGeazN5xaUCXKmp1Y3nujoMuCBp6ZJhFyWNXGJJ9MGQ8otkj3MgGcpcCx7u1A88yVZ2RmmPqPuUHJ5TsD1f7G2PoFNJqLc // pubkey

$signature = 0x304402201bd3260a7e1d76160df2e458d03828a2ed4c517bc0fefbaf1d2e097a75c0f1bf022007f60ca216d20196c3a0a306e779552e4ab8b53d09a0d209089bee14d501fe12 // bytes

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 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 have additional BIP 32 derivation path steps applied on them, using the / operator. 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.

Implementation note: When key derivation is actually applied

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 child derivation is only applied lazily when the tweaked child key is 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 example below, $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()
$a = PubKey(
  XPub(
    DescriptorXKey {
      origin: Some(
        (
          0x01aa02bb,
          5,
        ),
      ),
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          579cd8960baaa25688074ce710a58f8f2c965e328b09579435caa8e3f4dc9da25161f791b3050d15ad65cd850d73361c6d25e1be3d2d8028211ef21f22e013d7,
        ),
        chain_code: 0x8ab6af79520614a7e07c82fb8a6f33eb4dccbac0bddab1dd8f3ab63f8b08feb5,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$b = PubKey(
  XPub(
    DescriptorXKey {
      origin: Some(
        (
          0x01aa02bb,
          5,
        ),
      ),
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          579cd8960baaa25688074ce710a58f8f2c965e328b09579435caa8e3f4dc9da25161f791b3050d15ad65cd850d73361c6d25e1be3d2d8028211ef21f22e013d7,
        ),
        chain_code: 0x8ab6af79520614a7e07c82fb8a6f33eb4dccbac0bddab1dd8f3ab63f8b08feb5,
      },
      derivation_path: 1,
      wildcard: None,
    },
  ),
)

$c = PubKey(
  XPub(
    DescriptorXKey {
      origin: Some(
        (
          0x01aa02bb,
          5,
        ),
      ),
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          579cd8960baaa25688074ce710a58f8f2c965e328b09579435caa8e3f4dc9da25161f791b3050d15ad65cd850d73361c6d25e1be3d2d8028211ef21f22e013d7,
        ),
        chain_code: 0x8ab6af79520614a7e07c82fb8a6f33eb4dccbac0bddab1dd8f3ab63f8b08feb5,
      },
      derivation_path: 1/100,
      wildcard: None,
    },
  ),
)

$d = PubKey(
  Single(
    SinglePub {
      origin: Some(
        (
          0x01aa02bb,
          5/1/100,
        ),
      ),
      key: FullKey(
        PublicKey {
          compressed: true,
          inner: PublicKey(
            dd212b43f73b969db50dcef8a1f79c0e7db49131e5a7fccdc11671f12ffe6654bfe94497d63967ad00125954ab3fe3fa3a3b66d03774d02fd408b170794f1c82,
          ),
        },
      ),
    },
  ),
)

$x = PubKey(
  XPub(
    DescriptorXKey {
      origin: Some(
        (
          0x01aa02bb,
          5/1/100,
        ),
      ),
      xkey: Xpub {
        network: Main,
        depth: 2,
        parent_fingerprint: 0x25a707b5,
        child_number: Normal {
          index: 100,
        },
        public_key: PublicKey(
          dd212b43f73b969db50dcef8a1f79c0e7db49131e5a7fccdc11671f12ffe6654bfe94497d63967ad00125954ab3fe3fa3a3b66d03774d02fd408b170794f1c82,
        ),
        chain_code: 0x1f4c23cec7a23a79a675d4bf0fa7e29e6b77f9ee031ff8e914d593f5d0d7b607,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$alice = PubKey(
  XPub(
    DescriptorXKey {
      origin: None,
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          3d8e957eb3ac0c5273b124c6803900b52cfff84ffe057488c585a19632eb462a6440035fd5712dec683e6e9b366094c0ed6cbb3e0c6c8c6bf7bc949d689b869e,
        ),
        chain_code: 0xf249637335882d797bab89afb1704d61a8fe5635bd236f87c9ea55c8b0742539,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$alice_sk = SecKey(
  XPrv(
    DescriptorXKey {
      origin: None,
      xkey: Xpriv {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        private_key: SecretKey(
          #0a89a4bced29c281,
        ),
        chain_code: 0xf249637335882d797bab89afb1704d61a8fe5635bd236f87c9ea55c8b0742539,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$bob = PubKey(
  XPub(
    DescriptorXKey {
      origin: None,
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          1c970b3cbbebdee1b432ae9c36d27962cbc664317c8a9ec7d907c3a68fa4144d8b067d5d7f39dd985867868fdf37d5a80bcc9fad5d81a04263743ab4a146e8ea,
        ),
        chain_code: 0x7ac8373005a6f7470d93f974b9bd91d4b45e7d0e673cb05434fa25b97c3e126b,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$bob_sk = SecKey(
  XPrv(
    DescriptorXKey {
      origin: None,
      xkey: Xpriv {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        private_key: SecretKey(
          #cec9ce37ca22832b,
        ),
        chain_code: 0x7ac8373005a6f7470d93f974b9bd91d4b45e7d0e673cb05434fa25b97c3e126b,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$charlie = PubKey(
  XPub(
    DescriptorXKey {
      origin: None,
      xkey: Xpub {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        public_key: PublicKey(
          1775ce31adfe86389048710b84a9f4fdd42f03eaf3da60de6689e05b77030a0c1fc7aeb676d157d6cb42b3d8511e71c44f84c84d22128786ee4c50ecf586d22a,
        ),
        chain_code: 0x689d6a58139676a3506d6e7f201779cddb57fad2ef4d12c543f92197878b19bf,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

$charlie_sk = SecKey(
  XPrv(
    DescriptorXKey {
      origin: None,
      xkey: Xpriv {
        network: Main,
        depth: 0,
        parent_fingerprint: 0x00000000,
        child_number: Normal {
          index: 0,
        },
        private_key: SecretKey(
          #3a86ca643757280d,
        ),
        chain_code: 0x689d6a58139676a3506d6e7f201779cddb57fad2ef4d12c543f92197878b19bf,
      },
      derivation_path: ,
      wildcard: None,
    },
  ),
)

print = fn ($msg)

main = dyn fn main()

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>) == [ $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
$wallet = wpkh([df786318/84'/0'/0']xpub6DVuVGaCzdL26peRNyHctA6x7e9zCchq7SjVQ44jBWUNieZuDL8e9EaqEk4DEDsAoXZsjNAnUYsQQ7uz8NLHoUL6QkShSm5Rk4CnpnTTC2g/<0;1>/*) // descriptor

$receive = wpkh([df786318/84'/0'/0']xpub6DVuVGaCzdL26peRNyHctA6x7e9zCchq7SjVQ44jBWUNieZuDL8e9EaqEk4DEDsAoXZsjNAnUYsQQ7uz8NLHoUL6QkShSm5Rk4CnpnTTC2g/0/*) // descriptor

$change = wpkh([df786318/84'/0'/0']xpub6DVuVGaCzdL26peRNyHctA6x7e9zCchq7SjVQ44jBWUNieZuDL8e9EaqEk4DEDsAoXZsjNAnUYsQQ7uz8NLHoUL6QkShSm5Rk4CnpnTTC2g/1/*) // descriptor

$recv_address = tb1qn7ca6nel9vn8rjx24v07wphn94cnjxdffwfrnn // address

$receive_ = wpkh([df786318/84'/0'/0']xpub6DVuVGaCzdL26peRNyHctA6x7e9zCchq7SjVQ44jBWUNieZuDL8e9EaqEk4DEDsAoXZsjNAnUYsQQ7uz8NLHoUL6QkShSm5Rk4CnpnTTC2g/0/*) // descriptor

$change_ = wpkh([df786318/84'/0'/0']xpub6DVuVGaCzdL26peRNyHctA6x7e9zCchq7SjVQ44jBWUNieZuDL8e9EaqEk4DEDsAoXZsjNAnUYsQQ7uz8NLHoUL6QkShSm5Rk4CnpnTTC2g/1/*) // descriptor

$recv_address_ = tb1qn7ca6nel9vn8rjx24v07wphn94cnjxdffwfrnn // address

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);
$descriptor = wpkh(xpub661MyMwAqRbcGxLpmSg6J2PK7LafNMyws1wApmxpFmqKvgfAuxe7fBvjzhsAmM3QJ9bpzBLAB4NskRYDmeYrWiRLbC4JpTYf8zahjeEn6Z8/1/<0;1;2;3;4;5;6;7;8;9;10;11;12;13;14>) // descriptor

$addresses = [
  tb1qd6xd7yh6hl7ckt7hzjqcaq040kqegl9awxnv4t,
  tb1qa6mlmsdlg4d5u63zgs97rr0j8awhkjrgsr5nm6,
  tb1qukz9kvn7p5x2tmxc2ne92vdnyrvaked4fge73x,
  tb1qwscvezmkavvtyezugwkwx85nztk3njgvjn2nep,
  tb1q7ljnq6ajc4ls4q2pnftknv5eyqqv49fpnhl7ev,
  tb1q97nuum6gaykxmqkh5ds7yyscz59wxe0lyqlzyl,
  tb1q7rm2ws3e9g7xkfhzk9qf5rntry7eyh07mwfe4f,
  tb1qgkrnuyz3t79g8396wct3puarn9gstl37e2u7vy,
  tb1q8egsc7uqupxrtgtyu0p5lqy460dyctjpgdfdcn,
  tb1qn6wlctr0ks7qp0e2hpzcq6vkjc8z7arp90prqh,
  tb1qr8f5dmtjrwcsa6r52suhzweq4hgfcn95wjvqcw,
  tb1qxcqfg5l3qqn58uxx8q0yc4lsm7s9zcmfs255gg,
  tb1q3f3c9y27208v83x5pc6nw9uk3kzpe6lvqdaffh,
  tb1qynpszng4u0axz5z0y7gf9y58z97ssm2hel0u27,
  tb1qx0zxq8myasxm8ythu7n60359m2m8wa47wp2zgt
] // array

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
[
  tb1qqlnhxd22lxwdssa2gwsmh8f72zss9yhnnqrf5q,
  tb1qxk0csazu8psw4jz28hvwes8un3u55n07gmuy6s,
  tb1qmrv4z4s7dxrxpnpm80st3f7u00u8lu8crp7nj2,
  tb1qwez0yhyvvd5w5n3t0h623q8zyx9k9up86xn8lr,
  tb1qpahufwgsezgeqzh7x6s304nztsjc7hgkmxfr8r,
  tb1q2y575yq7r88jgdzhrw4v78elu5r7ade6w0wrtc,
  tb1q9dezem9jn3ce4mhfhawra6j8v8wevwvx4ekgar,
  tb1q65d0xysqn5ny9khvv4kvdh5syyxu3myc60yrh9,
  tb1q82u7lcl9ggsk9xu8d29mq3acc0z7gpxg8vx8aw,
  tb1qtpdvu4hlf66mpr5fq5j5edy5xqfdzwyuqlc5p7,
  tb1q7u0rq5zn7rxezk8x35vngauggx8hg3ua8fyp4u,
  tb1q5ld85lxv6l27mezpp7zsrc5xcvcyftlvrrt5xf,
  tb1qtyq0afjx4ztphnklp6j82mah9z5n3xjx44xvwr,
  tb1qftctv8gq0mrluakynekwjlwlaut94lv4ahefm0,
  tb1qr970f5dztsxayhjs500fzld444692z4aaj3500,
  tb1qd6xd7yh6hl7ckt7hzjqcaq040kqegl9awxnv4t,
  tb1qa6mlmsdlg4d5u63zgs97rr0j8awhkjrgsr5nm6,
  tb1qukz9kvn7p5x2tmxc2ne92vdnyrvaked4fge73x,
  tb1qwscvezmkavvtyezugwkwx85nztk3njgvjn2nep,
  tb1q7ljnq6ajc4ls4q2pnftknv5eyqqv49fpnhl7ev,
  tb1q97nuum6gaykxmqkh5ds7yyscz59wxe0lyqlzyl,
  tb1q7rm2ws3e9g7xkfhzk9qf5rntry7eyh07mwfe4f,
  tb1qgkrnuyz3t79g8396wct3puarn9gstl37e2u7vy,
  tb1q8egsc7uqupxrtgtyu0p5lqy460dyctjpgdfdcn,
  tb1qn6wlctr0ks7qp0e2hpzcq6vkjc8z7arp90prqh,
  tb1qr8f5dmtjrwcsa6r52suhzweq4hgfcn95wjvqcw,
  tb1qxcqfg5l3qqn58uxx8q0yc4lsm7s9zcmfs255gg,
  tb1q3f3c9y27208v83x5pc6nw9uk3kzpe6lvqdaffh,
  tb1qynpszng4u0axz5z0y7gf9y58z97ssm2hel0u27,
  tb1qx0zxq8myasxm8ythu7n60359m2m8wa47wp2zgt
]

Definite vs Indefinite

When a PubKey/SecKey corresponds to a single concrete EC 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 where a concrete definite key is needed, unless they're resolved into a definite instance first. For example, indefinite instances cannot be used with scriptPubKey(), address() or tr::ctrl().

Consider the following example:

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

// Definite Descriptor - represents a final definite key, cannot be derived
$desc  = wpkh($alice/1);
$addr1 = address($desc);   // Works 👍
$addr2 = address($desc/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`)
tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, `<0xe5de26c2b4e60d42e504c57123dec2f47c7964166cdfa193f44de6699ddffb70> OP_CHECKSIG`)

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

Key Generation

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

tprv8ZgxMBicQKsPeLTn3X3gvx9VMK8uGr3jsQr67QcVL5toZjShiYsLDDTFVBzHqUHDSrDRGhnHnc5GTUDBgUzxxTjHY1gXxi8t1x12eyCJtQg

BIP 39 Mnemonics

Generate a random BIP 39 mnemonic:

>>> bip39(12)
"rifle swing section claw right upgrade process kiwi urge tenant opera where"

BIP 39 mnemonic to Xpriv:

>>> bip39("sun catch parrot journey clock taste drop future giant frequent artefact scatter")
tprv8ZgxMBicQKsPds4KHBfnLyVNq5yumcD74XLEGU7gmEERTUM3oCGYmPpQYnqVCYbURUNavqJLM62PiCdxrUcdqEfZEnb7UWfWVFaKa578V7k

BIP 39 mnemonic to Wpkh address:

>>> address(wpkh(bip39($mnemonic)/84'/0'/0'/1/0))
tb1qnpe57f4etqq3k3sr7kqq0u3adwgyzs3kawuc28

See the BIP 39 reference for more functions and options.

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));
$msg = 0x793d458aa55ad77669005a95c77bb41425ba4dba731800bf74b0f50fcf153c8c // bytes

$signature = 0x304402200c01d219080a94d4f38bf0bd3f6345b6522ac430d308237ae51f00883f68027802207eb14ffed296b3ccc7846d10f4a689e6b252deb67d0ff441fade202d9b0dc083 // bytes

Hash Derivation

Minsc supports key derivation tweaked using a 256-bit hash, a form of 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 Sapio's 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);
$contract_hash = 0x8ed48f959d8e5ddb65ef84599ca462e5fd0400778bdfcc2c3d0ee8cd5484b901 // bytes

$alice_wpkh = wpkh(xpub661MyMwAqRbcGxLpmSg6J2PK7LafNMyws1wApmxpFmqKvgfAuxe7fBvjzhsAmM3QJ9bpzBLAB4NskRYDmeYrWiRLbC4JpTYf8zahjeEn6Z8/248811413/495869403/1710195801/480535269/2097414263/199216172/1024387277/1417984257/59) // descriptor

$alice_addr = tb1qutsmq27kprsvrfdpw0xzg53gf7yf2hnuy0fgjn // address

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

$escrow_abc = wsh(2 of [ $alice/*, $bob/*, $charlie/* ]);
$escrow_contract = $escrow_abc/$contract_hash;
$escrow_addr = address($escrow_contract);
$escrow_abc = wsh(multi(2,xpub661MyMwAqRbcGxLpmSg6J2PK7LafNMyws1wApmxpFmqKvgfAuxe7fBvjzhsAmM3QJ9bpzBLAB4NskRYDmeYrWiRLbC4JpTYf8zahjeEn6Z8/*,xpub661MyMwAqRbcFmLNCYgXGZtNT5UXqL6ECWbMB22t2MaXQtLPVcn4N7gwcGuKkysUWDoFcSVaEPEFtH1vXm81YLq1pq4T9h46TjgeBpQRonX/*,xpub661MyMwAqRbcFaqugwCZAhow4KYKLuo3uYLJDwtGAtJC9PGomu5HXyHJa1XxsJbZ3fC4AJrciz4kKreJCS8an3hRsNzGxq5jTrtfSh8iTho/*)) // descriptor

$escrow_contract = wsh(multi(2,xpub661MyMwAqRbcGxLpmSg6J2PK7LafNMyws1wApmxpFmqKvgfAuxe7fBvjzhsAmM3QJ9bpzBLAB4NskRYDmeYrWiRLbC4JpTYf8zahjeEn6Z8/248811413/495869403/1710195801/480535269/2097414263/199216172/1024387277/1417984257/59,xpub661MyMwAqRbcFmLNCYgXGZtNT5UXqL6ECWbMB22t2MaXQtLPVcn4N7gwcGuKkysUWDoFcSVaEPEFtH1vXm81YLq1pq4T9h46TjgeBpQRonX/248811413/495869403/1710195801/480535269/2097414263/199216172/1024387277/1417984257/59,xpub661MyMwAqRbcFaqugwCZAhow4KYKLuo3uYLJDwtGAtJC9PGomu5HXyHJa1XxsJbZ3fC4AJrciz4kKreJCS8an3hRsNzGxq5jTrtfSh8iTho/248811413/495869403/1710195801/480535269/2097414263/199216172/1024387277/1417984257/59)) // descriptor

$escrow_addr = tb1qmu3ktsz77t26q7z388kmwvf4q77ql445g475g4zlg005tuwefz8qlgqgep // address

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:

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

// Vault with a withdrawal contest period enforced by a CTV Emulator Oracle
$txo_amount = 0.1 BTC;
$vault_deposit = wsh($cold_pk || ($hot_pk && oracle::ctv[
  "output": wsh($cold_pk || ($hot_pk && older(3 weeks))): $txo_amount - 1000 sat,
]));
$vault_addr = address($vault_deposit);
ORACLE = pk(xpub661MyMwAqRbcEwGHZf9BN5accEwzg51XoNsWtqbXrjGiyW1zWapP1Hkof9Dks7ruCqrQotunxMgdpvwgiPh24jPwSDeQKRxAXdyWB54CYTN/*) // policy

oracle::ctv = fn oracle::ctv($tx, $input_index?) // function

$txo_amount = 10000000 // int

$vault_deposit = wsh(c:andor(pk(03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e),pk_k(xpub661MyMwAqRbcEwGHZf9BN5accEwzg51XoNsWtqbXrjGiyW1zWapP1Hkof9Dks7ruCqrQotunxMgdpvwgiPh24jPwSDeQKRxAXdyWB54CYTN/1502791943/579403930/1922839205/1361414138/955748127/1861458468/1946761977/758172133/140),pk_k(036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2))) // descriptor

$vault_addr = tb1q8p8uh0e6q0kpjn0nf7hwsujup3azjevltfcvavml98f47ct00shqle6n5j // address

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

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

ORACLE_SK = xprv9s21ZrQH143K2TBpTdcAzwdt4D7WGcHgS9wv6TBvJPjk6hgqy3W8TVSKos4kq9vZqj94f1dwnTT5ezqjw9WJfATKxfAYiEhupoCrj6B2VVX;

fn oracle::sign($psbt, $input_index=0) {
  $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:

// Create PSBT withdrawing from vault
$psbt_trigger = psbt[
  "input": [
    "prevout": f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1,
    "utxo": $vault_deposit:$txo_amount,
  ],
  "output": wsh($cold_pk || ($hot_pk && older(3 weeks))): $txo_amount - 1000 sat,
];

// Request CTV Emulator Oracle signature
$oracle_signed = oracle::sign($psbt_trigger, 0);

// Sign with the user's hot key
$user_signed = psbt::sign($psbt_trigger, $hot_sk);
// Combine the signatures and finalize the trigger transaction
$tx_trigger = psbt::finalize($user_signed + $oracle_signed);

// Create and sign the 2nd stage release transaction, valid after 3 weeks
$tx_release = psbt::sign_finalize([
  "input": [ "prevout": $psbt_trigger:0, "sequence": 3 weeks ],
  "output": bc1qdfac40sy0uk2g3z9g8z75z3xkrvnsjd0h0u8q5: $txo_amount - 2000 sat,
], $hot_sk);
$psbt_trigger = psbt[
  "unsigned_tx": tx[
    "version": 2,
    "inputs": [
      f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1
    ],
    "outputs": [
      `<0> <0xec73821b46b1220fca050f9a71917ca702ede4f51e188974c2656793d32d66aa>`:0.09999 BTC
    ]
  ],
  "version": 0,
  "inputs": [
    [
      "utxo": `<0> <0x384fcbbf3a03ec194df34faee8725c0c7a29659f5a70ceb37f29d35f616f7c2e>`:0.1 BTC,
      "witness_script": `<0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIG OP_NOTIF <0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_ELSE <0x03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7> OP_ENDIF OP_CHECKSIG`,
      "bip32_derivation": [
        [6676502c/1502791943/579403930/1922839205/1361414138/955748127/1861458468/1946761977/758172133/140]03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7,
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ],
  "outputs": [
    [
      "witness_script": `<0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_CHECKSIG OP_IFDUP OP_NOTIF <0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIGVERIFY <0xd80d40> OP_CHECKSEQUENCEVERIFY OP_ENDIF`,
      "bip32_derivation": [
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ]
] // psbt

$oracle_signed = psbt[
  "unsigned_tx": tx[
    "version": 2,
    "inputs": [
      f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1
    ],
    "outputs": [
      `<0> <0xec73821b46b1220fca050f9a71917ca702ede4f51e188974c2656793d32d66aa>`:0.09999 BTC
    ]
  ],
  "version": 0,
  "inputs": [
    [
      "utxo": `<0> <0x384fcbbf3a03ec194df34faee8725c0c7a29659f5a70ceb37f29d35f616f7c2e>`:0.1 BTC,
      "partial_sigs": [
        03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7: 0x304402200a3e0fd34e7c974bc102c8d7203f42999eec580cc9a9205ce06941d293659f8002201106b42d38f3a14911d316eb642a47b87a9f291ec702efbd72ab679626495f5201
      ],
      "witness_script": `<0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIG OP_NOTIF <0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_ELSE <0x03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7> OP_ENDIF OP_CHECKSIG`,
      "bip32_derivation": [
        [6676502c/1502791943/579403930/1922839205/1361414138/955748127/1861458468/1946761977/758172133/140]03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7,
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ],
  "outputs": [
    [
      "witness_script": `<0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_CHECKSIG OP_IFDUP OP_NOTIF <0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIGVERIFY <0xd80d40> OP_CHECKSEQUENCEVERIFY OP_ENDIF`,
      "bip32_derivation": [
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ]
] // psbt

$user_signed = psbt[
  "unsigned_tx": tx[
    "version": 2,
    "inputs": [
      f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1
    ],
    "outputs": [
      `<0> <0xec73821b46b1220fca050f9a71917ca702ede4f51e188974c2656793d32d66aa>`:0.09999 BTC
    ]
  ],
  "version": 0,
  "inputs": [
    [
      "utxo": `<0> <0x384fcbbf3a03ec194df34faee8725c0c7a29659f5a70ceb37f29d35f616f7c2e>`:0.1 BTC,
      "partial_sigs": [
        03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e: 0x3044022074b5b3835c9a7dfddfe917487488decee7ffbb6068299ccec0c46b031ec90755022078f4830b5d58e2334e7c20785be7e87e1b0e8f67e43682d9bec85bc7b94d746601
      ],
      "witness_script": `<0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIG OP_NOTIF <0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_ELSE <0x03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7> OP_ENDIF OP_CHECKSIG`,
      "bip32_derivation": [
        [6676502c/1502791943/579403930/1922839205/1361414138/955748127/1861458468/1946761977/758172133/140]03186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec7,
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ],
  "outputs": [
    [
      "witness_script": `<0x036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2> OP_CHECKSIG OP_IFDUP OP_NOTIF <0x03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e> OP_CHECKSIGVERIFY <0xd80d40> OP_CHECKSEQUENCEVERIFY OP_ENDIF`,
      "bip32_derivation": [
        [3cc92f90]036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2,
        [703df49a]03b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94e
      ]
    ]
  ]
] // psbt

$tx_trigger = tx[
  "version": 2,
  "inputs": [
    [
      "prevout": f0330f32f6cffac8ede7b3c13390afc13a49d0b919b6f9f387eb5cbf17a974cd:1,
      "witness": [ 0x304402200a3e0fd34e7c974bc102c8d7203f42999eec580cc9a9205ce06941d293659f8002201106b42d38f3a14911d316eb642a47b87a9f291ec702efbd72ab679626495f5201, 0x3044022074b5b3835c9a7dfddfe917487488decee7ffbb6068299ccec0c46b031ec90755022078f4830b5d58e2334e7c20785be7e87e1b0e8f67e43682d9bec85bc7b94d746601, 0x2103b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94eac6421036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2672103186cbfcc671f1dcb591c9f426a7226ef2d8e5e7af23973244101574c64b8aec768ac ]
    ]
  ],
  "outputs": [
    `<0> <0xec73821b46b1220fca050f9a71917ca702ede4f51e188974c2656793d32d66aa>`:0.09999 BTC
  ]
] // transaction

$tx_release = tx[
  "version": 2,
  "inputs": [
    [
      "prevout": a4485b5cf80a9d9ce8aa4f56f4fe57cad14e4a29198bae0b16f757041f5ea68b:0,
      "sequence": 1814528 seconds,
      "witness": [ 0x304402206da31e7051a1f45c663ce1146b6f83286521fc137abe08cc55d79478dd8a8eba022023aa7abad1f553b16619ad3df12309d34ef1591bb7013d3ea4d8c4755fbb70f701, 0x, 0x21036771073c1e92792d05c5c4e739b7094817f9d94bb287e109298ae1936a88f3b2ac73642103b694b0ada1fc2889ee79b01860131a64f981c34199e339e614a8079f117df94ead03d80d40b268 ]
    ]
  ],
  "outputs": [
    `<0> <0x6a7b8abe047f2ca4444541c5ea0a26b0d93849af>`:0.09998 BTC
  ]
] // transaction

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 repr()/str() string 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)
$pk = [11ff22ee/0/8]027d023ea4e2ef6d74360c3c9e588f6df2c886db0631aa147e320914dee9aa5883 // pubkey

$px = [96873aca/5/2]xpub6AUxv5HeUG94BAw1sGbWpXN1KwBDbmfoQgzLEumXVsahr9BKwgSFfXYyn2CkNHwoEcE4no8AK7oPQ6QDqn5V6zi2kx51sP6Rb1ks2hQoe8Q // pubkey

$pk_bytes = 0x027d023ea4e2ef6d74360c3c9e588f6df2c886db0631aa147e320914dee9aa5883 // bytes

$px_bytes = 0x0488b21e0260f39d44000000026207f07b048f47a8a935299754ae3622f2d9a64bbad2e5f9cb904815e85805a40304c5bd2e224ecc8818e86f4b9d2caf243f764ed931b8a50957e308f48a188563 // bytes

$pk_rt = pubkey(027d023ea4e2ef6d74360c3c9e588f6df2c886db0631aa147e320914dee9aa5883) // pubkey

$px_rt = [60f39d44/2]xpub6AUxv5HeUG94BAw1sGbWpXN1KwBDbmfoQgzLEumXVsahr9BKwgSFfXYyn2CkNHwoEcE4no8AK7oPQ6QDqn5V6zi2kx51sP6Rb1ks2hQoe8Q // pubkey