Skip to content

Commit 0ae87df

Browse files
Merge pull request #5289 from BitGo/BTC-1687.allow-wsh-2of3-cltv-drop
feat(abstract-utxo): permit Wsh2Of3CltvDrop in descriptor validation
2 parents 3792b0a + 95a393f commit 0ae87df

File tree

4 files changed

+47
-27
lines changed

4 files changed

+47
-27
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export type DescriptorBuilder =
1111
| DescriptorWithKeys<'Wsh2Of2'>
1212
| DescriptorWithKeys<'Wsh2Of3'>
1313
/*
14-
* This is a wrapped segwit 2of3 multisig that also uses a relative locktime with
15-
* an OP_DROP (requiring a miniscript extension).
14+
* This is a segwit (wrapped or native) 2of3 multisig that also uses a
15+
* relative locktime with an OP_DROP (requiring a miniscript extension).
1616
* It is basically what is used in CoreDao staking transactions.
1717
*/
18-
| (DescriptorWithKeys<'ShWsh2Of3CltvDrop'> & { locktime: number });
18+
| (DescriptorWithKeys<'ShWsh2Of3CltvDrop' | 'Wsh2Of3CltvDrop'> & { locktime: number });
1919

2020
function toXPub(k: BIP32Interface | string): string {
2121
if (typeof k === 'string') {
@@ -41,8 +41,10 @@ function getDescriptorString(builder: DescriptorBuilder): string {
4141
return `wsh(${multi(2, 3, builder.keys, builder.path)})`;
4242
case 'Wsh2Of2':
4343
return `wsh(${multi(2, 2, builder.keys, builder.path)})`;
44+
case 'Wsh2Of3CltvDrop':
45+
return `wsh(and_v(r:after(${builder.locktime}),${multi(2, 3, builder.keys, builder.path)}))`;
4446
case 'ShWsh2Of3CltvDrop':
45-
return `sh(wsh(and_v(r:after(${builder.locktime}),${multi(2, 3, builder.keys, builder.path)})))`;
47+
return `sh(${getDescriptorString({ ...builder, name: 'Wsh2Of3CltvDrop' })})`;
4648
}
4749
throw new Error(`Unknown descriptor template: ${builder}`);
4850
}

modules/abstract-utxo/src/descriptor/builder/parse.ts

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function parseMulti(node: unknown): {
5959
};
6060
}
6161

62-
export function parseDescriptorNode(node: unknown): DescriptorBuilder {
62+
function parseWshMulti(node: unknown): DescriptorBuilder | undefined {
6363
const wshMsMulti = unwrapNode(node, ['Wsh', 'Ms', 'Multi']);
6464
if (wshMsMulti) {
6565
const { threshold, keys, path } = parseMulti(wshMsMulti);
@@ -77,31 +77,47 @@ export function parseDescriptorNode(node: unknown): DescriptorBuilder {
7777
path,
7878
};
7979
}
80+
}
8081

81-
const shWshMsAndV = unwrapNode(node, ['Sh', 'Wsh', 'Ms', 'AndV']);
82-
if (shWshMsAndV) {
83-
if (Array.isArray(shWshMsAndV) && shWshMsAndV.length === 2) {
84-
const [a, b] = shWshMsAndV;
85-
const dropAfterAbsLocktime = unwrapNode(a, ['Drop', 'After', 'absLockTime']);
86-
if (typeof dropAfterAbsLocktime !== 'number') {
87-
throw new Error('Expected absLockTime number');
88-
}
89-
if (!isUnaryNode(b, 'Multi')) {
90-
throw new Error('Expected Multi node');
91-
}
92-
const multi = parseMulti(b.Multi);
93-
if (multi.threshold === 2 && multi.keys.length === 3) {
94-
return {
95-
name: 'ShWsh2Of3CltvDrop',
96-
locktime: dropAfterAbsLocktime,
97-
keys: multi.keys,
98-
path: multi.path,
99-
};
100-
}
82+
function parseCltvDrop(
83+
node: unknown,
84+
name: 'Wsh2Of3CltvDrop' | 'ShWsh2Of3CltvDrop',
85+
wrapping: string[]
86+
): DescriptorBuilder | undefined {
87+
const unwrapped = unwrapNode(node, wrapping);
88+
if (!unwrapped) {
89+
return;
90+
}
91+
if (Array.isArray(unwrapped) && unwrapped.length === 2) {
92+
const [a, b] = unwrapped;
93+
const dropAfterAbsLocktime = unwrapNode(a, ['Drop', 'After', 'absLockTime']);
94+
if (typeof dropAfterAbsLocktime !== 'number') {
95+
throw new Error('Expected absLockTime number');
96+
}
97+
if (!isUnaryNode(b, 'Multi')) {
98+
throw new Error('Expected Multi node');
99+
}
100+
const multi = parseMulti(b.Multi);
101+
if (multi.threshold === 2 && multi.keys.length === 3) {
102+
return {
103+
name,
104+
locktime: dropAfterAbsLocktime,
105+
keys: multi.keys,
106+
path: multi.path,
107+
};
101108
}
102109
}
110+
}
103111

104-
throw new Error('Not implemented');
112+
export function parseDescriptorNode(node: unknown): DescriptorBuilder {
113+
const parsed =
114+
parseWshMulti(node) ??
115+
parseCltvDrop(node, 'ShWsh2Of3CltvDrop', ['Sh', 'Wsh', 'Ms', 'AndV']) ??
116+
parseCltvDrop(node, 'Wsh2Of3CltvDrop', ['Wsh', 'Ms', 'AndV']);
117+
if (!parsed) {
118+
throw new Error('Failed to parse descriptor node');
119+
}
120+
return parsed;
105121
}
106122

107123
export function parseDescriptor(descriptor: Descriptor): DescriptorBuilder {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function getPolicyForEnv(env: EnvironmentName): DescriptorValidationPolic
6161
case 'adminProd':
6262
case 'prod':
6363
return {
64-
allowedTemplates: ['Wsh2Of3', 'ShWsh2Of3CltvDrop'],
64+
allowedTemplates: ['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop'],
6565
};
6666
default:
6767
return 'allowAll';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function getDescriptorBuilderForType(name: DescriptorBuilder['name']): Descripto
1313
keys: keys.slice(0, name === 'Wsh2Of3' ? 3 : 2),
1414
path: '0/*',
1515
};
16+
case 'Wsh2Of3CltvDrop':
1617
case 'ShWsh2Of3CltvDrop':
1718
return {
1819
name,
@@ -35,4 +36,5 @@ function describeForName(n: DescriptorBuilder['name']) {
3536

3637
describeForName('Wsh2Of2');
3738
describeForName('Wsh2Of3');
39+
describeForName('Wsh2Of3CltvDrop');
3840
describeForName('ShWsh2Of3CltvDrop');

0 commit comments

Comments
 (0)