Skip to content

Commit 1f2d9d1

Browse files
authored
Merge pull request #710 from LIT-Protocol/LIT-4016-epoch-change-events
fix(lit-core): LIT-4016 - Enhance error handling during epoch changes
2 parents b5ccf66 + 75ff638 commit 1f2d9d1

File tree

4 files changed

+67
-41
lines changed

4 files changed

+67
-41
lines changed

local-tests/setup/tinny-environment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ export class TinnyEnvironment {
277277
);
278278
}
279279

280+
this.litNodeClient.on('connected', () => {
281+
console.log(
282+
'Received `connected` event from `litNodeClient. Ready to go!'
283+
);
284+
});
285+
280286
await this.litNodeClient.connect();
281287

282288
if (!this.litNodeClient.ready) {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"date-and-time": "^2.4.1",
6161
"depd": "^2.0.0",
6262
"ethers": "^5.7.1",
63+
"eventemitter3": "^5.0.1",
6364
"jose": "^4.14.4",
6465
"micromodal": "^0.4.10",
6566
"multiformats": "^9.7.1",

packages/core/src/lib/lit-core.ts

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ethers } from 'ethers';
2+
import { EventEmitter } from 'eventemitter3';
23

34
import {
45
canonicalAccessControlConditionFormatter,
@@ -140,7 +141,7 @@ const FALLBACK_RPC_URLS = [
140141
'https://eth.llamarpc.com',
141142
];
142143

143-
export class LitCore {
144+
export class LitCore extends EventEmitter {
144145
config: LitNodeClientConfigWithDefaults = {
145146
alertWhenUnauthorized: false,
146147
debug: true,
@@ -173,6 +174,8 @@ export class LitCore {
173174

174175
// ========== Constructor ==========
175176
constructor(config: LitNodeClientConfig | CustomNetwork) {
177+
super();
178+
176179
if (!(config.litNetwork in LIT_NETWORKS)) {
177180
const validNetworks = Object.keys(LIT_NETWORKS).join(', ');
178181
throw new InvalidParamType(
@@ -300,20 +303,16 @@ export class LitCore {
300303
private async _handleStakingContractStateChange(
301304
state: STAKING_STATES_VALUES
302305
) {
303-
log(`New state detected: "${state}"`);
304-
305-
const validatorData = await this._getValidatorData();
306-
307-
if (state === STAKING_STATES.Active) {
308-
// We always want to track the most recent epoch number on _all_ networks
306+
try {
307+
log(`New state detected: "${state}"`);
309308

310-
this._epochState = await this._fetchCurrentEpochState(
311-
validatorData.epochInfo
312-
);
309+
if (state === STAKING_STATES.Active) {
310+
const validatorData = await this._getValidatorData();
313311

314-
if (CENTRALISATION_BY_NETWORK[this.config.litNetwork] !== 'centralised') {
315312
// We don't need to handle node urls changing on centralised networks, since their validator sets are static
316-
try {
313+
if (
314+
CENTRALISATION_BY_NETWORK[this.config.litNetwork] !== 'centralised'
315+
) {
317316
log(
318317
'State found to be new validator set locked, checking validator set'
319318
);
@@ -322,39 +321,54 @@ export class LitCore {
322321
const delta: string[] = validatorData.bootstrapUrls.filter((item) =>
323322
existingNodeUrls.includes(item)
324323
);
325-
// if the sets differ we reconnect.
324+
325+
// check if the node sets are non-matching and re-connect if they do not.
326326
if (delta.length > 1) {
327-
// check if the node sets are non-matching and re-connect if they do not.
328327
/*
329-
TODO: This covers *most* cases where a node may come in or out of the active
330-
set which we will need to re attest to the execution environments.
331-
However, the sdk currently does not know if there is an active network operation pending.
332-
Such that the state when the request was sent will now mutate when the response is sent back.
333-
The sdk should be able to understand its current execution environment and wait on an active
334-
network request to the previous epoch's node set before changing over.
335-
*/
328+
TODO: This covers *most* cases where a node may come in or out of the active
329+
set which we will need to re attest to the execution environments.
330+
However, the sdk currently does not know if there is an active network operation pending.
331+
Such that the state when the request was sent will now mutate when the response is sent back.
332+
The sdk should be able to understand its current execution environment and wait on an active
333+
network request to the previous epoch's node set before changing over.
334+
*/
336335
log(
337336
'Active validator sets changed, new validators ',
338337
delta,
339338
'starting node connection'
340339
);
340+
await this.connect(); // Will update `epochInfo`
341341
}
342-
343-
await this.connect();
344-
} catch (err: unknown) {
345-
// FIXME: We should emit an error event so that consumers know that we are de-synced and can connect() again
346-
// But for now, our every-30-second network sync will fix things in at most 30s from now.
347-
// this.ready = false; Should we assume core is invalid if we encountered errors refreshing from an epoch change?
348-
const { message = '' } = err as
349-
| Error
350-
| NodeClientErrorV0
351-
| NodeClientErrorV1;
352-
logError(
353-
'Error while attempting to reconnect to nodes after epoch transition:',
354-
message
342+
} else {
343+
// In case of centralised networks, we don't run `connect()` flow, so we will manually update epochInfo here
344+
this._epochState = await this._fetchCurrentEpochState(
345+
validatorData.epochInfo
355346
);
356347
}
357348
}
349+
} catch (err: unknown) {
350+
// Ensure that any methods that check `this.ready` throw errors to the caller, and any consumers can check appropriately
351+
this.ready = false;
352+
353+
const { message = '' } = err as
354+
| Error
355+
| NodeClientErrorV0
356+
| NodeClientErrorV1;
357+
logError(
358+
'Error while attempting to reconnect to nodes after epoch transition:',
359+
message
360+
);
361+
362+
const handshakeError = new Error(
363+
'Error while attempting to reconnect to nodes after epoch transition:' +
364+
message
365+
);
366+
367+
// Signal to any listeners that we've encountered a fatal error
368+
this.emit('error', handshakeError);
369+
370+
// Signal to any listeners that we're 'disconnected' from LIT network
371+
this.emit('disconnected', { reason: 'error', error: handshakeError });
358372
}
359373
}
360374

@@ -399,6 +413,7 @@ export class LitCore {
399413
this._stopListeningForNewEpoch();
400414
// this._stopNetworkPolling();
401415
setMiscLitConfig(undefined);
416+
this.emit('disconnected', { reason: 'disconnect' });
402417
}
403418

404419
// _stopNetworkPolling() {
@@ -477,11 +492,6 @@ export class LitCore {
477492
}
478493

479494
private async _connect() {
480-
// Ensure an ill-timed epoch change event doesn't trigger concurrent config changes while we're already doing that
481-
this._stopListeningForNewEpoch();
482-
// Ensure we don't fire an existing network sync poll handler while we're in the midst of connecting anyway
483-
// this._stopNetworkPolling();
484-
485495
// Initialize a contractContext if we were not given one; this allows interaction against the staking contract
486496
// to be handled locally from then on
487497
if (!this.config.contractContext) {
@@ -523,7 +533,6 @@ export class LitCore {
523533
}
524534

525535
// Re-use staking contract instance from previous connect() executions that succeeded to improve performance
526-
// noinspection ES6MissingAwait - intentionally not `awaiting` so we can run this in parallel below
527536
const validatorData = await this._getValidatorData();
528537

529538
this._stakingContract = validatorData.stakingContract;
@@ -554,6 +563,8 @@ export class LitCore {
554563
latestBlockhash: this.latestBlockhash,
555564
});
556565

566+
this.emit('connected', true);
567+
557568
// browser only
558569
if (isBrowser()) {
559570
document.dispatchEvent(new Event('lit-ready'));

packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ export class LitNodeClientNodeJs
874874
// ========== Validate Params ==========
875875
if (!this.ready) {
876876
const message =
877-
'[executeJs] LitNodeClient is not ready. Please call await litNodeClient.connect() first.';
877+
'[executeJs] LitNodeClient is not ready. Please call await litNodeClient.connect() first.';
878878

879879
throw new LitNodeClientNotReadyError({}, message);
880880
}
@@ -1988,6 +1988,14 @@ export class LitNodeClientNodeJs
19881988

19891989
const signatures: SessionSigsMap = {};
19901990

1991+
if (!this.ready) {
1992+
// If the client isn't ready, `connectedNodes` may be out-of-date, and we should throw an error immediately
1993+
const message =
1994+
'[executeJs] LitNodeClient is not ready. Please call await litNodeClient.connect() first.';
1995+
1996+
throw new LitNodeClientNotReadyError({}, message);
1997+
}
1998+
19911999
this.connectedNodes.forEach((nodeAddress: string) => {
19922000
const toSign: SessionSigningTemplate = {
19932001
...signingTemplate,

0 commit comments

Comments
 (0)