Skip to content

Commit ad504fd

Browse files
refactor(abstract-utxo): use better keychain types
Issue: BTC-1450
1 parent 345584c commit ad504fd

File tree

7 files changed

+66
-45
lines changed

7 files changed

+66
-45
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
isTriple,
3535
ITransactionExplanation as BaseTransactionExplanation,
3636
IWallet,
37-
Keychain,
3837
KeychainsTriplet,
3938
KeyIndices,
4039
P2shP2wshUnsupportedError,
@@ -75,7 +74,7 @@ import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptor
7574

7675
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
7776
import { CustomChangeOptions } from './transaction/fixedScript';
78-
import { NamedKeychains, toBip32Triple } from './keychains';
77+
import { toBip32Triple, UtxoKeychain, UtxoNamedKeychains } from './keychains';
7978

8079
const debug = debugLib('bitgo:v2:utxo');
8180

@@ -225,7 +224,7 @@ export interface ParseTransactionOptions<TNumber extends number | bigint = numbe
225224
}
226225

227226
export type ParsedTransaction<TNumber extends number | bigint = number> = {
228-
keychains: NamedKeychains;
227+
keychains: UtxoNamedKeychains;
229228
keySignatures: {
230229
backupPub?: string;
231230
bitgoPub?: string;
@@ -338,7 +337,7 @@ export interface VerifyKeySignaturesOptions {
338337
}
339338

340339
export interface VerifyUserPublicKeyOptions {
341-
userKeychain?: Keychain;
340+
userKeychain?: UtxoKeychain;
342341
disableNetworking: boolean;
343342
txParams: TransactionParams;
344343
}

modules/abstract-utxo/src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,3 @@ export * from './replayProtection';
55
export * from './sign';
66

77
export * as descriptor from './descriptor';
8-
export { fetchKeychains } from './keychains';
9-
export { toKeychainTriple } from './keychains';
10-
export { NamedKeychains } from './keychains';
Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
1+
import * as t from 'io-ts';
12
import * as utxolib from '@bitgo/utxo-lib';
2-
import { IRequestTracer, IWallet, Keychain, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
3+
import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
34

45
import { AbstractUtxoCoin } from './abstractUtxoCoin';
6+
import assert from 'assert';
57

6-
export type NamedKeychains = {
7-
user?: Keychain;
8-
backup?: Keychain;
9-
bitgo?: Keychain;
10-
};
8+
/*
119
12-
export function toKeychainTriple(keychains: NamedKeychains): Triple<Keychain> {
10+
The standard Keychain type from sdk-core requires a bunch of uninteresting parameters like `id` and `type` and leaves
11+
important fields like `pub` and `prv` optional.
12+
13+
This is a more focused type that only includes the fields we care about.
14+
*/
15+
16+
/**
17+
* A keychain for a UTXO wallet.
18+
*/
19+
export const UtxoKeychain = t.intersection(
20+
[
21+
t.type({
22+
pub: t.string,
23+
}),
24+
t.partial({
25+
prv: t.string,
26+
encryptedPrv: t.string,
27+
}),
28+
],
29+
'UtxoKeychain'
30+
);
31+
32+
export type UtxoKeychain = t.TypeOf<typeof UtxoKeychain>;
33+
34+
export const UtxoNamedKeychains = t.type({
35+
user: UtxoKeychain,
36+
backup: UtxoKeychain,
37+
bitgo: UtxoKeychain,
38+
});
39+
40+
export type UtxoNamedKeychains = t.TypeOf<typeof UtxoNamedKeychains>;
41+
42+
export function toKeychainTriple(keychains: UtxoNamedKeychains): Triple<UtxoKeychain> {
1343
const { user, backup, bitgo } = keychains;
14-
if (!user || !backup || !bitgo) {
15-
throw new Error('keychains must include user, backup, and bitgo');
16-
}
1744
return [user, backup, bitgo];
1845
}
1946

@@ -28,10 +55,12 @@ export async function fetchKeychains(
2855
coin: AbstractUtxoCoin,
2956
wallet: IWallet,
3057
reqId?: IRequestTracer
31-
): Promise<NamedKeychains> {
32-
return promiseProps({
58+
): Promise<UtxoNamedKeychains> {
59+
const result = await promiseProps({
3360
user: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.USER], reqId }),
3461
backup: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.BACKUP], reqId }),
3562
bitgo: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.BITGO], reqId }),
3663
});
64+
assert(UtxoNamedKeychains.is(result));
65+
return result;
3766
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {
55
IRequestTracer,
66
InvalidAddressDerivationPropertyError,
77
IWallet,
8-
Keychain,
98
TransactionPrebuild,
109
UnexpectedAddressError,
1110
VerificationOptions,
1211
ITransactionRecipient,
12+
Triple,
1313
} from '@bitgo/sdk-core';
1414
import { AbstractUtxoCoin, Output, isWalletOutput } from '../../abstractUtxoCoin';
1515

