diff --git a/package.json b/package.json index 988ea6c..740a37f 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,11 @@ "ensureTester": "./node_modules/@bitcoinerlab/configs/scripts/ensureTester.sh", "test:integration:soft": "npm run ensureTester && node test/integration/standardOutputs.js && echo \"\\n\\n\" && node test/integration/miniscript.js", "test:integration:ledger": "npm run ensureTester && node test/integration/ledger.js", + "test:integration:deprecated": "npm run ensureTester && node test/integration/standardOutputs-deprecated.js && echo \"\\n\\n\" && node test/integration/miniscript-deprecated.js && echo \"\\n\\n\" && node test/integration/ledger-deprecated.js", "test:unit": "jest", "test": "npm run lint && npm run build && npm run test:unit && npm run test:integration:soft", "testledger": "npm run lint && npm run build && npm run test:integration:ledger", - "prepublishOnly": "npm run test && echo \"\\n\\n\" && npm run test:integration:ledger" + "prepublishOnly": "npm run test && echo \"\\n\\n\" && npm run test:integration:deprecated && npm run test:integration:ledger" }, "files": [ "dist" diff --git a/test/descriptors-deprecated.test.ts b/test/descriptors-deprecated.test.ts new file mode 100644 index 0000000..ccc585f --- /dev/null +++ b/test/descriptors-deprecated.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com +// Distributed under the MIT software license + +// This file still needs to be properly converted to Typescript: +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck +/* eslint-enable @typescript-eslint/ban-ts-comment */ + +import { DescriptorsFactory } from '../dist'; +import { fixtures as customFixtures } from './fixtures/custom'; +import { fixtures as bitcoinCoreFixtures } from './fixtures/bitcoinCore'; +import * as ecc from '@bitcoinerlab/secp256k1'; +const { Descriptor, expand } = DescriptorsFactory(ecc); + +function partialDeepEqual(obj) { + if (typeof obj === 'object' && obj !== null && obj.constructor === Object) { + const newObj = {}; + for (const key in obj) { + newObj[key] = partialDeepEqual(obj[key]); + } + return expect.objectContaining(newObj); + } else { + return obj; + } +} + +for (const fixtures of [customFixtures, bitcoinCoreFixtures]) { + describe(`Deprecated API - Parse valid ${ + fixtures === customFixtures ? 'custom fixtures' : 'Bitcoin Core fixtures' + }`, () => { + for (const fixture of fixtures.valid) { + fixture.expression = fixture.descriptor; + delete fixture.descriptor; + test(`Parse valid ${fixture.expression}`, () => { + const descriptor = new Descriptor(fixture); + let expansion; + expect(() => { + expansion = expand({ + expression: fixture.expression, + network: fixture.network, + allowMiniscriptInP2SH: fixture.allowMiniscriptInP2SH + }); + }).not.toThrow(); + + if (fixture.expansion) { + expect(expansion).toEqual(partialDeepEqual(fixture.expansion)); + } + + if (!fixture.script && !fixture.address) + throw new Error(`Error: pass a valid test for ${fixture.expression}`); + if (fixture.script) { + expect(descriptor.getScriptPubKey().toString('hex')).toEqual( + fixture.script + ); + } + if (fixture.address) { + expect(descriptor.getAddress()).toEqual(fixture.address); + } + }); + } + }); + describe(`Deprecated API - Parse invalid ${ + fixtures === customFixtures ? 'custom fixtures' : 'Bitcoin Core fixtures' + }`, () => { + for (const fixture of fixtures.invalid) { + fixture.expression = fixture.descriptor; + delete fixture.descriptor; + test(`Parse invalid ${fixture.expression}`, () => { + if (typeof fixture.throw !== 'string') { + expect(() => { + new Descriptor(fixture); + }).toThrow(); + } else { + expect(() => { + new Descriptor(fixture); + }).toThrow(fixture.throw); + } + }); + } + }); +} diff --git a/test/integration/ledger-deprecated.ts b/test/integration/ledger-deprecated.ts new file mode 100644 index 0000000..5efd3f3 --- /dev/null +++ b/test/integration/ledger-deprecated.ts @@ -0,0 +1,297 @@ +// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com +// Distributed under the MIT software license + +/* +This test will create a set of UTXOs for a Ledger wallet: + * 1 P2PKH output on an internal address, change 1, in account 0, index 0 + * 1 P2PKH output on an external address, change 0, in account 0, index 0 + * 1 P2WSH output corresponding to a script based on this policy: + and(and(and(pk(@ledger),pk(@soft)),older(${OLDER})),sha256(${SHA256_DIGEST})), + which means it can be spent by co-signing with a Ledger and a Software + wallet after BLOCKS blocks since it was mined and providing a preimage for + a certain SHA256_DIGEST. + +In the test, the UTXOs are created, funded (each one with UTXO_VALUE), +and finally spent by co-signing (Ledger + Soft) a partially-signed Bitcoin +Transaction (PSBT), finalizing it and broadcasting it to the network. + +================================================================================ + +To run this test, follow these steps: + +1. Clone the `descriptors` repository by running + `git clone https://github.com/bitcoinerlab/descriptors.git`. + +2. Install the necessary dependencies by running `npm install`. + +3. Ensure that you are running a Bitcoin regtest node and have set up this + Express-based bitcoind manager: https://github.com/bitcoinjs/regtest-server + running on 127.0.0.1:8080. + You can use the following steps to install and run a Docker image already + configured with the mentioned services: + + docker pull junderw/bitcoinjs-regtest-server + docker run -d -p 127.0.0.1:8080:8080 junderw/bitcoinjs-regtest-server + +4. Connect your Ledger device, unlock it, and open the Bitcoin Testnet 2.1 App. + +5. You are now ready to run the test: + npx ts-node test/integration/ledger.ts + +*/ + +console.log( + 'Ledger integration tests: 2 pkh inputs (one internal & external addresses) + 1 miniscript input (co-signed with a software wallet) -> 1 output' +); +import Transport from '@ledgerhq/hw-transport-node-hid'; +import { networks, Psbt } from 'bitcoinjs-lib'; +import { mnemonicToSeedSync } from 'bip39'; +const { encode: olderEncode } = require('bip68'); +import { RegtestUtils } from 'regtest-client'; +const regtestUtils = new RegtestUtils(); + +const NETWORK = networks.regtest; + +const UTXO_VALUE = 2e4; +const FINAL_ADDRESS = regtestUtils.RANDOM_ADDRESS; +const FEE = 1000; +const BLOCKS = 5; +const OLDER = olderEncode({ blocks: BLOCKS }); +const PREIMAGE = + '107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'; +const SHA256_DIGEST = + '6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333'; + +const POLICY = `and(and(and(pk(@ledger),pk(@soft)),older(${OLDER})),sha256(${SHA256_DIGEST}))`; + +const WSH_ORIGIN_PATH = `/69420'/1'/0'`; //Actually, this could be any random path. Note that the Ledger will show a warning for non-standardness, though. +const WSH_RECEIVE_INDEX = 0; + +const SOFT_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +import * as ecc from '@bitcoinerlab/secp256k1'; +import { + finalizePsbt, + signers, + keyExpressionBIP32, + keyExpressionLedger, + scriptExpressions, + DescriptorsFactory, + DescriptorInstance, + ledger, + LedgerState +} from '../../dist/'; +const { signLedger, signBIP32 } = signers; +const { pkhLedger } = scriptExpressions; +const { registerLedgerWallet, assertLedgerApp } = ledger; +import { AppClient } from 'ledger-bitcoin'; +const { Descriptor, BIP32 } = DescriptorsFactory(ecc); + +import { compilePolicy } from '@bitcoinerlab/miniscript'; + +//Create the psbt that will spend the pkh and wsh outputs and send funds to FINAL_ADDRESS: +const psbt = new Psbt({ network: NETWORK }); + +//Build the miniscript-based descriptor. +//POLICY will be: 'and(and(and(pk(@ledger),pk(@soft)),older(5)),sha256(6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333))' +//and miniscript: 'and_v(v:sha256(6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333),and_v(and_v(v:pk(@ledger),v:pk(@soft)),older(5)))' +const { miniscript, issane }: { miniscript: string; issane: boolean } = + compilePolicy(POLICY); +if (!issane) throw new Error(`Error: miniscript not sane`); + +let txHex: string; +let txId: string; +let vout: number; +let inputIndex: number; +//In this array, we will keep track of the descriptors of each input: +const psbtInputDescriptors: DescriptorInstance[] = []; + +(async () => { + let transport; + try { + transport = await Transport.create(3000, 3000); + } catch (err) { + throw new Error(`Error: Ledger device not detected`); + } + //Throw if not running Bitcoin Test >= 2.1.0 + await assertLedgerApp({ + transport, + name: 'Bitcoin Test', + minVersion: '2.1.0' + }); + + const ledgerClient = new AppClient(transport); + //The Ledger is stateless. We keep state externally (keeps track of masterFingerprint, xpubs, wallet policies, ...) + const ledgerState: LedgerState = {}; + + //Let's create the utxos. First create a descriptor expression using a Ledger. + //pkhExternalExpression will be something like this: + //pkh([1597be92/44'/1'/0']tpubDCxfn3TkomFUmqNzKq5AEDS6VHA7RupajLi38JkahFrNeX3oBGp2C7SVWi5a1kr69M8GpeqnGkgGLdja5m5Xbe7E87PEwR5kM2PWKcSZMoE/0/0) + const pkhExternalExpression: string = await pkhLedger({ + ledgerClient, + ledgerState, + network: NETWORK, + account: 0, + change: 0, + index: 0 + }); + const pkhExternalDescriptor = new Descriptor({ + network: NETWORK, + expression: pkhExternalExpression + }); + //Fund this utxo. regtestUtils communicates with the regtest node manager on port 8080. + ({ txId, vout } = await regtestUtils.faucet( + pkhExternalDescriptor.getAddress(), + UTXO_VALUE + )); + //Retrieve the tx from the mempool: + txHex = (await regtestUtils.fetch(txId)).txHex; + //Now add an input to the psbt. updatePsbt would also update timelock if needed (not in this case). + inputIndex = pkhExternalDescriptor.updatePsbt({ psbt, txHex, vout }); + //Save the descriptor for later, indexed by its psbt input number. + psbtInputDescriptors[inputIndex] = pkhExternalDescriptor; + + //Repeat the same for another pkh change address: + const pkhChangeExpression = await pkhLedger({ + ledgerClient, + ledgerState, + network: NETWORK, + account: 0, + change: 1, + index: 0 + }); + const pkhChangeDescriptor = new Descriptor({ + network: NETWORK, + expression: pkhChangeExpression + }); + ({ txId, vout } = await regtestUtils.faucet( + pkhChangeDescriptor.getAddress(), + UTXO_VALUE + )); + txHex = (await regtestUtils.fetch(txId)).txHex; + inputIndex = pkhChangeDescriptor.updatePsbt({ psbt, txHex, vout }); + psbtInputDescriptors[inputIndex] = pkhChangeDescriptor; + + //Here we create the BIP32 software wallet that will be used to co-sign the 3rd utxo of this test: + const masterNode = BIP32.fromSeed(mnemonicToSeedSync(SOFT_MNEMONIC), NETWORK); + + //Let's prepare the wsh utxo. First create the Ledger and Soft key expressions + //that will be used to co-sign the wsh output. + //First, create a ranged key expression (index: '*') using the software wallet + //on the WSH_ORIGIN_PATH origin path. + //We could have also created a non-ranged key expression by providing a number + //to index. + //softKeyExpression will be something like this: + //[73c5da0a/69420'/1'/0']tpubDDB5ZuMuWmdzs7r4h58fwZQ1eYJvziXaLMiAfHYrAev3jFrfLtsYsu7Cp1hji8KcG9z9CcvHe1FfkvpsjbvMd2JTLwFkwXQCYjTZKGy8jWg/0/* + const softKeyExpression: string = keyExpressionBIP32({ + masterNode, + originPath: WSH_ORIGIN_PATH, + change: 0, + index: '*' + }); + //Create the equivalent ranged key expression using the Ledger wallet. + //ledgerKeyExpression will be something like this: + //[1597be92/69420'/1'/0']tpubDCNNkdMMfhdsCFf1uufBVvHeHSEAEMiXydCvxuZKgM2NS3NcRCUP7dxihYVTbyu1H87pWakBynbYugEQcCbpR66xyNRVQRzr1TcTqqsWJsK/0/* + //Since WSH_ORIGIN_PATH is a non-standard path, the Ledger will warn the user about this. + const ledgerKeyExpression: string = await keyExpressionLedger({ + ledgerClient, + ledgerState, + originPath: WSH_ORIGIN_PATH, + change: 0, + index: '*' + }); + + //Now, we prepare the ranged miniscript descriptor expression for external addresses (change = 0). + //expression will be something like this: + //wsh(and_v(v:sha256(6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333),and_v(and_v(v:pk([1597be92/69420'/1'/0']tpubDCNNkdMMfhdsCFf1uufBVvHeHSEAEMiXydCvxuZKgM2NS3NcRCUP7dxihYVTbyu1H87pWakBynbYugEQcCbpR66xyNRVQRzr1TcTqqsWJsK/0/*),v:pk([73c5da0a/69420'/1'/0']tpubDDB5ZuMuWmdzs7r4h58fwZQ1eYJvziXaLMiAfHYrAev3jFrfLtsYsu7Cp1hji8KcG9z9CcvHe1FfkvpsjbvMd2JTLwFkwXQCYjTZKGy8jWg/0/*)),older(5)))) + const expression = `wsh(${miniscript + .replace('@ledger', ledgerKeyExpression) + .replace('@soft', softKeyExpression)})`; + //Get the descriptor for index WSH_RECEIVE_INDEX. Here we need to pass the index because + //we used range key expressions above. `index` is only necessary when using range expressions. + //We also pass the PREIMAGE so that miniscriptDescriptor will be able to finalize the tx later (creating the scriptWitness) + const miniscriptDescriptor = new Descriptor({ + expression, + index: WSH_RECEIVE_INDEX, + preimages: [{ digest: `sha256(${SHA256_DIGEST})`, preimage: PREIMAGE }], + network: NETWORK + }); + //We can now fund the wsh utxo: + ({ txId, vout } = await regtestUtils.faucet( + miniscriptDescriptor.getAddress(), + UTXO_VALUE + )); + txHex = (await regtestUtils.fetch(txId)).txHex; + + //Now add a the input to the psbt (including bip32 derivation info & sequence) and + //set the tx timelock, if needed. + //In this case the timelock won't be set since this is a relative-timelock + //script (it will set the sequence in the input) + inputIndex = miniscriptDescriptor.updatePsbt({ psbt, txHex, vout }); + //Save the descriptor, indexed by input index, for later: + psbtInputDescriptors[inputIndex] = miniscriptDescriptor; + + //Now add an ouput. This is where we'll send the funds. We'll send them to + //some random address that we don't care about in this test. + psbt.addOutput({ address: FINAL_ADDRESS, value: UTXO_VALUE * 3 - FEE }); + + //============= + //Register Ledger policies of non-standard descriptors. + //Registration is stored in ledgerState and is a necessary step before + //signing with non-standard policies when using a Ledger wallet. + //registerLedgerWallet internally takes all the necessary steps to register + //the generalized Ledger format: a policy template finished with /** and its keyRoots. + //So, even though this wallet policy is created using a descriptor representing + //an external address, the policy will be used interchangeably with internal + //and external addresses. + await registerLedgerWallet({ + ledgerClient, + ledgerState, + descriptor: miniscriptDescriptor, + policyName: 'BitcoinerLab' + }); + + //============= + //Sign the psbt with the Ledger. The relevant wallet policy is automatically + //retrieved from state by parsing the descriptors of each input and retrieving + //the wallet policy that can sign it. Also a Default Policy is automatically + //constructed when the input is of BIP 44, 49, 84 or 86 type. + await signLedger({ + ledgerClient, + ledgerState, + psbt, + descriptors: psbtInputDescriptors + }); + //Now sign the PSBT with the BIP32 node (the software wallet) + signBIP32({ psbt, masterNode }); + + //============= + //Finalize the psbt: + //descriptors must be indexed wrt its psbt input number. + //finalizePsbt uses the miniscript satisfier from @bitcoinerlab/miniscript to + //create the scriptWitness among other things. + finalizePsbt({ psbt, descriptors: psbtInputDescriptors }); + + //Since the miniscript uses a relative-timelock, we need to mine BLOCKS before + //broadcasting the tx so that it can be accepted by the network + await regtestUtils.mine(BLOCKS); + //Broadcast the tx: + const spendTx = psbt.extractTransaction(); + const resultSpend = await regtestUtils.broadcast(spendTx.toHex()); + //Mine it + await regtestUtils.mine(1); + //Verify that the tx was accepted. This will throw if not ok: + await regtestUtils.verify({ + txId: spendTx.getId(), + address: FINAL_ADDRESS, + vout: 0, + value: UTXO_VALUE * 3 - FEE + }); + + console.log({ + result: resultSpend === null ? 'success' : resultSpend, + psbt: psbt.toBase64(), + tx: spendTx.toHex() + }); +})(); diff --git a/test/integration/miniscript-deprecated.ts b/test/integration/miniscript-deprecated.ts new file mode 100644 index 0000000..0803e2e --- /dev/null +++ b/test/integration/miniscript-deprecated.ts @@ -0,0 +1,181 @@ +// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com +// Distributed under the MIT software license + +//npm run test:integration + +import { networks, Psbt, address } from 'bitcoinjs-lib'; +import { mnemonicToSeedSync } from 'bip39'; +const { encode: afterEncode } = require('bip65'); +const { encode: olderEncode } = require('bip68'); +import { RegtestUtils } from 'regtest-client'; +const regtestUtils = new RegtestUtils(); + +const BLOCKS = 5; +const NETWORK = networks.regtest; +const INITIAL_VALUE = 2e4; +const FINAL_VALUE = INITIAL_VALUE - 1000; +const FINAL_ADDRESS = regtestUtils.RANDOM_ADDRESS; +const FINAL_SCRIPTPUBKEY = address.toOutputScript(FINAL_ADDRESS, NETWORK); +const SOFT_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; +const POLICY = (older: number, after: number) => + `or(and(pk(@olderKey),older(${older})),and(pk(@afterKey),after(${after})))`; + +console.log( + `Miniscript integration tests: ${POLICY.toString().match(/`([^`]*)`/)![1]}` +); + +import * as ecc from '@bitcoinerlab/secp256k1'; +import { DescriptorsFactory, keyExpressionBIP32, signers } from '../../dist/'; +import { compilePolicy } from '@bitcoinerlab/miniscript'; +const { signBIP32, signECPair } = signers; + +const { Descriptor, BIP32, ECPair } = DescriptorsFactory(ecc); + +const masterNode = BIP32.fromSeed(mnemonicToSeedSync(SOFT_MNEMONIC), NETWORK); +const ecpair = ECPair.makeRandom(); + +const keys: { + [key: string]: { + originPath: string; + keyPath: string; + }; +} = { + '@olderKey': { originPath: "/0'/1'/0'", keyPath: '/0' }, + '@afterKey': { originPath: "/0'/1'/1'", keyPath: '/1' } +}; + +(async () => { + //The 3 for loops below test all possible combinations of + //signer type (BIP32 or ECPair), top-level scripts (sh, wsh, sh-wsh) and + //who is spending the tx: the "older" or the "after" branch + for (const keyExpressionType of ['BIP32', 'ECPair']) { + for (const template of [`sh(SCRIPT)`, `wsh(SCRIPT)`, `sh(wsh(SCRIPT))`]) { + for (const spendingBranch of Object.keys(keys)) { + const currentBlockHeight = await regtestUtils.height(); + const after = afterEncode({ blocks: currentBlockHeight + BLOCKS }); + const older = olderEncode({ blocks: BLOCKS }); //relative locktime (sequence) + //The policy below has been selected for the tests because it has 2 spending + //branches: the "after" and the "older" branch. + //Note that the hash to be signed depends on the nSequence and nLockTime + //values, which is different on each branch. + //This makes it an interesting test scenario. + const policy = POLICY(older, after); + const { miniscript: expandedMiniscript, issane } = + compilePolicy(policy); + if (!issane) + throw new Error( + `Error: miniscript ${expandedMiniscript} from policy ${policy} is not sane` + ); + + //Note that the hash to be signed is different depending on how we decide + //to spend the script. + //Here we decide how are we going to spend the script. + //spendingBranch is either @olderKey or @afterKey. + //Use signersPubKeys in Descriptor's constructor to account for this + let miniscript = expandedMiniscript; + const signersPubKeys: Buffer[] = []; + for (const key in keys) { + const keyValue = keys[key]; + if (!keyValue) throw new Error(); + const { originPath, keyPath } = keyValue; + const keyExpression = keyExpressionBIP32({ + masterNode, + originPath, + keyPath + }); + const node = masterNode.derivePath(`m${originPath}${keyPath}`); + const pubkey = node.publicKey; + if (key === spendingBranch) { + if (keyExpressionType === 'BIP32') { + miniscript = miniscript.replace( + new RegExp(key, 'g'), + keyExpression + ); + signersPubKeys.push(pubkey); + } else { + miniscript = miniscript.replace( + new RegExp(key, 'g'), + ecpair.publicKey.toString('hex') + ); + + signersPubKeys.push(ecpair.publicKey); + } + } else { + //For the non spending branch we can simply use the pubKey as key expressions + miniscript = miniscript.replace( + new RegExp(key, 'g'), + pubkey.toString('hex') + ); + } + } + const expression = template.replace('SCRIPT', miniscript); + const descriptor = new Descriptor({ + expression, + //Use signersPubKeys to mark which spending path will be used + //(which pubkey must be used) + signersPubKeys, + allowMiniscriptInP2SH: true, //Default is false. Activated to test sh(SCRIPT). + network: NETWORK + }); + + const { txId, vout } = await regtestUtils.faucetComplex( + descriptor.getScriptPubKey(), + INITIAL_VALUE + ); + const { txHex } = await regtestUtils.fetch(txId); + const psbt = new Psbt(); + const index = descriptor.updatePsbt({ psbt, vout, txHex }); + psbt.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + if (keyExpressionType === 'BIP32') signBIP32({ masterNode, psbt }); + else signECPair({ ecpair, psbt }); + descriptor.finalizePsbtInput({ index, psbt }); + const spendTx = psbt.extractTransaction(); + //Now let's mine BLOCKS - 1 and see how the node complains about + //trying to broadcast it now. + await regtestUtils.mine(BLOCKS - 1); + try { + await regtestUtils.broadcast(spendTx.toHex()); + throw new Error(`Error: mining BLOCKS - 1 did not fail`); + } catch (error) { + const expectedErrorMessage = + spendingBranch === '@olderKey' ? 'non-BIP68-final' : 'non-final'; + if ( + error instanceof Error && + error.message !== expectedErrorMessage + ) { + throw new Error(error.message); + } + } + //Mine the last block needed + await regtestUtils.mine(1); + await regtestUtils.broadcast(spendTx.toHex()); + await regtestUtils.verify({ + txId: spendTx.getId(), + address: FINAL_ADDRESS, + vout: 0, + value: FINAL_VALUE + }); + //Verify the final locking and sequence depending on the branch + if (spendingBranch === '@afterKey' && spendTx.locktime !== after) + throw new Error(`Error: final locktime was not correct`); + if ( + spendingBranch === '@olderKey' && + spendTx.ins[0]?.sequence !== older + ) + throw new Error(`Error: final sequence was not correct`); + console.log(`\nDescriptor: ${expression}`); + console.log( + `Branch: ${spendingBranch}, ${keyExpressionType} signing, tx locktime: ${ + psbt.locktime + }, input sequence: ${psbt.txInputs?.[0]?.sequence?.toString( + 16 + )}, ${descriptor + .expand() + .expandedExpression?.replace('@0', '@olderKey') + .replace('@1', '@afterKey')}: OK` + ); + } + } + } +})(); diff --git a/test/integration/standardOutputs-deprecated.ts b/test/integration/standardOutputs-deprecated.ts new file mode 100644 index 0000000..2dd2bda --- /dev/null +++ b/test/integration/standardOutputs-deprecated.ts @@ -0,0 +1,201 @@ +// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com +// Distributed under the MIT software license + +//npm run test:integration + +console.log('Standard output integration tests'); +import { networks, Psbt, address } from 'bitcoinjs-lib'; +import { mnemonicToSeedSync } from 'bip39'; +import { RegtestUtils } from 'regtest-client'; +const regtestUtils = new RegtestUtils(); + +const NETWORK = networks.regtest; +const INITIAL_VALUE = 2e4; +const FINAL_VALUE = INITIAL_VALUE - 1000; +const FINAL_ADDRESS = regtestUtils.RANDOM_ADDRESS; +const FINAL_SCRIPTPUBKEY = address.toOutputScript(FINAL_ADDRESS, NETWORK); +const SOFT_MNEMONIC = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +import * as ecc from '@bitcoinerlab/secp256k1'; +import { + DescriptorsFactory, + DescriptorInstance, + scriptExpressions, + keyExpressionBIP32, + signers +} from '../../dist/'; +const { wpkhBIP32, shWpkhBIP32, pkhBIP32 } = scriptExpressions; +const { signBIP32, signECPair } = signers; + +const { Descriptor, BIP32, ECPair } = DescriptorsFactory(ecc); + +const masterNode = BIP32.fromSeed(mnemonicToSeedSync(SOFT_MNEMONIC), NETWORK); +//masterNode will be able to sign all the expressions below: +const expressionsBIP32 = [ + `pk(${keyExpressionBIP32({ + masterNode, + originPath: "/0'/1'/0'", + change: 0, + index: 0 + })})`, + pkhBIP32({ masterNode, network: NETWORK, account: 0, change: 0, index: 0 }), + wpkhBIP32({ masterNode, network: NETWORK, account: 0, change: 0, index: 0 }), + shWpkhBIP32({ masterNode, network: NETWORK, account: 0, change: 0, index: 0 }) +]; +if ( + pkhBIP32({ masterNode, network: NETWORK, account: 0, keyPath: '/0/0' }) !== + pkhBIP32({ masterNode, network: NETWORK, account: 0, change: 0, index: 0 }) +) + throw new Error(`Error: cannot use keyPath <-> change, index, indistinctly`); + +const ecpair = ECPair.makeRandom(); +//The same ecpair will be able to sign all the expressions below: +const expressionsECPair = [ + `pk(${ecpair.publicKey.toString('hex')})`, + `pkh(${ecpair.publicKey.toString('hex')})`, + `wpkh(${ecpair.publicKey.toString('hex')})`, + `sh(wpkh(${ecpair.publicKey.toString('hex')}))` +]; + +(async () => { + const psbtMultiInputs = new Psbt(); + const multiInputsDescriptors: DescriptorInstance[] = []; + for (const expression of expressionsBIP32) { + const descriptorBIP32 = new Descriptor({ expression, network: NETWORK }); + + let { txId, vout } = await regtestUtils.faucetComplex( + descriptorBIP32.getScriptPubKey(), + INITIAL_VALUE + ); + let { txHex } = await regtestUtils.fetch(txId); + const psbt = new Psbt(); + //Add an input and update timelock (if necessary): + const index = descriptorBIP32.updatePsbt({ psbt, vout, txHex }); + if (descriptorBIP32.isSegwit()) { + //Do some additional tests. Create a tmp psbt using txId and value instead + //of txHex using Segwit. Passing the value instead of the txHex is not + //recommended anyway. It's the user's responsibility to make sure that + //the value is correct to avoid possible fee attacks. + //updatePsbt should output a Warning message. + const tmpPsbtSegwit = new Psbt(); + const originalWarn = console.warn; + let capturedOutput = ''; + console.warn = (message: string) => { + capturedOutput += message; + }; + //Add an input and update timelock (if necessary): + const indexSegwit = descriptorBIP32.updatePsbt({ + psbt: tmpPsbtSegwit, + vout, + txId, + value: INITIAL_VALUE + }); + if (capturedOutput !== 'Warning: missing txHex may allow fee attacks') + throw new Error(`Error: did not warn about fee attacks`); + console.warn = originalWarn; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nonFinalTxHex = (psbt as any).__CACHE.__TX.toHex(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nonFinalSegwitTxHex = (tmpPsbtSegwit as any).__CACHE.__TX.toHex(); + if (indexSegwit !== index || nonFinalTxHex !== nonFinalSegwitTxHex) + throw new Error( + `Error: could not create same psbt ${nonFinalTxHex} for Segwit not using txHex: ${nonFinalSegwitTxHex}` + ); + } + psbt.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + signBIP32({ psbt, masterNode }); + descriptorBIP32.finalizePsbtInput({ index, psbt }); + const spendTx = psbt.extractTransaction(); + await regtestUtils.broadcast(spendTx.toHex()); + await regtestUtils.verify({ + txId: spendTx.getId(), + address: FINAL_ADDRESS, + vout: 0, + value: FINAL_VALUE + }); + console.log(`${expression}: OK`); + + ///Update multiInputs PSBT with a similar BIP32 input + ({ txId, vout } = await regtestUtils.faucetComplex( + descriptorBIP32.getScriptPubKey(), + INITIAL_VALUE + )); + ({ txHex } = await regtestUtils.fetch(txId)); + //Adds an input and updates timelock (if necessary): + const bip32Index = descriptorBIP32.updatePsbt({ + psbt: psbtMultiInputs, + vout, + txHex + }); + multiInputsDescriptors[bip32Index] = descriptorBIP32; + } + + for (const expression of expressionsECPair) { + const descriptorECPair = new Descriptor({ + expression, + network: NETWORK + }); + let { txId, vout } = await regtestUtils.faucetComplex( + descriptorECPair.getScriptPubKey(), + INITIAL_VALUE + ); + let { txHex } = await regtestUtils.fetch(txId); + const psbtECPair = new Psbt(); + //Adds an input and updates timelock (if necessary): + const indexECPair = descriptorECPair.updatePsbt({ + psbt: psbtECPair, + vout, + txHex + }); + psbtECPair.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + signECPair({ psbt: psbtECPair, ecpair }); + descriptorECPair.finalizePsbtInput({ + index: indexECPair, + psbt: psbtECPair + }); + const spendTxECPair = psbtECPair.extractTransaction(); + await regtestUtils.broadcast(spendTxECPair.toHex()); + await regtestUtils.verify({ + txId: spendTxECPair.getId(), + address: FINAL_ADDRESS, + vout: 0, + value: FINAL_VALUE + }); + console.log(`${expression}: OK`); + + ///Update multiInputs PSBT with a similar ECPair input + ({ txId, vout } = await regtestUtils.faucetComplex( + descriptorECPair.getScriptPubKey(), + INITIAL_VALUE + )); + ({ txHex } = await regtestUtils.fetch(txId)); + //Add an input and update timelock (if necessary): + const ecpairIndex = descriptorECPair.updatePsbt({ + psbt: psbtMultiInputs, + vout, + txHex + }); + multiInputsDescriptors[ecpairIndex] = descriptorECPair; + } + + psbtMultiInputs.addOutput({ script: FINAL_SCRIPTPUBKEY, value: FINAL_VALUE }); + //Sign and finish psbtMultiInputs + signECPair({ psbt: psbtMultiInputs, ecpair }); + signBIP32({ psbt: psbtMultiInputs, masterNode }); + multiInputsDescriptors.forEach((descriptor, index) => + descriptor.finalizePsbtInput({ index, psbt: psbtMultiInputs }) + ); + + const spendTxMultiInputs = psbtMultiInputs.extractTransaction(); + await regtestUtils.broadcast(spendTxMultiInputs.toHex()); + await regtestUtils.verify({ + txId: spendTxMultiInputs.getId(), + address: FINAL_ADDRESS, + vout: 0, + value: FINAL_VALUE + }); + console.log( + `Spend Psbt with BIP32 & ECPair signers from multiple standard inputs: OK` + ); +})();