@nuggetslife/vc 0.3.1 → 0.4.0

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/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare function bbs2023Sign(options: any): any
11
11
  export declare function bbs2023Derive(options: any): any
12
12
  export declare function bbs2023HolderCommit(options: any): any
13
13
  export declare function bbs2023BlindSign(options: any): any
14
- export declare function bbs2023Verify(document: any, publicKey?: string | undefined | null): Bbs2023VerifyOutput
14
+ export declare function bbs2023Verify(document: any, publicKey?: string | undefined | null, additionalContexts?: any | undefined | null): Bbs2023VerifyOutput
15
15
  export interface BbsIetfKeyPair {
16
16
  /** Hex-encoded secret key (32 bytes) */
17
17
  secretKey: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuggetslife/vc",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -42,11 +42,10 @@
42
42
  },
43
43
  "packageManager": "yarn@4.3.1",
44
44
  "optionalDependencies": {
45
- "@nuggetslife/vc-darwin-arm64": "0.3.1",
46
- "@nuggetslife/vc-linux-arm64-gnu": "0.3.1",
47
- "@nuggetslife/vc-linux-arm64-musl": "0.3.1",
48
- "@nuggetslife/vc-linux-x64-gnu": "0.3.1",
49
- "@nuggetslife/vc-linux-x64-musl": "0.3.1"
50
- },
51
- "dependencies": {}
45
+ "@nuggetslife/vc-darwin-arm64": "0.4.0",
46
+ "@nuggetslife/vc-linux-arm64-gnu": "0.4.0",
47
+ "@nuggetslife/vc-linux-arm64-musl": "0.4.0",
48
+ "@nuggetslife/vc-linux-x64-gnu": "0.4.0",
49
+ "@nuggetslife/vc-linux-x64-musl": "0.4.0"
50
+ }
52
51
  }
