@nuggetslife/vc 0.0.29 → 0.1.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/Cargo.toml CHANGED
@@ -27,3 +27,14 @@ napi-build = "2.0.1"
27
27
  [profile.release]
28
28
  lto = true
29
29
  strip = "symbols"
30
+
31
+
32
+ # Mirrors the patch in vc/rs/Cargo.toml — when napi-rs builds vc/js, it
33
+ # needs the same patched vendored crates so the v2 path uses the forks.
34
+ [patch.crates-io]
35
+ json-ld = { path = "../../vendor/json-ld" }
36
+ json-ld-core = { path = "../../vendor/json-ld-core" }
37
+ json-ld-context-processing = { path = "../../vendor/json-ld-context-processing" }
38
+ json-ld-expansion = { path = "../../vendor/json-ld-expansion" }
39
+ json-ld-compaction = { path = "../../vendor/json-ld-compaction" }
40
+ json-ld-syntax = { path = "../../vendor/json-ld-syntax" }
@@ -0,0 +1,48 @@
1
+ # W3C Conformance Gate
2
+
3
+ The CI runs the W3C JSON-LD 1.1 API and RDF Dataset Canonicalization (RDFC-1.0)
4
+ test suites against the v2 JSON-LD path (`CLIENTFFI_JSONLD_V2=1`) via
5
+ `test_w3c_conformance.mjs`. The job fails only when a test that **previously
6
+ passed** starts failing — never on tests that were already failing in the
7
+ committed baseline.
8
+
9
+ ## Files
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `test_w3c_conformance.mjs` | Runner. Modes: `--check`, `--update`, `--verbose`, `--only <algo>`. |
14
+ | `w3c-baseline.json` | Committed baseline. The "passing" set per algo is what CI gates against. |
15
+ | `w3c-denylist.json` | Tests skipped because they trigger Rust-level deadlocks in clientffi. Each entry is a real bug — remove as fixed. |
16
+ | `scripts/fetch-w3c-tests.sh` | Clones the W3C test suites at pinned SHAs into `tmp-w3c-tests/` (gitignored). |
17
+
18
+ ## Local usage
19
+
20
+ ```bash
21
+ # Fetch test fixtures (one-off, ~50 MB)
22
+ yarn fetch:w3c-tests
23
+
24
+ # Verify nothing has regressed vs baseline
25
+ CLIENTFFI_JSONLD_V2=1 yarn test:w3c
26
+
27
+ # Re-run the suite and overwrite the baseline. Add `--runs 3` for
28
+ # fast algos (canonize, fromRdf) to take the intersection of 3 runs
29
+ # and filter out flaky tests.
30
+ CLIENTFFI_JSONLD_V2=1 yarn test:w3c:update
31
+ ```
32
+
33
+ ## Adding a denylist entry
34
+
35
+ If a new clientffi-side bug causes the runner to hang on a particular test,
36
+ add the test ID to the relevant `algo` array in `w3c-denylist.json` and open
37
+ an issue for the underlying bug.
38
+
39
+ ## Coverage caveats
40
+
41
+ - `flatten` and `toRdf` algos are **not yet** in the gated baseline. Each has
42
+ multiple deep deadlocks in clientffi that the JS-level per-test timeout
43
+ cannot unblock. They will be added once the underlying issues are mapped
44
+ (or after the migration to a mature JSON-LD crate).
45
+ - `expand` and `compact` use single-run baselines because intersection-of-3
46
+ runs exhausts the NAPI worker pool on these large suites. Minor flakes
47
+ (1-3 tests) may produce false regressions; rerun the CI job once before
48
+ investigating.
@@ -0,0 +1,203 @@
1
+ // Head-to-head frame() bench: jsonld.js (Digital Bazaar reference) vs.
2
+ // the three Rust backends behind the NAPI binding.
3
+ //
4
+ // Usage:
5
+ // node vc/js/bench/frame_compare.mjs # default 30w + 30t
6
+ // node vc/js/bench/frame_compare.mjs --warm 20 --timed 100
7
+ // node vc/js/bench/frame_compare.mjs --json
8
+ //
9
+ // Each backend runs in its own measurement loop. The Rust backends are
10
+ // selected via env flags inspected by the NAPI binding (vc/js/src/jsonld.rs):
11
+ // CLIENTFFI_JSONLD_V2=1 CLIENTFFI_FRAME_NATIVE=1 → frame_native
12
+ // CLIENTFFI_JSONLD_V2=1 CLIENTFFI_FRAME_FAST=1 → frame_v2_fast
13
+ // CLIENTFFI_JSONLD_V2=1 → frame_v2 (V1-algo full)
14
+ //
15
+ // Caveat: the env flags are read by `frame_native::use_native()` (cached
16
+ // via OnceLock) and inline `std::env::var` in the binding. We can't toggle
17
+ // them within a single Node process, so this script spawns a fresh child
18
+ // process per backend.
19
+
20
+ import { readFileSync, existsSync } from 'node:fs';
21
+ import { resolve, dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { performance } from 'node:perf_hooks';
24
+ import { spawnSync } from 'node:child_process';
25
+
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const repoRoot = resolve(__dirname, '../../..');
28
+
29
+ const args = process.argv.slice(2);
30
+ const flag = (n) => args.includes(n);
31
+ const valOf = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; };
32
+ const jsonOut = flag('--json');
33
+ const warmCount = Number(valOf('--warm') ?? 30);
34
+ const timedCount = Number(valOf('--timed') ?? 30);
35
+
36
+ const idv2Json = process.env.IDV2_JSON ?? '/Users/aml/conductor/workspaces/nuggets-v1/montpellier-v1/packages/bbs-signatures/src/__test__/identity-verifiable-credential-v2.json';
37
+ if (!existsSync(idv2Json)) {
38
+ console.error(`IDv2 fixture not found: ${idv2Json}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Backend selection lives in env. Re-exec ourselves with the right flags
43
+ // so OnceLock-cached choices in Rust are bound to the right path.
44
+ const backend = process.env.BENCH_BACKEND;
45
+
46
+ if (!backend) {
47
+ // Driver mode: spawn a child process per backend, each with the same
48
+ // bench protocol, collect their JSON outputs, print a comparison.
49
+ const protocols = [
50
+ {
51
+ label: 'jsonld.js',
52
+ env: { BENCH_BACKEND: 'jsonld_js' },
53
+ },
54
+ {
55
+ label: 'frame_v2',
56
+ env: { BENCH_BACKEND: 'rust', CLIENTFFI_JSONLD_V2: '1' },
57
+ },
58
+ {
59
+ label: 'frame_v2_fast',
60
+ env: { BENCH_BACKEND: 'rust', CLIENTFFI_JSONLD_V2: '1', CLIENTFFI_FRAME_FAST: '1' },
61
+ },
62
+ {
63
+ label: 'frame_native',
64
+ env: { BENCH_BACKEND: 'rust', CLIENTFFI_JSONLD_V2: '1', CLIENTFFI_FRAME_NATIVE: '1' },
65
+ },
66
+ ];
67
+
68
+ const allResults = [];
69
+ for (const p of protocols) {
70
+ process.stderr.write(`running ${p.label} (${warmCount}w + ${timedCount}t)…\n`);
71
+ const child = spawnSync(process.execPath, [
72
+ fileURLToPath(import.meta.url),
73
+ '--warm', String(warmCount),
74
+ '--timed', String(timedCount),
75
+ '--json',
76
+ ], {
77
+ env: { ...process.env, ...p.env, IDV2_JSON: idv2Json },
78
+ encoding: 'utf8',
79
+ maxBuffer: 4 * 1024 * 1024,
80
+ });
81
+ if (child.status !== 0) {
82
+ process.stderr.write(child.stderr);
83
+ console.error(`${p.label} failed (exit ${child.status})`);
84
+ continue;
85
+ }
86
+ try {
87
+ const result = JSON.parse(child.stdout);
88
+ result.label = p.label;
89
+ allResults.push(result);
90
+ } catch (e) {
91
+ console.error(`${p.label} produced unparseable output:`);
92
+ process.stderr.write(child.stdout);
93
+ throw e;
94
+ }
95
+ }
96
+
97
+ if (jsonOut) {
98
+ process.stdout.write(JSON.stringify(allResults, null, 2) + '\n');
99
+ } else {
100
+ const fastest = Math.min(...allResults.map(r => r.p50));
101
+ console.log(
102
+ `\n--- frame() on ${(allResults[0]?.fixtureBytes / 1024).toFixed(0) ?? '?'} KB IDv2, ${warmCount}w + ${timedCount}t ---`,
103
+ );
104
+ console.log(
105
+ 'backend p50 p95 min max vs fastest',
106
+ );
107
+ for (const r of allResults) {
108
+ const factor = (r.p50 / fastest).toFixed(2);
109
+ console.log(
110
+ `${r.label.padEnd(15)} ${r.p50.toFixed(2).padStart(6)}ms ${r.p95.toFixed(2).padStart(6)}ms ${r.min.toFixed(2).padStart(6)}ms ${r.max.toFixed(2).padStart(6)}ms ${factor}x`,
111
+ );
112
+ }
113
+ }
114
+ process.exit(0);
115
+ }
116
+
117
+ // Worker mode: actually run the bench for one backend.
118
+ const big = JSON.parse(readFileSync(idv2Json, 'utf8'));
119
+ delete big.proof;
120
+ const ctx = big['@context'];
121
+
122
+ // Match the Rust profile_frame harness: simple type frame.
123
+ const primaryType = Array.isArray(big.type) ? big.type[0] : big.type;
124
+ const frameDoc = { '@context': ctx, '@type': primaryType };
125
+
126
+ let frameFn;
127
+ if (backend === 'jsonld_js') {
128
+ const jsonld = (await import(join(repoRoot, 'vc/js/node_modules/jsonld/lib/index.js'))).default;
129
+
130
+ // Wire jsonld.js to the same bundled contexts as the Rust DocumentLoader.
131
+ // Each `vc/rs/src/document_loader/context/*.rs` defines a single
132
+ // `pub const NAME: &str = r#"<json>"#;` — extract the JSON literal.
133
+ const contextDir = join(repoRoot, 'vc/rs/src/document_loader/context');
134
+ const urlToFile = {
135
+ 'https://www.w3.org/2018/credentials/v1': 'credentials.rs',
136
+ 'https://schemas.nuggets.life/identity.json': 'identity.rs',
137
+ 'https://schemas.nuggets.life/identityV2.json': 'identity_v2.rs',
138
+ 'https://schemas.nuggets.life/identityV3.json': 'identity_v3.rs',
139
+ 'https://schemas.nuggets.life/bbsBoundv1.json': 'bbs_bound_v1.rs',
140
+ 'https://w3id.org/security/bbs/v1': 'bbs.rs',
141
+ 'https://w3id.org/security/v1': 'security_v1.rs',
142
+ 'https://w3id.org/security/v2': 'security_v2.rs',
143
+ 'https://w3id.org/security/suites/secp256k1-2019/v1': 'secp256k1.rs',
144
+ 'https://w3id.org/security/suites/jws-2020/v1': 'jws.rs',
145
+ 'https://www.w3.org/ns/did/v1': 'did.rs',
146
+ 'https://schema.org': 'schema_dot_org.rs',
147
+ 'https://w3id.org/citizenship/v1': 'citizenship.rs',
148
+ };
149
+ const docs = {};
150
+ for (const [url, file] of Object.entries(urlToFile)) {
151
+ try {
152
+ const src = readFileSync(join(contextDir, file), 'utf8');
153
+ const m = src.match(/r#"\s*([\s\S]*?)\s*"#/);
154
+ if (!m) continue;
155
+ docs[url] = JSON.parse(m[1]);
156
+ } catch {
157
+ // Some context files may be absent on this branch; skip silently.
158
+ }
159
+ }
160
+ const customLoader = async (url) => {
161
+ if (docs[url]) {
162
+ return { contextUrl: null, document: docs[url], documentUrl: url };
163
+ }
164
+ throw new Error(`bench: no bundled context for ${url}`);
165
+ };
166
+ frameFn = () => jsonld.frame(big, frameDoc, { documentLoader: customLoader });
167
+ } else if (backend === 'rust') {
168
+ const { JsonLd } = await import(join(repoRoot, 'vc/js/index.js'));
169
+ const proc = new JsonLd();
170
+ frameFn = () => proc.frame(big, frameDoc);
171
+ } else {
172
+ console.error(`unknown BENCH_BACKEND=${backend}`);
173
+ process.exit(1);
174
+ }
175
+
176
+ function pct(arr, p) {
177
+ const sorted = [...arr].sort((a, b) => a - b);
178
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
179
+ }
180
+
181
+ // Warm-up
182
+ for (let i = 0; i < warmCount; i++) await frameFn();
183
+
184
+ const samples = [];
185
+ for (let i = 0; i < timedCount; i++) {
186
+ const t = performance.now();
187
+ await frameFn();
188
+ samples.push(performance.now() - t);
189
+ }
190
+
191
+ const result = {
192
+ backend,
193
+ fixtureBytes: JSON.stringify(big).length,
194
+ warm: warmCount,
195
+ timed: timedCount,
196
+ p50: pct(samples, 0.5),
197
+ p95: pct(samples, 0.95),
198
+ min: Math.min(...samples),
199
+ max: Math.max(...samples),
200
+ samples: samples.length,
201
+ };
202
+
203
+ process.stdout.write(JSON.stringify(result));
@@ -0,0 +1,115 @@
1
+ // Sprint 2 stable bench harness for V2 JSON-LD primitive operations on the
2
+ // 166 KB IDv2 credential fixture. 30 warm + 30 timed samples per op.
3
+ //
4
+ // Usage:
5
+ // node vc/js/bench/v2_internals.mjs # V1 (default jsonld pipeline)
6
+ // CLIENTFFI_JSONLD_V2=1 node vc/js/bench/v2_internals.mjs # V2 (vendored crate pipeline)
7
+ // node vc/js/bench/v2_internals.mjs --json # machine-readable output
8
+ // node vc/js/bench/v2_internals.mjs --only compact # restrict to one op
9
+ //
10
+ // Compares before/after numbers across Sprint 2 patches.
11
+
12
+ import { readFileSync, existsSync } from 'node:fs';
13
+ import { resolve, dirname, join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { performance } from 'node:perf_hooks';
16
+ import { execSync } from 'node:child_process';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const repoRoot = resolve(__dirname, '../../..');
20
+
21
+ const args = process.argv.slice(2);
22
+ const flag = (n) => args.includes(n);
23
+ const valOf = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; };
24
+ const jsonOut = flag('--json');
25
+ const onlyArg = valOf('--only');
26
+ const warmCount = Number(valOf('--warm') ?? 30);
27
+ const timedCount = Number(valOf('--timed') ?? 30);
28
+
29
+ // Default fixture: prefer in-repo Rust test fixture, fall back to montpellier-v1
30
+ // IDv2 source JSON. The Rust fixture is N-Quads only; for expand/compact/flatten
31
+ // we need the JSON document.
32
+ const idv2Json = process.env.IDV2_JSON ?? '/Users/aml/conductor/workspaces/nuggets-v1/montpellier-v1/packages/bbs-signatures/src/__test__/identity-verifiable-credential-v2.json';
33
+ if (!existsSync(idv2Json)) {
34
+ console.error(`IDv2 fixture not found: ${idv2Json}\nSet IDV2_JSON to override.`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const { JsonLd } = await import(join(repoRoot, 'vc/js/index.js'));
39
+ const proc = new JsonLd();
40
+
41
+ const big = JSON.parse(readFileSync(idv2Json, 'utf8'));
42
+ delete big.proof;
43
+ const ctx = big['@context'];
44
+
45
+ let gitSha = 'unknown';
46
+ try { gitSha = execSync('git rev-parse --short HEAD', { cwd: repoRoot }).toString().trim(); } catch {}
47
+
48
+ function pct(arr, p) {
49
+ const sorted = [...arr].sort((a, b) => a - b);
50
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
51
+ }
52
+
53
+ async function bench(label, fn) {
54
+ for (let i = 0; i < warmCount; i++) await fn();
55
+ const samples = [];
56
+ for (let i = 0; i < timedCount; i++) {
57
+ const t = performance.now();
58
+ await fn();
59
+ samples.push(performance.now() - t);
60
+ }
61
+ return {
62
+ label,
63
+ p50: pct(samples, 0.5),
64
+ p95: pct(samples, 0.95),
65
+ min: Math.min(...samples),
66
+ max: Math.max(...samples),
67
+ samples: samples.length,
68
+ };
69
+ }
70
+
71
+ const ops = {
72
+ expand: () => proc.expand(big),
73
+ compact: async () => {
74
+ const expanded = await proc.expand(big);
75
+ return proc.compact(expanded, ctx);
76
+ },
77
+ flatten_nocompact: () => proc.flatten(big, null),
78
+ flatten_compact: () => proc.flatten(big, ctx),
79
+ };
80
+
81
+ const selected = onlyArg ? { [onlyArg]: ops[onlyArg] } : ops;
82
+ if (onlyArg && !ops[onlyArg]) {
83
+ console.error(`Unknown op: ${onlyArg}. Pick one of: ${Object.keys(ops).join(', ')}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ const variant = process.env.CLIENTFFI_JSONLD_V2 ? 'V2' : 'V1';
88
+ const results = {
89
+ meta: {
90
+ variant,
91
+ commit: gitSha,
92
+ fixture: idv2Json,
93
+ fixtureBytes: JSON.stringify(big).length,
94
+ warm: warmCount,
95
+ timed: timedCount,
96
+ timestamp: new Date().toISOString(),
97
+ },
98
+ ops: {},
99
+ };
100
+
101
+ for (const [name, fn] of Object.entries(selected)) {
102
+ const r = await bench(name, fn);
103
+ results.ops[name] = r;
104
+ }
105
+
106
+ if (jsonOut) {
107
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
108
+ } else {
109
+ console.log(`--- ${variant} on ${(results.meta.fixtureBytes / 1024).toFixed(0)} KB IDv2 (commit ${gitSha}, ${warmCount}w + ${timedCount}t) ---`);
110
+ for (const r of Object.values(results.ops)) {
111
+ console.log(
112
+ `${r.label.padEnd(10)} p50=${r.p50.toFixed(2).padStart(7)}ms p95=${r.p95.toFixed(2).padStart(7)}ms min=${r.min.toFixed(2)}ms max=${r.max.toFixed(2)}ms`
113
+ );
114
+ }
115
+ }
@@ -0,0 +1,308 @@
1
+ // End-to-end BBS+ verifiable credential ops bench.
2
+ //
3
+ // Times the full sign → verify → derive_proof → verify_derived flow for two
4
+ // fixtures, across three pipeline configurations. Spawns one Node child
5
+ // process per config so the OnceLock-cached env flags in Rust pick up the
6
+ // right path.
7
+ //
8
+ // v1 : default (V1 jsonld pipeline)
9
+ // v2 : CLIENTFFI_JSONLD_V2=1 (V2 pipeline, frame_v2)
10
+ // v2_native : CLIENTFFI_JSONLD_V2=1 + CLIENTFFI_FRAME_NATIVE=1
11
+ //
12
+ // Usage:
13
+ // node vc/js/bench/vc_ops.mjs
14
+ // node vc/js/bench/vc_ops.mjs --fixture idv2 # IDv2 166 KB instead of small
15
+ // node vc/js/bench/vc_ops.mjs --warm 5 --timed 20
16
+
17
+ import { readFileSync, existsSync } from 'node:fs';
18
+ import { resolve, dirname, join } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { performance } from 'node:perf_hooks';
21
+ import { spawnSync } from 'node:child_process';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const repoRoot = resolve(__dirname, '../../..');
25
+ const dataDir = resolve(__dirname, '../test-data');
26
+
27
+ const args = process.argv.slice(2);
28
+ const flag = (n) => args.includes(n);
29
+ const valOf = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; };
30
+ const jsonOut = flag('--json');
31
+ const fixtureArg = valOf('--fixture') ?? 'small';
32
+ const warmCount = Number(valOf('--warm') ?? 5);
33
+ const timedCount = Number(valOf('--timed') ?? 20);
34
+
35
+ const backend = process.env.BENCH_BACKEND;
36
+
37
+ if (!backend) {
38
+ // Driver: spawn child per pipeline config.
39
+ const configs = [
40
+ { label: 'js_ref', env: { BENCH_PIPELINE: 'js' } },
41
+ { label: 'v1', env: {} },
42
+ { label: 'v2', env: { CLIENTFFI_JSONLD_V2: '1' } },
43
+ { label: 'v2_native', env: { CLIENTFFI_JSONLD_V2: '1', CLIENTFFI_FRAME_NATIVE: '1' } },
44
+ ];
45
+
46
+ const allResults = [];
47
+ for (const c of configs) {
48
+ process.stderr.write(`running ${c.label} (${warmCount}w + ${timedCount}t, fixture=${fixtureArg})…\n`);
49
+ const child = spawnSync(process.execPath, [
50
+ fileURLToPath(import.meta.url),
51
+ '--warm', String(warmCount),
52
+ '--timed', String(timedCount),
53
+ '--fixture', fixtureArg,
54
+ '--json',
55
+ ], {
56
+ env: { ...process.env, ...c.env, BENCH_BACKEND: 'inproc' },
57
+ encoding: 'utf8',
58
+ maxBuffer: 8 * 1024 * 1024,
59
+ });
60
+ if (child.status !== 0) {
61
+ process.stderr.write(child.stderr);
62
+ console.error(`${c.label} failed (exit ${child.status})`);
63
+ continue;
64
+ }
65
+ try {
66
+ const result = JSON.parse(child.stdout);
67
+ result.label = c.label;
68
+ allResults.push(result);
69
+ } catch (e) {
70
+ process.stderr.write(child.stdout);
71
+ throw e;
72
+ }
73
+ }
74
+
75
+ if (jsonOut) {
76
+ process.stdout.write(JSON.stringify(allResults, null, 2) + '\n');
77
+ } else {
78
+ const ops = ['sign', 'verify', 'derive_proof', 'verify_derived', 'roundtrip'];
79
+ console.log(`\n--- VC ops on ${fixtureArg} fixture, ${warmCount}w + ${timedCount}t ---`);
80
+ const labels = allResults.map(r => r.label);
81
+ const header = ['op'.padEnd(16), ...labels.map(l => l.padStart(11))].join(' ');
82
+ console.log(header);
83
+ for (const op of ops) {
84
+ const cells = allResults.map(r => {
85
+ const m = r.ops[op];
86
+ return m ? `${m.p50.toFixed(2).padStart(7)}ms` : '—'.padStart(11);
87
+ });
88
+ console.log([op.padEnd(16), ...cells.map(c => c.padStart(11))].join(' '));
89
+ }
90
+ // Δ row vs leftmost (v1) — useful summary
91
+ if (allResults.length >= 2) {
92
+ console.log();
93
+ console.log('Δ vs v1 (negative = faster):');
94
+ const v1 = allResults[0];
95
+ for (const op of ops) {
96
+ const base = v1.ops[op]?.p50;
97
+ if (!base) continue;
98
+ const cells = allResults.map(r => {
99
+ const m = r.ops[op];
100
+ if (!m) return '—'.padStart(11);
101
+ const delta = m.p50 - base;
102
+ const pct = (delta / base) * 100;
103
+ return `${delta >= 0 ? '+' : ''}${delta.toFixed(2)}ms (${pct >= 0 ? '+' : ''}${pct.toFixed(0)}%)`.padStart(11);
104
+ });
105
+ console.log([op.padEnd(16), ...cells.map(c => c.padStart(20))].join(' '));
106
+ }
107
+ }
108
+ }
109
+ process.exit(0);
110
+ }
111
+
112
+ // ---- Worker mode: actually run the bench ----
113
+
114
+ const pipeline = process.env.BENCH_PIPELINE ?? 'rust';
115
+
116
+ let signFn, verifyFn, deriveFn;
117
+
118
+ if (pipeline === 'rust') {
119
+ const { ldSign, ldVerify, ldDeriveProof } = await import(join(repoRoot, 'vc/js/index.js'));
120
+ signFn = (document, keyPair, contexts) => ldSign({ document, keyPair, contexts });
121
+ verifyFn = (document, contexts) => ldVerify({ document, contexts });
122
+ deriveFn = (document, revealDocument, contexts) =>
123
+ ldDeriveProof({ document, revealDocument, contexts });
124
+ } else if (pipeline === 'js') {
125
+ const { createRequire } = await import('node:module');
126
+ const require = createRequire(import.meta.url);
127
+ const jsigs = require('jsonld-signatures');
128
+ const {
129
+ BbsBlsSignature2020,
130
+ BbsBlsSignatureProof2020,
131
+ Bls12381G2KeyPair,
132
+ deriveProof,
133
+ } = require('@mattrglobal/jsonld-signatures-bbs');
134
+ const { extendContextLoader } = jsigs;
135
+
136
+ // Build a static documentLoader from the same contexts the Rust path uses.
137
+ const buildLoader = (contextMap) => extendContextLoader((url) => {
138
+ const document = contextMap[url];
139
+ if (document) return { contextUrl: null, document, documentUrl: url };
140
+ throw new Error(`bench: no bundled context for ${url}`);
141
+ });
142
+
143
+ signFn = async (document, keyPair, contexts) => {
144
+ const key = await Bls12381G2KeyPair.from({
145
+ id: keyPair.id,
146
+ controller: keyPair.controller,
147
+ privateKeyBase58: keyPair.privateKeyBase58,
148
+ publicKeyBase58: keyPair.publicKeyBase58,
149
+ type: 'Bls12381G2Key2020',
150
+ });
151
+ return jsigs.sign(document, {
152
+ suite: new BbsBlsSignature2020({ key }),
153
+ purpose: new jsigs.purposes.AssertionProofPurpose(),
154
+ documentLoader: buildLoader(contexts),
155
+ });
156
+ };
157
+
158
+ verifyFn = async (document, contexts) => {
159
+ // jsigs.verify accepts an array of suites and dispatches based on
160
+ // proof.type. Passing a single (wrong) suite would cause derived
161
+ // proofs (BbsBlsSignatureProof2020) to fail immediately without
162
+ // running the actual ZKP check — that produced spuriously fast
163
+ // numbers in earlier runs of this bench.
164
+ const suite = [new BbsBlsSignature2020(), new BbsBlsSignatureProof2020()];
165
+ return jsigs.verify(document, {
166
+ suite,
167
+ purpose: new jsigs.purposes.AssertionProofPurpose(),
168
+ documentLoader: buildLoader(contexts),
169
+ });
170
+ };
171
+
172
+ deriveFn = async (document, revealDocument, contexts) =>
173
+ deriveProof(document, revealDocument, {
174
+ suite: new BbsBlsSignatureProof2020(),
175
+ documentLoader: buildLoader(contexts),
176
+ });
177
+ } else {
178
+ console.error(`unknown BENCH_PIPELINE=${pipeline}`);
179
+ process.exit(1);
180
+ }
181
+
182
+ const loadJson = (p) => JSON.parse(readFileSync(p, 'utf8'));
183
+
184
+ let inputDocument, revealDocument;
185
+ const keyPair = loadJson(resolve(dataDir, 'keyPair.json'));
186
+ const controllerDocument = loadJson(resolve(dataDir, 'controllerDocument.json'));
187
+ const citizenVocab = loadJson(resolve(dataDir, 'citizenVocab.json'));
188
+
189
+ if (fixtureArg === 'small') {
190
+ inputDocument = loadJson(resolve(dataDir, 'inputDocument.json'));
191
+ revealDocument = loadJson(resolve(dataDir, 'deriveProofFrame.json'));
192
+ } else if (fixtureArg === 'idv2') {
193
+ const idv2Path = process.env.IDV2_JSON
194
+ ?? '/Users/aml/conductor/workspaces/nuggets-v1/montpellier-v1/packages/bbs-signatures/src/__test__/identity-verifiable-credential-v2.json';
195
+ if (!existsSync(idv2Path)) {
196
+ console.error(`IDv2 fixture missing: ${idv2Path}`);
197
+ process.exit(1);
198
+ }
199
+ inputDocument = JSON.parse(readFileSync(idv2Path, 'utf8'));
200
+ delete inputDocument.proof;
201
+ // Re-issue the credential under our test issuer so signing works with
202
+ // the static keyPair. Reveal frame: just reveal @type (minimal frame).
203
+ inputDocument.issuer = keyPair.controller;
204
+ const ctxArr = Array.isArray(inputDocument['@context'])
205
+ ? inputDocument['@context']
206
+ : [inputDocument['@context']];
207
+ if (!ctxArr.includes('https://w3id.org/security/bbs/v1')) {
208
+ inputDocument['@context'] = [...ctxArr, 'https://w3id.org/security/bbs/v1'];
209
+ }
210
+ revealDocument = {
211
+ '@context': inputDocument['@context'],
212
+ type: inputDocument.type,
213
+ };
214
+ } else {
215
+ console.error(`unknown fixture: ${fixtureArg}`);
216
+ process.exit(1);
217
+ }
218
+
219
+ const contexts = {
220
+ [keyPair.id]: keyPair,
221
+ [keyPair.controller]: controllerDocument,
222
+ 'https://w3id.org/citizenship/v1': citizenVocab,
223
+ };
224
+
225
+ // For the JS reference path (no built-in nuggets context cache), pull in
226
+ // the same bundled JSON-LD contexts that vc/rs/src/document_loader/nuggets.rs
227
+ // embeds. The Rust pipeline gets these for free; the JS pipeline needs them
228
+ // in its documentLoader to load the IDv2 fixture without HTTP.
229
+ if (pipeline === 'js') {
230
+ const ctxDir = resolve(__dirname, '../../rs/src/document_loader/context');
231
+ const urlToFile = {
232
+ 'https://www.w3.org/2018/credentials/v1': 'credentials.rs',
233
+ 'https://schemas.nuggets.life/identity.json': 'identity.rs',
234
+ 'https://schemas.nuggets.life/identityV2.json': 'identity_v2.rs',
235
+ 'https://schemas.nuggets.life/identityV3.json': 'identity_v3.rs',
236
+ 'https://schemas.nuggets.life/bbsBoundv1.json': 'bbs_bound_v1.rs',
237
+ 'https://w3id.org/security/bbs/v1': 'bbs.rs',
238
+ 'https://w3id.org/security/v1': 'security_v1.rs',
239
+ 'https://w3id.org/security/v2': 'security_v2.rs',
240
+ 'https://w3id.org/security/suites/secp256k1-2019/v1': 'secp256k1.rs',
241
+ 'https://w3id.org/security/suites/jws-2020/v1': 'jws.rs',
242
+ 'https://www.w3.org/ns/did/v1': 'did.rs',
243
+ 'https://schema.org': 'schema_dot_org.rs',
244
+ };
245
+ for (const [url, file] of Object.entries(urlToFile)) {
246
+ try {
247
+ const src = readFileSync(resolve(ctxDir, file), 'utf8');
248
+ const m = src.match(/r#"\s*([\s\S]*?)\s*"#/);
249
+ if (m && !contexts[url]) contexts[url] = JSON.parse(m[1]);
250
+ } catch {
251
+ // ignore missing files
252
+ }
253
+ }
254
+ }
255
+
256
+ function pct(arr, p) {
257
+ const sorted = [...arr].sort((a, b) => a - b);
258
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
259
+ }
260
+
261
+ async function bench(fn) {
262
+ for (let i = 0; i < warmCount; i++) await fn();
263
+ const samples = [];
264
+ for (let i = 0; i < timedCount; i++) {
265
+ const t = performance.now();
266
+ await fn();
267
+ samples.push(performance.now() - t);
268
+ }
269
+ return {
270
+ p50: pct(samples, 0.5),
271
+ p95: pct(samples, 0.95),
272
+ min: Math.min(...samples),
273
+ max: Math.max(...samples),
274
+ samples: samples.length,
275
+ };
276
+ }
277
+
278
+ const ops = {};
279
+
280
+ // sign: produces a signed document
281
+ ops.sign = await bench(() => signFn(inputDocument, keyPair, contexts));
282
+
283
+ // One-time signed doc for downstream ops
284
+ const signed = await signFn(inputDocument, keyPair, contexts);
285
+
286
+ ops.verify = await bench(() => verifyFn(signed, contexts));
287
+
288
+ ops.derive_proof = await bench(() => deriveFn(signed, revealDocument, contexts));
289
+
290
+ const derived = await deriveFn(signed, revealDocument, contexts);
291
+ ops.verify_derived = await bench(() => verifyFn(derived, contexts));
292
+
293
+ // roundtrip = sign + verify + derive + verify_derived
294
+ ops.roundtrip = await bench(async () => {
295
+ const s = await signFn(inputDocument, keyPair, contexts);
296
+ await verifyFn(s, contexts);
297
+ const d = await deriveFn(s, revealDocument, contexts);
298
+ await verifyFn(d, contexts);
299
+ });
300
+
301
+ const result = {
302
+ fixture: fixtureArg,
303
+ fixtureBytes: JSON.stringify(inputDocument).length,
304
+ warm: warmCount,
305
+ timed: timedCount,
306
+ ops,
307
+ };
308
+ process.stdout.write(JSON.stringify(result));