Skip to content

Commit 0d3085f

Browse files
Merge pull request #7669 from BitGo/BTC-0.fix-utxobin
fix(utxo-bin): fix signature matching and add previous transaction support
2 parents 6e44eca + 8661c77 commit 0d3085f

File tree

4 files changed

+78
-11
lines changed

4 files changed

+78
-11
lines changed

modules/utxo-bin/src/InputParser.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,12 @@ export class InputParser extends Parser {
224224
...this.parseSignaturesWithSigners(
225225
parsed,
226226
signedBy.flatMap((v, i) => (v ? [i.toString()] : [])),
227-
parsed.signatures.map((k: Buffer | 0) => (k === 0 ? -1 : signedBy.indexOf(k)))
227+
parsed.signatures.map((signatureByKey: Buffer | 0) => {
228+
if (signatureByKey === 0) {
229+
return -1;
230+
}
231+
return signedBy.findIndex((k) => k && k.equals(signatureByKey));
232+
})
228233
)
229234
);
230235
} catch (e) {

modules/utxo-bin/src/commands/cmdParseTx.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ import {
1919
fetchTransactionStatus,
2020
getClient,
2121
} from '../fetch';
22-
import { getParserTxProperties } from '../ParserTx';
22+
import { getParserTxProperties, ParserTx } from '../ParserTx';
2323
import { parseUnknown } from '../parseUnknown';
2424
import { Parser } from '../Parser';
2525

2626
import { formatString } from './formatString';
27+
import { getPrevOutputsFromPrevTxs } from '../prevTx';
2728

2829
export type ArgsParseTransaction = ReadStringOptions & {
2930
network: utxolib.Network;
3031
txid?: string;
32+
prevTx?: string[];
3133
blockHeight?: number;
3234
txIndex?: number;
3335
all: boolean;
@@ -71,6 +73,7 @@ export const cmdParseTx = {
7173
.options(readStringOptions)
7274
.options(getNetworkOptionsDemand())
7375
.option('txid', { type: 'string' })
76+
.option('prevTx', { type: 'string', description: 'previous transaction hex or base64 string', array: true })
7477
.option('blockHeight', { type: 'number' })
7578
.option('txIndex', { type: 'number' })
7679
.option('fetchAll', { type: 'boolean', default: false })
@@ -128,11 +131,14 @@ export const cmdParseTx = {
128131
throw new Error(`no txdata`);
129132
}
130133

131-
const bytes = stringToBuffer(string, ['hex', 'base64']);
134+
function decodeBytes(bytes: Buffer): ParserTx {
135+
return utxolib.bitgo.isPsbt(bytes)
136+
? utxolib.bitgo.createPsbtFromBuffer(bytes, argv.network)
137+
: utxolib.bitgo.createTransactionFromBuffer(bytes, argv.network, { amountType: 'bigint' });
138+
}
132139

133-
let tx = utxolib.bitgo.isPsbt(bytes)
134-
? utxolib.bitgo.createPsbtFromBuffer(bytes, argv.network)
135-
: utxolib.bitgo.createTransactionFromBuffer(bytes, argv.network, { amountType: 'bigint' });
140+
const bytes = stringToBuffer(string, ['hex', 'base64']);
141+
let tx = decodeBytes(bytes);
136142

137143
const { id: txid } = getParserTxProperties(tx, undefined);
138144
if (tx instanceof utxolib.bitgo.UtxoTransaction) {
@@ -144,6 +150,11 @@ export const cmdParseTx = {
144150
tx = tx.extractTransaction();
145151
}
146152

153+
const prevTxs: ParserTx[] = (argv.prevTx ?? []).map((s) => {
154+
const buf = stringToBuffer(s, ['hex', 'base64']);
155+
return decodeBytes(buf);
156+
});
157+
147158
if (argv.parseAsUnknown) {
148159
console.log(formatString(parseUnknown(new Parser(), 'tx', tx), argv));
149160
return;
@@ -157,7 +168,7 @@ export const cmdParseTx = {
157168

158169
const parsed = getTxParser(argv).parse(tx, {
159170
status: argv.fetchStatus && txid ? await fetchTransactionStatus(httpClient, txid, argv.network) : undefined,
160-
prevOutputs: argv.fetchInputs ? await fetchPrevOutputs(httpClient, tx) : undefined,
171+
prevOutputs: argv.fetchInputs ? await fetchPrevOutputs(httpClient, tx) : getPrevOutputsFromPrevTxs(tx, prevTxs),
161172
prevOutputSpends: argv.fetchSpends ? await fetchPrevOutputSpends(httpClient, tx) : undefined,
162173
outputSpends:
163174
argv.fetchSpends && tx instanceof utxolib.bitgo.UtxoTransaction

modules/utxo-bin/src/prevTx.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { ParserTx } from './ParserTx';
3+
4+
function getPrevOutForOutpoint(outpoint: utxolib.bitgo.TxOutPoint, prevTx: ParserTx) {
5+
if (prevTx instanceof utxolib.bitgo.UtxoTransaction) {
6+
const hash = prevTx.getId();
7+
if (hash !== outpoint.txid) {
8+
return undefined;
9+
}
10+
const out = prevTx.outs[outpoint.vout];
11+
if (!out) {
12+
throw new Error(`vout ${outpoint.vout} not found in prevTx ${hash}`);
13+
}
14+
return out;
15+
}
16+
17+
if (prevTx instanceof utxolib.bitgo.UtxoPsbt) {
18+
throw new Error(`not implemented for Psbt yet`);
19+
}
20+
21+
throw new Error(`unknown tx type`);
22+
}
23+
24+
export function getPrevOutputsFromPrevTxs(tx: ParserTx, prevTxs: ParserTx[]): utxolib.TxOutput<bigint>[] | undefined {
25+
if (prevTxs.length === 0) {
26+
return undefined;
27+
}
28+
if (tx instanceof utxolib.bitgo.UtxoTransaction) {
29+
const outpoints = tx.ins.map((i) => utxolib.bitgo.getOutputIdForInput(i));
30+
return outpoints.map((o) => {
31+
const matches = prevTxs.flatMap((t) => getPrevOutForOutpoint(o, t));
32+
if (matches.length === 0) {
33+
throw new Error(`no prevTx found for input ${o.txid}:${o.vout}`);
34+
}
35+
if (matches.length > 1) {
36+
throw new Error(`more than one prevTx found for input ${o.txid}:${o.vout}`);
37+
}
38+
return matches[0] as utxolib.TxOutput<bigint>;
39+
});
40+
}
41+
42+
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
43+
throw new Error(`not implemented for Psbt yet`);
44+
}
45+
46+
throw new Error(`unknown tx type`);
47+
}

modules/utxo-bin/test/fixtures/formatTransaction/p2sh_networkFullSigned_all_prevOuts.txt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f
3434
│ │ ├── signed by: [0, 2]
3535
│ │ ├─┬ 0
3636
│ │ │ ├── bytes: 3044022048a059c22437630fdd5bc3c46bd1e30438a3623a59050d5e5f2fb1a36686fd9e02202ed8206b2f2472efd3ea145bc3c52703a7874fc747bdccc97cd5b862c8d62b7401 (71 bytes)
37-
│ │ │ ├── valid: false
37+
│ │ │ ├── valid: true
38+
│ │ │ ├── signedBy: user
3839
│ │ │ ├── isCanonical: true
3940
│ │ │ ├── hashType: 1
4041
│ │ │ ├── r: 48a059c22437630fdd5bc3c46bd1e30438a3623a59050d5e5f2fb1a36686fd9e (32 bytes)
4142
│ │ │ ├── s: 2ed8206b2f2472efd3ea145bc3c52703a7874fc747bdccc97cd5b862c8d62b74 (32 bytes)
4243
│ │ │ └── highS: false
4344
│ │ └─┬ 1
4445
│ │ ├── bytes: 30440220138087371ab51032457bdca8ef134eb566a61b16631cf6bc296c8b243f938bf70220720ab74bfa5e39f2c764d1879dd6fea9b1f287611b1ea714fe5b0928bd13d46901 (71 bytes)
45-
│ │ ├── valid: false
46+
│ │ ├── valid: true
47+
│ │ ├── signedBy: bitgo
4648
│ │ ├── isCanonical: true
4749
│ │ ├── hashType: 1
4850
│ │ ├── r: 138087371ab51032457bdca8ef134eb566a61b16631cf6bc296c8b243f938bf7 (32 bytes)
@@ -72,15 +74,17 @@ transaction: 58603ea06d55bd9dd6fec9b0d1ea5662d622f923e0f10045507723a893388e7f
7274
│ ├── signed by: [0, 2]
7375
│ ├─┬ 0
7476
│ │ ├── bytes: 3045022100afde55c0cfa9dc3a20fb8706c5a7f86c754efcbbaf866e09e84a18668c53148402203a7e0bb62cd5a951def97181f6a26d78b70c7ac22af79029f22b5b130785939f01 (72 bytes)
75-
│ │ ├── valid: false
77+
│ │ ├── valid: true
78+
│ │ ├── signedBy: user
7679
│ │ ├── isCanonical: true
7780
│ │ ├── hashType: 1
7881
│ │ ├── r: afde55c0cfa9dc3a20fb8706c5a7f86c754efcbbaf866e09e84a18668c531484 (32 bytes)
7982
│ │ ├── s: 3a7e0bb62cd5a951def97181f6a26d78b70c7ac22af79029f22b5b130785939f (32 bytes)
8083
│ │ └── highS: false
8184
│ └─┬ 1
8285
│ ├── bytes: 3044022033ae8e7c8ef2109b804bc338e884109e5ed9e534307761aa5695a0eea3ce0615022045b4af31359339b6c696ca0f956e844baa8dd642b0e74ad2475e8fe5b4c055e801 (71 bytes)
83-
│ ├── valid: false
86+
│ ├── valid: true
87+
│ ├── signedBy: bitgo
8488
│ ├── isCanonical: true
8589
│ ├── hashType: 1
8690
│ ├── r: 33ae8e7c8ef2109b804bc338e884109e5ed9e534307761aa5695a0eea3ce0615 (32 bytes)

0 commit comments

Comments
 (0)