From c2187fbf1d5ea150cc8a69630e1b819dd2a300e0 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 4 Dec 2024 12:42:43 +0100 Subject: [PATCH 1/2] feat(abstract-utxo): include test files in tsconfig.json Issue: BTC-1450 --- modules/abstract-utxo/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/abstract-utxo/tsconfig.json b/modules/abstract-utxo/tsconfig.json index 7403ea33d4..fac925b67f 100644 --- a/modules/abstract-utxo/tsconfig.json +++ b/modules/abstract-utxo/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] }, - "include": ["src/**/*"], + "include": ["src/**/*", "test/**/*"], "exclude": ["node_modules"], "references": [ { From 6ca55f5fb0df16e2a38fcd7a9bcd7286bbb68d5d Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 4 Dec 2024 11:30:55 +0100 Subject: [PATCH 2/2] feat(abstract-utxo): add package src/core Adds psbt and descriptor utilities TICKET: BTC-1450 --- modules/abstract-utxo/.mocharc.js | 6 +- modules/abstract-utxo/package.json | 5 +- .../src/core/descriptor/DescriptorMap.ts | 9 ++ .../src/core/descriptor/Output.ts | 51 +++++++ .../src/core/descriptor/VirtualSize.ts | 103 +++++++++++++ .../src/core/descriptor/address.ts | 17 +++ .../src/core/descriptor/index.ts | 10 ++ .../core/descriptor/parseTransaction/index.ts | 1 + .../parseTransaction/parseTransaction.ts | 82 ++++++++++ .../core/descriptor/psbt/assertSatisfiable.ts | 72 +++++++++ .../src/core/descriptor/psbt/createPsbt.ts | 66 ++++++++ .../core/descriptor/psbt/findDescriptors.ts | 116 ++++++++++++++ .../src/core/descriptor/psbt/index.ts | 4 + .../src/core/descriptor/psbt/wrap.ts | 40 +++++ .../src/core/descriptor/signTxLocal.ts | 7 + .../test/core/descriptor/descriptor.utils.ts | 76 ++++++++++ .../test/core/descriptor/psbt/VirtualSize.ts | 66 ++++++++ .../core/descriptor/psbt/assertSatisfiable.ts | 24 +++ .../test/core/descriptor/psbt/createPsbt.ts | 64 ++++++++ .../core/descriptor/psbt/findDescriptors.ts | 38 +++++ .../ShWsh2Of3CltvDrop.createPsbt.json | 120 +++++++++++++++ .../ShWsh2Of3CltvDrop.createPsbtFinal.json | 87 +++++++++++ .../ShWsh2Of3CltvDrop.createPsbtFinalTx.hex | 1 + .../ShWsh2Of3CltvDrop.createPsbtFinalTx.json | 52 +++++++ .../ShWsh2Of3CltvDrop.createPsbtSignedA.json | 133 +++++++++++++++++ .../ShWsh2Of3CltvDrop.createPsbtSignedAB.json | 141 ++++++++++++++++++ .../psbt/fixtures/Wsh2Of3.createPsbt.json | 117 +++++++++++++++ .../fixtures/Wsh2Of3.createPsbtFinal.json | 84 +++++++++++ .../fixtures/Wsh2Of3.createPsbtFinalTx.hex | 1 + .../fixtures/Wsh2Of3.createPsbtFinalTx.json | 52 +++++++ .../fixtures/Wsh2Of3.createPsbtSignedA.json | 130 ++++++++++++++++ .../fixtures/Wsh2Of3.createPsbtSignedAB.json | 138 +++++++++++++++++ .../test/core/descriptor/psbt/mock.utils.ts | 104 +++++++++++++ .../test/core/descriptor/psbt/psbt.utils.ts | 25 ++++ .../abstract-utxo/test/core/fixtures.utils.ts | 85 +++++++++++ modules/abstract-utxo/test/core/key.utils.ts | 28 ++++ .../test/core/toPlainObject.utils.ts | 86 +++++++++++ 37 files changed, 2236 insertions(+), 5 deletions(-) create mode 100644 modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/Output.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/VirtualSize.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/address.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/index.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/parseTransaction/index.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/parseTransaction/parseTransaction.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/psbt/assertSatisfiable.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/psbt/createPsbt.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/psbt/findDescriptors.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/psbt/index.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/psbt/wrap.ts create mode 100644 modules/abstract-utxo/src/core/descriptor/signTxLocal.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/descriptor.utils.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/VirtualSize.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/assertSatisfiable.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/createPsbt.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/findDescriptors.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbt.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinal.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.hex create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedA.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedAB.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbt.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinal.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.hex create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedA.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedAB.json create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/mock.utils.ts create mode 100644 modules/abstract-utxo/test/core/descriptor/psbt/psbt.utils.ts create mode 100644 modules/abstract-utxo/test/core/fixtures.utils.ts create mode 100644 modules/abstract-utxo/test/core/key.utils.ts create mode 100644 modules/abstract-utxo/test/core/toPlainObject.utils.ts diff --git a/modules/abstract-utxo/.mocharc.js b/modules/abstract-utxo/.mocharc.js index ae5ec5d5c8..e88f8a81b8 100644 --- a/modules/abstract-utxo/.mocharc.js +++ b/modules/abstract-utxo/.mocharc.js @@ -2,9 +2,7 @@ module.exports = { require: 'ts-node/register', - timeout: '20000', - reporter: 'min', - 'reporter-option': ['cdn=true', 'json=false'], + timeout: '2000', exit: true, - spec: ['test/unit/**/*.ts'], + spec: ['test/**/*.ts'], }; diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index bb87772532..e39f440607 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -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 ", "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/bip174@3.1.0-master.4", "bignumber.js": "^9.0.2", "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", "bluebird": "^3.5.3", diff --git a/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts b/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts new file mode 100644 index 0000000000..d726a9935f --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/DescriptorMap.ts @@ -0,0 +1,9 @@ +import { Descriptor } from '@bitgo/wasm-miniscript'; + +/** Map from descriptor name to descriptor */ +export type DescriptorMap = Map; + +/** 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')])); +} diff --git a/modules/abstract-utxo/src/core/descriptor/Output.ts b/modules/abstract-utxo/src/core/descriptor/Output.ts new file mode 100644 index 0000000000..9f8cc83efb --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/Output.ts @@ -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 & { + descriptor: Descriptor; +}; + +export type WithOptDescriptor = T & { + descriptor?: Descriptor; +}; + +export type PrevOutput = { + hash: string; + index: number; + witnessUtxo: Output; +}; + +export type DescriptorWalletOutput = PrevOutput & { + descriptorName: string; + descriptorIndex: number; +}; + +export type DerivedDescriptorWalletOutput = WithDescriptor; + +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), + }; +} diff --git a/modules/abstract-utxo/src/core/descriptor/VirtualSize.ts b/modules/abstract-utxo/src/core/descriptor/VirtualSize.ts new file mode 100644 index 0000000000..a1e035dd62 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/VirtualSize.ts @@ -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 { + 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 = { + inputs: TInput[]; + outputs: OutputWithScript[]; +}; + +export function getVirtualSize(tx: Tx): number; +export function getVirtualSize(tx: Tx, descriptors: DescriptorMap): number; +export function getVirtualSize( + tx: Tx | Tx, + 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; +} diff --git a/modules/abstract-utxo/src/core/descriptor/address.ts b/modules/abstract-utxo/src/core/descriptor/address.ts new file mode 100644 index 0000000000..f813931800 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/address.ts @@ -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); +} diff --git a/modules/abstract-utxo/src/core/descriptor/index.ts b/modules/abstract-utxo/src/core/descriptor/index.ts new file mode 100644 index 0000000000..aee12d9220 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/index.ts @@ -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'; diff --git a/modules/abstract-utxo/src/core/descriptor/parseTransaction/index.ts b/modules/abstract-utxo/src/core/descriptor/parseTransaction/index.ts new file mode 100644 index 0000000000..21b5fc603b --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/parseTransaction/index.ts @@ -0,0 +1 @@ +export * from './parseTransaction'; diff --git a/modules/abstract-utxo/src/core/descriptor/parseTransaction/parseTransaction.ts b/modules/abstract-utxo/src/core/descriptor/parseTransaction/parseTransaction.ts new file mode 100644 index 0000000000..b770f05ee3 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/parseTransaction/parseTransaction.ts @@ -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 }), + }; +} diff --git a/modules/abstract-utxo/src/core/descriptor/psbt/assertSatisfiable.ts b/modules/abstract-utxo/src/core/descriptor/psbt/assertSatisfiable.ts new file mode 100644 index 0000000000..46e7eefbb9 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/psbt/assertSatisfiable.ts @@ -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})`); + } + } +} diff --git a/modules/abstract-utxo/src/core/descriptor/psbt/createPsbt.ts b/modules/abstract-utxo/src/core/descriptor/psbt/createPsbt.ts new file mode 100644 index 0000000000..bc293b8ecc --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/psbt/createPsbt.ts @@ -0,0 +1,66 @@ +import * as utxolib from '@bitgo/utxo-lib'; +import { Descriptor } from '@bitgo/wasm-miniscript'; + +import { DerivedDescriptorWalletOutput, Output, WithOptDescriptor } from '../Output'; + +import { toUtxoPsbt, toWrappedPsbt } from './wrap'; +import { assertSatisfiable } from './assertSatisfiable'; + +/** + * Non-Final (Replaceable) + * Reference: https://github.com/bitcoin/bitcoin/blob/v25.1/src/rpc/rawtransaction_util.cpp#L49 + * */ +export const MAX_BIP125_RBF_SEQUENCE = 0xffffffff - 2; + +function updateInputsWithDescriptors(psbt: utxolib.bitgo.UtxoPsbt, descriptors: Descriptor[]) { + if (psbt.txInputs.length !== descriptors.length) { + throw new Error(`Input count mismatch (psbt=${psbt.txInputs.length}, descriptors=${descriptors.length})`); + } + const wrappedPsbt = toWrappedPsbt(psbt); + for (const [inputIndex, descriptor] of descriptors.entries()) { + assertSatisfiable(psbt, inputIndex, descriptor); + wrappedPsbt.updateInputWithDescriptor(inputIndex, descriptor); + } + const unwrappedPsbt = toUtxoPsbt(wrappedPsbt, psbt.network); + for (const inputIndex in psbt.txInputs) { + psbt.data.inputs[inputIndex] = unwrappedPsbt.data.inputs[inputIndex]; + } +} + +function updateOutputsWithDescriptors(psbt: utxolib.bitgo.UtxoPsbt, descriptors: WithOptDescriptor[]) { + const wrappedPsbt = toWrappedPsbt(psbt); + for (const [outputIndex, { descriptor }] of descriptors.entries()) { + if (descriptor) { + wrappedPsbt.updateOutputWithDescriptor(outputIndex, descriptor); + } + } + const unwrappedPsbt = toUtxoPsbt(wrappedPsbt, psbt.network); + for (const outputIndex in psbt.txOutputs) { + psbt.data.outputs[outputIndex] = unwrappedPsbt.data.outputs[outputIndex]; + } +} + +export type PsbtParams = { + network: utxolib.Network; + version?: number; + locktime?: number; + sequence?: number; +}; + +export function createPsbt( + params: PsbtParams, + inputs: DerivedDescriptorWalletOutput[], + outputs: WithOptDescriptor[] +): utxolib.bitgo.UtxoPsbt { + const psbt = utxolib.bitgo.UtxoPsbt.createPsbt({ network: params.network }); + psbt.setVersion(params.version ?? 2); + psbt.setLocktime(params.locktime ?? 0); + psbt.addInputs(inputs.map((i) => ({ ...i, sequence: params.sequence ?? MAX_BIP125_RBF_SEQUENCE }))); + psbt.addOutputs(outputs); + updateInputsWithDescriptors( + psbt, + inputs.map((i) => i.descriptor) + ); + updateOutputsWithDescriptors(psbt, outputs); + return psbt; +} diff --git a/modules/abstract-utxo/src/core/descriptor/psbt/findDescriptors.ts b/modules/abstract-utxo/src/core/descriptor/psbt/findDescriptors.ts new file mode 100644 index 0000000000..c4e1dd8df1 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/psbt/findDescriptors.ts @@ -0,0 +1,116 @@ +/* + +Utilities for mapping back from PSBT inputs to descriptors. + +This is a somewhat brute-force attempt that relies on the `bip32Derivation` field to be set. + +It will probably only work correctly if all xpubs in the descriptor are derivable. + +We should take a look at a more robust and standard approach like this: https://github.com/bitcoin/bips/pull/1548 + + */ +import { PsbtInput, PsbtOutput } from 'bip174/src/lib/interfaces'; +import { Descriptor } from '@bitgo/wasm-miniscript'; + +import { DescriptorMap } from '../DescriptorMap'; + +type DescriptorWithIndex = { descriptor: Descriptor; index: number }; + +/** + * Find a descriptor in the descriptor map that matches the given script and derivation index. + * @param script + * @param index + * @param descriptorMap + * @returns DescriptorWithIndex if found, undefined otherwise + */ +function findDescriptorForDerivationIndex( + script: Buffer, + index: number, + descriptorMap: DescriptorMap +): DescriptorWithIndex | undefined { + for (const descriptor of descriptorMap.values()) { + if (Buffer.from(descriptor.atDerivationIndex(index).scriptPubkey()).equals(script)) { + return { descriptor, index }; + } + } + + return undefined; +} + +function getDerivationIndexFromPath(path: string): number { + const indexStr = path.split('/').pop(); + if (!indexStr) { + throw new Error('Invalid derivation path'); + } + const index = parseInt(indexStr, 10); + if (index.toString() !== indexStr) { + throw new Error(`Invalid derivation path ${path}`); + } + return index; +} + +/** + * Wrapper around findDescriptorForDerivationPath that tries multiple derivation paths. + * @param script + * @param derivationPaths + * @param descriptorMap + */ +function findDescriptorForAnyDerivationPath( + script: Buffer, + derivationPaths: string[], + descriptorMap: DescriptorMap +): DescriptorWithIndex | undefined { + const derivationIndexSet = new Set(derivationPaths.map((p) => getDerivationIndexFromPath(p))); + for (const index of [...derivationIndexSet]) { + const desc = findDescriptorForDerivationIndex(script, index, descriptorMap); + if (desc) { + return desc; + } + } + + return undefined; +} + +/** + * @param input + * @param descriptorMap + * @returns DescriptorWithIndex for the input if found, undefined otherwise + */ +export function findDescriptorForInput( + input: PsbtInput, + descriptorMap: DescriptorMap +): DescriptorWithIndex | undefined { + const script = input.witnessUtxo?.script; + if (!script) { + throw new Error('Missing script'); + } + if (!input.bip32Derivation) { + throw new Error('Missing derivation paths'); + } + return findDescriptorForAnyDerivationPath( + script, + input.bip32Derivation.map((v) => v.path), + descriptorMap + ); +} + +/** + * @param script - the output script + * @param output - the PSBT output + * @param descriptorMap + * @returns DescriptorWithIndex for the output if found, undefined otherwise + */ +export function findDescriptorForOutput( + script: Buffer, + output: PsbtOutput, + descriptorMap: DescriptorMap +): DescriptorWithIndex | undefined { + if (!output.bip32Derivation) { + return undefined; + } + return findDescriptorForAnyDerivationPath( + script, + output.bip32Derivation.map((d) => d.path), + descriptorMap + ); +} diff --git a/modules/abstract-utxo/src/core/descriptor/psbt/index.ts b/modules/abstract-utxo/src/core/descriptor/psbt/index.ts new file mode 100644 index 0000000000..78fd236f51 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/psbt/index.ts @@ -0,0 +1,4 @@ +export type { PsbtParams } from './createPsbt'; +export { createPsbt } from './createPsbt'; + +export { finalizePsbt } from './wrap'; diff --git a/modules/abstract-utxo/src/core/descriptor/psbt/wrap.ts b/modules/abstract-utxo/src/core/descriptor/psbt/wrap.ts new file mode 100644 index 0000000000..c94699e6f5 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/psbt/wrap.ts @@ -0,0 +1,40 @@ +import { Psbt as WasmPsbt } from '@bitgo/wasm-miniscript'; +import * as utxolib from '@bitgo/utxo-lib'; + +export function toWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | utxolib.Psbt | Buffer | Uint8Array): WasmPsbt { + if (psbt instanceof utxolib.bitgo.UtxoPsbt || psbt instanceof utxolib.Psbt) { + psbt = psbt.toBuffer(); + } + if (psbt instanceof Buffer || psbt instanceof Uint8Array) { + return WasmPsbt.deserialize(psbt); + } + throw new Error('Invalid input'); +} + +export function toUtxoPsbt(psbt: WasmPsbt | Buffer | Uint8Array, network: utxolib.Network): utxolib.bitgo.UtxoPsbt { + if (psbt instanceof WasmPsbt) { + psbt = psbt.serialize(); + } + if (psbt instanceof Buffer || psbt instanceof Uint8Array) { + return utxolib.bitgo.UtxoPsbt.fromBuffer(Buffer.from(psbt), { network }); + } + throw new Error('Invalid input'); +} + +/** + * Use `wasm-miniscript` to finalize a PSBT. + * Miniscript based finalization is more powerful than bitcoinjs-lib's / utxo-lib's finalization + * and can finalize more complex scripts (e.g. miniscript descriptors). + * @param psbt + */ +export function finalizePsbt(psbt: utxolib.bitgo.UtxoPsbt): void { + if (utxolib.getMainnet(psbt.network) !== utxolib.networks.bitcoin) { + throw new Error('only bitcoin and testnet are supported'); + } + const wrappedPsbt = toWrappedPsbt(psbt); + wrappedPsbt.finalize(); + const unwrappedPsbt = toUtxoPsbt(wrappedPsbt, psbt.network); + for (let i = 0; i < psbt.data.inputs.length; i++) { + psbt.data.inputs[i] = unwrappedPsbt.data.inputs[i]; + } +} diff --git a/modules/abstract-utxo/src/core/descriptor/signTxLocal.ts b/modules/abstract-utxo/src/core/descriptor/signTxLocal.ts new file mode 100644 index 0000000000..ae48312888 --- /dev/null +++ b/modules/abstract-utxo/src/core/descriptor/signTxLocal.ts @@ -0,0 +1,7 @@ +import * as utxolib from '@bitgo/utxo-lib'; + +export function signTxLocal(tx: utxolib.bitgo.UtxoPsbt, key: utxolib.BIP32Interface): utxolib.bitgo.UtxoPsbt { + tx = tx.clone(); + tx.signAllInputsHD(key); + return tx; +} diff --git a/modules/abstract-utxo/test/core/descriptor/descriptor.utils.ts b/modules/abstract-utxo/test/core/descriptor/descriptor.utils.ts new file mode 100644 index 0000000000..c4a1f7048c --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/descriptor.utils.ts @@ -0,0 +1,76 @@ +import { Descriptor } from '@bitgo/wasm-miniscript'; + +import { DescriptorMap, PsbtParams } from '../../../src/core/descriptor'; +import { getKeyTriple } from '../key.utils'; + +export function getDefaultXPubs(seed?: string): string[] { + return getKeyTriple(seed).map((k) => k.neutered().toBase58()); +} + +function toDescriptorMap(v: Record): DescriptorMap { + return new Map(Object.entries(v).map(([k, v]) => [k, Descriptor.fromString(v, 'derivable')])); +} + +export type DescriptorTemplate = + | 'Wsh2Of3' + | 'Wsh2Of2' + /* + * This is a wrapped segwit 2of3 multisig that also uses a relative locktime with + * an OP_DROP (requiring a miniscript extension). + * It is basically what is used in CoreDao staking transactions. + */ + | 'ShWsh2Of3CltvDrop'; + +function multi(m: number, n: number, keys: string[], path: string): string { + if (n < m) { + throw new Error(`Cannot create ${m} of ${n} multisig`); + } + if (keys.length < n) { + throw new Error(`Not enough keys for ${m} of ${n} multisig: keys.length=${keys.length}`); + } + keys = keys.slice(0, n); + return `multi(${m},${keys.map((k) => `${k}/${path}`).join(',')})`; +} + +export function getPsbtParams(t: DescriptorTemplate): Partial { + switch (t) { + case 'Wsh2Of3': + case 'Wsh2Of2': + return {}; + case 'ShWsh2Of3CltvDrop': + return { locktime: 1 }; + } +} + +export function getDescriptorString( + template: DescriptorTemplate, + keys: string[] = getDefaultXPubs(), + path = '0/*' +): string { + switch (template) { + case 'Wsh2Of3': + return `wsh(${multi(2, 3, keys, path)})`; + case 'ShWsh2Of3CltvDrop': + const { locktime } = getPsbtParams(template); + return `sh(wsh(and_v(r:after(${locktime}),${multi(2, 3, keys, path)})))`; + case 'Wsh2Of2': { + return `wsh(${multi(2, 2, keys, path)})`; + } + } + throw new Error(`Unknown descriptor template: ${template}`); +} + +export function getDescriptor( + template: DescriptorTemplate, + keys: string[] = getDefaultXPubs(), + path = '0/*' +): Descriptor { + return Descriptor.fromString(getDescriptorString(template, keys, path), 'derivable'); +} + +export function getDescriptorMap(template: DescriptorTemplate, keys: string[] = getDefaultXPubs()): DescriptorMap { + return toDescriptorMap({ + external: getDescriptor(template, keys, '0/*').toString(), + internal: getDescriptor(template, keys, '1/*').toString(), + }); +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/VirtualSize.ts b/modules/abstract-utxo/test/core/descriptor/psbt/VirtualSize.ts new file mode 100644 index 0000000000..836572d152 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/VirtualSize.ts @@ -0,0 +1,66 @@ +import * as assert from 'assert'; + +import { + getChangeOutputVSizesForDescriptor, + getInputVSizesForDescriptors, + getVirtualSize, +} from '../../../../src/core/descriptor/VirtualSize'; +import { getDescriptor, getDescriptorMap } from '../descriptor.utils'; + +describe('VirtualSize', function () { + describe('getInputVSizesForDescriptorWallet', function () { + it('returns the input virtual sizes for a descriptor wallet', function () { + assert.deepStrictEqual( + getInputVSizesForDescriptors( + new Map([ + ['foo', getDescriptor('Wsh2Of2')], + ['bar', getDescriptor('Wsh2Of2')], + ]) + ), + { + foo: 96, + bar: 96, + } + ); + assert.deepStrictEqual(getInputVSizesForDescriptors(getDescriptorMap('Wsh2Of3')), { + external: 105, + internal: 105, + }); + }); + }); + + describe('getChangeOutputVSizesForDescriptor', function () { + it('returns the output virtual sizes for a descriptor', function () { + assert.deepStrictEqual(getChangeOutputVSizesForDescriptor(getDescriptor('Wsh2Of2')), { + inputVSize: 96, + outputVSize: 34, + }); + }); + }); + + describe('getVirtualSize', function () { + it('returns expected virtual size', function () { + assert.deepStrictEqual( + getVirtualSize( + { + inputs: [{ descriptorName: 'internal' }], + outputs: [{ script: Buffer.alloc(32) }], + }, + getDescriptorMap('Wsh2Of3') + ), + 157 + ); + + const descriptor = getDescriptor('Wsh2Of3'); + + assert.deepStrictEqual( + getVirtualSize({ + /* as proof we can pass 10_000 inputs */ + inputs: Array.from({ length: 10_000 }).map(() => descriptor), + outputs: [{ script: Buffer.alloc(32) }], + }), + 1_050_052 + ); + }); + }); +}); diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/assertSatisfiable.ts b/modules/abstract-utxo/test/core/descriptor/psbt/assertSatisfiable.ts new file mode 100644 index 0000000000..553389ccff --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/assertSatisfiable.ts @@ -0,0 +1,24 @@ +import * as assert from 'assert'; + +import { Descriptor } from '@bitgo/wasm-miniscript'; + +import { getDefaultXPubs } from '../descriptor.utils'; +import { getRequiredLocktime } from '../../../../src/core/descriptor/psbt/assertSatisfiable'; + +function d(s: string): Descriptor { + return Descriptor.fromString(s, 'derivable'); +} + +describe('assertSatisfiable', function () { + describe('getRequiredLocktime', function () { + const xpubs = getDefaultXPubs(); + it('has expected result', function () { + // OP_DROP + assert.strictEqual(getRequiredLocktime(d(`wsh(and_v(r:after(100),pk(${xpubs[0]})))`)), 100); + // OP_VERIFY + assert.strictEqual(getRequiredLocktime(d(`wsh(and_v(v:after(100),pk(${xpubs[0]})))`)), 100); + // no locktime at all + assert.strictEqual(getRequiredLocktime(d(`wsh(pk(${xpubs[0]}))`)), undefined); + }); + }); +}); diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/createPsbt.ts b/modules/abstract-utxo/test/core/descriptor/psbt/createPsbt.ts new file mode 100644 index 0000000000..9b67fc56a8 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/createPsbt.ts @@ -0,0 +1,64 @@ +import * as assert from 'assert'; + +import * as utxolib from '@bitgo/utxo-lib'; +import { BIP32Interface } from '@bitgo/utxo-lib'; + +import { DescriptorTemplate, getPsbtParams } from '../descriptor.utils'; +import { getFixture } from '../../fixtures.utils'; +import { finalizePsbt } from '../../../../src/core/descriptor'; +import { getKeyTriple } from '../../key.utils'; + +import { mockPsbtDefaultWithDescriptorTemplate } from './mock.utils'; +import { toPlainObjectFromPsbt, toPlainObjectFromTx } from './psbt.utils'; + +async function assertEqualsFixture(t: DescriptorTemplate, filename: string, value: unknown) { + filename = __dirname + '/fixtures/' + t + '.' + filename; + if (value instanceof Buffer) { + return assert.deepStrictEqual(value.toString('hex'), (await getFixture(filename, value)).toString('hex')); + } + + if (value instanceof utxolib.Psbt) { + return assert.deepStrictEqual( + toPlainObjectFromPsbt(value), + await getFixture(filename, toPlainObjectFromPsbt(value)) + ); + } + + if (value instanceof utxolib.Transaction) { + return assert.deepStrictEqual(toPlainObjectFromTx(value), await getFixture(filename, toPlainObjectFromTx(value))); + } + + throw new Error(`unknown value type: ${typeof value}`); +} + +function describeCreatePsbt(t: DescriptorTemplate) { + describe(`createPsbt ${t}`, function () { + const keysA = getKeyTriple('a'); + + function getSignedPsbt(psbt: utxolib.bitgo.UtxoPsbt, keys: BIP32Interface[]) { + const cloned = utxolib.bitgo.createPsbtFromBuffer(psbt.toBuffer(), psbt.network); + for (const key of keys) { + cloned.signAllInputsHD(key); + } + return cloned; + } + + it('creates psbt with expected properties', async function () { + const psbt = mockPsbtDefaultWithDescriptorTemplate(t, getPsbtParams(t)); + await assertEqualsFixture(t, 'createPsbt.json', psbt); + const psbtSignedA = getSignedPsbt(psbt, keysA.slice(0, 1)); + await assertEqualsFixture(t, 'createPsbtSignedA.json', psbtSignedA); + const psbtSignedAB = getSignedPsbt(psbt, keysA.slice(0, 2)); + await assertEqualsFixture(t, 'createPsbtSignedAB.json', psbtSignedAB); + const psbtFinal = psbtSignedAB.clone(); + finalizePsbt(psbtFinal); + await assertEqualsFixture(t, 'createPsbtFinal.json', psbtFinal); + const networkTx = psbtFinal.extractTransaction(); + await assertEqualsFixture(t, 'createPsbtFinalTx.json', networkTx); + await assertEqualsFixture(t, 'createPsbtFinalTx.hex', networkTx.toBuffer()); + }); + }); +} + +describeCreatePsbt('Wsh2Of3'); +describeCreatePsbt('ShWsh2Of3CltvDrop'); diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/findDescriptors.ts b/modules/abstract-utxo/test/core/descriptor/psbt/findDescriptors.ts new file mode 100644 index 0000000000..00df566634 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/findDescriptors.ts @@ -0,0 +1,38 @@ +import * as assert from 'assert'; + +import { getDefaultXPubs, getDescriptor } from '../descriptor.utils'; +import { findDescriptorForInput, findDescriptorForOutput } from '../../../../src/core/descriptor/psbt/findDescriptors'; + +import { mockPsbt } from './mock.utils'; + +describe('parsePsbt', function () { + const descriptorA = getDescriptor('Wsh2Of3', getDefaultXPubs('a')); + const descriptorB = getDescriptor('Wsh2Of3', getDefaultXPubs('b')); + const descriptorMap = new Map([ + ['a', descriptorA], + ['b', descriptorB], + ]); + + it('finds descriptors for PSBT inputs/outputs', function () { + const psbt = mockPsbt( + [ + { descriptor: descriptorA, index: 0 }, + { descriptor: descriptorB, index: 1, id: { vout: 1 } }, + ], + [{ descriptor: descriptorA, index: 2, value: BigInt(1e6) }] + ); + + assert.deepStrictEqual(findDescriptorForInput(psbt.data.inputs[0], descriptorMap), { + descriptor: descriptorA, + index: 0, + }); + assert.deepStrictEqual(findDescriptorForInput(psbt.data.inputs[1], descriptorMap), { + descriptor: descriptorB, + index: 1, + }); + assert.deepStrictEqual(findDescriptorForOutput(psbt.txOutputs[0].script, psbt.data.outputs[0], descriptorMap), { + descriptor: descriptorA, + index: 2, + }); + }); +}); diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbt.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbt.json new file mode 100644 index 0000000000..8be6c0a5f9 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbt.json @@ -0,0 +1,120 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087", + "value": "1000000" + }, + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "m/0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "m/0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "m/0/0" + } + ] + }, + { + "witnessUtxo": { + "script": "a914dffa7f206867afb1a540a953a9d1c599e98ca0aa87", + "value": "1000000" + }, + "redeemScript": "0020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8", + "witnessScript": "51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "m/0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "m/0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "m/0/1" + } + ] + } + ], + "outputs": [ + {}, + { + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "m/0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "m/0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "m/0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 1, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "script": "a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87", + "value": "400000" + }, + { + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087", + "value": "400000" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000 + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinal.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinal.json new file mode 100644 index 0000000000..eaff5e720e --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinal.json @@ -0,0 +1,87 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087", + "value": "1000000" + }, + "finalScriptSig": "2200203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "finalScriptWitness": "0400483045022100bb207aea6ab1d391da3a7d9edfcb3a4705dc2484de1304657500f14f5457c5fa022055ab8a85c1d970280f961c185fb013df669551e7605fa159cd7cf7c2a71f44eb014730440220075c57b8163a4f901d7635cbc1b104155c14e3bd413a6b89d8a5dbca4bb1765c022000e66d03f079bdb593d7e7f05d3abb0fce6308ca3fa8052221fd3f33ee9b45c6016c51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae" + }, + { + "witnessUtxo": { + "script": "a914dffa7f206867afb1a540a953a9d1c599e98ca0aa87", + "value": "1000000" + }, + "finalScriptSig": "220020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8", + "finalScriptWitness": "040047304402201729de4f4711211c8beae573dcff1db3ce63ef77de1983c17117bcf0652a64370220295381b677540db4f7f4caea1c105cabc11f160e09e35f6802f4149cc99d08ac01483045022100b9419f9fdffd9a45f522927a557681b439c885465b50d53595a2b5b9b980e8ec02202dbd3dcf9a83e89a2943c808ed3d6f4232f44c79ca8d79be968a8fd8d091384f016c51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae" + } + ], + "outputs": [ + {}, + { + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 1, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87" + }, + { + "value": "400000", + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.hex b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.hex new file mode 100644 index 0000000000..136a8ff976 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.hex @@ -0,0 +1 @@ +02000000000102010101010101010101010101010101010101010101010101010101010101010100000000232200203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753bfdffffff01010101010101010101010101010101010101010101010101010101010101010100000023220020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8fdffffff02801a06000000000017a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87801a06000000000017a914d43b97dbaddd2c6930c42f04ecfdf90a061b1520870400483045022100bb207aea6ab1d391da3a7d9edfcb3a4705dc2484de1304657500f14f5457c5fa022055ab8a85c1d970280f961c185fb013df669551e7605fa159cd7cf7c2a71f44eb014730440220075c57b8163a4f901d7635cbc1b104155c14e3bd413a6b89d8a5dbca4bb1765c022000e66d03f079bdb593d7e7f05d3abb0fce6308ca3fa8052221fd3f33ee9b45c6016c51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae040047304402201729de4f4711211c8beae573dcff1db3ce63ef77de1983c17117bcf0652a64370220295381b677540db4f7f4caea1c105cabc11f160e09e35f6802f4149cc99d08ac01483045022100b9419f9fdffd9a45f522927a557681b439c885465b50d53595a2b5b9b980e8ec02202dbd3dcf9a83e89a2943c808ed3d6f4232f44c79ca8d79be968a8fd8d091384f016c51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae01000000 \ No newline at end of file diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.json new file mode 100644 index 0000000000..0cdcaa2aae --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtFinalTx.json @@ -0,0 +1,52 @@ +{ + "version": 2, + "locktime": 1, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "2200203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "sequence": 4294967293, + "witness": [ + "", + "3045022100bb207aea6ab1d391da3a7d9edfcb3a4705dc2484de1304657500f14f5457c5fa022055ab8a85c1d970280f961c185fb013df669551e7605fa159cd7cf7c2a71f44eb01", + "30440220075c57b8163a4f901d7635cbc1b104155c14e3bd413a6b89d8a5dbca4bb1765c022000e66d03f079bdb593d7e7f05d3abb0fce6308ca3fa8052221fd3f33ee9b45c601", + "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae" + ] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "220020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8", + "sequence": 4294967293, + "witness": [ + "", + "304402201729de4f4711211c8beae573dcff1db3ce63ef77de1983c17117bcf0652a64370220295381b677540db4f7f4caea1c105cabc11f160e09e35f6802f4149cc99d08ac01", + "3045022100b9419f9fdffd9a45f522927a557681b439c885465b50d53595a2b5b9b980e8ec02202dbd3dcf9a83e89a2943c808ed3d6f4232f44c79ca8d79be968a8fd8d091384f01", + "51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae" + ] + } + ], + "outs": [ + { + "value": "400000", + "script": "a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87" + }, + { + "value": "400000", + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087" + } + ], + "network": { + "messagePrefix": "\u0018Bitcoin Signed Message:\n", + "bech32": "bc", + "bip32": { + "public": 76067358, + "private": 76066276 + }, + "pubKeyHash": 0, + "scriptHash": 5, + "wif": 128, + "coin": "btc" + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedA.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedA.json new file mode 100644 index 0000000000..1b1576e80b --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedA.json @@ -0,0 +1,133 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087", + "value": "1000000" + }, + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ], + "partialSig": [ + { + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "signature": "3045022100bb207aea6ab1d391da3a7d9edfcb3a4705dc2484de1304657500f14f5457c5fa022055ab8a85c1d970280f961c185fb013df669551e7605fa159cd7cf7c2a71f44eb01" + } + ] + }, + { + "witnessUtxo": { + "script": "a914dffa7f206867afb1a540a953a9d1c599e98ca0aa87", + "value": "1000000" + }, + "redeemScript": "0020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8", + "witnessScript": "51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "0/1" + } + ], + "partialSig": [ + { + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "signature": "304402201729de4f4711211c8beae573dcff1db3ce63ef77de1983c17117bcf0652a64370220295381b677540db4f7f4caea1c105cabc11f160e09e35f6802f4149cc99d08ac01" + } + ] + } + ], + "outputs": [ + {}, + { + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 1, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87" + }, + { + "value": "400000", + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedAB.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedAB.json new file mode 100644 index 0000000000..8f81b22301 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/ShWsh2Of3CltvDrop.createPsbtSignedAB.json @@ -0,0 +1,141 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087", + "value": "1000000" + }, + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ], + "partialSig": [ + { + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "signature": "3045022100bb207aea6ab1d391da3a7d9edfcb3a4705dc2484de1304657500f14f5457c5fa022055ab8a85c1d970280f961c185fb013df669551e7605fa159cd7cf7c2a71f44eb01" + }, + { + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "signature": "30440220075c57b8163a4f901d7635cbc1b104155c14e3bd413a6b89d8a5dbca4bb1765c022000e66d03f079bdb593d7e7f05d3abb0fce6308ca3fa8052221fd3f33ee9b45c601" + } + ] + }, + { + "witnessUtxo": { + "script": "a914dffa7f206867afb1a540a953a9d1c599e98ca0aa87", + "value": "1000000" + }, + "redeemScript": "0020a27f0d14b1d42b029ce8fc96b3c933d45a01ffb368d0f577f0b0bb0963f399a8", + "witnessScript": "51b1755221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "0/1" + } + ], + "partialSig": [ + { + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "signature": "304402201729de4f4711211c8beae573dcff1db3ce63ef77de1983c17117bcf0652a64370220295381b677540db4f7f4caea1c105cabc11f160e09e35f6802f4149cc99d08ac01" + }, + { + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "signature": "3045022100b9419f9fdffd9a45f522927a557681b439c885465b50d53595a2b5b9b980e8ec02202dbd3dcf9a83e89a2943c808ed3d6f4232f44c79ca8d79be968a8fd8d091384f01" + } + ] + } + ], + "outputs": [ + {}, + { + "redeemScript": "00203ab8b31b07c3b64804cff426e92a4b773dc28bc744bb00b36d1b0ad5af29753b", + "witnessScript": "51b17552210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 1, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "a91433e6c6f6c323cda4a56fda71ebb188005eadbe3a87" + }, + { + "value": "400000", + "script": "a914d43b97dbaddd2c6930c42f04ecfdf90a061b152087" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbt.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbt.json new file mode 100644 index 0000000000..67b188bac0 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbt.json @@ -0,0 +1,117 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104", + "value": "1000000" + }, + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "m/0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "m/0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "m/0/0" + } + ] + }, + { + "witnessUtxo": { + "script": "0020e822404ded4c14f401afdaf6aaed1cf319ef2380c3aaba8cc6e3d8895ff09d7d", + "value": "1000000" + }, + "witnessScript": "5221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "m/0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "m/0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "m/0/1" + } + ] + } + ], + "outputs": [ + {}, + { + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "m/0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "m/0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "m/0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 0, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "script": "002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8", + "value": "400000" + }, + { + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104", + "value": "400000" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000 + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinal.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinal.json new file mode 100644 index 0000000000..0fdd1cdae3 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinal.json @@ -0,0 +1,84 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104", + "value": "1000000" + }, + "finalScriptWitness": "040048304502210082bf9de4293adac47f9895bb781ef014521ec5627ca450f15f052035b8c795ca022062afb9278352b002676ab32c8923329db883d89d5ff55b88e922c70f190e13e601473044022028ed89385ff5215417ce4ed138ca77309ec17a52ece239402e0355d3b4ba4dba02202067b68ea5b5b80fc129a7bbf30a745fa24f6f308ea088a860e8a0eeb33d9906016952210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae" + }, + { + "witnessUtxo": { + "script": "0020e822404ded4c14f401afdaf6aaed1cf319ef2380c3aaba8cc6e3d8895ff09d7d", + "value": "1000000" + }, + "finalScriptWitness": "0400483045022100c6d05f74ef5f84ed1628de34e617ffd252897b88c192c893f40ae3cdf036b217022036fc634fc63506fb2b7b0b1dc41bd6e0c607f4edb1226c644fa49ceb493b690501483045022100e558379fdbf91b583fdeba831dd53d8c36863e1b96a768d2bd03a26d92e720f202205f0979b87df0951f3fb74999bb1b66b632429b94771ce67152070e420d23463701695221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae" + } + ], + "outputs": [ + {}, + { + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 0, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8" + }, + { + "value": "400000", + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.hex b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.hex new file mode 100644 index 0000000000..84f6c00836 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.hex @@ -0,0 +1 @@ +0200000000010201010101010101010101010101010101010101010101010101010101010101010000000000fdffffff01010101010101010101010101010101010101010101010101010101010101010100000000fdffffff02801a06000000000022002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8801a060000000000220020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104040048304502210082bf9de4293adac47f9895bb781ef014521ec5627ca450f15f052035b8c795ca022062afb9278352b002676ab32c8923329db883d89d5ff55b88e922c70f190e13e601473044022028ed89385ff5215417ce4ed138ca77309ec17a52ece239402e0355d3b4ba4dba02202067b68ea5b5b80fc129a7bbf30a745fa24f6f308ea088a860e8a0eeb33d9906016952210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae0400483045022100c6d05f74ef5f84ed1628de34e617ffd252897b88c192c893f40ae3cdf036b217022036fc634fc63506fb2b7b0b1dc41bd6e0c607f4edb1226c644fa49ceb493b690501483045022100e558379fdbf91b583fdeba831dd53d8c36863e1b96a768d2bd03a26d92e720f202205f0979b87df0951f3fb74999bb1b66b632429b94771ce67152070e420d23463701695221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae00000000 \ No newline at end of file diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.json new file mode 100644 index 0000000000..91d15cbe63 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtFinalTx.json @@ -0,0 +1,52 @@ +{ + "version": 2, + "locktime": 0, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [ + "", + "304502210082bf9de4293adac47f9895bb781ef014521ec5627ca450f15f052035b8c795ca022062afb9278352b002676ab32c8923329db883d89d5ff55b88e922c70f190e13e601", + "3044022028ed89385ff5215417ce4ed138ca77309ec17a52ece239402e0355d3b4ba4dba02202067b68ea5b5b80fc129a7bbf30a745fa24f6f308ea088a860e8a0eeb33d990601", + "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae" + ] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [ + "", + "3045022100c6d05f74ef5f84ed1628de34e617ffd252897b88c192c893f40ae3cdf036b217022036fc634fc63506fb2b7b0b1dc41bd6e0c607f4edb1226c644fa49ceb493b690501", + "3045022100e558379fdbf91b583fdeba831dd53d8c36863e1b96a768d2bd03a26d92e720f202205f0979b87df0951f3fb74999bb1b66b632429b94771ce67152070e420d23463701", + "5221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae" + ] + } + ], + "outs": [ + { + "value": "400000", + "script": "002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8" + }, + { + "value": "400000", + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104" + } + ], + "network": { + "messagePrefix": "\u0018Bitcoin Signed Message:\n", + "bech32": "bc", + "bip32": { + "public": 76067358, + "private": 76066276 + }, + "pubKeyHash": 0, + "scriptHash": 5, + "wif": 128, + "coin": "btc" + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedA.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedA.json new file mode 100644 index 0000000000..30887407b2 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedA.json @@ -0,0 +1,130 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104", + "value": "1000000" + }, + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ], + "partialSig": [ + { + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "signature": "304502210082bf9de4293adac47f9895bb781ef014521ec5627ca450f15f052035b8c795ca022062afb9278352b002676ab32c8923329db883d89d5ff55b88e922c70f190e13e601" + } + ] + }, + { + "witnessUtxo": { + "script": "0020e822404ded4c14f401afdaf6aaed1cf319ef2380c3aaba8cc6e3d8895ff09d7d", + "value": "1000000" + }, + "witnessScript": "5221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "0/1" + } + ], + "partialSig": [ + { + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "signature": "3045022100c6d05f74ef5f84ed1628de34e617ffd252897b88c192c893f40ae3cdf036b217022036fc634fc63506fb2b7b0b1dc41bd6e0c607f4edb1226c644fa49ceb493b690501" + } + ] + } + ], + "outputs": [ + {}, + { + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 0, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8" + }, + { + "value": "400000", + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedAB.json b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedAB.json new file mode 100644 index 0000000000..e557b298dc --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/fixtures/Wsh2Of3.createPsbtSignedAB.json @@ -0,0 +1,138 @@ +{ + "data": { + "inputs": [ + { + "witnessUtxo": { + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104", + "value": "1000000" + }, + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ], + "partialSig": [ + { + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "signature": "304502210082bf9de4293adac47f9895bb781ef014521ec5627ca450f15f052035b8c795ca022062afb9278352b002676ab32c8923329db883d89d5ff55b88e922c70f190e13e601" + }, + { + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "signature": "3044022028ed89385ff5215417ce4ed138ca77309ec17a52ece239402e0355d3b4ba4dba02202067b68ea5b5b80fc129a7bbf30a745fa24f6f308ea088a860e8a0eeb33d990601" + } + ] + }, + { + "witnessUtxo": { + "script": "0020e822404ded4c14f401afdaf6aaed1cf319ef2380c3aaba8cc6e3d8895ff09d7d", + "value": "1000000" + }, + "witnessScript": "5221024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e742103266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f2432102d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "path": "0/1" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "02d502c2c474f4874a96b544afa9f8a9aa49e03ca8fcf736b1a1d150a663db13a6", + "path": "0/1" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "path": "0/1" + } + ], + "partialSig": [ + { + "pubkey": "024062b7c830cca69d5c808306526e7a980fbdb5356a8a5c9fd6549f2d1bf76e74", + "signature": "3045022100c6d05f74ef5f84ed1628de34e617ffd252897b88c192c893f40ae3cdf036b217022036fc634fc63506fb2b7b0b1dc41bd6e0c607f4edb1226c644fa49ceb493b690501" + }, + { + "pubkey": "03266670018fb65f1384da4e9b9b92c79d5c42d5f51c3ceb1e516babfb7595f243", + "signature": "3045022100e558379fdbf91b583fdeba831dd53d8c36863e1b96a768d2bd03a26d92e720f202205f0979b87df0951f3fb74999bb1b66b632429b94771ce67152070e420d23463701" + } + ] + } + ], + "outputs": [ + {}, + { + "witnessScript": "52210285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c392102d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e21029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de40653ae", + "bip32Derivation": [ + { + "masterFingerprint": "86ea6d35", + "pubkey": "0285f9fc63871a4f7e7877923a162c3f14fbaed12fab925e341173cff43eb93c39", + "path": "0/0" + }, + { + "masterFingerprint": "96daa738", + "pubkey": "029a50a261452c3233d75f3aaa79d7187b823050b329fa1c51d7d5e1067b9de406", + "path": "0/0" + }, + { + "masterFingerprint": "702fc808", + "pubkey": "02d5298e6df3cc36ef883e10698e31c4d39108bdad04066111b5bb70241373cd3e", + "path": "0/0" + } + ] + } + ], + "globalMap": { + "unsignedTx": { + "tx": { + "version": 2, + "locktime": 0, + "ins": [ + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 0, + "script": "", + "sequence": 4294967293, + "witness": [] + }, + { + "hash": "0101010101010101010101010101010101010101010101010101010101010101", + "index": 1, + "script": "", + "sequence": 4294967293, + "witness": [] + } + ], + "outs": [ + { + "value": "400000", + "script": "002064d5e2fcca2e107ac79ef06388fa61e93ceaf31533a8d2a8c6e95423a1b1fdb8" + }, + { + "value": "400000", + "script": "0020513bcd5692ffad4f7bfbecc132f5fcfe11806dee18f66795a10b636c391c6104" + } + ] + } + } + } + }, + "nonceStore": { + "nonces": [] + }, + "opts": { + "maximumFeeRate": 5000, + "bip32PathsAbsolute": false + } +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/mock.utils.ts b/modules/abstract-utxo/test/core/descriptor/psbt/mock.utils.ts new file mode 100644 index 0000000000..27a8717d43 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/mock.utils.ts @@ -0,0 +1,104 @@ +import { Descriptor } from '@bitgo/wasm-miniscript'; +import * as utxolib from '@bitgo/utxo-lib'; + +import { createScriptPubKeyFromDescriptor, createPsbt, PsbtParams } from '../../../../src/core/descriptor'; +import { DerivedDescriptorWalletOutput } from '../../../../src/core/descriptor/Output'; +import { DescriptorTemplate, getDefaultXPubs, getDescriptor } from '../descriptor.utils'; + +type MockOutputIdParams = { hash?: string; vout?: number }; + +type BaseMockDescriptorOutputParams = { + id?: MockOutputIdParams; + index?: number; + value?: bigint; +}; + +function mockOutputId(id?: MockOutputIdParams): { + hash: string; + vout: number; +} { + const hash = id?.hash ?? Buffer.alloc(32, 1).toString('hex'); + const vout = id?.vout ?? 0; + return { hash, vout }; +} + +export function mockDerivedDescriptorWalletOutput( + descriptor: Descriptor, + outputParams: BaseMockDescriptorOutputParams = {} +): DerivedDescriptorWalletOutput { + const { value = BigInt(1e6) } = outputParams; + const { hash, vout } = mockOutputId(outputParams.id); + return { + hash, + index: vout, + witnessUtxo: { + script: createScriptPubKeyFromDescriptor(descriptor), + value, + }, + descriptor, + }; +} + +type MockInput = BaseMockDescriptorOutputParams & { + index: number; + descriptor: Descriptor; +}; + +type MockOutput = { + descriptor: Descriptor; + index: number; + value: bigint; + external?: boolean; +}; + +export function mockPsbt( + inputs: MockInput[], + outputs: MockOutput[], + params: Partial = {} +): utxolib.bitgo.UtxoPsbt { + return createPsbt( + { ...params, network: params.network ?? utxolib.networks.bitcoin }, + inputs.map((i) => mockDerivedDescriptorWalletOutput(i.descriptor.atDerivationIndex(i.index), i)), + outputs.map((o) => { + const derivedDescriptor = o.descriptor.atDerivationIndex(o.index); + return { + script: createScriptPubKeyFromDescriptor(derivedDescriptor), + value: o.value, + descriptor: o.external ? undefined : derivedDescriptor, + }; + }) + ); +} + +export function mockPsbtDefault({ + descriptorSelf = getDescriptor('Wsh2Of3', getDefaultXPubs('a')), + descriptorOther = getDescriptor('Wsh2Of3', getDefaultXPubs('b')), + params = {}, +}: { + descriptorSelf?: Descriptor; + descriptorOther?: Descriptor; + params?: Partial; +} = {}): utxolib.bitgo.UtxoPsbt { + return mockPsbt( + [ + { descriptor: descriptorSelf, index: 0 }, + { descriptor: descriptorSelf, index: 1, id: { vout: 1 } }, + ], + [ + { descriptor: descriptorOther, index: 0, value: BigInt(4e5), external: true }, + { descriptor: descriptorSelf, index: 0, value: BigInt(4e5) }, + ], + params + ); +} + +export function mockPsbtDefaultWithDescriptorTemplate( + t: DescriptorTemplate, + params: Partial = {} +): utxolib.bitgo.UtxoPsbt { + return mockPsbtDefault({ + descriptorSelf: getDescriptor(t, getDefaultXPubs('a')), + descriptorOther: getDescriptor(t, getDefaultXPubs('b')), + params, + }); +} diff --git a/modules/abstract-utxo/test/core/descriptor/psbt/psbt.utils.ts b/modules/abstract-utxo/test/core/descriptor/psbt/psbt.utils.ts new file mode 100644 index 0000000000..af16fca1b5 --- /dev/null +++ b/modules/abstract-utxo/test/core/descriptor/psbt/psbt.utils.ts @@ -0,0 +1,25 @@ +import * as utxolib from '@bitgo/utxo-lib'; + +import { matchPath, PathElement, toPlainObject } from '../../toPlainObject.utils'; + +export function toPlainObjectFromPsbt(v: utxolib.Psbt): unknown { + return toPlainObject( + v, + { + propertyDescriptors: true, + ignorePaths(path: PathElement[]) { + return ( + matchPath(path, ['__CACHE']) || + matchPath(path, ['opts', 'network']) || + matchPath(path, ['data', 'globalMap', 'unsignedTx', 'tx', 'network']) || + matchPath(path, ['network']) + ); + }, + }, + [] + ); +} + +export function toPlainObjectFromTx(v: utxolib.Transaction): unknown { + return toPlainObject(v, {}, []); +} diff --git a/modules/abstract-utxo/test/core/fixtures.utils.ts b/modules/abstract-utxo/test/core/fixtures.utils.ts new file mode 100644 index 0000000000..6af6f55a68 --- /dev/null +++ b/modules/abstract-utxo/test/core/fixtures.utils.ts @@ -0,0 +1,85 @@ +/** + * Contains helpers for working with test fixtures + */ + +import * as fs from 'fs'; +import * as mpath from 'path'; + +type FixtureEncoding = 'json' | 'hex'; + +function fixtureEncoding(path: string): FixtureEncoding { + if (path.endsWith('.json')) { + return 'json'; + } + if (path.endsWith('.hex')) { + return 'hex'; + } + throw new Error(`unknown fixture encoding for ${path}`); +} + +function decodeFixture(raw: string, encoding: FixtureEncoding): unknown { + switch (encoding) { + case 'json': + return JSON.parse(raw); + case 'hex': + return Buffer.from(raw, 'hex'); + } +} + +function encodeFixture(value: unknown, encoding: FixtureEncoding): string { + switch (encoding) { + case 'json': + return JSON.stringify(value, null, 2) + '\n'; + case 'hex': + if (!Buffer.isBuffer(value)) { + throw new Error(`expected Buffer, got ${typeof value}`); + } + return value.toString('hex'); + } +} + +/** + * Return fixture described in `path`. + * + * If file does not exist and `defaultValue` is provided, writes defaultValue to `path` and throws an error. + * + * @param path + * @param defaultValue + * @return T - fixture content + */ +export async function getFixture(path: string, defaultValue?: T | (() => Promise)): Promise { + try { + await fs.promises.stat(mpath.dirname(path)); + } catch (e) { + if (e.code === 'ENOENT') { + throw new Error(`fixture directory ${mpath.dirname(path)} not found, please create it first`); + } + throw e; + } + + const encoding = fixtureEncoding(path); + + try { + return decodeFixture(await fs.promises.readFile(path, 'utf8'), encoding) as T; + } catch (e) { + if (e.code === 'ENOENT') { + if (process.env.WRITE_FIXTURES === '0') { + throw new Error(`fixture ${path} not found, WRITE_FIXTURES=0`); + } + if (defaultValue === undefined) { + throw new Error(`fixture ${path} not found and no default value given`); + } + if (typeof defaultValue === 'function') { + defaultValue = await (defaultValue as () => Promise)(); + } + await fs.promises.writeFile(path, encodeFixture(defaultValue, encoding)); + throw new Error(`wrote default value for ${path}, please inspect and restart test`); + } + + throw e; + } +} + +export function jsonNormalize(v: T): T { + return JSON.parse(JSON.stringify(v)) as T; +} diff --git a/modules/abstract-utxo/test/core/key.utils.ts b/modules/abstract-utxo/test/core/key.utils.ts new file mode 100644 index 0000000000..7628eabf30 --- /dev/null +++ b/modules/abstract-utxo/test/core/key.utils.ts @@ -0,0 +1,28 @@ +import { Triple } from '@bitgo/sdk-core'; +import * as crypto from 'crypto'; + +import * as utxolib from '@bitgo/utxo-lib'; +import { BIP32Interface } from '@bitgo/utxo-lib'; + +export type KeyTriple = Triple; + +/** + * Create new bip32 key. Uses random seed if none is passed. + * @param seed + */ +export function getKey(seed?: string): BIP32Interface { + const finalSeed = seed === undefined ? crypto.randomBytes(32) : crypto.createHash('sha256').update(seed).digest(); + return utxolib.bip32.fromSeed(finalSeed); +} + +/** + * Return deterministic key triple of bip32 keys + * @param prefix + */ +export function getKeyTriple(prefix = ''): KeyTriple { + return Array.from({ length: 3 }).map((_, i) => getKey(`${prefix}${i}`)) as KeyTriple; +} + +export function getRootWalletKeys(prefix = '', derivationPrefixes?: Triple): utxolib.bitgo.RootWalletKeys { + return new utxolib.bitgo.RootWalletKeys(getKeyTriple(prefix), derivationPrefixes); +} diff --git a/modules/abstract-utxo/test/core/toPlainObject.utils.ts b/modules/abstract-utxo/test/core/toPlainObject.utils.ts new file mode 100644 index 0000000000..ab278fbb0a --- /dev/null +++ b/modules/abstract-utxo/test/core/toPlainObject.utils.ts @@ -0,0 +1,86 @@ +type ToPlainObjectOpts = { + propertyDescriptors?: boolean; + skipUndefinedValues?: boolean; + ignorePaths?: string[] | ((path: PathElement[]) => boolean); +}; +export type PathElement = string | number; + +export function matchPath(a: PathElement[], b: PathElement[]): boolean { + return a.length === b.length && a.every((e, i) => e === b[i]); +} + +function includePath(opts: ToPlainObjectOpts, path: PathElement[]): boolean { + if (!opts.ignorePaths) { + return true; + } + if (typeof opts.ignorePaths === 'function') { + return !opts.ignorePaths(path); + } + return !opts.ignorePaths.some((ignorePath) => matchPath(path, ignorePath.split('.'))); +} + +function toPlainEntries( + key: string, + value: string, + opts: ToPlainObjectOpts, + path: PathElement[] +): [] | [[string, unknown]] { + if (!includePath(opts, [...path, key])) { + return []; + } + if (value === undefined && (opts.skipUndefinedValues ?? true)) { + return []; + } + return [[key, toPlainObject(value, opts, [...path, key])]]; +} + +function toPlainObjectFromPropertyDescriptors(v: unknown, opts: ToPlainObjectOpts, path: PathElement[]) { + const descriptors = Object.getOwnPropertyDescriptors(v); + return Object.fromEntries( + Object.entries(descriptors).flatMap(([key, descriptor]) => { + if (descriptor.value !== undefined) { + return toPlainEntries(key, descriptor.value, opts, path); + } + if (typeof descriptor.get === 'function') { + return toPlainEntries(key, descriptor.get.call(v), opts, path); + } + return []; + }) + ); +} + +export function toPlainObject(v: unknown, opts: ToPlainObjectOpts, path: PathElement[]): unknown { + switch (typeof v) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + return v; + case 'bigint': + return v.toString(); + case 'function': + case 'symbol': + return undefined; + } + + if (v === null) { + return v; + } + + if (Buffer.isBuffer(v)) { + return v.toString('hex'); + } + if (Array.isArray(v)) { + return v.map((e, i) => toPlainObject(e, opts, [...path, i])); + } + if (typeof v === 'object') { + const result = Object.fromEntries( + Object.entries(v).flatMap(([key, value]) => toPlainEntries(key, value, opts, path)) + ); + if (opts.propertyDescriptors) { + Object.assign(result, toPlainObjectFromPropertyDescriptors(v, opts, path)); + } + return result; + } + throw new Error(`unknown v ${typeof v}`); +}