Minsc

A Miniscript-based scripting language for Bitcoin contracts

Minsc is a high-level scripting language for expressing Bitcoin Script spending conditions. It is based on the Miniscript Policy language, with additional features and syntactic sugar sprinkled on top, including variables, functions, infix notation, and more.

Implemented in Rust with LALRPOP. Source code is available on GitHub, released under the MIT license.

Live Minsc-to-Policy-to-Miniscript-to-Script compiler

Loading WASM...
Policy
Miniscript / Descriptor
Bitcoin Script

Reference

Minsc is using the rust-miniscript implementation.

Any valid Miniscript Policy is also a valid Minsc expression.

Jump to: Logical Operators · Threshold Operator · Execution Probabilities · Time & Durations · Variables · Arrays · Public Keys · Hashes · Descriptors & Addresses · Functions

The snippets below use the A...L and xxx_pk variables, which are pre-populated in the playground with example data to keep things short.

Infix Logical Operators

// One of two keys
pk(A) || pk(B)
// Traditional preimage-based HTLC
(pk(A) && sha256(H)) || (pk(B) && older(10))

Supports >2 branches by compiling to thresh(N, ...) or thresh(1, ...).

// All of four keys
pk(A) && pk(B) && pk(C) && pk(D)

Threshold Operator

// 2-of-3 escrow contract
2 of [ pk(buyer_pk), pk(seller_pk), pk(arbiter_pk) ]

Execution Probabilities

You can indicate which of the or branches is more likely to be executed with @. Miniscript uses this information to optimize for lower spending costs.

// One of two keys, A 10x more likely than B
10@pk(A) || pk(B)

You may use the likely keyword as an alias for 10.

likely@pk(A) || pk(B)

Probabilities are only supported with two branches, but you can do something like:

// One of four keys, A more likely
likely@pk(A) || (pk(B) || pk(C) || pk(D))

Can alternatively be used as functions: likely(policy) and prob(n, policy).

Time & Durations

after() accepts dates formatted as YYYY-MM-DD, optionally with HH:MM.

// Lock some coins until 2030
pk(A) && after(2030-01-01)

older() accepts time durations with years, months, weeks, days, hours, minutes and seconds.

// A user and a 2FA service need to sign off, but after 90 days the user alone is enough
pk(user_pk) && (9@pk(service_pk) || older(90 days))
// A 3-of-3 that turns into 2-of-3 after a timeout
3 of [ pk(A), pk(B), pk(C), older(1 month 2 weeks) ]

The heightwise keyword can be used to produce block-height-wise durations.

// Lock some coins for 6 blocks (approx 1 hour)
pk(A) && older(heightwise 1 hour)

The blocks keyword can optionally be specified for block count durations. This typically simply compiles to the number, but also verifies that it is within the allowed range and may be more readable.

// Fails compilation, BIP 68 only supports up to 65535 blocks
pk(A) && older(65536 blocks)

// older(65536) compiles, but doesn't work as can be expected!
// 🛑 🦶 🔫

Note that time durations are encoded in granularity of 512 seconds and are rounded up (i.e. 513 seconds becomes 1024 seconds).

Variables

// Traditional preimage-based HTLC

$redeem = pk(A) && sha256(H);
$refund = pk(B) && older(10);

likely@$redeem || $refund
// Liquid-like federated pegin with emergency recovery keys
// Funds are normally held by a 4-of-5 federation, but can be recovered by the emergency backup keys after 3 months of inactivity

$federation = 4 of [ pk(A), pk(B), pk(C), pk(D), pk(E) ];
$recovery = 2 of [ pk(F), pk(G), pk(I) ];
$timeout = older(heightwise 3 months);

likely@$federation || ($timeout && $recovery)

The $ variable prefix is optional.
Variables are immutable, but can be shadowed over in inner scopes.
Scoping is currently dynamic, expected to eventually be replaced with lexical scoping.

Arrays

// One of two keys
$keys = [ pk(A), pk(B) ];
$keys.0 || $keys.1

any($arr) can be used to require that one of the subpolicies is met.

// The CEO plus any of the directors are needed to sign off
$directors = [ pk(A), pk(B), pk(C) ];
pk(ceo_pk) && any($directors) // thresh(1, $directors)

all($arr) can be used to require that all of the subpolicies are met.

// The CEO or all of the directors are needed to sign off
$directors = [ pk(A), pk(B), pk(C) ];
pk(ceo_pk) || all($directors) // thresh(3, $directors)

The last example could alternatively be written as pk(ceo) || $directors.

Public keys

Public keys can be specified in hex (for standalone keys, compressed only) or as xpubs. Both can optionally be prefixed with the bip32 origin information.

