Skip to content

Commit d329d98

Browse files
Merge pull request #5254 from BitGo/BTC-1450.guard-descriptor-wallets.2
feat(abstract-utxo): guard descriptor wallets
2 parents 807ff70 + 45dda5a commit d329d98

File tree

9 files changed

+107
-36
lines changed

9 files changed

+107
-36
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,20 @@ import { supportedCrossChainRecoveries } from './config';
6666
import {
6767
assertValidTransactionRecipient,
6868
explainTx,
69+
parseTransaction,
70+
verifyTransaction,
6971
fromExtendedAddressFormat,
7072
isScriptRecipient,
7173
} from './transaction';
72-
import { assertDescriptorWalletAddress } from './descriptor';
74+
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
7375

7476
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
75-
import { CustomChangeOptions, parseTransaction } from './transaction/fixedScript';
77+
import { CustomChangeOptions } from './transaction/fixedScript';
7678
import { NamedKeychains } from './keychains';
7779

7880
const debug = debugLib('bitgo:v2:utxo');
7981

8082
import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
81-
import { verifyTransaction } from './transaction/fixedScript/verifyTransaction';
8283
import { verifyKeySignature, verifyUserPublicKey } from './verifyKey';
8384

8485
type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
@@ -107,8 +108,13 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
107108
};
108109

109110
const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } = bitgo;
111+
110112
type Unspent<TNumber extends number | bigint = number> = bitgo.Unspent<TNumber>;
111113

114+
type DecodedTransaction<TNumber extends number | bigint> =
115+
| utxolib.bitgo.UtxoTransaction<TNumber>
116+
| utxolib.bitgo.UtxoPsbt;
117+
112118
type RootWalletKeys = bitgo.RootWalletKeys;
113119

114120
export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific;
@@ -516,7 +522,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
516522
if (_.isUndefined(prebuild.txHex)) {
517523
throw new Error('missing required txPrebuild property txHex');
518524
}
519-
const tx = this.decodeTransaction(prebuild.txHex);
525+
const tx = this.decodeTransactionFromPrebuild(prebuild);
520526
if (_.isUndefined(prebuild.blockHeight)) {
521527
prebuild.blockHeight = (await this.getLatestBlockHeight()) as number;
522528
}
@@ -559,9 +565,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
559565
return utxolib.bitgo.createTransactionFromHex<TNumber>(hex, this.network, this.amountType);
560566
}
561567

