Skip to content

Commit bf45f36

Browse files
committed
WIP: Allow nonWitnessUtxo with segwit
1 parent 27473d7 commit bf45f36

File tree

4 files changed

+236
-51
lines changed

4 files changed

+236
-51
lines changed

src/psbt.js

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -815,13 +815,29 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) {
815815
} else {
816816
script = prevout.script;
817817
}
818-
if (isP2WPKH(script) || isP2WSHScript(script)) {
819-
throw new Error(
820-
`Input #${inputIndex} has nonWitnessUtxo but segwit script: ` +
821-
`${script.toString('hex')}`,
818+
if (isP2WSHScript(script)) {
819+
if (!input.witnessScript)
820+
throw new Error('Segwit input needs witnessScript if not P2WPKH');
821+
checkWitnessScript(inputIndex, script, input.witnessScript);
822+
hash = unsignedTx.hashForWitnessV0(
823+
inputIndex,
824+
input.witnessScript,
825+
prevout.value,
826+
sighashType,
827+
);
828+
script = input.witnessScript;
829+
} else if (isP2WPKH(script)) {
830+
// P2WPKH uses the P2PKH template for prevoutScript when signing
831+
const signingScript = payments.p2pkh({ hash: script.slice(2) }).output;
832+
hash = unsignedTx.hashForWitnessV0(
833+
inputIndex,
834+
signingScript,
835+
prevout.value,
836+
sighashType,
822837
);
838+
} else {
839+
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
823840
}
824-
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
825841
} else if (input.witnessUtxo) {
826842
let _script; // so we don't shadow the `let script` above
827843
if (input.redeemScript) {
@@ -927,34 +943,27 @@ function getScriptFromInput(inputIndex, input, cache) {
927943
isP2SH: false,
928944
isP2WSH: false,
929945
};
930-
if (input.nonWitnessUtxo) {
931-
if (input.redeemScript) {
932-
res.isP2SH = true;
933-
res.script = input.redeemScript;
934-
} else {
946+
res.isP2SH = !!input.redeemScript;
947+
res.isP2WSH = !!input.witnessScript;
948+
if (input.witnessScript) {
949+
res.script = input.witnessScript;
950+
} else if (input.redeemScript) {
951+
res.script = input.redeemScript;
952+
} else {
953+
if (input.nonWitnessUtxo) {
935954
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
936955
cache,
937956
input,
938957
inputIndex,
939958
);
940959
const prevoutIndex = unsignedTx.ins[inputIndex].index;
941960
res.script = nonWitnessUtxoTx.outs[prevoutIndex].script;
961+
} else if (input.witnessUtxo) {
962+
res.script = input.witnessUtxo.script;
942963
}
943-
} else if (input.witnessUtxo) {
964+
}
965+
if (input.witnessScript || isP2WPKH(res.script)) {
944966
res.isSegwit = true;
945-
res.isP2SH = !!input.redeemScript;
946-
res.isP2WSH = !!input.witnessScript;
947-
if (input.witnessScript) {
948-
res.script = input.witnessScript;
949-
} else if (input.redeemScript) {
950-
res.script = payments.p2wpkh({
951-
hash: input.redeemScript.slice(2),
952-
}).output;
953-
} else {
954-
res.script = payments.p2wpkh({
955-
hash: input.witnessUtxo.script.slice(2),
956-
}).output;
957-
}
958967
}
959968
return res;
960969
}

test/fixtures/psbt.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@
513513
{
514514
"type": "P2SH-P2WPKH",
515515
"psbt": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAAiAgKj88rhJwk3Zxm0p0Rp+xC/6cxmj+I741DHPWPWN7iA+0cwRAIgTRhd9WUpoHYl9tUVmoJ336fJAJInIjdYsoatvRiW8hgCIGOYMlpKRHiHA428Sfa2CdAIIGGQCGhuIgIzj2FN6USnAQEEFgAU4sZupXPxqhcsOB1ghJxBvH4XcesAAA==",
516-
"result": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAABBxcWABTixm6lc/GqFyw4HWCEnEG8fhdx6wAA"
516+
"result": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAABBxcWABTixm6lc/GqFyw4HWCEnEG8fhdx6wEIawJHMEQCIE0YXfVlKaB2JfbVFZqCd9+nyQCSJyI3WLKGrb0YlvIYAiBjmDJaSkR4hwONvEn2tgnQCCBhkAhobiICM49hTelEpwEhAqPzyuEnCTdnGbSnRGn7EL/pzGaP4jvjUMc9Y9Y3uID7AAA="
517517
},
518518
{
519519
"type": "P2WPKH",

test/integration/transactions-psbt.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,50 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
282282
});
283283
});
284284

