Skip to content

Commit 9c24f9f

Browse files
committed
feature: compile & verifier.sol
1 parent 17ddb3c commit 9c24f9f

File tree

5 files changed

+283
-9
lines changed

5 files changed

+283
-9
lines changed

apps/noir-compiler/src/app/app.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ function App() {
4444
dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: null })
4545
})
4646
plugin.internalEvents.on('noir_compiling_errored', noirCompilerErrored)
47+
48+
plugin.internalEvents.on('noir_proofing_start', () => {
49+
dispatch({ type: 'SET_PROOFING_STATUS', payload: 'proofing' })
50+
})
51+
plugin.internalEvents.on('noir_proofing_done', (inputs) => {
52+
dispatch({ type: 'SET_PROOFING_STATUS', payload: 'succeed' })
53+
dispatch({ type: 'SET_VERIFIER_INPUTS', payload: inputs })
54+
})
55+
plugin.internalEvents.on('noir_proofing_errored', (error: Error) => {
56+
dispatch({ type: 'SET_PROOFING_STATUS', payload: 'errored' })
57+
dispatch({ type: 'SET_COMPILER_FEEDBACK', payload: error.message })
58+
})
59+
4760
setIsPluginActivated(true)
4861
})
4962
}, [])

apps/noir-compiler/src/app/components/container.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,23 @@ export function Container () {
3737
compileNoirCircuit(noirApp.plugin, noirApp.appState)
3838
}
3939

40+
const handleGenerateProofClick = () => {
41+
if (!noirApp.appState.filePath) {
42+
console.error("No file path selected for generating proof.")
43+
return
44+
}
45+
noirApp.plugin.generateProof(noirApp.appState.filePath)
46+
}
47+
4048
const handleViewProgramArtefact = (e: React.MouseEvent<HTMLAnchorElement>) => {
4149
e.preventDefault()
50+
const projectRoot = noirApp.appState.filePath.substring(0, noirApp.appState.filePath.lastIndexOf('/src/'))
51+
const buildPath = projectRoot === '' ? 'build' : `${projectRoot}/build`
4252
noirApp.plugin.call('fileManager', 'open', 'build/program.json')
4353
}
4454

55+
const formattedPublicInputsString = JSON.stringify(noirApp.appState.formattedPublicInputs, null, 2)
56+
4557
return (
4658
<section>
4759
<article>
@@ -66,9 +78,66 @@ export function Container () {
6678
<CompilerFeedback feedback={noirApp.appState.compilerFeedback} filePathToId={noirApp.appState.filePathToId} openErrorLocation={handleOpenErrorLocation} hideWarnings={noirApp.appState.hideWarnings} askGPT={askGPT} />
6779
</RenderIf>
6880
<RenderIf condition={noirApp.appState.status === 'succeed'}>
69-
<a data-id="view-noir-compilation-result" className="cursor-pointer text-decoration-none" href='#' onClick={handleViewProgramArtefact}>
70-
<i className="text-success mt-1 px-1 fas fa-check"></i> View compiled noir program artefact.
71-
</a>
81+
<>
82+
<div className='mt-2'>
83+
<a data-id="view-noir-compilation-result" className="cursor-pointer text-decoration-none" href='#' onClick={handleViewProgramArtefact}>
84+
<i className="text-success mt-1 px-1 fas fa-check"></i> View compiled noir program artefact.
85+
</a>
86+
</div>
87+
88+
<div className="mt-2">
89+
<button
90+
id="noir_generate_proof"
91+
className="btn btn-primary w-100"
92+
onClick={handleGenerateProofClick}
93+
disabled={noirApp.appState.proofingStatus === 'proofing' || noirApp.appState.status === 'compiling'}
94+
>
95+
{noirApp.appState.proofingStatus === 'proofing' ? (
96+
<>
97+
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
98+
<span className="ms-1">Generating Proof...</span>
99+
</>
100+
) : (
101+
<FormattedMessage id="noir.generateProof" defaultMessage="Generate Proof" />
102+
)}
103+
</button>
104+
</div>
105+
</>
106+
</RenderIf>
107+
<RenderIf condition={noirApp.appState.proofingStatus === 'succeed' && !!noirApp.appState.formattedProof}>
108+
<div className="mt-3">
109+
<label className="noir_label form-check-label">
110+
<FormattedMessage id="noir.verifierInputs" defaultMessage="Verifier Inputs" />
111+
</label>
112+
113+
{/* _proof (bytes) */}
114+
<div className="mt-2">
115+
<label className="form-label small mb-0">
116+
<code>_proof (bytes)</code>
117+
</label>
118+
<textarea
119+
className="form-control form-control-sm"
120+
value={noirApp.appState.formattedProof}
121+
readOnly
122+
rows={4}
123+
data-id="noir-verifier-input-proof"
124+
></textarea>
125+
</div>
126+
127+
{/* _publicInputs (bytes32[]) */}
128+
<div className="mt-2">
129+
<label className="form-label small mb-0">
130+
<code>_publicInputs (bytes32[])</code>
131+
</label>
132+
<textarea
133+
className="form-control form-control-sm"
134+
value={formattedPublicInputsString}
135+
readOnly
136+
rows={4}
137+
data-id="noir-verifier-input-public-inputs"
138+
></textarea>
139+
</div>
140+
</div>
72141
</RenderIf>
73142
</div>
74143
</div>

apps/noir-compiler/src/app/reducers/state.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ export const appInitialState: AppState = {
66
autoCompile: false,
77
hideWarnings: false,
88
status: 'idle',
9-
compilerFeedback: ''
9+
compilerFeedback: '',
10+
proofingStatus: 'idle',
11+
formattedProof: '',
12+
formattedPublicInputs: []
1013
}
1114

1215
export const appReducer = (state = appInitialState, action: Actions): AppState => {
@@ -37,11 +40,41 @@ export const appReducer = (state = appInitialState, action: Actions): AppState =
3740
}
3841

3942
case 'SET_COMPILER_STATUS':
43+
if (action.payload === 'compiling') {
44+
return {
45+
...state,
46+
status: action.payload,
47+
proofingStatus: 'idle',
48+
formattedProof: '',
49+
formattedPublicInputs: []
50+
}
51+
}
4052
return {
4153
...state,
4254
status: action.payload
4355
}
4456

57+
case 'SET_PROOFING_STATUS':
58+
if (action.payload === 'proofing') {
59+
return {
60+
...state,
61+
proofingStatus: action.payload,
62+
formattedProof: '',
63+
formattedPublicInputs: []
64+
}
65+
}
66+
return {
67+
...state,
68+
proofingStatus: action.payload
69+
}
70+
71+
case 'SET_VERIFIER_INPUTS':
72+
return {
73+
...state,
74+
formattedProof: action.payload.proof,
75+
formattedPublicInputs: action.payload.publicInputs
76+
}
77+
4578
default:
4679
throw new Error()
4780
}

apps/noir-compiler/src/app/services/noirPluginClient.ts

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { DEFAULT_TOML_CONFIG } from '../actions/constants'
55
import NoirParser from './noirParser'
66
import { extractNameFromKey } from '@remix-ui/helper'
77
import axios from 'axios'
8+
import JSZip from 'jszip'
9+
import { VerifierInputs } from '../types'
10+
11+
interface NoirAbi {
12+
parameters: { name: string, type: any, visibility: 'public' | 'private' }[]
13+
return_type?: { visibility: 'public' | 'private' }
14+
}
15+
816
export class NoirPluginClient extends PluginClient {
917
public internalEvents: EventManager
1018
public parser: NoirParser
@@ -17,7 +25,7 @@ export class NoirPluginClient extends PluginClient {
1725

1826
constructor() {
1927
super()
20-
this.methods = ['init', 'parse', 'compile']
28+
this.methods = ['init', 'parse', 'compile', 'generateProof']
2129
createClient(this)
2230
this.internalEvents = new EventManager()
2331
this.parser = new NoirParser()
@@ -71,6 +79,10 @@ export class NoirPluginClient extends PluginClient {
7179
}
7280
}
7381

82+
private bytesToHex(bytes: Uint8Array): string {
83+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
84+
}
85+
7486
generateRequestID(): string {
7587
const timestamp = Math.floor(Date.now() / 1000)
7688
const random = Math.random().toString(36).substring(2, 15)
@@ -151,9 +163,10 @@ export class NoirPluginClient extends PluginClient {
151163
const { compiledJson, proverToml } = response.data
152164

153165
const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build`
154-
155166
this.call('fileManager', 'writeFile', `${buildPath}/program.json`, compiledJson)
156-
this.call('fileManager', 'writeFile', `${buildPath}/prover.toml`, proverToml)
167+
168+
const proverTomlPath = projectRoot === '/' ? 'Prover.toml' : `${projectRoot}/Prover.toml`
169+
this.call('fileManager', 'writeFile', proverTomlPath, proverToml)
157170

158171
this.internalEvents.emit('noir_compiling_done')
159172
this.emit('statusChanged', { key: 'succeed', title: 'Noir circuit compiled successfully', type: 'success' })
@@ -169,6 +182,140 @@ export class NoirPluginClient extends PluginClient {
169182
}
170183
}
171184

185+
async generateProof(path: string): Promise<void> {
186+
const requestID = this.generateRequestID()
187+
console.log(`[${requestID}] New proof generation request for: ${path}`)
188+
189+
// this.internalEvents.emit('noir_proofing_start')
190+
this.internalEvents.emit('noir_proofing_start')
191+
this.emit('statusChanged', { key: 'loading', title: 'Generating Proof...', type: 'info' })
192+
this.call('terminal', 'log', { type: 'log', value: 'Generating proof for ' + path })
193+
194+
let projectRoot: string | null = null
195+
196+
try {
197+
if (this.ws.readyState !== WebSocket.OPEN) {
198+
throw new Error('WebSocket connection not open. Cannot generate proof.')
199+
}
200+
201+
projectRoot = await this.findProjectRoot(path)
202+
if (projectRoot === null) {
203+
throw new Error(`Invalid project structure for '${path}'. Could not find project root.`)
204+
}
205+
206+
// @ts-ignore
207+
const zippedProject: Blob = await this.call('fileManager', 'download', projectRoot, false)
208+
const formData = new FormData()
209+
formData.append('file', zippedProject, `${extractNameFromKey(path)}.zip`)
210+
211+
this.ws.send(JSON.stringify({ requestId: requestID }))
212+
213+
// @ts-ignore
214+
const response = await axios.post(`${BASE_URL}/generate-proof-with-verifier?requestId=${requestID}`, formData, {
215+
responseType: 'blob'
216+
})
217+
218+
if (response.status !== 200) {
219+
try {
220+
const errorJson = JSON.parse(await response.data.text())
221+
throw new Error(errorJson.error || `Backend returned status ${response.status}`)
222+
} catch (parseError) {
223+
throw new Error(`Backend returned status ${response.status}: ${response.statusText}`)
224+
}
225+
}
226+
227+
const receivedBlob = response.data
228+
this.call('terminal', 'log', { type: 'log', value: 'Received proof artifacts. Extracting files...' })
229+
230+
const zip = await JSZip.loadAsync(receivedBlob)
231+
const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build`
232+
233+
const filesToSave = {
234+
'proof': { path: `${buildPath}/proof`, type: 'hex' },
235+
'vk': { path: `${buildPath}/vk`, type: 'hex' },
236+
'verifier/solidity/Verifier.sol': { path: 'contracts/Verifier.sol', type: 'string' },
237+
'program.json': { path: `${buildPath}/program.json`, type: 'string' },
238+
}
239+
240+
for (const [zipPath, info] of Object.entries(filesToSave)) {
241+
const file = zip.file(zipPath)
242+
if (file) {
243+
let content: string;
244+
if (info.type === 'hex') {
245+
const bytes = await file.async('uint8array');
246+
content = this.bytesToHex(bytes);
247+
} else {
248+
content = await file.async('string');
249+
}
250+
await this.call('fileManager', 'writeFile', info.path, content)
251+
// @ts-ignore
252+
this.call('terminal', 'log', { type: 'log', value: `Wrote artifact: ${info.path}` })
253+
}
254+
}
255+
256+
257+
this.call('terminal', 'log', { type: 'log', value: 'Formatting Verifier.sol inputs...' })
258+
259+
const proofFile = zip.file('formatted_proof.txt');
260+
const inputsFile = zip.file('formatted_public_inputs.json');
261+
262+
if (!proofFile || !inputsFile) {
263+
throw new Error("Formatted proof or public inputs not found in zip response from backend.");
264+
}
265+
266+
const formattedProof = await proofFile.async('string');
267+
const formattedPublicInputsStr = await inputsFile.async('string');
268+
const formattedPublicInputs = JSON.parse(formattedPublicInputsStr);
269+
270+
const verifierInputs: VerifierInputs = {
271+
proof: formattedProof,
272+
publicInputs: formattedPublicInputs
273+
}
274+
275+
this.internalEvents.emit('noir_proofing_done', verifierInputs)
276+
277+
this.emit('statusChanged', { key: 'succeed', title: 'Proof generated successfully', type: 'success' })
278+
this.call('terminal', 'log', { type: 'log', value: 'Proof generation and file extraction complete.' })
279+
// this.internalEvents.emit('noir_proofing_done')
280+
281+
} catch (e) {
282+
console.error(`[${requestID}] Proof generation failed:`, e)
283+
let errorMsg = e.message || 'Unknown error during proof generation'
284+
285+
if (e.response && e.response.data) {
286+
try {
287+
let errorData = e.response.data
288+
289+
if (e.response.data instanceof Blob) {
290+
const errorText = await e.response.data.text()
291+
errorData = JSON.parse(errorText)
292+
}
293+
294+
if (errorData.error) {
295+
errorMsg = errorData.error
296+
} else if (typeof errorData === 'string') {
297+
errorMsg = errorData
298+
}
299+
} catch (parseError) {
300+
console.error('Failed to parse backend error response:', parseError)
301+
errorMsg = e.response.statusText || e.message
302+
}
303+
}
304+
this.internalEvents.emit('noir_proofing_errored', e)
305+
this.call('terminal', 'log', { type: 'error', value: errorMsg })
306+
// this.internalEvents.emit('noir_proofing_errored', new Error(errorMsg))
307+
308+
if (projectRoot !== null) {
309+
try {
310+
const buildPath = projectRoot === '/' ? 'build' : `${projectRoot}/build`
311+
await this.call('fileManager', 'writeFile', `${buildPath}/proof_error.log`, errorMsg)
312+
} catch (logError) {
313+
console.error('Failed to write error log file:', logError)
314+
}
315+
}
316+
}
317+
}
318+
172319
async parse(path: string, content?: string): Promise<void> {
173320
if (!content) content = await this.call('fileManager', 'readFile', path)
174321
const result = this.parser.parseNoirCode(content)

apps/noir-compiler/src/app/types/index.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Dispatch } from 'react'
33
import type { NoirPluginClient } from '../services/noirPluginClient'
44

55
export type CompilerStatus = "compiling" | "idle" | "errored" | "warning" | "succeed"
6+
export type ProofingStatus = "idle" | "proofing" | "succeed" | "errored"
7+
68
export interface INoirAppContext {
79
appState: AppState
810
dispatch: Dispatch<Actions>,
@@ -15,15 +17,25 @@ export interface AppState {
1517
autoCompile: boolean,
1618
hideWarnings: boolean,
1719
status: CompilerStatus,
18-
compilerFeedback: string
20+
compilerFeedback: string,
21+
proofingStatus: ProofingStatus,
22+
formattedProof: string,
23+
formattedPublicInputs: string[]
24+
}
25+
26+
export interface VerifierInputs {
27+
proof: string,
28+
publicInputs: string[]
1929
}
2030

2131
export interface ActionPayloadTypes {
2232
SET_AUTO_COMPILE: boolean,
2333
SET_HIDE_WARNINGS: boolean,
2434
SET_FILE_PATH: string,
2535
SET_COMPILER_FEEDBACK: string,
26-
SET_COMPILER_STATUS: CompilerStatus
36+
SET_COMPILER_STATUS: CompilerStatus,
37+
SET_PROOFING_STATUS: ProofingStatus,
38+
SET_VERIFIER_INPUTS: VerifierInputs
2739
}
2840
export interface Action<T extends keyof ActionPayloadTypes> {
2941
type: T

0 commit comments

Comments
 (0)