From 2d8d57a01c9d785ecfb7d435486db5984810c431 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Mon, 13 Oct 2025 13:32:11 -0700 Subject: [PATCH 1/4] Removed unnecessary check for tapInternalKey for signature validations of taproot inputs --- src/cjs/psbt.cjs | 69 ++++++++++++++++++++++++++++++++++++++------ src/esm/psbt.js | 68 +++++++++++++++++++++++++++++++++++++------ ts_src/psbt.ts | 75 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 186 insertions(+), 26 deletions(-) diff --git a/src/cjs/psbt.cjs b/src/cjs/psbt.cjs index c6ccbf07d..6e3a5a82d 100644 --- a/src/cjs/psbt.cjs +++ b/src/cjs/psbt.cjs @@ -516,14 +516,14 @@ class Psbt { throw new Error('Need validator function to validate signatures'); pubkey = pubkey && (0, bip371_js_1.toXOnly)(pubkey); const allHashses = pubkey - ? getTaprootHashesForSig( + ? getTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, pubkey, this.__CACHE, ) - : getAllTaprootHashesForSig( + : getAllTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, @@ -900,7 +900,7 @@ class Psbt { throw new Error( `Need Schnorr Signer to sign taproot input #${inputIndex}.`, ); - const hashesForSig = getTaprootHashesForSig( + const hashesForSig = getTaprootHashesForSigning( inputIndex, input, this.data.inputs, @@ -1348,7 +1348,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { hash, }; } -function getAllTaprootHashesForSig(inputIndex, input, inputs, cache) { +function getAllTaprootHashesForSigValidation(inputIndex, input, inputs, cache) { const allPublicKeys = []; if (input.tapInternalKey) { const key = getPrevoutTaprootKey(inputIndex, input, cache); @@ -1361,7 +1361,13 @@ function getAllTaprootHashesForSig(inputIndex, input, inputs, cache) { allPublicKeys.push(...tapScriptPubkeys); } const allHashes = allPublicKeys.map(publicKey => - getTaprootHashesForSig(inputIndex, input, inputs, publicKey, cache), + getTaprootHashesForSigValidation( + inputIndex, + input, + inputs, + publicKey, + cache, + ), ); return allHashes.flat(); } @@ -1372,7 +1378,7 @@ function getPrevoutTaprootKey(inputIndex, input, cache) { function trimTaprootSig(signature) { return signature.length === 64 ? signature : signature.subarray(0, 64); } -function getTaprootHashesForSig( +function getTaprootHashesForSigning( inputIndex, input, inputs, @@ -1381,17 +1387,64 @@ function getTaprootHashesForSig( tapLeafHashToSign, allowedSighashTypes, ) { - const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_js_1.Transaction.SIGHASH_DEFAULT; checkSighashTypeAllowed(sighashType, allowedSighashTypes); + const keySpend = Boolean(input.tapInternalKey && !tapLeafHashToSign); + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + tapLeafHashToSign, + ); +} +function getTaprootHashesForSigValidation( + inputIndex, + input, + inputs, + pubkey, + cache, +) { + const sighashType = + input.sighashType || transaction_js_1.Transaction.SIGHASH_DEFAULT; + const keySpend = Boolean(input.tapKeySig); + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + ); +} +/* + * This helper method is used for both generating a hash for signing as well for validating; + * thus depending on context key spend can be detected either with tapInternalKey with no leaves + * or tapKeySig (note tapKeySig is a signature to the prevout pk, so tapInternalKey is not needed). + */ +function getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + tapLeafHashToSign, +) { + const unsignedTx = cache.__TX; const prevOuts = inputs.map((i, index) => getScriptAndAmountFromUtxo(index, i, cache), ); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); const hashes = []; - if (input.tapInternalKey && !tapLeafHashToSign) { + if (keySpend) { const outputKey = getPrevoutTaprootKey(inputIndex, input, cache) || Uint8Array.from([]); if (tools.compare((0, bip371_js_1.toXOnly)(pubkey), outputKey) === 0) { diff --git a/src/esm/psbt.js b/src/esm/psbt.js index 5e2e39b93..15c5e016d 100644 --- a/src/esm/psbt.js +++ b/src/esm/psbt.js @@ -475,14 +475,14 @@ export class Psbt { throw new Error('Need validator function to validate signatures'); pubkey = pubkey && toXOnly(pubkey); const allHashses = pubkey - ? getTaprootHashesForSig( + ? getTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, pubkey, this.__CACHE, ) - : getAllTaprootHashesForSig( + : getAllTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, @@ -840,7 +840,7 @@ export class Psbt { throw new Error( `Need Schnorr Signer to sign taproot input #${inputIndex}.`, ); - const hashesForSig = getTaprootHashesForSig( + const hashesForSig = getTaprootHashesForSigning( inputIndex, input, this.data.inputs, @@ -1274,7 +1274,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { hash, }; } -function getAllTaprootHashesForSig(inputIndex, input, inputs, cache) { +function getAllTaprootHashesForSigValidation(inputIndex, input, inputs, cache) { const allPublicKeys = []; if (input.tapInternalKey) { const key = getPrevoutTaprootKey(inputIndex, input, cache); @@ -1287,7 +1287,13 @@ function getAllTaprootHashesForSig(inputIndex, input, inputs, cache) { allPublicKeys.push(...tapScriptPubkeys); } const allHashes = allPublicKeys.map(publicKey => - getTaprootHashesForSig(inputIndex, input, inputs, publicKey, cache), + getTaprootHashesForSigValidation( + inputIndex, + input, + inputs, + publicKey, + cache, + ), ); return allHashes.flat(); } @@ -1298,7 +1304,7 @@ function getPrevoutTaprootKey(inputIndex, input, cache) { function trimTaprootSig(signature) { return signature.length === 64 ? signature : signature.subarray(0, 64); } -function getTaprootHashesForSig( +function getTaprootHashesForSigning( inputIndex, input, inputs, @@ -1307,16 +1313,62 @@ function getTaprootHashesForSig( tapLeafHashToSign, allowedSighashTypes, ) { - const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; checkSighashTypeAllowed(sighashType, allowedSighashTypes); + const keySpend = Boolean(input.tapInternalKey && !tapLeafHashToSign); + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + tapLeafHashToSign, + ); +} +function getTaprootHashesForSigValidation( + inputIndex, + input, + inputs, + pubkey, + cache, +) { + const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; + const keySpend = Boolean(input.tapKeySig); + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + ); +} +/* + * This helper method is used for both generating a hash for signing as well for validating; + * thus depending on context key spend can be detected either with tapInternalKey with no leaves + * or tapKeySig (note tapKeySig is a signature to the prevout pk, so tapInternalKey is not needed). + */ +function getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + tapLeafHashToSign, +) { + const unsignedTx = cache.__TX; const prevOuts = inputs.map((i, index) => getScriptAndAmountFromUtxo(index, i, cache), ); const signingScripts = prevOuts.map(o => o.script); const values = prevOuts.map(o => o.value); const hashes = []; - if (input.tapInternalKey && !tapLeafHashToSign) { + if (keySpend) { const outputKey = getPrevoutTaprootKey(inputIndex, input, cache) || Uint8Array.from([]); if (tools.compare(toXOnly(pubkey), outputKey) === 0) { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 357a059cd..a0ff08443 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -604,14 +604,14 @@ export class Psbt { pubkey = pubkey && toXOnly(pubkey); const allHashses = pubkey - ? getTaprootHashesForSig( + ? getTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, pubkey, this.__CACHE, ) - : getAllTaprootHashesForSig( + : getAllTaprootHashesForSigValidation( inputIndex, input, this.data.inputs, @@ -1056,7 +1056,7 @@ export class Psbt { `Need Schnorr Signer to sign taproot input #${inputIndex}.`, ); - const hashesForSig = getTaprootHashesForSig( + const hashesForSig = getTaprootHashesForSigning( inputIndex, input, this.data.inputs, @@ -1720,7 +1720,7 @@ function getHashForSig( }; } -function getAllTaprootHashesForSig( +function getAllTaprootHashesForSigValidation( inputIndex: number, input: PsbtInput, inputs: PsbtInput[], @@ -1742,7 +1742,13 @@ function getAllTaprootHashesForSig( } const allHashes = allPublicKeys.map(publicKey => - getTaprootHashesForSig(inputIndex, input, inputs, publicKey, cache), + getTaprootHashesForSigValidation( + inputIndex, + input, + inputs, + publicKey, + cache, + ), ); return allHashes.flat(); @@ -1761,7 +1767,7 @@ function trimTaprootSig(signature: Uint8Array): Uint8Array { return signature.length === 64 ? signature : signature.subarray(0, 64); } -function getTaprootHashesForSig( +function getTaprootHashesForSigning( inputIndex: number, input: PsbtInput, inputs: PsbtInput[], @@ -1769,12 +1775,61 @@ function getTaprootHashesForSig( cache: PsbtCache, tapLeafHashToSign?: Uint8Array, allowedSighashTypes?: number[], -): { pubkey: Uint8Array; hash: Uint8Array; leafHash?: Uint8Array }[] { - const unsignedTx = cache.__TX; - +) { const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; checkSighashTypeAllowed(sighashType, allowedSighashTypes); + const keySpend = Boolean(input.tapInternalKey && !tapLeafHashToSign); + + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + tapLeafHashToSign, + ); +} + +function getTaprootHashesForSigValidation( + inputIndex: number, + input: PsbtInput, + inputs: PsbtInput[], + pubkey: Uint8Array, + cache: PsbtCache, +) { + const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; + const keySpend = Boolean(input.tapKeySig); + return getTaprootHashesForSig( + inputIndex, + input, + inputs, + pubkey, + cache, + keySpend, + sighashType, + ); +} + +/* + * This helper method is used for both generating a hash for signing as well for validating; + * thus depending on context key spend can be detected either with tapInternalKey with no leaves + * or tapKeySig (note tapKeySig is a signature to the prevout pk, so tapInternalKey is not needed). + */ +function getTaprootHashesForSig( + inputIndex: number, + input: PsbtInput, + inputs: PsbtInput[], + pubkey: Uint8Array, + cache: PsbtCache, + keySpend: boolean, + sighashType: number, + tapLeafHashToSign?: Uint8Array, +): { pubkey: Uint8Array; hash: Uint8Array; leafHash?: Uint8Array }[] { + const unsignedTx = cache.__TX; + const prevOuts: Output[] = inputs.map((i, index) => getScriptAndAmountFromUtxo(index, i, cache), ); @@ -1782,7 +1837,7 @@ function getTaprootHashesForSig( const values = prevOuts.map(o => o.value); const hashes = []; - if (input.tapInternalKey && !tapLeafHashToSign) { + if (keySpend) { const outputKey = getPrevoutTaprootKey(inputIndex, input, cache) || Uint8Array.from([]); if (tools.compare(toXOnly(pubkey), outputKey) === 0) { From 1e4a2c0cd6cac5a3d90329578e1e1027d11c58d2 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Mon, 13 Oct 2025 17:24:28 -0700 Subject: [PATCH 2/4] Update ts_src/psbt.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts_src/psbt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index a0ff08443..def52a207 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1775,7 +1775,7 @@ function getTaprootHashesForSigning( cache: PsbtCache, tapLeafHashToSign?: Uint8Array, allowedSighashTypes?: number[], -) { +): { pubkey: Uint8Array; hash: Uint8Array; leafHash?: Uint8Array }[] { const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; checkSighashTypeAllowed(sighashType, allowedSighashTypes); From 2cc07b1ff1e6a0e2626875e2aa5b075ab8aeabfc Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Mon, 13 Oct 2025 17:24:35 -0700 Subject: [PATCH 3/4] Update ts_src/psbt.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ts_src/psbt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index def52a207..f072f1d48 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1799,7 +1799,7 @@ function getTaprootHashesForSigValidation( inputs: PsbtInput[], pubkey: Uint8Array, cache: PsbtCache, -) { +): { pubkey: Uint8Array; hash: Uint8Array; leafHash?: Uint8Array }[] { const sighashType = input.sighashType || Transaction.SIGHASH_DEFAULT; const keySpend = Boolean(input.tapKeySig); return getTaprootHashesForSig( From 2e56918691e062ad903141b4b87794f6fff765dd Mon Sep 17 00:00:00 2001 From: Jonathan Underwood Date: Tue, 14 Oct 2025 18:05:15 +0900 Subject: [PATCH 4/4] Upgrade setup-node action from v3 to v6 --- .github/workflows/main_ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml index 9c5879b9a..cbe825744 100644 --- a/.github/workflows/main_ci.yml +++ b/.github/workflows/main_ci.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ @@ -65,7 +65,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/ @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/ @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/ @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/ @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/ @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - - uses: actions/setup-node@eeb10cff27034e7acf239c5d29f62154018672fd # v3 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org/