@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 +11 -0
- package/W3C_CONFORMANCE.md +6 -5
- package/bench/frame_compare.mjs +203 -0
- package/bench/v2_internals.mjs +115 -0
- package/bench/vc_ops.mjs +308 -0
- package/package.json +6 -6
- package/scripts/fetch-w3c-tests.sh +8 -0
- package/src/jsonld.rs +210 -140
- package/test_jsonld_crossverify.mjs +8 -2
- package/test_w3c_conformance.mjs +80 -7
- package/w3c-baseline.json +883 -262
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" }
|
package/W3C_CONFORMANCE.md
CHANGED
|
@@ -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
|
|
5
|
-
only when a test that **previously
|
|
6
|
-
that were already failing in the
|
|
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
|
+
}
|
package/bench/vc_ops.mjs
ADDED
|
@@ -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));
|