Skip to content

Commit e213c46

Browse files
OttoAllmendingerllm-git
andcommitted
refactor(abstract-utxo): split sign.ts into separate modules
Split the large sign.ts file into smaller, more focused modules: - signLegacyTransaction.ts for legacy transaction format - signPsbt.ts for PSBT-related signing functionality - SigningError.ts for error classes This refactoring improves code organization and makes it easier to maintain the different signing pathways. Issue: BTC-2806 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 1eee8fe commit e213c46

File tree

9 files changed

+289
-228
lines changed

9 files changed

+289
-228
lines changed

modules/abstract-utxo/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export * from './address';
33
export * from './config';
44
export * from './recovery';
55
export * from './transaction/fixedScript/replayProtection';
6-
export * from './transaction/fixedScript/sign';
6+
export * from './transaction/fixedScript/signLegacyTransaction';
77

88
export { UtxoWallet } from './wallet';
99
export * as descriptor from './descriptor';

modules/abstract-utxo/src/recovery/backupKeyRecovery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { getMainnet, networks } from '@bitgo/utxo-lib';
1717

1818
import { AbstractUtxoCoin } from '../abstractUtxoCoin';
19-
import { signAndVerifyPsbt } from '../transaction/fixedScript/sign';
19+
import { signAndVerifyPsbt } from '../transaction/fixedScript/signPsbt';
2020
import { generateAddressWithChainAndIndex } from '../address';
2121

2222
import { forCoin, RecoveryProvider } from './RecoveryProvider';

modules/abstract-utxo/src/recovery/crossChainRecovery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BitGoBase, IWallet, Keychain, Triple, Wallet } from '@bitgo/sdk-core';
55
import { decrypt } from '@bitgo/sdk-api';
66

77
import { AbstractUtxoCoin, TransactionInfo } from '../abstractUtxoCoin';
8-
import { signAndVerifyWalletTransaction } from '../transaction/fixedScript/sign';
8+
import { signAndVerifyWalletTransaction } from '../transaction/fixedScript/signLegacyTransaction';
99

1010
const { unspentSum, scriptTypeForChain, outputScripts } = utxolib.bitgo;
1111
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
import type { PsbtParsedScriptType } from './signPsbt';
4+
5+
type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<TNumber>;
6+
7+
export class InputSigningError<TNumber extends number | bigint = number> extends Error {
8+
static expectedWalletUnspent<TNumber extends number | bigint>(
9+
inputIndex: number,
10+
inputType: PsbtParsedScriptType | null, // null for legacy transaction format
11+
unspent: Unspent<TNumber> | { id: string }
12+
): InputSigningError<TNumber> {
13+
return new InputSigningError(
14+
inputIndex,
15+
inputType,
16+
unspent,
17+
`not a wallet unspent, not a replay protection unspent`
18+
);
19+
}
20+
21+
constructor(
22+
public inputIndex: number,
23+
public inputType: PsbtParsedScriptType | null, // null for legacy transaction format
24+
public unspent: Unspent<TNumber> | { id: string },
25+
public reason: Error | string
26+
) {
27+
super(`signing error at input ${inputIndex}: type=${inputType} unspentId=${unspent.id}: ${reason}`);
28+
}
29+
}
30+
31+
export class TransactionSigningError<TNumber extends number | bigint = number> extends Error {
32+
constructor(signErrors: InputSigningError<TNumber>[], verifyError: InputSigningError<TNumber>[]) {
33+
super(
34+
`sign errors at inputs: [${signErrors.join(',')}], ` +
35+
`verify errors at inputs: [${verifyError.join(',')}], see log for details`
36+
);
37+
}
38+
}

modules/abstract-utxo/src/transaction/fixedScript/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export { explainPsbtWasm } from './explainPsbtWasm';
33
export { parseTransaction } from './parseTransaction';
44
export { CustomChangeOptions } from './parseOutput';
55
export { verifyTransaction } from './verifyTransaction';
6-
export { signTransaction, Musig2Participant } from './signTransaction';
7-
export * from './sign';
6+
export { signTransaction } from './signTransaction';
7+
export { Musig2Participant } from './signPsbt';
8+
export * from './signLegacyTransaction';
9+
export * from './SigningError';
810
export * from './replayProtection';

