Skip to content

Commit 361ea7c

Browse files
committed
Add inputHasPubkey and outputHasPubkey methods
1 parent 4eb698d commit 361ea7c

File tree

4 files changed

+349
-21
lines changed

4 files changed

+349
-21
lines changed

src/psbt.js

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ class Psbt {
277277
this.data.clearFinalizedInput(inputIndex);
278278
return this;
279279
}
280+
inputHasPubkey(inputIndex, pubkey) {
281+
const input = utils_1.checkForInput(this.data.inputs, inputIndex);
282+
return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE);
283+
}
284+
outputHasPubkey(outputIndex, pubkey) {
285+
const output = utils_1.checkForOutput(this.data.outputs, outputIndex);
286+
return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE);
287+
}
280288
validateSignaturesOfAllInputs() {
281289
utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one
282290
const results = range(this.data.inputs.length).map(idx =>
@@ -653,6 +661,7 @@ const isP2PK = isPaymentFactory(payments.p2pk);
653661
const isP2PKH = isPaymentFactory(payments.p2pkh);
654662
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
655663
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
664+
const isP2SHScript = isPaymentFactory(payments.p2sh);
656665
function check32Bit(num) {
657666
if (
658667
typeof num !== 'number' ||
@@ -723,14 +732,7 @@ function checkPartialSigSighashes(input) {
723732
});
724733
}
725734
function checkScriptForPubkey(pubkey, script, action) {
726-
const pubkeyHash = crypto_1.hash160(pubkey);
727-
const decompiled = bscript.decompile(script);
728-
if (decompiled === null) throw new Error('Unknown script error');
729-
const hasKey = decompiled.some(element => {
730-
if (typeof element === 'number') return false;
731-
return element.equals(pubkey) || element.equals(pubkeyHash);
732-
});
733-
if (!hasKey) {
735+
if (!pubkeyInScript(pubkey, script)) {
734736
throw new Error(
735737
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
736738
);
@@ -1219,6 +1221,88 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) {
12191221
}
12201222
return c[inputIndex];
12211223
}
1224+
function pubkeyInInput(pubkey, input, inputIndex, cache) {
1225+
let script;
1226+
if (input.witnessUtxo !== undefined) {
1227+
script = input.witnessUtxo.script;
1228+
} else if (input.nonWitnessUtxo !== undefined) {
1229+
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
1230+
cache,
1231+
input,
1232+
inputIndex,
1233+
);
1234+
script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
1235+
} else {
1236+
throw new Error("Can't find pubkey in input without Utxo data");
1237+
}
1238+
const meaningfulScript = checkScripts(
1239+
script,
1240+
input.redeemScript,
1241+
input.witnessScript,
1242+
);
1243+
return pubkeyInScript(pubkey, meaningfulScript);
1244+
}
1245+
function pubkeyInOutput(pubkey, output, outputIndex, cache) {
1246+
const script = cache.__TX.outs[outputIndex].script;
1247+
const meaningfulScript = checkScripts(
1248+
script,
1249+
output.redeemScript,
1250+
output.witnessScript,
1251+
);
1252+
return pubkeyInScript(pubkey, meaningfulScript);
1253+
}
1254+
function checkScripts(script, redeemScript, witnessScript) {
1255+
let fail = false;
1256+
if (isP2SHScript(script)) {
1257+
if (redeemScript === undefined) {
1258+
fail = true;
1259+
} else if (isP2WSHScript(redeemScript)) {
1260+
if (witnessScript === undefined) {
1261+
fail = true;
1262+
} else {
1263+
fail = !payments
1264+
.p2sh({
1265+
redeem: payments.p2wsh({
1266+
redeem: { output: witnessScript },
1267+
}),
1268+
})
1269+
.output.equals(script);
1270+
if (!fail) return witnessScript;
1271+
}
1272+
} else {
1273+
fail = !payments
1274+
.p2sh({
1275+
redeem: { output: redeemScript },
1276+
})
1277+
.output.equals(script);
1278+
if (!fail) return redeemScript;
1279+
}
1280+
} else if (isP2WSHScript(script)) {
1281+
if (witnessScript === undefined) {
1282+
fail = true;
1283+
} else {
1284+
fail = !payments
1285+
.p2wsh({
1286+
redeem: { output: witnessScript },
1287+
})
1288+
.output.equals(script);
1289+
if (!fail) return witnessScript;
1290+
}
1291+
}
1292+
if (fail) {
1293+
throw new Error('Incomplete script information');
1294+
}
1295+
return script;
1296+
}
1297+
function pubkeyInScript(pubkey, script) {
1298+
const pubkeyHash = crypto_1.hash160(pubkey);
1299+
const decompiled = bscript.decompile(script);
1300+
if (decompiled === null) throw new Error('Unknown script error');
1301+
return decompiled.some(element => {
1302+
if (typeof element === 'number') return false;
1303+
return element.equals(pubkey) || element.equals(pubkeyHash);
1304+
});
1305+
}
12221306
function classifyScript(script) {
12231307
if (isP2WPKH(script)) return 'witnesspubkeyhash';
12241308
if (isP2PKH(script)) return 'pubkeyhash';

test/psbt.spec.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'assert';
22
import { describe, it } from 'mocha';
33

4-
import { bip32, ECPair, networks as NETWORKS, Psbt } from '..';
4+
import { bip32, ECPair, networks as NETWORKS, Psbt, payments } from '..';
55

66
import * as preFixtures from './fixtures/psbt.json';
77

@@ -542,6 +542,143 @@ describe(`Psbt`, () => {
542542
});
543543
});
544544

545+
describe('inputHasPubkey', () => {
546+
it('should throw', () => {
547+
const psbt = new Psbt();
548+
psbt.addInput({
549+
hash:
550+
'0000000000000000000000000000000000000000000000000000000000000000',
551+
index: 0,
552+
});
553+
554+
assert.throws(() => {
555+
psbt.inputHasPubkey(0, Buffer.from([]));
556+
}, new RegExp("Can't find pubkey in input without Utxo data"));
557+
558+
psbt.updateInput(0, {
559+
witnessUtxo: {
560+
value: 1337,
561+
script: payments.p2sh({
562+
redeem: { output: Buffer.from([0x51]) },
563+
}).output!,
564+
},
565+
});
566+
567+
assert.throws(() => {
568+
psbt.inputHasPubkey(0, Buffer.from([]));
569+
}, new RegExp('Incomplete script information'));
570+
571+
delete psbt.data.inputs[0].witnessUtxo;
572+
573+
psbt.updateInput(0, {
574+
witnessUtxo: {
575+
value: 1337,
576+
script: payments.p2wsh({
577+
redeem: { output: Buffer.from([0x51]) },
578+
}).output!,
579+
},
580+
});
581+
582+
assert.throws(() => {
583+
psbt.inputHasPubkey(0, Buffer.from([]));
584+
}, new RegExp('Incomplete script information'));
585+
586+
delete psbt.data.inputs[0].witnessUtxo;
587+
588+
psbt.updateInput(0, {
589+
witnessUtxo: {
590+
value: 1337,
591+
script: payments.p2sh({
592+
redeem: payments.p2wsh({
593+
redeem: { output: Buffer.from([0x51]) },
594+
}),
595+
}).output!,
596+
},
597+
redeemScript: payments.p2wsh({
598+
redeem: { output: Buffer.from([0x51]) },
599+
}).output!,
600+
});
601+
602+
assert.throws(() => {
603+
psbt.inputHasPubkey(0, Buffer.from([]));
604+
}, new RegExp('Incomplete script information'));
605+
606+
psbt.updateInput(0, {
607+
witnessScript: Buffer.from([0x51]),
608+
});
609+
610+
assert.doesNotThrow(() => {
611+
psbt.inputHasPubkey(0, Buffer.from([0x51]));
612+
});
613+
});
614+
});
615+
616+
describe('outputHasPubkey', () => {
617+
it('should throw', () => {
618+
const psbt = new Psbt();
619+
psbt
620+
.addInput({
621+
hash:
622+
'0000000000000000000000000000000000000000000000000000000000000000',
623+
index: 0,
624+
})
625+
.addOutput({
626+
script: payments.p2sh({
627+
redeem: { output: Buffer.from([0x51]) },
628+
}).output!,
629+
value: 1337,
630+
});
631+
632+
assert.throws(() => {
633+
psbt.outputHasPubkey(0, Buffer.from([]));
634+
}, new RegExp('Incomplete script information'));
635+
636+
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2wsh({
637+
redeem: { output: Buffer.from([0x51]) },
638+
}).output!;
639+
640+
assert.throws(() => {
641+
psbt.outputHasPubkey(0, Buffer.from([]));
642+
}, new RegExp('Incomplete script information'));
643+
644+
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2sh({
645+
redeem: payments.p2wsh({
646+
redeem: { output: Buffer.from([0x51]) },
647+
}),
648+
}).output!;
649+
650+
psbt.updateOutput(0, {
651+
redeemScript: payments.p2wsh({
652+
redeem: { output: Buffer.from([0x51]) },
653+
}).output!,
654+
});
655+
656+
assert.throws(() => {
657+
psbt.outputHasPubkey(0, Buffer.from([]));
658+
}, new RegExp('Incomplete script information'));
659+
660+
delete psbt.data.outputs[0].redeemScript;
661+
662+
psbt.updateOutput(0, {
663+
witnessScript: Buffer.from([0x51]),
664+
});
665+
666+
assert.throws(() => {
667+
psbt.outputHasPubkey(0, Buffer.from([]));
668+
}, new RegExp('Incomplete script information'));
669+
670+
psbt.updateOutput(0, {
671+
redeemScript: payments.p2wsh({
672+
redeem: { output: Buffer.from([0x51]) },
673+
}).output!,
674+
});
675+
676+
assert.doesNotThrow(() => {
677+
psbt.outputHasPubkey(0, Buffer.from([0x51]));
678+
});
679+
});
680+
});
681+
545682
describe('clone', () => {
546683
it('Should clone a psbt exactly with no reference', () => {
547684
const f = fixtures.clone;
@@ -643,6 +780,8 @@ describe(`Psbt`, () => {
643780
assert.throws(() => {
644781
psbt.setVersion(3);
645782
}, new RegExp('Can not modify transaction, signatures exist.'));
783+
assert.strictEqual(psbt.inputHasPubkey(0, alice.publicKey), true);
784+
assert.strictEqual(psbt.outputHasPubkey(0, alice.publicKey), false);
646785
assert.strictEqual(
647786
psbt.extractTransaction().toHex(),
648787
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +

0 commit comments

Comments
 (0)