diff --git a/src/cjs/payments/p2ms.cjs b/src/cjs/payments/p2ms.cjs index b84158af3..9a40c7077 100644 --- a/src/cjs/payments/p2ms.cjs +++ b/src/cjs/payments/p2ms.cjs @@ -47,11 +47,28 @@ Object.defineProperty(exports, '__esModule', { value: true }); exports.p2ms = p2ms; const networks_js_1 = require('../networks.cjs'); const bscript = __importStar(require('../script.cjs')); +const scriptNumber = __importStar(require('../script_number.cjs')); const types_js_1 = require('../types.cjs'); const lazy = __importStar(require('./lazy.cjs')); const v = __importStar(require('valibot')); const OPS = bscript.OPS; const OP_INT_BASE = OPS.OP_RESERVED; // OP_1 - 1 +function encodeSmallOrScriptNum(n) { + return n <= 16 ? OP_INT_BASE + n : scriptNumber.encode(n); +} +function decodeSmallOrScriptNum(chunk) { + if (typeof chunk === 'number') { + const val = chunk - OP_INT_BASE; + if (val < 1 || val > 16) + throw new TypeError(`Invalid opcode: expected OP_1–OP_16, got ${chunk}`); + return val; + } else return scriptNumber.decode(chunk); +} +function isSmallOrScriptNum(chunk) { + if (typeof chunk === 'number') + return chunk - OP_INT_BASE >= 1 && chunk - OP_INT_BASE <= 16; + else return Number.isInteger(scriptNumber.decode(chunk)); +} // input: OP_0 [signatures ...] // output: m [pubKeys ...] n OP_CHECKMULTISIG /** @@ -104,8 +121,9 @@ function p2ms(a, opts) { if (decoded) return; decoded = true; chunks = bscript.decompile(output); - o.m = chunks[0] - OP_INT_BASE; - o.n = chunks[chunks.length - 2] - OP_INT_BASE; + if (chunks.length < 3) throw new TypeError('Output is invalid'); + o.m = decodeSmallOrScriptNum(chunks[0]); + o.n = decodeSmallOrScriptNum(chunks[chunks.length - 2]); o.pubkeys = chunks.slice(1, -2); } lazy.prop(o, 'output', () => { @@ -114,9 +132,9 @@ function p2ms(a, opts) { if (!a.pubkeys) return; return bscript.compile( [].concat( - OP_INT_BASE + a.m, + encodeSmallOrScriptNum(a.m), a.pubkeys, - OP_INT_BASE + o.n, + encodeSmallOrScriptNum(o.n), OPS.OP_CHECKMULTISIG, ), ); @@ -155,13 +173,13 @@ function p2ms(a, opts) { if (opts.validate) { if (a.output) { decode(a.output); - v.parse(v.number(), chunks[0], { message: 'Output is invalid' }); - v.parse(v.number(), chunks[chunks.length - 2], { - message: 'Output is invalid', - }); + if (!isSmallOrScriptNum(chunks[0])) + throw new TypeError('Output is invalid'); + if (!isSmallOrScriptNum(chunks[chunks.length - 2])) + throw new TypeError('Output is invalid'); if (chunks[chunks.length - 1] !== OPS.OP_CHECKMULTISIG) throw new TypeError('Output is invalid'); - if (o.m <= 0 || o.n > 16 || o.m > o.n || o.n !== chunks.length - 3) + if (o.m <= 0 || o.n > 20 || o.m > o.n || o.n !== chunks.length - 3) throw new TypeError('Output is invalid'); if (!o.pubkeys.every(x => (0, types_js_1.isPoint)(x))) throw new TypeError('Output is invalid'); diff --git a/src/esm/payments/p2ms.js b/src/esm/payments/p2ms.js index 200ee1bca..2d82e510e 100644 --- a/src/esm/payments/p2ms.js +++ b/src/esm/payments/p2ms.js @@ -1,10 +1,27 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks.js'; import * as bscript from '../script.js'; +import * as scriptNumber from '../script_number.js'; import { BufferSchema, isPoint, stacksEqual } from '../types.js'; import * as lazy from './lazy.js'; import * as v from 'valibot'; const OPS = bscript.OPS; const OP_INT_BASE = OPS.OP_RESERVED; // OP_1 - 1 +function encodeSmallOrScriptNum(n) { + return n <= 16 ? OP_INT_BASE + n : scriptNumber.encode(n); +} +function decodeSmallOrScriptNum(chunk) { + if (typeof chunk === 'number') { + const val = chunk - OP_INT_BASE; + if (val < 1 || val > 16) + throw new TypeError(`Invalid opcode: expected OP_1–OP_16, got ${chunk}`); + return val; + } else return scriptNumber.decode(chunk); +} +function isSmallOrScriptNum(chunk) { + if (typeof chunk === 'number') + return chunk - OP_INT_BASE >= 1 && chunk - OP_INT_BASE <= 16; + else return Number.isInteger(scriptNumber.decode(chunk)); +} // input: OP_0 [signatures ...] // output: m [pubKeys ...] n OP_CHECKMULTISIG /** @@ -54,8 +71,9 @@ export function p2ms(a, opts) { if (decoded) return; decoded = true; chunks = bscript.decompile(output); - o.m = chunks[0] - OP_INT_BASE; - o.n = chunks[chunks.length - 2] - OP_INT_BASE; + if (chunks.length < 3) throw new TypeError('Output is invalid'); + o.m = decodeSmallOrScriptNum(chunks[0]); + o.n = decodeSmallOrScriptNum(chunks[chunks.length - 2]); o.pubkeys = chunks.slice(1, -2); } lazy.prop(o, 'output', () => { @@ -64,9 +82,9 @@ export function p2ms(a, opts) { if (!a.pubkeys) return; return bscript.compile( [].concat( - OP_INT_BASE + a.m, + encodeSmallOrScriptNum(a.m), a.pubkeys, - OP_INT_BASE + o.n, + encodeSmallOrScriptNum(o.n), OPS.OP_CHECKMULTISIG, ), ); @@ -105,13 +123,13 @@ export function p2ms(a, opts) { if (opts.validate) { if (a.output) { decode(a.output); - v.parse(v.number(), chunks[0], { message: 'Output is invalid' }); - v.parse(v.number(), chunks[chunks.length - 2], { - message: 'Output is invalid', - }); + if (!isSmallOrScriptNum(chunks[0])) + throw new TypeError('Output is invalid'); + if (!isSmallOrScriptNum(chunks[chunks.length - 2])) + throw new TypeError('Output is invalid'); if (chunks[chunks.length - 1] !== OPS.OP_CHECKMULTISIG) throw new TypeError('Output is invalid'); - if (o.m <= 0 || o.n > 16 || o.m > o.n || o.n !== chunks.length - 3) + if (o.m <= 0 || o.n > 20 || o.m > o.n || o.n !== chunks.length - 3) throw new TypeError('Output is invalid'); if (!o.pubkeys.every(x => isPoint(x))) throw new TypeError('Output is invalid'); diff --git a/test/fixtures/p2ms.json b/test/fixtures/p2ms.json index f84b4d5db..fe9d0d6bb 100644 --- a/test/fixtures/p2ms.json +++ b/test/fixtures/p2ms.json @@ -180,6 +180,44 @@ "input": "OP_0 OP_0 300602010102010001", "witness": [] } + }, + { + "description": "output from output (20-of-20 multisig)", + "arguments": { + "output": "14 0255355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116 03e3e592638b492e642f8c389f9577b0809d4f73032c4c0f9981cb57cb3eebbe4c 02b0a4e912141c3b1044cdc13f196ff95c916f05f43d04184b7dcefa6977fac24a 02958361ee738c994b5e799c13c964602915eaa847ed7e5a5a3f8c42312cd39a61 0227654f4d0ddea28183970c7532692fadf8dd042e31a51c5936f85487c5a1ec02 02c38b046055858679daf9468ac44c991cce4bf91f9f8f4eab6ea7f9d2041e499f 0335676ec077b748a253dd92a1ca9387533818e511741281ebc96d61eccd86cf39 03165f2a7bbd0789c795f66ca0c383d963fa17bf4289d9faae8b8b8f098b3e669f 032c3263ced2ce21ac62ff8828f67cf12a4cc3cb93edfd432ea4a1cba2d533bae8 0304f4f8a3039ab91a1bbe211a1e16b80b549d9feffb4b83cd9e4d43d7e55964d1 03e3dfe07b7c83bdd7908795f890ba8de2117fc3303d048edd54a72de1183ed737 02ef28db8ca852fd8f871835d31a600728cf1f76744dc6b22c9d36394d146a04e5 03ce600e3f61f8b72de1715654010fa54da7a12d39b8d8cb9969f160888eaa2e0e 02df6d74c70b197cf0fc65216ab5ce25b120338a02049a45907f6e54b2e7c779b8 029f5f53b28673bb834c082f3ccd4e73c1a2099368fe7b8567cb817b7675531e26 023ccd807197e3af4139ad4647a0350bef5829f41651729defac3863e964cd3cb1 02482d77f0fb886bb23d9c431960933499982f6ecaf45f2e279203dbe642aca03b 02312b2cac8fb58150596ce11f7db6a50775d257fb8f9bdd1a3e129eef10ea182c 038c6bd3d819d30aa07cb52a2ca4aaaaf83e63bc9947a9e0230abe5233af1c12dd 038e3b9db1442165010102596f30536020e451b6e645ef1fb0c21cb965f84e3eee 14 OP_CHECKMULTISIG" + }, + "options": {}, + "expected": { + "m": 20, + "n": 20, + "name": "p2ms(20 of 20)", + "output": "14 0255355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116 03e3e592638b492e642f8c389f9577b0809d4f73032c4c0f9981cb57cb3eebbe4c 02b0a4e912141c3b1044cdc13f196ff95c916f05f43d04184b7dcefa6977fac24a 02958361ee738c994b5e799c13c964602915eaa847ed7e5a5a3f8c42312cd39a61 0227654f4d0ddea28183970c7532692fadf8dd042e31a51c5936f85487c5a1ec02 02c38b046055858679daf9468ac44c991cce4bf91f9f8f4eab6ea7f9d2041e499f 0335676ec077b748a253dd92a1ca9387533818e511741281ebc96d61eccd86cf39 03165f2a7bbd0789c795f66ca0c383d963fa17bf4289d9faae8b8b8f098b3e669f 032c3263ced2ce21ac62ff8828f67cf12a4cc3cb93edfd432ea4a1cba2d533bae8 0304f4f8a3039ab91a1bbe211a1e16b80b549d9feffb4b83cd9e4d43d7e55964d1 03e3dfe07b7c83bdd7908795f890ba8de2117fc3303d048edd54a72de1183ed737 02ef28db8ca852fd8f871835d31a600728cf1f76744dc6b22c9d36394d146a04e5 03ce600e3f61f8b72de1715654010fa54da7a12d39b8d8cb9969f160888eaa2e0e 02df6d74c70b197cf0fc65216ab5ce25b120338a02049a45907f6e54b2e7c779b8 029f5f53b28673bb834c082f3ccd4e73c1a2099368fe7b8567cb817b7675531e26 023ccd807197e3af4139ad4647a0350bef5829f41651729defac3863e964cd3cb1 02482d77f0fb886bb23d9c431960933499982f6ecaf45f2e279203dbe642aca03b 02312b2cac8fb58150596ce11f7db6a50775d257fb8f9bdd1a3e129eef10ea182c 038c6bd3d819d30aa07cb52a2ca4aaaaf83e63bc9947a9e0230abe5233af1c12dd 038e3b9db1442165010102596f30536020e451b6e645ef1fb0c21cb965f84e3eee 14 OP_CHECKMULTISIG", + "pubkeys": [ + "0255355ca83c973f1d97ce0e3843c85d78905af16b4dc531bc488e57212d230116", + "03e3e592638b492e642f8c389f9577b0809d4f73032c4c0f9981cb57cb3eebbe4c", + "02b0a4e912141c3b1044cdc13f196ff95c916f05f43d04184b7dcefa6977fac24a", + "02958361ee738c994b5e799c13c964602915eaa847ed7e5a5a3f8c42312cd39a61", + "0227654f4d0ddea28183970c7532692fadf8dd042e31a51c5936f85487c5a1ec02", + "02c38b046055858679daf9468ac44c991cce4bf91f9f8f4eab6ea7f9d2041e499f", + "0335676ec077b748a253dd92a1ca9387533818e511741281ebc96d61eccd86cf39", + "03165f2a7bbd0789c795f66ca0c383d963fa17bf4289d9faae8b8b8f098b3e669f", + "032c3263ced2ce21ac62ff8828f67cf12a4cc3cb93edfd432ea4a1cba2d533bae8", + "0304f4f8a3039ab91a1bbe211a1e16b80b549d9feffb4b83cd9e4d43d7e55964d1", + "03e3dfe07b7c83bdd7908795f890ba8de2117fc3303d048edd54a72de1183ed737", + "02ef28db8ca852fd8f871835d31a600728cf1f76744dc6b22c9d36394d146a04e5", + "03ce600e3f61f8b72de1715654010fa54da7a12d39b8d8cb9969f160888eaa2e0e", + "02df6d74c70b197cf0fc65216ab5ce25b120338a02049a45907f6e54b2e7c779b8", + "029f5f53b28673bb834c082f3ccd4e73c1a2099368fe7b8567cb817b7675531e26", + "023ccd807197e3af4139ad4647a0350bef5829f41651729defac3863e964cd3cb1", + "02482d77f0fb886bb23d9c431960933499982f6ecaf45f2e279203dbe642aca03b", + "02312b2cac8fb58150596ce11f7db6a50775d257fb8f9bdd1a3e129eef10ea182c", + "038c6bd3d819d30aa07cb52a2ca4aaaaf83e63bc9947a9e0230abe5233af1c12dd", + "038e3b9db1442165010102596f30536020e451b6e645ef1fb0c21cb965f84e3eee" + ], + "signatures": null, + "input": null, + "witness": null + } } ], "invalid": [ @@ -225,14 +263,14 @@ }, { "description": "m is 0", - "exception": "Output is invalid", + "exception": "Invalid opcode: expected OP_1–OP_16, got 0", "arguments": { "output": "OP_0 OP_2 OP_CHECKMULTISIG" } }, { "description": "n is 0 (m > n)", - "exception": "Output is invalid", + "exception": "Invalid opcode: expected OP_1–OP_16, got 0", "arguments": { "output": "OP_2 OP_0 OP_CHECKMULTISIG" } @@ -368,6 +406,13 @@ ], "input": "OP_0 ffffffffffffffff" } + }, + { + "description": "n > 20 (2-of-21 multisig)", + "exception": "Output is invalid", + "arguments": { + "output": "OP_2 020000000000000000000000000000000000000000000000000000000000000001 020000000000000000000000000000000000000000000000000000000000000002 020000000000000000000000000000000000000000000000000000000000000003 020000000000000000000000000000000000000000000000000000000000000004 020000000000000000000000000000000000000000000000000000000000000005 020000000000000000000000000000000000000000000000000000000000000006 020000000000000000000000000000000000000000000000000000000000000007 020000000000000000000000000000000000000000000000000000000000000008 020000000000000000000000000000000000000000000000000000000000000009 02000000000000000000000000000000000000000000000000000000000000000a 02000000000000000000000000000000000000000000000000000000000000000b 02000000000000000000000000000000000000000000000000000000000000000c 02000000000000000000000000000000000000000000000000000000000000000d 02000000000000000000000000000000000000000000000000000000000000000e 02000000000000000000000000000000000000000000000000000000000000000f 020000000000000000000000000000000000000000000000000000000000000010 020000000000000000000000000000000000000000000000000000000000000011 020000000000000000000000000000000000000000000000000000000000000012 020000000000000000000000000000000000000000000000000000000000000013 020000000000000000000000000000000000000000000000000000000000000014 15 OP_CHECKMULTISIG" + } } ], "dynamic": { diff --git a/test/integration/transactions.spec.ts b/test/integration/transactions.spec.ts index 994af186b..bf2e64ef7 100644 --- a/test/integration/transactions.spec.ts +++ b/test/integration/transactions.spec.ts @@ -451,6 +451,68 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }); }); + it( + 'can create (and broadcast via 3PBP) a Transaction, w/ a ' + + 'P2WSH(P2MS(20 of 20)) input', + async () => { + const keys = []; + for (let i = 0; i < 20; i++) { + keys.push(ECPair.makeRandom({ network: regtest, rng })); + } + + const multisig = createPayment('p2wsh-p2ms(20 of 20)', keys); + + const inputData = await getInputData( + 5e5, + multisig.payment, + true, + 'p2wsh', + ); + { + const { hash, index, witnessUtxo, witnessScript } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, witnessScript }, + inputData, + ); + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: BigInt(3e5), + }); + + for (let i = 0; i < 20; i++) { + psbt.signInput(0, multisig.keys[i]); + } + + for (let i = 0; i < 20; i++) { + assert.strictEqual( + psbt.validateSignaturesOfInput( + 0, + validator, + multisig.keys[i].publicKey, + ), + true, + ); + } + + psbt.finalizeAllInputs(); + + const tx = psbt.extractTransaction(); + + await regtestUtils.broadcast(tx.toHex()); + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 3e5, + }); + }, + ); + it( 'can create (and broadcast via 3PBP) a Transaction, w/ a ' + 'P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', diff --git a/ts_src/payments/p2ms.ts b/ts_src/payments/p2ms.ts index a40d7c35b..862448a6a 100644 --- a/ts_src/payments/p2ms.ts +++ b/ts_src/payments/p2ms.ts @@ -1,5 +1,6 @@ import { bitcoin as BITCOIN_NETWORK } from '../networks.js'; import * as bscript from '../script.js'; +import * as scriptNumber from '../script_number.js'; import { BufferSchema, isPoint, stacksEqual } from '../types.js'; import { Payment, PaymentOpts, Stack } from './index.js'; import * as lazy from './lazy.js'; @@ -8,6 +9,22 @@ const OPS = bscript.OPS; const OP_INT_BASE = OPS.OP_RESERVED; // OP_1 - 1 +function encodeSmallOrScriptNum(n: number): number | Uint8Array { + return n <= 16 ? OP_INT_BASE + n : scriptNumber.encode(n); +} +function decodeSmallOrScriptNum(chunk: number | Uint8Array): number { + if (typeof chunk === 'number') { + const val = chunk - OP_INT_BASE; + if (val < 1 || val > 16) + throw new TypeError(`Invalid opcode: expected OP_1–OP_16, got ${chunk}`); + return val; + } else return scriptNumber.decode(chunk); +} +function isSmallOrScriptNum(chunk: number | Uint8Array): boolean { + if (typeof chunk === 'number') + return chunk - OP_INT_BASE >= 1 && chunk - OP_INT_BASE <= 16; + else return Number.isInteger(scriptNumber.decode(chunk)); +} // input: OP_0 [signatures ...] // output: m [pubKeys ...] n OP_CHECKMULTISIG /** @@ -65,8 +82,9 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { if (decoded) return; decoded = true; chunks = bscript.decompile(output) as Stack; - o.m = (chunks[0] as number) - OP_INT_BASE; - o.n = (chunks[chunks.length - 2] as number) - OP_INT_BASE; + if (chunks.length < 3) throw new TypeError('Output is invalid'); + o.m = decodeSmallOrScriptNum(chunks[0]); + o.n = decodeSmallOrScriptNum(chunks[chunks.length - 2]); o.pubkeys = chunks.slice(1, -2) as Uint8Array[]; } @@ -76,9 +94,9 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { if (!a.pubkeys) return; return bscript.compile( ([] as Stack).concat( - OP_INT_BASE + a.m, + encodeSmallOrScriptNum(a.m), a.pubkeys, - OP_INT_BASE + o.n, + encodeSmallOrScriptNum(o.n), OPS.OP_CHECKMULTISIG, ), ); @@ -118,14 +136,14 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { if (opts.validate) { if (a.output) { decode(a.output); - v.parse(v.number(), chunks[0], { message: 'Output is invalid' }); - v.parse(v.number(), chunks[chunks.length - 2], { - message: 'Output is invalid', - }); + if (!isSmallOrScriptNum(chunks[0])) + throw new TypeError('Output is invalid'); + if (!isSmallOrScriptNum(chunks[chunks.length - 2])) + throw new TypeError('Output is invalid'); if (chunks[chunks.length - 1] !== OPS.OP_CHECKMULTISIG) throw new TypeError('Output is invalid'); - if (o.m! <= 0 || o.n! > 16 || o.m! > o.n! || o.n !== chunks.length - 3) + if (o.m! <= 0 || o.n! > 20 || o.m! > o.n! || o.n !== chunks.length - 3) throw new TypeError('Output is invalid'); if (!o.pubkeys!.every(x => isPoint(x))) throw new TypeError('Output is invalid');