beignet 0.21.9 → 0.21.10
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/dist/cli/beignet-node.js +147 -24
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/cli/daemon.js +4 -2
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/errors.js +3 -0
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/ffor-receive.js +5 -3
- package/dist/cli/ffor-receive.js.map +1 -1
- package/dist/cli/offline-receive.js +122 -38
- package/dist/cli/offline-receive.js.map +1 -1
- package/dist/cli/openapi.js +50 -15
- package/dist/cli/openapi.js.map +1 -1
- package/dist/lightning/advisor/liquidity-advisor.js +4 -1
- package/dist/lightning/advisor/liquidity-advisor.js.map +1 -1
- package/dist/lightning/channel/channel-manager.js +261 -152
- package/dist/lightning/channel/channel-manager.js.map +1 -1
- package/dist/lightning/channel/channel-state.js +20 -2
- package/dist/lightning/channel/channel-state.js.map +1 -1
- package/dist/lightning/channel/channel.js +150 -35
- package/dist/lightning/channel/channel.js.map +1 -1
- package/dist/lightning/channel/types.js.map +1 -1
- package/dist/lightning/direct-funding/receiver/engine.js +20 -13
- package/dist/lightning/direct-funding/receiver/engine.js.map +1 -1
- package/dist/lightning/direct-funding/receiver/types.js.map +1 -1
- package/dist/lightning/node/lightning-node.js +259 -19
- package/dist/lightning/node/lightning-node.js.map +1 -1
- package/dist/lightning/node/types.js.map +1 -1
- package/dist/lightning/recovery/channel-status.js +2 -0
- package/dist/lightning/recovery/channel-status.js.map +1 -1
- package/dist/lightning/storage/serialization.js +6 -0
- package/dist/lightning/storage/serialization.js.map +1 -1
- package/dist/lightning/storage/sqlite-storage.js +6 -0
- package/dist/lightning/storage/sqlite-storage.js.map +1 -1
- package/dist/lightning/storage/types.js.map +1 -1
- package/dist/types/cli/beignet-node.d.ts +7 -1
- package/dist/types/cli/errors.d.ts +2 -0
- package/dist/types/cli/offline-receive.d.ts +24 -2
- package/dist/types/cli/types.d.ts +7 -1
- package/dist/types/lightning/advisor/liquidity-advisor.d.ts +3 -0
- package/dist/types/lightning/channel/channel-manager.d.ts +13 -0
- package/dist/types/lightning/channel/channel-state.d.ts +16 -1
- package/dist/types/lightning/channel/channel.d.ts +7 -0
- package/dist/types/lightning/direct-funding/receiver/engine.d.ts +1 -0
- package/dist/types/lightning/node/lightning-node.d.ts +14 -0
- package/dist/types/lightning/node/types.d.ts +4 -0
- package/dist/types/lightning/recovery/channel-status.d.ts +2 -0
- package/dist/types/lightning/storage/serialization.d.ts +4 -1
- package/dist/types/lightning/storage/sqlite-storage.d.ts +1 -0
- package/dist/types/lightning/storage/types.d.ts +1 -0
- package/docs/AUTOMATIC-OFFLINE-RECEIVE.md +46 -15
- package/docs/RECOVERY-PROTOCOL.md +39 -0
- package/package.json +1 -1
package/dist/cli/beignet-node.js
CHANGED
|
@@ -454,6 +454,9 @@ function jitInvoiceError(err) {
|
|
|
454
454
|
if (/^LSP declined the JIT receive intent|above the accepted maximum|without an intercept scid/.test(message)) {
|
|
455
455
|
return new errors_1.BeignetError(errors_1.BeignetErrorCode.JIT_REFUSED, message);
|
|
456
456
|
}
|
|
457
|
+
if (/^JIT receive needs a new channel from the LSP/.test(message)) {
|
|
458
|
+
return new errors_1.BeignetError(errors_1.BeignetErrorCode.NEW_CHANNELS_REFUSED, message);
|
|
459
|
+
}
|
|
457
460
|
if (/timed out waiting for the LSP/.test(message)) {
|
|
458
461
|
return new errors_1.BeignetError(errors_1.BeignetErrorCode.JIT_TIMEOUT, message);
|
|
459
462
|
}
|
|
@@ -470,8 +473,33 @@ function requireChannelIdHex(value, field = 'channelId') {
|
|
|
470
473
|
return Buffer.from(value, 'hex');
|
|
471
474
|
}
|
|
472
475
|
function isHeldRestore(ch) {
|
|
473
|
-
return ch.restoreRecencyUnproven === true
|
|
476
|
+
return (ch.restoreRecencyUnproven === true ||
|
|
477
|
+
ch.reestablishRecencyUnproven === true ||
|
|
478
|
+
ch.reestablishSecretMissing === true ||
|
|
479
|
+
ch.restoreRevokedRisk === true);
|
|
474
480
|
}
|
|
481
|
+
const STALE_STATE_FORCE_CLOSE_REFUSAL = 'This channel was restored from a Recovery Capsule and its state ' +
|
|
482
|
+
'cannot be proven current, so the node will not broadcast its ' +
|
|
483
|
+
'commitment on its own initiative. If the peer holds a newer ' +
|
|
484
|
+
'state, force closing publishes a revoked commitment and the ' +
|
|
485
|
+
'whole channel balance is lost to the justice path. Waiting for ' +
|
|
486
|
+
'the peer to close is the safe outcome. Set ' +
|
|
487
|
+
'acceptStaleStateRisk: true to force close anyway.';
|
|
488
|
+
const REESTABLISH_FORCE_CLOSE_REFUSAL = 'The peer claimed at channel_reestablish that this channel state is ' +
|
|
489
|
+
'behind and showed no proof. Its recency cannot be proven, so the node ' +
|
|
490
|
+
'will not broadcast its commitment on its own initiative. If the claim ' +
|
|
491
|
+
'is true, force closing publishes a revoked commitment and the whole ' +
|
|
492
|
+
'channel balance is lost to the justice path; if the peer is lying, ' +
|
|
493
|
+
'the close is safe. Waiting for the peer to close is the safe outcome. ' +
|
|
494
|
+
'Set acceptStaleStateRisk: true to force close anyway.';
|
|
495
|
+
const SECRET_MISSING_FORCE_CLOSE_REFUSAL = 'This node could not produce the per-commitment secret its own ' +
|
|
496
|
+
'channel_reestablish owes this peer, so local storage is damaged or ' +
|
|
497
|
+
'incomplete and the recency of this channel cannot be proven. The node ' +
|
|
498
|
+
'will not broadcast its commitment on its own initiative. If the stored ' +
|
|
499
|
+
'state is the stale one, force closing publishes a revoked commitment ' +
|
|
500
|
+
'and the whole channel balance is lost to the justice path. Waiting for ' +
|
|
501
|
+
'the peer to close is the safe outcome. Set acceptStaleStateRisk: true ' +
|
|
502
|
+
'to force close anyway.';
|
|
475
503
|
class BeignetNode extends events_1.EventEmitter {
|
|
476
504
|
getOfflineReceive() {
|
|
477
505
|
if (!this.offlineReceive)
|
|
@@ -533,6 +561,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
533
561
|
this._autoApplySettleMs = BeignetNode.DEFAULT_AUTO_APPLY_SETTLE_MS;
|
|
534
562
|
this._autoApplyMaxWaitMs = BeignetNode.DEFAULT_AUTO_APPLY_MAX_WAIT_MS;
|
|
535
563
|
this._bootTargetEmpty = false;
|
|
564
|
+
this._bootTargetEmptyAt = 0;
|
|
536
565
|
this._autoApply = { phase: 'idle' };
|
|
537
566
|
this._resuming = false;
|
|
538
567
|
this._lockPath = null;
|
|
@@ -730,13 +759,14 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
730
759
|
if (this._recoveryAutoApply && this.recoveryMode !== 'peer-storage') {
|
|
731
760
|
throw new errors_1.BeignetError('INVALID_PARAMS', 'recoveryAutoApply applies to peer-storage mode only');
|
|
732
761
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
762
|
+
try {
|
|
763
|
+
(0, recovery_1.assertEmptyTarget)(this.storage);
|
|
764
|
+
this._bootTargetEmpty = true;
|
|
765
|
+
this._bootTargetEmptyAt = Date.now();
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
this._bootTargetEmpty = false;
|
|
769
|
+
if (this._recoveryAutoApply) {
|
|
740
770
|
this.log('info', 'Recovery auto-apply is armed but this database already holds ' +
|
|
741
771
|
'state; capsules storage peers return are reported, not applied');
|
|
742
772
|
}
|
|
@@ -894,6 +924,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
894
924
|
coinType,
|
|
895
925
|
network: lnNetwork,
|
|
896
926
|
storage: this.storage,
|
|
927
|
+
newChannelsRefused: () => this.newChannelRefusal(),
|
|
897
928
|
enableNetworking: true,
|
|
898
929
|
autoReconnect: opts.autoReconnect ?? true,
|
|
899
930
|
autoUpdateChannelFees: opts.autoUpdateChannelFees ?? false,
|
|
@@ -1430,7 +1461,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
1430
1461
|
const hex = channelId.toString('hex');
|
|
1431
1462
|
this.emit('ffor:enforce', {
|
|
1432
1463
|
channelId: hex,
|
|
1433
|
-
epoch: this.fforEpochView(hex, record)
|
|
1464
|
+
epoch: this.fforEpochView(hex, record),
|
|
1465
|
+
...this.recencyHold(channelId)
|
|
1434
1466
|
});
|
|
1435
1467
|
});
|
|
1436
1468
|
this.node.on('recovery:durable', (through) => {
|
|
@@ -2340,6 +2372,42 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
2340
2372
|
lastReason: a.lastReason ?? null
|
|
2341
2373
|
};
|
|
2342
2374
|
}
|
|
2375
|
+
emptyBootRestoreUndecided() {
|
|
2376
|
+
if (!this._bootTargetEmpty)
|
|
2377
|
+
return false;
|
|
2378
|
+
if (!this._recoveryAutoApply || this.recoveryMode !== 'peer-storage') {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
const phase = this._autoApply.phase;
|
|
2382
|
+
if (phase === 'applied' || phase === 'refused')
|
|
2383
|
+
return false;
|
|
2384
|
+
return Date.now() - this._bootTargetEmptyAt < this._autoApplyMaxWaitMs;
|
|
2385
|
+
}
|
|
2386
|
+
newChannelRefusal() {
|
|
2387
|
+
const node = this.node;
|
|
2388
|
+
if (!node) {
|
|
2389
|
+
return 'New channels are refused until this node has finished booting';
|
|
2390
|
+
}
|
|
2391
|
+
const phase = this._autoApply.phase;
|
|
2392
|
+
if (phase === 'settling' || phase === 'applying' || this._resuming) {
|
|
2393
|
+
const stage = this._resuming ? 'rebuilding' : phase;
|
|
2394
|
+
return ('New channels are refused while a Recovery Capsule restore is ' +
|
|
2395
|
+
`unresolved (auto-apply ${stage}): the restored key-index table ` +
|
|
2396
|
+
'decides the next channel key index');
|
|
2397
|
+
}
|
|
2398
|
+
if (this.emptyBootRestoreUndecided()) {
|
|
2399
|
+
return ('New channels are refused while this empty boot waits for a ' +
|
|
2400
|
+
'Recovery Capsule (auto-apply is armed and has not decided): the ' +
|
|
2401
|
+
'capsule carries the key-index table the next channel key index ' +
|
|
2402
|
+
'continues from');
|
|
2403
|
+
}
|
|
2404
|
+
if (node.getCurrentBlockHeight() === 0 &&
|
|
2405
|
+
node.getChannelManager().channelIndexTipFloorArmed) {
|
|
2406
|
+
return ('New channels are refused until the chain tip is known so ' +
|
|
2407
|
+
'recovery can initialize channel keys');
|
|
2408
|
+
}
|
|
2409
|
+
return null;
|
|
2410
|
+
}
|
|
2343
2411
|
get resuming() {
|
|
2344
2412
|
return this._resuming;
|
|
2345
2413
|
}
|
|
@@ -3998,9 +4066,22 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3998
4066
|
}
|
|
3999
4067
|
async fforRecover(body) {
|
|
4000
4068
|
const idBuf = this.fforChannelId(body.channelId);
|
|
4001
|
-
|
|
4069
|
+
if (body.forceCloseIfUnreachable === true &&
|
|
4070
|
+
this.node.getFforEpoch(idBuf.toString('hex'))?.role === 'R') {
|
|
4071
|
+
this.requireForceCloseAcknowledgement(idBuf, body.acceptStaleStateRisk === true);
|
|
4072
|
+
}
|
|
4073
|
+
const r = await this.node
|
|
4074
|
+
.rescueFforEpoch(idBuf.toString('hex'), {
|
|
4002
4075
|
forceCloseIfUnreachable: body.forceCloseIfUnreachable === true,
|
|
4076
|
+
acceptStaleStateRisk: body.acceptStaleStateRisk === true,
|
|
4003
4077
|
destinationScript: this.fforDestinationScript()
|
|
4078
|
+
})
|
|
4079
|
+
.catch((err) => {
|
|
4080
|
+
if (err instanceof types_5.InvalidRequestError) {
|
|
4081
|
+
this.requireForceCloseAcknowledgement(idBuf, body.acceptStaleStateRisk === true);
|
|
4082
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', err.message);
|
|
4083
|
+
}
|
|
4084
|
+
throw err;
|
|
4004
4085
|
});
|
|
4005
4086
|
return {
|
|
4006
4087
|
action: r.action,
|
|
@@ -4060,14 +4141,14 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4060
4141
|
error: status.error ?? null
|
|
4061
4142
|
};
|
|
4062
4143
|
}
|
|
4063
|
-
fforEnforce(channelId) {
|
|
4144
|
+
fforEnforce(channelId, acceptStaleStateRisk = false) {
|
|
4064
4145
|
const idBuf = this.fforChannelId(channelId);
|
|
4065
4146
|
const f = this.node.getFforEpoch(idBuf.toString('hex'));
|
|
4066
4147
|
if (!f || f.role !== 'R') {
|
|
4067
4148
|
throw new errors_1.BeignetError(errors_1.BeignetErrorCode.NOT_FOUND, 'no FFOR epoch of ours on this channel');
|
|
4068
4149
|
}
|
|
4069
4150
|
return {
|
|
4070
|
-
...this.forceCloseChannel(idBuf.toString('hex')),
|
|
4151
|
+
...this.forceCloseChannel(idBuf.toString('hex'), acceptStaleStateRisk),
|
|
4071
4152
|
preimagesKnown: f.knownPreimages.filter((p) => p !== null).length
|
|
4072
4153
|
};
|
|
4073
4154
|
}
|
|
@@ -4120,19 +4201,19 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4120
4201
|
throw new errors_1.BeignetError(errors_1.BeignetErrorCode.CHANNEL_NOT_FOUND, `Channel not found: ${idBuf.toString('hex')}`);
|
|
4121
4202
|
}
|
|
4122
4203
|
const canonicalId = idBuf.toString('hex');
|
|
4123
|
-
const
|
|
4204
|
+
const row = this.node
|
|
4124
4205
|
.getRecoveryStatus()
|
|
4125
|
-
.channels.find((c) => c.channelId === canonicalId)
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
'
|
|
4130
|
-
'
|
|
4131
|
-
'
|
|
4132
|
-
'
|
|
4133
|
-
'
|
|
4134
|
-
|
|
4135
|
-
|
|
4206
|
+
.channels.find((c) => c.channelId === canonicalId);
|
|
4207
|
+
if (row?.restoreRevokedRisk === true) {
|
|
4208
|
+
throw new errors_1.BeignetError(errors_1.BeignetErrorCode.FORCE_CLOSE_REVOKED, "This channel's peer has shown, in channel_reestablish, that it " +
|
|
4209
|
+
'already holds the revocation for the stored commitment. Force ' +
|
|
4210
|
+
'closing would publish a revoked commitment and the whole channel ' +
|
|
4211
|
+
'balance would be lost to the justice path. There is no risk to ' +
|
|
4212
|
+
'accept, so acceptStaleStateRisk does not apply: wait for the ' +
|
|
4213
|
+
'peer to force close, or for its retransmission to bring this ' +
|
|
4214
|
+
'channel level again.');
|
|
4215
|
+
}
|
|
4216
|
+
this.requireForceCloseAcknowledgement(idBuf, acceptStaleStateRisk);
|
|
4136
4217
|
let destinationScript = this.sweepDestinationScript;
|
|
4137
4218
|
if (!destinationScript) {
|
|
4138
4219
|
const bitcoin = require('bitcoinjs-lib');
|
|
@@ -4140,6 +4221,36 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4140
4221
|
}
|
|
4141
4222
|
return this.node.forceCloseChannel(idBuf, destinationScript);
|
|
4142
4223
|
}
|
|
4224
|
+
recencyHold(channelId) {
|
|
4225
|
+
const state = this.node
|
|
4226
|
+
.getChannelManager()
|
|
4227
|
+
.getChannel(channelId)
|
|
4228
|
+
?.getFullState();
|
|
4229
|
+
return {
|
|
4230
|
+
...(state?.restoreRecencyUnproven === true
|
|
4231
|
+
? { restoreRecencyUnproven: true }
|
|
4232
|
+
: {}),
|
|
4233
|
+
...(state?.reestablishRecencyUnproven === true
|
|
4234
|
+
? { reestablishRecencyUnproven: true }
|
|
4235
|
+
: {}),
|
|
4236
|
+
...(state?.reestablishSecretMissing === true
|
|
4237
|
+
? { reestablishSecretMissing: true }
|
|
4238
|
+
: {})
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
requireForceCloseAcknowledgement(channelId, acceptStaleStateRisk) {
|
|
4242
|
+
if (acceptStaleStateRisk === true)
|
|
4243
|
+
return;
|
|
4244
|
+
const hold = this.recencyHold(channelId);
|
|
4245
|
+
if (hold.reestablishSecretMissing) {
|
|
4246
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', SECRET_MISSING_FORCE_CLOSE_REFUSAL);
|
|
4247
|
+
}
|
|
4248
|
+
if (hold.restoreRecencyUnproven || hold.reestablishRecencyUnproven) {
|
|
4249
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', hold.restoreRecencyUnproven
|
|
4250
|
+
? STALE_STATE_FORCE_CLOSE_REFUSAL
|
|
4251
|
+
: REESTABLISH_FORCE_CLOSE_REFUSAL);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4143
4254
|
rebroadcastClose(channelId) {
|
|
4144
4255
|
return this.node.rebroadcastClose(Buffer.from(channelId, 'hex'));
|
|
4145
4256
|
}
|
|
@@ -4179,6 +4290,12 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4179
4290
|
const acceptsNewThrough = channel.acceptsNewHtlcs(true);
|
|
4180
4291
|
if (state.restoreRecencyUnproven === true)
|
|
4181
4292
|
issues.push('HELD_RESTORE: Channel was restored from a Recovery Capsule and its state has not been proven current, so it takes no new HTLCs. Routing hints will be skipped.');
|
|
4293
|
+
if (state.reestablishRecencyUnproven === true)
|
|
4294
|
+
issues.push('HELD_REESTABLISH: The peer claimed at channel_reestablish that this channel state is behind and showed no proof, so the channel is held and takes no new HTLCs. Routing hints will be skipped.');
|
|
4295
|
+
if (state.reestablishSecretMissing === true)
|
|
4296
|
+
issues.push('HELD_SECRET_MISSING: This node could not produce the per-commitment secret its own channel_reestablish owes this peer, so local storage is damaged or incomplete and the channel is held and takes no new HTLCs. Routing hints will be skipped. The store cannot recover the secret, so the exits are the peer closing or an acknowledged force close.');
|
|
4297
|
+
if (state.restoreRevokedRisk === true)
|
|
4298
|
+
issues.push('HELD_REVOKED: The peer proved at channel_reestablish that it already holds the revocation for this commitment, so the channel takes no new HTLCs and no force close of it is permitted, the acknowledged one included. Routing hints will be skipped. It clears when the peer retransmits and this channel levels.');
|
|
4182
4299
|
if (state.fundingUnaccounted === true)
|
|
4183
4300
|
issues.push('FUNDING_UNACCOUNTED: Neither mempool nor chain can account for the funding transaction, so the channel takes no new HTLCs. Routing hints will be skipped. Existing HTLCs still settle, and the quarantine lifts by itself if the funding reappears.');
|
|
4184
4301
|
if (state.state !== 'NORMAL' &&
|
|
@@ -4249,6 +4366,12 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4249
4366
|
info.htlcUsable = ch.htlcUsable;
|
|
4250
4367
|
if (ch.restoreRecencyUnproven)
|
|
4251
4368
|
info.restoreRecencyUnproven = ch.restoreRecencyUnproven;
|
|
4369
|
+
if (ch.reestablishRecencyUnproven)
|
|
4370
|
+
info.reestablishRecencyUnproven = ch.reestablishRecencyUnproven;
|
|
4371
|
+
if (ch.reestablishSecretMissing)
|
|
4372
|
+
info.reestablishSecretMissing = ch.reestablishSecretMissing;
|
|
4373
|
+
if (ch.restoreRevokedRisk)
|
|
4374
|
+
info.restoreRevokedRisk = ch.restoreRevokedRisk;
|
|
4252
4375
|
if (ch.fundingUnaccounted)
|
|
4253
4376
|
info.fundingUnaccounted = ch.fundingUnaccounted;
|
|
4254
4377
|
if (ch.payThroughSplice !== undefined)
|