The Lock You Can Never Open

You have just funded a corporate treasury output. The address was generated cleanly, the transaction confirmed, and the balance shows exactly what you sent. Then someone asks: what happens if the CFO leaves? You pull up the policy file. You read it twice. The recovery branch requires two independent signatures from the same key. The funds are locked. Permanently. And the compiler you used never said a word.

That is the failure mode Miniscript exists to prevent, and the mechanism it uses is worth understanding precisely.

For years, the structural consistency of a Bitcoin spending policy was entirely the developer's problem to verify. Miniscript's compiler makes it the machine's problem instead.

Script's Original Problem

Bitcoin Script is a stack-based language, intentionally limited: no loops, no recursion, no dynamic memory. Those constraints make Script analyzable, which is a genuine virtue. But writing it by hand is hazardous, because nothing in the base layer validates that your locking conditions are internally consistent.

A raw Script can encode an `OP_CHECKMULTISIG` requiring 3-of-2 keys. Two keys. Three required. The Script compiles, broadcasts, and locks funds into an address that is mathematically unspendable. The network accepts it without complaint.

This is not hypothetical. Researchers have catalogued real outputs on the Bitcoin blockchain that are provably unspendable due to exactly this class of error, sometimes from test transactions, sometimes from production mistakes. The amounts are typically small. The lesson is not.

What Miniscript Actually Is

Miniscript, developed primarily by Pieter Wuille, Andrew Poelstra, and Sanket Sanyal, is a structured language that sits on top of Bitcoin Script. You write a policy in Miniscript's grammar; the compiler translates it into correct Script and, critically, validates it along the way.

The grammar is compositional. You build complex spending conditions by nesting simpler ones:

  • `pk(key)`: requires a signature from a specific key
  • `older(n)`: requires that at least `n` blocks have elapsed since the output was confirmed
  • `thresh(k, expr1, expr2, ...)`: requires that at least `k` of the listed sub-expressions are satisfied
  • `and(expr1, expr2)`: both must be satisfied
  • `or(expr1, expr2)`: either must be satisfied

Because every node in this tree has a defined type and a defined satisfaction requirement, the compiler can reason about the whole structure before emitting a single opcode. The analysis is static. It finishes before a satoshi moves.

The Satisfiability Check, Step by Step

This is where the rejection mechanism lives.

The compiler assigns each sub-expression a type drawn from a small alphabet: `B` (base), `V` (verify), `K` (key), `W` (wrapped). These types encode not just what the expression does but whether it can be satisfied at all given its context. Alongside the type, the compiler tracks two boolean properties for every node:

`s` (satisfiable): can a valid witness be constructed for this expression in at least one possible world?

`f` (forced): is this expression always dissatisfied, meaning no witness can ever work?

Those properties propagate upward through the tree like a crack traveling through glass, each defect in a leaf telegraphing itself to the root. Consider `and(expr1, expr2)`: the compound is satisfiable only if both children are satisfiable. If either child carries the `f` flag, the parent is also always dissatisfied, and the compiler marks it accordingly.

Now consider `or(expr1, expr2)`. The compound is satisfiable if either child is satisfiable. But if one child is `f`, the compiler does not silently ignore it. It flags the dead branch and, depending on configuration, rejects the entire policy. The reasoning is sound and the conclusion is firm: a branch that can never be taken is not merely useless, it is evidence that the policy author believed they were adding a recovery path that does not exist.

Here is a worked scenario. A developer writes this policy for a corporate treasury:

``` or( and(pk(CEO), pk(CFO)), and(pk(CEO), older(52560)) ) ```

Left branch: CEO and CFO both sign. Right branch: CEO signs after roughly one year (52,560 blocks at ten minutes each). Reasonable. But the developer makes a typo and instead writes:

``` or( and(pk(CEO), pk(CFO)), and(thresh(2, pk(CEO), pk(CEO)), older(52560)) ) ```

The right branch now requires two distinct signatures from the same key. Miniscript's compiler, evaluating `thresh(2, pk(CEO), pk(CEO))`, determines that it cannot collect two independent satisfactions from a single key. The `s` property on that `thresh` node evaluates to false. The `and` wrapping it is therefore also unsatisfiable. The `or` at the root has one dead branch. The compiler rejects the policy and returns an error before any Script is generated.

No address is produced. No funds travel to an unspendable output, because the output never gets created.

The Malleability Dimension

Satisfiability analysis does not stop at a binary yes or no. The compiler also tracks whether a satisfaction is non-malleable, meaning a third party cannot modify the witness after signing without invalidating it.

This matters for time-locked contracts and payment channels. An `or` expression with two satisfiable branches is fine for basic use, but if a broadcast transaction could have its witness mutated to switch which branch was used without the signer's consent, a class of griefing attacks opens up. The compiler tracks an `m` property (non-malleable) separately from `s`, and it will reject policies that cannot guarantee non-malleability in contexts where it is required, such as inside a Tapscript leaf used in a Lightning channel.

The compiler's rejection logic is not a single gate, then. It is a lattice of properties computed bottom-up across the expression tree, where a defect at any leaf propagates toward the root with mathematical certainty.

What People Get Wrong About This

The common misconception is that Miniscript's safety guarantees come from a runtime check. They do not. By the time any transaction is broadcast, the analysis is long finished. The compiler is a static verifier, closer in character to a type-checker in a programming language than to a validator on a network node.

This means the guarantee is only as strong as the policy you actually feed the compiler. Write a semantically wrong policy, one that correctly expresses a condition but expresses the wrong condition for your business logic, and Miniscript has nothing to catch. It will compile a policy requiring three board members to sign when you needed two. The tool eliminates a class of structural bugs. Intent bugs are still yours to own.

There is also a subtler trap, and it deserves explicit attention. Policy languages like the one used in tools such as Liana wallet or Bitcoin Keeper allow you to write high-level expressions that get lowered into Miniscript. If that lowering step has a bug, the Miniscript compiler's guarantees do not protect you from the layer above it. Trust the compiler; audit whatever feeds it.

And ask yourself this: if a policy compiles cleanly, does that mean every branch is wise? It does not. It means every branch is reachable and satisfiable. Those are different things entirely.

Putting It to Work

For developers building multisig custody or recovery schemes, the practical workflow is straightforward: write the policy, run it through a Miniscript compiler (the reference implementation is available in Bitcoin Core as of version 25.0, and in libraries such as `rust-miniscript`), read the type annotations the compiler emits, and treat any rejection as a gift rather than an obstacle.

The rejection is the compiler reporting that one of your spending paths leads to a locked door with no key. Fix the policy before the funds exist. After the funds exist, you are negotiating with mathematics, and mathematics does not settle.

The genuine value here is not that Miniscript makes Script easier to write, though it does. It is that the compiler converts an entire category of catastrophic, silent, permanent mistakes into loud, early, fixable errors. In a system where transactions are irreversible, the distance between silent failure and loud failure is the only distance that matters.