paid-services 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/capacity/accept_capacity_change.js +284 -0
- package/capacity/ask_for_change_details.js +505 -0
- package/capacity/ask_for_decrease.js +185 -0
- package/capacity/assemble_replacement_tx.js +171 -0
- package/capacity/capacity_change_requests.js +76 -0
- package/capacity/change_channel_capacity.js +183 -0
- package/capacity/encode_change_request.js +55 -0
- package/capacity/encode_sign_capacity_response.js +32 -0
- package/capacity/fee_for_replacement.js +108 -0
- package/capacity/finalize_capacity_replacement.js +67 -0
- package/capacity/get_capacity_change_requests.js +98 -0
- package/capacity/get_capacity_replacement.js +371 -0
- package/capacity/get_replacement_signature.js +232 -0
- package/capacity/index.js +4 -0
- package/capacity/initiate_capacity_change.js +506 -0
- package/capacity/interim_replacement_psbt.js +57 -0
- package/capacity/is_open_witness_script.js +40 -0
- package/capacity/parse_accept_request.js +58 -0
- package/capacity/parse_accept_response.js +52 -0
- package/capacity/parse_capacity_change_request.js +101 -0
- package/capacity/parse_sign_capacity_request.js +59 -0
- package/capacity/propose_capacity_change.js +361 -0
- package/capacity/service_types.json +6 -0
- package/capacity/sign_capacity_replacement.js +119 -0
- package/capacity/witness_script_from_close_txs.js +50 -0
- package/index.js +2 -0
- package/p2p/encode_peer_response.js +0 -1
- package/p2p/make_peer_request.js +3 -3
- package/package.json +6 -4
- package/test/capacity/test_assemble_replacement_tx.js +46 -0
- package/test/capacity/test_capacity_change_requests.js +54 -0
- package/test/capacity/test_encode_change_request.js +49 -0
- package/test/capacity/test_encode_sign_capacity_response.js +47 -0
- package/test/capacity/test_fee_for_replacement.js +40 -0
- package/test/capacity/test_finalize_capacity_replacement.js +45 -0
- package/test/capacity/test_interim_replacement_psbt.js +44 -0
- package/test/capacity/test_is_open_witness_script.js +40 -0
- package/test/capacity/test_parse_accept_request.js +42 -0
- package/test/capacity/test_parse_capacity_change_request.js +51 -0
- package/test/capacity/test_parse_sign_capacity_request.js +49 -0
- package/test/capacity/test_witness_script_from_close_txs.js +53 -0
- package/test/integration/test_accept_capacity_change.js +152 -0
- package/test/integration/test_change_channel_capacity.js +196 -0
- package/test/integration/test_decrease_channel_capacity.js +206 -0
- package/test/integration/test_increase_channel_capacity.js +197 -0
- package/test/integration/test_sign_capacity_replacement.js +142 -0
- package/test/p2p/test_encode_peer_request.js +53 -0
- package/test/p2p/test_encode_peer_response.js +38 -0
- package/test/p2p/test_encode_response_code.js +48 -0
- package/trades/network_from_network_record.js +6 -0
- package/trades/network_record_from_request.js +5 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getChannels} = require('ln-service');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
const {subscribeToPeerMessages} = require('ln-service');
|
|
5
|
+
|
|
6
|
+
const capacityChangeRequests = require('./capacity_change_requests');
|
|
7
|
+
const {serviceTypeChangeCapacity} = require('./service_types');
|
|
8
|
+
const parseCapacityChangeRequest = require('./parse_capacity_change_request');
|
|
9
|
+
const {servicePeerRequests} = require('./../p2p');
|
|
10
|
+
|
|
11
|
+
const defaultDelayMs = 1000 * 10;
|
|
12
|
+
const waitForRequestsTimeoutMs = 5000;
|
|
13
|
+
|
|
14
|
+
/** Get capacity change requests
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
[delay]: <Milliseconds To Wait For Change Requests Number>
|
|
18
|
+
lnd: <Authenticated LND API Object>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
requests: [{
|
|
24
|
+
[address]: <Cooperative Close Address String>
|
|
25
|
+
capacity: <Channel Capacity Tokens Number>
|
|
26
|
+
channel: <Standard Format Channel Id String>
|
|
27
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
28
|
+
from: <From Node Public Key Id Hex String>
|
|
29
|
+
id: <Change Request Id Hex String>
|
|
30
|
+
[increase]: <Add Capacity Tokens Number>
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
34
|
+
module.exports = ({delay, lnd}, cbk) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
return asyncAuto({
|
|
37
|
+
// Check arguments
|
|
38
|
+
validate: cbk => {
|
|
39
|
+
if (!lnd) {
|
|
40
|
+
return cbk([400, 'ExpectedLndToGetCapacityChangeRequests']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return cbk();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Wait for incoming capacity change requests
|
|
47
|
+
getRequests: ['validate', ({}, cbk) => {
|
|
48
|
+
const requests = [];
|
|
49
|
+
const service = servicePeerRequests({lnd});
|
|
50
|
+
|
|
51
|
+
// Listen for capacity change requests
|
|
52
|
+
service.request({type: serviceTypeChangeCapacity}, (req, res) => {
|
|
53
|
+
const {from, records} = req;
|
|
54
|
+
|
|
55
|
+
const {request} = parseCapacityChangeRequest({from, records});
|
|
56
|
+
|
|
57
|
+
// Exit early when the change request message was not understood
|
|
58
|
+
if (!request) {
|
|
59
|
+
return res.failure([400, 'FailedToParseChangeRequest']);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only add new requests
|
|
63
|
+
if (!requests.find(n => n.id === request.id)) {
|
|
64
|
+
requests.push(request);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return res.success({});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return setTimeout(() => {
|
|
71
|
+
// Stop listening for requests after a little while
|
|
72
|
+
service.stop({});
|
|
73
|
+
|
|
74
|
+
return cbk(null, requests);
|
|
75
|
+
},
|
|
76
|
+
delay || waitForRequestsTimeoutMs);
|
|
77
|
+
}],
|
|
78
|
+
|
|
79
|
+
// Get the set of channels
|
|
80
|
+
getChannels: ['getRequests', ({}, cbk) => getChannels({lnd}, cbk)],
|
|
81
|
+
|
|
82
|
+
// Final set of capacity change requests
|
|
83
|
+
requests: [
|
|
84
|
+
'getChannels',
|
|
85
|
+
'getRequests',
|
|
86
|
+
({getChannels, getRequests}, cbk) =>
|
|
87
|
+
{
|
|
88
|
+
const {requests} = capacityChangeRequests({
|
|
89
|
+
channels: getChannels.channels,
|
|
90
|
+
requests: getRequests,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return cbk(null, {requests});
|
|
94
|
+
}],
|
|
95
|
+
},
|
|
96
|
+
returnResult({reject, resolve, of: 'requests'}, cbk));
|
|
97
|
+
});
|
|
98
|
+
};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {closeChannel} = require('ln-service');
|
|
3
|
+
const {decodeChanId} = require('bolt07');
|
|
4
|
+
const {getChainFeeRate} = require('ln-service');
|
|
5
|
+
const {getChainTransactions} = require('ln-service');
|
|
6
|
+
const {getHeight} = require('ln-service');
|
|
7
|
+
const {getPendingChannels} = require('ln-service');
|
|
8
|
+
const {openChannels} = require('ln-service');
|
|
9
|
+
const {returnResult} = require('asyncjs-util');
|
|
10
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
11
|
+
|
|
12
|
+
const assembleReplacementTx = require('./assemble_replacement_tx');
|
|
13
|
+
const feeForReplacement = require('./fee_for_replacement');
|
|
14
|
+
const signCapacityReplacement = require('./sign_capacity_replacement');
|
|
15
|
+
|
|
16
|
+
const blocksBuffer = 2e3;
|
|
17
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
18
|
+
const defaultIncrease = 0;
|
|
19
|
+
const {fromHex} = Transaction;
|
|
20
|
+
const fuzzBlocks = 10;
|
|
21
|
+
const highValue = 3000;
|
|
22
|
+
const {isArray} = Array;
|
|
23
|
+
const maxCommitmentOutputs = 4;
|
|
24
|
+
const maxHighValueOutputs = 2;
|
|
25
|
+
const notFoundIndex = -1;
|
|
26
|
+
const positive = n => Math.max(1, n);
|
|
27
|
+
const slowTarget = 1000;
|
|
28
|
+
const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
|
|
29
|
+
const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
|
|
30
|
+
|
|
31
|
+
/** Close a channel and get a capacity replacement transaction
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
[add_funds_transaction_id]: <Adding Funds Input Tx Id Hex Encoded String>
|
|
35
|
+
[add_funds_transaction_vout]: <Adding Funds Output Index Number>
|
|
36
|
+
bitcoinjs_network: <BitcoinJs Network Name String>
|
|
37
|
+
decrease: [{
|
|
38
|
+
address: <Spend to Chain Address String>
|
|
39
|
+
output: <Spend to Output Script Hex String>
|
|
40
|
+
tokens: <Spend Value Number>
|
|
41
|
+
}]
|
|
42
|
+
id: <Original Channel Standard Format Channel Id String>
|
|
43
|
+
[increase]: <Increase Channel Size Tokens Number>
|
|
44
|
+
[is_private]: <Original Channel Is Private Bool>
|
|
45
|
+
lnd: <Authenticated LND API Object>
|
|
46
|
+
transaction_id: <Original Funding Transaction Id Hex String>
|
|
47
|
+
transaction_vout: <Original Funding Output Index Number>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@returns via cbk or Promise
|
|
51
|
+
{
|
|
52
|
+
[add_funds_vin]: <Funding Spend Input Index Number>
|
|
53
|
+
capacity: <Replacement Channel Capacity Tokens Number>
|
|
54
|
+
remote_balance: <Initial Balance for Peer Tokens Number>
|
|
55
|
+
signature: <Signature for Replacement Transaction Hex Encoded String>
|
|
56
|
+
signing_key: <Signing Public Key Hex String>
|
|
57
|
+
pending_channel_id: <Proposed Channel Pending Id Hex String>
|
|
58
|
+
transaction_id: <Replacement Channel Transaction Id Hex String>
|
|
59
|
+
transaction_vin: <Original Channel Input Index Number>
|
|
60
|
+
transaction_vout: <Replacement Channel Transaction Output Index Number>
|
|
61
|
+
unsigned_transaction: <Unsigned Transaction Hex Encoded String>
|
|
62
|
+
witness_script: <Original Channel Witness Script Hex String>
|
|
63
|
+
}
|
|
64
|
+
*/
|
|
65
|
+
module.exports = (args, cbk) => {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
return asyncAuto({
|
|
68
|
+
// Check arguments
|
|
69
|
+
validate: cbk => {
|
|
70
|
+
if (!args.bitcoinjs_network) {
|
|
71
|
+
return cbk([400, 'ExpectedNetworkNameToGetCapacityReplacement']);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isArray(args.decrease)) {
|
|
75
|
+
return cbk([400, 'ExpectedDecreaseArrayToGetCapacityReplacement']);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!args.lnd) {
|
|
79
|
+
return cbk([400, 'ExpectedLndApiToGetCapacityReplacement']);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!args.transaction_id) {
|
|
83
|
+
return cbk([400, 'ExpectedFundingTxIdToGetCapacityReplacement']);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (args.transaction_vout === undefined) {
|
|
87
|
+
return cbk([400, 'ExpectedFundingOutIndexToGetCapacityReplacement']);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return cbk();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Get the chain fee rate for a slow confirmation to calculate fee delta
|
|
94
|
+
getFeeRate: ['validate', ({}, cbk) => {
|
|
95
|
+
return getChainFeeRate({
|
|
96
|
+
confirmation_target: slowTarget,
|
|
97
|
+
lnd: args.lnd,
|
|
98
|
+
},
|
|
99
|
+
cbk);
|
|
100
|
+
}],
|
|
101
|
+
|
|
102
|
+
// Get the current chain height
|
|
103
|
+
getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
|
|
104
|
+
|
|
105
|
+
// Force close the channel in order to get a non-final sequence number
|
|
106
|
+
channelClose: ['getFeeRate', ({}, cbk) => {
|
|
107
|
+
return closeChannel({
|
|
108
|
+
lnd: args.lnd,
|
|
109
|
+
is_force_close: true,
|
|
110
|
+
transaction_id: args.transaction_id,
|
|
111
|
+
transaction_vout: args.transaction_vout,
|
|
112
|
+
},
|
|
113
|
+
cbk);
|
|
114
|
+
}],
|
|
115
|
+
|
|
116
|
+
// Get the pending channels
|
|
117
|
+
getPending: ['channelClose', ({}, cbk) => {
|
|
118
|
+
return getPendingChannels({lnd: args.lnd}, cbk);
|
|
119
|
+
}],
|
|
120
|
+
|
|
121
|
+
// Find the pending closing channel
|
|
122
|
+
pendingChannel: ['getPending', ({getPending}, cbk) => {
|
|
123
|
+
const channel = getPending.pending_channels.find(chan => {
|
|
124
|
+
if (chan.transaction_id !== args.transaction_id) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return chan.transaction_vout === args.transaction_vout;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!channel) {
|
|
132
|
+
return cbk([503, 'FailedToFindPendingCloseChannelForReplacement']);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!!channel.pending_payments && !!channel.pending_payments.length) {
|
|
136
|
+
return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cbk(null, {
|
|
140
|
+
capacity: channel.capacity,
|
|
141
|
+
partner_public_key: channel.partner_public_key,
|
|
142
|
+
remote_balance: channel.remote_balance,
|
|
143
|
+
});
|
|
144
|
+
}],
|
|
145
|
+
|
|
146
|
+
// Get the chain transactions to pull out the raw close tx
|
|
147
|
+
getUpdatedTx: ['channelClose', 'getHeight', ({getHeight}, cbk) => {
|
|
148
|
+
return getChainTransactions({
|
|
149
|
+
after: getHeight.current_block_height - fuzzBlocks,
|
|
150
|
+
lnd: args.lnd,
|
|
151
|
+
},
|
|
152
|
+
cbk);
|
|
153
|
+
}],
|
|
154
|
+
|
|
155
|
+
// Find the force close transaction
|
|
156
|
+
closeTx: [
|
|
157
|
+
'channelClose',
|
|
158
|
+
'getUpdatedTx',
|
|
159
|
+
'pendingChannel',
|
|
160
|
+
({channelClose, getUpdatedTx, pendingChannel}, cbk) =>
|
|
161
|
+
{
|
|
162
|
+
const closeId = channelClose.transaction_id;
|
|
163
|
+
const id = args.transaction_id;
|
|
164
|
+
const {capacity} = pendingChannel;
|
|
165
|
+
|
|
166
|
+
const tx = getUpdatedTx.transactions.find(n => n.id === closeId);
|
|
167
|
+
|
|
168
|
+
if (!tx || !tx.transaction) {
|
|
169
|
+
return cbk([503, 'ExpectedCloseTransactionInTransactionsList']);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const commitment = fromHex(tx.transaction);
|
|
173
|
+
|
|
174
|
+
// Make sure there aren't unexpectedly too-many outputs
|
|
175
|
+
if (commitment.outs.length > maxCommitmentOutputs) {
|
|
176
|
+
return cbk([503, 'ExpectedFewerOutputsOnCommitmentTransaction']);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const highValueOuts = commitment.outs.filter(n => n.value > highValue);
|
|
180
|
+
|
|
181
|
+
// There shouldn't be many high value outputs
|
|
182
|
+
if (highValueOuts.length > maxHighValueOutputs) {
|
|
183
|
+
return cbk([500, 'ExpectedFewerHighValueOutputsOnCommitTx']);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Input details for the commitment transaction
|
|
187
|
+
const [{hash, index, witness}, other] = commitment.ins;
|
|
188
|
+
|
|
189
|
+
// The commitment transaction should be spending the funding outpoint
|
|
190
|
+
if (!hash.equals(txIdAsHash(id)) || index !== args.transaction_vout) {
|
|
191
|
+
return cbk([503, 'ExpectedSpendingOpenTransactionOutputIndex']);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// There should be no other inputs on the closing tx
|
|
195
|
+
if (!!other) {
|
|
196
|
+
return cbk([503, 'ExpectedSingleInputForCloseTransaction']);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The spending script is the final element in the witness
|
|
200
|
+
const [fundingScript] = witness.slice().reverse();
|
|
201
|
+
|
|
202
|
+
// The difference between commitment outputs and its input is the fee
|
|
203
|
+
const fee = capacity - sumOf(commitment.outs.map(n => n.value));
|
|
204
|
+
|
|
205
|
+
return cbk(null, {
|
|
206
|
+
fee,
|
|
207
|
+
id: commitment.getId(),
|
|
208
|
+
script: bufferAsHex(fundingScript),
|
|
209
|
+
transaction: tx.transaction,
|
|
210
|
+
weight: commitment.weight(),
|
|
211
|
+
});
|
|
212
|
+
}],
|
|
213
|
+
|
|
214
|
+
// Find the open transaction
|
|
215
|
+
openTx: ['getUpdatedTx', ({getUpdatedTx}, cbk) => {
|
|
216
|
+
const funding = fromHex(args.open_transaction);
|
|
217
|
+
const id = args.transaction_id;
|
|
218
|
+
|
|
219
|
+
if (!funding.outs[args.transaction_vout]) {
|
|
220
|
+
return cbk([503, 'ExpectedTransactionOutputOnFundingTransaction']);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const capacity = funding.outs[args.transaction_vout].value;
|
|
224
|
+
const output = bufferAsHex(funding.outs[args.transaction_vout].script);
|
|
225
|
+
|
|
226
|
+
return cbk(null, {capacity, output});
|
|
227
|
+
}],
|
|
228
|
+
|
|
229
|
+
// Calculate the new channel's capacity
|
|
230
|
+
newCapacity: [
|
|
231
|
+
'closeTx',
|
|
232
|
+
'getFeeRate',
|
|
233
|
+
'pendingChannel',
|
|
234
|
+
({closeTx, getFeeRate, pendingChannel}, cbk) =>
|
|
235
|
+
{
|
|
236
|
+
// The fee for the replacement tx must meet the relay rate minimum
|
|
237
|
+
const {fee} = feeForReplacement({
|
|
238
|
+
capacity: pendingChannel.capacity,
|
|
239
|
+
commit_transaction_fee: closeTx.fee,
|
|
240
|
+
commit_transaction_weight: closeTx.weight,
|
|
241
|
+
decrease: args.decrease,
|
|
242
|
+
increase: args.increase,
|
|
243
|
+
tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// The new capacity is capacity + increase - decreases - tx fee
|
|
247
|
+
const newCapacity = sumOf([
|
|
248
|
+
pendingChannel.capacity,
|
|
249
|
+
args.increase || defaultIncrease,
|
|
250
|
+
-sumOf(args.decrease.map(n => n.tokens)),
|
|
251
|
+
-fee,
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
return cbk(null, newCapacity);
|
|
255
|
+
}],
|
|
256
|
+
|
|
257
|
+
// Propose the new channel to replace the existing one
|
|
258
|
+
proposeChannel: [
|
|
259
|
+
'newCapacity',
|
|
260
|
+
'pendingChannel',
|
|
261
|
+
({newCapacity, pendingChannel}, cbk) =>
|
|
262
|
+
{
|
|
263
|
+
return openChannels({
|
|
264
|
+
channels: [{
|
|
265
|
+
capacity: newCapacity,
|
|
266
|
+
give_tokens: pendingChannel.remote_balance,
|
|
267
|
+
partner_public_key: pendingChannel.partner_public_key,
|
|
268
|
+
}],
|
|
269
|
+
is_avoiding_broadcast: true,
|
|
270
|
+
is_private: args.is_private,
|
|
271
|
+
lnd: args.lnd,
|
|
272
|
+
},
|
|
273
|
+
cbk);
|
|
274
|
+
}],
|
|
275
|
+
|
|
276
|
+
// Put together the unsigned replacement transaction
|
|
277
|
+
replacementTx: [
|
|
278
|
+
'closeTx',
|
|
279
|
+
'newCapacity',
|
|
280
|
+
'proposeChannel',
|
|
281
|
+
({closeTx, newCapacity, proposeChannel}, cbk) =>
|
|
282
|
+
{
|
|
283
|
+
// The replacement tx pays to the proposed channel multisig address
|
|
284
|
+
const [{address}] = proposeChannel.pending;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const replacement = assembleReplacementTx({
|
|
288
|
+
add_funds_transaction_id: args.add_funds_transaction_id,
|
|
289
|
+
add_funds_transaction_vout: args.add_funds_transaction_vout,
|
|
290
|
+
bitcoinjs_network: args.bitcoinjs_network,
|
|
291
|
+
close_transaction: closeTx.transaction,
|
|
292
|
+
decrease: args.decrease.map(decrease => ({
|
|
293
|
+
output: decrease.output,
|
|
294
|
+
tokens: decrease.tokens,
|
|
295
|
+
})),
|
|
296
|
+
funding_address: address,
|
|
297
|
+
funding_tokens: newCapacity,
|
|
298
|
+
transaction_id: args.transaction_id,
|
|
299
|
+
transaction_vout: args.transaction_vout,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return cbk(null, {
|
|
303
|
+
add_funds_vin: replacement.add_funds_vin,
|
|
304
|
+
transaction: replacement.transaction,
|
|
305
|
+
transaction_vin: replacement.transaction_vin,
|
|
306
|
+
transaction_id: replacement.transaction_id,
|
|
307
|
+
transaction_vout: replacement.transaction_vout,
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return cbk([503, err.message]);
|
|
311
|
+
}
|
|
312
|
+
}],
|
|
313
|
+
|
|
314
|
+
// Partially sign the replacement transaction using the 2:2 key
|
|
315
|
+
signReplacement: [
|
|
316
|
+
'closeTx',
|
|
317
|
+
'openTx',
|
|
318
|
+
'pendingChannel',
|
|
319
|
+
'replacementTx',
|
|
320
|
+
({closeTx, openTx, pendingChannel, replacementTx}, cbk) =>
|
|
321
|
+
{
|
|
322
|
+
return signCapacityReplacement({
|
|
323
|
+
capacity: openTx.capacity,
|
|
324
|
+
lnd: args.lnd,
|
|
325
|
+
output: openTx.output,
|
|
326
|
+
script: closeTx.script,
|
|
327
|
+
transaction: replacementTx.transaction,
|
|
328
|
+
vin: replacementTx.transaction_vin,
|
|
329
|
+
},
|
|
330
|
+
cbk);
|
|
331
|
+
}],
|
|
332
|
+
|
|
333
|
+
// Details of proposed replacement
|
|
334
|
+
proposedReplacement: [
|
|
335
|
+
'closeTx',
|
|
336
|
+
'newCapacity',
|
|
337
|
+
'pendingChannel',
|
|
338
|
+
'proposeChannel',
|
|
339
|
+
'replacementTx',
|
|
340
|
+
'signReplacement',
|
|
341
|
+
({
|
|
342
|
+
closeTx,
|
|
343
|
+
newCapacity,
|
|
344
|
+
pendingChannel,
|
|
345
|
+
proposeChannel,
|
|
346
|
+
replacementTx,
|
|
347
|
+
signReplacement,
|
|
348
|
+
},
|
|
349
|
+
cbk) =>
|
|
350
|
+
{
|
|
351
|
+
const [pending] = proposeChannel.pending;
|
|
352
|
+
|
|
353
|
+
return cbk(null, {
|
|
354
|
+
add_funds_vin: replacementTx.add_funds_vin,
|
|
355
|
+
capacity: newCapacity,
|
|
356
|
+
force_close_tx_id: closeTx.id,
|
|
357
|
+
remote_balance: pendingChannel.remote_balance,
|
|
358
|
+
signature: signReplacement.signature,
|
|
359
|
+
signing_key: signReplacement.key,
|
|
360
|
+
pending_channel_id: pending.id,
|
|
361
|
+
transaction_id: replacementTx.transaction_id,
|
|
362
|
+
transaction_vin: replacementTx.transaction_vin,
|
|
363
|
+
transaction_vout: replacementTx.transaction_vout,
|
|
364
|
+
unsigned_transaction: replacementTx.transaction,
|
|
365
|
+
witness_script: closeTx.script,
|
|
366
|
+
});
|
|
367
|
+
}],
|
|
368
|
+
},
|
|
369
|
+
returnResult({reject, resolve, of: 'proposedReplacement'}, cbk));
|
|
370
|
+
});
|
|
371
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncRetry = require('async/retry');
|
|
3
|
+
const {closeChannel} = require('ln-service');
|
|
4
|
+
const {decodeChanId} = require('bolt07');
|
|
5
|
+
const {getChainTransactions} = require('ln-service');
|
|
6
|
+
const {getPendingChannels} = require('ln-service');
|
|
7
|
+
const {returnResult} = require('asyncjs-util');
|
|
8
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
9
|
+
|
|
10
|
+
const signCapacityReplacement = require('./sign_capacity_replacement');
|
|
11
|
+
const witnessScriptFromCloseTxs = require('./witness_script_from_close_txs');
|
|
12
|
+
|
|
13
|
+
const blocksBuffer = 1e3;
|
|
14
|
+
const {fromHex} = Transaction;
|
|
15
|
+
const interval = 200;
|
|
16
|
+
const notFoundIndex = -1;
|
|
17
|
+
const times = 72000;
|
|
18
|
+
const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
|
|
19
|
+
|
|
20
|
+
/** Close channel and then sign the unsigned replacement transaction
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
channel: <Replace Channel Standard Format Id String>
|
|
24
|
+
id: <Channel Funding Transaction Id Hex String>
|
|
25
|
+
lnd: <Authenticated LND API Object>
|
|
26
|
+
output: <Original Funding Output Script Hex String>
|
|
27
|
+
unsigned: <Unsigned Replacement Transaction Hex String>
|
|
28
|
+
vout: <Channel Funding Output Index Number>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@returns via cbk or Promise
|
|
32
|
+
{
|
|
33
|
+
signature: <Funding Spend Signature Hex String>
|
|
34
|
+
transaction_id: <Replacement Transaction Id Hex String>
|
|
35
|
+
transaction_vout: <Replacement Transaction Output Index Number>
|
|
36
|
+
}
|
|
37
|
+
*/
|
|
38
|
+
module.exports = ({channel, id, lnd, output, unsigned, vout}, cbk) => {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
return asyncAuto({
|
|
41
|
+
// Check arguments
|
|
42
|
+
validate: cbk => {
|
|
43
|
+
if (!channel) {
|
|
44
|
+
return cbk([400, 'ExpectedChannelIdToSignReplacementTransaction']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!id) {
|
|
48
|
+
return cbk([400, 'ExpectedFundingTxIdToSignReplacementTransaction']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!lnd) {
|
|
52
|
+
return cbk([400, 'ExpectedLndToSignReplacementTransaction']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!output) {
|
|
56
|
+
return cbk([400, 'ExpectedOutputScriptToGetReplacementSignature']);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!unsigned) {
|
|
60
|
+
return cbk([400, 'ExpectedUnsignedTxToGetReplacementSignature']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (vout === undefined) {
|
|
64
|
+
return cbk([400, 'ExpectedFundingOutputIndexToGetReplacementSig']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cbk();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Find input index that is respending the original funding outpoint
|
|
71
|
+
vin: ['validate', ({}, cbk) => {
|
|
72
|
+
const inputIndex = fromHex(unsigned).ins.findIndex(input => {
|
|
73
|
+
return input.index === vout && input.hash.equals(txIdAsHash(id));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (inputIndex === notFoundIndex) {
|
|
77
|
+
return cbk([503, 'ExpectedReplacementTransactionSpendingFunding']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cbk(null, inputIndex);
|
|
81
|
+
}],
|
|
82
|
+
|
|
83
|
+
// Force close the channel to allow for looking up the public key
|
|
84
|
+
closeChannel: ['vin', ({}, cbk) => {
|
|
85
|
+
return closeChannel({
|
|
86
|
+
lnd,
|
|
87
|
+
is_force_close: true,
|
|
88
|
+
transaction_id: id,
|
|
89
|
+
transaction_vout: vout,
|
|
90
|
+
},
|
|
91
|
+
cbk);
|
|
92
|
+
}],
|
|
93
|
+
|
|
94
|
+
// Get the pending close channels to see the final channel balance
|
|
95
|
+
getPending: ['closeChannel', ({}, cbk) => {
|
|
96
|
+
return getPendingChannels({lnd}, cbk);
|
|
97
|
+
}],
|
|
98
|
+
|
|
99
|
+
// Get the post close channel transactions to find the open/close txs
|
|
100
|
+
getTx: ['closeChannel', ({}, cbk) => {
|
|
101
|
+
const after = decodeChanId({channel}).block_height - blocksBuffer;
|
|
102
|
+
|
|
103
|
+
return getChainTransactions({after, lnd}, cbk);
|
|
104
|
+
}],
|
|
105
|
+
|
|
106
|
+
// Find the funding witness script in the close transaction
|
|
107
|
+
script: ['closeChannel', 'getTx', ({closeChannel, getTx}, cbk) => {
|
|
108
|
+
try {
|
|
109
|
+
const {script} = witnessScriptFromCloseTxs({
|
|
110
|
+
closing_tx_id: closeChannel.transaction_id,
|
|
111
|
+
transactions: getTx.transactions,
|
|
112
|
+
transaction_id: id,
|
|
113
|
+
transaction_vout: vout,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return cbk(null, script);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return cbk([503, err.message]);
|
|
119
|
+
}
|
|
120
|
+
}],
|
|
121
|
+
|
|
122
|
+
// Find the pending channel close that was just force closed
|
|
123
|
+
pending: [
|
|
124
|
+
'closeChannel',
|
|
125
|
+
'getPending',
|
|
126
|
+
({closeChannel, getPending}, cbk) =>
|
|
127
|
+
{
|
|
128
|
+
// The pending close tx has the closing transaction id
|
|
129
|
+
const pending = getPending.pending_channels.find(chan => {
|
|
130
|
+
return chan.transaction_id === id && chan.transaction_vout === vout;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!pending) {
|
|
134
|
+
return cbk([503, 'ExpectedPendingChannelToGetReplacementSignature']);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// There should be no pending payments on the channel
|
|
138
|
+
if (!!pending.pending_payments && !!pending.pending_payments.length) {
|
|
139
|
+
return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Return the local balance expected to be returned in replacement
|
|
143
|
+
return cbk(null, {
|
|
144
|
+
capacity: pending.capacity,
|
|
145
|
+
local_balance: pending.local_balance,
|
|
146
|
+
partner_public_key: pending.partner_public_key,
|
|
147
|
+
});
|
|
148
|
+
}],
|
|
149
|
+
|
|
150
|
+
// Given the unsigned transaction, look for the matching open proposal
|
|
151
|
+
waitForProposal: ['pending', ({pending}, cbk) => {
|
|
152
|
+
const id = fromHex(unsigned).getId();
|
|
153
|
+
|
|
154
|
+
return asyncRetry({interval, times}, cbk => {
|
|
155
|
+
return getPendingChannels({lnd}, (err, res) => {
|
|
156
|
+
if (!!err) {
|
|
157
|
+
return cbk(err);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find an incoming channel that indicates it's safe to sign reopen
|
|
161
|
+
const incomingChannel = res.pending_channels.find(channel => {
|
|
162
|
+
// This will be an opening channel
|
|
163
|
+
if (!channel.is_opening) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The new channel must be from the same peer
|
|
168
|
+
if (channel.partner_public_key !== pending.partner_public_key) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The new channel must be spending the old transaction
|
|
173
|
+
if (channel.transaction_id !== id) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The new channel must have at least the balance of the old one
|
|
178
|
+
if (channel.local_balance < pending.local_balance) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!incomingChannel) {
|
|
186
|
+
return cbk([503, 'FailedToFindPendingReplacementChannel']);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return cbk(null, {
|
|
190
|
+
transaction_id: incomingChannel.transaction_id,
|
|
191
|
+
transaction_vout: incomingChannel.transaction_vout,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
cbk);
|
|
196
|
+
}],
|
|
197
|
+
|
|
198
|
+
// Sign the channel funding to prepare to authorize the new channel
|
|
199
|
+
signFunding: [
|
|
200
|
+
'pending',
|
|
201
|
+
'script',
|
|
202
|
+
'vin',
|
|
203
|
+
'waitForProposal',
|
|
204
|
+
({pending, script, vin, waitForProposal}, cbk) =>
|
|
205
|
+
{
|
|
206
|
+
return signCapacityReplacement({
|
|
207
|
+
lnd,
|
|
208
|
+
output,
|
|
209
|
+
script,
|
|
210
|
+
vin,
|
|
211
|
+
capacity: pending.capacity,
|
|
212
|
+
transaction: unsigned,
|
|
213
|
+
},
|
|
214
|
+
cbk);
|
|
215
|
+
}],
|
|
216
|
+
|
|
217
|
+
// Final signature
|
|
218
|
+
signature: [
|
|
219
|
+
'signFunding',
|
|
220
|
+
'waitForProposal',
|
|
221
|
+
({signFunding, waitForProposal}, cbk) =>
|
|
222
|
+
{
|
|
223
|
+
return cbk(null, {
|
|
224
|
+
signature: signFunding.signature,
|
|
225
|
+
transaction_id: waitForProposal.transaction_id,
|
|
226
|
+
transaction_vout: waitForProposal.transaction_vout,
|
|
227
|
+
});
|
|
228
|
+
}],
|
|
229
|
+
},
|
|
230
|
+
returnResult({reject, resolve, of: 'signature'}, cbk));
|
|
231
|
+
});
|
|
232
|
+
};
|