@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 +1 -1
- package/package.json +7 -8
- package/src/bbs_2023.rs +13 -1
- package/test_worker_concurrency.mjs +304 -0
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
|
+
"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.
|
|
46
|
-
"@nuggetslife/vc-linux-arm64-gnu": "0.
|
|
47
|
-
"@nuggetslife/vc-linux-arm64-musl": "0.
|
|
48
|
-
"@nuggetslife/vc-linux-x64-gnu": "0.
|
|
49
|
-
"@nuggetslife/vc-linux-x64-musl": "0.
|
|
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');
|