Skip to content

Commit 087f0ff

Browse files
refactor(utxo-core): improve descriptor handling with derivation checks
Issue: BTC-1826
1 parent 53c7ffd commit 087f0ff

File tree

6 files changed

+114
-20
lines changed

6 files changed

+114
-20
lines changed

modules/utxo-core/src/descriptor/Output.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import assert from 'assert';
2+
13
import { Descriptor } from '@bitgo/wasm-miniscript';
24

35
import { getFixedOutputSum, MaxOutput, Output, PrevOutput } from '../Output';
46

57
import { DescriptorMap } from './DescriptorMap';
6-
import { createScriptPubKeyFromDescriptor } from './address';
8+
import { getDescriptorAtIndexCheckScript } from './derive';
79

810
export type WithDescriptor<T> = T & {
911
descriptor: Descriptor;
@@ -31,7 +33,7 @@ export function getExternalFixedAmount(outputs: WithOptDescriptor<Output | MaxOu
3133

3234
export type DescriptorWalletOutput = PrevOutput & {
3335
descriptorName: string;
34-
descriptorIndex: number;
36+
descriptorIndex: number | undefined;
3537
};
3638

3739
export type DerivedDescriptorWalletOutput = WithDescriptor<PrevOutput>;
@@ -44,17 +46,17 @@ export function toDerivedDescriptorWalletOutput(
4446
if (!descriptor) {
4547
throw new Error(`Descriptor not found: ${output.descriptorName}`);
4648
}
47-
const derivedDescriptor = descriptor.atDerivationIndex(output.descriptorIndex);
48-
const script = createScriptPubKeyFromDescriptor(derivedDescriptor);
49-
if (!script.equals(output.witnessUtxo.script)) {
50-
throw new Error(
51-
`Script mismatch: descriptor ${output.descriptorName} ${descriptor.toString()} script=${script.toString('hex')}`
52-
);
53-
}
49+
assert(descriptor instanceof Descriptor);
50+
const descriptorAtIndex = getDescriptorAtIndexCheckScript(
51+
descriptor,
52+
output.descriptorIndex,
53+
output.witnessUtxo.script,
54+
output.descriptorName
55+
);
5456
return {
5557
hash: output.hash,
5658
index: output.index,
5759
witnessUtxo: output.witnessUtxo,
58-
descriptor: descriptor.atDerivationIndex(output.descriptorIndex),
60+
descriptor: descriptorAtIndex,
5961
};
6062
}

modules/utxo-core/src/descriptor/address.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Descriptor } from '@bitgo/wasm-miniscript';
22
import * as utxolib from '@bitgo/utxo-lib';
33

4-
export function createScriptPubKeyFromDescriptor(descriptor: Descriptor, index?: number): Buffer {
4+
export function createScriptPubKeyFromDescriptor(descriptor: Descriptor, index: number | undefined): Buffer {
55
if (index === undefined) {
66
return Buffer.from(descriptor.scriptPubkey());
77
}
8-
return createScriptPubKeyFromDescriptor(descriptor.atDerivationIndex(index));
8+
return createScriptPubKeyFromDescriptor(descriptor.atDerivationIndex(index), undefined);
99
}
1010

1111
export function createAddressFromDescriptor(
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import assert from 'assert';
2+
3+
import { Descriptor } from '@bitgo/wasm-miniscript';
4+
5+
/**
6+
* Get a descriptor at a specific derivation index.
7+
* For wildcard descriptors (containing '*'), the index is required and used for derivation.
8+
* For definite descriptors (not containing '*'), no index should be provided.
9+
* @param descriptor - The descriptor to derive from
10+
* @param index - The derivation index for wildcard descriptors
11+
* @returns A new descriptor at the specified index for wildcard descriptors, or the original descriptor for definite ones
12+
* @throws {Error} If index is undefined for a wildcard descriptor or if index is provided for a definite descriptor
13+
*/
14+
export function getDescriptorAtIndex(descriptor: Descriptor, index: number | undefined): Descriptor {
15+
assert(descriptor instanceof Descriptor);
16+
Descriptor.fromString(descriptor.toString(), 'derivable');
17+
descriptor = Descriptor.fromStringDetectType(descriptor.toString());
18+
if (descriptor.hasWildcard()) {
19+
if (index === undefined) {
20+
throw new Error('Derivable descriptor requires an index');
21+
}
22+
return descriptor.atDerivationIndex(index);
23+
} else {
24+
if (index !== undefined) {
25+
throw new Error('Definite descriptor cannot be derived with index');
26+
}
27+
return descriptor;
28+
}
29+
}
30+
31+
export function getDescriptorAtIndexCheckScript(
32+
descriptor: Descriptor,
33+
index: number | undefined,
34+
script: Buffer,
35+
descriptorString = descriptor.toString()
36+
): Descriptor {
37+
assert(descriptor instanceof Descriptor);
38+
const descriptorAtIndex = getDescriptorAtIndex(descriptor, index);
39+
if (!script.equals(descriptorAtIndex.scriptPubkey())) {
40+
throw new Error(`Script mismatch: descriptor ${descriptorString} script=${script.toString('hex')}`);
41+
}
42+
assert(descriptorAtIndex instanceof Descriptor);
43+
return descriptorAtIndex;
44+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function mockDerivedDescriptorWalletOutput(
3333
hash,
3434
index: vout,
3535
witnessUtxo: {
36-
script: createScriptPubKeyFromDescriptor(descriptor),
36+
script: createScriptPubKeyFromDescriptor(descriptor, undefined),
3737
value,
3838
},
3939
descriptor,
@@ -67,7 +67,7 @@ export function mockPsbt(
6767
outputs.map((o) => {
6868
const derivedDescriptor = tryDeriveAtIndex(o.descriptor, o.index);
6969
return {
70-
script: createScriptPubKeyFromDescriptor(derivedDescriptor),
70+
script: createScriptPubKeyFromDescriptor(derivedDescriptor, undefined),
7171
value: o.value,
7272
descriptor: o.external ? undefined : derivedDescriptor,
7373
};
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
import assert from 'assert';
22

3-
import { Descriptor } from '@bitgo/wasm-miniscript';
4-
5-
import { isExternalOutput, isInternalOutput } from '../../src/descriptor';
3+
import { isExternalOutput, isInternalOutput, toDerivedDescriptorWalletOutput } from '../../src/descriptor/Output';
4+
import { getDescriptor } from '../../src/testutil/descriptor';
5+
import { createScriptPubKeyFromDescriptor } from '../../src/descriptor';
66

77
describe('decscriptor.Output', function () {
8-
const mockDescriptor = {} as Descriptor;
8+
const descriptor = getDescriptor('Wsh2Of3');
99

1010
it('isInternalOutput correctly identifies internal outputs', function () {
11-
const internalOutput = { value: 1n, descriptor: mockDescriptor };
11+
const internalOutput = { value: 1n, descriptor };
1212
const externalOutput = { value: 1n };
1313

1414
assert.strictEqual(isInternalOutput(internalOutput), true);
1515
assert.strictEqual(isInternalOutput(externalOutput), false);
1616
});
1717

1818
it('isExternalOutput correctly identifies external outputs', function () {
19-
const internalOutput = { value: 1n, descriptor: mockDescriptor };
19+
const internalOutput = { value: 1n, descriptor };
2020
const externalOutput = { value: 1n };
2121

2222
assert.strictEqual(isExternalOutput(internalOutput), false);
2323
assert.strictEqual(isExternalOutput(externalOutput), true);
2424
});
25+
26+
it('toDerivedDescriptorWalletOutput returns expected values', function () {
27+
const derivable = descriptor;
28+
const definite = derivable.atDerivationIndex(0);
29+
for (const descriptor of [derivable, definite]) {
30+
const descriptorIndex = descriptor === derivable ? 0 : undefined;
31+
const descriptorMap = new Map([['desc', descriptor]]);
32+
const descriptorWalletOutput = {
33+
hash: Buffer.alloc(32).toString('hex'),
34+
index: 0,
35+
witnessUtxo: {
36+
script: createScriptPubKeyFromDescriptor(descriptor, descriptorIndex),
37+
value: 1n,
38+
},
39+
descriptorName: 'desc',
40+
descriptorIndex,
41+
};
42+
assert.strictEqual(
43+
toDerivedDescriptorWalletOutput(descriptorWalletOutput, descriptorMap).descriptor.toString(),
44+
definite.toString()
45+
);
46+
}
47+
});
2548
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import assert from 'assert';
2+
3+
import { getDescriptor } from '../../src/testutil/descriptor';
4+
import { getDescriptorAtIndex, getDescriptorAtIndexCheckScript } from '../../src/descriptor/derive';
5+
6+
describe('derive', function () {
7+
const derivable = getDescriptor('Wsh2Of3');
8+
const definite = derivable.atDerivationIndex(0);
9+
10+
it('getDescriptorAtIndex', function () {
11+
assert(derivable.hasWildcard());
12+
assert(!definite.hasWildcard());
13+
assert.strictEqual(getDescriptorAtIndex(derivable, 0).toString(), definite.toString());
14+
assert.strictEqual(getDescriptorAtIndex(definite, undefined).toString(), definite.toString());
15+
assert.throws(() => getDescriptorAtIndex(derivable, undefined), /Derivable descriptor requires an index/);
16+
assert.throws(() => getDescriptorAtIndex(definite, 0), /Definite descriptor cannot be derived with index/);
17+
});
18+
19+
it('getDescriptorAtIndexCheckScript', function () {
20+
const script0 = Buffer.from(derivable.atDerivationIndex(0).scriptPubkey());
21+
const script1 = Buffer.from(derivable.atDerivationIndex(1).scriptPubkey());
22+
assert.strictEqual(getDescriptorAtIndexCheckScript(derivable, 0, script0).toString(), definite.toString());
23+
assert.throws(() => getDescriptorAtIndexCheckScript(derivable, 0, script1), /Script mismatch/);
24+
});
25+
});

0 commit comments

Comments
 (0)