From c9a1c970101e2a02f2b7197774b4ffcb274b8ac0 Mon Sep 17 00:00:00 2001 From: 0xsatoshi99 <0xsatoshi99@gmail.com> Date: Mon, 1 Dec 2025 14:18:37 +0100 Subject: [PATCH] fix: handle zero-input transaction parsing When parsing a transaction with 0 inputs, the byte sequence 00 01 (vinLen=0, voutLen=1) was incorrectly interpreted as segwit marker/flag. This fix validates the segwit interpretation by checking: 1. If vinLen would be 0 (segwit tx with 0 inputs is invalid) 2. If the buffer is too small to contain the claimed number of inputs If either check fails, fall back to non-segwit parsing. Fixes #2293 --- src/cjs/transaction.cjs | 14 +++++++++++++- src/esm/transaction.js | 16 +++++++++++++++- ts_src/transaction.ts | 20 +++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/cjs/transaction.cjs b/src/cjs/transaction.cjs index 476023b2a..e4bed413d 100644 --- a/src/cjs/transaction.cjs +++ b/src/cjs/transaction.cjs @@ -107,7 +107,19 @@ class Transaction { marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG ) { - hasWitnesses = true; + const { bigintValue: vinLenPeek, bytes: vinLenBytes } = + bufferutils_js_1.varuint.decode(buffer, bufferReader.offset); + const minInputSize = 41; + const remainingAfterVinLen = + buffer.length - bufferReader.offset - vinLenBytes; + if ( + vinLenPeek === BigInt(0) || + Number(vinLenPeek) * minInputSize > remainingAfterVinLen + ) { + bufferReader.offset -= 2; + } else { + hasWitnesses = true; + } } else { bufferReader.offset -= 2; } diff --git a/src/esm/transaction.js b/src/esm/transaction.js index dfe233389..e5530b3d1 100644 --- a/src/esm/transaction.js +++ b/src/esm/transaction.js @@ -65,7 +65,21 @@ export class Transaction { marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG ) { - hasWitnesses = true; + const { bigintValue: vinLenPeek, bytes: vinLenBytes } = varuint.decode( + buffer, + bufferReader.offset, + ); + const minInputSize = 41; + const remainingAfterVinLen = + buffer.length - bufferReader.offset - vinLenBytes; + if ( + vinLenPeek === BigInt(0) || + Number(vinLenPeek) * minInputSize > remainingAfterVinLen + ) { + bufferReader.offset -= 2; + } else { + hasWitnesses = true; + } } else { bufferReader.offset -= 2; } diff --git a/ts_src/transaction.ts b/ts_src/transaction.ts index 8b943d713..bd72d47c3 100644 --- a/ts_src/transaction.ts +++ b/ts_src/transaction.ts @@ -77,23 +77,33 @@ export class Transaction { static fromBuffer(buffer: Uint8Array, _NO_STRICT?: boolean): Transaction { const bufferReader = new BufferReader(buffer); - const tx = new Transaction(); tx.version = bufferReader.readUInt32(); - const marker = bufferReader.readUInt8(); const flag = bufferReader.readUInt8(); - let hasWitnesses = false; if ( marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG ) { - hasWitnesses = true; + const { bigintValue: vinLenPeek, bytes: vinLenBytes } = varuint.decode( + buffer, + bufferReader.offset, + ); + const minInputSize = 41; + const remainingAfterVinLen = + buffer.length - bufferReader.offset - vinLenBytes; + if ( + vinLenPeek === BigInt(0) || + Number(vinLenPeek) * minInputSize > remainingAfterVinLen + ) { + bufferReader.offset -= 2; + } else { + hasWitnesses = true; + } } else { bufferReader.offset -= 2; } - const vinLen = bufferReader.readVarInt(); for (let i = 0; i < vinLen; ++i) { tx.ins.push({