UNPKG

ln-service

Version:

Interaction helper for your Lightning Network daemon

369 lines (287 loc) 10.4 kB
const {equal} = require('node:assert').strict; const test = require('node:test'); const asyncRetry = require('async/retry'); const {address} = require('bitcoinjs-lib'); const {controlBlock} = require('p2tr'); const {createPsbt} = require('psbt'); const {hashForTree} = require('p2tr'); const {networks} = require('bitcoinjs-lib'); const {script} = require('bitcoinjs-lib'); const {scriptElementsAsScript} = require('@alexbosworth/blockchain'); const {spawnLightningCluster} = require('ln-docker-daemons'); const tinysecp = require('tiny-secp256k1'); const {Transaction} = require('bitcoinjs-lib'); const {v1OutputScript} = require('p2tr'); const {beginGroupSigningSession} = require('./../../'); const {broadcastChainTransaction} = require('./../../'); const {createChainAddress} = require('./../../'); const {fundPsbt} = require('./../../'); const {getPublicKey} = require('./../../'); const {getUtxos} = require('./../../'); const {signPsbt} = require('./../../'); const {signTransaction} = require('./../../'); const compile = elements => scriptElementsAsScript({elements}).script; const count = 100; const defaultInternalKey = '0350929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; const {fromHex} = Transaction; const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const interval = retryCount => 10 * Math.pow(2, retryCount); const OP_CHECKSIG = 172; const smallTokens = 2e5; const times = 20; const {toOutputScript} = address; const tokens = 1e6; // Signing a taproot transaction should result in a valid signature test(`Sign a taproot transaction`, async () => { const ecp = (await import('ecpair')).ECPairFactory(tinysecp); const {kill, nodes} = await spawnLightningCluster({}); const [{generate, lnd}] = nodes; try { await beginGroupSigningSession({ lnd, is_key_spend: true, key_family: 0, key_index: 0, public_keys: [Buffer.alloc(33, 2).toString('hex')], }); } catch (err) { // On LND 0.14.5 and below, taproot signing is not supported if (err.slice().shift() === 501) { await kill({}); return; } throw err; } await generate({count}); const {address} = await createChainAddress({lnd}); const [utxo] = (await getUtxos({lnd})).utxos; const funded = await asyncRetry({interval, times}, async () => { try { return await fundPsbt({ lnd, inputs: [{ transaction_id: utxo.transaction_id, transaction_vout: utxo.transaction_vout, }], outputs: [{address, tokens}], }); } catch (err) { // On LND 0.11.1 and below, funding a PSBT is not supported if (err.slice().shift() === 501) { return; } throw err; } }); // A Taproot script output should be funded and spent with script try { await generate({count}); const scriptKey = await getPublicKey({lnd, family: 805}); const publicKey = hexAsBuffer(scriptKey.public_key); const witnessScript = compile([publicKey.slice(1), OP_CHECKSIG]); const branches = [{script: witnessScript}]; const {hash} = hashForTree({branches}); const output = v1OutputScript({hash, internal_key: defaultInternalKey}); const [utxo] = (await getUtxos({lnd})).utxos.reverse(); // Make a PSBT paying to the Taproot output const {psbt} = createPsbt({ outputs: [{tokens, script: output.script}], utxos: [{id: utxo.transaction_id, vout: utxo.transaction_vout}], }); // Sign the PSBT const signed = await signPsbt({ lnd, psbt: (await fundPsbt({lnd, psbt})).psbt, }); // Send the tx to the chain await broadcastChainTransaction({lnd, transaction: signed.transaction}); // Make a new tx that will spend the output back into the wallet const tx = new Transaction(); // The new tx spends the Taproot output tx.addInput( fromHex(signed.transaction).getHash(), fromHex(signed.transaction).outs.findIndex(n => n.value === tokens) ); // Make an output to pay back into the wallet const chainOutput = toOutputScript( (await createChainAddress({lnd})).address, networks.regtest ); // Add output to the pay back transaction tx.addOutput(chainOutput, smallTokens); const {signatures} = await signTransaction({ lnd, inputs: [{ key_family: 805, key_index: scriptKey.index, output_script: output.script, output_tokens: tokens, root_hash: hash, sighash: Transaction.SIGHASH_DEFAULT, vin: 0, witness_script: witnessScript, }], transaction: tx.toHex(), }); const [signature] = signatures.map(hexAsBuffer); const {block} = controlBlock({ external_key: output.external_key, leaf_script: witnessScript, script_branches: branches, }); // Add the signature to the input tx.ins.forEach((input, i) => { return tx.setWitness(i, [ signature, hexAsBuffer(witnessScript), hexAsBuffer(block), ]); }); await broadcastChainTransaction({lnd, transaction: tx.toHex()}); await asyncRetry({interval, times}, async () => { await generate({}); const {utxos} = await getUtxos({lnd}); const utxo = utxos.find(n => n.transaction_id === tx.getId()); if (!utxo || !utxo.confirmation_count) { throw new Error('ExpectedReceivedTaprootSpend'); } }); } catch (err) { equal(err, null, 'Expected no error'); } // A Taproot script can be funded and spent with internal key + script hash try { await generate({count}); const topLevelKey = await getPublicKey({lnd, family: 805}); const unusedKey = ecp.makeRandom({network: networks.regtest}); const witnessScript = compile([ Buffer.from(unusedKey.publicKey).slice(1), OP_CHECKSIG, ]); const branches = [{script: witnessScript}]; const {hash} = hashForTree({branches}); const output = v1OutputScript({ hash, internal_key: topLevelKey.public_key, }); const [utxo] = (await getUtxos({lnd})).utxos.reverse(); // Make a PSBT paying to the Taproot output const {psbt} = createPsbt({ outputs: [{tokens, script: output.script}], utxos: [{id: utxo.transaction_id, vout: utxo.transaction_vout}], }); // Sign the PSBT const signed = await signPsbt({ lnd, psbt: (await fundPsbt({lnd, psbt})).psbt, }); // Send the tx to the chain await broadcastChainTransaction({lnd, transaction: signed.transaction}); // Make a new tx that will spend the output back into the wallet const tx = new Transaction(); // The new tx spends the Taproot output tx.addInput( fromHex(signed.transaction).getHash(), fromHex(signed.transaction).outs.findIndex(n => n.value === tokens) ); // Make an output to pay back into the wallet const chainOutput = toOutputScript( (await createChainAddress({lnd})).address, networks.regtest ); // Add output to the pay back transaction tx.addOutput(chainOutput, smallTokens); const {signatures} = await signTransaction({ lnd, inputs: [{ key_family: 805, key_index: topLevelKey.index, output_script: output.script, output_tokens: tokens, root_hash: hash, sighash: Transaction.SIGHASH_DEFAULT, vin: 0, }], transaction: tx.toHex(), }); const [signature] = signatures.map(hexAsBuffer); // Add the signature to the input tx.ins.forEach((input, i) => tx.setWitness(i, [signature])); await broadcastChainTransaction({lnd, transaction: tx.toHex()}); await asyncRetry({interval, times}, async () => { await generate({}); const {utxos} = await getUtxos({lnd}); const utxo = utxos.find(n => n.transaction_id === tx.getId()); if (!utxo || !utxo.confirmation_count) { throw new Error('ExpectedReceivedTaprootSpend'); } }); } catch (err) { console.log("ERR", err); await kill({}); equal(err, null, 'Expected no error'); } // A Taproot script can be funded and spent with bip86 internal key try { await generate({count}); const topLevelKey = await getPublicKey({lnd, family: 805}); const output = v1OutputScript({ internal_key: topLevelKey.public_key, }); const [utxo] = (await getUtxos({lnd})).utxos.reverse(); // Make a PSBT paying to the Taproot output const {psbt} = createPsbt({ outputs: [{tokens, script: output.script}], utxos: [{id: utxo.transaction_id, vout: utxo.transaction_vout}], }); // Sign the PSBT const signed = await signPsbt({ lnd, psbt: (await fundPsbt({lnd, psbt})).psbt, }); // Send the tx to the chain await broadcastChainTransaction({lnd, transaction: signed.transaction}); // Make a new tx that will spend the output back into the wallet const tx = new Transaction(); // The new tx spends the Taproot output tx.addInput( fromHex(signed.transaction).getHash(), fromHex(signed.transaction).outs.findIndex(n => n.value === tokens) ); // Make an output to pay back into the wallet const chainOutput = toOutputScript( (await createChainAddress({lnd})).address, networks.regtest ); // Add output to the pay back transaction tx.addOutput(chainOutput, smallTokens); const {signatures} = await signTransaction({ lnd, inputs: [{ key_family: 805, key_index: topLevelKey.index, output_script: output.script, output_tokens: tokens, sighash: Transaction.SIGHASH_DEFAULT, vin: 0, }], transaction: tx.toHex(), }); const [signature] = signatures.map(hexAsBuffer); // Add the signature to the input tx.ins.forEach((input, i) => tx.setWitness(i, [signature])); await broadcastChainTransaction({lnd, transaction: tx.toHex()}); await asyncRetry({interval, times}, async () => { await generate({}); const {utxos} = await getUtxos({lnd}); const utxo = utxos.find(n => n.transaction_id === tx.getId()); if (!utxo || !utxo.confirmation_count) { throw new Error('ExpectedReceivedTaprootSpend'); } }); } catch (err) { equal(err, null, 'Expected no error'); } await kill({}); return; });