Skip to content

Commit 7046b8a

Browse files
feat(utxo-core): add new module for UTXO types and functions
Add core types and functions for Bitcoin-like UTXO-based cryptocurrencies. This module provides the glue between SDK-dependent modules and low-level libraries. Issue: BTC-1821
1 parent 6e244e6 commit 7046b8a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3193
-1
lines changed

CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
/modules/sdk-core/src/bitgo/lightning/ @BitGo/btc-team
2525
/modules/unspents/ @BitGo/btc-team
2626
/modules/utxo-bin/ @BitGo/btc-team
27-
/modules/utxo-staking/ @BitGo/btc-team
27+
/modules/utxo-core/ @BitGo/btc-team
2828
/modules/utxo-lib/ @BitGo/btc-team
2929
/modules/utxo-ord/ @BitGo/btc-team
30+
/modules/utxo-staking/ @BitGo/btc-team
3031

3132
# Lightning coin modules
3233
/modules/abstract-lightning/ @BitGo/btc-team

modules/abstract-utxo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@bitgo/sdk-core": "^28.23.0",
4848
"@bitgo/unspents": "^0.47.17",
4949
"@bitgo/utxo-lib": "^11.2.1",
50+
"@bitgo/utxo-core": "^1.0.0",
5051
"@bitgo/wasm-miniscript": "^2.0.0-beta.2",
5152
"@types/bluebird": "^3.5.25",
5253
"@types/lodash": "^4.14.121",

modules/abstract-utxo/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
},
2222
{
2323
"path": "../utxo-lib"
24+
},
25+
{
26+
"path": "../utxo-core"
2427
}
2528
]
2629
}

modules/utxo-core/.mocharc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
module.exports = {
4+
require: 'ts-node/register',
5+
extension: ['.js', '.ts'],
6+
};

modules/utxo-core/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# utxo-core
2+
3+
This repository contains core types and functions for Bitcoin-like UTXO-based cryptocurrencies.
4+
5+
It is the glue between SDK-dependent modules like `abstract-utxo` and low-level libraries like `utxo-lib` and
6+
`wasm-miniscript`.

