UNPKG

paid-services

Version:
440 lines (384 loc) 15.2 kB
const {address} = require('bitcoinjs-lib'); const asyncAuto = require('async/auto'); const asyncEach = require('async/each'); const {closeChannel} = require('ln-service'); const {connectPeer} = require('ln-sync'); const {decodeChanId} = require('bolt07'); const {getChainFeeRate} = require('ln-service'); const {getChainTransactions} = require('ln-service'); const {getHeight} = require('ln-service'); const {getPendingChannels} = require('ln-service'); const {networks} = require('bitcoinjs-lib'); const {openChannels} = require('ln-service'); const {returnResult} = require('asyncjs-util'); const {Transaction} = require('bitcoinjs-lib'); const assembleReplacementTx = require('./assemble_replacement_tx'); const feeForReplacement = require('./fee_for_replacement'); const signCapacityReplacement = require('./sign_capacity_replacement'); const blocksBuffer = 2e3; const bufferAsHex = buffer => buffer.toString('hex'); const defaultIncrease = 0; const {fromHex} = Transaction; const fuzzBlocks = 10; const highValue = 3000; const {isArray} = Array; const maxCommitmentOutputs = 4; const maxHighValueOutputs = 2; const notFoundIndex = -1; const positive = n => Math.max(1, n); const slowTarget = 1000; const sumOf = arr => arr.reduce((sum, n) => sum + n, 0); const {toOutputScript} = address; const txIdAsHash = id => Buffer.from(id, 'hex').reverse(); /** Close a channel and get a capacity replacement transaction { [add_funds_transaction_id]: <Adding Funds Input Tx Id Hex Encoded String> [add_funds_transaction_vout]: <Adding Funds Output Index Number> bitcoinjs_network: <BitcoinJs Network Name String> decrease: [{ address: <Spend to Chain Address String> [node]: <Node to Open Channel to Public Key Hex String> [output]: <Spend to Output Script Hex String> tokens: <Spend Value Number> }] id: <Original Channel Standard Format Channel Id String> [increase]: <Increase Channel Size Tokens Number> [is_private]: <Original Channel Is Private Bool> lnd: <Authenticated LND API Object> open_lnd: <Open Channel with LND API Object> transaction_id: <Original Funding Transaction Id Hex String> transaction_vout: <Original Funding Output Index Number> } @returns via cbk or Promise { [add_funds_vin]: <Funding Spend Input Index Number> capacity: <Replacement Channel Capacity Tokens Number> force_close_tx: <Raw Force Close Transaction Hex String> open_pending_ids: [<Proposed Additional Channel Id String>] pending_channel_id: <Proposed Replacement Channel Pending Id Hex String> remote_balance: <Initial Balance for Peer Tokens Number> signature: <Signature for Replacement Transaction Hex Encoded String> signing_key: <Signing Public Key Hex String> transaction_id: <Replacement Channel Transaction Id Hex String> transaction_vin: <Original Channel Input Index Number> transaction_vout: <Replacement Channel Transaction Output Index Number> unsigned_transaction: <Unsigned Transaction Hex Encoded String> witness_script: <Original Channel Witness Script Hex String> } */ module.exports = (args, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!args.bitcoinjs_network) { return cbk([400, 'ExpectedNetworkNameToGetCapacityReplacement']); } if (!isArray(args.decrease)) { return cbk([400, 'ExpectedDecreaseArrayToGetCapacityReplacement']); } if (!args.lnd) { return cbk([400, 'ExpectedLndApiToGetCapacityReplacement']); } if (!args.open_lnd) { return cbk([400, 'ExpectedLndToUseToOpenReplacementChannel']); } if (!args.transaction_id) { return cbk([400, 'ExpectedFundingTxIdToGetCapacityReplacement']); } if (args.transaction_vout === undefined) { return cbk([400, 'ExpectedFundingOutIndexToGetCapacityReplacement']); } return cbk(); }, // Get the chain fee rate for a slow confirmation to calculate fee delta getFeeRate: ['validate', ({}, cbk) => { return getChainFeeRate({ confirmation_target: slowTarget, lnd: args.lnd, }, cbk); }], // Get the current chain height getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)], // Connect to nodes to open channels to connect: ['validate', ({}, cbk) => { return asyncEach(args.decrease.filter(n => !!n.node), (spend, cbk) => { return connectPeer({id: spend.node, lnd: args.lnd}, cbk); }, cbk); }], // Propose any additional channels to nodes openChannels: ['connect', ({}, cbk) => { // Exit early when there are no nodes to propose channels to if (!args.decrease.filter(n => !!n.node).length) { return cbk(null, {pending: []}); } return openChannels({ channels: args.decrease.filter(n => !!n.node).map(decrease => ({ capacity: decrease.tokens, partner_public_key: decrease.node, })), is_avoiding_broadcast: true, lnd: args.lnd, }, cbk); }], // Force close the channel in order to get a non-final sequence number channelClose: ['getFeeRate','openChannels', ({}, cbk) => { return closeChannel({ lnd: args.lnd, is_force_close: true, transaction_id: args.transaction_id, transaction_vout: args.transaction_vout, }, cbk); }], // Get the pending channels getPending: ['channelClose', ({}, cbk) => { return getPendingChannels({lnd: args.lnd}, cbk); }], // Find the pending closing channel pendingChannel: ['getPending', ({getPending}, cbk) => { const channel = getPending.pending_channels.find(chan => { if (chan.transaction_id !== args.transaction_id) { return false; } return chan.transaction_vout === args.transaction_vout; }); if (!channel) { return cbk([503, 'FailedToFindPendingCloseChannelForReplacement']); } if (!!channel.pending_payments && !!channel.pending_payments.length) { return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']); } return cbk(null, { capacity: channel.capacity, partner_public_key: channel.partner_public_key, remote_balance: channel.remote_balance, }); }], // Get the chain transactions to pull out the raw close tx getUpdatedTx: ['channelClose', 'getHeight', ({getHeight}, cbk) => { return getChainTransactions({ after: getHeight.current_block_height - fuzzBlocks, lnd: args.lnd, }, cbk); }], // Find the force close transaction closeTx: [ 'channelClose', 'getUpdatedTx', 'pendingChannel', ({channelClose, getUpdatedTx, pendingChannel}, cbk) => { const closeId = channelClose.transaction_id; const id = args.transaction_id; const {capacity} = pendingChannel; const tx = getUpdatedTx.transactions.find(n => n.id === closeId); if (!tx || !tx.transaction) { return cbk([503, 'ExpectedCloseTransactionInTransactionsList']); } const commitment = fromHex(tx.transaction); // Make sure there aren't unexpectedly too-many outputs if (commitment.outs.length > maxCommitmentOutputs) { return cbk([503, 'ExpectedFewerOutputsOnCommitmentTransaction']); } const highValueOuts = commitment.outs.filter(n => n.value > highValue); // There shouldn't be many high value outputs if (highValueOuts.length > maxHighValueOutputs) { return cbk([500, 'ExpectedFewerHighValueOutputsOnCommitTx']); } // Input details for the commitment transaction const [{hash, index, witness}, other] = commitment.ins; // The commitment transaction should be spending the funding outpoint if (!hash.equals(txIdAsHash(id)) || index !== args.transaction_vout) { return cbk([503, 'ExpectedSpendingOpenTransactionOutputIndex']); } // There should be no other inputs on the closing tx if (!!other) { return cbk([503, 'ExpectedSingleInputForCloseTransaction']); } // The spending script is the final element in the witness const [fundingScript] = witness.slice().reverse(); // The difference between commitment outputs and its input is the fee const fee = capacity - sumOf(commitment.outs.map(n => n.value)); return cbk(null, { fee, id: commitment.getId(), script: bufferAsHex(fundingScript), transaction: tx.transaction, weight: commitment.weight(), }); }], // Find the open transaction openTx: ['getUpdatedTx', ({getUpdatedTx}, cbk) => { const funding = fromHex(args.open_transaction); const id = args.transaction_id; if (!funding.outs[args.transaction_vout]) { return cbk([503, 'ExpectedTransactionOutputOnFundingTransaction']); } const capacity = funding.outs[args.transaction_vout].value; const output = bufferAsHex(funding.outs[args.transaction_vout].script); return cbk(null, {capacity, output}); }], // Calculate the new channel's capacity newCapacity: [ 'closeTx', 'getFeeRate', 'pendingChannel', ({closeTx, getFeeRate, pendingChannel}, cbk) => { // The fee for the replacement tx must meet the relay rate minimum const {fee} = feeForReplacement({ capacity: pendingChannel.capacity, commit_transaction_fee: closeTx.fee, commit_transaction_weight: closeTx.weight, decrease: args.decrease, increase: args.increase, tokens_per_vbyte: getFeeRate.tokens_per_vbyte, }); // The new capacity is capacity + increase - decreases - tx fee const newCapacity = sumOf([ pendingChannel.capacity, args.increase || defaultIncrease, -sumOf(args.decrease.map(n => n.tokens)), -fee, ]); return cbk(null, newCapacity); }], // Make sure that we are still connected to the original peer confirmConnection: [ 'newCapacity', 'pendingChannel', ({pendingChannel}, cbk) => { return connectPeer({ id: pendingChannel.partner_public_key, lnd: args.open_lnd, }, cbk); }], // Propose the new channel to replace the existing one proposeChannel: [ 'confirmConnection', 'newCapacity', 'pendingChannel', ({newCapacity, pendingChannel}, cbk) => { return openChannels({ channels: [{ capacity: newCapacity, give_tokens: pendingChannel.remote_balance, is_private: args.is_private, partner_public_key: pendingChannel.partner_public_key, }], is_avoiding_broadcast: true, lnd: args.open_lnd, }, cbk); }], // Put together the unsigned replacement transaction replacementTx: [ 'closeTx', 'newCapacity', 'openChannels', 'proposeChannel', ({closeTx, newCapacity, openChannels, proposeChannel}, cbk) => { // The replacement tx pays to the proposed channel multisig address const [{address}] = proposeChannel.pending; const network = networks[args.bitcoinjs_network]; // Map pending channel opens to match the spends const openAsSpends = openChannels.pending.map(({address, tokens}) => ({ tokens, output: bufferAsHex(toOutputScript(address, network)), })); // Decreases are spends to regular addresses plus channel outputs const decreases = [].concat(args.decrease).concat(openAsSpends); try { const replacement = assembleReplacementTx({ add_funds_transaction_id: args.add_funds_transaction_id, add_funds_transaction_vout: args.add_funds_transaction_vout, bitcoinjs_network: args.bitcoinjs_network, close_transaction: closeTx.transaction, decrease: decreases.filter(n => !!n.output).map(decrease => ({ output: decrease.output, tokens: decrease.tokens, })), funding_address: address, funding_tokens: newCapacity, transaction_id: args.transaction_id, transaction_vout: args.transaction_vout, }); return cbk(null, { add_funds_vin: replacement.add_funds_vin, transaction: replacement.transaction, transaction_vin: replacement.transaction_vin, transaction_id: replacement.transaction_id, transaction_vout: replacement.transaction_vout, }); } catch (err) { return cbk([503, err.message]); } }], // Partially sign the replacement transaction using the 2:2 key signReplacement: [ 'closeTx', 'openTx', 'pendingChannel', 'replacementTx', ({closeTx, openTx, pendingChannel, replacementTx}, cbk) => { return signCapacityReplacement({ capacity: openTx.capacity, lnd: args.lnd, output: openTx.output, script: closeTx.script, transaction: replacementTx.transaction, vin: replacementTx.transaction_vin, }, cbk); }], // Details of proposed replacement proposedReplacement: [ 'closeTx', 'newCapacity', 'openChannels', 'pendingChannel', 'proposeChannel', 'replacementTx', 'signReplacement', ({ closeTx, newCapacity, openChannels, pendingChannel, proposeChannel, replacementTx, signReplacement, }, cbk) => { const [pending] = proposeChannel.pending; return cbk(null, { add_funds_vin: replacementTx.add_funds_vin, capacity: newCapacity, force_close_tx: closeTx.transaction, open_pending_ids: openChannels.pending.map(n => n.id), pending_channel_id: pending.id, remote_balance: pendingChannel.remote_balance, signature: signReplacement.signature, signing_key: signReplacement.key, transaction_id: replacementTx.transaction_id, transaction_vin: replacementTx.transaction_vin, transaction_vout: replacementTx.transaction_vout, unsigned_transaction: replacementTx.transaction, witness_script: closeTx.script, }); }], }, returnResult({reject, resolve, of: 'proposedReplacement'}, cbk)); }); };