Skip to content

Commit

Permalink
API change. The satisfier now returns satisfactions such as this { as…
Browse files Browse the repository at this point in the history
…m, nLockTime, nSequence} instead of { witness, nLockTime, nSequence }
  • Loading branch information
landabaso committed Feb 1, 2023
1 parent a99e853 commit a12cdc8
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 204 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ It includes a novel Miniscript Satisfier for generating explicit script witnesse

- Compile Policies into Miniscript and Bitcoin scripts.
- A Miniscript Satisfier that discards malleable solutions.
- The Miniscript Satisfier is able to generate explicit script witnesses from Miniscripts using variables, such as `pk(key)`.
- The Miniscript Satisfier is able to generate explicit script witnesses from Miniscript expressions using variables, such as `pk(key)`.

For example, Miniscript `and_v(v:pk(key),after(10))` can be satisfied with `[{ witness: '<sig(key)>', nLockTime: 10 }]`.
For example, Miniscript `and_v(v:pk(key),after(10))` can be satisfied with `[{ asm: '<sig(key)>', nLockTime: 10 }]`.

- The ability to generate different satisfactions depending on the presence of `unknowns`.

For example, Miniscript `c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))` can be satisfied with: `[{ witness: '<sig(key2)> <ripemd160_preimage(H)> 0' }]`.
For example, Miniscript `c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))` can be satisfied with: `[{ asm: '<sig(key2)> <ripemd160_preimage(H)> 0' }]`.

However, if `unknowns: ['<ripemd160_preimage(H)>']` is set, then the Miniscript can be satisfied with: `[{ witness: '<sig(key2)> <sig(key1)>' }]` because this solution can no longer be considered malleable, given then assumption that an attacker does not have access to the preimage.
However, if `unknowns: ['<ripemd160_preimage(H)>']` is set, then the Miniscript can be satisfied with: `[{ asm: '<sig(key2)> <sig(key1)>' }]` because this solution can no longer be considered malleable, given then assumption that an attacker does not have access to the preimage.

- Thoroughly tested.

Expand Down Expand Up @@ -80,9 +80,9 @@ In the example above `nonMalleableSats` is:

```javascript
nonMalleableSats: [
{witness: "<sig(key4)> 0"}
{witness: "<sig(key3)> <key3> 0 <key1> 1"}
{witness: "<sig(key2)> <key2> <sig(key1)> <key1> 1"}
{asm: "<sig(key4)> 0"}
{asm: "<sig(key3)> <key3> 0 <key1> 1"}
{asm: "<sig(key2)> <key2> <sig(key1)> <key1> 1"}
]
```

