beignet 0.9.2 → 0.9.3
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/README.md +1 -1
- package/dist/cli/auth.js +2 -0
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/beignet-node.js +688 -43
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/cli/cli.js +25 -2
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.js +6 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/daemon.js +49 -2
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/openapi.js +47 -4
- package/dist/cli/openapi.js.map +1 -1
- package/dist/lightning/node/lightning-node.js +18 -0
- package/dist/lightning/node/lightning-node.js.map +1 -1
- package/dist/lightning/node/types.js.map +1 -1
- package/dist/lightning/recovery/assembly.js +67 -7
- package/dist/lightning/recovery/assembly.js.map +1 -1
- package/dist/lightning/recovery/capsule.js +73 -5
- package/dist/lightning/recovery/capsule.js.map +1 -1
- package/dist/lightning/recovery/guardian-client.js +5 -1
- package/dist/lightning/recovery/guardian-client.js.map +1 -1
- package/dist/lightning/recovery/startup-gate.js +42 -18
- package/dist/lightning/recovery/startup-gate.js.map +1 -1
- package/dist/types/cli/beignet-node.d.ts +83 -2
- package/dist/types/cli/types.d.ts +5 -1
- package/dist/types/lightning/node/lightning-node.d.ts +3 -0
- package/dist/types/lightning/node/types.d.ts +2 -0
- package/dist/types/lightning/recovery/assembly.d.ts +13 -0
- package/dist/types/lightning/recovery/capsule.d.ts +5 -0
- package/dist/types/lightning/recovery/guardian-client.d.ts +1 -0
- package/dist/types/lightning/recovery/startup-gate.d.ts +2 -0
- package/docs/RECOVERY-PROTOCOL.md +102 -1
- package/package.json +1 -1
package/dist/cli/beignet-node.js
CHANGED
|
@@ -54,6 +54,8 @@ const electrum_backend_1 = require("../lightning/chain/electrum-backend");
|
|
|
54
54
|
const types_2 = require("../lightning/invoice/types");
|
|
55
55
|
const wallet_keys_1 = require("../lightning/keys/wallet-keys");
|
|
56
56
|
const recovery_1 = require("../lightning/recovery");
|
|
57
|
+
const ecdh_1 = require("../lightning/crypto/ecdh");
|
|
58
|
+
const auth_1 = require("./auth");
|
|
57
59
|
const types_3 = require("../lightning/channel/types");
|
|
58
60
|
const decode_1 = require("../lightning/invoice/decode");
|
|
59
61
|
const decode_2 = require("../lightning/offer/decode");
|
|
@@ -68,6 +70,40 @@ function parseRecoveryMode(value) {
|
|
|
68
70
|
: 'off';
|
|
69
71
|
}
|
|
70
72
|
exports.parseRecoveryMode = parseRecoveryMode;
|
|
73
|
+
function redactUrl(url) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(url);
|
|
76
|
+
if (parsed.username === '' && parsed.password === '')
|
|
77
|
+
return url;
|
|
78
|
+
parsed.username = '';
|
|
79
|
+
parsed.password = '';
|
|
80
|
+
return parsed.toString();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return '<invalid url>';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function redactGuardians(descriptors) {
|
|
87
|
+
return descriptors.map((g) => ({
|
|
88
|
+
guardianId: g.guardianId,
|
|
89
|
+
transports: g.transports.map((t) => ({
|
|
90
|
+
type: t.type,
|
|
91
|
+
url: redactUrl(t.url)
|
|
92
|
+
}))
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function sameGuardianIds(a, b) {
|
|
96
|
+
if (a.length !== b.length)
|
|
97
|
+
return false;
|
|
98
|
+
const sortedA = a.map((id) => id.toLowerCase()).sort();
|
|
99
|
+
const sortedB = b.map((id) => id.toLowerCase()).sort();
|
|
100
|
+
return sortedA.every((id, i) => id === sortedB[i]);
|
|
101
|
+
}
|
|
102
|
+
function writeFileAtomic(filePath, content) {
|
|
103
|
+
const tmp = `${filePath}.tmp`;
|
|
104
|
+
fs.writeFileSync(tmp, content);
|
|
105
|
+
fs.renameSync(tmp, filePath);
|
|
106
|
+
}
|
|
71
107
|
const RESTORE_ERROR_CODES = {
|
|
72
108
|
'no-quorum': 'RESTORE_NO_QUORUM',
|
|
73
109
|
'unknown-namespace': 'RESTORE_UNKNOWN_NAMESPACE',
|
|
@@ -224,6 +260,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
224
260
|
this.recoveryGuardianSet = [];
|
|
225
261
|
this._restorePending = false;
|
|
226
262
|
this._restoreInFlight = false;
|
|
263
|
+
this._leaseCheckInFlight = false;
|
|
264
|
+
this._leaseCheckIntervalMs = BeignetNode.DEFAULT_LEASE_CHECK_INTERVAL_MS;
|
|
227
265
|
this._lockPath = null;
|
|
228
266
|
this._lockExitHandler = null;
|
|
229
267
|
this.destroyed = false;
|
|
@@ -246,6 +284,10 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
246
284
|
this._draining = false;
|
|
247
285
|
this.peerStorageEnabled = true;
|
|
248
286
|
this._peerRetrievedScb = null;
|
|
287
|
+
this._peerRetrievedCapsules = new Map();
|
|
288
|
+
this._pushedChannelBackup = false;
|
|
289
|
+
this._restartRequired = false;
|
|
290
|
+
this._recoveryFenceRelayed = false;
|
|
249
291
|
this.mnemonic = mnemonic;
|
|
250
292
|
this.networkName = networkName;
|
|
251
293
|
this.dataDir = dataDir;
|
|
@@ -277,6 +319,14 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
277
319
|
this.peerStorageEnabled = opts.peerStorageEnabled ?? true;
|
|
278
320
|
this.rapidGossipSync = opts.rapidGossipSync ?? true;
|
|
279
321
|
this.rapidGossipSyncUrl = opts.rapidGossipSyncUrl;
|
|
322
|
+
if (opts.recoveryLeaseCheckIntervalMs !== undefined) {
|
|
323
|
+
const ms = opts.recoveryLeaseCheckIntervalMs;
|
|
324
|
+
if (!Number.isInteger(ms) || ms < 0 || ms > BeignetNode.MAX_TIMER_MS) {
|
|
325
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', `recoveryLeaseCheckIntervalMs must be an integer between 0 and ` +
|
|
326
|
+
`${BeignetNode.MAX_TIMER_MS} (got ${String(ms)})`);
|
|
327
|
+
}
|
|
328
|
+
this._leaseCheckIntervalMs = ms;
|
|
329
|
+
}
|
|
280
330
|
const networkName = this.networkName;
|
|
281
331
|
if (!opts.allowMultipleInstances) {
|
|
282
332
|
const lockPath = path.join(this.dataDir, `${networkName}.lock`);
|
|
@@ -316,6 +366,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
316
366
|
if (opts.storageEncryption ?? true) {
|
|
317
367
|
encryptionKey = (0, encryption_1.deriveStorageKey)(bip39.mnemonicToSeedSync(this.mnemonic));
|
|
318
368
|
}
|
|
369
|
+
this._storageEncryptionKey = encryptionKey;
|
|
370
|
+
this.finishStagedCapsuleRestore(dbPath);
|
|
319
371
|
this.storage = new sqlite_storage_1.SqliteStorage(dbPath, (err) => {
|
|
320
372
|
this.log('warn', 'Skipped corrupted storage row during load', {
|
|
321
373
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -701,16 +753,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
701
753
|
this.node.on('recovery:fenced', (superseding) => {
|
|
702
754
|
this.log('error', 'Recovery fenced: a newer writer epoch owns this namespace; ' +
|
|
703
755
|
'this device must not send another channel message');
|
|
704
|
-
this.
|
|
705
|
-
supersededBy: superseding
|
|
706
|
-
? {
|
|
707
|
-
epoch: superseding.lease.epoch.toString(),
|
|
708
|
-
writerPublicKey: superseding.lease.writerPublicKey.toString('hex'),
|
|
709
|
-
sequence: superseding.logHead.sequence.toString(),
|
|
710
|
-
frameHash: superseding.logHead.frameHash.toString('hex')
|
|
711
|
-
}
|
|
712
|
-
: null
|
|
713
|
-
});
|
|
756
|
+
this.relayRecoveryFenced(superseding);
|
|
714
757
|
});
|
|
715
758
|
this.node.on('recovery:backfill-lost', (detail) => {
|
|
716
759
|
this.log('error', 'Recovery backfill lost', { detail });
|
|
@@ -796,13 +839,11 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
796
839
|
this.recoveryNodeConfig = { enabled: true, durability: 'local' };
|
|
797
840
|
return;
|
|
798
841
|
}
|
|
799
|
-
const guardians = (opts.recoveryGuardians ?? []).map(recovery_1.
|
|
842
|
+
const guardians = (opts.recoveryGuardians ?? []).map(recovery_1.parseGuardianEntry);
|
|
800
843
|
this.recoveryGuardianSet = guardians;
|
|
801
|
-
const coinType = this.toCoinType(this.networkName);
|
|
802
|
-
const keys = (0, wallet_keys_1.deriveLightningKeysFromMnemonic)(this.mnemonic, undefined, coinType);
|
|
803
844
|
const decision = await (0, recovery_1.buildGuardianRecovery)({
|
|
804
845
|
storage: this.storage,
|
|
805
|
-
nodeSecret:
|
|
846
|
+
nodeSecret: this.nodeSecret(),
|
|
806
847
|
durability: mode,
|
|
807
848
|
guardians,
|
|
808
849
|
onBarrierEvent: (event) => {
|
|
@@ -855,19 +896,11 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
855
896
|
this.log('info', 'Recovery gate confirmed; peer traffic released', {
|
|
856
897
|
confirming: outcome.confirming
|
|
857
898
|
});
|
|
899
|
+
this.startRecoveryLeaseCheck();
|
|
858
900
|
return;
|
|
859
901
|
}
|
|
860
902
|
if (outcome.state === 'fenced') {
|
|
861
|
-
this.
|
|
862
|
-
supersededBy: outcome.supersededBy
|
|
863
|
-
? {
|
|
864
|
-
epoch: outcome.supersededBy.lease.epoch.toString(),
|
|
865
|
-
writerPublicKey: outcome.supersededBy.lease.writerPublicKey.toString('hex'),
|
|
866
|
-
sequence: outcome.supersededBy.logHead.sequence.toString(),
|
|
867
|
-
frameHash: outcome.supersededBy.logHead.frameHash.toString('hex')
|
|
868
|
-
}
|
|
869
|
-
: null
|
|
870
|
-
});
|
|
903
|
+
this.relayRecoveryFenced(outcome.supersededBy);
|
|
871
904
|
return;
|
|
872
905
|
}
|
|
873
906
|
this.log('warn', 'Recovery gate still quarantined; retrying', {
|
|
@@ -889,6 +922,57 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
889
922
|
};
|
|
890
923
|
void attempt();
|
|
891
924
|
}
|
|
925
|
+
relayRecoveryFenced(supersededBy) {
|
|
926
|
+
this.stopRecoveryLeaseCheck();
|
|
927
|
+
if (this._recoveryFenceRelayed)
|
|
928
|
+
return;
|
|
929
|
+
this._recoveryFenceRelayed = true;
|
|
930
|
+
this.emit('recovery:fenced', {
|
|
931
|
+
supersededBy: supersededBy
|
|
932
|
+
? {
|
|
933
|
+
epoch: supersededBy.lease.epoch.toString(),
|
|
934
|
+
writerPublicKey: supersededBy.lease.writerPublicKey.toString('hex'),
|
|
935
|
+
sequence: supersededBy.logHead.sequence.toString(),
|
|
936
|
+
frameHash: supersededBy.logHead.frameHash.toString('hex')
|
|
937
|
+
}
|
|
938
|
+
: null
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
startRecoveryLeaseCheck() {
|
|
942
|
+
const run = this._recoveryRun;
|
|
943
|
+
if (!run || this._leaseCheckTimer || this._leaseCheckIntervalMs <= 0) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
this._leaseCheckTimer = setInterval(() => {
|
|
947
|
+
if (this.destroyed || this._leaseCheckInFlight)
|
|
948
|
+
return;
|
|
949
|
+
this._leaseCheckInFlight = true;
|
|
950
|
+
run
|
|
951
|
+
.recheck()
|
|
952
|
+
.then((outcome) => {
|
|
953
|
+
if (outcome.state !== 'fenced')
|
|
954
|
+
return;
|
|
955
|
+
this.log('error', 'Recovery lease check: a newer writer epoch owns this ' +
|
|
956
|
+
'namespace; this device is fenced');
|
|
957
|
+
this.relayRecoveryFenced(outcome.supersededBy);
|
|
958
|
+
})
|
|
959
|
+
.catch((err) => {
|
|
960
|
+
this.log('debug', 'Recovery lease check failed', {
|
|
961
|
+
error: err instanceof Error ? err.message : String(err)
|
|
962
|
+
});
|
|
963
|
+
})
|
|
964
|
+
.finally(() => {
|
|
965
|
+
this._leaseCheckInFlight = false;
|
|
966
|
+
});
|
|
967
|
+
}, this._leaseCheckIntervalMs);
|
|
968
|
+
this._leaseCheckTimer.unref?.();
|
|
969
|
+
}
|
|
970
|
+
stopRecoveryLeaseCheck() {
|
|
971
|
+
if (this._leaseCheckTimer) {
|
|
972
|
+
clearInterval(this._leaseCheckTimer);
|
|
973
|
+
this._leaseCheckTimer = undefined;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
892
976
|
get restorePending() {
|
|
893
977
|
return this._restorePending;
|
|
894
978
|
}
|
|
@@ -900,13 +984,30 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
900
984
|
const profile = this.recoveryMode === 'async-remote' || this.recoveryMode === 'quorum'
|
|
901
985
|
? 'crash-v1'
|
|
902
986
|
: null;
|
|
987
|
+
const capsules = this.describeRetrievedCapsules();
|
|
988
|
+
const restore = {
|
|
989
|
+
inProgress: this._restoreInFlight,
|
|
990
|
+
...(this._lastRestoreEvent ? { lastEvent: this._lastRestoreEvent } : {})
|
|
991
|
+
};
|
|
992
|
+
if (this._restartRequired) {
|
|
993
|
+
return {
|
|
994
|
+
mode: this.recoveryMode,
|
|
995
|
+
profile,
|
|
996
|
+
guardians,
|
|
997
|
+
state: 'restart-required',
|
|
998
|
+
node: null,
|
|
999
|
+
capsules,
|
|
1000
|
+
restore
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
903
1003
|
if (this.recoveryMode === 'off') {
|
|
904
1004
|
return {
|
|
905
1005
|
mode: 'off',
|
|
906
1006
|
profile: null,
|
|
907
1007
|
guardians: [],
|
|
908
1008
|
state: 'disabled',
|
|
909
|
-
node: null
|
|
1009
|
+
node: null,
|
|
1010
|
+
capsules
|
|
910
1011
|
};
|
|
911
1012
|
}
|
|
912
1013
|
if (this._restorePending) {
|
|
@@ -916,12 +1017,8 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
916
1017
|
guardians,
|
|
917
1018
|
state: this._restoreInFlight ? 'restoring' : 'restore-required',
|
|
918
1019
|
node: null,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
...(this._lastRestoreEvent
|
|
922
|
-
? { lastEvent: this._lastRestoreEvent }
|
|
923
|
-
: {})
|
|
924
|
-
}
|
|
1020
|
+
capsules,
|
|
1021
|
+
restore
|
|
925
1022
|
};
|
|
926
1023
|
}
|
|
927
1024
|
const node = this.node.getRecoveryStatus();
|
|
@@ -930,9 +1027,402 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
930
1027
|
profile,
|
|
931
1028
|
guardians,
|
|
932
1029
|
state: node.fenced || node.gate === 'fenced' ? 'fenced' : 'running',
|
|
933
|
-
node
|
|
1030
|
+
node,
|
|
1031
|
+
capsules
|
|
934
1032
|
};
|
|
935
1033
|
}
|
|
1034
|
+
get restartRequired() {
|
|
1035
|
+
return this._restartRequired;
|
|
1036
|
+
}
|
|
1037
|
+
describeRetrievedCapsules() {
|
|
1038
|
+
let best = null;
|
|
1039
|
+
for (const [fromPeer, c] of this._peerRetrievedCapsules) {
|
|
1040
|
+
if (!best ||
|
|
1041
|
+
c.writerEpoch > best.epoch ||
|
|
1042
|
+
(c.writerEpoch === best.epoch && c.latestSequence > best.sequence) ||
|
|
1043
|
+
(c.writerEpoch === best.epoch &&
|
|
1044
|
+
c.latestSequence === best.sequence &&
|
|
1045
|
+
c.inline &&
|
|
1046
|
+
!best.inline)) {
|
|
1047
|
+
best = {
|
|
1048
|
+
epoch: c.writerEpoch,
|
|
1049
|
+
sequence: c.latestSequence,
|
|
1050
|
+
writerEpoch: c.writerEpoch.toString(),
|
|
1051
|
+
latestSequence: c.latestSequence.toString(),
|
|
1052
|
+
inline: c.inline,
|
|
1053
|
+
channelCount: c.channelCount,
|
|
1054
|
+
guardians: redactGuardians(c.guardians),
|
|
1055
|
+
fromPeer,
|
|
1056
|
+
receivedAt: c.receivedAt
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (!best)
|
|
1061
|
+
return { candidates: 0, best: null };
|
|
1062
|
+
const { epoch: _e, sequence: _s, ...rest } = best;
|
|
1063
|
+
return { candidates: this._peerRetrievedCapsules.size, best: rest };
|
|
1064
|
+
}
|
|
1065
|
+
revealCapsuleGuardians() {
|
|
1066
|
+
const best = this.describeRetrievedCapsules().best;
|
|
1067
|
+
if (!best) {
|
|
1068
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_NO_CANDIDATES', 'No storage peer has returned a Recovery Capsule this session. ' +
|
|
1069
|
+
'Connect to the peers this node had channels with and retry.');
|
|
1070
|
+
}
|
|
1071
|
+
const held = this._peerRetrievedCapsules.get(best.fromPeer);
|
|
1072
|
+
return {
|
|
1073
|
+
fromPeer: best.fromPeer,
|
|
1074
|
+
head: {
|
|
1075
|
+
writerEpoch: best.writerEpoch,
|
|
1076
|
+
latestSequence: best.latestSequence
|
|
1077
|
+
},
|
|
1078
|
+
guardians: held.guardians,
|
|
1079
|
+
entries: held.guardians.map((g) => ({
|
|
1080
|
+
guardianId: g.guardianId,
|
|
1081
|
+
url: g.transports[0].url,
|
|
1082
|
+
...(g.auth ? { auth: g.auth } : {})
|
|
1083
|
+
}))
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
async restoreFromCapsules(options = {}) {
|
|
1087
|
+
if (this._restoreInFlight) {
|
|
1088
|
+
throw new errors_1.BeignetError('RESTORE_IN_PROGRESS', 'A restore is already running; poll the status route.');
|
|
1089
|
+
}
|
|
1090
|
+
if (options.unfenced !== undefined &&
|
|
1091
|
+
typeof options.unfenced !== 'boolean') {
|
|
1092
|
+
throw new errors_1.BeignetError('INVALID_PARAMS', 'unfenced must be a boolean');
|
|
1093
|
+
}
|
|
1094
|
+
const unfencedRequested = options.unfenced === true;
|
|
1095
|
+
if (this.recoveryMode !== 'peer-storage' || this._restorePending) {
|
|
1096
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_UNSUPPORTED', 'Capsule restore applies to peer-storage mode only. Guardian ' +
|
|
1097
|
+
'modes restore through the guardian set (the /recovery/restore ' +
|
|
1098
|
+
'route); with recovery off there is no journal to restore, so ' +
|
|
1099
|
+
'use the SCB a peer returned (/backup/peer-retrieved) with the ' +
|
|
1100
|
+
'SCB restore route instead.');
|
|
1101
|
+
}
|
|
1102
|
+
if (this._peerRetrievedCapsules.size === 0) {
|
|
1103
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_NO_CANDIDATES', 'No storage peer has returned a Recovery Capsule this session. ' +
|
|
1104
|
+
'Connect to the peers this node had channels with and retry.');
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
(0, recovery_1.assertEmptyTarget)(this.storage);
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_TARGET_DIRTY', 'This database already holds channel, payment or invoice state ' +
|
|
1111
|
+
'that a restore would discard; run the restore from a fresh ' +
|
|
1112
|
+
'data directory (' +
|
|
1113
|
+
(err instanceof Error ? err.message : String(err)) +
|
|
1114
|
+
')');
|
|
1115
|
+
}
|
|
1116
|
+
this._restoreInFlight = true;
|
|
1117
|
+
const progress = (type, detail) => {
|
|
1118
|
+
this._lastRestoreEvent = { type, detail };
|
|
1119
|
+
this.log('info', `Capsule restore: ${type}`, { detail });
|
|
1120
|
+
this.emit('recovery:restore-progress', { type, detail });
|
|
1121
|
+
};
|
|
1122
|
+
const stagedPath = path.join(this.dataDir, `${this.networkName}.db.capsule-restore`);
|
|
1123
|
+
const dropStaged = () => {
|
|
1124
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
1125
|
+
try {
|
|
1126
|
+
fs.unlinkSync(`${stagedPath}${suffix}`);
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
try {
|
|
1133
|
+
const blobs = [...this._peerRetrievedCapsules.values()].map((c) => c.blob);
|
|
1134
|
+
progress('capsule:selecting', `${blobs.length} candidate capsule(s) from storage peers`);
|
|
1135
|
+
dropStaged();
|
|
1136
|
+
const installFailure = (err) => err instanceof recovery_1.CapsuleCandidateError
|
|
1137
|
+
? new errors_1.BeignetError('CAPSULE_RESTORE_FAILED', `No retrieved capsule could be restored: ${err.message}`)
|
|
1138
|
+
: new errors_1.BeignetError('CAPSULE_RESTORE_INSTALL_FAILED', `Could not write the restored database: ${err instanceof Error ? err.message : String(err)}`);
|
|
1139
|
+
let staged;
|
|
1140
|
+
try {
|
|
1141
|
+
staged = new sqlite_storage_1.SqliteStorage(stagedPath, () => {
|
|
1142
|
+
}, this._storageEncryptionKey
|
|
1143
|
+
? { encryptionKey: this._storageEncryptionKey }
|
|
1144
|
+
: undefined);
|
|
1145
|
+
staged.open();
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
dropStaged();
|
|
1149
|
+
throw installFailure(err);
|
|
1150
|
+
}
|
|
1151
|
+
let result;
|
|
1152
|
+
try {
|
|
1153
|
+
result = (0, recovery_1.restoreBestRecoveryCapsule)(blobs, staged, this.nodeSecret(), {
|
|
1154
|
+
scratchStorage: () => {
|
|
1155
|
+
const scratch = new sqlite_storage_1.SqliteStorage(':memory:');
|
|
1156
|
+
scratch.open();
|
|
1157
|
+
return scratch;
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
const expectedNetwork = this.toLnNetwork(this.networkName);
|
|
1161
|
+
if (result.scb.network !== expectedNetwork) {
|
|
1162
|
+
throw new recovery_1.CapsuleCandidateError(`capsule SCB network "${result.scb.network}" does not match ` +
|
|
1163
|
+
`this node's network "${expectedNetwork}"`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
catch (err) {
|
|
1167
|
+
staged.close();
|
|
1168
|
+
dropStaged();
|
|
1169
|
+
throw installFailure(err);
|
|
1170
|
+
}
|
|
1171
|
+
const head = {
|
|
1172
|
+
writerEpoch: result.capsule.writerEpoch.toString(),
|
|
1173
|
+
latestSequence: result.capsule.latestSequence.toString()
|
|
1174
|
+
};
|
|
1175
|
+
const newestSeenHead = {
|
|
1176
|
+
writerEpoch: result.newestSeenHead.writerEpoch.toString(),
|
|
1177
|
+
latestSequence: result.newestSeenHead.latestSequence.toString()
|
|
1178
|
+
};
|
|
1179
|
+
if (result.tier === 2 &&
|
|
1180
|
+
(0, recovery_1.chainPromisedQuorum)(staged, (0, recovery_1.deriveRecoveryMasterKey)(this.nodeSecret()), (0, ecdh_1.getPublicKey)(this.nodeSecret()))) {
|
|
1181
|
+
staged.close();
|
|
1182
|
+
dropStaged();
|
|
1183
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_QUORUM_NAMESPACE', 'The best capsule carries a quorum-durability journal, which ' +
|
|
1184
|
+
'cannot boot without its guardian quorum even unfenced; ' +
|
|
1185
|
+
'restore through the guardian set, or recover the channels ' +
|
|
1186
|
+
'from the peer-retrieved backup with the SCB restore route.');
|
|
1187
|
+
}
|
|
1188
|
+
let unfenced;
|
|
1189
|
+
if (result.capsule.guardians.length > 0) {
|
|
1190
|
+
const locators = redactGuardians(result.capsule.guardians);
|
|
1191
|
+
if (!unfencedRequested) {
|
|
1192
|
+
staged.close();
|
|
1193
|
+
dropStaged();
|
|
1194
|
+
this.log('warn', 'Capsule restore refused: capsule names a guardian set', {
|
|
1195
|
+
guardians: locators
|
|
1196
|
+
});
|
|
1197
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_GUARDIAN_BACKED', `The best capsule names ${locators.length} guardian(s): its state ` +
|
|
1198
|
+
'belongs to a guardian-backed namespace, which restores through ' +
|
|
1199
|
+
'the guardian set with fencing, not from peer storage. Restart ' +
|
|
1200
|
+
'in that recovery mode with the guardians the capsule names ' +
|
|
1201
|
+
'(capsules.best.guardians on the status route, or the ' +
|
|
1202
|
+
'capsule-guardians handoff when they need credentials); for an ' +
|
|
1203
|
+
'emergency SCB-only recovery use the peer-retrieved backup with ' +
|
|
1204
|
+
'the SCB restore route. An operator whose guardian set is gone ' +
|
|
1205
|
+
'can pass unfenced: true, which cannot fence the old writer.');
|
|
1206
|
+
}
|
|
1207
|
+
unfenced = { guardians: locators };
|
|
1208
|
+
this.log('warn', 'UNFENCED capsule restore: the capsule names a guardian set and ' +
|
|
1209
|
+
'the previous writer is not fenced; if it still runs, it keeps ' +
|
|
1210
|
+
'acting on these channels', { guardians: locators });
|
|
1211
|
+
progress('capsule:unfenced', `restoring a guardian-backed capsule without fencing: the previous writer is NOT fenced (${locators.length} guardian(s) named)`);
|
|
1212
|
+
}
|
|
1213
|
+
const common = {
|
|
1214
|
+
channelCount: result.scb.channels.length,
|
|
1215
|
+
framesApplied: result.framesApplied,
|
|
1216
|
+
head,
|
|
1217
|
+
newestSeenHead,
|
|
1218
|
+
rejectedCandidates: result.rejectedCandidates,
|
|
1219
|
+
...(unfenced ? { unfenced } : {})
|
|
1220
|
+
};
|
|
1221
|
+
if (result.tier === 1) {
|
|
1222
|
+
staged.close();
|
|
1223
|
+
dropStaged();
|
|
1224
|
+
progress('capsule:tier1', `no inline journal validated; recovering ${common.channelCount} channel(s) from the embedded SCB`);
|
|
1225
|
+
const { recovering, skipped } = await this.node.recoverFromStaticChannelBackup(result.scb.channels);
|
|
1226
|
+
const info = {
|
|
1227
|
+
tier: 1,
|
|
1228
|
+
...common,
|
|
1229
|
+
restartRequired: false,
|
|
1230
|
+
recovering,
|
|
1231
|
+
skipped
|
|
1232
|
+
};
|
|
1233
|
+
progress('restore:complete', `Tier 1: ${recovering.length} channel(s) recovering, ${skipped.length} skipped`);
|
|
1234
|
+
this.emit('recovery:restored', {
|
|
1235
|
+
exact: false,
|
|
1236
|
+
framesApplied: 0,
|
|
1237
|
+
guardiansRepaired: 0,
|
|
1238
|
+
epoch: head.writerEpoch,
|
|
1239
|
+
tier: 1,
|
|
1240
|
+
restartRequired: false
|
|
1241
|
+
});
|
|
1242
|
+
return info;
|
|
1243
|
+
}
|
|
1244
|
+
try {
|
|
1245
|
+
this.carryDaemonState(this.storage, staged);
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
staged.close();
|
|
1249
|
+
dropStaged();
|
|
1250
|
+
throw installFailure(err);
|
|
1251
|
+
}
|
|
1252
|
+
staged.close();
|
|
1253
|
+
progress('capsule:installed', `Tier 2: ${result.framesApplied} frame(s) replayed at epoch ${head.writerEpoch} sequence ${head.latestSequence}`);
|
|
1254
|
+
const dbPath = path.join(this.dataDir, `${this.networkName}.db`);
|
|
1255
|
+
const keptPath = `${dbPath}.pre-capsule-restore-${new Date()
|
|
1256
|
+
.toISOString()
|
|
1257
|
+
.replace(/[:.]/g, '-')}`;
|
|
1258
|
+
const marker = {
|
|
1259
|
+
version: 1,
|
|
1260
|
+
stagedAt: Date.now(),
|
|
1261
|
+
staged: path.basename(stagedPath),
|
|
1262
|
+
keep: path.basename(keptPath),
|
|
1263
|
+
head,
|
|
1264
|
+
tier: 2
|
|
1265
|
+
};
|
|
1266
|
+
try {
|
|
1267
|
+
writeFileAtomic(this.capsuleRestoreMarkerPath(), JSON.stringify(marker));
|
|
1268
|
+
}
|
|
1269
|
+
catch (err) {
|
|
1270
|
+
dropStaged();
|
|
1271
|
+
throw installFailure(err);
|
|
1272
|
+
}
|
|
1273
|
+
this._restartRequired = true;
|
|
1274
|
+
this.teardownNodeForRestart();
|
|
1275
|
+
try {
|
|
1276
|
+
this.finishStagedCapsuleRestore(dbPath);
|
|
1277
|
+
}
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
this.log('error', 'Capsule restore swap did not complete', {
|
|
1280
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1281
|
+
});
|
|
1282
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_INSTALL_FAILED', 'The restored database was written but the file swap did not ' +
|
|
1283
|
+
'complete; restart the daemon, which finishes the install (' +
|
|
1284
|
+
(err instanceof Error ? err.message : String(err)) +
|
|
1285
|
+
')');
|
|
1286
|
+
}
|
|
1287
|
+
const info = {
|
|
1288
|
+
tier: 2,
|
|
1289
|
+
...common,
|
|
1290
|
+
restartRequired: true
|
|
1291
|
+
};
|
|
1292
|
+
progress('restore:complete', `restored database installed (previous kept at ${path.basename(keptPath)}); restart the daemon to resume ${common.channelCount} channel(s)`);
|
|
1293
|
+
this.emit('recovery:restored', {
|
|
1294
|
+
exact: true,
|
|
1295
|
+
framesApplied: result.framesApplied,
|
|
1296
|
+
guardiansRepaired: 0,
|
|
1297
|
+
epoch: head.writerEpoch,
|
|
1298
|
+
tier: 2,
|
|
1299
|
+
restartRequired: true
|
|
1300
|
+
});
|
|
1301
|
+
return info;
|
|
1302
|
+
}
|
|
1303
|
+
finally {
|
|
1304
|
+
this._restoreInFlight = false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
teardownNodeForRestart() {
|
|
1308
|
+
if (this.backupTimer) {
|
|
1309
|
+
clearInterval(this.backupTimer);
|
|
1310
|
+
this.backupTimer = undefined;
|
|
1311
|
+
}
|
|
1312
|
+
if (this._sweepRefreshTimer) {
|
|
1313
|
+
clearInterval(this._sweepRefreshTimer);
|
|
1314
|
+
this._sweepRefreshTimer = undefined;
|
|
1315
|
+
}
|
|
1316
|
+
if (this._fallbackRecoveryTimer) {
|
|
1317
|
+
clearInterval(this._fallbackRecoveryTimer);
|
|
1318
|
+
this._fallbackRecoveryTimer = undefined;
|
|
1319
|
+
}
|
|
1320
|
+
this.paymentQueue?.removeAllListeners();
|
|
1321
|
+
this.node.destroy();
|
|
1322
|
+
void this.wallet?.stop().catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
capsuleRestoreMarkerPath() {
|
|
1326
|
+
return path.join(this.dataDir, `${this.networkName}.capsule-restore.json`);
|
|
1327
|
+
}
|
|
1328
|
+
carryDaemonState(from, to) {
|
|
1329
|
+
const overrides = from.loadWalletData(auth_1.AUTH_KEY_OVERRIDES_STORAGE_KEY);
|
|
1330
|
+
if (overrides !== null) {
|
|
1331
|
+
to.saveWalletData(auth_1.AUTH_KEY_OVERRIDES_STORAGE_KEY, overrides);
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
for (const hook of from.loadAllWebhooks()) {
|
|
1335
|
+
to.saveWebhook(hook.id, hook.url, hook.events, hook.secretHash, hook.createdAt);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
catch (err) {
|
|
1339
|
+
this.log('warn', 'Could not carry webhooks into the restore', {
|
|
1340
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
for (const peer of from.loadAllPeerAddresses()) {
|
|
1345
|
+
to.savePeerAddress(peer.pubkey, peer.host, peer.port);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
this.log('warn', 'Could not carry peer addresses into the restore', {
|
|
1350
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
finishStagedCapsuleRestore(dbPath) {
|
|
1355
|
+
const markerPath = this.capsuleRestoreMarkerPath();
|
|
1356
|
+
if (!fs.existsSync(markerPath))
|
|
1357
|
+
return;
|
|
1358
|
+
let marker;
|
|
1359
|
+
try {
|
|
1360
|
+
marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
1361
|
+
if (marker.version !== 1 ||
|
|
1362
|
+
typeof marker.staged !== 'string' ||
|
|
1363
|
+
typeof marker.keep !== 'string') {
|
|
1364
|
+
throw new Error('unrecognized marker shape');
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
catch (err) {
|
|
1368
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_INSTALL_FAILED', `Unreadable capsule restore marker at ${markerPath} (${err instanceof Error ? err.message : String(err)}); inspect the data directory before starting`);
|
|
1369
|
+
}
|
|
1370
|
+
const stagedPath = path.join(this.dataDir, marker.staged);
|
|
1371
|
+
const keptPath = path.join(this.dataDir, marker.keep);
|
|
1372
|
+
const sidecars = ['-wal', '-shm'];
|
|
1373
|
+
const moveAside = () => {
|
|
1374
|
+
const current = new sqlite_storage_1.SqliteStorage(dbPath, () => {
|
|
1375
|
+
}, this._storageEncryptionKey
|
|
1376
|
+
? { encryptionKey: this._storageEncryptionKey }
|
|
1377
|
+
: undefined);
|
|
1378
|
+
current.open();
|
|
1379
|
+
try {
|
|
1380
|
+
(0, recovery_1.assertEmptyTarget)(current);
|
|
1381
|
+
}
|
|
1382
|
+
finally {
|
|
1383
|
+
current.close();
|
|
1384
|
+
}
|
|
1385
|
+
fs.renameSync(dbPath, keptPath);
|
|
1386
|
+
for (const suffix of sidecars) {
|
|
1387
|
+
try {
|
|
1388
|
+
fs.renameSync(`${dbPath}${suffix}`, `${keptPath}${suffix}`);
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
const stagedExists = fs.existsSync(stagedPath);
|
|
1395
|
+
const dbExists = fs.existsSync(dbPath);
|
|
1396
|
+
if (stagedExists) {
|
|
1397
|
+
if (dbExists)
|
|
1398
|
+
moveAside();
|
|
1399
|
+
for (const suffix of sidecars) {
|
|
1400
|
+
try {
|
|
1401
|
+
fs.unlinkSync(`${stagedPath}${suffix}`);
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
fs.renameSync(stagedPath, dbPath);
|
|
1407
|
+
this.log('info', 'Capsule restore installed', {
|
|
1408
|
+
head: marker.head,
|
|
1409
|
+
previous: marker.keep
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
else if (!dbExists) {
|
|
1413
|
+
fs.unlinkSync(markerPath);
|
|
1414
|
+
throw new errors_1.BeignetError('CAPSULE_RESTORE_INSTALL_FAILED', `Capsule restore marker found but neither ${marker.staged} nor ` +
|
|
1415
|
+
`the database exists; the previous database may be at ` +
|
|
1416
|
+
`${marker.keep}. Inspect the data directory before starting`);
|
|
1417
|
+
}
|
|
1418
|
+
fs.unlinkSync(markerPath);
|
|
1419
|
+
}
|
|
1420
|
+
nodeSecret() {
|
|
1421
|
+
if (!this._nodeSecret) {
|
|
1422
|
+
this._nodeSecret = (0, wallet_keys_1.deriveLightningKeysFromMnemonic)(this.mnemonic, undefined, this.toCoinType(this.networkName)).nodePrivateKey;
|
|
1423
|
+
}
|
|
1424
|
+
return this._nodeSecret;
|
|
1425
|
+
}
|
|
936
1426
|
async restoreFromGuardians() {
|
|
937
1427
|
if (this._restoreInFlight) {
|
|
938
1428
|
throw new errors_1.BeignetError('RESTORE_IN_PROGRESS', 'A guardian restore is already running; watch ' +
|
|
@@ -3404,6 +3894,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3404
3894
|
? 'Channel balances are healthy'
|
|
3405
3895
|
: `All ${readyChannels.length} channel(s) are >90% depleted in one direction`
|
|
3406
3896
|
});
|
|
3897
|
+
checks.push(this.channelBackupReadiness());
|
|
3407
3898
|
let score = 100;
|
|
3408
3899
|
let hasCriticalFailure = false;
|
|
3409
3900
|
for (const check of checks) {
|
|
@@ -3425,6 +3916,65 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3425
3916
|
checks
|
|
3426
3917
|
};
|
|
3427
3918
|
}
|
|
3919
|
+
channelBackupReadiness() {
|
|
3920
|
+
const name = 'CHANNEL_BACKUP';
|
|
3921
|
+
const mode = this.recoveryMode;
|
|
3922
|
+
if (mode === 'off') {
|
|
3923
|
+
return {
|
|
3924
|
+
name,
|
|
3925
|
+
status: 'WARN',
|
|
3926
|
+
severity: 'WARNING',
|
|
3927
|
+
message: 'No recovery mode: a lost device can only force-close its ' +
|
|
3928
|
+
'channels from the SCB (set BEIGNET_RECOVERY_MODE)'
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
const status = this.node.getRecoveryStatus();
|
|
3932
|
+
if (status.fenced || status.gate === 'fenced') {
|
|
3933
|
+
return {
|
|
3934
|
+
name,
|
|
3935
|
+
status: 'FAIL',
|
|
3936
|
+
severity: 'CRITICAL',
|
|
3937
|
+
message: 'Another device owns this recovery namespace; this node is ' +
|
|
3938
|
+
'fenced and must not operate'
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
if (status.backfillLost) {
|
|
3942
|
+
return {
|
|
3943
|
+
name,
|
|
3944
|
+
status: 'FAIL',
|
|
3945
|
+
severity: 'CRITICAL',
|
|
3946
|
+
message: 'This recovery namespace lost its guardian backfill; no ' +
|
|
3947
|
+
'further channel state can be proven durable'
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
if (mode === 'peer-storage') {
|
|
3951
|
+
return {
|
|
3952
|
+
name,
|
|
3953
|
+
status: 'WARN',
|
|
3954
|
+
severity: 'WARNING',
|
|
3955
|
+
message: 'Recovery capsules go to storage peers only (Tier 2 restore ' +
|
|
3956
|
+
'via /recovery/restore-capsule); no guardian quorum, so no ' +
|
|
3957
|
+
'fencing between devices'
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
if (status.gate !== 'confirmed') {
|
|
3961
|
+
return {
|
|
3962
|
+
name,
|
|
3963
|
+
status: 'WARN',
|
|
3964
|
+
severity: 'WARNING',
|
|
3965
|
+
message: `Guardian mode ${mode}: the quorum has not confirmed this ` +
|
|
3966
|
+
'lease yet; channels stay quarantined'
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
return {
|
|
3970
|
+
name,
|
|
3971
|
+
status: 'PASS',
|
|
3972
|
+
severity: 'WARNING',
|
|
3973
|
+
message: `Channel state replicated to a guardian quorum (${mode}, durable ` +
|
|
3974
|
+
`through ${status.lastDurableSequence}); a lost device resumes ` +
|
|
3975
|
+
'its channels'
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3428
3978
|
getLiquiditySnapshot() {
|
|
3429
3979
|
const snapshot = this.node.getLiquiditySnapshot();
|
|
3430
3980
|
let reserveMsat = 0n;
|
|
@@ -3954,15 +4504,21 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3954
4504
|
return { recovering, skipped, channelCount: backup.channels.length };
|
|
3955
4505
|
}
|
|
3956
4506
|
refreshStaticChannelBackup() {
|
|
3957
|
-
if (this.destroyed)
|
|
4507
|
+
if (this.destroyed || this._restartRequired)
|
|
3958
4508
|
return;
|
|
3959
4509
|
try {
|
|
3960
|
-
const { encoded } = this.exportStaticChannelBackup();
|
|
3961
|
-
if (this.peerStorageEnabled)
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
4510
|
+
const { encoded, channelCount } = this.exportStaticChannelBackup();
|
|
4511
|
+
if (!this.peerStorageEnabled)
|
|
4512
|
+
return;
|
|
4513
|
+
if (this.recoveryNodeConfig?.enabled)
|
|
4514
|
+
return;
|
|
4515
|
+
if (channelCount === 0 && !this._pushedChannelBackup)
|
|
4516
|
+
return;
|
|
4517
|
+
const sent = this.node.distributePeerStorage(Buffer.from(encoded, 'utf8'));
|
|
4518
|
+
if (channelCount > 0)
|
|
4519
|
+
this._pushedChannelBackup = true;
|
|
4520
|
+
if (sent > 0) {
|
|
4521
|
+
this.log('debug', 'Pushed SCB to peer storage', { peers: sent });
|
|
3966
4522
|
}
|
|
3967
4523
|
}
|
|
3968
4524
|
catch (err) {
|
|
@@ -3972,6 +4528,66 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3972
4528
|
}
|
|
3973
4529
|
}
|
|
3974
4530
|
handleRetrievedPeerStorage(peerPubkey, blob) {
|
|
4531
|
+
const capsule = (0, recovery_1.decodeRecoveryCapsuleBlob)(blob, this.nodeSecret());
|
|
4532
|
+
if (capsule) {
|
|
4533
|
+
let embedded;
|
|
4534
|
+
try {
|
|
4535
|
+
embedded = (0, scb_1.decodeScb)(capsule.encryptedScb, this.nodeSecret());
|
|
4536
|
+
}
|
|
4537
|
+
catch {
|
|
4538
|
+
this.log('debug', 'Ignoring capsule whose embedded SCB does not decode', {
|
|
4539
|
+
fromPeer: peerPubkey
|
|
4540
|
+
});
|
|
4541
|
+
return;
|
|
4542
|
+
}
|
|
4543
|
+
if (!this.retrievedBackupMatchesNetwork(embedded, peerPubkey, 'capsule')) {
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
const held = this._peerRetrievedCapsules.get(peerPubkey);
|
|
4547
|
+
const inline = capsule.inlineRecoveryState !== undefined;
|
|
4548
|
+
if (!held ||
|
|
4549
|
+
capsule.writerEpoch > held.writerEpoch ||
|
|
4550
|
+
(capsule.writerEpoch === held.writerEpoch &&
|
|
4551
|
+
capsule.latestSequence > held.latestSequence) ||
|
|
4552
|
+
(capsule.writerEpoch === held.writerEpoch &&
|
|
4553
|
+
capsule.latestSequence === held.latestSequence &&
|
|
4554
|
+
(inline || !held.inline))) {
|
|
4555
|
+
this._peerRetrievedCapsules.set(peerPubkey, {
|
|
4556
|
+
blob: Buffer.from(blob),
|
|
4557
|
+
writerEpoch: capsule.writerEpoch,
|
|
4558
|
+
latestSequence: capsule.latestSequence,
|
|
4559
|
+
inline,
|
|
4560
|
+
channelCount: embedded.channels.length,
|
|
4561
|
+
guardians: capsule.guardians,
|
|
4562
|
+
receivedAt: Date.now()
|
|
4563
|
+
});
|
|
4564
|
+
this.log('info', 'Recovered capsule from peer storage', {
|
|
4565
|
+
fromPeer: peerPubkey,
|
|
4566
|
+
writerEpoch: capsule.writerEpoch.toString(),
|
|
4567
|
+
latestSequence: capsule.latestSequence.toString(),
|
|
4568
|
+
inline,
|
|
4569
|
+
channelCount: embedded.channels.length,
|
|
4570
|
+
guardians: capsule.guardians.length
|
|
4571
|
+
});
|
|
4572
|
+
if (this.recoveryGuardianSet.length > 0 &&
|
|
4573
|
+
!sameGuardianIds(capsule.guardians.map((g) => g.guardianId), this.recoveryGuardianSet.map((g) => g.guardianId.toString('hex'))) &&
|
|
4574
|
+
(!held ||
|
|
4575
|
+
!sameGuardianIds(capsule.guardians.map((g) => g.guardianId), held.guardians.map((g) => g.guardianId)))) {
|
|
4576
|
+
this.log('warn', 'Retrieved capsule names a guardian set that differs from ' +
|
|
4577
|
+
'the configured one; the configured set stays in force', {
|
|
4578
|
+
fromPeer: peerPubkey,
|
|
4579
|
+
capsule: redactGuardians(capsule.guardians),
|
|
4580
|
+
configured: this.recoveryGuardianSet.map((g) => ({
|
|
4581
|
+
guardianId: g.guardianId.toString('hex'),
|
|
4582
|
+
url: g.url
|
|
4583
|
+
}))
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
const seed = bip39.mnemonicToSeedSync(this.mnemonic);
|
|
4588
|
+
this.offerRetrievedScb((0, scb_1.encodeScb)(embedded, seed), embedded, peerPubkey, 'capsule');
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
3975
4591
|
let backup;
|
|
3976
4592
|
const encoded = blob.toString('utf8');
|
|
3977
4593
|
try {
|
|
@@ -3984,17 +4600,42 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
3984
4600
|
});
|
|
3985
4601
|
return;
|
|
3986
4602
|
}
|
|
3987
|
-
if (this.
|
|
3988
|
-
this._peerRetrievedScb.createdAt >= backup.createdAt) {
|
|
4603
|
+
if (!this.retrievedBackupMatchesNetwork(backup, peerPubkey, 'scb'))
|
|
3989
4604
|
return;
|
|
4605
|
+
this.offerRetrievedScb(encoded, backup, peerPubkey, 'scb');
|
|
4606
|
+
}
|
|
4607
|
+
retrievedBackupMatchesNetwork(backup, fromPeer, source) {
|
|
4608
|
+
const expected = this.toLnNetwork(this.networkName);
|
|
4609
|
+
if (backup.network === expected)
|
|
4610
|
+
return true;
|
|
4611
|
+
this.log('debug', 'Ignoring peer storage backup from another network', {
|
|
4612
|
+
fromPeer,
|
|
4613
|
+
source,
|
|
4614
|
+
network: backup.network,
|
|
4615
|
+
expected
|
|
4616
|
+
});
|
|
4617
|
+
return false;
|
|
4618
|
+
}
|
|
4619
|
+
offerRetrievedScb(encoded, backup, fromPeer, source) {
|
|
4620
|
+
const held = this._peerRetrievedScb;
|
|
4621
|
+
if (held) {
|
|
4622
|
+
if (backup.channels.length === 0 && held.channelCount > 0)
|
|
4623
|
+
return;
|
|
4624
|
+
if (held.createdAt >= backup.createdAt &&
|
|
4625
|
+
!(held.channelCount === 0 && backup.channels.length > 0)) {
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
3990
4628
|
}
|
|
3991
4629
|
this._peerRetrievedScb = {
|
|
3992
4630
|
encoded,
|
|
3993
4631
|
createdAt: backup.createdAt,
|
|
3994
|
-
fromPeer
|
|
4632
|
+
fromPeer,
|
|
4633
|
+
channelCount: backup.channels.length,
|
|
4634
|
+
source
|
|
3995
4635
|
};
|
|
3996
4636
|
this.log('info', 'Recovered SCB from peer storage', {
|
|
3997
|
-
fromPeer
|
|
4637
|
+
fromPeer,
|
|
4638
|
+
source,
|
|
3998
4639
|
createdAt: backup.createdAt,
|
|
3999
4640
|
channelCount: backup.channels.length
|
|
4000
4641
|
});
|
|
@@ -4036,6 +4677,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4036
4677
|
clearTimeout(this._confirmTimer);
|
|
4037
4678
|
this._confirmTimer = undefined;
|
|
4038
4679
|
}
|
|
4680
|
+
this.stopRecoveryLeaseCheck();
|
|
4039
4681
|
if (this._backupPromise) {
|
|
4040
4682
|
await this._backupPromise.catch(() => {
|
|
4041
4683
|
});
|
|
@@ -4070,6 +4712,7 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4070
4712
|
clearTimeout(this._confirmTimer);
|
|
4071
4713
|
this._confirmTimer = undefined;
|
|
4072
4714
|
}
|
|
4715
|
+
this.stopRecoveryLeaseCheck();
|
|
4073
4716
|
this.paymentQueue?.removeAllListeners();
|
|
4074
4717
|
this.node?.destroy();
|
|
4075
4718
|
this.storage.close();
|
|
@@ -4139,4 +4782,6 @@ class BeignetNode extends events_1.EventEmitter {
|
|
|
4139
4782
|
}
|
|
4140
4783
|
exports.BeignetNode = BeignetNode;
|
|
4141
4784
|
BeignetNode.INITIAL_GOSSIP_PRIME_TIMEOUT_MS = 90000;
|
|
4785
|
+
BeignetNode.DEFAULT_LEASE_CHECK_INTERVAL_MS = 5 * 60000;
|
|
4786
|
+
BeignetNode.MAX_TIMER_MS = 2147483647;
|
|
4142
4787
|
//# sourceMappingURL=beignet-node.js.map
|