modules/abstract-utxo/src/transaction/fixedScript/sign.ts renamed to modules/abstract-utxo/src/transaction/fixedScript/signLegacyTransaction.ts

Lines changed: 46 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import assert from 'assert';
2+
13
import * as utxolib from '@bitgo/utxo-lib';
4+
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
5+
import { bitgo } from '@bitgo/utxo-lib';
6+
import { isTriple, Triple } from '@bitgo/sdk-core';
27
import debugLib from 'debug';
38

49
import { getReplayProtectionAddresses } from './replayProtection';
10+
import { InputSigningError, TransactionSigningError } from './SigningError';
511

612
const debug = debugLib('bitgo:v2:utxo');
713

@@ -11,132 +17,6 @@ type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<T
1117

1218
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
1319

14-
type PsbtParsedScriptType =
15-
| 'p2sh'
16-
| 'p2wsh'
17-
| 'p2shP2wsh'
18-
| 'p2shP2pk'
19-
| 'taprootKeyPathSpend'
20-
| 'taprootScriptPathSpend';
21-
22-
export class InputSigningError<TNumber extends number | bigint = number> extends Error {
23-
static expectedWalletUnspent<TNumber extends number | bigint>(
24-
inputIndex: number,
25-
inputType: PsbtParsedScriptType | null, // null for legacy transaction format
26-
unspent: Unspent<TNumber> | { id: string }
27-
): InputSigningError<TNumber> {
28-
return new InputSigningError(
29-
inputIndex,
30-
inputType,
31-
unspent,
32-
`not a wallet unspent, not a replay protection unspent`
33-
);
34-
}
35-
36-
constructor(
37-
public inputIndex: number,
38-
public inputType: PsbtParsedScriptType | null, // null for legacy transaction format
39-
public unspent: Unspent<TNumber> | { id: string },
40-
public reason: Error | string
41-
) {
42-
super(`signing error at input ${inputIndex}: type=${inputType} unspentId=${unspent.id}: ${reason}`);
43-
}
44-
}
45-
46-
export class TransactionSigningError<TNumber extends number | bigint = number> extends Error {
47-
constructor(signErrors: InputSigningError<TNumber>[], verifyError: InputSigningError<TNumber>[]) {
48-
super(
49-
`sign errors at inputs: [${signErrors.join(',')}], ` +
50-
`verify errors at inputs: [${verifyError.join(',')}], see log for details`
51-
);
52-
}
53-
}
54-
55-
/**
56-
* Sign all inputs of a psbt and verify signatures after signing.
57-
* Collects and logs signing errors and verification errors, throws error in the end if any of them
58-
* failed.
59-
*
60-
* If it is the last signature, finalize and extract the transaction from the psbt.
61-
*
62-
* This function mirrors signAndVerifyWalletTransaction, but is used for signing PSBTs instead of
63-
* using TransactionBuilder
64-
*
65-
* @param psbt
66-
* @param signerKeychain
67-
* @param isLastSignature
68-
*/
69-
export function signAndVerifyPsbt(
70-
psbt: utxolib.bitgo.UtxoPsbt,
71-
signerKeychain: utxolib.BIP32Interface,
72-
{
73-
isLastSignature,
74-
/** deprecated */
75-
allowNonSegwitSigningWithoutPrevTx,
76-
}: { isLastSignature: boolean; allowNonSegwitSigningWithoutPrevTx?: boolean }
77-
): utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint> {
78-
const txInputs = psbt.txInputs;
79-
const outputIds: string[] = [];
80-
const scriptTypes: PsbtParsedScriptType[] = [];
81-
82-
const signErrors: InputSigningError<bigint>[] = psbt.data.inputs
83-
.map((input, inputIndex: number) => {
84-
const outputId = utxolib.bitgo.formatOutputId(utxolib.bitgo.getOutputIdForInput(txInputs[inputIndex]));
85-
outputIds.push(outputId);
86-
87-
const { scriptType } = utxolib.bitgo.parsePsbtInput(input);
88-
scriptTypes.push(scriptType);
89-
90-
if (scriptType === 'p2shP2pk') {
91-
debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, psbt.data.inputs.length);
92-
return;
93-
}
94-
95-
try {
96-
psbt.signInputHD(inputIndex, signerKeychain);
97-
debug('Successfully signed input %d of %d', inputIndex + 1, psbt.data.inputs.length);
98-
} catch (e) {
99-
return new InputSigningError<bigint>(inputIndex, scriptType, { id: outputId }, e);
100-
}
101-
})
102-
.filter((e): e is InputSigningError<bigint> => e !== undefined);
103-
104-
const verifyErrors: InputSigningError<bigint>[] = psbt.data.inputs
105-
.map((input, inputIndex) => {
106-
const scriptType = scriptTypes[inputIndex];
107-
if (scriptType === 'p2shP2pk') {
108-
debug(
109-
'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)',
110-
inputIndex + 1,
111-
psbt.data.inputs.length
112-
);
113-
return;
114-
}
115-
116-
const outputId = outputIds[inputIndex];
117-
try {
118-
if (!psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain)) {
119-
return new InputSigningError(inputIndex, scriptType, { id: outputId }, new Error(`invalid signature`));
120-
}
121-
} catch (e) {
122-
debug('Invalid signature');
123-
return new InputSigningError<bigint>(inputIndex, scriptType, { id: outputId }, e);
124-
}
125-
})
126-
.filter((e): e is InputSigningError<bigint> => e !== undefined);
127-
128-
if (signErrors.length || verifyErrors.length) {
129-
throw new TransactionSigningError(signErrors, verifyErrors);
130-
}
131-
132-
if (isLastSignature) {
133-
psbt.finalizeAllInputs();
134-
return psbt.extractTransaction();
135-
}
136-
137-
return psbt;
138-
}
139-
14020
/**
14121
* Sign all inputs of a wallet transaction and verify signatures after signing.
14222
* Collects and logs signing errors and verification errors, throws error in the end if any of them
@@ -232,3 +112,43 @@ export function signAndVerifyWalletTransaction<TNumber extends number | bigint>(
232112

233113
return signedTransaction;
234114
}
115+
116+
export function signLegacyTransaction<TNumber extends number | bigint>(
117+
tx: utxolib.bitgo.UtxoTransaction<TNumber>,
118+
signerKeychain: BIP32Interface | undefined,
119+
params: {
120+
isLastSignature: boolean;
121+
signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined;
122+
txInfo: { unspents?: utxolib.bitgo.Unspent<TNumber>[] } | undefined;
123+
pubs: string[] | undefined;
124+
cosignerPub: string | undefined;
125+
}
126+
): utxolib.bitgo.UtxoTransaction<TNumber> {
127+
switch (params.signingStep) {
128+
case 'signerNonce':
129+
case 'cosignerNonce':
130+
/**
131+
* In certain cases, the caller of this method may not know whether the txHex contains a psbt with taproot key path spend input(s).
132+
* Instead of throwing error, no-op and return the txHex. So that the caller can call this method in the same sequence.
133+
*/
134+
return tx;
135+
}
136+
137+
if (tx.ins.length !== params.txInfo?.unspents?.length) {
138+
throw new Error('length of unspents array should equal to the number of transaction inputs');
139+
}
140+
141+
if (!params.pubs || !isTriple(params.pubs)) {
142+
throw new Error(`must provide xpub array`);
143+
}
144+
145+
const keychains = params.pubs.map((pub) => bip32.fromBase58(pub)) as Triple<BIP32Interface>;
146+
const cosignerPub = params.cosignerPub ?? params.pubs[2];
147+
const cosignerKeychain = bip32.fromBase58(cosignerPub);
148+
149+
assert(signerKeychain);
150+
const walletSigner = new bitgo.WalletUnspentSigner<RootWalletKeys>(keychains, signerKeychain, cosignerKeychain);
151+
return signAndVerifyWalletTransaction(tx, params.txInfo.unspents, walletSigner, {
152+
isLastSignature: params.isLastSignature,
153+
}) as utxolib.bitgo.UtxoTransaction<TNumber>;
154+
}

0 commit comments

Comments
 (0)