-
Notifications
You must be signed in to change notification settings - Fork 276
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
feat: add `abstract-utxo/src/core`
- Loading branch information
Showing
38 changed files
with
2,237 additions
and
6 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,9 @@ | |
"check-fmt": "prettier --check .", | ||
"clean": "rm -r ./dist", | ||
"lint": "eslint --quiet .", | ||
"prepare": "npm run build" | ||
"prepare": "npm run build", | ||
"test": "npm run unit-test", | ||
"unit-test": "mocha --recursive test/" | ||
}, | ||
"author": "BitGo SDK Team <[email protected]>", | ||
"license": "MIT", | ||
|
@@ -46,6 +48,7 @@ | |
"@types/bluebird": "^3.5.25", | ||
"@types/lodash": "^4.14.121", | ||
"@types/superagent": "4.1.15", | ||
"bip174": "npm:@bitgo-forks/[email protected]", | ||
"bignumber.js": "^9.0.2", | ||
"bitcoinjs-message": "npm:@bitgo-forks/[email protected]", | ||
"bluebird": "^3.5.3", | ||
|
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 @@ | ||
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: string }[]): DescriptorMap { | ||
return new Map(descriptors.map((d) => [d.name, 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), | ||
}; | ||
} |
103 changes: 103 additions & 0 deletions
103
modules/abstract-utxo/src/core/descriptor/VirtualSize.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,103 @@ | ||
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': | ||
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh | ||
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,10 @@ | ||
export type { Output, DescriptorWalletOutput, WithDescriptor, WithOptDescriptor } from './Output'; | ||
export type { DescriptorMap } from './DescriptorMap'; | ||
export type { PsbtParams } from './psbt'; | ||
|
||
export { createAddressFromDescriptor, createScriptPubKeyFromDescriptor } from './address'; | ||
export { createPsbt, finalizePsbt } from './psbt'; | ||
export { toDescriptorMap } from './DescriptorMap'; | ||
export { toDerivedDescriptorWalletOutput } from './Output'; | ||
export { signTxLocal } from './signTxLocal'; | ||
export { parseAndValidateTransaction } from './parseTransaction'; |
1 change: 1 addition & 0 deletions
1
modules/abstract-utxo/src/core/descriptor/parseTransaction/index.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 @@ | ||
export * from './parseTransaction'; |
82 changes: 82 additions & 0 deletions
82
modules/abstract-utxo/src/core/descriptor/parseTransaction/parseTransaction.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,82 @@ | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
|
||
import { DescriptorMap } from '../DescriptorMap'; | ||
import { getVirtualSize } from '../VirtualSize'; | ||
import { findDescriptorForInput, findDescriptorForOutput } from '../psbt/findDescriptors'; | ||
import { assertSatisfiable } from '../psbt/assertSatisfiable'; | ||
|
||
type ScriptId = { descriptor: Descriptor; index: number }; | ||
|
||
type ParsedInput = { | ||
address: string; | ||
value: bigint; | ||
scriptId: ScriptId; | ||
}; | ||
|
||
type ParsedOutput = { | ||
address?: string; | ||
script: Buffer; | ||
value: bigint; | ||
scriptId?: ScriptId; | ||
}; | ||
|
||
type ParsedDescriptorTransaction = { | ||
inputs: ParsedInput[]; | ||
outputs: ParsedOutput[]; | ||
spendAmount: bigint; | ||
minerFee: bigint; | ||
virtualSize: number; | ||
}; | ||
|
||
function sum(...values: bigint[]): bigint { | ||
return values.reduce((a, b) => a + b, BigInt(0)); | ||
} | ||
|
||
export function parseAndValidateTransaction( | ||
psbt: utxolib.Psbt, | ||
descriptorMap: DescriptorMap, | ||
network: utxolib.Network | ||
): ParsedDescriptorTransaction { | ||
const inputs = psbt.data.inputs.map((input, inputIndex): ParsedInput => { | ||
if (!input.witnessUtxo) { | ||
throw new Error('invalid input: no witnessUtxo'); | ||
} | ||
if (!input.witnessUtxo.value) { | ||
throw new Error('invalid input: no value'); | ||
} | ||
const descriptorWithIndex = findDescriptorForInput(input, descriptorMap); | ||
if (!descriptorWithIndex) { | ||
throw new Error('invalid input: no descriptor found'); | ||
} | ||
assertSatisfiable(psbt, inputIndex, descriptorWithIndex.descriptor); | ||
return { | ||
address: utxolib.address.fromOutputScript(input.witnessUtxo.script, network), | ||
value: input.witnessUtxo.value, | ||
scriptId: descriptorWithIndex, | ||
}; | ||
}); | ||
const outputs = psbt.txOutputs.map((output, i): ParsedOutput => { | ||
if (output.value === undefined) { | ||
throw new Error('invalid output: no value'); | ||
} | ||
const descriptorWithIndex = findDescriptorForOutput(output.script, psbt.data.outputs[i], descriptorMap); | ||
return { | ||
address: output.address, | ||
script: output.script, | ||
value: output.value, | ||
scriptId: descriptorWithIndex, | ||
}; | ||
}); | ||
const inputAmount = sum(...inputs.map((input) => input.value)); | ||
const outputSum = sum(...outputs.map((output) => output.value)); | ||
const spendAmount = sum(...outputs.filter((output) => !('descriptor' in output)).map((output) => output.value)); | ||
const minerFee = inputAmount - outputSum; | ||
return { | ||
inputs, | ||
outputs, | ||
spendAmount, | ||
minerFee, | ||
virtualSize: getVirtualSize({ inputs: inputs.map((i) => i.scriptId.descriptor), outputs }), | ||
}; | ||
} |
72 changes: 72 additions & 0 deletions
72
modules/abstract-utxo/src/core/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.