$alice = tpubD6NzVbkrYhZ4XDA7mimo1E8vqhJBSh34B8XfkGL3Guw9jitqTLu7i2Fp5YtDMhNsoj3jdUxAy1adBV7uz2AE8hx3Stp8tBEpAzwp8dRKpwW/9/0;
$bob = [a091e2c6/0/7]03a5cc183f0676b681e8f8d2829c3d3a76d5c4e1c1c6e4d01cda5df82614e1c32d;

pk($alice) && pk($bob)

Key derivation works at runtime:

$alice = xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/9/0;
$account = 10;

pk($alice/$account/0)

This can be useful for avoiding key reuse, by using different derivation paths for different branches.

(pk($alice/0/*) && older(1 day)) || (pk($alice/1/*) && sha256(H))

Hashes

Hashes are hex encoded and can be either 32 bytes (for sha256/hash256) or 20 bytes (for ripemd160/hash160).

$hash_256 = 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b;
$hash_160 = 4355a46b19d348dc2f57c046f8ef63d4538ebb93;

sha256($hash_256) && hash160($hash_160) && pk($alice)

Descriptors & Addresses

Explicit Policy -> Miniscript -> Descriptor -> Address conversion:

$alice = xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/9/0;

$policy = pk($alice/1/3) && older(1 month);
$miniscript = miniscript($policy); // compile policy to miniscript
$descriptor = wsh($miniscript); // wrap with a p2wsh descriptor
$address = address($descriptor); // generate the address

[ $policy, $miniscript, $descriptor, $address ]

address() supports regtest or testnet (the default) as its second argument. Mainnet is unsupported.

Descriptors containing wildcard keys (ending with /*) can be derived as well.

$policy = pk($alice/1/*) && older(1 month);
$descriptor = wsh(miniscript($policy));
address($descriptor/3) // same as previous example

Policies, miniscripts and public keys get auto-coerced into descriptors, so explicit miniscript() and wsh() calls are typically not necessary.

// Policies are automatically compiled and wrapped with a p2wsh descriptor
$policy = pk($alice/*) && older(1 month);
$address1 = address($policy/10); // == address(wsh(miniscript($policy))/10)

// Public keys are wrapped with a p2wpkh descriptor
$address2 = address($alice/2); // == address(wpkh($alice/2))

[ $address1, $address2 ]

sh() can be used to get p2sh-p2wsh and p2sh-p2wpkh descriptors.

// p2sh-p2wsh
$policy = pk($alice) && older(1 month);
$desc1 = sh($policy); // == sh(wsh($policy))

// p2sh-p2pkh
$desc2 = sh($alice); // == sh(wpkh($alice))

[ $desc1, $desc2, address($desc1), address($desc2) ]

Non-segwit descriptors are unsupported.

Functions

// The BOLT #3 received HTLC policy
fn bolt3_htlc_received($revoke_pk, $local_pk, $remote_pk, $secret, $delay) {
  $success = pk($local_pk) && hash160($secret);
  $timeout = older($delay);

  pk($revoke_pk) || (pk($remote_pk) && ($success || $timeout))
}

bolt3_htlc_received(A, B, C, H1, 2 hours)
// Two factor authentication with a timeout recovery clause
fn two_factor($user, $provider, $delay) = 
  $user && (likely@$provider || older($delay));

// 2FA where the user has a 2-of-2 setup and the service provider is a 3-of-4 federation

$user = pk(desktop_pk) && pk(mobile_pk);
$providers = [ pk(A), pk(B), pk(C), pk(D) ];

two_factor($user, 3 of $providers, 4 months)

If you prefer to start with the high-level policy description first and then delve into the details, you can declare a main() function like so:

// Revault multi-party vault (github.com/re-vault)

fn main() = $managers && (likely($timeout && $cosigners) || $non_managers);

$timeout = older(100 blocks);
$managers = [ pk(A), pk(B), pk(C) ];
$non_managers = [ pk(D), pk(E), pk(F), pk(G) ];
$cosigners = [ pk(I), pk(J), pk(K), pk(L) ];

// main() is implicitly returned

Functions are first-class and can be of higher order (accept function arguments and return them).

// Traditional preimage-based HTLC, with a configurable hash function
fn htlc($redeem_pk, $refund_pk, $hash, $expiry, $hash_fn) {
    $redeem = pk($redeem_pk) && $hash_fn($hash);
    $refund = pk($refund_pk) && older($expiry);
    9@$redeem || $refund
}

htlc(A, B, H, 10 blocks, sha256)
// Stuckless payments
// See https://lists.linuxfoundation.org/pipermail/lightning-dev/2019-September/002152.html

// The same HTLC function from the previous snippet (just more compact)
fn htlc($redeem_pk, $refund_pk, $hash, $expiry, $hash_fn) = 9@(pk($redeem_pk) && $hash_fn($hash)) || (pk($refund_pk) && older($expiry));

// Stuckless HTLC, implemented using htlc() with a custom hash function
fn stuckless_hash($h) = hash160($h.0) && hash160($h.1);
htlc(A, B, [ H1, H2 ], 6 hours, stuckless_hash)