@cofhe/sdk 0.7.1 → 0.8.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/CHANGELOG.md +35 -0
- package/acps/acp.ts +10 -1
- package/acps/index.ts +11 -0
- package/acps/onchain-utils.ts +188 -49
- package/acps/registry.ts +73 -0
- package/acps/test/onchainAccessStatus.test.ts +116 -0
- package/acps/types.ts +16 -0
- package/chains/chains/arbitrum.ts +14 -0
- package/chains/chains/ethereum.ts +14 -0
- package/chains/index.ts +5 -1
- package/chains/test/chains.test.ts +24 -3
- package/core/acps.ts +51 -143
- package/core/client.ts +13 -8
- package/core/clientTypes.ts +29 -4
- package/core/config.ts +25 -1
- package/core/consts.ts +14 -2
- package/core/decrypt/verifyDecryptResult.ts +4 -3
- package/core/encrypt/encryptInputsBuilder.ts +6 -0
- package/core/encrypt/zkPackProveVerify.ts +7 -1
- package/core/error.ts +1 -0
- package/core/fetchKeys.ts +2 -2
- package/core/index.ts +9 -1
- package/core/keyStore.ts +23 -10
- package/core/taskManager.ts +48 -0
- package/core/test/acps.test.ts +91 -1
- package/core/test/client.test.ts +41 -0
- package/core/test/config.test.ts +16 -0
- package/core/test/decrypt.test.ts +11 -4
- package/core/test/encryptInputsBuilder.test.ts +1 -1
- package/core/test/fetchKeys.test.ts +23 -2
- package/core/test/keyStore.test.ts +18 -6
- package/core/test/taskManager.test.ts +57 -0
- package/core/types.ts +8 -0
- package/dist/acps.cjs +314 -35
- package/dist/acps.d.cts +251 -4
- package/dist/acps.d.ts +251 -4
- package/dist/acps.js +2 -2
- package/dist/chains.cjs +26 -0
- package/dist/chains.d.cts +45 -1
- package/dist/chains.d.ts +45 -1
- package/dist/chains.js +1 -1
- package/dist/{chunk-43USWPEH.js → chunk-GGI7Y6AW.js} +59 -264
- package/dist/chunk-KSOV5B4B.js +50 -0
- package/dist/{chunk-ESMZCFJY.js → chunk-NMKMCAI3.js} +4 -2
- package/dist/{chunk-Q7CBWGQX.js → chunk-UJYOBHYA.js} +374 -37
- package/dist/{chunk-N6IDQRRU.js → chunk-UKQAJK4N.js} +25 -1
- package/dist/{clientTypes-BN3nbzYM.d.ts → clientTypes-5YPPR16m.d.ts} +77 -14
- package/dist/{clientTypes-CYZjFznO.d.cts → clientTypes-D3aIXmUp.d.cts} +77 -14
- package/dist/core.cjs +271 -136
- package/dist/core.d.cts +29 -8
- package/dist/core.d.ts +29 -8
- package/dist/core.js +4 -4
- package/dist/node.cjs +266 -136
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +5 -5
- package/dist/{acp-Wi6isVQI.d.cts → onchain-utils-CUAK8glx.d.cts} +31 -1
- package/dist/{acp-Wi6isVQI.d.ts → onchain-utils-CUAK8glx.d.ts} +31 -1
- package/dist/web.cjs +360 -150
- package/dist/web.d.cts +49 -3
- package/dist/web.d.ts +49 -3
- package/dist/web.js +51 -20
- package/dist/zkProve.worker.cjs +69 -15
- package/dist/zkProve.worker.js +23 -17
- package/node/storage.ts +2 -2
- package/node/test/inherited.test.ts +5 -1
- package/node/test/storage-memory-fallback.test.ts +47 -0
- package/package.json +5 -3
- package/web/index.ts +100 -26
- package/web/test/countingZkProveWorker.ts +31 -0
- package/web/test/inherited.web.test.ts +5 -1
- package/web/test/mainThreadPool.web.test.ts +174 -0
- package/web/test/threadPoolBenchHelper.ts +140 -0
- package/web/test/threadPoolOff.web.test.ts +17 -0
- package/web/test/threadPoolOn.web.test.ts +80 -0
- package/web/test/workerInitRace.web.test.ts +79 -0
- package/web/tfheThreadPool.ts +140 -0
- package/web/workerManager.ts +4 -2
- package/web/zkProve.worker.ts +42 -17
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { THREADPOOL_TESTS, fetchNetworkKeys } from './threadPoolBenchHelper';
|
|
3
|
+
|
|
4
|
+
// What happens when the zkProve worker receives two proof requests before it
|
|
5
|
+
// has finished initializing tfhe — e.g. an app doing
|
|
6
|
+
// `Promise.all([encrypt(a), encrypt(b)])` as its first encryption?
|
|
7
|
+
//
|
|
8
|
+
// `initThreadPool` may only run once per wasm instance. It spawns its N rayon
|
|
9
|
+
// workers first and only then builds rayon's global pool, so a second call
|
|
10
|
+
// spawns N more workers, fails at the build step, and leaves those N stranded
|
|
11
|
+
// for the lifetime of the zkProve worker.
|
|
12
|
+
//
|
|
13
|
+
// The rayon workers are created inside the zkProve worker, out of the page's
|
|
14
|
+
// sight, so this test runs the real worker inside `countingZkProveWorker`,
|
|
15
|
+
// which counts them from the inside.
|
|
16
|
+
|
|
17
|
+
const THREADS = 2;
|
|
18
|
+
|
|
19
|
+
type WorkerReply = { id: string; type: string; error?: string; count?: number };
|
|
20
|
+
|
|
21
|
+
describe.skipIf(!THREADPOOL_TESTS)('@cofhe/sdk/web - zkProve worker init race', () => {
|
|
22
|
+
let worker: Worker;
|
|
23
|
+
let keys: { fheKey: string; crs: string };
|
|
24
|
+
let replies: WorkerReply[];
|
|
25
|
+
let rayonWorkersStarted: number;
|
|
26
|
+
|
|
27
|
+
const request = (message: Record<string, unknown>) =>
|
|
28
|
+
new Promise<WorkerReply>((resolve) => {
|
|
29
|
+
worker.addEventListener('message', function onReply(event: MessageEvent) {
|
|
30
|
+
if (event.data?.id !== message.id) return;
|
|
31
|
+
worker.removeEventListener('message', onReply);
|
|
32
|
+
resolve(event.data);
|
|
33
|
+
});
|
|
34
|
+
worker.postMessage(message);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const prove = (id: string, value: number) =>
|
|
38
|
+
request({
|
|
39
|
+
id,
|
|
40
|
+
type: 'zkProve',
|
|
41
|
+
fheKeyHex: keys.fheKey,
|
|
42
|
+
crsHex: keys.crs,
|
|
43
|
+
items: [{ utype: 'uint32', data: String(value) }],
|
|
44
|
+
metadata: Array.from(new Uint8Array(32).fill(7)),
|
|
45
|
+
tfheThreads: THREADS,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
keys = await fetchNetworkKeys();
|
|
50
|
+
|
|
51
|
+
worker = new Worker(new URL('./countingZkProveWorker.ts', import.meta.url), { type: 'module' });
|
|
52
|
+
await new Promise<void>((resolve, reject) => {
|
|
53
|
+
worker.addEventListener('message', function onReady(event: MessageEvent) {
|
|
54
|
+
if (event.data?.type !== 'ready') return;
|
|
55
|
+
worker.removeEventListener('message', onReady);
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
worker.addEventListener('error', (event) => reject(new Error(`worker failed to load: ${event.message}`)));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Both requests are posted before the worker can finish its first init.
|
|
62
|
+
replies = await Promise.all([prove('race-a', 1), prove('race-b', 2)]);
|
|
63
|
+
|
|
64
|
+
rayonWorkersStarted = (await request({ id: 'count', type: 'countRayonWorkers' })).count ?? -1;
|
|
65
|
+
}, 240000);
|
|
66
|
+
|
|
67
|
+
afterAll(() => worker?.terminate());
|
|
68
|
+
|
|
69
|
+
it('generates both proofs', () => {
|
|
70
|
+
expect(globalThis.crossOriginIsolated).toBe(true);
|
|
71
|
+
expect(replies.map((reply) => reply.type)).toEqual(['success', 'success']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// The worker memoises its in-flight init, so the second request waits for the
|
|
75
|
+
// first one's pool instead of starting another.
|
|
76
|
+
it('starts exactly one rayon pool', () => {
|
|
77
|
+
expect(rayonWorkersStarted).toBe(THREADS);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rayon thread pool setup for the browser build of tfhe.
|
|
3
|
+
*
|
|
4
|
+
* `tfhe` is built with wasm-bindgen-rayon, so the ZK proof (the
|
|
5
|
+
* `build_with_proof_packed` call, by far the most expensive step of
|
|
6
|
+
* `encryptInputs`) can run across several threads. Those threads are real Web
|
|
7
|
+
* Workers that share the wasm linear memory, and `initThreadPool` hands them
|
|
8
|
+
* that memory over `postMessage`.
|
|
9
|
+
*
|
|
10
|
+
* Sharing memory across threads is only allowed in a cross-origin-isolated
|
|
11
|
+
* context, so the host page must be served with:
|
|
12
|
+
*
|
|
13
|
+
* Cross-Origin-Opener-Policy: same-origin
|
|
14
|
+
* Cross-Origin-Embedder-Policy: require-corp (or `credentialless`)
|
|
15
|
+
*
|
|
16
|
+
* Plenty of apps can't or don't set those headers, and calling
|
|
17
|
+
* `initThreadPool` without them throws ("SharedArrayBuffer transfer requires
|
|
18
|
+
* self.crossOriginIsolated"). So this is strictly an optimisation: we probe for
|
|
19
|
+
* isolation, and when it isn't available we leave tfhe single-threaded rather
|
|
20
|
+
* than failing the encryption. Single-threaded tfhe works in every context.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { TfheThreadsSetting } from '@/core';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cap for the auto-derived thread count.
|
|
27
|
+
*
|
|
28
|
+
* Proving keeps getting faster all the way up to core count, but with sharply
|
|
29
|
+
* diminishing returns — measured on a 12-core machine (median of 5 proofs of
|
|
30
|
+
* uint128 + uint64 + uint32):
|
|
31
|
+
*
|
|
32
|
+
* threads | 1 | 2 | 4 | 6 | 8 | 12
|
|
33
|
+
* median | 3199 | 1687 | 964 | 728 | 657 | 624 (ms)
|
|
34
|
+
* speedup | 1.0x | 1.9x | 3.3x | 4.4x | 4.9x | 5.1x
|
|
35
|
+
*
|
|
36
|
+
* Past 8 threads the curve is flat (8 -> 12 is ~5% for 50% more workers), and
|
|
37
|
+
* every rayon thread is a real Web Worker competing with the host app's main
|
|
38
|
+
* thread. 8 captures ~95% of the achievable speedup, so we stop there.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_AUTO_THREADS = 8;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a `config.tfheThreads` setting into a concrete thread count.
|
|
44
|
+
* Returns 1 to mean "don't start a pool".
|
|
45
|
+
*
|
|
46
|
+
* For `'auto'`, Zama's guidance is `initThreadPool(navigator.hardwareConcurrency)`;
|
|
47
|
+
* we follow that but clamp to {@link MAX_AUTO_THREADS}. `hardwareConcurrency`
|
|
48
|
+
* counts logical cores and some browsers deliberately misreport it (Firefox with
|
|
49
|
+
* `privacy.resistFingerprinting` returns 2, Safari clamps it), and it can be
|
|
50
|
+
* absent outside a browser — hence the `?? 1` fallback and the floor of 1.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveThreadCount(setting: TfheThreadsSetting = 'auto'): number {
|
|
53
|
+
// Explicit opt-out.
|
|
54
|
+
if (setting === false) return 1;
|
|
55
|
+
|
|
56
|
+
// Explicit count: trust the caller over `hardwareConcurrency` (which browsers
|
|
57
|
+
// clamp for privacy) but keep it a sane integer.
|
|
58
|
+
if (typeof setting === 'number') {
|
|
59
|
+
return Number.isFinite(setting) && setting >= 1 ? Math.floor(setting) : 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cores = (globalThis as { navigator?: { hardwareConcurrency?: number } }).navigator?.hardwareConcurrency ?? 1;
|
|
63
|
+
|
|
64
|
+
if (!Number.isFinite(cores) || cores < 1) return 1;
|
|
65
|
+
|
|
66
|
+
return Math.min(Math.floor(cores), MAX_AUTO_THREADS);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type TfheThreadPoolResult = {
|
|
70
|
+
/** Whether a multi-threaded rayon pool is now running */
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
/** Threads requested from `initThreadPool` (1 when single-threaded) */
|
|
73
|
+
threads: number;
|
|
74
|
+
/** Why the pool was not started, when `enabled` is false */
|
|
75
|
+
reason?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Minimal shape of the parts of the tfhe module this file touches. */
|
|
79
|
+
type TfheThreadPoolModule = {
|
|
80
|
+
initThreadPool?: (numThreads: number) => Promise<unknown>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** True when the current context may share wasm memory between threads. */
|
|
84
|
+
export function isCrossOriginIsolated(): boolean {
|
|
85
|
+
return (globalThis as { crossOriginIsolated?: boolean }).crossOriginIsolated === true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start tfhe's rayon thread pool, if the environment allows it.
|
|
90
|
+
*
|
|
91
|
+
* Safe to call when unsupported — it reports why it bailed instead of throwing,
|
|
92
|
+
* leaving tfhe single-threaded. `initThreadPool` must only run once per wasm
|
|
93
|
+
* instance, so callers are responsible for calling this once (both call sites
|
|
94
|
+
* in this package memoise their whole tfhe init).
|
|
95
|
+
*/
|
|
96
|
+
export async function initTfheThreadPool(
|
|
97
|
+
mod: TfheThreadPoolModule,
|
|
98
|
+
setting: TfheThreadsSetting = 'auto'
|
|
99
|
+
): Promise<TfheThreadPoolResult> {
|
|
100
|
+
if (setting === false) {
|
|
101
|
+
return { enabled: false, threads: 1, reason: 'disabled via config.tfheThreads' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof mod.initThreadPool !== 'function') {
|
|
105
|
+
return { enabled: false, threads: 1, reason: 'tfhe build has no initThreadPool (single-threaded wasm)' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof Worker === 'undefined') {
|
|
109
|
+
return { enabled: false, threads: 1, reason: 'Worker is unavailable' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The real gate. Without isolation `initThreadPool` throws on postMessage.
|
|
113
|
+
if (!isCrossOriginIsolated()) {
|
|
114
|
+
return {
|
|
115
|
+
enabled: false,
|
|
116
|
+
threads: 1,
|
|
117
|
+
reason:
|
|
118
|
+
'not cross-origin isolated — serve the page with Cross-Origin-Opener-Policy: same-origin and ' +
|
|
119
|
+
'Cross-Origin-Embedder-Policy: require-corp (or credentialless) to enable multi-threaded proving',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const threads = resolveThreadCount(setting);
|
|
124
|
+
if (threads <= 1) {
|
|
125
|
+
return { enabled: false, threads: 1, reason: 'resolved to a single thread' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await mod.initThreadPool(threads);
|
|
130
|
+
return { enabled: true, threads };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Nested-worker spawning and shared-memory transfer are the flaky parts
|
|
133
|
+
// across browsers; a failure here must not take the encryption down.
|
|
134
|
+
return {
|
|
135
|
+
enabled: false,
|
|
136
|
+
threads: 1,
|
|
137
|
+
reason: `initThreadPool failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
package/web/workerManager.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages worker lifecycle and request/response handling
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ZkProveWorkerRequest, ZkProveWorkerResponse } from '@/core';
|
|
6
|
+
import type { TfheThreadsSetting, ZkProveWorkerRequest, ZkProveWorkerResponse } from '@/core';
|
|
7
7
|
|
|
8
8
|
// Declare Worker type for environments where it's not available
|
|
9
9
|
declare const Worker: any;
|
|
@@ -124,7 +124,8 @@ class ZkProveWorkerManager {
|
|
|
124
124
|
fheKeyHex: string,
|
|
125
125
|
crsHex: string,
|
|
126
126
|
items: Array<{ utype: string; data: any }>,
|
|
127
|
-
metadata: Uint8Array
|
|
127
|
+
metadata: Uint8Array,
|
|
128
|
+
tfheThreads?: TfheThreadsSetting
|
|
128
129
|
): Promise<Uint8Array> {
|
|
129
130
|
// Initialize worker if needed
|
|
130
131
|
await this.initializeWorker();
|
|
@@ -150,6 +151,7 @@ class ZkProveWorkerManager {
|
|
|
150
151
|
crsHex,
|
|
151
152
|
items,
|
|
152
153
|
metadata: Array.from(metadata),
|
|
154
|
+
tfheThreads,
|
|
153
155
|
};
|
|
154
156
|
|
|
155
157
|
(this.worker as any).postMessage(message);
|
package/web/zkProve.worker.ts
CHANGED
|
@@ -8,28 +8,53 @@
|
|
|
8
8
|
|
|
9
9
|
import { TFHE_RS_SAFE_SERIALIZATION_SIZE_LIMIT } from '../core/consts';
|
|
10
10
|
import type { ZkProveWorkerRequest, ZkProveWorkerResponse } from '../core/encrypt/zkPackProveVerify.js';
|
|
11
|
+
import { initTfheThreadPool, type TfheThreadPoolResult } from './tfheThreadPool.js';
|
|
12
|
+
import type { TfheThreadsSetting } from '../core/types.js';
|
|
11
13
|
|
|
12
14
|
// TFHE module (will be initialized on first use)
|
|
13
15
|
let tfheModule: any = null;
|
|
14
|
-
let
|
|
16
|
+
let initPromise: Promise<void> | null = null;
|
|
17
|
+
let threadPool: TfheThreadPoolResult | null = null;
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Initialize TFHE in worker context
|
|
21
|
+
*
|
|
22
|
+
* This is where the heavy `build_with_proof_packed` call runs, so it's also
|
|
23
|
+
* where tfhe's rayon thread pool matters most — the main-thread pool only helps
|
|
24
|
+
* the non-worker fallback path. The pool spawns nested Workers that share this
|
|
25
|
+
* worker's wasm memory, which requires the page to be cross-origin isolated;
|
|
26
|
+
* `initTfheThreadPool` degrades to single-threaded instead of throwing when it
|
|
27
|
+
* isn't.
|
|
28
|
+
*
|
|
29
|
+
* Memoised on the in-flight promise rather than on completion: further proof
|
|
30
|
+
* requests can arrive while the first one is still initializing, and
|
|
31
|
+
* `initThreadPool` may only run once per wasm instance — a second call spawns
|
|
32
|
+
* its workers, fails to build the pool, and leaves those workers stranded.
|
|
18
33
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
function initTfhe(tfheThreads: TfheThreadsSetting = 'auto'): Promise<void> {
|
|
35
|
+
initPromise ??= (async () => {
|
|
36
|
+
try {
|
|
37
|
+
// Dynamic import of tfhe module
|
|
38
|
+
tfheModule = await import('tfhe');
|
|
39
|
+
await tfheModule.default();
|
|
40
|
+
await tfheModule.init_panic_hook();
|
|
41
|
+
|
|
42
|
+
threadPool = await initTfheThreadPool(tfheModule, tfheThreads);
|
|
43
|
+
|
|
44
|
+
console.log(
|
|
45
|
+
threadPool.enabled
|
|
46
|
+
? `[Worker] TFHE initialized (rayon thread pool: ${threadPool.threads} threads)`
|
|
47
|
+
: `[Worker] TFHE initialized (single-threaded: ${threadPool.reason})`
|
|
48
|
+
);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[Worker] Failed to initialize TFHE:', error);
|
|
51
|
+
// Let the next request retry from scratch.
|
|
52
|
+
initPromise = null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
return initPromise;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
/**
|
|
@@ -51,7 +76,7 @@ if (typeof self !== 'undefined') {
|
|
|
51
76
|
* Main message handler
|
|
52
77
|
*/
|
|
53
78
|
self.onmessage = async (event: MessageEvent) => {
|
|
54
|
-
const { id, type, fheKeyHex, crsHex, items, metadata } = event.data as ZkProveWorkerRequest;
|
|
79
|
+
const { id, type, fheKeyHex, crsHex, items, metadata, tfheThreads } = event.data as ZkProveWorkerRequest;
|
|
55
80
|
|
|
56
81
|
if (type !== 'zkProve') {
|
|
57
82
|
self.postMessage({
|
|
@@ -64,7 +89,7 @@ if (typeof self !== 'undefined') {
|
|
|
64
89
|
|
|
65
90
|
try {
|
|
66
91
|
// Initialize TFHE if needed
|
|
67
|
-
await initTfhe();
|
|
92
|
+
await initTfhe(tfheThreads);
|
|
68
93
|
|
|
69
94
|
if (!tfheModule) {
|
|
70
95
|
throw new Error('TFHE module not initialized');
|