diff --git a/README.md b/README.md index cbad1ee..438208e 100644 --- a/README.md +++ b/README.md @@ -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: '', 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: ' 0' }]`. @@ -99,7 +99,7 @@ const unknowns = ['', '']; const { nonMalleableSats, malleableSats, unknownSats } = satisfier( miniscript, - unknowns + { unknowns } ); ``` @@ -113,6 +113,8 @@ nonMalleableSats: [ unknownSats: [ {asm: " 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 `['', '']`. 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. diff --git a/package.json b/package.json index 333c42f..71c6700 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/satisfier/index.js b/src/satisfier/index.js index ecb6787..521289a 100644 --- a/src/satisfier/index.js +++ b/src/satisfier/index.js @@ -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. @@ -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 || []; @@ -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( + / OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 OP_EQUAL', + unknowns: [''], + unknownSats: [{ asm: ' ' }], + //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: + ' OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 OP_EQUAL', + knowns: [''], + unknownSats: [{ asm: ' ' }], + //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: + ' OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 OP_EQUAL', + knowns: ['', ''], + unknownSats: [], + //If the preimage is unknown we cannot compute any satisfaction + nonMalleableSats: [{ asm: ' ' }], + malleableSats: [] + }, + 'throws with both knowns and unknowns - and_v(v:pk(k),sha256(H))': { + miniscript: 'and_v(v:pk(k),sha256(H))', + script: + ' OP_CHECKSIGVERIFY OP_SIZE <20> OP_EQUALVERIFY OP_SHA256 OP_EQUAL', + knowns: [''], + unknowns: [''], + 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: + ' OP_CHECKSIG OP_NOTIF OP_SIZE <20> OP_EQUALVERIFY OP_RIPEMD160 OP_EQUALVERIFY OP_ENDIF OP_CHECKSIG', + unknowns: [''], + unknownSats: [{ asm: ' 0' }], + nonMalleableSats: [{ asm: ' ' }], + 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: + ' OP_CHECKSIG OP_NOTIF OP_SIZE <20> OP_EQUALVERIFY OP_RIPEMD160 OP_EQUALVERIFY OP_ENDIF OP_CHECKSIG', + knowns: ['', ''], + unknownSats: [{ asm: ' 0' }], + nonMalleableSats: [{ asm: ' ' }], + malleableSats: [] + } +}; diff --git a/test/satisfier.test.js b/test/satisfier.test.js index 025b340..a3906be 100644 --- a/test/satisfier.test.js +++ b/test/satisfier.test.js @@ -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) ); @@ -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) @@ -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); diff --git a/types/index.d.ts b/types/index.d.ts index 9e9de6d..10309e5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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;