-
Notifications
You must be signed in to change notification settings - Fork 302
Expand file tree
/
Copy pathparseOutput.ts
More file actions
332 lines (307 loc) · 13.1 KB
/
parseOutput.ts
File metadata and controls
332 lines (307 loc) · 13.1 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import debugLib from 'debug';
import _ from 'lodash';
import {
AddressVerificationData,
IRequestTracer,
InvalidAddressDerivationPropertyError,
IWallet,
TransactionPrebuild,
UnexpectedAddressError,
VerificationOptions,
ITransactionRecipient,
Triple,
} from '@bitgo/sdk-core';
import { AbstractUtxoCoin } from '../../abstractUtxoCoin';
import { Output, FixedScriptWalletOutput } from '../types';
import { fromExtendedAddressFormatToScript } from '../recipient';
const debug = debugLib('bitgo:v2:parseoutput');
export function isWalletOutput(output: Output): output is FixedScriptWalletOutput {
return (
(output as FixedScriptWalletOutput).chain !== undefined && (output as FixedScriptWalletOutput).index !== undefined
);
}
interface HandleVerifyAddressErrorResponse {
external: boolean;
needsCustomChangeKeySignatureVerification?: boolean;
}
/**
* Check an address which failed initial validation to see if it's the base address of a migrated v1 bch wallet.
*
* The wallet in question could be a migrated SafeHD BCH wallet, and the transaction we
* are currently parsing is trying to spend change back to the v1 wallet base address.
*
* It does this since we don't allow new address creation for these wallets,
* and instead return the base address from the v1 wallet when a new address is requested.
* If this new address is requested for the purposes of spending change back to the wallet,
* the change will go to the v1 wallet base address. This address *is* on the wallet,
* but it will still cause an error to be thrown by verifyAddress, since the derivation path
* used for this address is non-standard. (I have seen these addresses derived using paths m/0/0 and m/101,
* whereas the v2 addresses are derived using path m/0/0/${chain}/${index}).
*
* This means we need to check for this case explicitly in this catch block, and classify
* these types of outputs as internal instead of external. Failing to do so would cause the
* transaction's implicit external outputs (ie, outputs which go to addresses not specified in
* the recipients array) to add up to more than the 150 basis point limit which we enforce on
* pay-as-you-go outputs (which should be the only implicit external outputs on our transactions).
*
* The 150 basis point limit for implicit external sends is enforced in verifyTransaction,
* which calls this function to get information on the total external/internal spend amounts
* for a transaction. The idea here is to protect from the transaction being maliciously modified
* to add more implicit external spends (eg, to an attacker-controlled wallet).
*
* See verifyTransaction for more information on how transaction prebuilds are verified before signing.
*
* @param wallet {Wallet} wallet which is making the transaction
* @param currentAddress {string} address to check for externality relative to v1 wallet base address
*/
function isMigratedAddress(wallet: IWallet, currentAddress: string): boolean {
if (_.isString(wallet.migratedFrom()) && wallet.migratedFrom() === currentAddress) {
debug('found address %s which was migrated from v1 wallet, address is not external', currentAddress);
return true;
}
return false;
}
interface VerifyCustomChangeAddressOptions {
coin: AbstractUtxoCoin;
customChangeKeys: HandleVerifyAddressErrorOptions['customChangeKeys'];
addressDetails: HandleVerifyAddressErrorOptions['addressDetails'];
currentAddress: HandleVerifyAddressErrorOptions['currentAddress'];
}
/**
* Check to see if an address is derived from the given custom change keys
* @param {VerifyCustomChangeAddressOptions} params
* @return {boolean}
*/
async function verifyCustomChangeAddress(params: VerifyCustomChangeAddressOptions): Promise<boolean> {
const { coin, customChangeKeys, addressDetails, currentAddress } = params;
try {
return await coin.verifyAddress(
_.extend({}, addressDetails, {
keychains: customChangeKeys,
address: currentAddress,
})
);
} catch (e) {
debug('failed to verify custom change address %s', currentAddress);
return false;
}
}
interface HandleVerifyAddressErrorOptions {
e: Error;
currentAddress: string;
wallet: IWallet;
txParams: {
changeAddress?: string;
};
customChangeKeys?: CustomChangeOptions['keys'];
coin: AbstractUtxoCoin;
addressDetails?: any;
considerMigratedFromAddressInternal?: boolean;
}
async function handleVerifyAddressError({
e,
currentAddress,
wallet,
txParams,
customChangeKeys,
coin,
addressDetails,
considerMigratedFromAddressInternal,
}: HandleVerifyAddressErrorOptions): Promise<HandleVerifyAddressErrorResponse> {
// Todo: name server-side errors to avoid message-based checking [BG-5124]
const walletAddressNotFound = e.message.includes('wallet address not found');
const unexpectedAddress = e instanceof UnexpectedAddressError;
if (walletAddressNotFound || unexpectedAddress) {
if (unexpectedAddress && !walletAddressNotFound) {
// check to see if this is a migrated v1 bch address - it could be internal
const isMigrated = isMigratedAddress(wallet, currentAddress);
if (isMigrated) {
return { external: considerMigratedFromAddressInternal === false };
}
debug('Address %s was found on wallet but could not be reconstructed', currentAddress);
// attempt to verify address using custom change address keys if the wallet has that feature enabled
if (
customChangeKeys &&
(await verifyCustomChangeAddress({ coin, addressDetails, currentAddress, customChangeKeys }))
) {
// address is valid against the custom change keys. Mark address as not external
// and request signature verification for the custom change keys
debug('Address %s verified as derived from the custom change keys', currentAddress);
return { external: false, needsCustomChangeKeySignatureVerification: true };
}
}
// the address was found, but not on the wallet, which simply means it's external
debug('Address %s presumed external', currentAddress);
return { external: true };
} else if (e instanceof InvalidAddressDerivationPropertyError && currentAddress === txParams.changeAddress) {
// expect to see this error when passing in a custom changeAddress with no chain or index
return { external: false };
}
console.error('Address classification failed for address', currentAddress);
console.trace(e);
/**
* It might be a completely invalid address or a bad validation attempt or something else completely, in
* which case we do not proceed and rather rethrow the error, which is safer than assuming that the address
* validation failed simply because it's external to the wallet.
*/
throw e;
}
interface FetchAddressDetailsOptions {
reqId?: IRequestTracer;
disableNetworking: boolean;
addressDetailsPrebuild: any;
addressDetailsVerification: any;
currentAddress: string;
wallet: IWallet;
}
async function fetchAddressDetails({
reqId,
disableNetworking,
addressDetailsPrebuild,
addressDetailsVerification,
currentAddress,
wallet,
}: FetchAddressDetailsOptions) {
let addressDetails = _.extend({}, addressDetailsPrebuild, addressDetailsVerification);
debug('Locally available address %s details: %O', currentAddress, addressDetails);
if (_.isEmpty(addressDetails) && !disableNetworking) {
addressDetails = await wallet.getAddress({ address: currentAddress, reqId });
debug('Downloaded address %s details: %O', currentAddress, addressDetails);
}
return addressDetails;
}
export interface CustomChangeOptions {
keys: Triple<{ pub: string }>;
signatures: Triple<string>;
}
export interface ParseOutputOptions {
currentOutput: Output;
coin: AbstractUtxoCoin;
txPrebuild: TransactionPrebuild;
verification: VerificationOptions;
keychainArray: Triple<{ pub: string }>;
wallet: IWallet;
txParams: {
recipients: ITransactionRecipient[];
changeAddress?: string;
};
customChange?: CustomChangeOptions;
reqId?: IRequestTracer;
}
export async function parseOutput({
currentOutput,
coin,
txPrebuild,
verification,
keychainArray,
wallet,
txParams,
customChange,
reqId,
}: ParseOutputOptions): Promise<Output> {
const disableNetworking = !!verification.disableNetworking;
const currentAddress = currentOutput.address;
if (currentAddress === undefined) {
// In the case that the address is undefined, it means that the output has a non-encodeable scriptPubkey
// If this is the case, then we need to check that the amount is 0 and we can skip the rest.
if (currentOutput.amount.toString() !== '0') {
throw new Error('output with undefined address must have amount of 0');
}
return currentOutput;
}
// attempt to grab the address details from either the prebuilt tx, or the verification params.
// If both of these are empty, then we will try to get the address details from bitgo instead
const addressDetailsPrebuild = _.get(txPrebuild, `txInfo.walletAddressDetails.${currentAddress}`, {});
const addressDetailsVerification: AddressVerificationData = verification?.addresses?.[currentAddress] ?? {};
debug('Parsing address details for %s', currentAddress);
let currentAddressDetails = undefined;
const RECIPIENT_THRESHOLD = 1000;
try {
// In the case of PSBTs, we can already determine the internal/external status of the output addresses
// based on the derivation information being included in the PSBT. We can short circuit GET v2.wallet.address
// and save on network requests. Since we have the derivation information already, we can still verify the address
if (currentOutput.external !== undefined) {
// In the case that we have a custom change wallet, we need to verify the address against the custom change keys
// and not the wallet keys. This check is done in the handleVerifyAddressError function if this error is thrown.
if (customChange !== undefined) {
throw new UnexpectedAddressError('`address validation failure');
}
// If it is an internal address, we can skip the network request and just verify the address locally with the
// derivation information we have. Otherwise, if the address is external, which is the only remaining case, we
// can just return the current output as is without contacting the server.
if (isWalletOutput(currentOutput)) {
const res = await coin.isWalletAddress({
keychains: keychainArray,
address: currentAddress,
chain: currentOutput.chain,
index: currentOutput.index,
});
if (!res) {
throw new UnexpectedAddressError();
}
}
return currentOutput;
}
/**
* For transaction with the legacy transaction format, the only way to
* determine whether an address is known on the wallet is to initiate a
* network request and fetch it. Should the request fail and return a 404,
* it will throw and therefore has to be caught. For that reason, address
* wallet ownership detection is wrapped in a try/catch. Additionally, once
* the address details are fetched on the wallet, a local address validation
* is run, whose errors however are generated client-side and can therefore
* be analyzed with more granularity and type checking.
*/
/**
* In order to minimize API requests, we assume that explicit recipients are always external when the
* recipient list is > 1000 This is not always a valid assumption and could lead greater apparent spend (but never lower)
*/
if (txParams.recipients !== undefined && txParams.recipients.length > RECIPIENT_THRESHOLD) {
const isCurrentAddressInRecipients = txParams.recipients.some((recipient) =>
fromExtendedAddressFormatToScript(recipient.address, coin.name).equals(
fromExtendedAddressFormatToScript(currentAddress, coin.name)
)
);
if (isCurrentAddressInRecipients) {
return { ...currentOutput };
}
}
const addressDetails = await fetchAddressDetails({
reqId,
addressDetailsVerification,
addressDetailsPrebuild,
currentAddress,
disableNetworking,
wallet,
});
// verify that the address is on the wallet. verifyAddress throws if
// it fails to correctly rederive the address, meaning it's external
currentAddressDetails = addressDetails;
await coin.verifyAddress(
_.extend({}, addressDetails, {
keychains: keychainArray,
address: currentAddress,
})
);
debug('Address %s verification passed', currentAddress);
// verify address succeeded without throwing, so the address was
// correctly rederived from the wallet keychains, making it not external
return _.extend({}, currentOutput, addressDetails, { external: false });
} catch (e) {
debug('Address %s verification threw an error:', currentAddress, e);
return _.extend(
{},
currentOutput,
await handleVerifyAddressError({
e,
coin,
currentAddress,
wallet,
txParams,
customChangeKeys: customChange && customChange.keys,
addressDetails: currentAddressDetails,
considerMigratedFromAddressInternal: verification.considerMigratedFromAddressInternal,
})
);
}
}