UNPKG

ln-service

Version:

Interaction helper for your Lightning Network daemon

444 lines (350 loc) 12.8 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 {leafHash} = require('p2tr'); const {networks} = require('bitcoinjs-lib'); const {scriptElementsAsScript} = require('@alexbosworth/blockchain'); const {spawnLightningCluster} = require('ln-docker-daemons'); const {Transaction} = require('bitcoinjs-lib'); const {v1OutputScript} = require('p2tr'); const {beginGroupSigningSession} = require('./../../'); const {broadcastChainTransaction} = require('./../../'); const {createChainAddress} = require('./../../'); const {endGroupSigningSession} = require('./../../'); const {fundPsbt} = require('./../../'); const {getPublicKey} = require('./../../'); const {getUtxos} = require('./../../'); const {signPsbt} = require('./../../'); const {updateGroupSigningSession} = require('./../../'); const compile = elements => scriptElementsAsScript({elements}).script; const count = 100; const defaultInternalKey = '0350929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; const {from} = Buffer; const {fromHex} = Transaction; const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const interval = 100; const OP_CHECKSIG = 172; const size = 2; const smallTokens = 2e5; const times = 20; const {toOutputScript} = address; const tokens = 1e6; // Starting a group signing session should result in a new MuSig2 session test(`Begin group signing session`, async () => { const {kill, nodes} = await spawnLightningCluster({size}); const [{generate, lnd}, target] = 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, group signing is not supported if (err.slice().shift() === 501) { await kill({}); return; } throw err; } // A Taproot script can be funded and spent with MuSig2 try { await generate({count}); const controlKey = await getPublicKey({lnd, family: 0}); const targetKey = await getPublicKey({family: 0, lnd: target.lnd}); const controlGroup = await beginGroupSigningSession({ lnd, is_key_spend: true, key_family: 0, key_index: controlKey.index, public_keys: [targetKey.public_key], }); const targetGroup = await beginGroupSigningSession({ is_key_spend: true, key_family: 0, key_index: targetKey.index, lnd: target.lnd, public_keys: [controlKey.public_key], }); equal(controlGroup.external_key, targetGroup.external_key, 'Equal e-keys'); equal(controlGroup.internal_key, targetGroup.internal_key, 'Equal i-keys'); const script = Buffer.concat([ Buffer.from([81]), Buffer.from([32]), hexAsBuffer(controlGroup.external_key), ]); const [utxo] = (await getUtxos({lnd})).utxos.reverse(); // Make a PSBT paying to the Taproot output const {psbt} = createPsbt({ outputs: [{tokens, script: script.toString('hex')}], 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 [hashToSign] = tx.ins.map((input, i) => { return tx.hashForWitnessV1( i, [script], [tokens], Transaction.SIGHASH_DEFAULT, ); }); const controlSign = await updateGroupSigningSession({ lnd, hash: hashToSign.toString('hex'), id: controlGroup.id, nonces: [targetGroup.nonce], }); const targetSign = await updateGroupSigningSession({ hash: hashToSign.toString('hex'), id: targetGroup.id, lnd: target.lnd, nonces: [controlGroup.nonce], }); await endGroupSigningSession({lnd: target.lnd, id: targetGroup.id}); const {signature} = await endGroupSigningSession({ lnd, id: controlGroup.id, signatures: [targetSign.signature], }); // Add the signature to the input tx.ins.forEach((input, i) => tx.setWitness(i, [hexAsBuffer(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'); } // A Taproot script can be funded and spent with MuSig2 for a script output try { await generate({count}); const controlKey = await getPublicKey({lnd, family: 0}); const targetKey = await getPublicKey({family: 0, lnd: target.lnd}); const unusedKey = (await getPublicKey({lnd, family: 805})).public_key; const xOnlyUnused = hexAsBuffer(unusedKey.slice(2)); const witnessScript = compile([xOnlyUnused, OP_CHECKSIG]); const branches = [{script: witnessScript}]; const {hash} = hashForTree({branches}); const controlGroup = await beginGroupSigningSession({ lnd, key_family: 0, key_index: controlKey.index, public_keys: [targetKey.public_key], root_hash: hash, }); const targetGroup = await beginGroupSigningSession({ key_family: 0, key_index: targetKey.index, lnd: target.lnd, public_keys: [controlKey.public_key], root_hash: hash, }); equal(controlGroup.external_key, targetGroup.external_key, 'Equal e-keys'); equal(controlGroup.internal_key, targetGroup.internal_key, 'Equal i-keys'); const script = Buffer.concat([ from([81]), from([32]), hexAsBuffer(controlGroup.external_key), ]); const [utxo] = (await getUtxos({lnd})).utxos.reverse(); // Make a PSBT paying to the Taproot output const {psbt} = createPsbt({ outputs: [{tokens, script: script.toString('hex')}], 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 [hashToSign] = tx.ins.map((input, i) => { return tx.hashForWitnessV1( i, [script], [tokens], Transaction.SIGHASH_DEFAULT, ); }); const controlSign = await updateGroupSigningSession({ lnd, hash: hashToSign.toString('hex'), id: controlGroup.id, nonces: [targetGroup.nonce], }); const targetSign = await updateGroupSigningSession({ hash: hashToSign.toString('hex'), id: targetGroup.id, lnd: target.lnd, nonces: [controlGroup.nonce], }); await endGroupSigningSession({lnd: target.lnd, id: targetGroup.id}); const {signature} = await endGroupSigningSession({ lnd, id: controlGroup.id, signatures: [targetSign.signature], }); // Add the signature to the input tx.ins.forEach((input, i) => tx.setWitness(i, [hexAsBuffer(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'); } // A Taproot script can be funded and spent on the script path try { await generate({count}); const controlKey = await getPublicKey({lnd, family: 0}); const targetKey = await getPublicKey({family: 0, lnd: target.lnd}); const controlGroup = await beginGroupSigningSession({ lnd, key_family: 0, key_index: controlKey.index, public_keys: [targetKey.public_key], }); const targetGroup = await beginGroupSigningSession({ key_family: 0, key_index: targetKey.index, lnd: target.lnd, public_keys: [controlKey.public_key], }); const scriptKey = hexAsBuffer(controlGroup.external_key); const witnessScript = compile([scriptKey, 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 [hashToSign] = tx.ins.map((input, i) => { return tx.hashForWitnessV1( i, [hexAsBuffer(output.script)], [tokens], Transaction.SIGHASH_DEFAULT, hexAsBuffer(leafHash({script: witnessScript}).hash), ); }); const controlSign = await updateGroupSigningSession({ lnd, hash: hashToSign.toString('hex'), id: controlGroup.id, nonces: [targetGroup.nonce], }); const targetSign = await updateGroupSigningSession({ hash: hashToSign.toString('hex'), id: targetGroup.id, lnd: target.lnd, nonces: [controlGroup.nonce], }); await endGroupSigningSession({lnd: target.lnd, id: targetGroup.id}); const {signature} = await endGroupSigningSession({ lnd, id: controlGroup.id, signatures: [targetSign.signature], }); 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, [ hexAsBuffer(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'); } await kill({}); return; });