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...
Bitcoin Script


Minsc is using the sipa implementation of Miniscript.

Any valid Miniscript Policy is also a valid Minsc expression.

Jump to: Logical Operators · Threshold Operator · Execution Probabilities · Time & Durations · Variables · Arrays · Functions

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(seller), pk(arbiter) ]

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(C))

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).


// 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(H) ];
$timeout = older(heightwise 3 months);

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

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


// 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) && 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) || all($directors) // thresh(3, $directors)

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


// 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, H, 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(user_desktop) && pk(user_mobile);
$providers = [ pk(P1), pk(P2), pk(P3), pk(P4) ];

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(H), pk(I), pk(J), pk(K) ];

// 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)