@nuggetslife/vc 0.0.30 → 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" }
@@ -1,9 +1,10 @@
1
1
  # W3C Conformance Gate
2
2
 
3
3
  The CI runs the W3C JSON-LD 1.1 API and RDF Dataset Canonicalization (RDFC-1.0)
4
- test suites against this binding via `test_w3c_conformance.mjs`. The job fails
5
- only when a test that **previously passed** starts failing — never on tests
6
- that were already failing in the committed baseline.
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.
7
8
 
8
9
  ## Files
9
10
 
@@ -21,12 +22,12 @@ that were already failing in the committed baseline.
21
22
  yarn fetch:w3c-tests
22
23
 
23
24
  # Verify nothing has regressed vs baseline
24
- yarn test:w3c
25
+ CLIENTFFI_JSONLD_V2=1 yarn test:w3c
25
26
 
26
27
  # Re-run the suite and overwrite the baseline. Add `--runs 3` for
27
28
  # fast algos (canonize, fromRdf) to take the intersection of 3 runs
28
29
  # and filter out flaky tests.
29
- yarn test:w3c:update
30
+ CLIENTFFI_JSONLD_V2=1 yarn test:w3c:update
30
31
  ```
31
32
 
32
33
  ## Adding a denylist entry
@@ -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));