Skip to content

Commit e08dfe0

Browse files
Merge pull request #5641 from BitGo/BTC-1826.test-tr1of3-tree.3
feat(utxo-core): add support for descriptors with plain keys
2 parents 18b9716 + 52616ac commit e08dfe0

File tree

6 files changed

+1061
-26
lines changed

6 files changed

+1061
-26
lines changed

modules/utxo-core/src/descriptor/psbt/findDescriptors.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ import { Descriptor } from '@bitgo/wasm-miniscript';
1414

1515
import { DescriptorMap } from '../DescriptorMap';
1616

17+
type DescriptorWithoutIndex = { descriptor: Descriptor; index: undefined };
18+
19+
/**
20+
* Find a definite descriptor in the descriptor map that matches the given script.
21+
* @param script
22+
* @param descriptorMap
23+
*/
24+
function findDescriptorWithoutDerivation(
25+
script: Buffer,
26+
descriptorMap: DescriptorMap
27+
): DescriptorWithoutIndex | undefined {
28+
for (const descriptor of descriptorMap.values()) {
29+
if (!descriptor.hasWildcard()) {
30+
if (Buffer.from(descriptor.scriptPubkey()).equals(script)) {
31+
return { descriptor, index: undefined };
32+
}
33+
}
34+
}
35+
36+
return undefined;
37+
}
38+
1739
type DescriptorWithIndex = { descriptor: Descriptor; index: number };
1840

