Skip to content

Commit 057cdc5

Browse files
committed
feat: check if taproot signature are present before allowing psbt changes
1 parent e17e2bd commit 057cdc5

File tree

3 files changed

+195
-61
lines changed

3 files changed

+195
-61
lines changed

src/psbt.js

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,41 +1035,81 @@ function checkFees(psbt, cache, opts) {
10351035
}
10361036
}
10371037
function checkInputsForPartialSig(inputs, action) {
1038-
// todo: add for taproot
10391038
inputs.forEach(input => {
1040-
let throws = false;
1041-
let pSigs = [];
1042-
if ((input.partialSig || []).length === 0) {
1043-
if (!input.finalScriptSig && !input.finalScriptWitness) return;
1044-
pSigs = getPsigsFromInputFinalScripts(input);
1045-
} else {
1046-
pSigs = input.partialSig;
1047-
}
1048-
pSigs.forEach(pSig => {
1049-
const { hashType } = bscript.signature.decode(pSig.signature);
1050-
const whitelist = [];
1051-
const isAnyoneCanPay =
1052-
hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY;
1053-
if (isAnyoneCanPay) whitelist.push('addInput');
1054-
const hashMod = hashType & 0x1f;
1055-
switch (hashMod) {
1056-
case transaction_1.Transaction.SIGHASH_ALL:
1057-
break;
1058-
case transaction_1.Transaction.SIGHASH_SINGLE:
1059-
case transaction_1.Transaction.SIGHASH_NONE:
1060-
whitelist.push('addOutput');
1061-
whitelist.push('setInputSequence');
1062-
break;
1063-
}
1064-
if (whitelist.indexOf(action) === -1) {
1065-
throws = true;
1066-
}
1067-
});
1068-
if (throws) {
1039+
const throws = (0, bip371_1.isTaprootInput)(input)
1040+
? checkTaprootInputForSigs(input, action)
1041+
: checkInputForSig(input, action);
1042+
if (throws)
10691043
throw new Error('Can not modify transaction, signatures exist.');
1070-
}
10711044
});
10721045
}
1046+
function checkInputForSig(input, action) {
1047+
const pSigs = extractPartialSigs(input);
1048+
return pSigs.some(pSig =>
1049+
signatureBlocksAction(pSig, bscript.signature.decode, action),
1050+
);
1051+
}
1052+
function checkTaprootInputForSigs(input, action) {
1053+
const sigs = extractTaprootSigs(input);
1054+
return sigs.some(sig =>
1055+
signatureBlocksAction(sig, decodeSchnorSignature, action),
1056+
);
1057+
}
1058+
function decodeSchnorSignature(signature) {
1059+
return {
1060+
signature: signature.slice(0, 64),
1061+
hashType:
1062+
signature.slice(64)[0] || transaction_1.Transaction.SIGHASH_DEFAULT,
1063+
};
1064+
}
1065+
function extractTaprootSigs(input) {
1066+
const sigs = [];
1067+
if (input.tapKeySig) sigs.push(input.tapKeySig);
1068+
if (input.tapScriptSig)
1069+
sigs.push(...input.tapScriptSig.map(s => s.signature));
1070+
if (!sigs.length) {
1071+
const finalTapKeySig = getTapKeySigFromWithness(input.finalScriptWitness);
1072+
if (finalTapKeySig) sigs.push(finalTapKeySig);
1073+
}
1074+
return sigs;
1075+
}
1076+
function getTapKeySigFromWithness(finalScriptWitness) {
1077+
if (!finalScriptWitness) return;
1078+
const witness = finalScriptWitness.slice(2);
1079+
// todo: add schnor signature validation
1080+
if (witness.length === 64 || witness.length === 65) return witness;
1081+
}
1082+
function extractPartialSigs(input) {
1083+
let pSigs = [];
1084+
if ((input.partialSig || []).length === 0) {
1085+
if (!input.finalScriptSig && !input.finalScriptWitness) return [];
1086+
pSigs = getPsigsFromInputFinalScripts(input);
1087+
} else {
1088+
pSigs = input.partialSig;
1089+
}
1090+
return pSigs.map(p => p.signature);
1091+
}
1092+
function signatureBlocksAction(signature, signatureDecodeFn, action) {
1093+
const { hashType } = signatureDecodeFn(signature);
1094+
const whitelist = [];
1095+
const isAnyoneCanPay =
1096+
hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY;
1097+
if (isAnyoneCanPay) whitelist.push('addInput');
1098+
const hashMod = hashType & 0x1f;
1099+
switch (hashMod) {
1100+
case transaction_1.Transaction.SIGHASH_ALL:
1101+
break;
1102+
case transaction_1.Transaction.SIGHASH_SINGLE:
1103+
case transaction_1.Transaction.SIGHASH_NONE:
1104+
whitelist.push('addOutput');
1105+
whitelist.push('setInputSequence');
1106+
break;
1107+
}
1108+
if (whitelist.indexOf(action) === -1) {
1109+
return true;
1110+
}
1111+
return false;
1112+
}
10731113
function checkPartialSigSighashes(input) {
10741114
if (!input.sighashType || !input.partialSig) return;
10751115
const { partialSig, sighashType } = input;

test/fixtures/psbt.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,36 @@
628628
"redeemScript": "Buffer.from('5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae', 'hex')"
629629
},
630630
"exception": "Invalid arguments for Psbt.addOutput. Cannot use both taproot and non-taproot fields."
631+
},
632+
{
633+
"description": "Adds output after tapkey signature has been added",
634+
"isTaproot": true,
635+
"psbt": "cHNidP8BAIcCAAAAAvz6EyMK2VQ0DFKWZyIyGZLZCnaIDtG5RI55OrGVs6u7AAAAAAD/////oB9A2l2NN5WPNjBDaRsz9Nmla1WdM3jl96ElSV5WWHMAAAAAAP////8BkEEGAAAAAAAiUSCnucwjGG2x9zGqP+E0PwxTkptd/9AYMW12ch7ffaogBAAAAAAAAQEroGgGAAAAAAAiUSDbRhL5phuWYLyqKbSqVb/Pic2mpPL46idVKQur9i06ugETQOPidQWL9dHDxd819Pn5QNNGyH0fmDv58pexSJVDalZ0W/HPVliPFZy7s//0TfTbvFce87/nKXcrwz5YuItIY6cBFyAOgJIoTQyYcpAqNfsK9CutgWckFE3mBMgpL50lKXb7oQABAL8CAAAAAcdkwPNWdYy1O8QM1xRkrJjas72QGQPEanYEpv6iT3PXAAAAAGpHMEQCIFpl6Nx8hoZA1o5Huo958RS/Hndy+DloLhp0nvRyBzN8AiB9jEbitThSs6TZ1XVBHZebj4nZbLMg3CCtss8WSZpv7QEhA+VxIxwCWeqoeDMYcEw3EC0ZPxIkOqt0YSK9Y3hAKTd1/////wGgaAYAAAAAABl2qRS9gA7n636cw49cUBJWm7IDJPKuuoisAAAAACICAknGKVC7w5Ek4l3IK5XCUwLNCeU4DD+76cdMAYDB72g3RzBEAiAjekFbBddaHY/LZidwKd3sBbifjmbEpka2Ps0B2Crg+wIgUoN4BhGCwMRTDaHbAX+0MXSGYA9nSlqeQJXpWQRCBfQBAAEFIE6DvlLK0f4BrtpMgez01WQYKLUQZ8HtbfuDPD52hmSvAA==",
636+
"outputData": {
637+
"address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3",
638+
"value": 410000
639+
},
640+
"exception": "Can not modify transaction, signatures exist."
641+
},
642+
{
643+
"description": "Adds output after tapkey spend input has been finalized",
644+
"isTaproot": true,
645+
"psbt": "cHNidP8BAF4CAAAAAfyhtBTm+3OPYBuMHvdPMf9jqniZDY925hbmnt8hxbA6AAAAAAD/////AZBBBgAAAAAAIlEgV6CyzSs5a6Tc8A+TYOiozWnh6FRH9E/VWFanhMAEbLEAAAAAAAEBK6BoBgAAAAAAIlEgV6CyzSs5a6Tc8A+TYOiozWnh6FRH9E/VWFanhMAEbLEBCEIBQOAqWBD/2jhPWzQvesT8sjkN2Cowphp3QvmlWbHiLx753ChcUovvWyBlWiCq77Kk+lZGEhC4vjClSjc26br+dc8AAA==",
646+
"outputData": {
647+
"address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3",
648+
"value": 410000
649+
},
650+
"exception": "Can not modify transaction, signatures exist."
651+
},
652+
{
653+
"description": "Adds output after tapscript signatures have been added",
654+
"isTaproot": true,
655+
"psbt": "cHNidP8BAF4CAAAAAa/0mhnSBXdEBKbbMC+2hm6AZZtCLBxBeubd1sjtau5dAAAAAAD/////AZBBBgAAAAAAIlEgISRIfamb9rCYzad52ikfoUUuFlvyTcImZMavR0jEaUQAAAAAAAEBK6BoBgAAAAAAIlEgISRIfamb9rCYzad52ikfoUUuFlvyTcImZMavR0jEaURBFBnEcOpwiHjNYPtWJOrQ8Pgc9bxBKyZh/i2D837Z1rC8BibL1C4Z/5e6dKzWfkzpsIbE5WEVn1bYpAAjrqIKMHlAKkl3w3Gfpkl9b0yDVbTlZd4yCEL9V2DJs6zpPrEmn3wiohBy8wwE6EZ0FxQdrCupnHKXhHBjpcHVwfJRQfcy9EEULx1ijisiHgGb/9/hBNhsIOv1ZyWsfmi/Ql+oz7AOuqAGJsvULhn/l7p0rNZ+TOmwhsTlYRWfVtikACOuogoweUBKNkxBf6vT8m7ISt1WikLWW9udCP7OQLXztr1IPalJT5z+esAWmgeLS7QoLgzTu8AnYp/rHxsgZ6CgiV8tlkciQRRXE7VxCk67h7Ee6CbSgNyotChx7CgwNTfxdJkyvCS0DgYmy9QuGf+XunSs1n5M6bCGxOVhFZ9W2KQAI66iCjB5QFN6DGtLlSIFBjZbdh3rbKBtBcEDSiEcuVxnSPpdM1RnQRmw5Ujo+/76wZfmGBMFzV0IA7vnHzzXN73jT6O8/wJiFcDBdB6IhNxYUBYgZT1K7FG5SblQ3S6nQMKRLc2vPcA0BhpSnJ+zzX53bWG2IltsYQ6JBvuPqmxZrFw+lbX4LSnWGlKcn7PNfndtYbYiW2xhDokG+4+qbFmsXD6VtfgtKdZpIFcTtXEKTruHsR7oJtKA3Ki0KHHsKDA1N/F0mTK8JLQOrCAvHWKOKyIeAZv/3+EE2Gwg6/VnJax+aL9CX6jPsA66oLogGcRw6nCIeM1g+1Yk6tDw+Bz1vEErJmH+LYPzftnWsLy6U5zAAAA=",
656+
"outputData": {
657+
"address": "bc1px4ssshedlz4jc56ses3lftz462a06jwy8my4pwpx6twx30vvv6nsgwcpu3",
658+
"value": 410000
659+
},
660+
"exception": "Can not modify transaction, signatures exist."
631661
}
632662
]
633663
},

ts_src/psbt.ts

Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,41 +1367,105 @@ function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void {
13671367
}
13681368

13691369
function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void {
1370-
// todo: add for taproot
13711370
inputs.forEach(input => {
1372-
let throws = false;
1373-
let pSigs: PartialSig[] = [];
1374-
if ((input.partialSig || []).length === 0) {
1375-
if (!input.finalScriptSig && !input.finalScriptWitness) return;
1376-
pSigs = getPsigsFromInputFinalScripts(input);
1377-
} else {
1378-
pSigs = input.partialSig!;
1379-
}
1380-
pSigs.forEach(pSig => {
1381-
const { hashType } = bscript.signature.decode(pSig.signature);
1382-
const whitelist: string[] = [];
1383-
const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY;
1384-
if (isAnyoneCanPay) whitelist.push('addInput');
1385-
const hashMod = hashType & 0x1f;
1386-
switch (hashMod) {
1387-
case Transaction.SIGHASH_ALL:
1388-
break;
1389-
case Transaction.SIGHASH_SINGLE:
1390-
case Transaction.SIGHASH_NONE:
1391-
whitelist.push('addOutput');
1392-
whitelist.push('setInputSequence');
1393-
break;
1394-
}
1395-
if (whitelist.indexOf(action) === -1) {
1396-
throws = true;
1397-
}
1398-
});
1399-
if (throws) {
1371+
const throws = isTaprootInput(input)
1372+
? checkTaprootInputForSigs(input, action)
1373+
: checkInputForSig(input, action);
1374+
if (throws)
14001375
throw new Error('Can not modify transaction, signatures exist.');
1401-
}
14021376
});
14031377
}
14041378

