commit
005d713ab4
@ -0,0 +1,77 @@ |
||||
import {CopyToClipboard} from '@remix-ui/clipboard' |
||||
import {CustomTooltip} from '@remix-ui/helper' |
||||
import React, {useEffect, useState} from 'react' |
||||
import {FormattedMessage, useIntl} from 'react-intl' |
||||
import {SindriSettingsProps} from '../types' |
||||
import {sindriAccessTokenLink} from './constants' |
||||
|
||||
export function SindriSettings(props: SindriSettingsProps) { |
||||
const [sindriToken, setSindriToken] = useState<string>('') |
||||
const intl = useIntl() |
||||
|
||||
useEffect(() => { |
||||
if (props.config) { |
||||
const sindriToken = props.config.get('settings/sindri-access-token') || '' |
||||
setSindriToken(sindriToken) |
||||
} |
||||
}, [props.config]) |
||||
|
||||
const handleChangeTokenState = (event) => { |
||||
const token = event.target.value ? event.target.value.trim() : event.target.value |
||||
setSindriToken(token) |
||||
} |
||||
|
||||
// api key settings
|
||||
const saveSindriToken = () => { |
||||
props.saveToken(sindriToken) |
||||
} |
||||
|
||||
const removeToken = () => { |
||||
setSindriToken('') |
||||
props.removeToken() |
||||
} |
||||
|
||||
return ( |
||||
<div className="border-top"> |
||||
<div className="card-body pt-3 pb-2"> |
||||
<h6 className="card-title"> |
||||
<FormattedMessage id="settings.sindriAccessTokenTitle" /> |
||||
</h6> |
||||
<p className="mb-1"> |
||||
<FormattedMessage id="settings.sindriAccessTokenText" /> |
||||
</p> |
||||
<p className=""> |
||||
<FormattedMessage id="settings.sindriAccessTokenText2" /> |
||||
</p> |
||||
<p className="mb-1"> |
||||
<a className="text-primary" target="_blank" href={sindriAccessTokenLink}> |
||||
{sindriAccessTokenLink} |
||||
</a> |
||||
</p> |
||||
<div> |
||||
<label className="mb-0 pb-0"> |
||||
<FormattedMessage id="settings.token" />: |
||||
</label> |
||||
<div className="input-group text-secondary mb-0 h6"> |
||||
<input id="sindriaccesstoken" data-id="settingsTabSindriAccessToken" type="password" className="form-control" onChange={(e) => handleChangeTokenState(e)} value={sindriToken} /> |
||||
<div className="input-group-append"> |
||||
<CopyToClipboard tip={intl.formatMessage({id: 'settings.copy'})} content={sindriToken} data-id="copyToClipboardCopyIcon" className="far fa-copy ml-1 p-2 mt-1" direction={'top'} /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
<div className="text-secondary mb-0 h6"> |
||||
<div className="d-flex justify-content-end pt-2"> |
||||
<input className="btn btn-sm btn-primary ml-2" id="savesindritoken" data-id="settingsTabSaveSindriToken" onClick={saveSindriToken} value={intl.formatMessage({id: 'settings.save'})} type="button"></input> |
||||
<CustomTooltip tooltipText={<FormattedMessage id="settings.deleteSindriCredentials" />} tooltipClasses="text-nowrap" tooltipId="removesindritokenTooltip" placement="top-start"> |
||||
<button className="btn btn-sm btn-secondary ml-2" id="removesindritoken" data-id="settingsTabRemoveSindriToken" onClick={removeToken}> |
||||
<FormattedMessage id="settings.remove" /> |
||||
</button> |
||||
</CustomTooltip> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,4 @@ |
||||
# Files to exclude from Sindri circuit uploads (uses `.gitignore` syntax). |
||||
/.deps/ |
||||
/scripts/ |
||||
/templates/ |
@ -0,0 +1,120 @@ |
||||
# Sindri Scripts |
||||
|
||||
The `sindri/scripts/` directory contains scripts for compiling circuits and generating Zero-Knowledge Proofs remotely using [Sindri](https://sindri.app). |
||||
This README file will walk you through all of the steps necessary to compile your circuit and generate proofs. |
||||
As you read through it, you might also find it helpful to refer to external documentation: |
||||
|
||||
- [Circom 2 Documentation](https://docs.circom.io/) |
||||
- [Sindri Documentation](https://sindri.app/docs/) |
||||
|
||||
## Add the Sindri ZK Scripts |
||||
|
||||
If you're seeing this README, then you've probably already figured out this step on your own! |
||||
You can add the Sindri ZK scripts and related project files to your workspace by clicking the hamburger icon in the upper left corner of the **File Explorer** and selecting **Add Sindri ZK scripts**. |
||||
This will automatically add this README file, several TypeScript files, a `sindri.json` project manifest, and a `.sindriignore` file to your workspace. |
||||
We'll cover these files in more detail below. |
||||
|
||||
## API Key |
||||
|
||||
To interact with the Sindri API, you will first need to create a Sindri account, generate an API key, and add it to your Remix IDE settings. |
||||
This only needs to be done once, your credentials will be shared across all of your current and future workspaces once you've added your API key. |
||||
|
||||
1. Visit [The Sindri Homepage](https://sindri.app/) and request a demo to create your account. |
||||
2. Follow the instructions in the [Access Management](https://sindri.app/docs/topic-guides/access-management/#api-key-creation-and-management) documentation to generate an API key. |
||||
3. Open the **Settings** panel by clicking on the gear icon at the very bottom of the icon panel on the left side of the Remix IDE (see the [Remix IDE Settings](https://remix-ide.readthedocs.io/en/latest/settings.html) documentation if you're having trouble finding it. |
||||
4. Navigate to the **Sindri Credentials** section of the **Settings** panel, enter your Sindri API key under **Token**, and click the **Save** button. |
||||
|
||||
## Customize `sindri.json` _(Optional)_ |
||||
|
||||
A `sindri.json` file was added to the root of your workspace, and automatically customized to fit your project layout. |
||||
This file is the **Sindri Manifest** and is required for all projects deployed to Sindri. |
||||
It's also used by the [Sindri CLI](https://github.com/Sindri-Labs/sindri-js) for local circuit operations which don't require a Sindri account. |
||||
|
||||
If the automatic customization missed something, or if you'd like to make further customizations, then you'll need to edit this file yourself. |
||||
When editing `sindri.json` in the Remix IDE, you should get in-editor diagnostics and documentation about the format of the file. |
||||
You can mouse over the different properties and their values to view their documentation and any potential errors with the values. |
||||
|
||||
The fields that you're most likely to want to customize are: |
||||
|
||||
- `name` - This is a unique project identifier for your circuit. |
||||
You can think of it as being analogous to a GitHub project name or a DockerHub image name. |
||||
Every time you compile a circuit with Sindri, the compiled circuit will be associated with the project and one or more tags (`latest` by default). |
||||
We guessed this based on your workspace name, but you can change this to something else if you don't like that name. |
||||
- `circuitPath` - This defines the entrypoint for a Circom circuit (_i.e_ the `.circom` source file which contains your `main` component). |
||||
We did our best to guess this as well, but you'll need to update this manually if you refactor your circuit files or the wrong entrypoint was detected. |
||||
|
||||
## Customize `.sindriignore` _(Optional)_ |
||||
|
||||
A `.sindriignore` file was automatically added to the root of your workspace when you added the ZK scripts. |
||||
This file can be used to exclude files and directories from your circuit package when deploying it to Sindri, and it follows the [`.gitignore` Format](https://git-scm.com/docs/gitignore). |
||||
The generated file includes some sane defaults, but you can feel free to customize it as you see fit. |
||||
This is particularly useful if you want to exclude files that contain sensitive information like credentials or secret keys. |
||||
Excluding irrelevant files will also have a positive impact on the performance of compiling and generating proofs because less data needs to be transferred. |
||||
|
||||
## Compile the Circuit |
||||
|
||||
The `scripts/sindri/run_compile.ts` script can be used to compile a new version of your circuit. |
||||
To run it, you can open the script from the **File Explorer**, then either click the play button icon in the upper left corner of the editor panel or press `CTRL + SHIFT + S`. |
||||
After running it, you should see something like |
||||
|
||||
``` |
||||
Compiling circuit "multiplier2"... |
||||
Circuit compiled successfully, circuit id: f593a775-723c-4c57-8d75-196aa8c22aa0 |
||||
``` |
||||
|
||||
indicating that the circuit compiled successfully. |
||||
|
||||
By default, this newly compiled circuit will be assigned a tag of `latest` and replace any previous circuit with that tag. |
||||
If you would like to use alternative tags, you can modify the script to pass an array of tags to the `compile()` function call in the script. |
||||
We recommend starting out with the default of `latest` as you're getting started, and then moving towards tighter tag management once you're closer to productionizing your circuit. |
||||
|
||||
## Generate a Proof |
||||
|
||||
Once you've compiled your circuit, you're almost ready to use the `scripts/sindri/run_prove.ts` script to generate a proof. |
||||
You'll first need to modify this file to pass in the input signals that you would like to generate a proof for when calling `prove(signals)` (see Circom's [Signals & Variables](https://docs.circom.io/circom-language/signals/) documentation if you need a refresher on circuit signals). |
||||
Towards the top of the script, you'll see where the `signals` variable is defined. |
||||
|
||||
```typescript |
||||
const signals: {[name: string]: number | string} = {} |
||||
``` |
||||
|
||||
You'll need to modify this object to include a map from your circuit's signal names to the values you would like to generate a proof with. |
||||
If the signals aren't set correctly, then you'll get an error when you try to generate a proof, so make sure you don't skip this step. |
||||
|
||||
While the `scripts/sindri/run_prove.ts` script is open in the editor, you can click the play icon or press `CTRL + SHIFT + S` to run the script. |
||||
If proof generation is successful, you should see an output like this. |
||||
|
||||
``` |
||||
Proving circuit "multiplier2"... |
||||
Proof generated successfully, proof id: 8c457574-99cd-4042-a598-0514ee83ea28 |
||||
Proof: |
||||
{ |
||||
"pi_a": [ |
||||
"6067132175610399619979395342154926888794311761598436094198046058376456187483", |
||||
"12601521866404307402196517712981356634013036480344794909770435164414221099781", |
||||
"1" |
||||
], |
||||
"pi_b": [ |
||||
[ |
||||
"4834637265002576910303922443793957462767968914058257618737938706178679757759", |
||||
"9112483377654285712375849001111771826297690938023943203596780715231459796539" |
||||
], |
||||
[ |
||||
"10769047435756102293620257834720404252539733306406452142820929656229947907912", |
||||
"13357635314682194333795190402038393873064494630028726306217246944693858036728" |
||||
], |
||||
[ |
||||
"1", |
||||
"0" |
||||
] |
||||
], |
||||
"pi_c": [ |
||||
"14880777940364750676687351211095959384403767617776048892575602333362895582325", |
||||
"16991336882479219442414889002846661737157620156103416755440340170710340617407", |
||||
"1" |
||||
], |
||||
"protocol": "groth16" |
||||
} |
||||
``` |
||||
|
||||
You can either manually copy the proof to wherever you would like to use it, or modify the script to save it to a dedicated location. |
@ -0,0 +1,94 @@ |
||||
const getWorkspaceFilesByPath = async (plugin: any, pathRegex: RegExp | null = null): Promise<{[path: string]: File}> => { |
||||
const filesByPath: {[path: string]: File} = {} |
||||
interface Workspace { |
||||
children?: Workspace |
||||
content?: string |
||||
} |
||||
const workspace: Workspace = await plugin.call('fileManager', 'copyFolderToJson', '/') |
||||
const childQueue: Array<[string, Workspace]> = Object.entries(workspace) |
||||
while (childQueue.length > 0) { |
||||
const [path, child] = childQueue.pop() |
||||
if ('content' in child && (pathRegex === null || pathRegex.test(path))) { |
||||
filesByPath[path] = new File([child.content], path) |
||||
} |
||||
if ('children' in child) { |
||||
childQueue.push(...Object.entries(child.children)) |
||||
} |
||||
} |
||||
return filesByPath |
||||
} |
||||
|
||||
export const sindriScripts = async (plugin: any) => { |
||||
// Load in all of the Sindri or circuit-related files in the workspace.
|
||||
const existingFilesByPath = await getWorkspaceFilesByPath(plugin, /sindri|\.circom$/i) |
||||
const writeIfNotExists = async (path: string, content: string) => { |
||||
if (!(path in existingFilesByPath)) { |
||||
await plugin.call('fileManager', 'writeFile', path, content) |
||||
} |
||||
} |
||||
|
||||
// Write out all of the static files if they don't exist.
|
||||
// @ts-ignore
|
||||
await writeIfNotExists('.sindriignore', (await import('!!raw-loader!./.sindriignore')).default) |
||||
// @ts-ignore
|
||||
await writeIfNotExists('scripts/sindri/README.md', (await import('!!raw-loader!./README.md')).default) |
||||
// @ts-ignore
|
||||
await writeIfNotExists('scripts/sindri/run_compile.ts', (await import('!!raw-loader!./run_compile.ts')).default) |
||||
// @ts-ignore
|
||||
await writeIfNotExists('scripts/sindri/run_prove.ts', (await import('!!raw-loader!./run_prove.ts')).default) |
||||
// @ts-ignore
|
||||
await writeIfNotExists('scripts/sindri/utils.ts', (await import('!!raw-loader!./utils.ts')).default) |
||||
|
||||
// Only write out the `sindri.json` file if it doesn't already exist.
|
||||
if (!('sindri.json' in existingFilesByPath)) { |
||||
// @ts-ignore
|
||||
const sindriManifest = (await import('./sindri.json')).default |
||||
|
||||
// TODO: We can try to infer the circuit framework here from the project contents.
|
||||
// For now, we only support Circom.
|
||||
|
||||
// Infer manifest properties from the existing files in the workspace.
|
||||
if (sindriManifest.circuitType === 'circom') { |
||||
// Try to find the best `.circom` source file to use as the main component.
|
||||
// First, we limit ourselves to `.circom` files.
|
||||
const circomPathsAndContents = await Promise.all( |
||||
Object.entries(existingFilesByPath) |
||||
.filter(([path]) => /\.circom$/i.test(path)) |
||||
.map(async ([path, file]) => [path, await file.text()]) |
||||
) |
||||
// Now we apply some heuristics to find the "best" file.
|
||||
const circomCircuitPath = |
||||
circomPathsAndContents |
||||
.map(([path, content]) => ({ |
||||
content, |
||||
hasMainComponent: !!/^[ \t\f]*component[ \t\f]+main[^\n\r]*;[ \t\f]*$/m.test(content), |
||||
// These files are the entrypoints to the Remix Circom templates, so we give them a boost if there are multiple main components.
|
||||
isTemplateEntrypoint: !!['calculate_hash.circom', 'rln.circom', 'semaphore.circom'].includes(path.split('/').pop() ?? ''), |
||||
path, |
||||
})) |
||||
.sort((a, b) => { |
||||
if (a.hasMainComponent !== b.hasMainComponent) return +b.hasMainComponent - +a.hasMainComponent |
||||
if (a.isTemplateEntrypoint !== b.isTemplateEntrypoint) return +b.isTemplateEntrypoint - +a.isTemplateEntrypoint |
||||
return a.path.localeCompare(b.path) |
||||
}) |
||||
.map(({path}) => path)[0] || './circuit.circom' |
||||
sindriManifest.circuitPath = circomCircuitPath |
||||
} |
||||
|
||||
// Derive the circuit name from the workspace name.
|
||||
const {name: workspaceName} = await plugin.call('filePanel', 'getCurrentWorkspace') |
||||
sindriManifest.name = |
||||
workspaceName |
||||
.replace(/\s*-+\s*\d*$/, '') |
||||
.replace(/[^a-zA-Z0-9]+/g, '-') |
||||
.replace(/^[^a-zA-Z]+/, '') |
||||
.toLowerCase() || `my-${sindriManifest.circuitType}-circuit` |
||||
|
||||
// Write out the modified manifest file.
|
||||
writeIfNotExists('sindri.json', JSON.stringify(sindriManifest, null, 2)) |
||||
} |
||||
|
||||
// Open the README file in the editor.
|
||||
await plugin.call('doc-viewer' as any, 'viewDocs', ["scripts/sindri/README.md"]) |
||||
plugin.call('tabs' as any, 'focus', 'doc-viewer') |
||||
} |
@ -0,0 +1,7 @@ |
||||
import {compile} from './utils' |
||||
|
||||
const main = async () => { |
||||
const circuit = await compile() |
||||
} |
||||
|
||||
main() |
@ -0,0 +1,15 @@ |
||||
import {prove} from './utils' |
||||
|
||||
// You must modify the input signals to include the data you're trying to generate a proof for.
|
||||
const signals: {[name: string]: number | string} = {} |
||||
|
||||
const main = async () => { |
||||
if (Object.keys(signals).length === 0) { |
||||
console.error("You must modify the input signals to include the data you're trying to generate a proof for.") |
||||
return |
||||
} |
||||
const proofResponse = await prove(signals) |
||||
console.log('Proof:\n', JSON.stringify(proofResponse.proof, null, 2)) |
||||
} |
||||
|
||||
main() |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"$schema": "https://sindri.app/api/v1/sindri-manifest-schema.json", |
||||
"name": "circuit_name", |
||||
"circuitPath": "./circuits/circuit.circom", |
||||
"circuitType": "circom", |
||||
"curve": "bn254", |
||||
"provingScheme": "groth16", |
||||
"witnessCompiler": "c++" |
||||
} |
@ -0,0 +1,139 @@ |
||||
import sindriClient from 'sindri' |
||||
import type {CircuitInfoResponse, ProofInfoResponse} from 'sindri' |
||||
|
||||
sindriClient.logLevel = 'info' |
||||
|
||||
const authorize = async () => { |
||||
try { |
||||
const apiKey = await remix.call('settings', 'get', 'settings/sindri-access-token') |
||||
if (!apiKey) { |
||||
throw new Error('Missing API key.') |
||||
} |
||||
sindriClient.authorize({apiKey}) |
||||
} catch { |
||||
const message = 'No Sindri API key found. Please click the gear in the lower left corner to open the settings page, and add your API key under "Sindri Credentials".' |
||||
await remix.call('notification', 'toast', message) |
||||
throw new Error(message) |
||||
} |
||||
} |
||||
|
||||
const getSindriManifest = async () => { |
||||
const sindriJson = await remix.call('fileManager', 'readFile', `sindri.json`) |
||||
return JSON.parse(sindriJson) |
||||
} |
||||
|
||||
const normalizePath = (path: string): string => { |
||||
while (path.startsWith('/') || path.startsWith('./')) { |
||||
path = path.replace(/^(\.\/|\/)/, '') |
||||
} |
||||
return path |
||||
} |
||||
|
||||
/** |
||||
* Compile the circuit. |
||||
* |
||||
* @param {string | string[] | null} tags - The tag or tags to use when compiling the circuit. |
||||
* @returns {CircuitInfoResponse} compiled circuit |
||||
*/ |
||||
export const compile = async (tags: string | string[] | null = ['latest']): CircuitInfoResponse => { |
||||
await authorize() |
||||
const sindriManifest = await getSindriManifest() |
||||
|
||||
// Create a map from file paths to `File` objects for (almost) all files in the workspace.
|
||||
// We exclude `.deps/` files because these are resolved to more intuitive locations so they can
|
||||
// be used by the circuit without specifying a complex import path. We'll merge the dependencies
|
||||
// into the files at their expected import paths in a later step.
|
||||
const excludeRegex = /^\.deps\// |
||||
const filesByPath: {[path: string]: File} = {} |
||||
interface Workspace { |
||||
children?: Workspace |
||||
content?: string |
||||
} |
||||
const workspace: Workspace = await remix.call('fileManager', 'copyFolderToJson', '/') |
||||
const childQueue: Array<[string, Workspace]> = Object.entries(workspace) |
||||
while (childQueue.length > 0) { |
||||
const [path, child] = childQueue.pop() |
||||
if ('content' in child && !excludeRegex.test(path)) { |
||||
filesByPath[path] = new File([child.content], path) |
||||
} |
||||
if ('children' in child) { |
||||
childQueue.push(...Object.entries(child.children)) |
||||
} |
||||
} |
||||
|
||||
// Merge any of the circuit's resolved dependencies into the files at their expected import paths.
|
||||
if (sindriManifest.circuitType === 'circom') { |
||||
const circuitPath = normalizePath(sindriManifest.circuitPath || 'circuit.circom') |
||||
let circuitContent: string |
||||
try { |
||||
circuitContent = await remix.call('fileManager', 'readFile', circuitPath) |
||||
} catch (error) { |
||||
console.error(`No circuit file found at "${circuitPath}", try setting "circuitPath" in "sindri.json".`) |
||||
} |
||||
const dependencies: {[path: string]: string} = await remix.call('circuit-compiler' as any, 'resolveDependencies', circuitPath, circuitContent) |
||||
Object.entries(dependencies).forEach(([rawPath, rawContent]) => { |
||||
// Convert absolute file paths to paths relative to the project root.
|
||||
const path = normalizePath(rawPath) |
||||
// Removes any leading `/`s from Circom `include` paths to make them relative to the root.
|
||||
const content = path.endsWith('.circom') ? rawContent.replace(/^\s*include\s+"\/+([^"]+)"\s*;\s*$/gm, 'include "$1";') : rawContent |
||||
filesByPath[path] = new File([content], path) |
||||
}) |
||||
} |
||||
|
||||
console.log(`Compiling circuit "${sindriManifest.name}"...`) |
||||
const files = Object.values(filesByPath) |
||||
try { |
||||
const circuitResponse = await sindriClient.createCircuit(files, tags) |
||||
if (circuitResponse.status === 'Ready') { |
||||
console.log(`Circuit compiled successfully, circuit id: ${circuitResponse.circuit_id}`) |
||||
} else { |
||||
console.error('Circuit compilation failed:', circuitResponse.error || 'Unknown error') |
||||
} |
||||
return circuitResponse |
||||
} catch (error) { |
||||
if ('status' in error && error.status === 401) { |
||||
const message = 'Sindri API key authentication failed, please check that your key is correct in the settings.' |
||||
console.error(message) |
||||
throw new Error(message) |
||||
} else { |
||||
console.error('Unknown error occurred.') |
||||
throw error |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Generate a proof against the circuit. |
||||
* |
||||
* @param {Object} signals - Input signals for the circuit. |
||||
* @returns {ProofInfoResponse} The generated proof. |
||||
*/ |
||||
export const prove = async (signals: {[id: string]: number | string}): ProofInfoResponse => { |
||||
await authorize() |
||||
const sindriManifest = await getSindriManifest() |
||||
|
||||
const circuitName = sindriManifest.name |
||||
console.log(`Proving circuit "${circuitName}"...`) |
||||
try { |
||||
const proofResponse = await sindriClient.proveCircuit(circuitName, JSON.stringify(signals)) |
||||
if (proofResponse.status === 'Ready') { |
||||
console.log(`Proof generated successfully, proof id: ${proofResponse.proof_id}`) |
||||
} else { |
||||
console.error('Proof generation failed:', proofResponse.error || 'Unknown error') |
||||
} |
||||
return proofResponse |
||||
} catch (error) { |
||||
if ('status' in error && error.status === 401) { |
||||
const message = 'Sindri API key authentication failed, please check that your key is correct in the settings.' |
||||
console.error(message) |
||||
throw new Error(message) |
||||
} else if ('status' in error && error.status === 404) { |
||||
const message = `No compiled circuit "${circuitName}" found, have you successfully compiled the circuit?` |
||||
console.error(message) |
||||
throw new Error(message) |
||||
} else { |
||||
console.error('Unknown error occurred.') |
||||
throw error |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue