-
Notifications
You must be signed in to change notification settings - Fork 285
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
6e244e6
commit 7046b8a
Showing
51 changed files
with
3,193 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,9 @@ | |
}, | ||
{ | ||
"path": "../utxo-lib" | ||
}, | ||
{ | ||
"path": "../utxo-core" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
'use strict'; | ||
|
||
module.exports = { | ||
require: 'ts-node/register', | ||
extension: ['.js', '.ts'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# utxo-core | ||
|
||
This repository contains core types and functions for Bitcoin-like UTXO-based cryptocurrencies. | ||
|
||
It is the glue between SDK-dependent modules like `abstract-utxo` and low-level libraries like `utxo-lib` and | ||
`wasm-miniscript`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@bitgo/utxo-core", | ||
"version": "1.0.0", | ||
"description": "BitGo UTXO Core types", | ||
"main": "./dist/src/index.js", | ||
"types": "./dist/src/index.d.ts", | ||
"files": [ | ||
"dist/src" | ||
], | ||
"exports": { | ||
".": "./dist/src/index.js", | ||
"./descriptor": "./dist/src/descriptor/index.js", | ||
"./testutil": "./dist/src/testutil/index.js", | ||
"./testutil/descriptor": "./dist/src/testutil/descriptor/index.js" | ||
}, | ||
"scripts": { | ||
"build": "yarn tsc --build --incremental --verbose .", | ||
"fmt": "prettier --write .", | ||
"check-fmt": "prettier --check .", | ||
"clean": "rm -r ./dist", | ||
"lint": "eslint --quiet .", | ||
"prepare": "npm run build", | ||
"test": "npm run unit-test", | ||
"unit-test": "mocha --recursive test/" | ||
}, | ||
"author": "BitGo SDK Team <[email protected]>", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/BitGo/BitGoJS.git", | ||
"directory": "modules/utxo-core" | ||
}, | ||
"lint-staged": { | ||
"*.{js,ts}": [ | ||
"yarn prettier --write", | ||
"yarn eslint --fix" | ||
] | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"nyc": { | ||
"extension": [ | ||
".ts" | ||
] | ||
}, | ||
"dependencies": { | ||
"@bitgo/unspents": "^0.47.17", | ||
"@bitgo/utxo-lib": "^11.2.1", | ||
"@bitgo/wasm-miniscript": "^2.0.0-beta.2", | ||
"bip174": "npm:@bitgo-forks/[email protected]" | ||
}, | ||
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
|
||
/** Map from descriptor name to descriptor */ | ||
export type DescriptorMap = Map<string, Descriptor>; | ||
|
||
/** Convert an array of descriptor name-value pairs to a descriptor map */ | ||
export function toDescriptorMap(descriptors: { name: string; value: Descriptor | string }[]): DescriptorMap { | ||
return new Map( | ||
descriptors.map((d) => [ | ||
d.name, | ||
d.value instanceof Descriptor ? d.value : Descriptor.fromString(d.value, 'derivable'), | ||
]) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
|
||
import { DescriptorMap } from './DescriptorMap'; | ||
import { createScriptPubKeyFromDescriptor } from './address'; | ||
|
||
export type Output = { | ||
script: Buffer; | ||
value: bigint; | ||
}; | ||
|
||
export type WithDescriptor<T> = T & { | ||
descriptor: Descriptor; | ||
}; | ||
|
||
export type WithOptDescriptor<T> = T & { | ||
descriptor?: Descriptor; | ||
}; | ||
|
||
export type PrevOutput = { | ||
hash: string; | ||
index: number; | ||
witnessUtxo: Output; | ||
}; | ||
|
||
export type DescriptorWalletOutput = PrevOutput & { | ||
descriptorName: string; | ||
descriptorIndex: number; | ||
}; | ||
|
||
export type DerivedDescriptorWalletOutput = WithDescriptor<PrevOutput>; | ||
|
||
export function toDerivedDescriptorWalletOutput( | ||
output: DescriptorWalletOutput, | ||
descriptorMap: DescriptorMap | ||
): DerivedDescriptorWalletOutput { | ||
const descriptor = descriptorMap.get(output.descriptorName); | ||
if (!descriptor) { | ||
throw new Error(`Descriptor not found: ${output.descriptorName}`); | ||
} | ||
const derivedDescriptor = descriptor.atDerivationIndex(output.descriptorIndex); | ||
const script = createScriptPubKeyFromDescriptor(derivedDescriptor); | ||
if (!script.equals(output.witnessUtxo.script)) { | ||
throw new Error(`Script mismatch: descriptor ${output.descriptorName} ${descriptor.toString()} script=${script}`); | ||
} | ||
return { | ||
hash: output.hash, | ||
index: output.index, | ||
witnessUtxo: output.witnessUtxo, | ||
descriptor: descriptor.atDerivationIndex(output.descriptorIndex), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { Dimensions, VirtualSizes } from '@bitgo/unspents'; | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
|
||
import { DescriptorMap } from './DescriptorMap'; | ||
|
||
function getScriptPubKeyLength(descType: string): number { | ||
// See https://bitcoinops.org/en/tools/calc-size/ | ||
switch (descType) { | ||
case 'Wpkh': | ||
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wpkh | ||
return 22; | ||
case 'Sh': | ||
case 'ShWsh': | ||
case 'ShWpkh': | ||
// https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki#specification | ||
return 23; | ||
case 'Pkh': | ||
return 25; | ||
case 'Wsh': | ||
case 'Tr': | ||
// P2WSH: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh | ||
// P2TR: https://github.com/bitcoin/bips/blob/58ffd93812ff25e87d53d1f202fbb389fdfb85bb/bip-0341.mediawiki#script-validation-rules | ||
// > A Taproot output is a native SegWit output (see BIP141) with version number 1, and a 32-byte witness program. | ||
// 32 bytes for the hash, 1 byte for the version, 1 byte for the push opcode | ||
return 34; | ||
case 'Bare': | ||
throw new Error('cannot determine scriptPubKey length for Bare descriptor'); | ||
default: | ||
throw new Error('unexpected descriptor type ' + descType); | ||
} | ||
} | ||
|
||
function getInputVSizeForDescriptor(descriptor: Descriptor): number { | ||
// FIXME(BTC-1489): this can overestimate the size of the input significantly | ||
const maxWeight = descriptor.maxWeightToSatisfy(); | ||
const maxVSize = Math.ceil(maxWeight / 4); | ||
const sizeOpPushdata1 = 1; | ||
const sizeOpPushdata2 = 2; | ||
return ( | ||
// inputId | ||
32 + | ||
// vOut | ||
4 + | ||
// nSequence | ||
4 + | ||
// script overhead | ||
(maxVSize < 255 ? sizeOpPushdata1 : sizeOpPushdata2) + | ||
// script | ||
maxVSize | ||
); | ||
} | ||
|
||
export function getInputVSizesForDescriptors(descriptors: DescriptorMap): Record<string, number> { | ||
return Object.fromEntries( | ||
Array.from(descriptors.entries()).map(([name, d]) => { | ||
return [name, getInputVSizeForDescriptor(d)]; | ||
}) | ||
); | ||
} | ||
|
||
export function getChangeOutputVSizesForDescriptor(d: Descriptor): { | ||
inputVSize: number; | ||
outputVSize: number; | ||
} { | ||
return { | ||
inputVSize: getInputVSizeForDescriptor(d), | ||
outputVSize: getScriptPubKeyLength(d.descType()), | ||
}; | ||
} | ||
|
||
type InputWithDescriptorName = { descriptorName: string }; | ||
type OutputWithScript = { script: Buffer }; | ||
|
||
type Tx<TInput> = { | ||
inputs: TInput[]; | ||
outputs: OutputWithScript[]; | ||
}; | ||
|
||
export function getVirtualSize(tx: Tx<Descriptor>): number; | ||
export function getVirtualSize(tx: Tx<InputWithDescriptorName>, descriptors: DescriptorMap): number; | ||
export function getVirtualSize( | ||
tx: Tx<Descriptor> | Tx<InputWithDescriptorName>, | ||
descriptorMap?: DescriptorMap | ||
): number { | ||
const lookup = descriptorMap ? getInputVSizesForDescriptors(descriptorMap) : undefined; | ||
const inputVSize = tx.inputs.reduce((sum, input) => { | ||
if (input instanceof Descriptor) { | ||
return sum + getInputVSizeForDescriptor(input); | ||
} | ||
if ('descriptorName' in input) { | ||
if (!lookup) { | ||
throw new Error('missing descriptorMap'); | ||
} | ||
const vsize = lookup[input.descriptorName]; | ||
if (!vsize) { | ||
throw new Error(`Could not find descriptor ${input.descriptorName}`); | ||
} | ||
return sum + vsize; | ||
} | ||
throw new Error('unexpected input'); | ||
}, 0); | ||
const outputVSize = tx.outputs.reduce((sum, o) => { | ||
return sum + Dimensions.getVSizeForOutputWithScriptLength(o.script.length); | ||
}, 0); | ||
// we will just assume that we have at least one segwit input | ||
return inputVSize + outputVSize + VirtualSizes.txSegOverheadVSize; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
|
||
export function createScriptPubKeyFromDescriptor(descriptor: Descriptor, index?: number): Buffer { | ||
if (index === undefined) { | ||
return Buffer.from(descriptor.scriptPubkey()); | ||
} | ||
return createScriptPubKeyFromDescriptor(descriptor.atDerivationIndex(index)); | ||
} | ||
|
||
export function createAddressFromDescriptor( | ||
descriptor: Descriptor, | ||
index: number | undefined, | ||
network: utxolib.Network | ||
): string { | ||
return utxolib.address.fromOutputScript(createScriptPubKeyFromDescriptor(descriptor, index), network); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export type { Output, DescriptorWalletOutput, WithDescriptor, WithOptDescriptor } from './Output'; | ||
export type { DescriptorMap } from './DescriptorMap'; | ||
export * as psbt from './psbt'; | ||
export type { PsbtParams } from './psbt'; | ||
|
||
export { createAddressFromDescriptor, createScriptPubKeyFromDescriptor } from './address'; | ||
export { createPsbt, finalizePsbt, parse } from './psbt'; | ||
export { toDescriptorMap } from './DescriptorMap'; | ||
export { toDerivedDescriptorWalletOutput } from './Output'; |
72 changes: 72 additions & 0 deletions
72
modules/utxo-core/src/descriptor/psbt/assertSatisfiable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* These are some helpers for testing satisfiability of descriptors in PSBTs. | ||
* | ||
* They are mostly a debugging aid - if an input cannot be satisified, the `finalizePsbt()` method will fail, but | ||
* the error message is pretty vague. | ||
* | ||
* The methods here have the goal of catching certain cases earlier and with a better error message. | ||
* | ||
* The goal is not an exhaustive check, but to catch common mistakes. | ||
*/ | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
|
||
export const FINAL_SEQUENCE = 0xffffffff; | ||
|
||
/** | ||
* Get the required locktime for a descriptor. | ||
* @param descriptor | ||
*/ | ||
export function getRequiredLocktime(descriptor: Descriptor | unknown): number | undefined { | ||
if (descriptor instanceof Descriptor) { | ||
return getRequiredLocktime(descriptor.node()); | ||
} | ||
if (typeof descriptor !== 'object' || descriptor === null) { | ||
return undefined; | ||
} | ||
if ('Wsh' in descriptor) { | ||
return getRequiredLocktime(descriptor.Wsh); | ||
} | ||
if ('Sh' in descriptor) { | ||
return getRequiredLocktime(descriptor.Sh); | ||
} | ||
if ('Ms' in descriptor) { | ||
return getRequiredLocktime(descriptor.Ms); | ||
} | ||
if ('AndV' in descriptor) { | ||
if (!Array.isArray(descriptor.AndV)) { | ||
throw new Error('Expected an array'); | ||
} | ||
if (descriptor.AndV.length !== 2) { | ||
throw new Error('Expected exactly two elements'); | ||
} | ||
const [a, b] = descriptor.AndV; | ||
return getRequiredLocktime(a) ?? getRequiredLocktime(b); | ||
} | ||
if ('Drop' in descriptor) { | ||
return getRequiredLocktime(descriptor.Drop); | ||
} | ||
if ('Verify' in descriptor) { | ||
return getRequiredLocktime(descriptor.Verify); | ||
} | ||
if ('After' in descriptor && typeof descriptor.After === 'object' && descriptor.After !== null) { | ||
if ('absLockTime' in descriptor.After && typeof descriptor.After.absLockTime === 'number') { | ||
return descriptor.After.absLockTime; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
export function assertSatisfiable(psbt: utxolib.Psbt, inputIndex: number, descriptor: Descriptor): void { | ||
// If the descriptor requires a locktime, the input must have a non-final sequence number | ||
const requiredLocktime = getRequiredLocktime(descriptor); | ||
if (requiredLocktime !== undefined) { | ||
const input = psbt.txInputs[inputIndex]; | ||
if (input.sequence === FINAL_SEQUENCE) { | ||
throw new Error(`Input ${inputIndex} has a non-final sequence number, but requires a timelock`); | ||
} | ||
if (psbt.locktime !== requiredLocktime) { | ||
throw new Error(`psbt locktime (${psbt.locktime}) does not match required locktime (${requiredLocktime})`); | ||
} | ||
} | ||
} |
Oops, something went wrong.