diff --git a/src/lib/crypto/local-keystore.ts b/src/lib/crypto/local-keystore.ts
index db1ea2d..03623ae 100644
--- a/src/lib/crypto/local-keystore.ts
+++ b/src/lib/crypto/local-keystore.ts
@@ -1,6 +1,6 @@
/**
* Local Keystore using SIWE-derived WebCrypto
- *
+ *
* Implements HKDF from signature → AES-GCM encrypt/decrypt for identity storage
* Replaces deprecated MetaMask encryption path (eth_getEncryptionPublicKey / eth_decrypt)
*/
@@ -9,180 +9,167 @@
* Derive encryption key from SIWE signature using HKDF
*/
async function deriveKeyFromSignature(
- signature: string,
- salt: string = "shadowgraph-identity-v1"
+ signature: string,
+ salt: string = "shadowgraph-identity-v1"
): Promise
{
- // Convert signature (hex string) to bytes
- const sigBytes = new Uint8Array(
- signature.startsWith("0x")
- ? signature.slice(2).match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))
- : []
- );
-
- // Import signature as raw key material
- const keyMaterial = await crypto.subtle.importKey(
- "raw",
- sigBytes,
- "HKDF",
- false,
- ["deriveKey"]
- );
-
- // Derive AES-GCM key using HKDF
- const derivedKey = await crypto.subtle.deriveKey(
- {
- name: "HKDF",
- hash: "SHA-256",
- salt: new TextEncoder().encode(salt),
- info: new TextEncoder().encode("shadowgraph-keystore"),
- },
- keyMaterial,
- { name: "AES-GCM", length: 256 },
- false,
- ["encrypt", "decrypt"]
- );
-
- return derivedKey;
+ // Convert signature (hex string) to bytes
+ const sigBytes = new Uint8Array(
+ signature.startsWith("0x")
+ ? signature
+ .slice(2)
+ .match(/.{1,2}/g)!
+ .map((byte) => parseInt(byte, 16))
+ : []
+ );
+
+ // Import signature as raw key material
+ const keyMaterial = await crypto.subtle.importKey("raw", sigBytes, "HKDF", false, ["deriveKey"]);
+
+ // Derive AES-GCM key using HKDF
+ const derivedKey = await crypto.subtle.deriveKey(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt: new TextEncoder().encode(salt),
+ info: new TextEncoder().encode("shadowgraph-keystore"),
+ },
+ keyMaterial,
+ { name: "AES-GCM", length: 256 },
+ false,
+ ["encrypt", "decrypt"]
+ );
+
+ return derivedKey;
}
/**
* Encrypt string using AES-GCM
*/
-export async function encryptString(
- plaintext: string,
- signature: string
-): Promise {
- const key = await deriveKeyFromSignature(signature);
-
- // Generate random IV
- const iv = crypto.getRandomValues(new Uint8Array(12));
-
- // Encrypt plaintext
- const encrypted = await crypto.subtle.encrypt(
- { name: "AES-GCM", iv },
- key,
- new TextEncoder().encode(plaintext)
- );
-
- // Combine IV + ciphertext
- const combined = new Uint8Array(iv.length + encrypted.byteLength);
- combined.set(iv, 0);
- combined.set(new Uint8Array(encrypted), iv.length);
-
- // Return as base64
- return btoa(String.fromCharCode(...combined));
+export async function encryptString(plaintext: string, signature: string): Promise {
+ const key = await deriveKeyFromSignature(signature);
+
+ // Generate random IV
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+
+ // Encrypt plaintext
+ const encrypted = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv },
+ key,
+ new TextEncoder().encode(plaintext)
+ );
+
+ // Combine IV + ciphertext
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
+ combined.set(iv, 0);
+ combined.set(new Uint8Array(encrypted), iv.length);
+
+ // Return as base64
+ return btoa(String.fromCharCode(...combined));
}
/**
* Decrypt string using AES-GCM
*/
-export async function decryptString(
- ciphertext: string,
- signature: string
-): Promise {
- const key = await deriveKeyFromSignature(signature);
-
- // Decode base64
- const combined = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
-
- // Extract IV and ciphertext
- const iv = combined.slice(0, 12);
- const encrypted = combined.slice(12);
-
- // Decrypt
- const decrypted = await crypto.subtle.decrypt(
- { name: "AES-GCM", iv },
- key,
- encrypted
- );
-
- return new TextDecoder().decode(decrypted);
+export async function decryptString(ciphertext: string, signature: string): Promise {
+ const key = await deriveKeyFromSignature(signature);
+
+ // Decode base64
+ const combined = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0));
+
+ // Extract IV and ciphertext
+ const iv = combined.slice(0, 12);
+ const encrypted = combined.slice(12);
+
+ // Decrypt
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encrypted);
+
+ return new TextDecoder().decode(decrypted);
}
/**
* Local keystore for identity and sensitive data
*/
export class LocalKeystore {
- private storageKey = "shadowgraph_keystore";
- private signature: string | null = null;
-
- /**
- * Initialize keystore with SIWE signature
- */
- async initialize(signature: string): Promise {
- this.signature = signature;
- }
-
- /**
- * Store encrypted data
- */
- async store(key: string, value: string): Promise {
- if (!this.signature) {
- throw new Error("Keystore not initialized with signature");
- }
-
- const encrypted = await encryptString(value, this.signature);
-
- // Get existing store
- const store = this.getStore();
- store[key] = encrypted;
-
- // Save to localStorage
- localStorage.setItem(this.storageKey, JSON.stringify(store));
- }
-
- /**
- * Retrieve and decrypt data
- */
- async retrieve(key: string): Promise {
- if (!this.signature) {
- throw new Error("Keystore not initialized with signature");
- }
-
- const store = this.getStore();
- const encrypted = store[key];
-
- if (!encrypted) {
- return null;
- }
-
- try {
- return await decryptString(encrypted, this.signature);
- } catch (error) {
- console.error("[Keystore] Decryption failed:", error);
- return null;
- }
- }
-
- /**
- * Remove data
- */
- remove(key: string): void {
- const store = this.getStore();
- delete store[key];
- localStorage.setItem(this.storageKey, JSON.stringify(store));
- }
-
- /**
- * Clear all keystore data
- */
- clear(): void {
- localStorage.removeItem(this.storageKey);
- }
-
- /**
- * Get raw store from localStorage
- */
- private getStore(): Record {
- const stored = localStorage.getItem(this.storageKey);
- if (!stored) return {};
-
- try {
- return JSON.parse(stored);
- } catch {
- return {};
- }
- }
+ private storageKey = "shadowgraph_keystore";
+ private signature: string | null = null;
+
+ /**
+ * Initialize keystore with SIWE signature
+ */
+ async initialize(signature: string): Promise {
+ this.signature = signature;
+ }
+
+ /**
+ * Store encrypted data
+ */
+ async store(key: string, value: string): Promise {
+ if (!this.signature) {
+ throw new Error("Keystore not initialized with signature");
+ }
+
+ const encrypted = await encryptString(value, this.signature);
+
+ // Get existing store
+ const store = this.getStore();
+ store[key] = encrypted;
+
+ // Save to localStorage
+ localStorage.setItem(this.storageKey, JSON.stringify(store));
+ }
+
+ /**
+ * Retrieve and decrypt data
+ */
+ async retrieve(key: string): Promise {
+ if (!this.signature) {
+ throw new Error("Keystore not initialized with signature");
+ }
+
+ const store = this.getStore();
+ const encrypted = store[key];
+
+ if (!encrypted) {
+ return null;
+ }
+
+ try {
+ return await decryptString(encrypted, this.signature);
+ } catch (error) {
+ console.error("[Keystore] Decryption failed:", error);
+ return null;
+ }
+ }
+
+ /**
+ * Remove data
+ */
+ remove(key: string): void {
+ const store = this.getStore();
+ delete store[key];
+ localStorage.setItem(this.storageKey, JSON.stringify(store));
+ }
+
+ /**
+ * Clear all keystore data
+ */
+ clear(): void {
+ localStorage.removeItem(this.storageKey);
+ }
+
+ /**
+ * Get raw store from localStorage
+ */
+ private getStore(): Record {
+ const stored = localStorage.getItem(this.storageKey);
+ if (!stored) return {};
+
+ try {
+ return JSON.parse(stored);
+ } catch {
+ return {};
+ }
+ }
}
// Export singleton instance
diff --git a/src/lib/proof/index.ts b/src/lib/proof/index.ts
index d524b11..8df31f0 100644
--- a/src/lib/proof/index.ts
+++ b/src/lib/proof/index.ts
@@ -68,12 +68,7 @@ export {
} from "./api";
// Worker Pool (Horizontal Scaling)
-export {
- WorkerPoolManager,
- workerPool,
- type WorkerNode,
- type WorkerTask,
-} from "./workerPool";
+export { WorkerPoolManager, workerPool, type WorkerNode, type WorkerTask } from "./workerPool";
// Performance Profiler
export {
diff --git a/src/lib/proof/profiler.ts b/src/lib/proof/profiler.ts
index 2371962..61f1b32 100644
--- a/src/lib/proof/profiler.ts
+++ b/src/lib/proof/profiler.ts
@@ -85,9 +85,7 @@ export class PerformanceProfiler {
try {
for (const circuitType of config.circuitTypes) {
for (const networkSize of config.networkSizes) {
- console.log(
- `Profiling ${circuitType} circuit with ${networkSize} attestations...`
- );
+ console.log(`Profiling ${circuitType} circuit with ${networkSize} attestations...`);
// Warmup iterations
if (config.warmupIterations) {
@@ -118,9 +116,7 @@ export class PerformanceProfiler {
.sort((a, b) => a - b);
const avgDuration =
- durations.length > 0
- ? durations.reduce((sum, d) => sum + d, 0) / durations.length
- : 0;
+ durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
const minDuration = durations.length > 0 ? durations[0] : 0;
const maxDuration = durations.length > 0 ? durations[durations.length - 1] : 0;
@@ -131,7 +127,8 @@ export class PerformanceProfiler {
: 0;
const stdDev = Math.sqrt(variance);
- const successRate = iterationResults.filter((r) => r.success).length / iterationResults.length;
+ const successRate =
+ iterationResults.filter((r) => r.success).length / iterationResults.length;
const p50 = this.getPercentile(durations, 0.5);
const p95 = this.getPercentile(durations, 0.95);
@@ -158,10 +155,8 @@ export class PerformanceProfiler {
const totalDuration = Date.now() - startTime;
const overallSuccessRate =
- results.reduce(
- (sum, r) => sum + r.statistics.successRate * r.iterations,
- 0
- ) / results.reduce((sum, r) => sum + r.iterations, 0);
+ results.reduce((sum, r) => sum + r.statistics.successRate * r.iterations, 0) /
+ results.reduce((sum, r) => sum + r.iterations, 0);
const recommendations = this.generateRecommendations(results);
@@ -197,23 +192,20 @@ export class PerformanceProfiler {
cpuPercent?: number;
}> {
// Create mock attestations
- const attestations: TrustAttestation[] = Array.from(
- { length: networkSize },
- (_, idx) => ({
- source: `0xsource${idx}`,
- target: "0xtarget",
- opinion: {
- belief: Math.random() * 0.5 + 0.3,
- disbelief: Math.random() * 0.2,
- uncertainty: Math.random() * 0.3,
- base_rate: 0.5,
- },
- attestation_type: "trust" as const,
- weight: 1.0,
- created_at: Date.now(),
- expires_at: Date.now() + 86400000,
- })
- );
+ const attestations: TrustAttestation[] = Array.from({ length: networkSize }, (_, idx) => ({
+ source: `0xsource${idx}`,
+ target: "0xtarget",
+ opinion: {
+ belief: Math.random() * 0.5 + 0.3,
+ disbelief: Math.random() * 0.2,
+ uncertainty: Math.random() * 0.3,
+ base_rate: 0.5,
+ },
+ attestation_type: "trust" as const,
+ weight: 1.0,
+ created_at: Date.now(),
+ expires_at: Date.now() + 86400000,
+ }));
const startTime = Date.now();
let memoryBefore: number | undefined;
diff --git a/src/lib/proof/workerPool.ts b/src/lib/proof/workerPool.ts
index 119a124..cc6fa13 100644
--- a/src/lib/proof/workerPool.ts
+++ b/src/lib/proof/workerPool.ts
@@ -237,7 +237,9 @@ export class WorkerPoolManager extends EventEmitter {
return {
proof: Array.from({ length: 8 }, () => Math.floor(Math.random() * 1000000)),
publicInputs: [750000],
- hash: "0x" + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join(""),
+ hash:
+ "0x" +
+ Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join(""),
fusedOpinion: {
belief: 0.7,
disbelief: 0.2,
diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts
index a536725..f3b4ad7 100644
--- a/src/lib/telemetry.ts
+++ b/src/lib/telemetry.ts
@@ -1,231 +1,230 @@
/**
* Minimal Privacy-Safe Telemetry Module
- *
+ *
* Tracks proof generation events without collecting PII
* Events fire to dev console today; can be hooked to real sink later
*/
export interface ProofTelemetryEvent {
- /** Proof generation method: local WASM, remote server, or simulation */
- method: "local" | "remote" | "simulation";
- /** Duration in milliseconds */
- ms: number;
- /** Circuit size: 16, 32, 64, etc. */
- size: number;
- /** Device capabilities summary (no identifying info) */
- device: {
- /** Device type: desktop, mobile, tablet */
- type: "desktop" | "mobile" | "tablet" | "unknown";
- /** RAM category: low (<4GB), medium (4-8GB), high (>8GB) */
- ramCategory: "low" | "medium" | "high" | "unknown";
- /** Browser family (no version) */
- browser: "chrome" | "firefox" | "safari" | "edge" | "other";
- /** Whether WASM is supported */
- wasmSupported: boolean;
- };
- /** Timestamp of event (not user's timezone) */
- timestamp: number;
- /** Success or failure */
- success: boolean;
- /** Error type if failed (no stack traces) */
- errorType?: string;
+ /** Proof generation method: local WASM, remote server, or simulation */
+ method: "local" | "remote" | "simulation";
+ /** Duration in milliseconds */
+ ms: number;
+ /** Circuit size: 16, 32, 64, etc. */
+ size: number;
+ /** Device capabilities summary (no identifying info) */
+ device: {
+ /** Device type: desktop, mobile, tablet */
+ type: "desktop" | "mobile" | "tablet" | "unknown";
+ /** RAM category: low (<4GB), medium (4-8GB), high (>8GB) */
+ ramCategory: "low" | "medium" | "high" | "unknown";
+ /** Browser family (no version) */
+ browser: "chrome" | "firefox" | "safari" | "edge" | "other";
+ /** Whether WASM is supported */
+ wasmSupported: boolean;
+ };
+ /** Timestamp of event (not user's timezone) */
+ timestamp: number;
+ /** Success or failure */
+ success: boolean;
+ /** Error type if failed (no stack traces) */
+ errorType?: string;
}
export interface TelemetryConfig {
- /** Enable telemetry collection */
- enabled: boolean;
- /** Log to console in development */
- logToConsole: boolean;
- /** Custom sink function for production */
- sink?: (event: ProofTelemetryEvent) => void | Promise;
+ /** Enable telemetry collection */
+ enabled: boolean;
+ /** Log to console in development */
+ logToConsole: boolean;
+ /** Custom sink function for production */
+ sink?: (event: ProofTelemetryEvent) => void | Promise;
}
class TelemetryManager {
- private config: TelemetryConfig = {
- enabled: true,
- logToConsole: true,
- };
-
- private events: ProofTelemetryEvent[] = [];
- private maxEvents = 100; // Keep last 100 events in memory
-
- /**
- * Configure telemetry settings
- */
- configure(config: Partial): void {
- this.config = { ...this.config, ...config };
- }
-
- /**
- * Track a proof generation event
- */
- trackProof(params: {
- method: "local" | "remote" | "simulation";
- ms: number;
- size: number;
- device?: Partial;
- success?: boolean;
- errorType?: string;
- }): void {
- if (!this.config.enabled) return;
-
- const event: ProofTelemetryEvent = {
- method: params.method,
- ms: params.ms,
- size: params.size,
- device: {
- type: params.device?.type || this.detectDeviceType(),
- ramCategory: params.device?.ramCategory || this.detectRAMCategory(),
- browser: params.device?.browser || this.detectBrowser(),
- wasmSupported: params.device?.wasmSupported ?? this.detectWASMSupport(),
- },
- timestamp: Date.now(),
- success: params.success ?? true,
- errorType: params.errorType,
- };
-
- // Store in memory
- this.events.push(event);
- if (this.events.length > this.maxEvents) {
- this.events.shift();
- }
-
- // Log to console if enabled
- if (this.config.logToConsole) {
- console.log("[Telemetry] Proof event:", {
- method: event.method,
- duration: `${event.ms}ms`,
- size: event.size,
- device: event.device,
- success: event.success,
- ...(event.errorType && { error: event.errorType }),
- });
- }
-
- // Send to sink if configured
- if (this.config.sink) {
- try {
- this.config.sink(event);
- } catch (error) {
- console.error("[Telemetry] Sink error:", error);
- }
- }
- }
-
- /**
- * Get all stored events
- */
- getEvents(): ProofTelemetryEvent[] {
- return [...this.events];
- }
-
- /**
- * Clear all stored events
- */
- clearEvents(): void {
- this.events = [];
- }
-
- /**
- * Get aggregated statistics
- */
- getStats(): {
- totalProofs: number;
- successRate: number;
- avgDuration: number;
- methodBreakdown: Record;
- deviceBreakdown: Record;
- } {
- if (this.events.length === 0) {
- return {
- totalProofs: 0,
- successRate: 0,
- avgDuration: 0,
- methodBreakdown: {},
- deviceBreakdown: {},
- };
- }
-
- const successful = this.events.filter((e) => e.success).length;
- const totalDuration = this.events.reduce((sum, e) => sum + e.ms, 0);
-
- const methodBreakdown: Record = {};
- const deviceBreakdown: Record = {};
-
- for (const event of this.events) {
- methodBreakdown[event.method] = (methodBreakdown[event.method] || 0) + 1;
- deviceBreakdown[event.device.type] = (deviceBreakdown[event.device.type] || 0) + 1;
- }
-
- return {
- totalProofs: this.events.length,
- successRate: successful / this.events.length,
- avgDuration: totalDuration / this.events.length,
- methodBreakdown,
- deviceBreakdown,
- };
- }
-
- /**
- * Detect device type from user agent (no PII)
- */
- private detectDeviceType(): ProofTelemetryEvent["device"]["type"] {
- if (typeof navigator === "undefined") return "unknown";
-
- const ua = navigator.userAgent.toLowerCase();
- if (/mobile|android|iphone|ipod/.test(ua)) return "mobile";
- if (/tablet|ipad/.test(ua)) return "tablet";
- return "desktop";
- }
-
- /**
- * Detect RAM category (no exact value)
- */
- private detectRAMCategory(): ProofTelemetryEvent["device"]["ramCategory"] {
- if (typeof navigator === "undefined") return "unknown";
-
- // @ts-ignore - navigator.deviceMemory is not in all browsers
- const deviceMemory = navigator.deviceMemory;
- if (typeof deviceMemory !== "number") return "unknown";
-
- if (deviceMemory < 4) return "low";
- if (deviceMemory <= 8) return "medium";
- return "high";
- }
-
- /**
- * Detect browser family (no version)
- */
- private detectBrowser(): ProofTelemetryEvent["device"]["browser"] {
- if (typeof navigator === "undefined") return "other";
-
- const ua = navigator.userAgent.toLowerCase();
- if (ua.includes("edg")) return "edge";
- if (ua.includes("chrome")) return "chrome";
- if (ua.includes("firefox")) return "firefox";
- if (ua.includes("safari")) return "safari";
- return "other";
- }
-
- /**
- * Detect WASM support
- */
- private detectWASMSupport(): boolean {
- try {
- if (typeof WebAssembly === "object" &&
- typeof WebAssembly.instantiate === "function") {
- const module = new WebAssembly.Module(
- Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
- );
- if (module instanceof WebAssembly.Module) {
- return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
- }
- }
- } catch (e) {
- return false;
- }
- return false;
- }
+ private config: TelemetryConfig = {
+ enabled: true,
+ logToConsole: true,
+ };
+
+ private events: ProofTelemetryEvent[] = [];
+ private maxEvents = 100; // Keep last 100 events in memory
+
+ /**
+ * Configure telemetry settings
+ */
+ configure(config: Partial): void {
+ this.config = { ...this.config, ...config };
+ }
+
+ /**
+ * Track a proof generation event
+ */
+ trackProof(params: {
+ method: "local" | "remote" | "simulation";
+ ms: number;
+ size: number;
+ device?: Partial;
+ success?: boolean;
+ errorType?: string;
+ }): void {
+ if (!this.config.enabled) return;
+
+ const event: ProofTelemetryEvent = {
+ method: params.method,
+ ms: params.ms,
+ size: params.size,
+ device: {
+ type: params.device?.type || this.detectDeviceType(),
+ ramCategory: params.device?.ramCategory || this.detectRAMCategory(),
+ browser: params.device?.browser || this.detectBrowser(),
+ wasmSupported: params.device?.wasmSupported ?? this.detectWASMSupport(),
+ },
+ timestamp: Date.now(),
+ success: params.success ?? true,
+ errorType: params.errorType,
+ };
+
+ // Store in memory
+ this.events.push(event);
+ if (this.events.length > this.maxEvents) {
+ this.events.shift();
+ }
+
+ // Log to console if enabled
+ if (this.config.logToConsole) {
+ console.log("[Telemetry] Proof event:", {
+ method: event.method,
+ duration: `${event.ms}ms`,
+ size: event.size,
+ device: event.device,
+ success: event.success,
+ ...(event.errorType && { error: event.errorType }),
+ });
+ }
+
+ // Send to sink if configured
+ if (this.config.sink) {
+ try {
+ this.config.sink(event);
+ } catch (error) {
+ console.error("[Telemetry] Sink error:", error);
+ }
+ }
+ }
+
+ /**
+ * Get all stored events
+ */
+ getEvents(): ProofTelemetryEvent[] {
+ return [...this.events];
+ }
+
+ /**
+ * Clear all stored events
+ */
+ clearEvents(): void {
+ this.events = [];
+ }
+
+ /**
+ * Get aggregated statistics
+ */
+ getStats(): {
+ totalProofs: number;
+ successRate: number;
+ avgDuration: number;
+ methodBreakdown: Record;
+ deviceBreakdown: Record;
+ } {
+ if (this.events.length === 0) {
+ return {
+ totalProofs: 0,
+ successRate: 0,
+ avgDuration: 0,
+ methodBreakdown: {},
+ deviceBreakdown: {},
+ };
+ }
+
+ const successful = this.events.filter((e) => e.success).length;
+ const totalDuration = this.events.reduce((sum, e) => sum + e.ms, 0);
+
+ const methodBreakdown: Record = {};
+ const deviceBreakdown: Record = {};
+
+ for (const event of this.events) {
+ methodBreakdown[event.method] = (methodBreakdown[event.method] || 0) + 1;
+ deviceBreakdown[event.device.type] = (deviceBreakdown[event.device.type] || 0) + 1;
+ }
+
+ return {
+ totalProofs: this.events.length,
+ successRate: successful / this.events.length,
+ avgDuration: totalDuration / this.events.length,
+ methodBreakdown,
+ deviceBreakdown,
+ };
+ }
+
+ /**
+ * Detect device type from user agent (no PII)
+ */
+ private detectDeviceType(): ProofTelemetryEvent["device"]["type"] {
+ if (typeof navigator === "undefined") return "unknown";
+
+ const ua = navigator.userAgent.toLowerCase();
+ if (/mobile|android|iphone|ipod/.test(ua)) return "mobile";
+ if (/tablet|ipad/.test(ua)) return "tablet";
+ return "desktop";
+ }
+
+ /**
+ * Detect RAM category (no exact value)
+ */
+ private detectRAMCategory(): ProofTelemetryEvent["device"]["ramCategory"] {
+ if (typeof navigator === "undefined") return "unknown";
+
+ // @ts-ignore - navigator.deviceMemory is not in all browsers
+ const deviceMemory = navigator.deviceMemory;
+ if (typeof deviceMemory !== "number") return "unknown";
+
+ if (deviceMemory < 4) return "low";
+ if (deviceMemory <= 8) return "medium";
+ return "high";
+ }
+
+ /**
+ * Detect browser family (no version)
+ */
+ private detectBrowser(): ProofTelemetryEvent["device"]["browser"] {
+ if (typeof navigator === "undefined") return "other";
+
+ const ua = navigator.userAgent.toLowerCase();
+ if (ua.includes("edg")) return "edge";
+ if (ua.includes("chrome")) return "chrome";
+ if (ua.includes("firefox")) return "firefox";
+ if (ua.includes("safari")) return "safari";
+ return "other";
+ }
+
+ /**
+ * Detect WASM support
+ */
+ private detectWASMSupport(): boolean {
+ try {
+ if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
+ const module = new WebAssembly.Module(
+ Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)
+ );
+ if (module instanceof WebAssembly.Module) {
+ return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
+ }
+ }
+ } catch (e) {
+ return false;
+ }
+ return false;
+ }
}
// Export singleton instance
diff --git a/src/lib/workers/proofWorker.ts b/src/lib/workers/proofWorker.ts
index 98e722f..bc645b7 100644
--- a/src/lib/workers/proofWorker.ts
+++ b/src/lib/workers/proofWorker.ts
@@ -114,14 +114,13 @@ async function generateProof(
});
// Determine circuit size based on attestations count
- const circuitSize =
- data.circuitSize || determineCircuitSize(data.attestations.length);
+ const circuitSize = data.circuitSize || determineCircuitSize(data.attestations.length);
// Use simulation mode if requested or EZKL not initialized
if (data.useSimulation || !isInitialized) {
console.log("[ProofWorker] Using simulation mode");
const proof = await simulateZKProof(fusedOpinion, data.proofType, data.threshold);
-
+
return {
fusedOpinion,
proof,
diff --git a/src/lib/zkml/README.md b/src/lib/zkml/README.md
index c16e381..a69b1ec 100644
--- a/src/lib/zkml/README.md
+++ b/src/lib/zkml/README.md
@@ -5,6 +5,7 @@ Client-side ZK proof generation using EZKL WASM with circuit caching and hybrid
## Overview
This module provides:
+
- **EZKL WASM Integration** - Client-side proof generation in the browser
- **Circuit Caching** - Persistent IndexedDB storage with integrity verification
- **Hybrid Prover** - Automatic fallback from local to remote proof generation
@@ -109,6 +110,7 @@ const result = await hybridProver.generateProof(attestations, {
### Circuit Sizes
Circuits are automatically selected based on attestation count:
+
- **Small** (≤16 attestations): Fastest, lower memory
- **Medium** (17-64 attestations): Balanced performance
- **Large** (65+ attestations): Handles complex networks
@@ -135,6 +137,7 @@ await circuitManager.clearCache();
### Circuit Directory Structure
Expected server directory layout:
+
```
/circuits/
├── ebsl_small/
@@ -168,11 +171,13 @@ The proof worker runs in a separate thread to avoid blocking the UI:
### Worker Messages
**Initialize:**
+
```javascript
worker.postMessage({ type: "INIT" });
```
**Generate Proof:**
+
```javascript
worker.postMessage({
type: "GENERATE_PROOF",
@@ -186,26 +191,30 @@ worker.postMessage({
```
**Cancel:**
+
```javascript
worker.postMessage({
type: "CANCEL",
- data: { jobId: "job-123" }
+ data: { jobId: "job-123" },
});
```
### Worker Events
**Progress:**
+
```javascript
{ type: "PROGRESS", jobId: "job-123", progress: { stage: "Generating proof", progress: 60 } }
```
**Success:**
+
```javascript
{ type: "PROOF_GENERATED", jobId: "job-123", result: { proof, publicInputs, ... } }
```
**Error:**
+
```javascript
{ type: "PROOF_ERROR", jobId: "job-123", error: "message" }
```
@@ -278,7 +287,7 @@ try {
```svelte
- Proof Performance Dashboard
+ Proof Performance Dashboard
-
-
Proof Performance Dashboard
-
-
-
-
-
-
+
+
Proof Performance Dashboard
+
+
+
+
+
+
-
-
-
-
Total Proofs
-
{stats.total}
-
-
-
Success Rate
-
{stats.successRate.toFixed(1)}%
-
-
-
Avg Duration
-
{formatDuration(stats.avgDuration)}
-
-
-
Last Updated
-
{new Date().toLocaleTimeString()}
-
-
+
+
+
+
Total Proofs
+
{stats.total}
+
+
+
Success Rate
+
{stats.successRate.toFixed(1)}%
+
+
+
Avg Duration
+
{formatDuration(stats.avgDuration)}
+
+
+
Last Updated
+
{new Date().toLocaleTimeString()}
+
+
-
-
-
Duration Distribution
-
- {#each histogram as bucket}
-
-
-
{bucket.label}
-
{bucket.count}
-
- {/each}
-
-
+
+
+
Duration Distribution
+
+ {#each histogram as bucket}
+
+
+
{bucket.label}
+
{bucket.count}
+
+ {/each}
+
+
-
-
-
-
-
Method Breakdown
-
-
- Local:
- {stats.methodBreakdown.local}
-
-
- Remote:
- {stats.methodBreakdown.remote}
-
-
- Simulation:
- {stats.methodBreakdown.simulation}
-
-
-
+
+
+
+
+
Method Breakdown
+
+
+ Local:
+ {stats.methodBreakdown.local}
+
+
+ Remote:
+ {stats.methodBreakdown.remote}
+
+
+ Simulation:
+ {stats.methodBreakdown.simulation}
+
+
+
-
-
-
Device Breakdown
-
-
- Desktop:
- {stats.deviceBreakdown.desktop}
-
-
- Mobile:
- {stats.deviceBreakdown.mobile}
-
-
- Tablet:
- {stats.deviceBreakdown.tablet}
-
-
-
-
+
+
+
Device Breakdown
+
+
+ Desktop:
+ {stats.deviceBreakdown.desktop}
+
+
+ Mobile:
+ {stats.deviceBreakdown.mobile}
+
+
+ Tablet:
+ {stats.deviceBreakdown.tablet}
+
+
+
+
-
-
-
-
Recent Events ({filteredEvents.length})
-
-
+
+
+
+
Recent Events ({filteredEvents.length})
+
+
-
-
-
-
- | sortBy("timestamp")}
- >
- Timestamp {sortColumn === "timestamp" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
- |
- sortBy("method")}
- >
- Method {sortColumn === "method" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
- |
- sortBy("durationMs")}
- >
- Duration {sortColumn === "durationMs" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
- |
- Size |
- Device |
- Status |
-
-
-
- {#each sortedEvents as event}
-
- | {formatTimestamp(event.timestamp)} |
-
-
- {event.method.toUpperCase()}
-
- |
- {formatDuration(event.durationMs)} |
- {event.circuitSize} |
-
- {event.device.type}
- {#if event.device.ramCategory !== "unknown"}
- ({event.device.ramCategory} RAM)
- {/if}
- |
-
-
- {event.success ? "Success" : "Failed"}
-
- |
-
- {/each}
-
-
+
+
+
+
+ | sortBy("timestamp")}
+ >
+ Timestamp {sortColumn === "timestamp" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
+ |
+ sortBy("method")}
+ >
+ Method {sortColumn === "method" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
+ |
+ sortBy("durationMs")}
+ >
+ Duration {sortColumn === "durationMs" ? (sortDirection === "asc" ? "↑" : "↓") : ""}
+ |
+ Size |
+ Device |
+ Status |
+
+
+
+ {#each sortedEvents as event}
+
+ | {formatTimestamp(event.timestamp)} |
+
+
+ {event.method.toUpperCase()}
+
+ |
+ {formatDuration(event.durationMs)} |
+ {event.circuitSize} |
+
+ {event.device.type}
+ {#if event.device.ramCategory !== "unknown"}
+ ({event.device.ramCategory} RAM)
+ {/if}
+ |
+
+
+ {event.success ? "Success" : "Failed"}
+
+ |
+
+ {/each}
+
+
- {#if sortedEvents.length === 0}
-
No events to display
- {/if}
-
-
+ {#if sortedEvents.length === 0}
+
No events to display
+ {/if}
+
+
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 0e83dc6..e43265c 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -1,6 +1,6 @@
/**
* Service Worker for Circuit Pre-caching
- *
+ *
* Pre-caches /circuits/ebsl_16/32 & ezkl_bg.wasm
* Cache-first strategy for circuits
* After first load, proof works in airplane mode for cached sizes
@@ -14,143 +14,139 @@ const EZKL_CACHE_NAME = "shadowgraph-ezkl-v1";
// Circuits to pre-cache
const CIRCUIT_URLS = [
- "/circuits/ebsl_16/_compiled.wasm",
- "/circuits/ebsl_16/settings.json",
- "/circuits/ebsl_16/vk.key",
- "/circuits/ebsl_32/_compiled.wasm",
- "/circuits/ebsl_32/settings.json",
- "/circuits/ebsl_32/vk.key",
+ "/circuits/ebsl_16/_compiled.wasm",
+ "/circuits/ebsl_16/settings.json",
+ "/circuits/ebsl_16/vk.key",
+ "/circuits/ebsl_32/_compiled.wasm",
+ "/circuits/ebsl_32/settings.json",
+ "/circuits/ebsl_32/vk.key",
];
// EZKL WASM files
-const EZKL_URLS = [
- "/node_modules/@ezkljs/engine/ezkl_bg.wasm",
-];
+const EZKL_URLS = ["/node_modules/@ezkljs/engine/ezkl_bg.wasm"];
/**
* Install event - pre-cache critical files
*/
self.addEventListener("install", (event) => {
- console.log("[ServiceWorker] Installing...");
-
- event.waitUntil(
- (async () => {
- try {
- // Pre-cache circuits
- const circuitCache = await caches.open(CACHE_NAME);
- await circuitCache.addAll(CIRCUIT_URLS);
- console.log("[ServiceWorker] Circuits pre-cached");
-
- // Pre-cache EZKL WASM
- const ezklCache = await caches.open(EZKL_CACHE_NAME);
- await ezklCache.addAll(EZKL_URLS);
- console.log("[ServiceWorker] EZKL WASM pre-cached");
-
- // Skip waiting to activate immediately
- await self.skipWaiting();
- } catch (error) {
- console.error("[ServiceWorker] Pre-cache failed:", error);
- }
- })()
- );
+ console.log("[ServiceWorker] Installing...");
+
+ event.waitUntil(
+ (async () => {
+ try {
+ // Pre-cache circuits
+ const circuitCache = await caches.open(CACHE_NAME);
+ await circuitCache.addAll(CIRCUIT_URLS);
+ console.log("[ServiceWorker] Circuits pre-cached");
+
+ // Pre-cache EZKL WASM
+ const ezklCache = await caches.open(EZKL_CACHE_NAME);
+ await ezklCache.addAll(EZKL_URLS);
+ console.log("[ServiceWorker] EZKL WASM pre-cached");
+
+ // Skip waiting to activate immediately
+ await self.skipWaiting();
+ } catch (error) {
+ console.error("[ServiceWorker] Pre-cache failed:", error);
+ }
+ })()
+ );
});
/**
* Activate event - cleanup old caches
*/
self.addEventListener("activate", (event) => {
- console.log("[ServiceWorker] Activating...");
-
- event.waitUntil(
- (async () => {
- // Claim clients immediately
- await self.clients.claim();
-
- // Cleanup old caches
- const cacheNames = await caches.keys();
- await Promise.all(
- cacheNames
- .filter((name) =>
- name !== CACHE_NAME &&
- name !== EZKL_CACHE_NAME &&
- (name.startsWith("shadowgraph-") || name.startsWith("ezkl-"))
- )
- .map((name) => caches.delete(name))
- );
-
- console.log("[ServiceWorker] Activated");
- })()
- );
+ console.log("[ServiceWorker] Activating...");
+
+ event.waitUntil(
+ (async () => {
+ // Claim clients immediately
+ await self.clients.claim();
+
+ // Cleanup old caches
+ const cacheNames = await caches.keys();
+ await Promise.all(
+ cacheNames
+ .filter(
+ (name) =>
+ name !== CACHE_NAME &&
+ name !== EZKL_CACHE_NAME &&
+ (name.startsWith("shadowgraph-") || name.startsWith("ezkl-"))
+ )
+ .map((name) => caches.delete(name))
+ );
+
+ console.log("[ServiceWorker] Activated");
+ })()
+ );
});
/**
* Fetch event - cache-first strategy for circuits and EZKL
*/
self.addEventListener("fetch", (event) => {
- const url = new URL(event.request.url);
-
- // Only handle circuit and EZKL requests
- if (!url.pathname.startsWith("/circuits/") &&
- !url.pathname.includes("ezkl")) {
- return;
- }
-
- event.respondWith(
- (async () => {
- // Try cache first
- const cacheName = url.pathname.includes("ezkl")
- ? EZKL_CACHE_NAME
- : CACHE_NAME;
-
- const cache = await caches.open(cacheName);
- const cached = await cache.match(event.request);
-
- if (cached) {
- console.log(`[ServiceWorker] Cache hit: ${url.pathname}`);
- return cached;
- }
-
- // Cache miss - fetch from network
- console.log(`[ServiceWorker] Cache miss: ${url.pathname}`);
-
- try {
- const response = await fetch(event.request);
-
- // Cache successful responses
- if (response.ok) {
- cache.put(event.request, response.clone());
- }
-
- return response;
- } catch (error) {
- console.error(`[ServiceWorker] Fetch failed: ${url.pathname}`, error);
-
- // Return cached version if available (even if stale)
- const stale = await cache.match(event.request);
- if (stale) {
- console.log(`[ServiceWorker] Returning stale cache: ${url.pathname}`);
- return stale;
- }
-
- throw error;
- }
- })()
- );
+ const url = new URL(event.request.url);
+
+ // Only handle circuit and EZKL requests
+ if (!url.pathname.startsWith("/circuits/") && !url.pathname.includes("ezkl")) {
+ return;
+ }
+
+ event.respondWith(
+ (async () => {
+ // Try cache first
+ const cacheName = url.pathname.includes("ezkl") ? EZKL_CACHE_NAME : CACHE_NAME;
+
+ const cache = await caches.open(cacheName);
+ const cached = await cache.match(event.request);
+
+ if (cached) {
+ console.log(`[ServiceWorker] Cache hit: ${url.pathname}`);
+ return cached;
+ }
+
+ // Cache miss - fetch from network
+ console.log(`[ServiceWorker] Cache miss: ${url.pathname}`);
+
+ try {
+ const response = await fetch(event.request);
+
+ // Cache successful responses
+ if (response.ok) {
+ cache.put(event.request, response.clone());
+ }
+
+ return response;
+ } catch (error) {
+ console.error(`[ServiceWorker] Fetch failed: ${url.pathname}`, error);
+
+ // Return cached version if available (even if stale)
+ const stale = await cache.match(event.request);
+ if (stale) {
+ console.log(`[ServiceWorker] Returning stale cache: ${url.pathname}`);
+ return stale;
+ }
+
+ throw error;
+ }
+ })()
+ );
});
/**
* Message event - handle commands from main thread
*/
self.addEventListener("message", (event) => {
- if (event.data && event.data.type === "CLEAR_CACHE") {
- event.waitUntil(
- (async () => {
- await caches.delete(CACHE_NAME);
- await caches.delete(EZKL_CACHE_NAME);
- console.log("[ServiceWorker] Caches cleared");
- })()
- );
- }
+ if (event.data && event.data.type === "CLEAR_CACHE") {
+ event.waitUntil(
+ (async () => {
+ await caches.delete(CACHE_NAME);
+ await caches.delete(EZKL_CACHE_NAME);
+ console.log("[ServiceWorker] Caches cleared");
+ })()
+ );
+ }
});
export {};
diff --git a/tests/e2e/prover.fallback.test.ts b/tests/e2e/prover.fallback.test.ts
index c4c9ff7..d16f17e 100644
--- a/tests/e2e/prover.fallback.test.ts
+++ b/tests/e2e/prover.fallback.test.ts
@@ -1,121 +1,121 @@
/**
* E2E Test: Remote Fallback on Worker Crash
- *
+ *
* Simulates worker crash and asserts remote fallback succeeds
*/
import { test, expect } from "@playwright/test";
test.describe("Remote Fallback", () => {
- test("should fallback to remote on worker crash", async ({ page }) => {
- // Navigate to prover page
- await page.goto("/");
- await page.waitForLoadState("networkidle");
-
- // Inject code to simulate worker crash
- await page.evaluate(() => {
- // Override Worker constructor to make it crash
- const OriginalWorker = (window as any).Worker;
- (window as any).Worker = class extends OriginalWorker {
- constructor(scriptURL: string | URL, options?: WorkerOptions) {
- super(scriptURL, options);
-
- // Simulate crash after init
- setTimeout(() => {
- this.postMessage({ type: "CRASH_SIMULATION" });
- // Simulate worker error
- const errorEvent = new Event("error");
- this.dispatchEvent(errorEvent);
- }, 100);
- }
- };
- });
-
- // Track console messages
- let remoteMethodDetected = false;
- page.on("console", (msg) => {
- const text = msg.text();
- if (text.includes("method") && text.includes("remote")) {
- remoteMethodDetected = true;
- }
- });
-
- // Click generate proof button
- await page.click('[data-testid="generate-proof-button"]');
-
- // Wait for proof generation to complete (should fallback to remote)
- await page.waitForSelector('[data-testid="proof-success"]', {
- timeout: 30000, // Allow more time for remote
- });
-
- // Assert method badge shows "REMOTE" (fallback succeeded)
- const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
- expect(methodBadge).toContain("REMOTE");
-
- // Assert telemetry detected remote method
- expect(remoteMethodDetected).toBe(true);
-
- console.log("✅ Fallback test passed: Worker crash → Remote fallback succeeded");
- });
-
- test("should fallback to remote on timeout", async ({ page }) => {
- await page.goto("/");
- await page.waitForLoadState("networkidle");
-
- // Inject code to set very short timeout
- await page.evaluate(() => {
- // Override hybrid prover timeout to 1ms (forces timeout)
- (window as any).__FORCE_TIMEOUT = true;
- });
-
- // Click generate proof button
- await page.click('[data-testid="generate-proof-button"]');
-
- // Wait for proof generation to complete (should fallback to remote)
- await page.waitForSelector('[data-testid="proof-success"]', {
- timeout: 30000,
- });
-
- // Assert method badge shows "REMOTE"
- const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
- expect(methodBadge).toContain("REMOTE");
-
- console.log("✅ Timeout fallback test passed: Timeout → Remote fallback succeeded");
- });
-
- test("should fallback to remote on device capability restriction", async ({ page }) => {
- await page.goto("/");
- await page.waitForLoadState("networkidle");
-
- // Inject code to simulate low-RAM device
- await page.evaluate(() => {
- // Override device memory to simulate low RAM
- Object.defineProperty(navigator, "deviceMemory", {
- value: 2, // 2GB (below 4GB threshold)
- writable: false,
- });
- });
-
- // Reload to apply device detection
- await page.reload();
- await page.waitForLoadState("networkidle");
-
- // Check device capability message
- const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
- expect(deviceMessage).toContain("remote prover");
-
- // Click generate proof button
- await page.click('[data-testid="generate-proof-button"]');
-
- // Wait for proof generation to complete
- await page.waitForSelector('[data-testid="proof-success"]', {
- timeout: 30000,
- });
-
- // Assert method badge shows "REMOTE"
- const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
- expect(methodBadge).toContain("REMOTE");
-
- console.log("✅ Device capability test passed: Low RAM → Remote fallback");
- });
+ test("should fallback to remote on worker crash", async ({ page }) => {
+ // Navigate to prover page
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+
+ // Inject code to simulate worker crash
+ await page.evaluate(() => {
+ // Override Worker constructor to make it crash
+ const OriginalWorker = (window as any).Worker;
+ (window as any).Worker = class extends OriginalWorker {
+ constructor(scriptURL: string | URL, options?: WorkerOptions) {
+ super(scriptURL, options);
+
+ // Simulate crash after init
+ setTimeout(() => {
+ this.postMessage({ type: "CRASH_SIMULATION" });
+ // Simulate worker error
+ const errorEvent = new Event("error");
+ this.dispatchEvent(errorEvent);
+ }, 100);
+ }
+ };
+ });
+
+ // Track console messages
+ let remoteMethodDetected = false;
+ page.on("console", (msg) => {
+ const text = msg.text();
+ if (text.includes("method") && text.includes("remote")) {
+ remoteMethodDetected = true;
+ }
+ });
+
+ // Click generate proof button
+ await page.click('[data-testid="generate-proof-button"]');
+
+ // Wait for proof generation to complete (should fallback to remote)
+ await page.waitForSelector('[data-testid="proof-success"]', {
+ timeout: 30000, // Allow more time for remote
+ });
+
+ // Assert method badge shows "REMOTE" (fallback succeeded)
+ const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
+ expect(methodBadge).toContain("REMOTE");
+
+ // Assert telemetry detected remote method
+ expect(remoteMethodDetected).toBe(true);
+
+ console.log("✅ Fallback test passed: Worker crash → Remote fallback succeeded");
+ });
+
+ test("should fallback to remote on timeout", async ({ page }) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+
+ // Inject code to set very short timeout
+ await page.evaluate(() => {
+ // Override hybrid prover timeout to 1ms (forces timeout)
+ (window as any).__FORCE_TIMEOUT = true;
+ });
+
+ // Click generate proof button
+ await page.click('[data-testid="generate-proof-button"]');
+
+ // Wait for proof generation to complete (should fallback to remote)
+ await page.waitForSelector('[data-testid="proof-success"]', {
+ timeout: 30000,
+ });
+
+ // Assert method badge shows "REMOTE"
+ const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
+ expect(methodBadge).toContain("REMOTE");
+
+ console.log("✅ Timeout fallback test passed: Timeout → Remote fallback succeeded");
+ });
+
+ test("should fallback to remote on device capability restriction", async ({ page }) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+
+ // Inject code to simulate low-RAM device
+ await page.evaluate(() => {
+ // Override device memory to simulate low RAM
+ Object.defineProperty(navigator, "deviceMemory", {
+ value: 2, // 2GB (below 4GB threshold)
+ writable: false,
+ });
+ });
+
+ // Reload to apply device detection
+ await page.reload();
+ await page.waitForLoadState("networkidle");
+
+ // Check device capability message
+ const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
+ expect(deviceMessage).toContain("remote prover");
+
+ // Click generate proof button
+ await page.click('[data-testid="generate-proof-button"]');
+
+ // Wait for proof generation to complete
+ await page.waitForSelector('[data-testid="proof-success"]', {
+ timeout: 30000,
+ });
+
+ // Assert method badge shows "REMOTE"
+ const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
+ expect(methodBadge).toContain("REMOTE");
+
+ console.log("✅ Device capability test passed: Low RAM → Remote fallback");
+ });
});
diff --git a/tests/e2e/prover.local.test.ts b/tests/e2e/prover.local.test.ts
index f04bbfb..9cbc48d 100644
--- a/tests/e2e/prover.local.test.ts
+++ b/tests/e2e/prover.local.test.ts
@@ -1,6 +1,6 @@
/**
* E2E Test: Local WASM Proof Generation
- *
+ *
* Tests 16-op proof generation locally with progress events
* Asserts duration < 10000ms on capable hardware
*/
@@ -8,115 +8,115 @@
import { test, expect } from "@playwright/test";
test.describe("Local WASM Proof Generation", () => {
- test("should generate 16-op proof locally with progress events", async ({ page }) => {
- // Navigate to prover page
- await page.goto("/");
-
- // Wait for page to load
- await page.waitForLoadState("networkidle");
-
- // Check if local WASM is available
- const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
-
- // Skip test if device doesn't support local proving
- if (deviceMessage?.includes("Using remote prover")) {
- test.skip(true, "Device does not support local WASM proving");
- return;
- }
-
- // Track progress events
- const progressEvents: { stage: string; progress: number }[] = [];
-
- page.on("console", (msg) => {
- const text = msg.text();
- if (text.includes("[Telemetry] Proof event")) {
- console.log("Telemetry:", text);
- }
- // Capture progress events
- if (text.includes("stage") && text.includes("progress")) {
- try {
- const match = text.match(/stage: "([^"]+)", progress: (\d+)/);
- if (match) {
- progressEvents.push({
- stage: match[1],
- progress: parseInt(match[2]),
- });
- }
- } catch (e) {
- // Ignore parse errors
- }
- }
- });
-
- // Get start time
- const startTime = Date.now();
-
- // Click generate proof button
- await page.click('[data-testid="generate-proof-button"]');
-
- // Wait for proof generation to complete
- await page.waitForSelector('[data-testid="proof-success"]', {
- timeout: 15000, // Allow up to 15s for local generation
- });
-
- // Calculate duration
- const duration = Date.now() - startTime;
-
- // Assert duration < 10000ms (10 seconds)
- expect(duration).toBeLessThan(10000);
-
- // Assert progress events were received
- expect(progressEvents.length).toBeGreaterThan(0);
-
- // Assert progress went from 0 to 100
- const progresses = progressEvents.map((e) => e.progress);
- expect(Math.min(...progresses)).toBeLessThanOrEqual(0);
- expect(Math.max(...progresses)).toBeGreaterThanOrEqual(90);
-
- // Assert method badge shows "LOCAL"
- const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
- expect(methodBadge).toContain("LOCAL");
-
- // Assert elapsed time is displayed
- const elapsedTime = await page.locator('[data-testid="proof-duration"]').textContent();
- expect(elapsedTime).toMatch(/\d+\.\d+s/);
-
- console.log("✅ Test passed:");
- console.log(` - Duration: ${duration}ms (< 10000ms)`);
- console.log(` - Progress events: ${progressEvents.length}`);
- console.log(` - Method: LOCAL`);
- console.log(` - Elapsed: ${elapsedTime}`);
- });
-
- test("should support cancellation during proof generation", async ({ page }) => {
- await page.goto("/");
- await page.waitForLoadState("networkidle");
-
- // Skip if device doesn't support local
- const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
- if (deviceMessage?.includes("Using remote prover")) {
- test.skip(true, "Device does not support local WASM proving");
- return;
- }
-
- // Start proof generation
- await page.click('[data-testid="generate-proof-button"]');
-
- // Wait for progress to start
- await page.waitForSelector('[data-testid="proof-progress-bar"]', { timeout: 2000 });
-
- // Click cancel button
- await page.click('[data-testid="cancel-proof-button"]');
-
- // Wait for cancellation message or error
- await page.waitForSelector('[data-testid="proof-error"], [data-testid="proof-cancelled"]', {
- timeout: 2000,
- });
-
- // Assert proof generation stopped
- const hasSuccess = await page.locator('[data-testid="proof-success"]').count();
- expect(hasSuccess).toBe(0);
-
- console.log("✅ Cancellation test passed");
- });
+ test("should generate 16-op proof locally with progress events", async ({ page }) => {
+ // Navigate to prover page
+ await page.goto("/");
+
+ // Wait for page to load
+ await page.waitForLoadState("networkidle");
+
+ // Check if local WASM is available
+ const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
+
+ // Skip test if device doesn't support local proving
+ if (deviceMessage?.includes("Using remote prover")) {
+ test.skip(true, "Device does not support local WASM proving");
+ return;
+ }
+
+ // Track progress events
+ const progressEvents: { stage: string; progress: number }[] = [];
+
+ page.on("console", (msg) => {
+ const text = msg.text();
+ if (text.includes("[Telemetry] Proof event")) {
+ console.log("Telemetry:", text);
+ }
+ // Capture progress events
+ if (text.includes("stage") && text.includes("progress")) {
+ try {
+ const match = text.match(/stage: "([^"]+)", progress: (\d+)/);
+ if (match) {
+ progressEvents.push({
+ stage: match[1],
+ progress: parseInt(match[2]),
+ });
+ }
+ } catch (e) {
+ // Ignore parse errors
+ }
+ }
+ });
+
+ // Get start time
+ const startTime = Date.now();
+
+ // Click generate proof button
+ await page.click('[data-testid="generate-proof-button"]');
+
+ // Wait for proof generation to complete
+ await page.waitForSelector('[data-testid="proof-success"]', {
+ timeout: 15000, // Allow up to 15s for local generation
+ });
+
+ // Calculate duration
+ const duration = Date.now() - startTime;
+
+ // Assert duration < 10000ms (10 seconds)
+ expect(duration).toBeLessThan(10000);
+
+ // Assert progress events were received
+ expect(progressEvents.length).toBeGreaterThan(0);
+
+ // Assert progress went from 0 to 100
+ const progresses = progressEvents.map((e) => e.progress);
+ expect(Math.min(...progresses)).toBeLessThanOrEqual(0);
+ expect(Math.max(...progresses)).toBeGreaterThanOrEqual(90);
+
+ // Assert method badge shows "LOCAL"
+ const methodBadge = await page.locator('[data-testid="proof-method-badge"]').textContent();
+ expect(methodBadge).toContain("LOCAL");
+
+ // Assert elapsed time is displayed
+ const elapsedTime = await page.locator('[data-testid="proof-duration"]').textContent();
+ expect(elapsedTime).toMatch(/\d+\.\d+s/);
+
+ console.log("✅ Test passed:");
+ console.log(` - Duration: ${duration}ms (< 10000ms)`);
+ console.log(` - Progress events: ${progressEvents.length}`);
+ console.log(` - Method: LOCAL`);
+ console.log(` - Elapsed: ${elapsedTime}`);
+ });
+
+ test("should support cancellation during proof generation", async ({ page }) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+
+ // Skip if device doesn't support local
+ const deviceMessage = await page.locator('[data-testid="device-capability"]').textContent();
+ if (deviceMessage?.includes("Using remote prover")) {
+ test.skip(true, "Device does not support local WASM proving");
+ return;
+ }
+
+ // Start proof generation
+ await page.click('[data-testid="generate-proof-button"]');
+
+ // Wait for progress to start
+ await page.waitForSelector('[data-testid="proof-progress-bar"]', { timeout: 2000 });
+
+ // Click cancel button
+ await page.click('[data-testid="cancel-proof-button"]');
+
+ // Wait for cancellation message or error
+ await page.waitForSelector('[data-testid="proof-error"], [data-testid="proof-cancelled"]', {
+ timeout: 2000,
+ });
+
+ // Assert proof generation stopped
+ const hasSuccess = await page.locator('[data-testid="proof-success"]').count();
+ expect(hasSuccess).toBe(0);
+
+ console.log("✅ Cancellation test passed");
+ });
});