@@ -182,16 +182,16 @@ async function fetchAddressDetails({
182182
}
183183

184184
export interface CustomChangeOptions {
185-
keys: [Keychain, Keychain, Keychain];
186-
signatures: [string, string, string];
185+
keys: Triple<{ pub: string }>;
186+
signatures: Triple<string>;
187187
}
188188

189189
export interface ParseOutputOptions {
190190
currentOutput: Output;
191191
coin: AbstractUtxoCoin;
192192
txPrebuild: TransactionPrebuild;
193193
verification: VerificationOptions;
194-
keychainArray: [Keychain, Keychain, Keychain];
194+
keychainArray: Triple<{ pub: string }>;
195195
wallet: IWallet;
196196
txParams: {
197197
recipients: ITransactionRecipient[];
@@ -248,7 +248,7 @@ export async function parseOutput({
248248
if (isWalletOutput(currentOutput)) {
249249
const res = await coin.isWalletAddress({
250250
addressType: AbstractUtxoCoin.inferAddressType({ chain: currentOutput.chain }) || undefined,
251-
keychains: keychainArray as { pub: string; commonKeychain?: string | undefined }[],
251+
keychains: keychainArray,
252252
address: currentAddress,
253253
chain: currentOutput.chain,
254254
index: currentOutput.index,

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'assert';
22
import _ from 'lodash';
3-
import { Keychain, Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
3+
import { Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
44
import { CustomChangeOptions, parseOutput } from './parseOutput';
55
import * as utxolib from '@bitgo/utxo-lib';
66
import {
@@ -11,7 +11,7 @@ import {
1111
ParsedTransaction,
1212
ParseTransactionOptions,
1313
} from '../../abstractUtxoCoin';
14-
import { fetchKeychains, toKeychainTriple } from '../../keychains';
14+
import { fetchKeychains, toKeychainTriple, UtxoKeychain, UtxoNamedKeychains } from '../../keychains';
1515

1616
/**
1717
* @param first
@@ -44,19 +44,19 @@ export async function parseTransaction<TNumber extends bigint | number>(
4444
const disableNetworking = verification.disableNetworking;
4545

4646
// obtain the keychains and key signatures
47-
let keychains: VerificationOptions['keychains'] | undefined = verification.keychains;
47+
let keychains: UtxoNamedKeychains | VerificationOptions['keychains'] | undefined = verification.keychains;
4848
if (!keychains) {
4949
if (disableNetworking) {
5050
throw new Error('cannot fetch keychains without networking');
5151
}
5252
keychains = await fetchKeychains(coin, wallet, reqId);
5353
}
5454

55-
if (!keychains || !keychains.user || !keychains.backup || !keychains.bitgo) {
56-
throw new Error('keychains are required, but could not be fetched');
55+
if (!UtxoNamedKeychains.is(keychains)) {
56+
throw new Error('invalid keychains');
5757
}
5858

59-
const keychainArray: Triple<Keychain> = [keychains.user, keychains.backup, keychains.bitgo];
59+
const keychainArray: Triple<UtxoKeychain> = toKeychainTriple(keychains);
6060

6161
if (_.isUndefined(txPrebuild.txHex)) {
6262
throw new Error('missing required txPrebuild property txHex');
@@ -130,7 +130,7 @@ export async function parseTransaction<TNumber extends bigint | number>(
130130
}
131131

132132
if (customChangeKeys.user && customChangeKeys.backup && customChangeKeys.bitgo && customChangeWallet) {
133-
const customChangeKeychains: [Keychain, Keychain, Keychain] = [
133+
const customChangeKeychains: Triple<UtxoKeychain> = [
134134
customChangeKeys.user,
135135
customChangeKeys.backup,
136136
customChangeKeys.bitgo,

modules/abstract-utxo/src/verifyKey.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import assert from 'assert';
88
import * as utxolib from '@bitgo/utxo-lib';
99
import { bip32 } from '@bitgo/utxo-lib';
1010
import * as bitcoinMessage from 'bitcoinjs-message';
11-
import { BitGoBase, decryptKeychainPrivateKey, Keychain, KeyIndices } from '@bitgo/sdk-core';
11+
import { BitGoBase, decryptKeychainPrivateKey, KeyIndices } from '@bitgo/sdk-core';
1212
import { ParsedTransaction, VerifyKeySignaturesOptions, VerifyUserPublicKeyOptions } from './abstractUtxoCoin';
13+
import { UtxoKeychain } from './keychains';
14+
1315
const debug = buildDebug('bitgo:abstract-utxo:verifyKey');
1416

1517
/**
@@ -69,7 +71,7 @@ export function verifyKeySignature(params: VerifyKeySignaturesOptions): boolean
6971
*/
7072
export function verifyCustomChangeKeySignatures<TNumber extends number | bigint>(
7173
tx: ParsedTransaction<TNumber>,
72-
userKeychain: Keychain
74+
userKeychain: UtxoKeychain
7375
): boolean {
7476
if (!tx.customChange) {
7577
throw new Error('parsed transaction is missing required custom change verification data');
@@ -88,13 +90,7 @@ export function verifyCustomChangeKeySignatures<TNumber extends number | bigint>
8890
if (!keySignature) {
8991
throw new Error(`missing required custom change ${KeyIndices[keyIndex].toLowerCase()} keychain signature`);
9092
}
91-
if (
92-
!verifyKeySignature({
93-
userKeychain: userKeychain as { pub: string },
94-
keychainToVerify: keychainToVerify as { pub: string },
95-
keySignature,
96-
})
97-
) {
93+
if (!verifyKeySignature({ userKeychain, keychainToVerify, keySignature })) {
9894
debug('failed to verify custom change %s key signature!', KeyIndices[keyIndex].toLowerCase());
9995
return false;
10096
}

modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ describe('Abstract UTXO Coin:', () => {
418418

419419
it('should not allow more than 150 basis points of implicit external outputs (for paygo outputs)', async () => {
420420
const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
421-
keychains: {},
421+
keychains: {} as any,
422422
keySignatures: {},
423423
outputs: [],
424424
missingOutputs: [],
@@ -445,7 +445,7 @@ describe('Abstract UTXO Coin:', () => {
445445

446446
it('should allow 150 basis points of implicit external outputs (for paygo outputs)', async () => {
447447
const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
448-
keychains: {},
448+
keychains: {} as any,
449449
keySignatures: {},
450450
outputs: [],
451451
missingOutputs: [],
@@ -479,7 +479,7 @@ describe('Abstract UTXO Coin:', () => {
479479

480480
it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => {
481481
const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
482-
keychains: {},
482+
keychains: {} as any,
483483
keySignatures: {},
484484
outputs: [],
485485
missingOutputs: [],
@@ -511,7 +511,7 @@ describe('Abstract UTXO Coin:', () => {
511511

512512
it('should allow paygo outputs if empty verification object is passed', async () => {
513513
const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
514-
keychains: {},
514+
keychains: {} as any,
515515
keySignatures: {},
516516
outputs: [],
517517
missingOutputs: [],
@@ -549,7 +549,7 @@ describe('Abstract UTXO Coin:', () => {
549549
const bigintCoin = bitgo.coin('tdoge') as AbstractUtxoCoin;
550550

551551
const coinMock = sinon.stub(bigintCoin, 'parseTransaction').resolves({
552-
keychains: {},
552+
keychains: {} as any,
553553
keySignatures: {},
554554
outputs: [],
555555
missingOutputs: [],

0 commit comments

Comments
 (0)