Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/cjs/payments/p2ms.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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,
),
);
Expand Down Expand Up @@ -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');
Expand Down
36 changes: 27 additions & 9 deletions src/esm/payments/p2ms.js
Original file line number Diff line number Diff line change
@@ -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
/**
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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,
),
);
Expand Down Expand Up @@ -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');
Expand Down
49 changes: 47 additions & 2 deletions test/fixtures/p2ms.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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": {
Expand Down
62 changes: 62 additions & 0 deletions test/integration/transactions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
36 changes: 27 additions & 9 deletions ts_src/payments/p2ms.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
/**
Expand Down Expand Up @@ -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[];
}

Expand All @@ -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,
),
);
Expand Down Expand Up @@ -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');
Expand Down
Loading