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,52 @@
|
|
|
1
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
2
|
+
|
|
3
|
+
const findTxRecord = records => records.find(n => n.type === '1');
|
|
4
|
+
const {fromHex} = Transaction;
|
|
5
|
+
|
|
6
|
+
/** Parse channel capacity change accept response
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
records: [{
|
|
10
|
+
type: <Record Type Number String>
|
|
11
|
+
value: <Record Value Hex Encoded String>
|
|
12
|
+
}]
|
|
13
|
+
transaction_id: <Channel to Capacity Change Transaction Id Hex String>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@throws
|
|
17
|
+
<Error>
|
|
18
|
+
|
|
19
|
+
@returns
|
|
20
|
+
{
|
|
21
|
+
transaction: <Channel Open Transaction Hex String>
|
|
22
|
+
}
|
|
23
|
+
*/
|
|
24
|
+
module.exports = args => {
|
|
25
|
+
// Exit early when there are no records
|
|
26
|
+
if (!args.records) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const txRecord = findTxRecord(args.records);
|
|
31
|
+
|
|
32
|
+
// Exit early when there is no tx record
|
|
33
|
+
if (!txRecord) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Make sure that the tx record is a tx
|
|
38
|
+
try {
|
|
39
|
+
fromHex(txRecord.value);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new Error('ExpectedValidRawTransactionHexToParseAcceptResponse');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const txId = fromHex(txRecord.value).getId();
|
|
45
|
+
|
|
46
|
+
// Make sure that the tx matches the tx id
|
|
47
|
+
if (txId !== args.transaction_id) {
|
|
48
|
+
throw new Error('ExpectedTxRecordForRawChannelTransaction');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {transaction: txRecord.value};
|
|
52
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const {chanFormat} = require('bolt07');
|
|
2
|
+
const {decodeBigSize} = require('bolt01');
|
|
3
|
+
|
|
4
|
+
const channelIdHexLength = 16;
|
|
5
|
+
const decodeNumber = encoded => BigInt(decodeBigSize({encoded}).decoded);
|
|
6
|
+
const defaultRecord = {value: '00'};
|
|
7
|
+
const idHexLength = 64;
|
|
8
|
+
const tooLarge = BigInt(Number.MAX_SAFE_INTEGER);
|
|
9
|
+
|
|
10
|
+
const findChannelRecord = records => records.find(n => n.type === '2');
|
|
11
|
+
const findDecreaseRecord = records => records.find(n => n.type === '3');
|
|
12
|
+
const findIncreaseRecord = records => records.find(n => n.type === '4');
|
|
13
|
+
const findRequestIdRecord = records => records.find(n => n.type === '1');
|
|
14
|
+
const findVersionRecord = records => records.find(n => n.type === '0');
|
|
15
|
+
|
|
16
|
+
/** Parse a capacity change request
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
from: <From Public Key Hex String>
|
|
20
|
+
[records]: [{
|
|
21
|
+
type: <Record Type Number String>
|
|
22
|
+
value: <Record Hex Encoded Value String>
|
|
23
|
+
}]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@returns
|
|
27
|
+
{
|
|
28
|
+
[request]: {
|
|
29
|
+
channel: <Standard Format Channel Id String>
|
|
30
|
+
[decrease]: <Reduce Capacity Tokens Number>
|
|
31
|
+
from: <From Node Public Key Id Hex String>
|
|
32
|
+
id: <Change Request Id Hex String>
|
|
33
|
+
[increase]: <Add Capacity Tokens Number>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
*/
|
|
37
|
+
module.exports = ({from, records}) => {
|
|
38
|
+
// Exit early when there are no records
|
|
39
|
+
if (!records) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Exit early when there is a version record, indicating a future version
|
|
44
|
+
if (!!findVersionRecord(records)) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const channelRecord = findChannelRecord(records);
|
|
49
|
+
|
|
50
|
+
// Exit early when there is no channel
|
|
51
|
+
if (!channelRecord || channelRecord.value.length !== channelIdHexLength) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
chanFormat({id: channelRecord.value});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const idRecord = findRequestIdRecord(records);
|
|
62
|
+
|
|
63
|
+
// Exit early when there is no request id
|
|
64
|
+
if (!idRecord || idRecord.value.length !== idHexLength) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const decreaseRecord = findDecreaseRecord(records);
|
|
69
|
+
const increaseRecord = findIncreaseRecord(records);
|
|
70
|
+
|
|
71
|
+
// Exit early when there is a change in both directions
|
|
72
|
+
if (!!decreaseRecord && !!increaseRecord) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const {channel} = chanFormat({id: channelRecord.value});
|
|
77
|
+
const id = idRecord.value;
|
|
78
|
+
|
|
79
|
+
// Exit early when there is no decrease or increase
|
|
80
|
+
if (!decreaseRecord && !increaseRecord) {
|
|
81
|
+
return {request: {channel, from, id}};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const decrease = decodeNumber((decreaseRecord || defaultRecord).value);
|
|
85
|
+
const increase = decodeNumber((increaseRecord || defaultRecord).value);
|
|
86
|
+
|
|
87
|
+
// Exit early when there is no reasonably sized increase or decrease
|
|
88
|
+
if (decrease > tooLarge || increase > tooLarge) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
request: {
|
|
94
|
+
channel,
|
|
95
|
+
from,
|
|
96
|
+
id,
|
|
97
|
+
decrease: Number(decrease) || undefined,
|
|
98
|
+
increase: Number(increase) || undefined,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
2
|
+
|
|
3
|
+
const {fromHex} = Transaction;
|
|
4
|
+
const findTransactionRecord = records => records.find(n => n.type === '1');
|
|
5
|
+
const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
|
|
6
|
+
|
|
7
|
+
/** Parse request records requesting signing a capacity change request
|
|
8
|
+
|
|
9
|
+
{
|
|
10
|
+
id: <Funding Transaction Id Hex String>
|
|
11
|
+
increase: <Increase By Tokens Number String>
|
|
12
|
+
records: [{
|
|
13
|
+
type: <Record Type Number String>
|
|
14
|
+
value: <Record Value Hex Encoded String>
|
|
15
|
+
}]
|
|
16
|
+
vout: <Funding Transaction Output Index Number>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@throws
|
|
20
|
+
<Error>
|
|
21
|
+
|
|
22
|
+
@returns
|
|
23
|
+
{
|
|
24
|
+
unsigned: <Unsigned Transaction Replacement Hex String>
|
|
25
|
+
}
|
|
26
|
+
*/
|
|
27
|
+
module.exports = ({id, increase, records, vout}) => {
|
|
28
|
+
const txRecord = findTransactionRecord(records);
|
|
29
|
+
|
|
30
|
+
if (!txRecord) {
|
|
31
|
+
throw new Error('ExpectedTransactionRecordInSignCapacityChangeRequest');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check that the unsigned tx is a tx
|
|
35
|
+
try {
|
|
36
|
+
fromHex(txRecord.value);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new Error('FailedToParseSignCapacityChangeRequestTransaction');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hash = txIdAsHash(id);
|
|
42
|
+
const tx = fromHex(txRecord.value);
|
|
43
|
+
|
|
44
|
+
const fundingSpend = tx.ins.filter(input => {
|
|
45
|
+
return input.index === vout && input.hash.equals(hash);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Make sure that the tx spends the funding tx outpoint
|
|
49
|
+
if (!fundingSpend.length) {
|
|
50
|
+
throw new Error('ExpectedSpendOfFundingTransactionInChangeRequestTx');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Make sure that an increase has an additional input
|
|
54
|
+
if (!!increase && tx.ins.length === [fundingSpend].length) {
|
|
55
|
+
throw new Error('ExpectedAddedInputInChangeTxForChangeCapacityIncrease');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {unsigned: txRecord.value};
|
|
59
|
+
};
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncDoUntil = require('async/doUntil');
|
|
3
|
+
const asyncReflect = require('async/reflect');
|
|
4
|
+
const {broadcastChainTransaction} = require('ln-service');
|
|
5
|
+
const {cancelPendingChannel} = require('ln-service');
|
|
6
|
+
const {fundPendingChannels} = require('ln-service');
|
|
7
|
+
const {getChainTransactions} = require('ln-service');
|
|
8
|
+
const {getChannels} = require('ln-service');
|
|
9
|
+
const {getClosedChannels} = require('ln-service');
|
|
10
|
+
const {getHeight} = require('ln-service');
|
|
11
|
+
const {returnResult} = require('asyncjs-util');
|
|
12
|
+
const {signTransaction} = require('ln-service');
|
|
13
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
14
|
+
const {transactionAsPsbt} = require('psbt');
|
|
15
|
+
|
|
16
|
+
const finalizeCapacityReplacement = require('./finalize_capacity_replacement');
|
|
17
|
+
const getCapacityReplacement = require('./get_capacity_replacement');
|
|
18
|
+
const interimReplacementPsbt = require('./interim_replacement_psbt');
|
|
19
|
+
const {makePeerRequest} = require('./../p2p');
|
|
20
|
+
const {serviceTypeSignCapacityChange} = require('./service_types');
|
|
21
|
+
|
|
22
|
+
const capacityChangeIdType = '0';
|
|
23
|
+
const findSignatureRecord = records => records.find(n => n.type === '1');
|
|
24
|
+
const fuzzHeight = 3;
|
|
25
|
+
const maxSigHexLength = 146;
|
|
26
|
+
const peerRequestTimeoutMs = 1000 * 60 * 10;
|
|
27
|
+
const rebroadcastDelayMs = 1000 * 3;
|
|
28
|
+
const {SIGHASH_ALL} = Transaction;
|
|
29
|
+
const transitFamily = 805;
|
|
30
|
+
const unsignedTransactionType = '1';
|
|
31
|
+
|
|
32
|
+
/** Propose the capacity change to be accepted
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
bitcoinjs_network: <BitcoinJS Network Name String>
|
|
36
|
+
channel: <Channel to Change Standard Format Channel Id 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: <Capacity Change Request Id Hex String>
|
|
43
|
+
[increase]: <Increase Channel Size Tokens Number>
|
|
44
|
+
[increase_key_index]: <Increase Funds Transit Key Number>
|
|
45
|
+
[increase_output_script]: <Increase Funds Output Script Hex String>
|
|
46
|
+
[increase_transaction_id]: <Increase Funds Outpoint Tx Id Hex String>
|
|
47
|
+
[increase_transaction_vout]: <Increase Funds Outpoint Output Index Number>
|
|
48
|
+
[increase_witness_script]: <Increase Funds Witness Script Hex String>
|
|
49
|
+
lnd: <Authenticated LND API Object>
|
|
50
|
+
logger: <Winston Logger Object>
|
|
51
|
+
open_transaction: <Original Channel Funding Transaction Hex String>
|
|
52
|
+
partner_public_key: <Peer Public Key Hex Encoded String>
|
|
53
|
+
transaction_id: <Original Channel Transaction Id Hex String>
|
|
54
|
+
transaction_vout: <Original Channel Transaction Output Index Number>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@returns via cbk or Promise
|
|
58
|
+
{
|
|
59
|
+
transaction_id: <Replacement Channel Transaction Id Hex String>
|
|
60
|
+
transaction_vout: <Replacement Channel Transaction Output Index Number>
|
|
61
|
+
}
|
|
62
|
+
*/
|
|
63
|
+
module.exports = (args, cbk) => {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
return asyncAuto({
|
|
66
|
+
// Check arguments
|
|
67
|
+
validate: cbk => {
|
|
68
|
+
if (!args.bitcoinjs_network) {
|
|
69
|
+
return cbk([400, 'ExpectedBitcoinjsNetworkNameToProposeChange']);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!args.channel) {
|
|
73
|
+
return cbk([400, 'ExpectedChannelIdToProposeCapacityChangeFor']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!args.decrease) {
|
|
77
|
+
return cbk([400, 'ExpectedDecreaseSetToProposeCapacityChange']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!args.id) {
|
|
81
|
+
return cbk([400, 'ExpectedChangeRequestIdToProposeCapacityChange']);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!args.lnd) {
|
|
85
|
+
return cbk([400, 'ExpectedAuthenticatedLndToProposeCapacityChange']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!args.logger) {
|
|
89
|
+
return cbk([400, 'ExpectedWinstonLoggerToProposeCapacityChange']);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!args.open_transaction) {
|
|
93
|
+
return cbk([400, 'ExpectedChannelOpenTxToProposeCapacityChange']);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!args.partner_public_key) {
|
|
97
|
+
return cbk([400, 'ExpectedPeerNodeIdToProposeCapacityChange']);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!args.transaction_id) {
|
|
101
|
+
return cbk([400, 'ExpectedOriginalTxIdToProposeCapacityChange']);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (args.transaction_vout === undefined) {
|
|
105
|
+
return cbk([400, 'ExpectedOriginalChannelTxOutputIndexToPropose']);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return cbk();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Get the starting height
|
|
112
|
+
getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
|
|
113
|
+
|
|
114
|
+
// Close the current channal and get the unsigned replacement
|
|
115
|
+
getReplacement: ['validate', ({}, cbk) => {
|
|
116
|
+
return getCapacityReplacement({
|
|
117
|
+
add_funds_transaction_id: args.increase_transaction_id,
|
|
118
|
+
add_funds_transaction_vout: args.increase_transaction_vout,
|
|
119
|
+
bitcoinjs_network: args.bitcoinjs_network,
|
|
120
|
+
decrease: args.decrease,
|
|
121
|
+
id: args.channel,
|
|
122
|
+
increase: args.increase,
|
|
123
|
+
lnd: args.lnd,
|
|
124
|
+
open_transaction: args.open_transaction,
|
|
125
|
+
transaction_id: args.transaction_id,
|
|
126
|
+
transaction_vout: args.transaction_vout,
|
|
127
|
+
},
|
|
128
|
+
cbk);
|
|
129
|
+
}],
|
|
130
|
+
|
|
131
|
+
// Replacement channel outpoint
|
|
132
|
+
replacement: ['getReplacement', ({getReplacement}, cbk) => {
|
|
133
|
+
args.logger.info({pending_chan_id: getReplacement.pending_channel_id});
|
|
134
|
+
|
|
135
|
+
return cbk(null, {
|
|
136
|
+
transaction_id: getReplacement.transaction_id,
|
|
137
|
+
transaction_vout: getReplacement.transaction_vout,
|
|
138
|
+
});
|
|
139
|
+
}],
|
|
140
|
+
|
|
141
|
+
// Partially sign the replacement transaction using the add funds key
|
|
142
|
+
signAddFunds: ['getReplacement', ({getReplacement}, cbk) => {
|
|
143
|
+
// Exit early when there are no additional funds added
|
|
144
|
+
if (!args.increase) {
|
|
145
|
+
return cbk(null, {});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return signTransaction({
|
|
149
|
+
inputs: [{
|
|
150
|
+
key_family: transitFamily,
|
|
151
|
+
key_index: args.increase_key_index,
|
|
152
|
+
output_script: args.increase_output_script,
|
|
153
|
+
output_tokens: args.increase,
|
|
154
|
+
sighash: SIGHASH_ALL,
|
|
155
|
+
vin: getReplacement.add_funds_vin,
|
|
156
|
+
witness_script: args.increase_witness_script,
|
|
157
|
+
}],
|
|
158
|
+
lnd: args.lnd,
|
|
159
|
+
transaction: getReplacement.unsigned_transaction,
|
|
160
|
+
},
|
|
161
|
+
cbk);
|
|
162
|
+
}],
|
|
163
|
+
|
|
164
|
+
// Use the unsigned replacement transaction to "fund" the pending channel
|
|
165
|
+
fundWithReplacement: [
|
|
166
|
+
'getReplacement',
|
|
167
|
+
'signAddFunds',
|
|
168
|
+
({getReplacement, signAddFunds}, cbk) =>
|
|
169
|
+
{
|
|
170
|
+
const [addFundsSignature] = signAddFunds.signatures || [];
|
|
171
|
+
|
|
172
|
+
const {psbt} = interimReplacementPsbt({
|
|
173
|
+
increase_public_key: args.increase_key,
|
|
174
|
+
increase_signature: addFundsSignature,
|
|
175
|
+
increase_transaction: args.increase_transaction,
|
|
176
|
+
increase_transaction_vin: getReplacement.add_funds_vin,
|
|
177
|
+
open_transaction: args.open_transaction,
|
|
178
|
+
signature: getReplacement.signature,
|
|
179
|
+
unsigned_transaction: getReplacement.unsigned_transaction,
|
|
180
|
+
witness_script: getReplacement.witness_script,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Create the commitment transaction with the peer on the replacement
|
|
184
|
+
return fundPendingChannels({
|
|
185
|
+
channels: [getReplacement.pending_channel_id],
|
|
186
|
+
funding: psbt,
|
|
187
|
+
lnd: args.lnd,
|
|
188
|
+
},
|
|
189
|
+
cbk);
|
|
190
|
+
}],
|
|
191
|
+
|
|
192
|
+
// Send the final go ahead signal now that a proposal is made
|
|
193
|
+
getPeerSignature: [
|
|
194
|
+
'getReplacement',
|
|
195
|
+
'fundWithReplacement',
|
|
196
|
+
asyncReflect(({getReplacement}, cbk) =>
|
|
197
|
+
{
|
|
198
|
+
return makePeerRequest({
|
|
199
|
+
lnd: args.lnd,
|
|
200
|
+
records: [
|
|
201
|
+
{
|
|
202
|
+
type: unsignedTransactionType,
|
|
203
|
+
value: getReplacement.unsigned_transaction,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: capacityChangeIdType,
|
|
207
|
+
value: args.id,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
timeout: peerRequestTimeoutMs,
|
|
211
|
+
to: args.partner_public_key,
|
|
212
|
+
type: serviceTypeSignCapacityChange,
|
|
213
|
+
},
|
|
214
|
+
(err, res) => {
|
|
215
|
+
if (!!err) {
|
|
216
|
+
return cbk(err);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!res.records) {
|
|
220
|
+
return cbk([503, 'ExpectedSignatureResponseFromPeerForChange']);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const record = findSignatureRecord(res.records);
|
|
224
|
+
|
|
225
|
+
if (!record || record.value.length > maxSigHexLength) {
|
|
226
|
+
return cbk([503, 'ExpectedSignatureRecordFromPeerForChange']);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return cbk(null, record.value);
|
|
230
|
+
});
|
|
231
|
+
})],
|
|
232
|
+
|
|
233
|
+
// Cancel the funding proposal when there is an error
|
|
234
|
+
cancelFunding: [
|
|
235
|
+
'getPeerSignature',
|
|
236
|
+
'getReplacement',
|
|
237
|
+
({getPeerSignature, getReplacement}, cbk) =>
|
|
238
|
+
{
|
|
239
|
+
// Exit early when there is no need to cancel
|
|
240
|
+
if (!getPeerSignature.error) {
|
|
241
|
+
return cbk();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return cancelPendingChannel({
|
|
245
|
+
id: getReplacement.pending_channel_id,
|
|
246
|
+
lnd: args.lnd,
|
|
247
|
+
},
|
|
248
|
+
err => {
|
|
249
|
+
if (!!err) {
|
|
250
|
+
return cbk([503, 'FailedToCancelPendingChannel', {err}]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Return the original error
|
|
254
|
+
return cbk(getPeerSignature.error);
|
|
255
|
+
});
|
|
256
|
+
}],
|
|
257
|
+
|
|
258
|
+
// Broadcast the new funding transaction using the peer's signature
|
|
259
|
+
confirmFunding: [
|
|
260
|
+
'getHeight',
|
|
261
|
+
'getPeerSignature',
|
|
262
|
+
'getReplacement',
|
|
263
|
+
'signAddFunds',
|
|
264
|
+
({getHeight, getPeerSignature, getReplacement, signAddFunds}, cbk) =>
|
|
265
|
+
{
|
|
266
|
+
// Exit early when there was an error getting the peer signature
|
|
267
|
+
if (!!getPeerSignature.error) {
|
|
268
|
+
return cbk();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const [addFundsSignature] = signAddFunds.signatures || [];
|
|
272
|
+
|
|
273
|
+
// Insert the peer signature to get the final signed funding tx
|
|
274
|
+
const {transaction} = finalizeCapacityReplacement({
|
|
275
|
+
add_funds_vin: getReplacement.add_funds_vin,
|
|
276
|
+
add_funds_public_key: args.increase_key,
|
|
277
|
+
add_funds_signature: addFundsSignature,
|
|
278
|
+
local_public_key: getReplacement.signing_key,
|
|
279
|
+
local_signature: getReplacement.signature,
|
|
280
|
+
funding_spend_vin: getReplacement.transaction_vin,
|
|
281
|
+
remote_signature: getPeerSignature.value,
|
|
282
|
+
transaction: getReplacement.unsigned_transaction,
|
|
283
|
+
witness_script: getReplacement.witness_script,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
args.logger.info({publishing_new_channel_transaction: transaction});
|
|
287
|
+
|
|
288
|
+
return asyncDoUntil(
|
|
289
|
+
cbk => {
|
|
290
|
+
// Try and override the force close tx using the replacement
|
|
291
|
+
return broadcastChainTransaction({
|
|
292
|
+
transaction,
|
|
293
|
+
after: getHeight.current_block_height - fuzzHeight,
|
|
294
|
+
lnd: args.lnd,
|
|
295
|
+
},
|
|
296
|
+
cbk);
|
|
297
|
+
},
|
|
298
|
+
(broadcast, cbk) => {
|
|
299
|
+
args.logger.info({waiting_for_confirmation: broadcast.id});
|
|
300
|
+
|
|
301
|
+
// Rebroadcast until the transaction is confirmed
|
|
302
|
+
return getChainTransactions({lnd: args.lnd}, (err, res) => {
|
|
303
|
+
if (!!err) {
|
|
304
|
+
return cbk(err);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const confirmed = res.transactions.filter(n => !!n.is_confirmed);
|
|
308
|
+
|
|
309
|
+
// Find the confirmed tx
|
|
310
|
+
const tx = confirmed.find(tx => tx.id === broadcast.id);
|
|
311
|
+
|
|
312
|
+
if (!!tx) {
|
|
313
|
+
return cbk(null, true);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find the replacing tx
|
|
317
|
+
const replacing = confirmed.find(tx => {
|
|
318
|
+
return tx.id === getReplacement.force_close_tx_id;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Cancel the pending channel if the force close confirms
|
|
322
|
+
if (!!replacing) {
|
|
323
|
+
return cancelPendingChannel({
|
|
324
|
+
id: getReplacement.pending_channel_id,
|
|
325
|
+
lnd: args.lnd,
|
|
326
|
+
},
|
|
327
|
+
() => {
|
|
328
|
+
return cbk([503, 'FailedToReplaceForceCloseTransaction']);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return getChannels({
|
|
333
|
+
lnd: args.lnd,
|
|
334
|
+
partner_public_key: args.partner_public_key,
|
|
335
|
+
},
|
|
336
|
+
(err, res) => {
|
|
337
|
+
if (!!err) {
|
|
338
|
+
return cbk(err);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const channel = res.channels.find(channel => {
|
|
342
|
+
return channel.transaction_id === broadcast.id
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Stop broadcasting when a channel shows up
|
|
346
|
+
if (!!channel) {
|
|
347
|
+
return cbk(null, true);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Rebroadcast after a delay
|
|
351
|
+
return setTimeout(() => cbk(null, false), rebroadcastDelayMs);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
cbk
|
|
356
|
+
);
|
|
357
|
+
}],
|
|
358
|
+
},
|
|
359
|
+
returnResult({reject, resolve, of: 'replacement'}, cbk));
|
|
360
|
+
});
|
|
361
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncDetectLimit = require('async/detectLimit');
|
|
3
|
+
const {getPublicKey} = require('ln-service');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
const {script} = require('bitcoinjs-lib');
|
|
6
|
+
const {signTransaction} = require('ln-service');
|
|
7
|
+
const {Transaction} = require('bitcoinjs-lib');
|
|
8
|
+
|
|
9
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
10
|
+
const {decompile} = script;
|
|
11
|
+
const family = 0;
|
|
12
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
13
|
+
const {isBuffer} = Buffer;
|
|
14
|
+
const limit = 250;
|
|
15
|
+
const multiKeyLimit = 100000;
|
|
16
|
+
const range = max => [...Array(max).keys()];
|
|
17
|
+
const {SIGHASH_ALL} = Transaction;
|
|
18
|
+
|
|
19
|
+
/** Sign a capacity replacement given a witness for a channel close
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
capacity: <Original Channel Capacity Tokens Number>
|
|
23
|
+
lnd: <Authenticated LND API Object>
|
|
24
|
+
output: <Channel Funding Output Script Hex String>
|
|
25
|
+
script: <Witness Script String>
|
|
26
|
+
transaction: <Unsigned Transaction Hex String>
|
|
27
|
+
vin: <Replacement Transaction Input Index Number>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@returns via cbk or Promise
|
|
31
|
+
{
|
|
32
|
+
key: <Public Key Hex String>
|
|
33
|
+
signature: <Signature Hex String>
|
|
34
|
+
}
|
|
35
|
+
*/
|
|
36
|
+
module.exports = ({capacity, lnd, output, script, transaction, vin}, cbk) => {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
return asyncAuto({
|
|
39
|
+
// Check arguments
|
|
40
|
+
validate: cbk => {
|
|
41
|
+
if (!capacity) {
|
|
42
|
+
return cbk([400, 'ExpectedCapacityToSignCapacityReplacementTx']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!lnd) {
|
|
46
|
+
return cbk([400, 'ExpectedLndToSignCapacityChangeReplacementTx']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!output) {
|
|
50
|
+
return cbk([400, 'ExpectedOutputScriptToSignCapacityChangeTx']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!script) {
|
|
54
|
+
return cbk([400, 'ExpectedWitnessScriptToSignReplacementTx']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!transaction) {
|
|
58
|
+
return cbk([400, 'ExpectedUnsignedTransactionToSignReplacementTx']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (vin === undefined) {
|
|
62
|
+
return cbk([400, 'ExpectedUtxoSpendIndexToSignReplacementTx']);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return cbk();
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Find the local key index for the channel
|
|
69
|
+
findKeyIndex: ['validate', ({}, cbk) => {
|
|
70
|
+
const funding = hexAsBuffer(script);
|
|
71
|
+
|
|
72
|
+
const keys = decompile(funding).filter(isBuffer).map(bufferAsHex);
|
|
73
|
+
|
|
74
|
+
return asyncDetectLimit(range(multiKeyLimit), limit, (index, cbk) => {
|
|
75
|
+
return getPublicKey({family, index, lnd}, (err, res) => {
|
|
76
|
+
if (!!err) {
|
|
77
|
+
return cbk(err);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cbk(null, keys.includes(res.public_key));
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
cbk);
|
|
84
|
+
}],
|
|
85
|
+
|
|
86
|
+
// Get the public key
|
|
87
|
+
getKey: ['findKeyIndex', ({findKeyIndex}, cbk) => {
|
|
88
|
+
if (findKeyIndex === undefined) {
|
|
89
|
+
return cbk([503, 'FailedToFindChannelMultiSigKeyIndex']);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return getPublicKey({family, lnd, index: findKeyIndex}, cbk);
|
|
93
|
+
}],
|
|
94
|
+
|
|
95
|
+
// Sign the channel funds spend
|
|
96
|
+
signSpend: ['findKeyIndex', ({findKeyIndex}, cbk) => {
|
|
97
|
+
const inputs = [{
|
|
98
|
+
vin,
|
|
99
|
+
key_family: family,
|
|
100
|
+
key_index: findKeyIndex,
|
|
101
|
+
output_script: output,
|
|
102
|
+
output_tokens: capacity,
|
|
103
|
+
sighash: SIGHASH_ALL,
|
|
104
|
+
witness_script: script,
|
|
105
|
+
}];
|
|
106
|
+
|
|
107
|
+
return signTransaction({inputs, lnd, transaction}, cbk);
|
|
108
|
+
}],
|
|
109
|
+
|
|
110
|
+
// Final signature
|
|
111
|
+
signature: ['getKey', 'signSpend', ({getKey, signSpend}, cbk) => {
|
|
112
|
+
const [signature] = signSpend.signatures;
|
|
113
|
+
|
|
114
|
+
return cbk(null, {signature, key: getKey.public_key});
|
|
115
|
+
}],
|
|
116
|
+
},
|
|
117
|
+
returnResult({reject, resolve, of: 'signature'}, cbk));
|
|
118
|
+
});
|
|
119
|
+
};
|