Expand All @@ -107,15 +107,15 @@ When passing `unknowns`, `satisfier` returns an additional object: `{ unknownSat

```javascript
nonMalleableSats: [
{witness: "<sig(key4)> 0"}
{witness: "<sig(key3)> <key3> 0 <key1> 1"}
{asm: "<sig(key4)> 0"}
{asm: "<sig(key3)> <key3> 0 <key1> 1"}
]
unknownSats: [ {witness: "<sig(key2)> <key2> <sig(key1)> <key1> 1"} ]
unknownSats: [ {asm: "<sig(key2)> <key2> <sig(key1)> <key1> 1"} ]
```

The objects returned in the `nonMalleableSats`, `malleableSats` and `unknownSats` arrays consist of the following properties:

- `witness`: a string with the script witness.
- `asm`: a string with the script witness.
- `nSequence`: an integer representing the nSequence value, if needed.
- `nLockTime`: an integer representing the nLockTime value, if needed.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"descriptors"
],
"homepage": "https://bitcoinerlab.com/modules/miniscript",
"version": "1.0.6",
"version": "1.1.0",
"description": "Bitcoin Miniscript, a high-level language for describing Bitcoin spending conditions. It includes a Policy and Miniscript compiler, as well as a novel Satisfier for generating expressive witness scripts.",
"main": "dist/index.js",
"types": "types/index.d.ts",
Expand Down
18 changes: 9 additions & 9 deletions src/satisfier/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { compileMiniscript } from '../miniscript.js';
* @typedef {Object} module:satisfier.Solution
* @property {number} nSequence - the maximum nSequence time of all the sat() or dsat() expressions in the solution.
* @property {number} nLockTime - the maximum nLockTime of all the sat() or dsat() expressions in the solution.
* @property {string} witness - the resulting witness after combining all the sat() or dsat() expressions.
* @property {string} asm - the resulting witness after combining all the sat() or dsat() expressions.
*/

/**
Expand All @@ -22,14 +22,14 @@ import { compileMiniscript } from '../miniscript.js';

/**
* Computes the weight units (WU) of a witness.
* @param {string} witness - the witness to compute the WU of.
* @param {string} asm - the witness to compute the WU of.
* @returns {number} the weight units (WU) of the witness.
*/
function witnessWU(witness) {
function witnessWU(asm) {
// Split the witness string into an array of substrings
// a miniscipt witness is either, <sig..., <sha256..., <hash256...,
// <ripemd160..., <hash160..., <... (for pubkeys) 0 or 1
const substrings = witness.split(' ');
const substrings = asm.split(' ');

// Initialize the sum to 0
let wu = 0;
Expand Down Expand Up @@ -91,7 +91,7 @@ function malleabilityAnalysis(sats) {
//(does not mutate sats)
sat = { ...sat };
//Extract the signatures used in this witness as an array
sat.signatures = sat.witness.split(' ').filter(op => {
sat.signatures = sat.asm.split(' ').filter(op => {
return op.startsWith('<sig');
});
//A non-zero solution without a signature is malleable, and a solution
Expand All @@ -101,13 +101,13 @@ function malleabilityAnalysis(sats) {
}
//<random_preimage()> is a dissatisfaction for a preimage. It is
//interchangable for any 32 bytes random number and thus, it is malleable.
if (sat.witness.includes('<random_preimage()>')) {
if (sat.asm.includes('<random_preimage()>')) {
sat.dontuse = true;
}
return sat;
})
// Sort sats by weight unit in ascending order
.sort((a, b) => witnessWU(a.witness) - witnessWU(b.witness));
.sort((a, b) => witnessWU(a.asm) - witnessWU(b.asm));

for (const sat of sats) {
//For the same nLockTime and nSequence, check if otherSat signatures are a
Expand Down Expand Up @@ -315,8 +315,8 @@ export const satisfier = (miniscript, unknowns = []) => {
if (typeof sat.nSequence === 'undefined') delete sat.nSequence;
if (typeof sat.nLockTime === 'undefined') delete sat.nLockTime;
//Clean format: 1 consecutive spaces at most, no leading & trailing spaces
sat.witness = sat.witness.replace(/ +/g, ' ').trim();
if (unknowns.some(unknown => sat.witness.includes(unknown))) {
sat.asm = sat.asm.replace(/ +/g, ' ').trim();
if (unknowns.some(unknown => sat.asm.includes(unknown))) {
unknownSats.push(sat);
} else {
knownSats.push(sat);
Expand Down
64 changes: 32 additions & 32 deletions src/satisfier/satisfactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import { maxLock, ABSOLUTE, RELATIVE } from './maxLock.js';
* script, they can produce True or False. These solutions are called sats and
* dsats, respectively.
*
* For example, take this solution: `sol = {witness:"sig key"}`.
* For example, take this solution: `sol = {asm:"sig key"}`.
*
* When matched with `DUP HASH160 <HASH160(key)> EQUALVERIFY CHECKSIG` it
* produces `True`, that is, a valid sat solution: `<sig> <key> DUP HASH160 <HASH160(key)> EQUALVERIFY CHECKSIG`.
*
* A solution object can also contain other associated pieces of information: `solution: {witness, nLockTime, nSequence}`:
* A solution object can also contain other associated pieces of information: `solution: {asm, nLockTime, nSequence}`:
* - nLockTime, nSequence: (number/str) interlock attached to this transaction
*
* A `solutionTemplate` describes a solution using sats and dsats of subexpressions.
Expand Down Expand Up @@ -117,15 +117,15 @@ function combine(solutionTemplate, satisfactionsMap) {
postSolution.nLockTime,
ABSOLUTE
),
witness: `${pre}${currSolution.witness}${postSolution.witness}`
asm: `${pre}${currSolution.asm}${postSolution.asm}`
});
}
} else {
//This was the last instance of combine where there are no *post*
//sat/dsats
solutions.push({
...currSolution,
witness: `${pre}${currSolution.witness}${post}`
asm: `${pre}${currSolution.asm}${post}`
});
}
}
Expand All @@ -145,40 +145,40 @@ function combine(solutionTemplate, satisfactionsMap) {
*/
export const satisfactionsMaker = {
0: () => ({
dsats: [{ witness: `` }]
dsats: [{ asm: `` }]
}),
1: () => ({
sats: [{ witness: `` }]
sats: [{ asm: `` }]
}),
pk_k: key => ({
dsats: [{ witness: `0` }],
sats: [{ witness: `<sig(${key})>` }]
dsats: [{ asm: `0` }],
sats: [{ asm: `<sig(${key})>` }]
}),
pk_h: key => ({
dsats: [{ witness: `0 <${key}>` }],
sats: [{ witness: `<sig(${key})> <${key}>` }]
dsats: [{ asm: `0 <${key}>` }],
sats: [{ asm: `<sig(${key})> <${key}>` }]
}),
older: n => ({
sats: [{ witness: ``, nSequence: n }]
sats: [{ asm: ``, nSequence: n }]
}),
after: n => ({
sats: [{ witness: ``, nLockTime: n }]
sats: [{ asm: ``, nLockTime: n }]
}),
sha256: h => ({
sats: [{ witness: `<sha256_preimage(${h})>` }],
dsats: [{ witness: `<random_preimage()>` }]
sats: [{ asm: `<sha256_preimage(${h})>` }],
dsats: [{ asm: `<random_preimage()>` }]
}),
ripemd160: h => ({
sats: [{ witness: `<ripemd160_preimage(${h})>` }],
dsats: [{ witness: `<random_preimage()>` }]
sats: [{ asm: `<ripemd160_preimage(${h})>` }],
dsats: [{ asm: `<random_preimage()>` }]
}),
hash256: h => ({
sats: [{ witness: `<hash256_preimage(${h})>` }],
dsats: [{ witness: `<random_preimage()>` }]
sats: [{ asm: `<hash256_preimage(${h})>` }],
dsats: [{ asm: `<random_preimage()>` }]
}),
hash160: h => ({
sats: [{ witness: `<hash160_preimage(${h})>` }],
dsats: [{ witness: `<random_preimage()>` }]
sats: [{ asm: `<hash160_preimage(${h})>` }],
dsats: [{ asm: `<random_preimage()>` }]
}),
andor: (X, Y, Z) => ({
dsats: [
Expand Down Expand Up @@ -313,7 +313,7 @@ export const satisfactionsMaker = {
//OP_0 is a dummy OP, needed because of a bug in Bitcoin
if (typeof k !== 'number') throw new Error('k must be a number:' + k);
if (k === 0) throw new Error('k cannot be 0');
const dsats = [{ witness: '0 '.repeat(k + 1).trim() }];
const dsats = [{ asm: '0 '.repeat(k + 1).trim() }];

// Create all combinations of k signatures
function keyCombinations(keys, k) {
Expand All @@ -334,21 +334,21 @@ export const satisfactionsMaker = {
return combinations;
}

const witnesses = [];
// Create witnesses from keyCombination
const asms = [];
// Create asms from keyCombination
keyCombinations(keys, k).forEach(combination => {
let witness = '0';
let asm = '0';
combination.forEach(key => {
witness += ` <sig(${key})>`;
asm += ` <sig(${key})>`;
});
witnesses.push(witness);
asms.push(asm);
});

let witness = '0';
let asm = '0';
for (let i = 0; i < k; i++) {
witness += ` <sig(${keys[i]})>`;
asm += ` <sig(${keys[i]})>`;
}
const sats = witnesses.map(witness => ({ witness }));
const sats = asms.map(asm => ({ asm }));

return { sats, dsats };
},
Expand All @@ -365,7 +365,7 @@ export const satisfactionsMaker = {
sats: [...combine(`sat(X)`, { X })]
}),
d: X => ({
dsats: [{ witness: `0` }],
dsats: [{ asm: `0` }],
sats: [...combine(`sat(X) 1`, { X })]
}),
v: X => ({
Expand Down Expand Up @@ -409,13 +409,13 @@ export const satisfactionsMaker = {
const dsats = [];
const sats = [];

dsats.push({ witness: `0` });
dsats.push({ asm: `0` });

//Filter the dsats of X with Non Zero Top Stack (nztp).
const dsats_nzts = X.dsats.filter(
//The top stack corresponds to the last element pushed to the stack,
//that is, the last element in the produced witness
solution => solution.witness.trim().split(' ').pop() !== '0'
solution => solution.asm.trim().split(' ').pop() !== '0'
);
dsats.push(...dsats_nzts);

Expand Down
Loading

0 comments on commit a12cdc8

Please sign in to comment.