562-
decodeTransaction<TNumber extends number | bigint>(
563-
input: Buffer | string
564-
): utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoPsbt {
568+
decodeTransaction<TNumber extends number | bigint>(input: Buffer | string): DecodedTransaction<TNumber> {
565569
if (typeof input === 'string') {
566570
for (const format of ['hex', 'base64'] as const) {
567571
const buffer = Buffer.from(input, format);
@@ -586,6 +590,17 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
586590
}
587591
}
588592

593+
decodeTransactionFromPrebuild<TNumber extends number | bigint>(prebuild: {
594+
txHex?: string;
595+
txBase64?: string;
596+
}): DecodedTransaction<TNumber> {
597+
const string = prebuild.txHex ?? prebuild.txBase64;
598+
if (!string) {
599+
throw new Error('missing required txHex or txBase64 property');
600+
}
601+
return this.decodeTransaction(string);
602+
}
603+
589604
toCanonicalTransactionRecipient(output: { valueString: string; address?: string }): {
590605
amount: bigint;
591606
address?: string;
@@ -662,12 +677,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
662677
throw new InvalidAddressError(`invalid address: ${address}`);
663678
}
664679

665-
if (wallet) {
666-
const walletCoinSpecific = wallet.coinSpecific();
667-
if (walletCoinSpecific && 'descriptors' in walletCoinSpecific) {
668-
assertDescriptorWalletAddress(this.network, params, walletCoinSpecific.descriptors);
669-
return true;
670-
}
680+
if (wallet && isDescriptorWallet(wallet)) {
681+
assertDescriptorWalletAddress(this.network, params, getDescriptorMapFromWallet(wallet));
682+
return true;
671683
}
672684

673685
if ((_.isUndefined(chain) && _.isUndefined(index)) || !(_.isFinite(chain) && _.isFinite(index))) {
@@ -857,7 +869,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
857869
throw new Error('missing txPrebuild parameter');
858870
}
859871

860-
let tx = this.decodeTransaction(params.txPrebuild.txHex);
872+
let tx = this.decodeTransactionFromPrebuild(params.txPrebuild);
861873

862874
const isTxWithKeyPathSpendInput = tx instanceof bitgo.UtxoPsbt && bitgo.isTransactionWithKeyPathSpendInput(tx);
863875

@@ -1074,11 +1086,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
10741086
async explainTransaction<TNumber extends number | bigint = number>(
10751087
params: ExplainTransactionOptions<TNumber>
10761088
): Promise<TransactionExplanation> {
1077-
const { txHex } = params;
1078-
if (typeof txHex !== 'string' || !txHex.match(/^([a-f0-9]{2})+$/i)) {
1079-
throw new Error('invalid transaction hex, must be a valid hex string');
1080-
}
1081-
return explainTx(this.decodeTransaction(txHex), params, this.network);
1089+
return explainTx(this.decodeTransactionFromPrebuild(params), params, this.network);
10821090
}
10831091

10841092
/**

modules/abstract-utxo/src/descriptor/assertDescriptorWalletAddress.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import assert from 'assert';
2-
import * as t from 'io-ts';
32
import * as utxolib from '@bitgo/utxo-lib';
43
import { Descriptor } from '@bitgo/wasm-miniscript';
54

65
import { UtxoCoinSpecific, VerifyAddressOptions } from '../abstractUtxoCoin';
7-
import { NamedDescriptor } from './NamedDescriptor';
6+
import { DescriptorMap } from '../core/descriptor';
87

98
class DescriptorAddressMismatchError extends Error {
109
constructor(descriptor: Descriptor, index: number, derivedAddress: string, expectedAddress: string) {
@@ -17,19 +16,16 @@ class DescriptorAddressMismatchError extends Error {
1716
export function assertDescriptorWalletAddress(
1817
network: utxolib.Network,
1918
params: VerifyAddressOptions<UtxoCoinSpecific>,
20-
descriptors: unknown
19+
descriptors: DescriptorMap
2120
): void {
2221
assert(params.coinSpecific);
23-
assert(t.array(NamedDescriptor).is(descriptors));
2422
assert('descriptorName' in params.coinSpecific);
2523
assert('descriptorChecksum' in params.coinSpecific);
26-
const descriptorName = params.coinSpecific.descriptorName;
27-
const descriptorChecksum = params.coinSpecific.descriptorChecksum;
28-
const namedDescriptor = descriptors.find((d) => d.name === descriptorName);
29-
if (!namedDescriptor) {
24+
const { descriptorName, descriptorChecksum } = params.coinSpecific;
25+
const descriptor = descriptors.get(params.coinSpecific.descriptorName);
26+
if (!descriptor) {
3027
throw new Error(`Descriptor ${descriptorName} not found`);
3128
}
32-
const descriptor = Descriptor.fromString(namedDescriptor.value, 'derivable');
3329
const checksum = descriptor.toString().slice(-8);
3430
if (checksum !== descriptorChecksum) {
3531
throw new Error(

modules/abstract-utxo/src/descriptor/descriptorWallet.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,38 @@ import * as t from 'io-ts';
22
import { NamedDescriptor } from './NamedDescriptor';
33
import { AbstractUtxoCoinWalletData } from '../abstractUtxoCoin';
44
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';
5+
import { IWallet, WalletCoinSpecific } from '@bitgo/sdk-core';
6+
7+
type DescriptorWalletCoinSpecific = {
8+
descriptors: NamedDescriptor[];
9+
};
10+
11+
function isDescriptorWalletCoinSpecific(obj: unknown): obj is DescriptorWalletCoinSpecific {
12+
return (
13+
obj !== null && typeof obj === 'object' && 'descriptors' in obj && t.array(NamedDescriptor).is(obj.descriptors)
14+
);
15+
}
516

617
type DescriptorWalletData = AbstractUtxoCoinWalletData & {
7-
coinSpecific: {
8-
descriptors: NamedDescriptor[];
9-
};
18+
coinSpecific: DescriptorWalletCoinSpecific;
1019
};
1120

21+
interface IDescriptorWallet extends IWallet {
22+
coinSpecific(): WalletCoinSpecific & DescriptorWalletCoinSpecific;
23+
}
24+
1225
export function isDescriptorWalletData(obj: AbstractUtxoCoinWalletData): obj is DescriptorWalletData {
13-
if ('coinSpecific' in obj && 'descriptors' in obj.coinSpecific) {
14-
return t.array(NamedDescriptor).is(obj.coinSpecific.descriptors);
15-
}
16-
return false;
26+
return isDescriptorWalletCoinSpecific(obj.coinSpecific);
27+
}
28+
29+
export function isDescriptorWallet(obj: IWallet): obj is IDescriptorWallet {
30+
return isDescriptorWalletCoinSpecific(obj.coinSpecific());
1731
}
1832

1933
export function getDescriptorMapFromWalletData(wallet: DescriptorWalletData): DescriptorMap {
2034
return toDescriptorMap(wallet.coinSpecific.descriptors);
2135
}
36+
37+
export function getDescriptorMapFromWallet(wallet: IDescriptorWallet): DescriptorMap {
38+
return toDescriptorMap(wallet.coinSpecific().descriptors);
39+
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
export { Miniscript, Descriptor } from '@bitgo/wasm-miniscript';
22
export { assertDescriptorWalletAddress } from './assertDescriptorWalletAddress';
33
export { NamedDescriptor } from './NamedDescriptor';
4-
export { isDescriptorWalletData, getDescriptorMapFromWalletData } from './descriptorWallet';
4+
export {
5+
isDescriptorWallet,
6+
isDescriptorWalletData,
7+
getDescriptorMapFromWallet,
8+
getDescriptorMapFromWalletData,
9+
} from './descriptorWallet';

modules/abstract-utxo/src/transaction/explainTransaction.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@ import { TransactionExplanation } from '../abstractUtxoCoin';
44

55
import * as fixedScript from './fixedScript';
66

7+
import { IWallet } from '@bitgo/sdk-core';
8+
import { isDescriptorWallet } from '../descriptor';
9+
710
/**
811
* Decompose a raw transaction into useful information, such as the total amounts,
912
* change amounts, and transaction outputs.
1013
*/
1114
export function explainTx<TNumber extends number | bigint>(
1215
tx: utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoPsbt,
1316
params: {
17+
wallet?: IWallet;
1418
pubs?: string[];
1519
txInfo?: { unspents?: utxolib.bitgo.Unspent<TNumber>[] };
1620
changeInfo?: fixedScript.ChangeAddressInfo[];
1721
},
1822
network: utxolib.Network
1923
): TransactionExplanation {
24+
if (params.wallet && isDescriptorWallet(params.wallet)) {
25+
throw new Error('Descriptor wallets are not supported');
26+
}
2027
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
2128
return fixedScript.explainPsbt(tx, params, network);
2229
} else {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction';
22
export { parseTransaction } from './parseTransaction';
3+
export { verifyTransaction } from './verifyTransaction';
34
export { CustomChangeOptions } from './parseOutput';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './recipient';
22
export { explainTx } from './explainTransaction';
3+
export { parseTransaction } from './parseTransaction';
4+
export { verifyTransaction } from './verifyTransaction';
35
export * from './fetchInputs';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { AbstractUtxoCoin, ParsedTransaction, ParseTransactionOptions } from '../abstractUtxoCoin';
2+
3+
import { isDescriptorWallet } from '../descriptor';
4+
5+
import * as fixedScript from './fixedScript';
6+
7+
export async function parseTransaction<TNumber extends bigint | number>(
8+
coin: AbstractUtxoCoin,
9+
params: ParseTransactionOptions<TNumber>
10+
): Promise<ParsedTransaction<TNumber>> {
11+
if (isDescriptorWallet(params.wallet)) {
12+
throw new Error('Descriptor wallets are not supported');
13+
} else {
14+
return fixedScript.parseTransaction(coin, params);
15+
}
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BitGoBase } from '@bitgo/sdk-core';
2+
3+
import { AbstractUtxoCoin, VerifyTransactionOptions } from '../abstractUtxoCoin';
4+
import { isDescriptorWallet } from '../descriptor';
5+
6+
import * as fixedScript from './fixedScript';
7+
8+
export async function verifyTransaction<TNumber extends bigint | number>(
9+
coin: AbstractUtxoCoin,
10+
bitgo: BitGoBase,
11+
params: VerifyTransactionOptions<TNumber>
12+
): Promise<boolean> {
13+
if (isDescriptorWallet(params.wallet)) {
14+
throw new Error('Descriptor wallets are not supported');
15+
} else {
16+
return fixedScript.verifyTransaction(coin, bitgo, params);
17+
}
18+
}

0 commit comments

Comments
 (0)