From c7ce1e5c74eac0b8e6c55601b2827f070b59fa11 Mon Sep 17 00:00:00 2001 From: BitGo Agent Date: Fri, 7 Aug 2026 19:54:18 +0000 Subject: [PATCH 1/2] fix(psbt): route addOutput unknownKeyVals to output map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addOutput() was calling addUnknownKeyValToInput(outputIndex, keyVal) instead of addUnknownKeyValToOutput(outputIndex, keyVal). This caused two confirmed failure modes: 1. Silent corruption when outputIndex < inputs.length: the keyval was attached to inputs[outputIndex].unknownKeyVals and survived serialize/parse round-trips in the wrong (input) map. Because the duplicate-check also ran against InputTypes (16 entries) instead of OutputTypes (6 entries), output unknown-key type bytes 6–15 were additionally misrejected. 2. Crash ("No input #N") when outputIndex >= inputs.length — the common 1-input/2-output case — making PSBT construction impossible whenever unknownKeyVals accompany the second output. Both modes affect musig2/MPC coordination data carried as output proprietary fields: the signing ceremony crashes or corrupts the PSBT, leaving funds stuck in the shared wallet. Fix: change line 159 to call addUnknownKeyValToOutput, matching the upstream bip174 v2.1.1 fix. Add regression tests covering correct placement, round-trip survival, and the outputs>inputs case. Ticket: WCN-1934 Session-Id: 9abd2e08-b701-4f8d-9355-06124e17bf0c Task-Id: edef13cc-a93b-4c12-85ac-d51bcc1dfaaa --- package-lock.json | 16 ------- src/lib/psbt.js | 2 +- src/tests/addInputOutput.js | 79 +++++++++++++++++++++++++++++++ ts_src/lib/psbt.ts | 2 +- ts_src/tests/addInputOutput.ts | 86 ++++++++++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9059fc..850e840 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7745,22 +7745,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/semantic-release/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/semantic-release/node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", diff --git a/src/lib/psbt.js b/src/lib/psbt.js index 5f61863..be68d89 100644 --- a/src/lib/psbt.js +++ b/src/lib/psbt.js @@ -112,7 +112,7 @@ class Psbt { throw new Error('unknownKeyVals must be an Array'); } addKeyVals.forEach(keyVal => - this.addUnknownKeyValToInput(outputIndex, keyVal), + this.addUnknownKeyValToOutput(outputIndex, keyVal), ); utils_1.addOutputAttributes(this.outputs, outputData); return this; diff --git a/src/tests/addInputOutput.js b/src/tests/addInputOutput.js index f1e2f15..9e94b97 100644 --- a/src/tests/addInputOutput.js +++ b/src/tests/addInputOutput.js @@ -3,6 +3,85 @@ Object.defineProperty(exports, '__esModule', { value: true }); const tape = require('tape'); const psbt_1 = require('../lib/psbt'); const txTools_1 = require('./utils/txTools'); +const SCRIPT = Buffer.from( + 'a914e18870f2c297fbfca54c5c6f645c7745a5b66eda87', + 'hex', +); +// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is +// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is +// always an unknown key for either map). +const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); +const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); +tape( + 'Test: addOutput routes unknownKeyVals to output map, not input map', + t => { + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + // Key must be on the output, not the input + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals contains the proprietary key', + ); + t.deepEqual( + psbt.inputs[0].unknownKeyVals, + [], + 'input[0].unknownKeyVals is empty', + ); + // Placement must survive a serialize/parse round-trip + const psbt2 = psbt_1.Psbt.fromHex( + psbt.toHex(), + txTools_1.transactionFromBuffer, + ); + t.deepEqual( + psbt2.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals survives round-trip', + ); + // fromBuffer leaves unknownKeyVals undefined when there are none; + // either undefined or [] is acceptable here. + t.ok( + !psbt2.inputs[0].unknownKeyVals || + psbt2.inputs[0].unknownKeyVals.length === 0, + 'input[0].unknownKeyVals is empty after round-trip', + ); + t.end(); + }, +); +tape( + 'Test: addOutput with unknownKeyVals does not throw when outputs > inputs', + t => { + // Common case: 1 input, 2 outputs — the second addOutput formerly threw + // "No input #1" because it called addUnknownKeyValToInput(1, ...). + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 500000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + }, 'second addOutput with unknownKeyVals must not throw'); + t.deepEqual( + psbt.outputs[1].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[1] holds the proprietary key', + ); + t.end(); + }, +); tape('Test: add Input Output', t => { const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); psbt.addInput({ diff --git a/ts_src/lib/psbt.ts b/ts_src/lib/psbt.ts index 68905a3..30565d8 100644 --- a/ts_src/lib/psbt.ts +++ b/ts_src/lib/psbt.ts @@ -156,7 +156,7 @@ export class Psbt { throw new Error('unknownKeyVals must be an Array'); } addKeyVals.forEach((keyVal: KeyValue) => - this.addUnknownKeyValToInput(outputIndex, keyVal), + this.addUnknownKeyValToOutput(outputIndex, keyVal), ); addOutputAttributes(this.outputs, outputData); return this; diff --git a/ts_src/tests/addInputOutput.ts b/ts_src/tests/addInputOutput.ts index f49d5ab..30f204e 100644 --- a/ts_src/tests/addInputOutput.ts +++ b/ts_src/tests/addInputOutput.ts @@ -2,6 +2,92 @@ import * as tape from 'tape'; import { Psbt } from '../lib/psbt'; import { getDefaultTx, transactionFromBuffer } from './utils/txTools'; +const SCRIPT = Buffer.from( + 'a914e18870f2c297fbfca54c5c6f645c7745a5b66eda87', + 'hex', +); + +// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is +// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is +// always an unknown key for either map). +const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); +const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); + +tape( + 'Test: addOutput routes unknownKeyVals to output map, not input map', + t => { + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + + // Key must be on the output, not the input + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals contains the proprietary key', + ); + t.deepEqual( + psbt.inputs[0].unknownKeyVals, + [], + 'input[0].unknownKeyVals is empty', + ); + + // Placement must survive a serialize/parse round-trip + const psbt2 = Psbt.fromHex(psbt.toHex(), transactionFromBuffer); + t.deepEqual( + psbt2.outputs[0].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[0].unknownKeyVals survives round-trip', + ); + // fromBuffer leaves unknownKeyVals undefined when there are none; + // either undefined or [] is acceptable here. + t.ok( + !psbt2.inputs[0].unknownKeyVals || + psbt2.inputs[0].unknownKeyVals.length === 0, + 'input[0].unknownKeyVals is empty after round-trip', + ); + + t.end(); + }, +); + +tape( + 'Test: addOutput with unknownKeyVals does not throw when outputs > inputs', + t => { + // Common case: 1 input, 2 outputs — the second addOutput formerly threw + // "No input #1" because it called addUnknownKeyValToInput(1, ...). + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 500000, + unknownKeyVals: [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + }); + }, 'second addOutput with unknownKeyVals must not throw'); + + t.deepEqual( + psbt.outputs[1].unknownKeyVals, + [{ key: PROPRIETARY_KEY, value: PROPRIETARY_VAL }], + 'output[1] holds the proprietary key', + ); + + t.end(); + }, +); + tape('Test: add Input Output', t => { const psbt = new Psbt(getDefaultTx()); psbt.addInput({ From ba0680a373f11f06c0e1565a2127919ac04b3c95 Mon Sep 17 00:00:00 2001 From: BitGo Agent Date: Fri, 7 Aug 2026 20:02:03 +0000 Subject: [PATCH 2/2] fix(utils): use max-enum-value+1 as checkHasKey boundary; reject empty keys Two correctness bugs in checkHasKey / getEnumLength exposed by the addOutput routing fix (WCN-1934 primary fix): 1. getEnumLength counted named enum members rather than returning max-numeric-value + 1. For non-contiguous enums this diverges: OutputTypes has 6 named members but its highest type byte is 7 (TAP_BIP32_DERIVATION = 0x07). InputTypes has 16 named members but its highest type byte is 24 (TAP_MERKLE_ROOT = 0x18 = 24). The old count-based threshold let TAP_TREE (0x06) and TAP_BIP32_DERIVATION (0x07) bypass the "use the typed method" guard on the output path, and let all six taproot input types (0x13-0x18) bypass the same guard on the input path. A caller could silently store these known fields in unknownKeyVals alongside their typed counterparts, producing a PSBT with duplicate-key serialization that conformant parsers reject. Fix: derive the threshold as max(numeric enum values) + 1. 2. A zero-length key Buffer passed checkHasKey unchecked: key[0] is undefined, and undefined < N is false in JavaScript, so the guard did not throw. The zero-length key was stored in unknownKeyVals, then serialized as the PSBT end-of-map separator byte (0x00), causing silent data loss on round-trip without any error at write time. Fix: explicit key.length === 0 guard at the top of checkHasKey. Adds 7 regression tests covering: - key byte 0x08 (above OutputTypes max) is accepted as unknown - key bytes 0x06 / 0x07 (TAP_TREE / TAP_BIP32_DERIVATION) are rejected - zero-length key is rejected on output, input, and global paths Ticket: WCN-1934 Session-Id: 9abd2e08-b701-4f8d-9355-06124e17bf0c Task-Id: edef13cc-a93b-4c12-85ac-d51bcc1dfaaa --- src/lib/utils.js | 19 ++++-- src/tests/addInputOutput.js | 108 ++++++++++++++++++++++++++++- ts_src/lib/utils.ts | 19 ++++-- ts_src/tests/addInputOutput.ts | 121 ++++++++++++++++++++++++++++++++- 4 files changed, 247 insertions(+), 20 deletions(-) diff --git a/src/lib/utils.js b/src/lib/utils.js index 57f82ad..cd9272f 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -28,6 +28,9 @@ function checkForOutput(outputs, outputIndex) { } exports.checkForOutput = checkForOutput; function checkHasKey(checkKeyVal, keyVals, enumLength) { + if (checkKeyVal.key.length === 0) { + throw new Error(`Key must not be empty`); + } if (checkKeyVal.key[0] < enumLength) { throw new Error( `Use the method for your specific key instead of addUnknownKeyVal*`, @@ -41,14 +44,16 @@ function checkHasKey(checkKeyVal, keyVals, enumLength) { } } exports.checkHasKey = checkHasKey; +// Returns max numeric enum value + 1, which is the correct upper bound for +// known type bytes. Using member count would give the wrong answer for enums +// with non-contiguous values (e.g. OutputTypes jumps from 0x02 to 0x05). function getEnumLength(myenum) { - let count = 0; - Object.keys(myenum).forEach(val => { - if (Number(isNaN(Number(val)))) { - count++; - } - }); - return count; + return ( + Object.keys(myenum) + .map(Number) + .filter(n => !isNaN(n)) + .reduce((max, n) => Math.max(max, n), -1) + 1 + ); } exports.getEnumLength = getEnumLength; function inputCheckUncleanFinalized(inputIndex, input) { diff --git a/src/tests/addInputOutput.js b/src/tests/addInputOutput.js index 9e94b97..e8ae860 100644 --- a/src/tests/addInputOutput.js +++ b/src/tests/addInputOutput.js @@ -7,9 +7,10 @@ const SCRIPT = Buffer.from( 'a914e18870f2c297fbfca54c5c6f645c7745a5b66eda87', 'hex', ); -// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is -// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is -// always an unknown key for either map). +// Proprietary key: 0xfc prefix + identifier + subtype. Type byte 0xfc is +// above the highest defined byte in both InputTypes (0x18 = TAP_MERKLE_ROOT) +// and OutputTypes (0x07 = TAP_BIP32_DERIVATION), so it is always an unknown +// key for either map. const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); tape( @@ -82,6 +83,107 @@ tape( t.end(); }, ); +tape( + 'Test: addOutput accepts unknown key with type byte above max OutputTypes value', + t => { + // OutputTypes has a gap: values run 0,1,2 then jump to 5,6,7. Type byte 8 + // is above the highest defined output type (TAP_BIP32_DERIVATION = 0x07) + // and must be accepted as a genuine unknown key. Before the getEnumLength + // fix this would throw "Use the method for your specific key" because the + // old count-of-names threshold (6) was used and 8 >= 6 passed, but type + // bytes 6 (TAP_TREE) and 7 (TAP_BIP32_DERIVATION) were also incorrectly + // allowed. After the fix the threshold is max-value+1 = 8, so byte 8 is + // still accepted and bytes 6/7 are now correctly rejected. + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [ + { key: Buffer.from([0x08]), value: Buffer.from([0x01]) }, + ], + }); + }, 'key byte 0x08 (above max OutputTypes value) must be accepted'); + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: Buffer.from([0x08]), value: Buffer.from([0x01]) }], + 'output holds key byte 0x08 as unknown key', + ); + t.end(); + }, +); +tape( + 'Test: addOutput rejects known OutputTypes type bytes as unknown keys', + t => { + // TAP_TREE = 0x06 and TAP_BIP32_DERIVATION = 0x07 are defined OutputTypes. + // With max-value+1 = 8 as the threshold they are now correctly rejected. + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.from([0x06]), + value: Buffer.from([0x01]), + }), + /Use the method for your specific key/, + 'key byte 0x06 (TAP_TREE) must be rejected', + ); + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.from([0x07]), + value: Buffer.from([0x01]), + }), + /Use the method for your specific key/, + 'key byte 0x07 (TAP_BIP32_DERIVATION) must be rejected', + ); + t.end(); + }, +); +tape('Test: addUnknownKeyVal* rejects empty key Buffer', t => { + const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for output', + ); + t.throws( + () => + psbt.addUnknownKeyValToInput(0, { + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for input', + ); + t.throws( + () => + psbt.addUnknownKeyValToGlobal({ + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for global', + ); + t.end(); +}); tape('Test: add Input Output', t => { const psbt = new psbt_1.Psbt(txTools_1.getDefaultTx()); psbt.addInput({ diff --git a/ts_src/lib/utils.ts b/ts_src/lib/utils.ts index b073d79..4472abc 100644 --- a/ts_src/lib/utils.ts +++ b/ts_src/lib/utils.ts @@ -46,6 +46,9 @@ export function checkHasKey( keyVals: KeyValue[] | undefined, enumLength: number, ): void { + if (checkKeyVal.key.length === 0) { + throw new Error(`Key must not be empty`); + } if (checkKeyVal.key[0] < enumLength) { throw new Error( `Use the method for your specific key instead of addUnknownKeyVal*`, @@ -59,14 +62,16 @@ export function checkHasKey( } } +// Returns max numeric enum value + 1, which is the correct upper bound for +// known type bytes. Using member count would give the wrong answer for enums +// with non-contiguous values (e.g. OutputTypes jumps from 0x02 to 0x05). export function getEnumLength(myenum: any): number { - let count = 0; - Object.keys(myenum).forEach(val => { - if (Number(isNaN(Number(val)))) { - count++; - } - }); - return count; + return ( + Object.keys(myenum) + .map(Number) + .filter(n => !isNaN(n)) + .reduce((max, n) => Math.max(max, n), -1) + 1 + ); } export function inputCheckUncleanFinalized( diff --git a/ts_src/tests/addInputOutput.ts b/ts_src/tests/addInputOutput.ts index 30f204e..708d35d 100644 --- a/ts_src/tests/addInputOutput.ts +++ b/ts_src/tests/addInputOutput.ts @@ -7,9 +7,10 @@ const SCRIPT = Buffer.from( 'hex', ); -// Proprietary key: 0xfc prefix + identifier + subtype (type byte 0xfc is -// above both InputTypes (16 entries) and OutputTypes (6 entries), so it is -// always an unknown key for either map). +// Proprietary key: 0xfc prefix + identifier + subtype. Type byte 0xfc is +// above the highest defined byte in both InputTypes (0x18 = TAP_MERKLE_ROOT) +// and OutputTypes (0x07 = TAP_BIP32_DERIVATION), so it is always an unknown +// key for either map. const PROPRIETARY_KEY = Buffer.from('fc0a626974676f2f6d7573696700', 'hex'); const PROPRIETARY_VAL = Buffer.from('deadbeef', 'hex'); @@ -88,6 +89,120 @@ tape( }, ); +tape( + 'Test: addOutput accepts unknown key with type byte above max OutputTypes value', + t => { + // OutputTypes has a gap: values run 0,1,2 then jump to 5,6,7. Type byte 8 + // is above the highest defined output type (TAP_BIP32_DERIVATION = 0x07) + // and must be accepted as a genuine unknown key. Before the getEnumLength + // fix this would throw "Use the method for your specific key" because the + // old count-of-names threshold (6) was used and 8 >= 6 passed, but type + // bytes 6 (TAP_TREE) and 7 (TAP_BIP32_DERIVATION) were also incorrectly + // allowed. After the fix the threshold is max-value+1 = 8, so byte 8 is + // still accepted and bytes 6/7 are now correctly rejected. + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + + t.doesNotThrow(() => { + psbt.addOutput({ + script: SCRIPT, + value: 1000000, + unknownKeyVals: [ + { key: Buffer.from([0x08]), value: Buffer.from([0x01]) }, + ], + }); + }, 'key byte 0x08 (above max OutputTypes value) must be accepted'); + + t.deepEqual( + psbt.outputs[0].unknownKeyVals, + [{ key: Buffer.from([0x08]), value: Buffer.from([0x01]) }], + 'output holds key byte 0x08 as unknown key', + ); + + t.end(); + }, +); + +tape( + 'Test: addOutput rejects known OutputTypes type bytes as unknown keys', + t => { + // TAP_TREE = 0x06 and TAP_BIP32_DERIVATION = 0x07 are defined OutputTypes. + // With max-value+1 = 8 as the threshold they are now correctly rejected. + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.from([0x06]), + value: Buffer.from([0x01]), + }), + /Use the method for your specific key/, + 'key byte 0x06 (TAP_TREE) must be rejected', + ); + + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.from([0x07]), + value: Buffer.from([0x01]), + }), + /Use the method for your specific key/, + 'key byte 0x07 (TAP_BIP32_DERIVATION) must be rejected', + ); + + t.end(); + }, +); + +tape('Test: addUnknownKeyVal* rejects empty key Buffer', t => { + const psbt = new Psbt(getDefaultTx()); + psbt.addInput({ + hash: '865dce988413971fd812d0e81a3395ed916a87ea533e1a16c0f4e15df96fa7d4', + index: 0, + }); + psbt.addOutput({ script: SCRIPT, value: 1000000 }); + + t.throws( + () => + psbt.addUnknownKeyValToOutput(0, { + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for output', + ); + + t.throws( + () => + psbt.addUnknownKeyValToInput(0, { + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for input', + ); + + t.throws( + () => + psbt.addUnknownKeyValToGlobal({ + key: Buffer.alloc(0), + value: Buffer.from([0x01]), + }), + /Key must not be empty/, + 'zero-length key must be rejected for global', + ); + + t.end(); +}); + tape('Test: add Input Output', t => { const psbt = new Psbt(getDefaultTx()); psbt.addInput({