Skip to content

Commit d5b0b60

Browse files
refactor(abstract-utxo): make descriptor validation more flexible
Use a validator interface that allows for more complex validation and composition of validation rules. Add tests. Issue: BTC-1708
1 parent 9680124 commit d5b0b60

File tree

3 files changed

+105
-32
lines changed

3 files changed

+105
-32
lines changed

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

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,71 @@ import * as utxolib from '@bitgo/utxo-lib';
44

55
import { DescriptorMap, toDescriptorMap } from '../core/descriptor';
66

7-
import { DescriptorBuilder, parseDescriptor } from './builder';
7+
import { parseDescriptor } from './builder';
88
import { NamedDescriptor } from './NamedDescriptor';
99

10-
export type DescriptorValidationPolicy = { allowedTemplates: DescriptorBuilder['name'][] } | 'allowAll';
11-
1210
export type KeyTriple = Triple<utxolib.BIP32Interface>;
1311

14-
function isDescriptorWithTemplate(
15-
d: Descriptor,
16-
name: DescriptorBuilder['name'],
17-
walletKeys: Triple<utxolib.BIP32Interface>
18-
): boolean {
19-
const parsed = parseDescriptor(d);
20-
if (parsed.name !== name) {
21-
return false;
22-
}
23-
if (parsed.keys.length !== walletKeys.length) {
24-
return false;
12+
export interface DescriptorValidationPolicy {
13+
name: string;
14+
validate(d: Descriptor, walletKeys: KeyTriple): boolean;
15+
}
16+
17+
export const policyAllowAll: DescriptorValidationPolicy = {
18+
name: 'allowAll',
19+
validate: () => true,
20+
};
21+
22+
export function getValidatorDescriptorTemplate(name: string): DescriptorValidationPolicy {
23+
return {
24+
name: 'descriptorTemplate(' + name + ')',
25+
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
26+
const parsed = parseDescriptor(d);
27+
return (
28+
parsed.name === name &&
29+
parsed.keys.length === walletKeys.length &&
30+
parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].neutered().toBase58())
31+
);
32+
},
33+
};
34+
}
35+
36+
export function getValidatorEvery(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
37+
return {
38+
name: 'every(' + validators.map((v) => v.name).join(',') + ')',
39+
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
40+
return validators.every((v) => v.validate(d, walletKeys));
41+
},
42+
};
43+
}
44+
45+
export function getValidatorSome(validators: DescriptorValidationPolicy[]): DescriptorValidationPolicy {
46+
return {
47+
name: 'some(' + validators.map((v) => v.name).join(',') + ')',
48+
validate(d: Descriptor, walletKeys: KeyTriple): boolean {
49+
return validators.some((v) => v.validate(d, walletKeys));
50+
},
51+
};
52+
}
53+
54+
export function getValidatorOneOfTemplates(names: string[]): DescriptorValidationPolicy {
55+
return getValidatorSome(names.map(getValidatorDescriptorTemplate));
56+
}
57+
58+
export class DescriptorPolicyValidationError extends Error {
59+
constructor(descriptor: Descriptor, policy: DescriptorValidationPolicy) {
60+
super(`Descriptor ${descriptor.toString()} does not match policy ${policy.name}`);
2561
}
26-
return parsed.keys.every((k, i) => k.toBase58() === walletKeys[i].toBase58());
2762
}
2863

2964
export function assertDescriptorPolicy(
3065
descriptor: Descriptor,
3166
policy: DescriptorValidationPolicy,
32-
walletKeys: Triple<utxolib.BIP32Interface>
67+
walletKeys: KeyTriple
3368
): void {
34-
if (policy === 'allowAll') {
35-
return;
36-
}
37-
38-
if ('allowedTemplates' in policy) {
39-
const allowed = policy.allowedTemplates;
40-
if (!allowed.some((t) => isDescriptorWithTemplate(descriptor, t, walletKeys))) {
41-
throw new Error(`Descriptor ${descriptor.toString()} does not match any allowed template`);
42-
}
69+
if (!policy.validate(descriptor, walletKeys)) {
70+
throw new DescriptorPolicyValidationError(descriptor, policy);
4371
}
44-
45-
throw new Error(`Unknown descriptor validation policy: ${policy}`);
4672
}
4773

4874
export function toDescriptorMapValidate(
@@ -61,10 +87,8 @@ export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolic
6187
switch (env) {
6288
case 'adminProd':
6389
case 'prod':
64-
return {
65-
allowedTemplates: ['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop'],
66-
};
90+
return getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']);
6791
default:
68-
return 'allowAll';
92+
return policyAllowAll;
6993
}
7094
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getDescriptorMapFromWallet, isDescriptorWallet } from '../../src/descri
44
import { UtxoWallet } from '../../src/wallet';
55
import { getDefaultXPubs, getDescriptorMap } from '../core/descriptor/descriptor.utils';
66
import { toBip32Triple } from '../../src/keychains';
7+
import { policyAllowAll } from '../../src/descriptor/validatePolicy';
78

89
describe('isDescriptorWalletData', function () {
910
const descriptorMap = getDescriptorMap('Wsh2Of3');
@@ -21,7 +22,7 @@ describe('isDescriptorWalletData', function () {
2122

2223
assert(isDescriptorWallet(wallet));
2324
assert.strictEqual(
24-
getDescriptorMapFromWallet(wallet, toBip32Triple(getDefaultXPubs()), 'allowAll').size,
25+
getDescriptorMapFromWallet(wallet, toBip32Triple(getDefaultXPubs()), policyAllowAll).size,
2526
descriptorMap.size
2627
);
2728
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import assert from 'assert';
2+
3+
import { Descriptor } from '@bitgo/wasm-miniscript';
4+
import { Triple } from '@bitgo/sdk-core';
5+
import { BIP32Interface } from '@bitgo/utxo-lib';
6+
7+
import {
8+
assertDescriptorPolicy,
9+
DescriptorPolicyValidationError,
10+
DescriptorValidationPolicy,
11+
getPolicyForEnv,
12+
getValidatorDescriptorTemplate,
13+
} from '../../../src/descriptor/validatePolicy';
14+
import { getDescriptor } from '../../core/descriptor/descriptor.utils';
15+
import { getKeyTriple } from '../../core/key.utils';
16+
17+
function testAssertDescriptorPolicy(
18+
d: Descriptor,
19+
p: DescriptorValidationPolicy,
20+
k: Triple<BIP32Interface>,
21+
expectedError: DescriptorPolicyValidationError | null
22+
) {
23+
const f = () => assertDescriptorPolicy(d, p, k);
24+
if (expectedError) {
25+
assert.throws(f);
26+
} else {
27+
assert.doesNotThrow(f);
28+
}
29+
}
30+
31+
describe('assertDescriptorPolicy', function () {
32+
it('has expected result', function () {
33+
const keys = getKeyTriple();
34+
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getValidatorDescriptorTemplate('Wsh2Of3'), keys, null);
35+
36+
// prod does only allow Wsh2Of3-ish descriptors
37+
testAssertDescriptorPolicy(getDescriptor('Wsh2Of3', keys), getPolicyForEnv('prod'), keys, null);
38+
testAssertDescriptorPolicy(
39+
getDescriptor('Wsh2Of2', keys),
40+
getPolicyForEnv('prod'),
41+
keys,
42+
new DescriptorPolicyValidationError(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('prod'))
43+
);
44+
45+
// test is very permissive by default
46+
testAssertDescriptorPolicy(getDescriptor('Wsh2Of2', keys), getPolicyForEnv('test'), keys, null);
47+
});
48+
});

0 commit comments

Comments
 (0)