diff --git a/modules/utxo-lib/src/bitgo/Descriptor/Descriptor.ts b/modules/utxo-lib/src/bitgo/Descriptor/Descriptor.ts index d31e3e26af..03b218b028 100644 --- a/modules/utxo-lib/src/bitgo/Descriptor/Descriptor.ts +++ b/modules/utxo-lib/src/bitgo/Descriptor/Descriptor.ts @@ -3,7 +3,7 @@ import * as desc from '@saravanan7mani/descriptors'; import * as ms from '@bitcoinerlab/miniscript'; import { ecc } from '../../noble_ecc'; import { getMainnet, Network, networks } from '../../networks'; -import { filterByMasterFingerprint, getBip32PathIndexValue } from '../PsbtUtil'; +import { filterByMasterFingerprint, getIndexValueOfBip32Path } from '../PsbtUtil'; import { PsbtInput } from 'bip174/src/lib/interfaces'; import { Psbt } from 'bitcoinjs-lib/src/psbt'; @@ -17,7 +17,7 @@ export function assertDescriptorSupport(network: Network): void { assert(isDescriptorSupported(network), 'Descriptors are supported only for the Bitcoin'); } -export function endsWithRangeIndex(path: string): boolean { +export function endsWithWildcard(path: string): boolean { return /\*([hH'])?$/.test(path); } @@ -25,6 +25,15 @@ export function isHardenedPath(path: string): boolean { return /['Hh]/g.test(path); } +export function assertDescriptorPlaceholders(placeholders: string[], prefix: '@' | '#' | '$'): void { + const re = { '@': /^@(\d|[1-9]\d+)$/, '#': /^#(\d|[1-9]\d+)$/, $: /^\$(\d|[1-9]\d+)$/ }; + + const distinctPlaceholders = [...new Set(placeholders)]; + distinctPlaceholders.forEach((ph) => + assert(re[prefix].test(ph), `${ph} does not match the expanded placeholder format`) + ); +} + export function expandNonKeyLocks(descriptorOrMiniscript: string): string { const expandConfigs: { pattern: RegExp; prefix: '#' | '$' }[] = [ { pattern: /(?:older|after)\((\d+)\)/g, prefix: '#' }, @@ -37,36 +46,46 @@ export function expandNonKeyLocks(descriptorOrMiniscript: string): string { }, descriptorOrMiniscript); } -export function assertDescriptorKey( - keyInfo: desc.KeyInfo, - params: { allowPrivateKeys?: boolean } = { allowPrivateKeys: false } -): void { +export function assertDescriptorKey({ + key, + allowPrivateKeys, + allowXpubHardenedKeyPath, +}: { + key: desc.KeyInfo; + allowPrivateKeys?: boolean; + allowXpubHardenedKeyPath?: boolean; +}): void { const hasPrivateKey = - !!(keyInfo.bip32 && !keyInfo.bip32.isNeutered()) || - !!(keyInfo.ecpair && Buffer.isBuffer(keyInfo.ecpair.privateKey)); + !!(key.bip32 && !key.bip32.isNeutered()) || !!(key.ecpair && Buffer.isBuffer(key.ecpair.privateKey)); - const { allowPrivateKeys } = params; assert(allowPrivateKeys || !hasPrivateKey, 'Descriptor with a private key is not supported'); - const hasHardenedPath = keyInfo.keyPath && isHardenedPath(keyInfo.keyPath); - assert(allowPrivateKeys || !hasHardenedPath, 'Descriptor with a hardened path is not supported'); + const hasHardenedKeyPath = key.keyPath && isHardenedPath(key.keyPath); + assert( + allowXpubHardenedKeyPath || hasPrivateKey || !hasHardenedKeyPath, + 'Descriptor with a hardened key path for extended public key is not supported' + ); - const rangeIndexCount = (keyInfo.keyPath?.match(/\*/g) || []).length; + const wildcardCount = (key.keyPath?.match(/\*/g) || []).length; - assert(rangeIndexCount <= 1, 'Descriptor key path should have at most 1 ranged index'); + assert(wildcardCount <= 1, 'Descriptor key path should have at most 1 wildcard index'); assert( - rangeIndexCount === 0 || (keyInfo.keyPath && endsWithRangeIndex(keyInfo.keyPath)), - 'If ranged index is used in the descriptor key path, it should be the last index' + wildcardCount === 0 || (key.keyPath && endsWithWildcard(key.keyPath)), + 'If wildcard index is used in the descriptor key path, it should be the last index' ); } -export function assertDescriptorKeys( - keys: desc.KeyInfo[], - params: { allowPrivateKeys?: boolean } = { allowPrivateKeys: false } -): void { - const { allowPrivateKeys } = params; - keys.forEach((key) => assertDescriptorKey(key, { allowPrivateKeys })); +export function assertDescriptorKeys({ + keys, + allowPrivateKeys, + allowXpubHardenedKeyPath, +}: { + keys: desc.KeyInfo[]; + allowPrivateKeys?: boolean; + allowXpubHardenedKeyPath?: boolean; +}): void { + keys.forEach((key) => assertDescriptorKey({ key, allowPrivateKeys, allowXpubHardenedKeyPath })); } export function assertMiniscript(expandedMiniscript: string): void { @@ -74,25 +93,26 @@ export function assertMiniscript(expandedMiniscript: string): void { assert(issane, 'Invalid miniscript'); } -export function assertDescriptor( - descriptor: string, - network: Network, - params: { - allowPrivateKeys?: boolean; - allowWithNoKey?: boolean; - allowMiniscriptInP2SH?: boolean; - allowNonMiniscript?: boolean; - checksumRequired?: boolean; - } = { - allowPrivateKeys: false, - allowWithNoKey: false, - allowMiniscriptInP2SH: false, - allowNonMiniscript: false, - checksumRequired: true, - } -): void { +export function assertDescriptor({ + descriptor, + network, + allowPrivateKeys, + allowXpubHardenedKeyPath, + allowWithNoKey, + allowNonMiniscript, + allowMiniscriptInP2SH, + checksumRequired = true, +}: { + descriptor: string; + network: Network; + allowPrivateKeys?: boolean; + allowXpubHardenedKeyPath?: boolean; + allowWithNoKey?: boolean; + allowMiniscriptInP2SH?: boolean; + allowNonMiniscript?: boolean; + checksumRequired?: boolean; +}): void { assertDescriptorSupport(network); - const { allowPrivateKeys, allowWithNoKey, allowNonMiniscript, allowMiniscriptInP2SH, checksumRequired } = params; const { expandedMiniscript, expansionMap } = expand({ descriptor, network, @@ -100,7 +120,7 @@ export function assertDescriptor( allowMiniscriptInP2SH, }); if (expansionMap) { - assertDescriptorKeys(Object.values(expansionMap), { allowPrivateKeys }); + assertDescriptorKeys({ keys: Object.values(expansionMap), allowPrivateKeys, allowXpubHardenedKeyPath }); } else { assert(allowWithNoKey, 'Descriptor without keys is not supported'); } @@ -115,75 +135,147 @@ function sanitizeHardenedMarker(path: string): string { return path.replace(/[Hh]/g, "'"); } -function findMatchForRangePath(paths: string[], rangePath: string): string | undefined { - const sanitizedRangePath = sanitizeHardenedMarker(rangePath).slice(2); +function findWildcardPathMatch(paths: string[], wildcardPath: string): string | undefined { + const sanitizedWildcardPath = sanitizeHardenedMarker(wildcardPath).slice(2); return paths.find((path) => { - const index = getBip32PathIndexValue(path); - const rangeReplacedPath = sanitizedRangePath.replace(/[*]/g, index.toString()); - const sanitizedPath = path.replace(/^m\//, ''); - return rangeReplacedPath === sanitizedPath; + const index = getIndexValueOfBip32Path(path); + const sanitizedPath = sanitizedWildcardPath.replace(/[*]/g, index.toString()); + const pathToCompare = path.replace(/^m\//, ''); + return sanitizedPath === pathToCompare; }); } -function findRangeIndexValue(keyInfo: desc.KeyInfo, input: PsbtInput): number | undefined { - if (!keyInfo?.bip32 || !keyInfo?.path || !input.bip32Derivation?.length) { - return undefined; - } +function findValueOfWildcard(keyInfo: desc.KeyInfo, input: PsbtInput): number | undefined { + assert( + keyInfo?.bip32 && keyInfo?.path && input.bip32Derivation?.length, + 'Missing required data to find wildcard value' + ); const derivations = filterByMasterFingerprint( input.bip32Derivation, keyInfo.masterFingerprint || keyInfo.bip32.fingerprint ); const paths = derivations.map((v) => v.path); - const matchForRangePath = findMatchForRangePath(paths, keyInfo.path); - return matchForRangePath ? getBip32PathIndexValue(matchForRangePath) : undefined; + const path = findWildcardPathMatch(paths, keyInfo.path); + return path ? getIndexValueOfBip32Path(path) : undefined; +} + +function findKeyWithBip32WildcardPath(keys: desc.KeyInfo[]): desc.KeyInfo | undefined { + return keys.find((key) => key.keyPath && endsWithWildcard(key.keyPath)); +} + +function getValueForDescriptorWildcardIndex(input: PsbtInput, expansion: desc.Expansion): number | undefined { + if (!expansion.isRanged) { + return undefined; + } + assert(expansion.expansionMap, 'Missing expansionMap'); + const keys = Object.values(expansion.expansionMap); + assertDescriptorKeys({ keys, allowPrivateKeys: true, allowXpubHardenedKeyPath: true }); + const key = findKeyWithBip32WildcardPath(keys); + assert(key, 'Missing key with wildcard path'); + const index = findValueOfWildcard(key, input); + assert(index !== undefined, 'Missing index value'); + return index; } -function findKeyWithRangeIndex(keys: desc.KeyInfo[]): desc.KeyInfo | undefined { - return keys.find((key) => key.keyPath && endsWithRangeIndex(key.keyPath)); +export function createDescriptorOutput({ + descriptor, + index, + network, + checksumRequired = true, + allowMiniscriptInP2SH, +}: { + descriptor: string; + network: Network; + index?: number; + checksumRequired?: boolean; + allowMiniscriptInP2SH?: boolean; +}): desc.OutputInstance { + return new Output({ descriptor, index, network, allowMiniscriptInP2SH, checksumRequired }); +} + +export function expandDescriptor({ + descriptor, + network, + index, + checksumRequired, + allowMiniscriptInP2SH, +}: { + descriptor: string; + network: Network; + index?: number; + checksumRequired?: boolean; + allowMiniscriptInP2SH?: boolean; +}): desc.Expansion { + return expand({ + descriptor, + network, + index, + checksumRequired, + allowMiniscriptInP2SH, + }); } -export function assertPsbt(psbt: Psbt, descriptor: string, network: Network): void { +export function assertPsbt({ + psbt, + descriptor, + network, + scriptCheck = true, + sigCheck, + sigCheckKeyIds, +}: { + psbt: Psbt; + descriptor: string; + network: Network; + scriptCheck?: boolean; + sigCheck?: boolean; + sigCheckKeyIds?: string[]; +}): void { assertDescriptorSupport(network); - const { expansionMap, redeemScript, witnessScript, isRanged } = expand({ + + const expansion = expandDescriptor({ descriptor, network, allowMiniscriptInP2SH: true, }); - function createDescriptorOutput(index: number): desc.OutputInstance { - return new Output({ descriptor, index, allowMiniscriptInP2SH: true, network }); + function assertScripts(input: PsbtInput, descriptorOutput: desc.OutputInstance) { + const redeemScript = descriptorOutput.getRedeemScript(); + const witnessScript = descriptorOutput.getWitnessScript(); + assert(!redeemScript || !!input.redeemScript?.equals(redeemScript)); + assert(!witnessScript || !!input.witnessScript?.equals(witnessScript)); } - function getRangeIndexValue(input: PsbtInput) { - if (!isRanged) { - return undefined; - } - assert(expansionMap); - const keys = Object.values(expansionMap); - assertDescriptorKeys(keys, { allowPrivateKeys: true }); - const key = findKeyWithRangeIndex(keys); - assert(key); - const index = findRangeIndexValue(key, input); - assert(index !== undefined); - return index; + function assertSignature(inputIndex: number, key: desc.KeyInfo, keyId: string) { + assert(key.pubkey, `Missing pubkey for key ${keyId}`); + const isValid = psbt.validateSignaturesOfInput(inputIndex, (p, m, s) => ecc.verify(m, p, s, true), key.pubkey); + assert(isValid, `Key ${keyId} has no valid signature`); } - function getScripts(input: PsbtInput, descOutput: desc.OutputInstance | undefined) { - return descOutput - ? { redeemScript: descOutput.getRedeemScript(), witnessScript: descOutput.getWitnessScript() } - : { redeemScript, witnessScript }; + function assertSignatures(inputIndex: number, descriptorOutput: desc.OutputInstance) { + const expansionMap = descriptorOutput.expand().expansionMap; + assert(expansionMap, 'Missing expansionMap for signature check'); + assert(!!sigCheckKeyIds?.length, 'sigCheckKeyIds is required for signature check'); + assertDescriptorPlaceholders(sigCheckKeyIds, '@'); + sigCheckKeyIds.forEach((keyId) => { + const key = expansionMap[keyId]; + assert(key, `Key id ${keyId} has no match in the descriptor`); + assertSignature(inputIndex, key, keyId); + }); } - function assertScripts(input: PsbtInput, descOutput: desc.OutputInstance | undefined) { - assert(Buffer.isBuffer(input.redeemScript) || Buffer.isBuffer(input.witnessScript)); - const { redeemScript, witnessScript } = getScripts(input, descOutput); - assert(!Buffer.isBuffer(input.witnessScript) || witnessScript?.equals(input.witnessScript)); - assert(!Buffer.isBuffer(input.redeemScript) || redeemScript?.equals(input.redeemScript)); + function assertInput(inputIndex: number) { + const input = psbt.data.inputs[inputIndex]; + const index = getValueForDescriptorWildcardIndex(input, expansion); + const descriptorOutput = createDescriptorOutput({ descriptor, index, network, checksumRequired: false }); + if (scriptCheck) { + assertScripts(input, descriptorOutput); + } + if (sigCheck) { + assertSignatures(inputIndex, descriptorOutput); + } } - psbt.data.inputs.forEach((input) => { - const index = getRangeIndexValue(input); - const descOutput = index === undefined ? undefined : createDescriptorOutput(index); - assertScripts(input, descOutput); + psbt.data.inputs.forEach((_, inputIndex) => { + assertInput(inputIndex); }); } diff --git a/modules/utxo-lib/src/bitgo/PsbtUtil.ts b/modules/utxo-lib/src/bitgo/PsbtUtil.ts index f99238a40d..de452bf525 100644 --- a/modules/utxo-lib/src/bitgo/PsbtUtil.ts +++ b/modules/utxo-lib/src/bitgo/PsbtUtil.ts @@ -125,15 +125,15 @@ export function isValidBip32DerivationPath(path: string): boolean { return /^(m\/)?(\d+'?\/)*\d+'?$/.test(path); } -export function getBip32PathIndexValueStr(path: string): string { +export function getIndexValueStrOfBip32Path(path: string): string { assert(isValidBip32DerivationPath(path), `Invalid BIP32 derivation path: ${path}`); const match = path.match(/(?:^|\/)([^\/]+)$/); assert(match); return match[1]; } -export function getBip32PathIndexValue(path: string): number { - const indexStr = getBip32PathIndexValueStr(path); +export function getIndexValueOfBip32Path(path: string): number { + const indexStr = getIndexValueStrOfBip32Path(path); const isHardenedIndex = indexStr.endsWith("'"); return isHardenedIndex ? parseInt(indexStr.slice(0, -1), 10) : parseInt(indexStr, 10); } diff --git a/modules/utxo-lib/test/bitgo/descriptor/descriptor.ts b/modules/utxo-lib/test/bitgo/descriptor/descriptor.ts index 0909099237..a9c1ced088 100644 --- a/modules/utxo-lib/test/bitgo/descriptor/descriptor.ts +++ b/modules/utxo-lib/test/bitgo/descriptor/descriptor.ts @@ -1,23 +1,111 @@ import * as assert from 'assert'; import { constructPsbt, getDefaultWalletKeys, Input as InputType, Output as OutputType } from '../../../src/testutil'; -import { ecc, networks } from '../../../src'; +import { ecc, bip32, networks } from '../../../src'; import * as desc from '@saravanan7mani/descriptors'; import { assertDescriptor, assertPsbt, expandNonKeyLocks } from '../../../src/bitgo/Descriptor/Descriptor'; import { Psbt } from 'bitcoinjs-lib/src/psbt'; const { expand, Output } = desc.DescriptorsFactory(ecc); const rootWalletKeys = getDefaultWalletKeys(); -const network = networks.testnet; -const descriptor = - "wsh(andor(pk([73d83c6f/48'/1'/0'/2']tpubDEyGjD5PkjZf6rQiKWKqRi8wD8yvA43aurE8mwjE5yxjX3AJWyu7xnm94BcoxxirQKAd9AExoz7oqLiHAQL2tVC78r452bLedHyvE1GRzW8/*),older(10000),thresh(3,pk([0d78119c/48'/1'/0'/2']tpubDExqoVD2WKmN5YFBNQQZV9SU3ajPr4CLa1JBmLkNyCUv5nPDJxxAn6oqXKTUtaQxtRXrhPaGELi8hP1a5Rpjs7bNdqnfKMvVxebTtpmFtd6/*),s:pk([8f8c7811/48'/1'/0'/2']tpubDErfvxUCfe4Yd7vURSL69zSA2VtweQHvgmZAzQHNXwoY4UTcCq5F9qWfgGXtfDS3e6HZySbT8XXjmbGWkcyngqSsEBv4mYAazmETx3QfP9f/*),s:pk([a592f770/48'/1'/0'/2']tpubDE6Xb7UALq4fnjL39g6DUaf3z8MN8SeTzBMDFEhzyjYDVyn4oqY8A4K3qCUAnTCbsv8qheZK3EPNsrkE7sDnAzNcj2bXq5Rt22UESJ5wSCx/*),s:pk([4ef7afff/48'/1'/0'/2']tpubDFf5zVvuuzU285RfaHEsnsizhcgfW5Wyk6vQezW3BG1RBJXXAWdHSRzzesGfH9kQjeXgWNvFJJjZHxHPMk8xeqBj89WuuiwrfJKiVCKNMyH/*),s:pk([0d8a8cb4/48'/1'/0'/2']tpubDFQBGM1DQZyD6bWBgtgU8UQtzJvVxEmtuVfSg3cYf1R2tU6GnU4ZVUoUQ37SHGrhJyGtrZ5XG5y1Bz9ydn3yUYUkQRkvnQqq4KCcr7oaYLq/*),sln:after(840000))))"; +const network = networks.bitcoin; + +/** + * Compressed public key: + * 3eda9319 + * xprv9s21ZrQH143K3FD3eBfcR6mo5tvB2fBseMXdif66RShc6fMuAhPFpraTc3Qwirq44wk6vPvGfnDSm5Yz4cXCu9XWbad47qN2bsQ9ne6k8gC + * xpub661MyMwAqRbcFjHWkDCcnEiXdvkfS7uj1aTEX3VhynEayTh3iEhWNetwTM2U4W9Z67nBi1JxmZ1rVsQBTAUFy3vDccxMapX7XJGNyQtHyzK + * 03fda0d184f78dcdb33123129818495b39e1f5ebca259552e9283b7a5d3692dd6d + * + * Uncompressed public key: + * 64e38a2a + * xprv9s21ZrQH143K4bXszGVBuPpuvxAqSvxrCKZXc9jZsCVs2rDjhjKFrTiXBvFtHEj6T511TVuGRPdFFBAEayPMtRQgMCuDAPV4xWSyvTrHo9w + * xpub661MyMwAqRbcH5cM6J2CGXmeUz1KrPghZYV8QY9BRY2queYtFGdWQG313CVsomM8AHwvgpkeFTFeXH9KiU5uDiM9jdjg8cDx8wiycLk22j2 + * 044006e1a19e5a5479148c50a7f78ac0eadffbd16602def76dd2b5b8d723c9e7a5def45e0317c73cb93c69dbf447cabd1ed4a882359846e35e2e983dd14263dc83 + * + * Public key with key origin: + * 30590470 + * xprv9s21ZrQH143K3m5RPG2eA1WdsSvnLjtDuAa4KxEsgXMXj2vc8RsyaYvwwYDtbzDDrr9oR8hioYky8n9SxgSNgsoWYwsRX2ZggeRdSTVDa9A + * xpub661MyMwAqRbcGF9tVHZeX9TNRUmGkCc5GPVf8LeVErtWbqFkfyCE8MFRnpbkJStrP1PeEJfdzjc1jhcoGzwg5gMENKQpnzno3mYLu5o1d15 + * [30590470/m/0h/0h/0h]02830ef084e9783008acba8fe02fa6c3dc191749d6eb4b66d7b7fa87201c709a92 + * + * Public key with key origin (mixed hardened indicator): + * b5e0a030 + * xprv9s21ZrQH143K2th5ySt1QFRrs1jNUPrsDAciuv36B7EMMwZhXsJMhuKLs83dWec817t4SMgBb9bT7tQZ8nLSWMrFwrBgr3jZR6UzQZ9b43Z + * xpub661MyMwAqRbcFNmZ5UR1mPNbR3ZrsraiaPYKiJShjSmLEjtr5QccFhdpiRkHxmUvdW4ZDVQ3XPBeHi9rbTAHsBEysAycVwYAUwFdjQhCJuN + * [b5e0a030/m/0'/0h/0']038dd3f649c0e09b20614551368ea8ecf8fc1f6eb1364d2efed35b5cf0ebb6ef64 + * + * WIF uncompressed private key: + * 247b3c83 + * xprv9s21ZrQH143K4XmyUNxN4FgG3FZ1yvD8PtpvYfY2MNGJ2CBwndW2uM3NUXphZdF8RonZvYGEr42u7yQBJhZJLjKey75CBoyacpthcqBNHvt + * xpub661MyMwAqRbcH1rSaQVNRPczbHPWPNvym7kXM3wduhoGtzX6LApHT9MrKpphdSLmso9Uax3zSJULyPP4rbN8RzaYCtWmaAJJnEhSzFzbayS + * 5K9uth3PGkwuohubYn3e2nYGNMqdgzbghpnwskpjVREkYhgpMfJ + * + * WIF compressed private key: + * 9154f328 + * xprv9s21ZrQH143K2jWMnbz1sZB215RbDyhNXfZrxTfJQFLKv6Q62hNnEuvANhhwSgYkNaBfku1gqwug1zv8AuzxtMReKavWFD4JFHfJks5Ugqz + * xpub661MyMwAqRbcFDaptdX2Eh7kZ7G5dSRDttVTkr4uxasJntjEaEh2niEeDzCqnXkdRRfVYhUYGzab5UWzEd3Sg7qGnmvhajZSHRmMTeduKUv + * KySUEd6c3VTxhYHgXEqHLMxbHNa3pJLJD3keAuqPRqJf8bH8qBpn + * + * Extended public key: + * 1226afc5 + * xprv9s21ZrQH143K4Sh3uaHCp5oyK3UzEzXujCZuDM371shiopZtMn32fchsK3MCK6jjTTsDmeDxLqAguZ6gk8C5TCQkKnDHMwHs9p76c7pVcHp + * xpub661MyMwAqRbcGvmX1bpDBDkhs5KUeTFm6RVW1jSiaDEhgcu2uKMHDR2MAJT52X2kL1Hz8dTS2ysGbFzD8g5ucAJ9LLdeNwU7nQFFiiT8HGu + * + * Extended public key with key origin: + * e53b5e7b + * xprv9s21ZrQH143K2W8qPRLUpTxNWCsMkrceMvrabYaEkC6zTJJXU1Sbr3cf5J7w2EdR2SsKMm8YigdwTNaTEcrqBuXn8n93jNb54P5KdBmcK7A + * xpub661MyMwAqRbcEzDJVSsVBbu74EhrAKLVj9nBPvyrJXdyL6dg1YkrPqw8vZ4xsQAfi8TFTSLfN6mqu4FbhSscEuhzzPnmaJPebeX8EemJbQf + * xprv9yGdxVkDaZtVgHKe4ytm91ErMPmQ2Z2Y48FfPoaoup5kkYQRVNuRYgWNwX7K5yBRSn5PFLNBwnWQvbKDpiz4Q6dvQ8xHA7WLe564Arg3kSV + * [e53b5e7b/0h/1h/2h]xpub6CFzN1H7QwSntmQ7B1RmW9BauRbtS1kPRMBGCBzRU9cjdLja2vDg6Uprno22FtDpBGaxXWvQFZCZtWX3x56ioNJkxgHNq6HWzpNb5WKxcy7 + * + * Extended public key with derivation: + * f1bef795 + * xprv9s21ZrQH143K4LAzfCZCjBpiKhMAu5Lp5YUp7rGKVTXkxPbeaoa8WeCZhbrJffSCEetttwbTPuAL5D5zVjyvKpBVQauVEeaHBBepVgxVbTD + * xpub661MyMwAqRbcGpFTmE6D6KmSsjBfJY4fSmQQvEfw3o4jqBvo8LtP4SX3Ytd7sPzixeGgAUpp1ARFdY8cmqPnA3diq9SVEEyMyGeEiGmocsH + * xprv9zVLrcQeogxsD7qL3rxNpZTPVrfmVtP6i1AVKkr9SkRxQsrqitUN4w4d4QkN5cCFaMY4SmoYDk2zWeZWibCVM2ZDC8YZYEacUzixC6ojXj8 + * [f1bef795/0h/1h/2h]xpub6DUhG7wYe4XARbuo9tVPBhQ83tWFuM6x5E6689Fm15xwHgBzGRnccjP6uerqv8dDPKDX5uPDEJHdbwwFQgL8nr2UJ8JGXXpBmWxjLUm8bor/3/4/5 + * + * Extended public key with derivation and children: + * ea0648e9 + * xprv9s21ZrQH143K3enH5MmHQwpcAGX5UTdEX11mxDnKWwkQRCLLtcoWvsGjLiS5xgKYJXEyP1Zua3jgJH2tZNEtKirzdSrQDTgXoSELB2L3xmZ + * xpub661MyMwAqRbcG8rkBPJHn5mLiJMZsvM5tDwNkcBw5HHPHzfVSA7mUfbDBxreW78v4XAkv2LMR3Uy3EDSEyZUghkULzW2x2XtCJXX58sqozc + * xprv9z4SCcrCwbELH6Up7wXR2yjmbutLs65PsEhdNF2ditoe7okCH3nsg99ZuvrCaEmhtqA4XVA3teDwoHWtNzYQHTAthecTSrG7jpZi4XQiUz5 + * [ea0648e9/0h/1h/2h]xpub6D3nc8P6mxndVaZHDy4RQ7gW9wiqGYoFETdEAdSFHELczc5Lpb78DwU3mEN6QaeTCupScKDXxdmn8TY9A5agm9XEVSVyzYQJTwgPTek1kHz/3/4/5/* + * + * Extended public key with hardened derivation and unhardened children: + * + * + * + */ describe('Descriptors', function () { - it(`assertPsbt`, function () { + it(`dummy`, function () { + // assertDescriptorPlaceholders(['$10', '$0'], '$'); + // const a = bip32.fromSeed(crypto.randomBytes(32)); + // console.log(a.fingerprint.toString('hex')); + // console.log(a.toBase58()); + // console.log(a.neutered().toBase58()); + // console.log(a.derivePath("m/0'/1'/2'").toBase58()); + // console.log(a.derivePath("m/0'/1'/2'").neutered().toBase58()); + }); + it(`success`, function () { + // const descriptor = + // "wsh(andor(pk([73d83c6f/48'/1'/0'/2']tpubDEyGjD5PkjZf6rQiKWKqRi8wD8yvA43aurE8mwjE5yxjX3AJWyu7xnm94BcoxxirQKAd9AExoz7oqLiHAQL2tVC78r452bLedHyvE1GRzW8/*),older(10000),thresh(3,pk([0d78119c/48'/1'/0'/2']tpubDExqoVD2WKmN5YFBNQQZV9SU3ajPr4CLa1JBmLkNyCUv5nPDJxxAn6oqXKTUtaQxtRXrhPaGELi8hP1a5Rpjs7bNdqnfKMvVxebTtpmFtd6/*),s:pk([8f8c7811/48'/1'/0'/2']tpubDErfvxUCfe4Yd7vURSL69zSA2VtweQHvgmZAzQHNXwoY4UTcCq5F9qWfgGXtfDS3e6HZySbT8XXjmbGWkcyngqSsEBv4mYAazmETx3QfP9f/*),s:pk([a592f770/48'/1'/0'/2']tpubDE6Xb7UALq4fnjL39g6DUaf3z8MN8SeTzBMDFEhzyjYDVyn4oqY8A4K3qCUAnTCbsv8qheZK3EPNsrkE7sDnAzNcj2bXq5Rt22UESJ5wSCx/*),s:pk([4ef7afff/48'/1'/0'/2']tpubDFf5zVvuuzU285RfaHEsnsizhcgfW5Wyk6vQezW3BG1RBJXXAWdHSRzzesGfH9kQjeXgWNvFJJjZHxHPMk8xeqBj89WuuiwrfJKiVCKNMyH/*),s:pk([0d8a8cb4/48'/1'/0'/2']tpubDFQBGM1DQZyD6bWBgtgU8UQtzJvVxEmtuVfSg3cYf1R2tU6GnU4ZVUoUQ37SHGrhJyGtrZ5XG5y1Bz9ydn3yUYUkQRkvnQqq4KCcr7oaYLq/*),sln:after(840000))))"; + const descriptor = `wsh(andor(pk(03fda0d184f78dcdb33123129818495b39e1f5ebca259552e9283b7a5d3692dd6d),older(10000),thresh(3,pk([b5e0a030/0'/0'/0']038dd3f649c0e09b20614551368ea8ecf8fc1f6eb1364d2efed35b5cf0ebb6ef64),s:pk(xpub661MyMwAqRbcGvmX1bpDBDkhs5KUeTFm6RVW1jSiaDEhgcu2uKMHDR2MAJT52X2kL1Hz8dTS2ysGbFzD8g5ucAJ9LLdeNwU7nQFFiiT8HGu),s:pk([e53b5e7b/0'/1'/2']xpub6CFzN1H7QwSntmQ7B1RmW9BauRbtS1kPRMBGCBzRU9cjdLja2vDg6Uprno22FtDpBGaxXWvQFZCZtWX3x56ioNJkxgHNq6HWzpNb5WKxcy7),s:pk([f1bef795/0'/1'/2']xpub6DUhG7wYe4XARbuo9tVPBhQ83tWFuM6x5E6689Fm15xwHgBzGRnccjP6uerqv8dDPKDX5uPDEJHdbwwFQgL8nr2UJ8JGXXpBmWxjLUm8bor/3/4/5),s:pk([ea0648e9/0'/1'/2']xpub6D3nc8P6mxndVaZHDy4RQ7gW9wiqGYoFETdEAdSFHELczc5Lpb78DwU3mEN6QaeTCupScKDXxdmn8TY9A5agm9XEVSVyzYQJTwgPTek1kHz/3/4/5/*),sln:after(840000))))`; + assertDescriptor({ descriptor, network, checksumRequired: false }); + const { expandedMiniscript } = expand({ descriptor, network, checksumRequired: false, }); + assert(expandedMiniscript); + const nonKeyExpanded = expandNonKeyLocks(expandedMiniscript); + + assert( + nonKeyExpanded === 'andor(pk(@0),older(#0),thresh(3,pk(@1),s:pk(@2),s:pk(@3),s:pk(@4),s:pk(@5),sln:after(#1)))' + ); + const outputDescs: desc.OutputInstance[] = [0, 1, 2, 3, 4].map( (index) => new Output({ descriptor, index, network }) ); @@ -29,20 +117,19 @@ describe('Descriptors', function () { const inputPsbt = constructPsbt(inputs, outputs, network, rootWalletKeys, 'fullsigned'); const tx = inputPsbt.finalizeAllInputs().extractTransaction(); const txHex = tx.toHex(); - assert(expandedMiniscript); - const nonKeyExpanded = expandNonKeyLocks(expandedMiniscript); - assert( - nonKeyExpanded === 'andor(pk(@0),older(#0),thresh(3,pk(@1),s:pk(@2),s:pk(@3),s:pk(@4),s:pk(@5),sln:after(#1)))' - ); - assertDescriptor(descriptor, network, { checksumRequired: false }); const psbt = new Psbt({ network }); outputDescs.forEach((od, i) => od.updatePsbtAsInput({ psbt, txHex, vout: i })); new Output({ - descriptor: `addr(mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt)`, + descriptor: `addr(bc1q8qy6wmh5urjam6qdual8v6yud6w4kazlm2a8ur)`, network, }).updatePsbtAsOutput({ psbt, value: BigInt(10000) }); - assertPsbt(psbt, descriptor, network); + const hd = bip32.fromBase58( + 'xprv9s21ZrQH143K4LAzfCZCjBpiKhMAu5Lp5YUp7rGKVTXkxPbeaoa8WeCZhbrJffSCEetttwbTPuAL5D5zVjyvKpBVQauVEeaHBBepVgxVbTD' + ); + psbt.signAllInputsHD(hd); + + assertPsbt({ psbt, descriptor, network, scriptCheck: true, sigCheck: true, sigCheckKeyIds: ['@4'] }); }); }); diff --git a/yarn.lock b/yarn.lock index 6912bc5967..9e78bdc8aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6814,6 +6814,16 @@ bip32@^4.0.0: typeforce "^1.11.5" wif "^2.0.6" +bip32@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-4.0.0.tgz#7fac3c05072188d2d355a4d6596b37188f06aa2f" + integrity sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ== + dependencies: + "@noble/hashes" "^1.2.0" + "@scure/base" "^1.1.1" + typeforce "^1.11.5" + wif "^2.0.6" + bip39@3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz"