diff --git a/apps/noir-compiler/src/app/app.tsx b/apps/noir-compiler/src/app/app.tsx index cbf21a0e015..d7430d8c98d 100644 --- a/apps/noir-compiler/src/app/app.tsx +++ b/apps/noir-compiler/src/app/app.tsx @@ -44,6 +44,19 @@ function App() { dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: null }) }) plugin.internalEvents.on('noir_compiling_errored', noirCompilerErrored) + + plugin.internalEvents.on('noir_proofing_start', () => { + dispatch({ type: 'SET_PROOFING_STATUS', payload: 'proofing' }) + }) + plugin.internalEvents.on('noir_proofing_done', (inputs) => { + dispatch({ type: 'SET_PROOFING_STATUS', payload: 'succeed' }) + dispatch({ type: 'SET_VERIFIER_INPUTS', payload: inputs }) + }) + plugin.internalEvents.on('noir_proofing_errored', (error: Error) => { + dispatch({ type: 'SET_PROOFING_STATUS', payload: 'errored' }) + dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: error.message }) + }) + setIsPluginActivated(true) }) }, []) diff --git a/apps/noir-compiler/src/app/components/container.tsx b/apps/noir-compiler/src/app/components/container.tsx index 7b44adf1a90..d1044effcd5 100644 --- a/apps/noir-compiler/src/app/components/container.tsx +++ b/apps/noir-compiler/src/app/components/container.tsx @@ -5,9 +5,18 @@ import { NoirAppContext } from '../contexts' import { CompileOptions } from '@remix-ui/helper' import { compileNoirCircuit } from '../actions' +const NOIR_VERSION = 'v1.0.0-beta.12' +const BARRETENBERG_VERSION = 'v0.85.0' + export function Container () { const noirApp = useContext(NoirAppContext) + const projectRoot = noirApp.appState.filePath.substring(0, noirApp.appState.filePath.lastIndexOf('/src/')) + const buildPath = projectRoot === '' ? 'build' : `${projectRoot}/build` + const contractsPath = projectRoot === '' ? 'contracts' : `${projectRoot}/contracts` + const scriptsPath = projectRoot === '' ? 'scripts' : `${projectRoot}/scripts` + const proverTomlPath = projectRoot === '' ? 'Prover.toml' : `${projectRoot}/Prover.toml` + const showCompilerLicense = async (message = 'License not available') => { try { const response = await fetch('https://raw.githubusercontent.com/noir-lang/noir/master/LICENSE-APACHE') @@ -37,38 +46,135 @@ export function Container () { compileNoirCircuit(noirApp.plugin, noirApp.appState) } - const handleViewProgramArtefact = (e: React.MouseEvent) => { + const handleGenerateProofClick = () => { + if (!noirApp.appState.filePath) { + console.error("No file path selected for generating proof.") + return + } + noirApp.plugin.generateProof(noirApp.appState.filePath) + } + + const handleViewFile = (e: React.MouseEvent, filePath: string) => { e.preventDefault() - noirApp.plugin.call('fileManager', 'open', 'build/program.json') + noirApp.plugin.call('fileManager', 'open', filePath) } + + const formattedPublicInputsString = JSON.stringify(noirApp.appState.formattedPublicInputs, null, 2) + return (
- - - showCompilerLicense()}> - - -
+
+ + + showCompilerLicense()}> + + + {NOIR_VERSION} + +
+ {/* */} +
+
- - - - - View compiled noir program artefact. - + <> + + +
+
+ + + + +
+ +
+ +
+ + +
+ + +
+
+ + +
+
+
+ +
diff --git a/apps/noir-compiler/src/app/reducers/state.ts b/apps/noir-compiler/src/app/reducers/state.ts index 7fe068a42ab..540ee8681fe 100644 --- a/apps/noir-compiler/src/app/reducers/state.ts +++ b/apps/noir-compiler/src/app/reducers/state.ts @@ -6,7 +6,10 @@ export const appInitialState: AppState = { autoCompile: false, hideWarnings: false, status: 'idle', - compilerFeedback: '' + compilerFeedback: '', + proofingStatus: 'idle', + formattedProof: '', + formattedPublicInputs: [] } export const appReducer = (state = appInitialState, action: Actions): AppState => { @@ -37,11 +40,43 @@ export const appReducer = (state = appInitialState, action: Actions): AppState = } case 'SET_COMPILER_STATUS': + if (action.payload === 'compiling') { + return { + ...state, + status: action.payload, + proofingStatus: 'idle', + formattedProof: '', + formattedPublicInputs: [], + compilerFeedback: '' + } + } return { ...state, status: action.payload } + case 'SET_PROOFING_STATUS': + if (action.payload === 'proofing') { + return { + ...state, + proofingStatus: action.payload, + formattedProof: '', + formattedPublicInputs: [], + compilerFeedback: '' + } + } + return { + ...state, + proofingStatus: action.payload + } + + case 'SET_VERIFIER_INPUTS': + return { + ...state, + formattedProof: action.payload.proof, + formattedPublicInputs: action.payload.publicInputs + } + default: throw new Error() } diff --git a/apps/noir-compiler/src/app/services/noirPluginClient.ts b/apps/noir-compiler/src/app/services/noirPluginClient.ts index 05dab1a26a7..7c43e2402c1 100644 --- a/apps/noir-compiler/src/app/services/noirPluginClient.ts +++ b/apps/noir-compiler/src/app/services/noirPluginClient.ts @@ -5,6 +5,14 @@ import { DEFAULT_TOML_CONFIG } from '../actions/constants' import NoirParser from './noirParser' import { extractNameFromKey } from '@remix-ui/helper' import axios from 'axios' +import JSZip from 'jszip' +import { VerifierInputs } from '../types' + +interface NoirAbi { + parameters: { name: string, type: any, visibility: 'public' | 'private' }[] + return_type?: { visibility: 'public' | 'private' } +} + export class NoirPluginClient extends PluginClient { public internalEvents: EventManager public parser: NoirParser @@ -17,7 +25,7 @@ export class NoirPluginClient extends PluginClient { constructor() { super() - this.methods = ['init', 'parse', 'compile'] + this.methods = ['init', 'parse', 'compile', 'generateProof'] createClient(this) this.internalEvents = new EventManager() this.parser = new NoirParser() @@ -25,7 +33,6 @@ export class NoirPluginClient extends PluginClient { } init(): void { - console.log('initializing noir plugin...') } onActivation(): void { @@ -37,7 +44,6 @@ export class NoirPluginClient extends PluginClient { // @ts-ignore this.ws = new WebSocket(`${WS_URL}`) this.ws.onopen = () => { - console.log('WebSocket connection opened') } this.ws.onmessage = (event) => { const message = JSON.parse(event.data) @@ -54,7 +60,6 @@ export class NoirPluginClient extends PluginClient { this.logFn('WebSocket error: ' + event) } this.ws.onclose = () => { - console.log('WebSocket connection closed') // restart the websocket connection this.ws = null setTimeout(this.setupWebSocketEvents.bind(this), 5000) @@ -71,6 +76,10 @@ export class NoirPluginClient extends PluginClient { } } + private bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); + } + generateRequestID(): string { const timestamp = Math.floor(Date.now() / 1000) const random = Math.random().toString(36).substring(2, 15) @@ -151,9 +160,10 @@ export class NoirPluginClient extends PluginClient { const { compiledJson, proverToml } = response.data const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build` - this.call('fileManager', 'writeFile', `${buildPath}/program.json`, compiledJson) - this.call('fileManager', 'writeFile', `${buildPath}/prover.toml`, proverToml) + + const proverTomlPath = projectRoot === '/' ? 'Prover.toml' : `${projectRoot}/Prover.toml` + this.call('fileManager', 'writeFile', proverTomlPath, proverToml) this.internalEvents.emit('noir_compiling_done') this.emit('statusChanged', { key: 'succeed', title: 'Noir circuit compiled successfully', type: 'success' }) @@ -169,6 +179,152 @@ export class NoirPluginClient extends PluginClient { } } + async generateProof(path: string): Promise { + const requestID = this.generateRequestID() + + this.internalEvents.emit('noir_proofing_start') + this.emit('statusChanged', { key: 'loading', title: 'Generating Proof...', type: 'info' }) + this.call('terminal', 'log', { type: 'log', value: 'Generating proof for ' + path }) + + let projectRoot: string | null = null + + try { + if (this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket connection not open. Cannot generate proof.') + } + + projectRoot = await this.findProjectRoot(path) + if (projectRoot === null) { + throw new Error(`Invalid project structure for '${path}'. Could not find project root.`) + } + + // @ts-ignore + const zippedProject: Blob = await this.call('fileManager', 'download', projectRoot, false) + const formData = new FormData() + formData.append('file', zippedProject, `${extractNameFromKey(path)}.zip`) + + this.ws.send(JSON.stringify({ requestId: requestID })) + // @ts-ignore + const response = await axios.post(`${BASE_URL}/generate-proof-with-verifier?requestId=${requestID}`, formData, { + responseType: 'blob' + }) + + if (response.status !== 200) { + try { + const errorJson = JSON.parse(await response.data.text()) + throw new Error(errorJson.error || `Backend returned status ${response.status}`) + } catch (parseError) { + throw new Error(`Backend returned status ${response.status}: ${response.statusText}`) + } + } + + const receivedBlob = response.data + this.call('terminal', 'log', { type: 'log', value: 'Received proof artifacts. Extracting files...' }) + + const zip = await JSZip.loadAsync(receivedBlob) + const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build` + const contractsPath = projectRoot === '/' ? 'contracts' : `${projectRoot}/contracts` + const scriptsPath = projectRoot === '/' ? 'scripts' : `${projectRoot}/scripts` + + let formattedProof: string | null = null + let formattedPublicInputsStr: string | null = null + + const filesToSave = { + 'vk': { path: `${buildPath}/vk`, type: 'hex' }, + 'scripts/verify.ts': { path: `${scriptsPath}/verify.ts`, type: 'string', isScript: true }, + 'verifier/solidity/Verifier.sol': { path: `${contractsPath}/Verifier.sol`, type: 'string' }, + 'proof': { path: `${buildPath}/proof`, type: 'string', isProof: true }, + 'public_inputs': { path: `${buildPath}/public_inputs`, type: 'string', isPublicInputs: true }, + } + + for (const [zipPath, info] of Object.entries(filesToSave)) { + const file = zip.file(zipPath) + + if (file) { + let content: string; + + if (info.type === 'hex') { + const bytes = await file.async('uint8array'); + content = this.bytesToHex(bytes); + } else { + content = await file.async('string'); + } + + // @ts-ignore + if (info.isProof) formattedProof = content + // @ts-ignore + if (info.isPublicInputs) formattedPublicInputsStr = content + // @ts-ignore + if (info.isScript) { + content = content.replace(/%%BUILD_PATH%%/g, buildPath) + } + + await this.call('fileManager', 'writeFile', info.path, content) + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: `Wrote artifact: ${info.path}` }) + + } else { + // @ts-ignore + this.call('terminal', 'log', { type: 'warn', value: `Warning: File '${zipPath}' not found in zip from backend.` }) + } + } + // @ts-ignore + this.call('terminal', 'log', { type: 'log', value: 'Formatting Verifier.sol inputs...' }) + + if (!formattedProof || !formattedPublicInputsStr) { + console.error('[Noir Plugin] Error: formattedProof or formattedPublicInputsStr is null or empty after loop.') + throw new Error("Formatted proof or public inputs data could not be read from zip stream.") + } + + const formattedPublicInputs = JSON.parse(formattedPublicInputsStr) + + const verifierInputs: VerifierInputs = { + proof: formattedProof, + publicInputs: formattedPublicInputs + } + + this.internalEvents.emit('noir_proofing_done', verifierInputs) + + this.emit('statusChanged', { key: 'succeed', title: 'Proof generated successfully', type: 'success' }) + this.call('terminal', 'log', { type: 'log', value: 'Proof generation and file extraction complete.' }) + + } catch (e) { + console.error(`[${requestID}] Proof generation failed:`, e) + let errorMsg = e.message || 'Unknown error during proof generation' + + if (e.response && e.response.data) { + try { + let errorData = e.response.data + + if (e.response.data instanceof Blob) { + const errorText = await e.response.data.text() + errorData = JSON.parse(errorText) + } + + if (errorData.error) { + errorMsg = errorData.error + } else if (typeof errorData === 'string') { + errorMsg = errorData + } + } catch (parseError) { + console.error('Failed to parse backend error response:', parseError) + errorMsg = e.response.statusText || e.message + } + } + this.internalEvents.emit('noir_proofing_errored', e) + this.call('terminal', 'log', { type: 'error', value: errorMsg }) + + if (projectRoot !== null) { + try { + const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build` + await this.call('fileManager', 'writeFile', `${buildPath}/proof_error.log`, errorMsg) + } catch (logError) { + console.error('Failed to write error log file:', logError) + } + } + } + } + async parse(path: string, content?: string): Promise { if (!content) content = await this.call('fileManager', 'readFile', path) const result = this.parser.parseNoirCode(content) diff --git a/apps/noir-compiler/src/app/types/index.ts b/apps/noir-compiler/src/app/types/index.ts index 6f6c8f5eb11..9e823b33fc6 100644 --- a/apps/noir-compiler/src/app/types/index.ts +++ b/apps/noir-compiler/src/app/types/index.ts @@ -3,6 +3,8 @@ import { Dispatch } from 'react' import type { NoirPluginClient } from '../services/noirPluginClient' export type CompilerStatus = "compiling" | "idle" | "errored" | "warning" | "succeed" +export type ProofingStatus = "idle" | "proofing" | "succeed" | "errored" + export interface INoirAppContext { appState: AppState dispatch: Dispatch, @@ -15,7 +17,15 @@ export interface AppState { autoCompile: boolean, hideWarnings: boolean, status: CompilerStatus, - compilerFeedback: string + compilerFeedback: string, + proofingStatus: ProofingStatus, + formattedProof: string, + formattedPublicInputs: string[] +} + +export interface VerifierInputs { + proof: string, + publicInputs: string[] } export interface ActionPayloadTypes { @@ -23,7 +33,9 @@ export interface ActionPayloadTypes { SET_HIDE_WARNINGS: boolean, SET_FILE_PATH: string, SET_COMPILER_FEEDBACK: string, - SET_COMPILER_STATUS: CompilerStatus + SET_COMPILER_STATUS: CompilerStatus, + SET_PROOFING_STATUS: ProofingStatus, + SET_VERIFIER_INPUTS: VerifierInputs } export interface Action { type: T