modules/utxo-core/package.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@bitgo/utxo-core",
3+
"version": "1.0.0",
4+
"description": "BitGo UTXO Core types",
5+
"main": "./dist/src/index.js",
6+
"types": "./dist/src/index.d.ts",
7+
"files": [
8+
"dist/src"
9+
],
10+
"exports": {
11+
".": "./dist/src/index.js",
12+
"./descriptor": "./dist/src/descriptor/index.js",
13+
"./testutil": "./dist/src/testutil/index.js",
14+
"./testutil/descriptor": "./dist/src/testutil/descriptor/index.js"
15+
},
16+
"scripts": {
17+
"build": "yarn tsc --build --incremental --verbose .",
18+
"fmt": "prettier --write .",
19+
"check-fmt": "prettier --check .",
20+
"clean": "rm -r ./dist",
21+
"lint": "eslint --quiet .",
22+
"prepare": "npm run build",
23+
"test": "npm run unit-test",
24+
"unit-test": "mocha --recursive test/"
25+
},
26+
"author": "BitGo SDK Team <[email protected]>",
27+
"license": "MIT",
28+
"repository": {
29+
"type": "git",
30+
"url": "https://github.com/BitGo/BitGoJS.git",
31+
"directory": "modules/utxo-core"
32+
},
33+
"lint-staged": {
34+
"*.{js,ts}": [
35+
"yarn prettier --write",
36+
"yarn eslint --fix"
37+
]
38+
},
39+
"publishConfig": {
40+
"access": "public"
41+
},
42+
"nyc": {
43+
"extension": [
44+
".ts"
45+
]
46+
},
47+
"dependencies": {
48+
"@bitgo/unspents": "^0.47.17",
49+
"@bitgo/utxo-lib": "^11.2.1",
50+
"@bitgo/wasm-miniscript": "^2.0.0-beta.2",
51+
"bip174": "npm:@bitgo-forks/[email protected]"
52+
},
53+
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c"
54+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Descriptor } from '@bitgo/wasm-miniscript';
2+
3+
/** Map from descriptor name to descriptor */
4+
export type DescriptorMap = Map<string, Descriptor>;
5+
6+
/** Convert an array of descriptor name-value pairs to a descriptor map */
7+
export function toDescriptorMap(descriptors: { name: string; value: Descriptor | string }[]): DescriptorMap {
8+
return new Map(
9+
descriptors.map((d) => [
10+
d.name,
11+
d.value instanceof Descriptor ? d.value : Descriptor.fromString(d.value, 'derivable'),
12+
])
13+
);
14+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Descriptor } from '@bitgo/wasm-miniscript';
2+
3+
import { DescriptorMap } from './DescriptorMap';
4+
import { createScriptPubKeyFromDescriptor } from './address';
5+
6+
export type Output = {
7+
script: Buffer;
8+
value: bigint;
9+
};
10+
11+
export type WithDescriptor<T> = T & {
12+
descriptor: Descriptor;
13+
};
14+
15+
export type WithOptDescriptor<T> = T & {
16+
descriptor?: Descriptor;
17+
};
18+
19+
export type PrevOutput = {
20+
hash: string;
21+
index: number;
22+
witnessUtxo: Output;
23+
};
24+
25+
export type DescriptorWalletOutput = PrevOutput & {
26+
descriptorName: string;
27+
descriptorIndex: number;
28+
};
29+
30+
export type DerivedDescriptorWalletOutput = WithDescriptor<PrevOutput>;
31+
32+
export function toDerivedDescriptorWalletOutput(
33+
output: DescriptorWalletOutput,
34+
descriptorMap: DescriptorMap
35+
): DerivedDescriptorWalletOutput {
36+
const descriptor = descriptorMap.get(output.descriptorName);
37+
if (!descriptor) {
38+
throw new Error(`Descriptor not found: ${output.descriptorName}`);
39+
}
40+
const derivedDescriptor = descriptor.atDerivationIndex(output.descriptorIndex);
41+
const script = createScriptPubKeyFromDescriptor(derivedDescriptor);
42+
if (!script.equals(output.witnessUtxo.script)) {
43+
throw new Error(`Script mismatch: descriptor ${output.descriptorName} ${descriptor.toString()} script=${script}`);
44+
}
45+
return {
46+
hash: output.hash,
47+
index: output.index,
48+
witnessUtxo: output.witnessUtxo,
49+
descriptor: descriptor.atDerivationIndex(output.descriptorIndex),
50+
};
51+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Dimensions, VirtualSizes } from '@bitgo/unspents';
2+
import { Descriptor } from '@bitgo/wasm-miniscript';
3+
4+
import { DescriptorMap } from './DescriptorMap';
5+
6+
function getScriptPubKeyLength(descType: string): number {
7+
// See https://bitcoinops.org/en/tools/calc-size/
8+
switch (descType) {
9+
case 'Wpkh':
10+
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh
11+
return 22;
12+
case 'Sh':
13+
case 'ShWsh':
14+
case 'ShWpkh':
15+
// https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#specification
16+
return 23;
17+
case 'Pkh':
18+
return 25;
19+
case 'Wsh':
20+
case 'Tr':
21+
// P2WSH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh
22+
// P2TR: https://github.com/bitcoin/bips/blob/58ffd93812ff25e87d53d1f202fbb389fdfb85bb/bip-0341.mediawiki#script-validation-rules
23+
// > A Taproot output is a native SegWit output (see BIP141) with version number 1, and a 32-byte witness program.
24+
// 32 bytes for the hash, 1 byte for the version, 1 byte for the push opcode
25+
return 34;
26+
case 'Bare':
27+
throw new Error('cannot determine scriptPubKey length for Bare descriptor');
28+
default:
29+
throw new Error('unexpected descriptor type ' + descType);
30+
}
31+
}
32+
33+
function getInputVSizeForDescriptor(descriptor: Descriptor): number {
34+
// FIXME(BTC-1489): this can overestimate the size of the input significantly
35+
const maxWeight = descriptor.maxWeightToSatisfy();
36+
const maxVSize = Math.ceil(maxWeight / 4);
37+
const sizeOpPushdata1 = 1;
38+
const sizeOpPushdata2 = 2;
39+
return (
40+
// inputId
41+
32 +
42+
// vOut
43+
4 +
44+
// nSequence
45+
4 +
46+
// script overhead
47+
(maxVSize < 255 ? sizeOpPushdata1 : sizeOpPushdata2) +
48+
// script
49+
maxVSize
50+
);
51+
}
52+
53+
export function getInputVSizesForDescriptors(descriptors: DescriptorMap): Record<string, number> {
54+
return Object.fromEntries(
55+
Array.from(descriptors.entries()).map(([name, d]) => {
56+
return [name, getInputVSizeForDescriptor(d)];
57+
})
58+
);
59+
}
60+
61+
export function getChangeOutputVSizesForDescriptor(d: Descriptor): {
62+
inputVSize: number;
63+
outputVSize: number;
64+
} {
65+
return {
66+
inputVSize: getInputVSizeForDescriptor(d),
67+
outputVSize: getScriptPubKeyLength(d.descType()),
68+
};
69+
}
70+
71+
type InputWithDescriptorName = { descriptorName: string };
72+
type OutputWithScript = { script: Buffer };
73+
74+
type Tx<TInput> = {
75+
inputs: TInput[];
76+
outputs: OutputWithScript[];
77+
};
78+
79+
export function getVirtualSize(tx: Tx<Descriptor>): number;
80+
export function getVirtualSize(tx: Tx<InputWithDescriptorName>, descriptors: DescriptorMap): number;
81+
export function getVirtualSize(
82+
tx: Tx<Descriptor> | Tx<InputWithDescriptorName>,
83+
descriptorMap?: DescriptorMap
84+
): number {
85+
const lookup = descriptorMap ? getInputVSizesForDescriptors(descriptorMap) : undefined;
86+
const inputVSize = tx.inputs.reduce((sum, input) => {
87+
if (input instanceof Descriptor) {
88+
return sum + getInputVSizeForDescriptor(input);
89+
}
90+
if ('descriptorName' in input) {
91+
if (!lookup) {
92+
throw new Error('missing descriptorMap');
93+
}
94+
const vsize = lookup[input.descriptorName];
95+
if (!vsize) {
96+
throw new Error(`Could not find descriptor ${input.descriptorName}`);
97+
}
98+
return sum + vsize;
99+
}
100+
throw new Error('unexpected input');
101+
}, 0);
102+
const outputVSize = tx.outputs.reduce((sum, o) => {
103+
return sum + Dimensions.getVSizeForOutputWithScriptLength(o.script.length);
104+
}, 0);
105+
// we will just assume that we have at least one segwit input
106+
return inputVSize + outputVSize + VirtualSizes.txSegOverheadVSize;
107+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Descriptor } from '@bitgo/wasm-miniscript';
2+
import * as utxolib from '@bitgo/utxo-lib';
3+
4+
export function createScriptPubKeyFromDescriptor(descriptor: Descriptor, index?: number): Buffer {
5+
if (index === undefined) {
6+
return Buffer.from(descriptor.scriptPubkey());
7+
}
8+
return createScriptPubKeyFromDescriptor(descriptor.atDerivationIndex(index));
9+
}
10+
11+
export function createAddressFromDescriptor(
12+
descriptor: Descriptor,
13+
index: number | undefined,
14+
network: utxolib.Network
15+
): string {
16+
return utxolib.address.fromOutputScript(createScriptPubKeyFromDescriptor(descriptor, index), network);
17+
}

0 commit comments

Comments
 (0)