-
Notifications
You must be signed in to change notification settings - Fork 302
Expand file tree
/
Copy pathsign.ts
More file actions
227 lines (201 loc) · 8.31 KB
/
sign.ts
File metadata and controls
227 lines (201 loc) · 8.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import * as utxolib from '@bitgo/utxo-lib';
import debugLib from 'debug';
import { getReplayProtectionAddresses } from './replayProtection';
const debug = debugLib('bitgo:v2:utxo');
const { isWalletUnspent, signInputWithUnspent, toOutput } = utxolib.bitgo;
type Unspent<TNumber extends number | bigint = number> = utxolib.bitgo.Unspent<TNumber>;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type PsbtParsedScriptTypes =
| 'p2sh'
| 'p2wsh'
| 'p2shP2wsh'
| 'p2shP2pk'
| 'taprootKeyPathSpend'
| 'taprootScriptPathSpend';
export class InputSigningError<TNumber extends number | bigint = number> extends Error {
static expectedWalletUnspent<TNumber extends number | bigint>(
inputIndex: number,
unspent: Unspent<TNumber> | { id: string }
): InputSigningError<TNumber> {
return new InputSigningError(inputIndex, unspent, `not a wallet unspent, not a replay protection unspent`);
}
constructor(
public inputIndex: number,
public unspent: Unspent<TNumber> | { id: string },
public reason: Error | string
) {
super(`signing error at input ${inputIndex}: unspentId=${unspent.id}: ${reason}`);
}
}
export class TransactionSigningError<TNumber extends number | bigint = number> extends Error {
constructor(signErrors: InputSigningError<TNumber>[], verifyError: InputSigningError<TNumber>[]) {
super(
`sign errors at inputs: [${signErrors.join(',')}], ` +
`verify errors at inputs: [${verifyError.join(',')}], see log for details`
);
}
}
/**
* Sign all inputs of a psbt and verify signatures after signing.
* Collects and logs signing errors and verification errors, throws error in the end if any of them
* failed.
*
* If it is the last signature, finalize and extract the transaction from the psbt.
*
* This function mirrors signAndVerifyWalletTransaction, but is used for signing PSBTs instead of
* using TransactionBuilder
*
* @param psbt
* @param signerKeychain
* @param isLastSignature
*/
export function signAndVerifyPsbt(
psbt: utxolib.bitgo.UtxoPsbt,
signerKeychain: utxolib.BIP32Interface,
{
isLastSignature,
/** deprecated */
allowNonSegwitSigningWithoutPrevTx,
}: { isLastSignature: boolean; allowNonSegwitSigningWithoutPrevTx?: boolean }
): utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint> {
const txInputs = psbt.txInputs;
const outputIds: string[] = [];
const scriptTypes: PsbtParsedScriptTypes[] = [];
const signErrors: InputSigningError<bigint>[] = psbt.data.inputs
.map((input, inputIndex: number) => {
const outputId = utxolib.bitgo.formatOutputId(utxolib.bitgo.getOutputIdForInput(txInputs[inputIndex]));
outputIds.push(outputId);
const { scriptType } = utxolib.bitgo.parsePsbtInput(input);
scriptTypes.push(scriptType);
if (scriptType === 'p2shP2pk') {
debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, psbt.data.inputs.length);
return;
}
try {
psbt.signInputHD(inputIndex, signerKeychain);
debug('Successfully signed input %d of %d', inputIndex + 1, psbt.data.inputs.length);
} catch (e) {
return new InputSigningError<bigint>(inputIndex, { id: outputId }, e);
}
})
.filter((e): e is InputSigningError<bigint> => e !== undefined);
const verifyErrors: InputSigningError<bigint>[] = psbt.data.inputs
.map((input, inputIndex) => {
const scriptType = scriptTypes[inputIndex];
if (scriptType === 'p2shP2pk') {
debug(
'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)',
inputIndex + 1,
psbt.data.inputs.length
);
return;
}
const outputId = outputIds[inputIndex];
try {
if (!psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain)) {
return new InputSigningError(inputIndex, { id: outputId }, new Error(`invalid signature`));
}
} catch (e) {
debug('Invalid signature');
return new InputSigningError<bigint>(inputIndex, { id: outputId }, e);
}
})
.filter((e): e is InputSigningError<bigint> => e !== undefined);
if (signErrors.length || verifyErrors.length) {
throw new TransactionSigningError(signErrors, verifyErrors);
}
if (isLastSignature) {
psbt.finalizeAllInputs();
return psbt.extractTransaction();
}
return psbt;
}
/**
* Sign all inputs of a wallet transaction and verify signatures after signing.
* Collects and logs signing errors and verification errors, throws error in the end if any of them
* failed.
*
* @param transaction - wallet transaction (builder) to be signed
* @param unspents - transaction unspents
* @param walletSigner - signing parameters
* @param isLastSignature - Returns full-signed transaction when true. Builds half-signed when false.
* @param replayProtectionAddresses - List of replay protection addresses to skip signing
*/
export function signAndVerifyWalletTransaction<TNumber extends number | bigint>(
transaction: utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoTransactionBuilder<TNumber>,
unspents: Unspent<TNumber>[],
walletSigner: utxolib.bitgo.WalletUnspentSigner<RootWalletKeys>,
{
isLastSignature,
replayProtectionAddresses,
}: {
isLastSignature: boolean;
replayProtectionAddresses?: string[];
}
): utxolib.bitgo.UtxoTransaction<TNumber> {
const network = transaction.network as utxolib.Network;
if (replayProtectionAddresses === undefined) {
replayProtectionAddresses = getReplayProtectionAddresses(network);
}
const prevOutputs = unspents.map((u) => toOutput(u, network));
let txBuilder: utxolib.bitgo.UtxoTransactionBuilder<TNumber>;
if (transaction instanceof utxolib.bitgo.UtxoTransaction) {
txBuilder = utxolib.bitgo.createTransactionBuilderFromTransaction<TNumber>(transaction, prevOutputs);
if (transaction.ins.length !== unspents.length) {
throw new Error(`transaction inputs must match unspents`);
}
} else if (transaction instanceof utxolib.bitgo.UtxoTransactionBuilder) {
txBuilder = transaction;
} else {
throw new Error(`must pass UtxoTransaction or UtxoTransactionBuilder`);
}
const signErrors: InputSigningError<TNumber>[] = unspents
.map((unspent: Unspent<TNumber>, inputIndex: number) => {
if (replayProtectionAddresses.includes(unspent.address)) {
debug('Skipping signature for input %d of %d (RP input?)', inputIndex + 1, unspents.length);
return;
}
if (!isWalletUnspent<TNumber>(unspent)) {
return InputSigningError.expectedWalletUnspent<TNumber>(inputIndex, unspent);
}
try {
signInputWithUnspent<TNumber>(txBuilder, inputIndex, unspent, walletSigner);
debug('Successfully signed input %d of %d', inputIndex + 1, unspents.length);
} catch (e) {
return new InputSigningError<TNumber>(inputIndex, unspent, e);
}
})
.filter((e): e is InputSigningError<TNumber> => e !== undefined);
const signedTransaction = isLastSignature ? txBuilder.build() : txBuilder.buildIncomplete();
const verifyErrors: InputSigningError<TNumber>[] = signedTransaction.ins
.map((input, inputIndex) => {
const unspent = unspents[inputIndex] as Unspent<TNumber>;
if (replayProtectionAddresses.includes(unspent.address)) {
debug(
'Skipping input signature %d of %d (unspent from replay protection address which is platform signed only)',
inputIndex + 1,
unspents.length
);
return;
}
if (!isWalletUnspent<TNumber>(unspent)) {
return InputSigningError.expectedWalletUnspent<TNumber>(inputIndex, unspent);
}
try {
const publicKey = walletSigner.deriveForChainAndIndex(unspent.chain, unspent.index).signer.publicKey;
if (
!utxolib.bitgo.verifySignatureWithPublicKey<TNumber>(signedTransaction, inputIndex, prevOutputs, publicKey)
) {
return new InputSigningError(inputIndex, unspent, new Error(`invalid signature`));
}
} catch (e) {
debug('Invalid signature');
return new InputSigningError<TNumber>(inputIndex, unspent, e);
}
})
.filter((e): e is InputSigningError<TNumber> => e !== undefined);
if (signErrors.length || verifyErrors.length) {
throw new TransactionSigningError(signErrors, verifyErrors);
}
return signedTransaction;
}