From 921c801758bcdccce3db143cb5ddac249ce1475a Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 30 Sep 2025 17:23:03 +0200 Subject: [PATCH 1/2] fix(utxo-bin): fix signature matching in InputParser Replaces indexOf with findIndex to properly match signatures using buffer equality comparison. Issue: BTC-0 Co-authored-by: llm-git --- modules/utxo-bin/src/InputParser.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/utxo-bin/src/InputParser.ts b/modules/utxo-bin/src/InputParser.ts index 15b021a4df..0e4cb8ebae 100644 --- a/modules/utxo-bin/src/InputParser.ts +++ b/modules/utxo-bin/src/InputParser.ts @@ -224,7 +224,12 @@ export class InputParser extends Parser { ...this.parseSignaturesWithSigners( parsed, signedBy.flatMap((v, i) => (v ? [i.toString()] : [])), - parsed.signatures.map((k: Buffer | 0) => (k === 0 ? -1 : signedBy.indexOf(k))) + parsed.signatures.map((signatureByKey: Buffer | 0) => { + if (signatureByKey === 0) { + return -1; + } + return signedBy.findIndex((k) => k && k.equals(signatureByKey)); + }) ) ); } catch (e) { From 8661c77961d388dfbe644ab0f36cc31d03ad3883 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 30 Sep 2025 17:23:15 +0200 Subject: [PATCH 2/2] fix(utxo-bin): add support for providing previous transaction data Add --prevTx option to the parseTx command to provide previous transaction data directly instead of fetching from a remote source. Implement helper functions to extract previous outputs from these transactions. Issue: BTC-0 Co-authored-by: llm-git --- modules/utxo-bin/src/commands/cmdParseTx.ts | 23 ++++++--- modules/utxo-bin/src/prevTx.ts | 47 +++++++++++++++++++ .../p2sh_networkFullSigned_all_prevOuts.txt | 12 +++-- 3 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 modules/utxo-bin/src/prevTx.ts diff --git a/modules/utxo-bin/src/commands/cmdParseTx.ts b/modules/utxo-bin/src/commands/cmdParseTx.ts index 579a7d978b..5eda8ba669 100644 --- a/modules/utxo-bin/src/commands/cmdParseTx.ts +++ b/modules/utxo-bin/src/commands/cmdParseTx.ts @@ -19,15 +19,17 @@ import { fetchTransactionStatus, getClient, } from '../fetch'; -import { getParserTxProperties } from '../ParserTx'; +import { getParserTxProperties, ParserTx } from '../ParserTx'; import { parseUnknown } from '../parseUnknown'; import { Parser } from '../Parser'; import { formatString } from './formatString'; +import { getPrevOutputsFromPrevTxs } from '../prevTx'; export type ArgsParseTransaction = ReadStringOptions & { network: utxolib.Network; txid?: string; + prevTx?: string[]; blockHeight?: number; txIndex?: number; all: boolean; @@ -71,6 +73,7 @@ export const cmdParseTx = { .options(readStringOptions) .options(getNetworkOptionsDemand()) .option('txid', { type: 'string' }) + .option('prevTx', { type: 'string', description: 'previous transaction hex or base64 string', array: true }) .option('blockHeight', { type: 'number' }) .option('txIndex', { type: 'number' }) .option('fetchAll', { type: 'boolean', default: false }) @@ -128,11 +131,14 @@ export const cmdParseTx = { throw new Error(`no txdata`); } - const bytes = stringToBuffer(string, ['hex', 'base64']); + function decodeBytes(bytes: Buffer): ParserTx { + return utxolib.bitgo.isPsbt(bytes) + ? utxolib.bitgo.createPsbtFromBuffer(bytes, argv.network) + : utxolib.bitgo.createTransactionFromBuffer(bytes, argv.network, { amountType: 'bigint' }); + } - let tx = utxolib.bitgo.isPsbt(bytes) - ? utxolib.bitgo.createPsbtFromBuffer(bytes, argv.network) - : utxolib.bitgo.createTransactionFromBuffer(bytes, argv.network, { amountType: 'bigint' }); + const bytes = stringToBuffer(string, ['hex', 'base64']); + let tx = decodeBytes(bytes); const { id: txid } = getParserTxProperties(tx, undefined); if (tx instanceof utxolib.bitgo.UtxoTransaction) { @@ -144,6 +150,11 @@ export const cmdParseTx = { tx = tx.extractTransaction(); } + const prevTxs: ParserTx[] = (argv.prevTx ?? []).map((s) => { + const buf = stringToBuffer(s, ['hex', 'base64']); + return decodeBytes(buf); + }); + if (argv.parseAsUnknown) { console.log(formatString(parseUnknown(new Parser(), 'tx', tx), argv)); return; @@ -157,7 +168,7 @@ export const cmdParseTx = { const parsed = getTxParser(argv).parse(tx, { status: argv.fetchStatus && txid ? await fetchTransactionStatus(httpClient, txid, argv.network) : undefined, - prevOutputs: argv.fetchInputs ? await fetchPrevOutputs(httpClient, tx) : undefined, + prevOutputs: argv.fetchInputs ? await fetchPrevOutputs(httpClient, tx) : getPrevOutputsFromPrevTxs(tx, prevTxs), prevOutputSpends: argv.fetchSpends ? await fetchPrevOutputSpends(httpClient, tx) : undefined, outputSpends: argv.fetchSpends && tx instanceof utxolib.bitgo.UtxoTransaction diff --git a/modules/utxo-bin/src/prevTx.ts b/modules/utxo-bin/src/prevTx.ts new file mode 100644 index 0000000000..777a7aca32 --- /dev/null +++ b/modules/utxo-bin/src/prevTx.ts @@ -0,0 +1,47 @@ +import * as utxolib from '@bitgo/utxo-lib'; +import { ParserTx } from './ParserTx'; + +function getPrevOutForOutpoint(outpoint: utxolib.bitgo.TxOutPoint, prevTx: ParserTx) { + if (prevTx instanceof utxolib.bitgo.UtxoTransaction) { + const hash = prevTx.getId(); + if (hash !== outpoint.txid) { + return undefined; + } + const out = prevTx.outs[outpoint.vout]; + if (!out) { + throw new Error(`vout ${outpoint.vout} not found in prevTx ${hash}`); + } + return out; + } + + if (prevTx instanceof utxolib.bitgo.UtxoPsbt) { + throw new Error(`not implemented for Psbt yet`); + } + + throw new Error(`unknown tx type`); +} + +export function getPrevOutputsFromPrevTxs(tx: ParserTx, prevTxs: ParserTx[]): utxolib.TxOutput[] | undefined { + if (prevTxs.length === 0) { + return undefined; + } + if (tx instanceof utxolib.bitgo.UtxoTransaction) { + const outpoints = tx.ins.map((i) => utxolib.bitgo.getOutputIdForInput(i)); + return outpoints.map((o) => { + const matches = prevTxs.flatMap((t) => getPrevOutForOutpoint(o, t)); + if (matches.length === 0) { + throw new Error(`no prevTx found for input ${o.txid}:${o.vout}`); + } + if (matches.length > 1) { + throw new Error(`more than one prevTx found for input ${o.txid}:${o.vout}`); + } + return matches[0] as utxolib.TxOutput; + }); + } + + if (tx instanceof utxolib.bitgo.UtxoPsbt) { + throw new Error(`not implemented for Psbt yet`); + } + + throw new Error(`unknown tx type`); +} diff --git a/modules/utxo-bin/test/fixtures/formatTransaction/p2sh_networkFullSigned_all_prevOuts.txt b/modules/utxo-bin/test/fixtures/formatTransaction/p2sh_networkFullSigned_all_prevOuts.txt index 8d6c210dc0..fba582e4d8 100644 --- a/modules/utxo-bin/test/fixtures/formatTransaction/p2sh_networkFullSigned_all_prevOuts.txt +++ b/modules/utxo-bin/test/fixtures/formatTransaction/p2sh_networkFullSigned_all_prevOuts.txt @@ -34,7 +34,8 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f │ │ ├── signed by: [0, 2] │ │ ├─┬ 0 │ │ │ ├── bytes: 3044022048a059c22437630fdd5bc3c46bd1e30438a3623a59050d5e5f2fb1a36686fd9e02202ed8206b2f2472efd3ea145bc3c52703a7874fc747bdccc97cd5b862c8d62b7401 (71 bytes) -│ │ │ ├── valid: false +│ │ │ ├── valid: true +│ │ │ ├── signedBy: user │ │ │ ├── isCanonical: true │ │ │ ├── hashType: 1 │ │ │ ├── r: 48a059c22437630fdd5bc3c46bd1e30438a3623a59050d5e5f2fb1a36686fd9e (32 bytes) @@ -42,7 +43,8 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f │ │ │ └── highS: false │ │ └─┬ 1 │ │ ├── bytes: 30440220138087371ab51032457bdca8ef134eb566a61b16631cf6bc296c8b243f938bf70220720ab74bfa5e39f2c764d1879dd6fea9b1f287611b1ea714fe5b0928bd13d46901 (71 bytes) -│ │ ├── valid: false +│ │ ├── valid: true +│ │ ├── signedBy: bitgo │ │ ├── isCanonical: true │ │ ├── hashType: 1 │ │ ├── r: 138087371ab51032457bdca8ef134eb566a61b16631cf6bc296c8b243f938bf7 (32 bytes) @@ -72,7 +74,8 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f │ ├── signed by: [0, 2] │ ├─┬ 0 │ │ ├── bytes: 3045022100afde55c0cfa9dc3a20fb8706c5a7f86c754efcbbaf866e09e84a18668c53148402203a7e0bb62cd5a951def97181f6a26d78b70c7ac22af79029f22b5b130785939f01 (72 bytes) -│ │ ├── valid: false +│ │ ├── valid: true +│ │ ├── signedBy: user │ │ ├── isCanonical: true │ │ ├── hashType: 1 │ │ ├── r: afde55c0cfa9dc3a20fb8706c5a7f86c754efcbbaf866e09e84a18668c531484 (32 bytes) @@ -80,7 +83,8 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f │ │ └── highS: false │ └─┬ 1 │ ├── bytes: 3044022033ae8e7c8ef2109b804bc338e884109e5ed9e534307761aa5695a0eea3ce0615022045b4af31359339b6c696ca0f956e844baa8dd642b0e74ad2475e8fe5b4c055e801 (71 bytes) -│ ├── valid: false +│ ├── valid: true +│ ├── signedBy: bitgo │ ├── isCanonical: true │ ├── hashType: 1 │ ├── r: 33ae8e7c8ef2109b804bc338e884109e5ed9e534307761aa5695a0eea3ce0615 (32 bytes)