Skip to content

Commit 034c3da

Browse files
committed
feat(health-checks): implement Naga health check system with automated endpoint testing
- Added health check initialization and management for Naga networks. - Implemented tests for key functionalities: Handshake, PKP Sign, Sign Session Key, Execute JS, and Decrypt. - Integrated logging to Lit Status backend for monitoring. - Created GitHub Actions workflow for automated health checks on push and manual triggers. - Updated package.json and pnpm-lock.yaml for new dependencies and scripts.
1 parent 84bb588 commit 034c3da

File tree

7 files changed

+4439
-9145
lines changed

7 files changed

+4439
-9145
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
name: Naga Health Checks
2+
3+
on:
4+
push:
5+
branches:
6+
- feature/jss-29-feature-add-naga-uptime-bot
7+
# Temporarily disabled schedule; re-enable when satisfied
8+
# schedule:
9+
# - cron: '*/5 * * * *'
10+
workflow_dispatch:
11+
inputs:
12+
naga_branch:
13+
description: 'Branch to run health checks from (optional)'
14+
required: true
15+
default: 'naga'
16+
network:
17+
description: 'Specific network to test (leave empty for all)'
18+
required: false
19+
type: choice
20+
options:
21+
- naga-dev
22+
- naga-test
23+
24+
env:
25+
LIT_STATUS_WRITE_KEY: ${{ secrets.LIT_STATUS_WRITE_KEY }}
26+
LIT_STATUS_BACKEND_URL: ${{ vars.LIT_STATUS_BACKEND_URL }}
27+
28+
jobs:
29+
naga-health-check:
30+
runs-on: ubuntu-latest
31+
environment: Health Check
32+
strategy:
33+
# Don't cancel other network tests if one fails
34+
fail-fast: false
35+
matrix:
36+
network: [naga-dev, naga-test]
37+
38+
env:
39+
NETWORK: ${{ matrix.network }}
40+
LIVE_MASTER_ACCOUNT: ${{ matrix.network == 'naga-dev' && secrets.LIVE_MASTER_ACCOUNT_NAGA_DEV || secrets.LIVE_MASTER_ACCOUNT_NAGA_TEST }}
41+
LIT_YELLOWSTONE_PRIVATE_RPC_URL: ${{ vars.LIT_YELLOWSTONE_PRIVATE_RPC_URL }}
42+
43+
steps:
44+
- name: Checkout repository
45+
uses: actions/checkout@v4
46+
with:
47+
# If manually triggered and a branch is provided, use it; otherwise use the triggering ref
48+
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.naga_branch || github.ref }}
49+
fetch-depth: 1
50+
51+
- name: Setup Node.js
52+
uses: actions/setup-node@v4
53+
with:
54+
node-version: '22.18.0'
55+
registry-url: 'https://registry.npmjs.org'
56+
57+
- name: Enable corepack and setup pnpm
58+
run: |
59+
corepack enable
60+
corepack prepare [email protected] --activate
61+
62+
- name: Install dependencies
63+
run: pnpm install --frozen-lockfile
64+
65+
- name: Build project
66+
run: pnpm build
67+
68+
- name: Verify required environment variables
69+
run: |
70+
echo "Checking environment variables for ${{ matrix.network }}..."
71+
if [ -z "${LIVE_MASTER_ACCOUNT}" ]; then
72+
echo "❌ LIVE_MASTER_ACCOUNT is not set for ${{ matrix.network }}"
73+
exit 1
74+
fi
75+
if [ -z "${LIT_STATUS_WRITE_KEY}" ]; then
76+
echo "❌ LIT_STATUS_WRITE_KEY is not set"
77+
exit 1
78+
fi
79+
if [ -z "${LIT_STATUS_BACKEND_URL}" ]; then
80+
echo "❌ LIT_STATUS_BACKEND_URL is not set"
81+
exit 1
82+
fi
83+
echo "✅ All required environment variables are set"
84+
85+
- name: Run health check for ${{ matrix.network }}
86+
# If a specific network is selected via workflow_dispatch input, only run that one
87+
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.network == '' || github.event.inputs.network == matrix.network }}
88+
run: pnpm run ci:health
89+
timeout-minutes: 10
90+
env:
91+
NETWORK: ${{ matrix.network }}
92+
LIVE_MASTER_ACCOUNT: ${{ matrix.network == 'naga-dev' && secrets.LIVE_MASTER_ACCOUNT_NAGA_DEV || secrets.LIVE_MASTER_ACCOUNT_NAGA_TEST }}
93+
LIT_STATUS_WRITE_KEY: ${{ secrets.LIT_STATUS_WRITE_KEY }}
94+
LIT_STATUS_BACKEND_URL: ${{ vars.LIT_STATUS_BACKEND_URL }}
95+
LIT_YELLOWSTONE_PRIVATE_RPC_URL: ${{ vars.LIT_YELLOWSTONE_PRIVATE_RPC_URL }}
96+
LOG_LEVEL: info
97+
98+
- name: Health check summary
99+
if: always()
100+
run: |
101+
if [ ${{ job.status }} == 'success' ]; then
102+
echo "✅ Health check passed for ${{ matrix.network }}"
103+
echo "Time: $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
104+
else
105+
echo "❌ Health check failed for ${{ matrix.network }}"
106+
echo "Time: $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
107+
echo "Please check the logs above for details"
108+
fi
109+

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
"format:check": "npx nx format:check --all",
1414
"test:e2e": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 -t",
1515
"test:custom": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000",
16-
"test:e2e:ci": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} npx jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --runTestsByPath"
16+
"test:e2e:ci": "npx jest --clearCache --config ./jest.e2e.config.ts && LOG_LEVEL=${LOG_LEVEL:-silent} npx jest --runInBand --detectOpenHandles --forceExit --config ./jest.e2e.config.ts --testTimeout=50000000 --runTestsByPath",
17+
"test:health": "LOG_LEVEL=${LOG_LEVEL:-silent} dotenvx run --env-file=.env -- tsx packages/e2e/src/health/index.ts",
18+
"ci:health": "LOG_LEVEL=${LOG_LEVEL:-silent} tsx packages/e2e/src/health/index.ts"
1719
},
1820
"private": true,
1921
"dependencies": {
2022
"@babel/core": "^7.28.4",
2123
"@babel/preset-env": "^7.28.3",
2224
"@babel/preset-typescript": "^7.27.1",
2325
"@dotenvx/dotenvx": "^1.6.4",
26+
"@lit-protocol/lit-status-sdk": "^0.1.8",
2427
"@lit-protocol/nacl": "7.1.1",
2528
"@lit-protocol/uint8arrays": "7.1.1",
2629
"@metamask/eth-sig-util": "5.0.2",
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/**
2+
* Naga Health Manager
3+
*
4+
* This module implements health checks for Naga network endpoints.
5+
* It tests the core functionality of the Lit Protocol network by executing
6+
* a series of endpoint tests using a single test account.
7+
*
8+
* Tested Endpoints:
9+
* 1. Handshake - Verifies basic node connectivity
10+
* 2. PKP Sign - Tests PKP signing functionality
11+
* 3. Sign Session Key - Tests session key signing (via PKP auth context creation)
12+
* 4. Execute JS - Tests Lit Actions execution
13+
* 5. Decrypt - Tests encryption and decryption flow
14+
*
15+
* Usage:
16+
* const manager = new NagaHealthManager(ctx);
17+
* await manager.handshakeTest();
18+
* await manager.pkpSignTest();
19+
* // ... other tests
20+
*/
21+
22+
import { initHealthCheck } from './health-init';
23+
import { createAccBuilder } from '@lit-protocol/access-control-conditions';
24+
25+
type HealthCheckContext = Awaited<ReturnType<typeof initHealthCheck>>;
26+
27+
export class NagaHealthManager {
28+
private ctx: HealthCheckContext;
29+
30+
constructor(ctx: HealthCheckContext) {
31+
this.ctx = ctx;
32+
}
33+
34+
/**
35+
* Test 1: Handshake Test
36+
*
37+
* Verifies basic connectivity to Lit nodes by checking if the client
38+
* is properly initialized and connected.
39+
*
40+
* This is the most basic health check - if this fails, the network is down.
41+
*/
42+
handshakeTest = async (): Promise<void> => {
43+
try {
44+
// Fetch current context which includes the latest handshake result
45+
const ctx = await this.ctx.litClient.getContext();
46+
47+
if (!ctx?.handshakeResult) {
48+
throw new Error('Handshake result missing from client context');
49+
}
50+
51+
const { serverKeys, connectedNodes, threshold } = ctx.handshakeResult;
52+
53+
const numServers = serverKeys ? Object.keys(serverKeys).length : 0;
54+
const numConnected = connectedNodes ? connectedNodes.size : 0;
55+
56+
if (numServers === 0) {
57+
throw new Error('No server keys received during handshake');
58+
}
59+
60+
if (typeof threshold === 'number' && numConnected < threshold) {
61+
throw new Error(
62+
`Connected nodes (${numConnected}) below threshold (${threshold})`
63+
);
64+
}
65+
66+
console.log('✅ Handshake test passed');
67+
} catch (e) {
68+
console.error('❌ Handshake test failed:', e);
69+
throw e;
70+
}
71+
};
72+
73+
/**
74+
* Test 2: PKP Sign Test
75+
*
76+
* Tests the PKP signing endpoint by signing a simple message.
77+
* This verifies that:
78+
* - The PKP is accessible
79+
* - The auth context is valid
80+
* - The signing endpoint is operational
81+
*/
82+
pkpSignTest = async (): Promise<void> => {
83+
try {
84+
const testMessage = 'Hello from Naga health check!';
85+
86+
const result = await this.ctx.litClient.chain.ethereum.pkpSign({
87+
authContext: this.ctx.aliceEoaAuthContext,
88+
pubKey: this.ctx.aliceViemAccountPkp.pubkey,
89+
toSign: testMessage,
90+
});
91+
92+
if (!result.signature) {
93+
throw new Error('No signature returned from pkpSign');
94+
}
95+
96+
console.log('✅ PKP Sign test passed');
97+
} catch (e) {
98+
console.error('❌ PKP Sign test failed:', e);
99+
throw e;
100+
}
101+
};
102+
103+
/**
104+
* Test 3: Sign Session Key Test
105+
*
106+
* Tests the session key signing endpoint by creating a PKP auth context.
107+
* This involves the signSessionKey endpoint which is critical for
108+
* establishing authenticated sessions with PKPs.
109+
*/
110+
signSessionKeyTest = async (): Promise<void> => {
111+
try {
112+
// Creating a PKP auth context involves calling the signSessionKey endpoint
113+
const pkpAuthContext = await this.ctx.authManager.createPkpAuthContext({
114+
authData: this.ctx.aliceViemAccountAuthData,
115+
pkpPublicKey: this.ctx.aliceViemAccountPkp.pubkey,
116+
authConfig: {
117+
resources: [
118+
['pkp-signing', '*'],
119+
['lit-action-execution', '*'],
120+
],
121+
expiration: new Date(Date.now() + 1000 * 60 * 15).toISOString(), // 15 minutes
122+
},
123+
litClient: this.ctx.litClient,
124+
});
125+
126+
if (!pkpAuthContext) {
127+
throw new Error('Failed to create PKP auth context');
128+
}
129+
130+
console.log('✅ Sign Session Key test passed');
131+
} catch (e) {
132+
console.error('❌ Sign Session Key test failed:', e);
133+
throw e;
134+
}
135+
};
136+
137+
/**
138+
* Test 4: Execute JS Test
139+
*
140+
* Tests Lit Actions execution by running a simple JavaScript code
141+
* that performs an ECDSA signature.
142+
*
143+
* This verifies:
144+
* - Lit Actions runtime is operational
145+
* - Code execution environment is working
146+
* - Signing within actions works
147+
*/
148+
executeJsTest = async (): Promise<void> => {
149+
try {
150+
const litActionCode = `
151+
(async () => {
152+
const { sigName, toSign, publicKey } = jsParams;
153+
const { keccak256, arrayify } = ethers.utils;
154+
155+
const toSignBytes = new TextEncoder().encode(toSign);
156+
const toSignBytes32 = keccak256(toSignBytes);
157+
const toSignBytes32Array = arrayify(toSignBytes32);
158+
159+
const sigShare = await Lit.Actions.signEcdsa({
160+
toSign: toSignBytes32Array,
161+
publicKey,
162+
sigName,
163+
});
164+
})();`;
165+
166+
const result = await this.ctx.litClient.executeJs({
167+
code: litActionCode,
168+
authContext: this.ctx.aliceEoaAuthContext,
169+
jsParams: {
170+
sigName: 'health-check-sig',
171+
toSign: 'Health check message',
172+
publicKey: this.ctx.aliceViemAccountPkp.pubkey,
173+
},
174+
});
175+
176+
if (!result || !result.signatures) {
177+
throw new Error('No signatures returned from executeJs');
178+
}
179+
180+
console.log('✅ Execute JS test passed');
181+
} catch (e) {
182+
console.error('❌ Execute JS test failed:', e);
183+
throw e;
184+
}
185+
};
186+
187+
/**
188+
* Test 5: Decrypt Test
189+
*
190+
* Tests the encryption and decryption flow:
191+
* 1. Encrypts data with access control conditions
192+
* 2. Decrypts the data using the same account
193+
*
194+
* This verifies:
195+
* - Encryption endpoint works
196+
* - Access control condition evaluation works
197+
* - Decryption endpoint works
198+
* - End-to-end encryption flow is operational
199+
*/
200+
decryptTest = async (): Promise<void> => {
201+
try {
202+
const testData = 'Secret health check data';
203+
204+
// Create access control conditions for Alice's wallet
205+
const builder = createAccBuilder();
206+
const accs = builder
207+
.requireWalletOwnership(this.ctx.aliceViemAccount.address)
208+
.on('ethereum')
209+
.build();
210+
211+
// Encrypt the data
212+
const encryptedData = await this.ctx.litClient.encrypt({
213+
dataToEncrypt: testData,
214+
unifiedAccessControlConditions: accs,
215+
chain: 'ethereum',
216+
});
217+
218+
if (!encryptedData.ciphertext || !encryptedData.dataToEncryptHash) {
219+
throw new Error('Encryption failed - missing ciphertext or hash');
220+
}
221+
222+
// Decrypt the data
223+
const decryptedData = await this.ctx.litClient.decrypt({
224+
data: encryptedData,
225+
unifiedAccessControlConditions: accs,
226+
chain: 'ethereum',
227+
authContext: this.ctx.aliceEoaAuthContext,
228+
});
229+
230+
if (!decryptedData.convertedData) {
231+
throw new Error('Decryption failed - no converted data');
232+
}
233+
234+
// Verify the decrypted data matches
235+
if (decryptedData.convertedData !== testData) {
236+
throw new Error(
237+
`Decryption mismatch: expected "${testData}", got "${decryptedData.convertedData}"`
238+
);
239+
}
240+
241+
console.log('✅ Decrypt test passed');
242+
} catch (e) {
243+
console.error('❌ Decrypt test failed:', e);
244+
throw e;
245+
}
246+
};
247+
}
248+

0 commit comments

Comments
 (0)