285+
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input with nonWitnessUtxo', async () => {
286+
const p2sh = createPayment('p2sh-p2wpkh');
287+
const inputData = await getInputData(5e4, p2sh.payment, false, 'p2sh');
288+
const inputData2 = await getInputData(5e4, p2sh.payment, false, 'p2sh');
289+
{
290+
const {
291+
hash,
292+
index,
293+
nonWitnessUtxo,
294+
redeemScript,
295+
} = inputData;
296+
assert.deepStrictEqual(
297+
{ hash, index, nonWitnessUtxo, redeemScript },
298+
inputData,
299+
);
300+
}
301+
const keyPair = p2sh.keys[0];
302+
const outputData = {
303+
script: p2sh.payment.output, // sending to myself for fun
304+
value: 2e4,
305+
};
306+
const outputData2 = {
307+
script: p2sh.payment.output, // sending to myself for fun
308+
value: 7e4,
309+
};
310+
311+
const tx = new bitcoin.Psbt()
312+
.addInputs([inputData, inputData2])
313+
.addOutputs([outputData, outputData2])
314+
.signAllInputs(keyPair)
315+
.finalizeAllInputs()
316+
.extractTransaction();
317+
318+
// build and broadcast to the Bitcoin RegTest network
319+
await regtestUtils.broadcast(tx.toHex());
320+
321+
await regtestUtils.verify({
322+
txId: tx.getId(),
323+
address: p2sh.payment.address,
324+
vout: 0,
325+
value: 2e4,
326+
});
327+
});
328+
285329
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => {
286330
// the only thing that changes is you don't give a redeemscript for input data
287331

@@ -316,6 +360,40 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
316360
});
317361
});
318362

363+
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input with nonWitnessUtxo', async () => {
364+
// the only thing that changes is you don't give a redeemscript for input data
365+
366+
const p2wpkh = createPayment('p2wpkh');
367+
const inputData = await getInputData(5e4, p2wpkh.payment, false, 'noredeem');
368+
{
369+
const { hash, index, nonWitnessUtxo } = inputData;
370+
assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData);
371+
}
372+
373+
const psbt = new bitcoin.Psbt({ network: regtest })
374+
.addInput(inputData)
375+
.addOutput({
376+
address: regtestUtils.RANDOM_ADDRESS,
377+
value: 2e4,
378+
})
379+
.signInput(0, p2wpkh.keys[0]);
380+
381+
assert.strictEqual(psbt.validateSignaturesOfInput(0), true);
382+
psbt.finalizeAllInputs();
383+
384+
const tx = psbt.extractTransaction();
385+
386+
// build and broadcast to the Bitcoin RegTest network
387+
await regtestUtils.broadcast(tx.toHex());
388+
389+
await regtestUtils.verify({
390+
txId: tx.getId(),
391+
address: regtestUtils.RANDOM_ADDRESS,
392+
vout: 0,
393+
value: 2e4,
394+
});
395+
});
396+
319397
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => {
320398
const p2wsh = createPayment('p2wsh-p2pk');
321399
const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh');
@@ -356,6 +434,46 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
356434
});
357435
});
358436

437+
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input with nonWitnessUtxo', async () => {
438+
const p2wsh = createPayment('p2wsh-p2pk');
439+
const inputData = await getInputData(5e4, p2wsh.payment, false, 'p2wsh');
440+
{
441+
const {
442+
hash,
443+
index,
444+
nonWitnessUtxo,
445+
witnessScript, // NEW: A Buffer of the witnessScript
446+
} = inputData;
447+
assert.deepStrictEqual(
448+
{ hash, index, nonWitnessUtxo, witnessScript },
449+
inputData,
450+
);
451+
}
452+
453+
const psbt = new bitcoin.Psbt({ network: regtest })
454+
.addInput(inputData)
455+
.addOutput({
456+
address: regtestUtils.RANDOM_ADDRESS,
457+
value: 2e4,
458+
})
459+
.signInput(0, p2wsh.keys[0]);
460+
461+
assert.strictEqual(psbt.validateSignaturesOfInput(0), true);
462+
psbt.finalizeAllInputs();
463+
464+
const tx = psbt.extractTransaction();
465+
466+
// build and broadcast to the Bitcoin RegTest network
467+
await regtestUtils.broadcast(tx.toHex());
468+
469+
await regtestUtils.verify({
470+
txId: tx.getId(),
471+
address: regtestUtils.RANDOM_ADDRESS,
472+
vout: 0,
473+
value: 2e4,
474+
});
475+
});
476+
359477
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => {
360478
const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)');
361479
const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh');
@@ -406,6 +524,56 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
406524
});
407525
});
408526

