Skip to content

Commit

Permalink
Add new parameter knowns for satisfier, which is the complimentary to…
Browse files Browse the repository at this point in the history
… unknowns. API change
  • Loading branch information
landabaso committed Feb 2, 2023
1 parent 421e618 commit 60f5c6d
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 16 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ It includes a novel Miniscript Satisfier for generating explicit script witnesse

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`.
- The ability to generate different satisfactions depending on the presence of `unknowns` (or complimentary `knowns`).

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' }]`.

Expand Down Expand Up @@ -99,7 +99,7 @@ const unknowns = ['<sig(key1)>', '<sig(key2)>'];

const { nonMalleableSats, malleableSats, unknownSats } = satisfier(
miniscript,
unknowns
{ unknowns }
);
```

Expand All @@ -113,6 +113,8 @@ nonMalleableSats: [
unknownSats: [ {asm: "<sig(key2)> <key2> <sig(key1)> <key1> 1"} ]
```

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. For instance, in the example above, `knowns` would be `['<sig(key3)>', '<sig(key4)>']`. 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.

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

- `asm`: a string with the script witness.
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.1.1",
"version": "1.2.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
45 changes: 41 additions & 4 deletions src/satisfier/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ const evaluate = miniscript => {
* solutions. If the miniscript is sane, then unknowns can be set to produce
* more possible solutions, including preimages, as described above.
*
* @param {string[]} knowns - An array with the only pieces of information
* that can be used to build satisfactions. This is the complimentary to
* unknowns. Only `knowns` or `unknowns` must be passed.
*
* If neither knowns and unknowns is passed then it is assumed that there are
* no unknowns, in other words, that all pieces of information are known.
*
* @returns {Object} an object with three keys:
* - `nonMalleableSats`: an array of {@link module:satisfier.Solution} objects
* representing the non-malleable sat() expressions.
Expand All @@ -301,13 +308,24 @@ const evaluate = miniscript => {
* representing the sat() expressions that contain some of the `unknown` pieces of information.
* @see {@link module:satisfier.Solution}
*/
export const satisfier = (miniscript, unknowns = []) => {
export const satisfier = (miniscript, { unknowns, knowns } = {}) => {
const { issane, issanesublevel } = compileMiniscript(miniscript);

if (!issane) {
throw new Error(`Miniscript ${miniscript} is not sane.`);
}

if (typeof unknowns === 'undefined' && typeof knowns === 'undefined') {
unknowns = [];
} else if (typeof unknowns !== 'undefined' && typeof knowns !== 'undefined') {
throw new Error(`Cannot pass both knowns and unknowns`);
} else if (
(knowns && !Array.isArray(knowns)) ||
(unknowns && !Array.isArray(unknowns))
) {
throw new Error(`Incorrect types for unknowns / knowns`);
}

const knownSats = [];
const unknownSats = [];
const sats = evaluate(miniscript).sats || [];
Expand All @@ -316,10 +334,29 @@ export const satisfier = (miniscript, unknowns = []) => {
if (typeof sat.nLockTime === 'undefined') delete sat.nLockTime;
//Clean format: 1 consecutive spaces at most, no leading & trailing spaces
sat.asm = sat.asm.replace(/ +/g, ' ').trim();
if (unknowns.some(unknown => sat.asm.includes(unknown))) {
unknownSats.push(sat);

if (unknowns) {
if (unknowns.some(unknown => sat.asm.includes(unknown))) {
unknownSats.push(sat);
} else {
knownSats.push(sat);
}
} else {
knownSats.push(sat);
const delKnowns = knowns.reduce(
(acc, known) => acc.replace(known, ''),
sat.asm
);
if (
delKnowns.match(
/<sig\(|<sha256_preimage\(|<hash256_preimage\(|<ripemd160_preimage\(|<hash160_preimage\(/
)
) {
//Even thought all known pieces of information are removed, there are
//still other pieces of info needed. Thus, this sat is unkown.
unknownSats.push(sat);
} else {
knownSats.push(sat);
}
}
});

Expand Down
58 changes: 58 additions & 0 deletions test/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,61 @@ export const other = {
malleableSats: []
}
};
export const knowns = {
'with unknowns - and_v(v:pk(k),sha256(H))': {
miniscript: 'and_v(v:pk(k),sha256(H))',
script:
'<k> OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 <H> OP_EQUAL',
unknowns: ['<sha256_preimage(H)>'],
unknownSats: [{ asm: '<sha256_preimage(H)> <sig(k)>' }],
//If the preimage is unknown we cannot compute any satisfaction
nonMalleableSats: [],
malleableSats: []
},
'with knowns - and_v(v:pk(k),sha256(H))': {
miniscript: 'and_v(v:pk(k),sha256(H))',
script:
'<k> OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 <H> OP_EQUAL',
knowns: ['<sig(k)>'],
unknownSats: [{ asm: '<sha256_preimage(H)> <sig(k)>' }],
//If the preimage is unknown we cannot compute any satisfaction
nonMalleableSats: [],
malleableSats: []
},
'with all knowns - and_v(v:pk(k),sha256(H))': {
miniscript: 'and_v(v:pk(k),sha256(H))',
script:
'<k> OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 <H> OP_EQUAL',
knowns: ['<sig(k)>', '<sha256_preimage(H)>'],
unknownSats: [],
//If the preimage is unknown we cannot compute any satisfaction
nonMalleableSats: [{ asm: '<sha256_preimage(H)> <sig(k)>' }],
malleableSats: []
},
'throws with both knowns and unknowns - and_v(v:pk(k),sha256(H))': {
miniscript: 'and_v(v:pk(k),sha256(H))',
script:
'<k> OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 <H> OP_EQUAL',
knowns: ['<sig(k)>'],
unknowns: ['<sha256_preimage(H)>'],
throws: 'Cannot pass both knowns and unknowns'
},
'with unknows set - c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))': {
miniscript: 'c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))',
script:
'<key1> OP_CHECKSIG OP_NOTIF OP_SIZE <20> OP_EQUALVERIFY OP_RIPEMD160 <H> OP_EQUALVERIFY OP_ENDIF <key2> OP_CHECKSIG',
unknowns: ['<ripemd160_preimage(H)>'],
unknownSats: [{ asm: '<sig(key2)> <ripemd160_preimage(H)> 0' }],
nonMalleableSats: [{ asm: '<sig(key2)> <sig(key1)>' }],
malleableSats: []
},
'with knows set - c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))': {
miniscript: 'c:and_v(or_c(pk(key1),v:ripemd160(H)),pk_k(key2))',
script:
'<key1> OP_CHECKSIG OP_NOTIF OP_SIZE <20> OP_EQUALVERIFY OP_RIPEMD160 <H> OP_EQUALVERIFY OP_ENDIF <key2> OP_CHECKSIG',
knowns: ['<sig(key1)>', '<sig(key2)>'],
unknownSats: [{ asm: '<sig(key2)> <ripemd160_preimage(H)> 0' }],
nonMalleableSats: [{ asm: '<sig(key2)> <sig(key1)>' }],
malleableSats: []
}
};
18 changes: 10 additions & 8 deletions test/satisfier.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { primitives, timeLocks, other } from './fixtures.js';
import { primitives, timeLocks, other, knowns } from './fixtures.js';
import { satisfier } from '../src/satisfier/index.js';

const createGroupTest = (description, fixtures) =>
describe(description, () => {
for (const [testName, fixture] of Object.entries(fixtures)) {
const options =
fixture.unknowns || fixture.knowns
? { unknowns: fixture.unknowns, knowns: fixture.knowns }
: undefined;
if (fixture.throws) {
test(testName, () => {
expect(() => satisfier(fixture.miniscript, fixture.unknowns)).toThrow(
expect(() => satisfier(fixture.miniscript, options)).toThrow(
fixture.throws
);
});
} else {
test(testName, () => {
const result = satisfier(fixture.miniscript, fixture.unknowns);
const result = satisfier(fixture.miniscript, options);
expect(result.nonMalleableSats).toEqual(
expect.arrayContaining(fixture.nonMalleableSats)
);
Expand All @@ -21,10 +25,7 @@ const createGroupTest = (description, fixtures) =>
);

const malleableSats = fixture.malleableSats;
const unknownSats =
fixture.unknowns && fixture.unknowns.length
? fixture.unknownSats
: [];
const unknownSats = fixture.unknownSats || [];

expect(result.malleableSats).toEqual(
expect.arrayContaining(malleableSats)
Expand All @@ -39,6 +40,7 @@ const createGroupTest = (description, fixtures) =>
}
});

createGroupTest('Primitives', primitives);
createGroupTest('Timelocks', timeLocks);
createGroupTest('Primitives', primitives);
createGroupTest('Other', other);
createGroupTest('Knowns & unknowns combinations', knowns);
7 changes: 6 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export declare const compilePolicy: (miniscript: string) => {

export declare const satisfier: (
miniscript: string,
unknowns?: string[]
options:
| {
unknowns?: string[] | undefined;
knowns?: string[] | undefined;
}
| undefined
) => {
unknownSats?: Array<{
asm: string;
Expand Down

0 comments on commit 60f5c6d

Please sign in to comment.