package/src/bbs_2023.rs CHANGED
@@ -95,12 +95,24 @@ pub fn bbs2023_blind_sign(options: Value) -> napi::Result<Value> {
95
95
  pub fn bbs2023_verify(
96
96
  document: Value,
97
97
  public_key: Option<String>,
98
+ additional_contexts: Option<Value>,
98
99
  ) -> napi::Result<Bbs2023VerifyOutput> {
99
100
  let rt = tokio::runtime::Runtime::new()
100
101
  .map_err(|e| napi::Error::from_reason(format!("bbs2023Verify runtime: {e}")))?;
101
102
 
103
+ // LR-3 (issue #107 v1.14.1): caller-registered contexts forwarded to
104
+ // canonicalize. Accepts an optional JSON map; absence is treated as
105
+ // the empty map (bundled-static loader only).
106
+ let extras = additional_contexts
107
+ .map(|v| {
108
+ serde_json::from_value::<std::collections::HashMap<String, Value>>(v).map_err(|e| {
109
+ napi::Error::from_reason(format!("bbs2023Verify parse additional_contexts: {e}"))
110
+ })
111
+ })
112
+ .transpose()?;
113
+
102
114
  let result = rt
103
- .block_on(vc::bbs_2023::verify_proof(document, public_key))
115
+ .block_on(vc::bbs_2023::verify_proof(document, public_key, extras))
104
116
  .map_err(|e| napi::Error::from_reason(format!("bbs2023Verify failed: {e}")))?;
105
117
 
106
118
  Ok(Bbs2023VerifyOutput {
@@ -0,0 +1,304 @@
1
+ // Concurrent worker stress test for clientffi.
2
+ //
3
+ // The v1.13.0 SIGSEGV that motivated test_worker_teardown.mjs was triggered
4
+ // by the addon being dlopen'd into multiple Worker isolates. That regression
5
+ // test only exercises sequential worker startup/teardown (w1 exits before
6
+ // w2 starts) with no real work. This test runs N workers (default 4)
7
+ // concurrently, each tight-looping sign → verify → derive_proof →
8
+ // verify_derived against fixture VCs, including at least one VC whose
9
+ // `@context` resolution depends on the caller-registered
10
+ // `additional_contexts` overlay (exercising LayeredLoader under
11
+ // multi-thread load).
12
+ //
13
+ // Modes:
14
+ // --smoke 10s per worker (default for CI)
15
+ // (default) 30s per worker (pre-push / nightly)
16
+ //
17
+ // Env knobs:
18
+ // WORKERS=N override worker count (default 4)
19
+ //
20
+ // Assertions:
21
+ // * every worker exits with code 0
22
+ // * every iteration's verify() result was {verified: true}
23
+ // * total iterations across workers ≥ FLOOR (low to tolerate slow CI)
24
+ // * parent never receives a worker error event
25
+
26
+ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
27
+ import { readFileSync } from 'node:fs';
28
+ import { resolve, dirname } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+
34
+ // ---------- Fixture / context definitions ----------
35
+ //
36
+ // Defined at module top-level so both parent (for setup logging) and
37
+ // workers (re-importing this file) see the same shapes.
38
+
39
+ const dataDir = resolve(__dirname, 'test-data');
40
+ const loadJson = (name) => JSON.parse(readFileSync(resolve(dataDir, name), 'utf8'));
41
+
42
+ const inputDocument = loadJson('inputDocument.json');
43
+ const keyPair = loadJson('keyPair.json');
44
+ const controllerDocument = loadJson('controllerDocument.json');
45
+ const citizenVocab = loadJson('citizenVocab.json');
46
+ const revealDocument = loadJson('deriveProofFrame.json');
47
+
48
+ // Fixture A: standard inputDocument + standard caller contexts.
49
+ // `https://w3id.org/citizenship/v1` IS in the bundled loader but is also
50
+ // registered via additional_contexts (mirrors prod call shape).
51
+ const fixtureA = {
52
+ label: 'A_standard',
53
+ document: inputDocument,
54
+ revealDocument,
55
+ contexts: {
56
+ [keyPair.id]: keyPair,
57
+ [keyPair.controller]: controllerDocument,
58
+ 'https://w3id.org/citizenship/v1': citizenVocab,
59
+ },
60
+ };
61
+
62
+ // Fixture B: synthetic context URL NOT in the bundled loader. The VC
63
+ // references it from `@context` and uses a term defined only inside it.
64
+ // This forces every load_v2 / expand_v2 / canonize call to hit the
65
+ // LayeredLoader overlay path (PR #110) for the synthetic URL — the
66
+ // concurrency-relevant case.
67
+ const SYNTHETIC_CONTEXT_URL = 'https://example.org/concurrency-test.json';
68
+ const syntheticContextBody = {
69
+ '@context': {
70
+ '@version': 1.1,
71
+ '@protected': true,
72
+ 'ex': 'https://example.org/concurrency-test.json#',
73
+ 'TestCredential': 'ex:TestCredential',
74
+ 'workerId': {
75
+ '@id': 'ex:workerId',
76
+ '@type': 'http://www.w3.org/2001/XMLSchema#string',
77
+ },
78
+ 'iterationLabel': {
79
+ '@id': 'ex:iterationLabel',
80
+ '@type': 'http://www.w3.org/2001/XMLSchema#string',
81
+ },
82
+ },
83
+ };
84
+
85
+ const fixtureBDocument = {
86
+ '@context': [
87
+ 'https://www.w3.org/2018/credentials/v1',
88
+ SYNTHETIC_CONTEXT_URL,
89
+ 'https://w3id.org/security/bbs/v1',
90
+ ],
91
+ id: 'https://example.org/credentials/concurrency-test',
92
+ type: ['VerifiableCredential', 'TestCredential'],
93
+ issuer: keyPair.controller,
94
+ issuanceDate: '2024-01-01T00:00:00Z',
95
+ credentialSubject: {
96
+ workerId: 'placeholder',
97
+ iterationLabel: 'placeholder',
98
+ },
99
+ };
100
+
101
+ const fixtureBRevealDocument = {
102
+ '@context': fixtureBDocument['@context'],
103
+ '@type': ['VerifiableCredential', 'TestCredential'],
104
+ credentialSubject: {
105
+ '@explicit': true,
106
+ workerId: {},
107
+ },
108
+ };
109
+
110
+ const fixtureB = {
111
+ label: 'B_synthetic_overlay',
112
+ document: fixtureBDocument,
113
+ revealDocument: fixtureBRevealDocument,
114
+ contexts: {
115
+ [keyPair.id]: keyPair,
116
+ [keyPair.controller]: controllerDocument,
117
+ [SYNTHETIC_CONTEXT_URL]: syntheticContextBody,
118
+ },
119
+ };
120
+
121
+ const FIXTURES = [fixtureA, fixtureB];
122
+
123
+ // ---------- Worker body ----------
124
+
125
+ if (!isMainThread) {
126
+ const { durationMs, fixtureIndex, workerId } = workerData;
127
+ const fixture = FIXTURES[fixtureIndex];
128
+
129
+ const { ldSign, ldVerify, ldDeriveProof } = await import(resolve(__dirname, 'index.js'));
130
+
131
+ const start = Date.now();
132
+ let iterations = 0;
133
+ let verifySuccesses = 0;
134
+ let verifyDerivedSuccesses = 0;
135
+ let lastError = null;
136
+
137
+ try {
138
+ while (Date.now() - start < durationMs) {
139
+ // Vary the credentialSubject a little so signatures differ
140
+ // across iterations (prevents any spooky caching from masking
141
+ // a real cross-thread corruption signal).
142
+ const doc = JSON.parse(JSON.stringify(fixture.document));
143
+ if (doc.credentialSubject) {
144
+ if (Object.prototype.hasOwnProperty.call(doc.credentialSubject, 'workerId')) {
145
+ doc.credentialSubject.workerId = `w${workerId}`;
146
+ doc.credentialSubject.iterationLabel = `iter-${iterations}`;
147
+ } else if (Object.prototype.hasOwnProperty.call(doc.credentialSubject, 'givenName')) {
148
+ doc.credentialSubject.givenName = `JOHN_W${workerId}_I${iterations}`;
149
+ }
150
+ }
151
+
152
+ const signed = await ldSign({
153
+ document: doc,
154
+ keyPair,
155
+ contexts: fixture.contexts,
156
+ });
157
+
158
+ const verifyResult = await ldVerify({
159
+ document: signed,
160
+ contexts: fixture.contexts,
161
+ });
162
+ if (!verifyResult.verified) {
163
+ throw new Error(`verify failed iter=${iterations}: ${verifyResult.error}`);
164
+ }
165
+ verifySuccesses++;
166
+
167
+ const derived = await ldDeriveProof({
168
+ document: signed,
169
+ revealDocument: fixture.revealDocument,
170
+ contexts: fixture.contexts,
171
+ });
172
+
173
+ const verifyDerivedResult = await ldVerify({
174
+ document: derived,
175
+ contexts: fixture.contexts,
176
+ });
177
+ if (!verifyDerivedResult.verified) {
178
+ throw new Error(`verify_derived failed iter=${iterations}: ${verifyDerivedResult.error}`);
179
+ }
180
+ verifyDerivedSuccesses++;
181
+
182
+ iterations++;
183
+ }
184
+ } catch (e) {
185
+ lastError = e && e.stack ? e.stack : String(e);
186
+ }
187
+
188
+ parentPort.postMessage({
189
+ workerId,
190
+ fixtureLabel: fixture.label,
191
+ iterations,
192
+ verifySuccesses,
193
+ verifyDerivedSuccesses,
194
+ elapsedMs: Date.now() - start,
195
+ error: lastError,
196
+ });
197
+
198
+ // Allow the message to flush before the worker exits.
199
+ process.exit(lastError ? 1 : 0);
200
+ }
201
+
202
+ // ---------- Parent ----------
203
+
204
+ const args = process.argv.slice(2);
205
+ const smoke = args.includes('--smoke');
206
+ const durationMs = smoke ? 10_000 : 30_000;
207
+ const workerCount = Number(process.env.WORKERS ?? 4);
208
+ // Floor proves every worker got CPU time and completed real work — not
209
+ // a perf gate. CircleCI's Alpine docker executor ran ~3 iters per worker
210
+ // in 10s (vs 30+ on a Mac dev box), so we require only one full round
211
+ // per worker.
212
+ const ITER_FLOOR = workerCount;
213
+
214
+ console.log(`--- concurrent worker stress test ---`);
215
+ console.log(` workers=${workerCount} duration=${durationMs}ms mode=${smoke ? 'smoke' : 'full'}`);
216
+ console.log(` fixtures: ${FIXTURES.map((f) => f.label).join(', ')}`);
217
+
218
+ const reports = [];
219
+ const workers = [];
220
+ let nonZeroExits = 0;
221
+ let workerErrors = 0;
222
+
223
+ const runOne = (workerId) => {
224
+ // Distribute workers across fixtures round-robin so both fixtures
225
+ // see concurrent load. With WORKERS=4 we get 2× fixtureA, 2× fixtureB.
226
+ const fixtureIndex = workerId % FIXTURES.length;
227
+ return new Promise((resolveP) => {
228
+ const w = new Worker(__filename, {
229
+ workerData: { durationMs, fixtureIndex, workerId },
230
+ });
231
+ workers.push(w);
232
+ let report = null;
233
+ w.on('message', (msg) => {
234
+ report = msg;
235
+ });
236
+ w.on('error', (err) => {
237
+ workerErrors++;
238
+ console.error(` worker ${workerId} emitted error event:`, err);
239
+ });
240
+ w.on('exit', (code) => {
241
+ if (code !== 0) nonZeroExits++;
242
+ if (report) reports.push(report);
243
+ console.log(` worker ${workerId} exited code=${code} iters=${report ? report.iterations : '?'} fixture=${report ? report.fixtureLabel : '?'}`);
244
+ resolveP();
245
+ });
246
+ });
247
+ };
248
+
249
+ const t0 = Date.now();
250
+ await Promise.all(Array.from({ length: workerCount }, (_, i) => runOne(i)));
251
+ const wallMs = Date.now() - t0;
252
+
253
+ // ---------- Assertions ----------
254
+
255
+ let totalIterations = 0;
256
+ let totalVerify = 0;
257
+ let totalVerifyDerived = 0;
258
+ const errored = [];
259
+
260
+ for (const r of reports) {
261
+ totalIterations += r.iterations;
262
+ totalVerify += r.verifySuccesses;
263
+ totalVerifyDerived += r.verifyDerivedSuccesses;
264
+ if (r.error) errored.push(r);
265
+ }
266
+
267
+ console.log('');
268
+ console.log(` total iterations: ${totalIterations} (verify=${totalVerify}, verify_derived=${totalVerifyDerived})`);
269
+ console.log(` wall time: ${wallMs}ms`);
270
+ console.log(` non-zero exits: ${nonZeroExits}`);
271
+ console.log(` worker errors: ${workerErrors}`);
272
+ console.log('');
273
+
274
+ let failed = false;
275
+
276
+ if (nonZeroExits > 0) {
277
+ console.error(`FAIL: ${nonZeroExits} worker(s) exited non-zero (possible crash / panic)`);
278
+ failed = true;
279
+ }
280
+ if (workerErrors > 0) {
281
+ console.error(`FAIL: ${workerErrors} worker error event(s) observed`);
282
+ failed = true;
283
+ }
284
+ for (const r of errored) {
285
+ console.error(`FAIL: worker ${r.workerId} (${r.fixtureLabel}) reported error: ${r.error}`);
286
+ failed = true;
287
+ }
288
+ if (totalIterations < ITER_FLOOR) {
289
+ console.error(`FAIL: total iterations ${totalIterations} below floor ${ITER_FLOOR}`);
290
+ failed = true;
291
+ }
292
+ if (totalVerify !== totalIterations || totalVerifyDerived !== totalIterations) {
293
+ console.error(`FAIL: verify counts mismatch (iters=${totalIterations} verify=${totalVerify} derived=${totalVerifyDerived})`);
294
+ failed = true;
295
+ }
296
+ if (reports.length !== workerCount) {
297
+ console.error(`FAIL: expected ${workerCount} worker reports, got ${reports.length}`);
298
+ failed = true;
299
+ }
300
+
301
+ if (failed) {
302
+ process.exit(1);
303
+ }
304
+ console.log('PASS: concurrent worker stress test');