1379+
function checkInputForSig(input: PsbtInput, action: string): boolean {
1380+
const pSigs = extractPartialSigs(input);
1381+
return pSigs.some(pSig =>
1382+
signatureBlocksAction(pSig, bscript.signature.decode, action),
1383+
);
1384+
}
1385+
1386+
function checkTaprootInputForSigs(input: PsbtInput, action: string): boolean {
1387+
const sigs = extractTaprootSigs(input);
1388+
return sigs.some(sig =>
1389+
signatureBlocksAction(sig, decodeSchnorSignature, action),
1390+
);
1391+
}
1392+
1393+
function decodeSchnorSignature(
1394+
signature: Buffer,
1395+
): {
1396+
signature: Buffer;
1397+
hashType: number;
1398+
} {
1399+
return {
1400+
signature: signature.slice(0, 64),
1401+
hashType: signature.slice(64)[0] || Transaction.SIGHASH_DEFAULT,
1402+
};
1403+
}
1404+
1405+
function extractTaprootSigs(input: PsbtInput): Buffer[] {
1406+
const sigs: Buffer[] = [];
1407+
if (input.tapKeySig) sigs.push(input.tapKeySig);
1408+
if (input.tapScriptSig)
1409+
sigs.push(...input.tapScriptSig.map(s => s.signature));
1410+
if (!sigs.length) {
1411+
const finalTapKeySig = getTapKeySigFromWithness(input.finalScriptWitness);
1412+
if (finalTapKeySig) sigs.push(finalTapKeySig);
1413+
}
1414+
1415+
return sigs;
1416+
}
1417+
1418+
function getTapKeySigFromWithness(
1419+
finalScriptWitness?: Buffer,
1420+
): Buffer | undefined {
1421+
if (!finalScriptWitness) return;
1422+
const witness = finalScriptWitness.slice(2);
1423+
// todo: add schnor signature validation
1424+
if (witness.length === 64 || witness.length === 65) return witness;
1425+
}
1426+
1427+
function extractPartialSigs(input: PsbtInput): Buffer[] {
1428+
let pSigs: PartialSig[] = [];
1429+
if ((input.partialSig || []).length === 0) {
1430+
if (!input.finalScriptSig && !input.finalScriptWitness) return [];
1431+
pSigs = getPsigsFromInputFinalScripts(input);
1432+
} else {
1433+
pSigs = input.partialSig!;
1434+
}
1435+
return pSigs.map(p => p.signature);
1436+
}
1437+
1438+
type SignatureDecodeFunc = (
1439+
buffer: Buffer,
1440+
) => {
1441+
signature: Buffer;
1442+
hashType: number;
1443+
};
1444+
function signatureBlocksAction(
1445+
signature: Buffer,
1446+
signatureDecodeFn: SignatureDecodeFunc,
1447+
action: string,
1448+
): boolean {
1449+
const { hashType } = signatureDecodeFn(signature);
1450+
const whitelist: string[] = [];
1451+
const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY;
1452+
if (isAnyoneCanPay) whitelist.push('addInput');
1453+
const hashMod = hashType & 0x1f;
1454+
switch (hashMod) {
1455+
case Transaction.SIGHASH_ALL:
1456+
break;
1457+
case Transaction.SIGHASH_SINGLE:
1458+
case Transaction.SIGHASH_NONE:
1459+
whitelist.push('addOutput');
1460+
whitelist.push('setInputSequence');
1461+
break;
1462+
}
1463+
if (whitelist.indexOf(action) === -1) {
1464+
return true;
1465+
}
1466+
return false;
1467+
}
1468+
14051469
function checkPartialSigSighashes(input: PsbtInput): void {
14061470
if (!input.sighashType || !input.partialSig) return;
14071471
const { partialSig, sighashType } = input;

0 commit comments

Comments
 (0)