UNPKG

ln-service

Version:

Interaction helper for your Lightning Network daemon

442 lines (380 loc) 12.5 kB
const {join} = require('path'); const {readFile} = require('fs'); const {readFileSync} = require('fs'); const {spawn} = require('child_process'); const asyncAuto = require('async/auto'); const asyncMapSeries = require('async/mapSeries'); const asyncRetry = require('async/retry'); const {networks} = require('bitcoinjs-lib'); const openPortFinder = require('portfinder'); const tinysecp = require('tiny-secp256k1'); const {changePassword} = require('./../../'); const {createSeed} = require('./../../'); const {createWallet} = require('./../../'); const generateBlocks = require('./generate_blocks'); const {authenticatedLndGrpc} = require('./../../'); const spawnChainDaemon = require('./spawn_chain_daemon'); const {stopDaemon} = require('./../../'); const {unauthenticatedLndGrpc} = require('./../../'); const {unlockWallet} = require('./../../'); const adminMacaroonFileName = 'admin.macaroon'; const chainPass = '0k39BVOdg4uuS7qNCG2jbIXNpwU7d3Ft87PpHPPoCfk='; const chainRpcCertName = 'rpc.cert'; const chainUser = 'bitcoinrpc'; const interval = 100; const invoiceMacaroonFileName = 'invoice.macaroon'; const lightningDaemonExecFileName = 'lnd'; const lightningDaemonLogPath = 'logs/'; const lightningSeedPassphrase = 'passphrase'; const lightningTlsCertFileName = 'tls.cert'; const lightningTlsKeyFileName = 'tls.key'; const lightningWalletPassword = 'password'; const lndWalletUnlockerService = 'WalletUnlocker'; const localhost = '127.0.0.1'; const maxSpawnChainDaemonAttempts = 3; const readMacaroonFileName = 'readonly.macaroon'; const retryCreateSeedCount = 500; const startPortRange = 7593; const startWalletTimeoutMs = 4500; const times = 200; /** Run a change password test {} @returns via cbk { kill: <Stop Function> ({}, err => {}) lnd: <Authenticated LND gRPC API Object> } */ module.exports = ({network}, cbk) => { return asyncAuto({ // Import ECPair library ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp), // Find open ports for the listen, REST and RPC ports getPorts: cbk => { return asyncMapSeries(['listen', 'rest', 'rpc'], (_, cbk) => { const port = startPortRange + Math.round(Math.random() * 2000); const stopPort = port + 20000; return setTimeout(() => { return openPortFinder.getPort({port, stopPort}, cbk); }, 50); }, (err, ports) => { if (!!err || !Array.isArray(ports) || !ports.length) { return cbk([500, 'FailedToFindOpenPorts', err]); } const [listen, rest, rpc] = ports; return cbk(null, {listen, rest, rpc}); }); }, // Make a private key for mining rewards miningKey: ['ecp', ({ecp}, cbk) => { const keyPair = ecp.makeRandom({network: networks.testnet}); return cbk(null, { private_key: keyPair.toWIF(), public_key: keyPair.publicKey.toString('hex'), }); }], // Spawn a backing chain daemon for lnd spawnChainDaemon: ['miningKey', ({miningKey}, cbk) => { return asyncRetry(maxSpawnChainDaemonAttempts, cbk => { return spawnChainDaemon({ daemon: 'btcd', is_tls: true, mining_public_key: miningKey.public_key, }, cbk); }, cbk); }], // Get the chain daemon cert getChainDaemonCert: ['spawnChainDaemon', ({spawnChainDaemon}, cbk) => { return asyncRetry({interval, times}, cbk => { return readFile(spawnChainDaemon.rpc_cert, (err, data) => { if (!!err) { return cbk([503, 'FailedToGetChainDaemonRpcCert', {err}]); } return cbk(null, data); }); }, cbk); }], // Generate a block to prevent lnd from getting stuck generateBlock: [ 'getChainDaemonCert', 'miningKey', 'spawnChainDaemon', ({getChainDaemonCert, miningKey, spawnChainDaemon}, cbk) => { return asyncRetry({interval, times}, cbk => { try { return generateBlocks({ cert: getChainDaemonCert, count: 1, host: localhost, key: miningKey.public_key, pass: chainPass, port: spawnChainDaemon.rpc_port, user: chainUser, }, cbk); } catch (err) { return cbk([503, 'FailedToGenerateBlockWhenSpawningLnd', {err}]); } }, cbk); }], // Spawn LND spawnLightningDaemon: [ 'generateBlock', 'getPorts', 'spawnChainDaemon', ({getPorts, spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const daemon = spawn(lightningDaemonExecFileName, [ '--adminmacaroonpath', join(dir, adminMacaroonFileName), '--bitcoin.active', '--bitcoin.chaindir', dir, '--bitcoin.node', 'btcd', '--bitcoin.regtest', '--btcd.dir', dir, '--btcd.rpccert', join(dir, chainRpcCertName), '--btcd.rpchost', `${localhost}:${spawnChainDaemon.rpc_port}`, '--btcd.rpcpass', chainPass, '--btcd.rpcuser', chainUser, '--datadir', dir, '--debuglevel', 'trace', '--externalip', `${localhost}:${getPorts.listen}`, '--invoicemacaroonpath', join(dir, invoiceMacaroonFileName), '--listen', `${localhost}:${getPorts.listen}`, '--logdir', join(dir, lightningDaemonLogPath), '--nobootstrap', '--readonlymacaroonpath', join(dir, readMacaroonFileName), '--restlisten', `${localhost}:${getPorts.rest}`, '--rpclisten', `${localhost}:${getPorts.rpc}`, '--tlscertpath', join(dir, lightningTlsCertFileName), '--tlskeypath', join(dir, lightningTlsKeyFileName), ]); daemon.stderr.on('data', data => {}); let isReady = false; daemon.stdout.on('data', data => { if (!isReady && /gRPC.proxy.started/.test(data+'')) { isReady = true; return cbk(); }; return; }); return; }], // Get connection to the no-wallet lnd nonAuthenticatedLnd: [ 'getPorts', 'spawnChainDaemon', 'spawnLightningDaemon', ({getPorts, spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const cert = readFileSync(join(dir, lightningTlsCertFileName)); try { return cbk(null, unauthenticatedLndGrpc({ cert: cert.toString('base64'), socket: `localhost:${getPorts.rpc}`, }).lnd); } catch (err) { return cbk([503, 'FailedToInstantiateNonAuthenticatedLnd', {err}]); } }], // Create seed createSeed: [ 'getPorts', 'nonAuthenticatedLnd', 'spawnChainDaemon', ({getPorts, nonAuthenticatedLnd, spawnChainDaemon}, cbk) => { return asyncRetry({interval, times: retryCreateSeedCount}, cbk => { const {dir} = spawnChainDaemon; const cert = readFileSync(join(dir, lightningTlsCertFileName)); const {lnd} = unauthenticatedLndGrpc({ cert: cert.toString('base64'), socket: `localhost:${getPorts.rpc}`, }); return createSeed({lnd, passphrase: lightningSeedPassphrase}, cbk); }, cbk); }], // Create wallet createWallet: [ 'createSeed', 'nonAuthenticatedLnd', ({createSeed, nonAuthenticatedLnd}, cbk) => { return createWallet({ lnd: nonAuthenticatedLnd, passphrase: lightningSeedPassphrase, password: lightningWalletPassword, seed: createSeed.seed, }, err => { if (!!err) { return cbk(err); } return cbk(); }); }], // Get admin macaroon macaroon: [ 'createWallet', 'spawnChainDaemon', ({spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const interval = retryCount => 50 * Math.pow(2, retryCount); const times = 15; const macaroonPath = join(dir, adminMacaroonFileName); return asyncRetry({interval, times}, cbk => { try { return cbk(null, readFileSync(macaroonPath).toString('base64')); } catch (err) { return cbk([503, 'FailedToGetAdminMacaroon', err]); } }, cbk); }], // Wallet details wallet: [ 'macaroon', 'spawnChainDaemon', 'getPorts', ({getPorts, macaroon, spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const certPath = join(dir, lightningTlsCertFileName); return cbk(null, { macaroon, cert: readFileSync(certPath).toString('base64'), host: `localhost:${getPorts.rpc}`, }); }], // Wallet LND GRPC API lnd: ['wallet', ({wallet}, cbk) => { try { return cbk(null, authenticatedLndGrpc({ cert: wallet.cert, macaroon: wallet.macaroon, socket: wallet.host, }).lnd); } catch (err) { return cbk([503, 'FailedToInstantiateWalletLnd', err]); } }], // Stop LND stopLnd: ['lnd', async ({lnd}) => { const interval = 200; const times = 15; return await asyncRetry({interval, times}, async () => { return await stopDaemon({lnd}); }); }], // Restart LND (locked) restartLnd: [ 'getPorts', 'spawnChainDaemon', 'stopLnd', ({getPorts, spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const daemon = spawn(lightningDaemonExecFileName, [ '--adminmacaroonpath', join(dir, adminMacaroonFileName), '--bitcoin.active', '--bitcoin.chaindir', dir, '--bitcoin.node', 'btcd', '--bitcoin.regtest', '--btcd.dir', dir, '--btcd.rpccert', join(dir, chainRpcCertName), '--btcd.rpchost', `${localhost}:${spawnChainDaemon.rpc_port}`, '--btcd.rpcpass', chainPass, '--btcd.rpcuser', chainUser, '--datadir', dir, '--debuglevel', 'trace', '--externalip', `${localhost}:${getPorts.listen}`, '--invoicemacaroonpath', join(dir, invoiceMacaroonFileName), '--listen', `${localhost}:${getPorts.listen}`, '--logdir', join(dir, lightningDaemonLogPath), '--nobootstrap', '--readonlymacaroonpath', join(dir, readMacaroonFileName), '--restlisten', `${localhost}:${getPorts.rest}`, '--rpclisten', `${localhost}:${getPorts.rpc}`, '--tlscertpath', join(dir, lightningTlsCertFileName), '--tlskeypath', join(dir, lightningTlsKeyFileName), ]); daemon.stderr.on('data', data => {}); let isReady = false; daemon.stdout.on('data', data => { if (!isReady && /gRPC.proxy.started/.test(data+'')) { isReady = true; return cbk(null, {daemon}); }; return; }); return; }], // Get tls cert cert: [ 'restartLnd', 'spawnChainDaemon', ({spawnChainDaemon}, cbk) => { const {dir} = spawnChainDaemon; const times = 150; const certPath = join(dir, lightningTlsCertFileName); return asyncRetry({interval, times}, cbk => { try { return cbk(null, readFileSync(certPath).toString('base64')); } catch (err) { return cbk([503, 'FailedToGetCertAfterRestart', err]); } }, cbk); }], // Get locked restarted lnd restartedLnd: ['cert', 'getPorts', ({cert, getPorts}, cbk) => { try { return cbk(null, unauthenticatedLndGrpc({ cert, socket: `localhost:${getPorts.rpc}`, }).lnd); } catch (err) { return cbk([503, 'FailedToLaunchLightningDaemon', err]); } }], // Change password changePassword: ['restartedLnd', ({restartedLnd}, cbk) => { return asyncRetry({interval, times}, cbk => { return changePassword({ current_password: lightningWalletPassword, lnd: restartedLnd, new_password: 'changed_passphrase', }, cbk); }, cbk); }], }, (err, res) => { if (!!err) { return cbk(err); } const {lnd} = res; const kill = () => { res.spawnChainDaemon.daemon.kill(9); res.restartLnd.daemon.kill(9); return; }; process.on('uncaughtException', err => { kill(); setTimeout(() => process.exit(1), 1000); }); return cbk(null, {lnd, kill}); }); };