1941
/**
@@ -29,7 +51,7 @@ function findDescriptorForDerivationIndex(
2951
descriptorMap: DescriptorMap
3052
): DescriptorWithIndex | undefined {
3153
for (const descriptor of descriptorMap.values()) {
32-
if (Buffer.from(descriptor.atDerivationIndex(index).scriptPubkey()).equals(script)) {
54+
if (descriptor.hasWildcard() && Buffer.from(descriptor.atDerivationIndex(index).scriptPubkey()).equals(script)) {
3355
return { descriptor, index };
3456
}
3557
}
@@ -92,16 +114,16 @@ function getDerivationPaths(v: WithBip32Derivation | WithTapBip32Derivation): st
92114
export function findDescriptorForInput(
93115
input: PsbtInput,
94116
descriptorMap: DescriptorMap
95-
): DescriptorWithIndex | undefined {
117+
): DescriptorWithIndex | DescriptorWithoutIndex | undefined {
96118
const script = input.witnessUtxo?.script;
97119
if (!script) {
98120
throw new Error('Missing script');
99121
}
100-
const derivationPaths = getDerivationPaths(input);
101-
if (!derivationPaths) {
102-
throw new Error('Missing derivation paths');
103-
}
104-
return findDescriptorForAnyDerivationPath(script, derivationPaths, descriptorMap);
122+
const derivationPaths = getDerivationPaths(input) ?? [];
123+
return (
124+
findDescriptorWithoutDerivation(script, descriptorMap) ??
125+
findDescriptorForAnyDerivationPath(script, derivationPaths, descriptorMap)
126+
);
105127
}
106128

107129
/**

modules/utxo-core/src/descriptor/psbt/parse.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { getVirtualSize } from '../VirtualSize';
77
import { findDescriptorForInput, findDescriptorForOutput } from './findDescriptors';
88
import { assertSatisfiable } from './assertSatisfiable';
99

10-
export type ScriptId = { descriptor: Descriptor; index: number };
10+
export type ScriptId = { descriptor: Descriptor; index: number | undefined };
1111

1212
export type ParsedInput = {
1313
address: string;
@@ -46,15 +46,15 @@ export function parse(
4646
if (!input.witnessUtxo.value) {
4747
throw new Error('invalid input: no value');
4848
}
49-
const descriptorWithIndex = findDescriptorForInput(input, descriptorMap);
50-
if (!descriptorWithIndex) {
49+
const scriptId = findDescriptorForInput(input, descriptorMap);
50+
if (!scriptId) {
5151
throw new Error('invalid input: no descriptor found');
5252
}
53-
assertSatisfiable(psbt, inputIndex, descriptorWithIndex.descriptor);
53+
assertSatisfiable(psbt, inputIndex, scriptId.descriptor);
5454
return {
5555
address: utxolib.address.fromOutputScript(input.witnessUtxo.script, network),
5656
value: input.witnessUtxo.value,
57-
scriptId: descriptorWithIndex,
57+
scriptId: scriptId,
5858
};
5959
});
6060
const outputs = psbt.txOutputs.map((output, i): ParsedOutput => {

modules/utxo-core/src/testutil/descriptor/descriptors.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'assert';
22

33
import { Descriptor, ast } from '@bitgo/wasm-miniscript';
4-
import { BIP32Interface } from '@bitgo/utxo-lib';
4+
import { bip32, BIP32Interface } from '@bitgo/utxo-lib';
55

66
import { DescriptorMap, PsbtParams } from '../../descriptor';
77
import { getKeyTriple, Triple, KeyTriple } from '../key.utils';
@@ -41,6 +41,8 @@ function toDescriptorMap(v: Record<string, string>): DescriptorMap {
4141
export type DescriptorTemplate =
4242
| 'Wsh2Of3'
4343
| 'Tr1Of3-NoKeyPath-Tree'
44+
// no xpubs, just plain keys
45+
| 'Tr1Of3-NoKeyPath-Tree-Plain'
4446
| 'Tr2Of3-NoKeyPath'
4547
| 'Wsh2Of2'
4648
/*
@@ -57,6 +59,20 @@ function toXPub(k: BIP32Interface | string, path: string): string {
5759
return k.neutered().toBase58() + '/' + path;
5860
}
5961

62+
function toPlain(k: BIP32Interface | string, { xonly = false } = {}): string {
63+
if (typeof k === 'string') {
64+
if (k.startsWith('xpub') || k.startsWith('xprv')) {
65+
return toPlain(bip32.fromBase58(k), { xonly });
66+
}
67+
return k;
68+
}
69+
return k.publicKey.subarray(xonly ? 1 : 0).toString('hex');
70+
}
71+
72+
function toXOnly(k: BIP32Interface | string): string {
73+
return toPlain(k, { xonly: true });
74+
}
75+
6076
function multiArgs(m: number, n: number, keys: BIP32Interface[] | string[], path: string): [number, ...string[]] {
6177
if (n < m) {
6278
throw new Error(`Cannot create ${m} of ${n} multisig`);
@@ -70,13 +86,10 @@ function multiArgs(m: number, n: number, keys: BIP32Interface[] | string[], path
7086

7187
export function getPsbtParams(t: DescriptorTemplate): Partial<PsbtParams> {
7288
switch (t) {
73-
case 'Wsh2Of3':
74-
case 'Wsh2Of2':
75-
case 'Tr1Of3-NoKeyPath-Tree':
76-
case 'Tr2Of3-NoKeyPath':
77-
return {};
7889
case 'Wsh2Of3CltvDrop':
7990
return { locktime: 1 };
91+
default:
92+
return {};
8093
}
8194
}
8295

@@ -113,16 +126,29 @@ function getDescriptorNode(
113126
[{ pk: toXPub(keys[0], path) }, [{ pk: toXPub(keys[1], path) }, { pk: toXPub(keys[2], path) }]],
114127
],
115128
};
129+
case 'Tr1Of3-NoKeyPath-Tree-Plain':
130+
return {
131+
tr: [getUnspendableKey(), [{ pk: toXOnly(keys[0]) }, [{ pk: toXOnly(keys[1]) }, { pk: toXOnly(keys[2]) }]]],
132+
};
116133
}
117134
throw new Error(`Unknown descriptor template: ${template}`);
118135
}
119136

137+
function getDescriptorType(template: DescriptorTemplate): 'derivable' | 'definite' {
138+
switch (template) {
139+
case 'Tr1Of3-NoKeyPath-Tree-Plain':
140+
return 'definite';
141+
default:
142+
return 'derivable';
143+
}
144+
}
145+
120146
export function getDescriptor(
121147
template: DescriptorTemplate,
122148
keys: KeyTriple | string[] = getDefaultXPubs(),
123149
path = '0/*'
124150
): Descriptor {
125-
return Descriptor.fromString(ast.formatNode(getDescriptorNode(template, keys, path)), 'derivable');
151+
return Descriptor.fromString(ast.formatNode(getDescriptorNode(template, keys, path)), getDescriptorType(template));
126152
}
127153

128154
export function getDescriptorMap(

modules/utxo-core/src/testutil/descriptor/mock.utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,20 @@ type MockOutput = {
5252
external?: boolean;
5353
};
5454

55+
function tryDeriveAtIndex(descriptor: Descriptor, index: number): Descriptor {
56+
return descriptor.hasWildcard() ? descriptor.atDerivationIndex(index) : descriptor;
57+
}
58+
5559
export function mockPsbt(
5660
inputs: MockInput[],
5761
outputs: MockOutput[],
5862
params: Partial<PsbtParams> = {}
5963
): utxolib.bitgo.UtxoPsbt {
6064
return createPsbt(
6165
{ ...params, network: params.network ?? utxolib.networks.bitcoin },
62-
inputs.map((i) => mockDerivedDescriptorWalletOutput(i.descriptor.atDerivationIndex(i.index), i)),
66+
inputs.map((i) => mockDerivedDescriptorWalletOutput(tryDeriveAtIndex(i.descriptor, i.index), i)),
6367
outputs.map((o) => {
64-
const derivedDescriptor = o.descriptor.atDerivationIndex(o.index);
68+
const derivedDescriptor = tryDeriveAtIndex(o.descriptor, o.index);
6569
return {
6670
script: createScriptPubKeyFromDescriptor(derivedDescriptor),
6771
value: o.value,

0 commit comments

Comments
 (0)