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.
Files changed (52) hide show
  1. package/dist/cli/beignet-node.js +147 -24
  2. package/dist/cli/beignet-node.js.map +1 -1
  3. package/dist/cli/daemon.js +4 -2
  4. package/dist/cli/daemon.js.map +1 -1
  5. package/dist/cli/errors.js +3 -0
  6. package/dist/cli/errors.js.map +1 -1
  7. package/dist/cli/ffor-receive.js +5 -3
  8. package/dist/cli/ffor-receive.js.map +1 -1
  9. package/dist/cli/offline-receive.js +122 -38
  10. package/dist/cli/offline-receive.js.map +1 -1
  11. package/dist/cli/openapi.js +50 -15
  12. package/dist/cli/openapi.js.map +1 -1
  13. package/dist/lightning/advisor/liquidity-advisor.js +4 -1
  14. package/dist/lightning/advisor/liquidity-advisor.js.map +1 -1
  15. package/dist/lightning/channel/channel-manager.js +261 -152
  16. package/dist/lightning/channel/channel-manager.js.map +1 -1
  17. package/dist/lightning/channel/channel-state.js +20 -2
  18. package/dist/lightning/channel/channel-state.js.map +1 -1
  19. package/dist/lightning/channel/channel.js +150 -35
  20. package/dist/lightning/channel/channel.js.map +1 -1
  21. package/dist/lightning/channel/types.js.map +1 -1
  22. package/dist/lightning/direct-funding/receiver/engine.js +20 -13
  23. package/dist/lightning/direct-funding/receiver/engine.js.map +1 -1
  24. package/dist/lightning/direct-funding/receiver/types.js.map +1 -1
  25. package/dist/lightning/node/lightning-node.js +259 -19
  26. package/dist/lightning/node/lightning-node.js.map +1 -1
  27. package/dist/lightning/node/types.js.map +1 -1
  28. package/dist/lightning/recovery/channel-status.js +2 -0
  29. package/dist/lightning/recovery/channel-status.js.map +1 -1
  30. package/dist/lightning/storage/serialization.js +6 -0
  31. package/dist/lightning/storage/serialization.js.map +1 -1
  32. package/dist/lightning/storage/sqlite-storage.js +6 -0
  33. package/dist/lightning/storage/sqlite-storage.js.map +1 -1
  34. package/dist/lightning/storage/types.js.map +1 -1
  35. package/dist/types/cli/beignet-node.d.ts +7 -1
  36. package/dist/types/cli/errors.d.ts +2 -0
  37. package/dist/types/cli/offline-receive.d.ts +24 -2
  38. package/dist/types/cli/types.d.ts +7 -1
  39. package/dist/types/lightning/advisor/liquidity-advisor.d.ts +3 -0
  40. package/dist/types/lightning/channel/channel-manager.d.ts +13 -0
  41. package/dist/types/lightning/channel/channel-state.d.ts +16 -1
  42. package/dist/types/lightning/channel/channel.d.ts +7 -0
  43. package/dist/types/lightning/direct-funding/receiver/engine.d.ts +1 -0
  44. package/dist/types/lightning/node/lightning-node.d.ts +14 -0
  45. package/dist/types/lightning/node/types.d.ts +4 -0
  46. package/dist/types/lightning/recovery/channel-status.d.ts +2 -0
  47. package/dist/types/lightning/storage/serialization.d.ts +4 -1
  48. package/dist/types/lightning/storage/sqlite-storage.d.ts +1 -0
  49. package/dist/types/lightning/storage/types.d.ts +1 -0
  50. package/docs/AUTOMATIC-OFFLINE-RECEIVE.md +46 -15
  51. package/docs/RECOVERY-PROTOCOL.md +39 -0
  52. package/package.json +1 -1
@@ -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
- if (this._recoveryAutoApply) {
734
- try {
735
- (0, recovery_1.assertEmptyTarget)(this.storage);
736
- this._bootTargetEmpty = true;
737
- }
738
- catch {
739
- this._bootTargetEmpty = false;
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
- const r = await this.node.rescueFforEpoch(idBuf.toString('hex'), {
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 held = this.node
4204
+ const row = this.node
4124
4205
  .getRecoveryStatus()
4125
- .channels.find((c) => c.channelId === canonicalId)
4126
- ?.restoreRecencyUnproven;
4127
- if (held === true && acceptStaleStateRisk !== true) {
4128
- throw new errors_1.BeignetError('INVALID_PARAMS', 'This channel was restored from a Recovery Capsule and its state ' +
4129
- 'cannot be proven current, so the node will not broadcast its ' +
4130
- 'commitment on its own initiative. If the peer holds a newer ' +
4131
- 'state, force closing publishes a revoked commitment and the ' +
4132
- 'whole channel balance is lost to the justice path. Waiting for ' +
4133
- 'the peer to close is the safe outcome. Set ' +
4134
- 'acceptStaleStateRisk: true to force close anyway.');
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)