Miniscript is a structured, human-friendly way to express Bitcoin spending conditions, making scripts easier to reason about and analyze, while providing safety guarantees around spending behavior.
This package provides a TypeScript implementation for compiling and analyzing
miniscript and for deriving witnesses (the data a spending transaction must
provide to unlock an output). The satisfier is signer-agnostic: it derives
symbolic witness stacks using placeholders like <sig(key)> and preimage
markers, so you can reason about satisfactions without private keys.
To install the package, use npm:
npm install @bitcoinerlab/miniscriptYou can test the examples in this section using the online playground demo available at https://bitcoinerlab.com/modules/miniscript.
Policies are a higher-level, human-readable language for describing spending
conditions (for example: and/or clauses, timelocks and key checks). Policy
compilation is provided by the companion package
@bitcoinerlab/miniscript-policies.
const { compilePolicy } = require('@bitcoinerlab/miniscript-policies');
const policy = 'or(and(pk(A),older(8640)),pk(B))';
const { miniscript } = compilePolicy(policy);To compile a Miniscript into Bitcoin ASM you can use the compileMiniscript function:
const { compileMiniscript } = require('@bitcoinerlab/miniscript');
const miniscript = 'and_v(v:pk(key),or_b(l:after(100),al:after(200)))';
const { asm } = compileMiniscript(miniscript);To generate script witnesses from a Miniscript, you can use the satisfier function.
The satisfier runs the Miniscript static type
system analysis and throws when a
miniscript is not sane. A sane miniscript follows consensus and standardness
rules: it avoids malleable-only paths (more on that later), does not mix
timelock units on a single branch and does not contain duplicate keys.
Witnesses are derived and returned in symbolic form (e.g., <sig(key)>,
<sha256_preimage(H)>), so you can analyze solutions even without a signer:
const miniscript =
'c:or_i(andor(c:pk_h(key1),pk_h(key2),pk_h(key3)),pk_k(key4))';
const { nonMalleableSats } = satisfier(miniscript);In the example above nonMalleableSats is:
nonMalleableSats: [
{asm: "<sig(key4)> 0"}
{asm: "<sig(key3)> <key3> 0 <key1> 1"}
{asm: "<sig(key2)> <key2> <sig(key1)> <key1> 1"}
], where satisfactions are ordered in ascending Weight Unit size, so you can choose the smallest witness and pay less in fees.
In addition to asm, returned solutions may carry timelock requirements when
needed:
{
asm: string;
nSequence?: number;
nLockTime?: number;
}If a solution includes nSequence or nLockTime, the spending transaction must
set those fields at the input or transaction level (CSV uses nSequence per
input; CLTV uses nLockTime for the transaction).
Dealing with malleability
SegWit eliminated classic txid malleability, but witness malleability still exists: a spend can have multiple valid witness stacks that keep the txid unchanged while changing the transaction weight and fee rate. An attacker could swap a valid witness for another and make the transaction larger or smaller, which can affect fee policies or mempool acceptance. The satisfier therefore separates non-malleable solutions (stable, unique witnesses) from malleable ones (alternate valid witnesses that can change weight). Use only the non‑malleable set when constructing spends.
const { malleableSats, nonMalleableSats } = satisfier(miniscript);Managing Satisfactions Growth
Some scripts can generate many satisfactions, which can make computation
expensive or even infeasible. To keep this manageable, the satisfier enforces a
default cap of 1000 solutions. The cap counts all derived solutions, including
intermediate combinations and throws once exceeded. You can raise it
or disable it with satisfier(miniscript, { maxSolutions: null});.
However, the recommended way to avoid exponential blow-ups is to prune using
unknowns. Pass unknowns with the pieces of information that are not
available, e.g., <sig(key)> for signatures or preimage markers like
<ripemd160_preimage(H)>.
For example:
const miniscript =
'c:or_i(andor(c:pk_h(key1),pk_h(key2),pk_h(key3)),pk_k(key4))';
const unknowns = ['<sig(key1)>', '<sig(key2)>'];
const { nonMalleableSats, malleableSats } = satisfier(miniscript, { unknowns });produces:
nonMalleableSats: [
{asm: "<sig(key4)> 0"}
{asm: "<sig(key3)> <key3> 0 <key1> 1"}
]and discards:
{
asm: '<sig(key2)> <key2> <sig(key1)> <key1> 1'
}As a reference point, multi(2,key1,...,key20) produces 190 satisfactions and
completes in about 200ms on a laptop, while multi(4,key1,...,key20) yields
4,845 satisfactions and takes around 6 seconds.
If only six signatures are known, pruning yields 15 satisfactions (C(6,4) = 15) and completes almost instantly.
Instead of unknowns, the user has the option to provide the complementary
argument knowns: satisfier(miniscript, { knowns }). This argument
corresponds to the only pieces of information that are known. It's important to
note that either knowns or unknowns must be provided, but not both. If
neither argument is provided, it's assumed that all signatures and preimages are
known.
When modeling adversaries, include in knowns everything an attacker might
also possess, not only your own secrets. If you leave attacker material out of
knowns, you can mistakenly classify a malleable path as safe. A safe pattern
is to include all attacker-accessible material in knowns, then pick a
non-malleable satisfaction that uses only your own material.
For debugging or educational purposes, you can compute the discarded unknown
satisfactions by setting computeUnknowns: true. This populates unknownSats
with the solutions that contain unknown data:
const { nonMalleableSats, unknownSats } = satisfier(miniscript, {
unknowns,
computeUnknowns: true
});
nonMalleableSats: [
{asm: "<sig(key4)> 0"}
{asm: "<sig(key3)> <key3> 0 <key1> 1"}
]
unknownSats: [ {asm: "<sig(key2)> <key2> <sig(key1)> <key1> 1"} ]By default computeUnknowns is disabled to keep the number of solutions manageable, so
unknown satisfactions are pruned and unknownSats is not returned.
Miniscript validity depends on the script context. In tapscript, two key rules change:
MINIMALIFis consensus, so thed:Xwrapper becomes unit.- Multisig uses
multi_a(CHECKSIGADD) instead ofmulti(CHECKMULTISIG).
Because the satisfier runs the static analyzer first, you must pass
tapscript: true when working with tapscript miniscripts:
const miniscript = 'multi_a(2,key1,key2,key3)';
const { nonMalleableSats } = satisfier(miniscript, { tapscript: true });Some scripts are only sane under tapscript. For example,
and_v(v:pk(key1),or_d(d:v:1,pk(key2))) is not sane in SegWit v0 (P2WSH),
but it becomes valid in tapscript because d:v:1 is unit under MINIMALIF.
- Call
analyzeMiniscript(or inspectissanefromcompileMiniscript) to ensure the miniscript is sane before deriving witnesses. - When spending, construct witnesses only from
nonMalleableSats. - Treat
malleableSats(andunknownSatswhen enabled) as diagnostics and never use them for production spends.
Tip: use unknowns/knowns to keep satisfactions tractable, especially for
multi(4,20)‑style scripts (see "Managing Satisfactions Growth"). Enable
computeUnknowns only to inspect discarded solutions.
The project was initially developed and is currently maintained by Jose-Luis Landabaso. Contributions and help from other developers are welcome.
Here are some resources to help you get started with contributing:
To download the source code and build the project, follow these steps:
- Clone the repository:
git clone https://github.com/bitcoinerlab/miniscript.git
- Install the dependencies:
npm install
- Build the project:
npm run build
- Run the test suite:
npm test
This will build the project and generate the necessary files in the dist directory.
To generate the programmers's documentation, which describes the library's programming interface, use the following command:
npm run docs
This will generate the documentation in the docs directory.
Before committing any code, make sure it passes all tests by running:
npm run tests
This project is licensed under the MIT License.