527+
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input with nonWitnessUtxo', async () => {
528+
const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)');
529+
const inputData = await getInputData(5e4, p2sh.payment, false, 'p2sh-p2wsh');
530+
{
531+
const {
532+
hash,
533+
index,
534+
nonWitnessUtxo,
535+
redeemScript,
536+
witnessScript,
537+
} = inputData;
538+
assert.deepStrictEqual(
539+
{ hash, index, nonWitnessUtxo, redeemScript, witnessScript },
540+
inputData,
541+
);
542+
}
543+
544+
const psbt = new bitcoin.Psbt({ network: regtest })
545+
.addInput(inputData)
546+
.addOutput({
547+
address: regtestUtils.RANDOM_ADDRESS,
548+
value: 2e4,
549+
})
550+
.signInput(0, p2sh.keys[0])
551+
.signInput(0, p2sh.keys[2])
552+
.signInput(0, p2sh.keys[3]);
553+
554+
assert.strictEqual(psbt.validateSignaturesOfInput(0), true);
555+
assert.strictEqual(
556+
psbt.validateSignaturesOfInput(0, p2sh.keys[3].publicKey),
557+
true,
558+
);
559+
assert.throws(() => {
560+
psbt.validateSignaturesOfInput(0, p2sh.keys[1].publicKey);
561+
}, new RegExp('No signatures for this pubkey'));
562+
psbt.finalizeAllInputs();
563+
564+
const tx = psbt.extractTransaction();
565+
566+
// build and broadcast to the Bitcoin RegTest network
567+
await regtestUtils.broadcast(tx.toHex());
568+
569+
await regtestUtils.verify({
570+
txId: tx.getId(),
571+
address: regtestUtils.RANDOM_ADDRESS,
572+
vout: 0,
573+
value: 2e4,
574+
});
575+
});
576+
409577
it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input using HD', async () => {
410578
const hdRoot = bip32.fromSeed(rng(64));
411579
const masterFingerprint = hdRoot.fingerprint;

ts_src/psbt.ts

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ function getHashForSig(
10251025
}
10261026

10271027
const prevoutIndex = unsignedTx.ins[inputIndex].index;
1028-
const prevout = nonWitnessUtxoTx.outs[prevoutIndex];
1028+
const prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output;
10291029

10301030
if (input.redeemScript) {
10311031
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
@@ -1035,14 +1035,29 @@ function getHashForSig(
10351035
script = prevout.script;
10361036
}
10371037

1038-
if (isP2WPKH(script) || isP2WSHScript(script)) {
1039-
throw new Error(
1040-
`Input #${inputIndex} has nonWitnessUtxo but segwit script: ` +
1041-
`${script.toString('hex')}`,
1038+
if (isP2WSHScript(script)) {
1039+
if (!input.witnessScript)
1040+
throw new Error('Segwit input needs witnessScript if not P2WPKH');
1041+
checkWitnessScript(inputIndex, script, input.witnessScript);
1042+
hash = unsignedTx.hashForWitnessV0(
1043+
inputIndex,
1044+
input.witnessScript,
1045+
prevout.value,
1046+
sighashType,
10421047
);
1048+
script = input.witnessScript;
1049+
} else if (isP2WPKH(script)) {
1050+
// P2WPKH uses the P2PKH template for prevoutScript when signing
1051+
const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!;
1052+
hash = unsignedTx.hashForWitnessV0(
1053+
inputIndex,
1054+
signingScript,
1055+
prevout.value,
1056+
sighashType,
1057+
);
1058+
} else {
1059+
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
10431060
}
1044-
1045-
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
10461061
} else if (input.witnessUtxo) {
10471062
let _script: Buffer; // so we don't shadow the `let script` above
10481063
if (input.redeemScript) {
@@ -1165,34 +1180,27 @@ function getScriptFromInput(
11651180
isP2SH: false,
11661181
isP2WSH: false,
11671182
};
1168-
if (input.nonWitnessUtxo) {
1169-
if (input.redeemScript) {
1170-
res.isP2SH = true;
1171-
res.script = input.redeemScript;
1172-
} else {
1183+
res.isP2SH = !!input.redeemScript;
1184+
res.isP2WSH = !!input.witnessScript;
1185+
if (input.witnessScript) {
1186+
res.script = input.witnessScript;
1187+
} else if (input.redeemScript) {
1188+
res.script = input.redeemScript;
1189+
} else {
1190+
if (input.nonWitnessUtxo) {
11731191
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
11741192
cache,
11751193
input,
11761194
inputIndex,
11771195
);
11781196
const prevoutIndex = unsignedTx.ins[inputIndex].index;
11791197
res.script = nonWitnessUtxoTx.outs[prevoutIndex].script;
1198+
} else if (input.witnessUtxo) {
1199+
res.script = input.witnessUtxo.script;
11801200
}
1181-
} else if (input.witnessUtxo) {
1201+
}
1202+
if (input.witnessScript || isP2WPKH(res.script!)) {
11821203
res.isSegwit = true;
1183-
res.isP2SH = !!input.redeemScript;
1184-
res.isP2WSH = !!input.witnessScript;
1185-
if (input.witnessScript) {
1186-
res.script = input.witnessScript;
1187-
} else if (input.redeemScript) {
1188-
res.script = payments.p2wpkh({
1189-
hash: input.redeemScript.slice(2),
1190-
}).output!;
1191-
} else {
1192-
res.script = payments.p2wpkh({
1193-
hash: input.witnessUtxo.script.slice(2),
1194-
}).output!;
1195-
}
11961204
}
11971205
return res;
11981206
}

0 commit comments

Comments
 (0)