Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a sindri proof create command #90

Merged
merged 13 commits into from
Mar 15, 2024
2 changes: 2 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { deployCommand } from "cli/deploy";
import { lintCommand } from "cli/lint";
import { loginCommand } from "cli/login";
import { logoutCommand } from "cli/logout";
import { proofCommand } from "cli/proof";
import { whoamiCommand } from "cli/whoami";
import { loadPackageJson } from "cli/utils";
import sindri from "lib";
Expand All @@ -35,6 +36,7 @@ export const program = new Command()
.addCommand(lintCommand)
.addCommand(loginCommand)
.addCommand(logoutCommand)
.addCommand(proofCommand)
.addCommand(whoamiCommand)
// Parse the base command options and respond to them before invoking the subcommand.
.hook("preAction", async (command) => {
Expand Down
164 changes: 164 additions & 0 deletions src/cli/proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import fs from "fs";
import path from "path";
import process from "process";

import { Command } from "@commander-js/extra-typings";

import { findFileUpwards } from "cli/utils";
import sindri from "lib";
import { ApiError } from "lib/api";
import { print } from "lib/logging";

const readStdin = async (): Promise<string> => {
let inputData = "";
return new Promise((resolve) => {
process.stdin.on("data", (chunk) => (inputData += chunk));
process.stdin.on("end", () => resolve(inputData));
});
};

const proofCreateCommand = new Command()
.name("create")
.description("Create a proof for the circuit.")
.option(
"-i, --input <input>",
"Input file for the proof (defaults to stdin in on-TTY; " +
"`input.json`, `example-input.json`, or `Prover.toml` otherwise).",
)
.option("-t, --tag <tag>", "Tag to generate the proof from.", "latest")
.option(
"-v, --verify",
"Perform verification of the proof after creating it.",
)
.action(async ({ input, tag, verify }) => {
// Check that the API client is authorized.
if (!sindri.apiKey || !sindri.baseUrl) {
sindri.logger.warn("You must login first with `sindri login`.");
return process.exit(1);
}

// Find `sindri.json` and move into the root of the project directory.
const currentDirectoryPath = path.resolve(".");
if (!fs.existsSync(currentDirectoryPath)) {
sindri.logger.error(
`The "${currentDirectoryPath}" directory does not exist. Aborting.`,
);
return process.exit(1);
}
const sindriJsonPath = findFileUpwards(
/^sindri.json$/i,
currentDirectoryPath,
);
if (!sindriJsonPath) {
sindri.logger.error(
`No "sindri.json" file was found in or above "${currentDirectoryPath}". Aborting.`,
);
return process.exit(1);
}
sindri.logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`);
const rootDirectory = path.dirname(sindriJsonPath);
sindri.logger.debug(`Changing current directory to "${rootDirectory}".`);
process.chdir(rootDirectory);

// Load `sindri.json` and find the circuit name.
let sindriJson: object = {};
try {
const sindriJsonContent = fs.readFileSync(sindriJsonPath, {
encoding: "utf-8",
});
sindriJson = JSON.parse(sindriJsonContent);
sindri.logger.debug(
`Successfully loaded "sindri.json" from "${sindriJsonPath}":`,
);
sindri.logger.debug(sindriJson);
} catch (error) {
sindri.logger.fatal(
`Error loading "${sindriJsonPath}", perhaps it is not valid JSON?`,
);
sindri.logger.error(error);
return process.exit(1);
}
if (!("name" in sindriJson)) {
sindri.logger.error('No "name" field found in "sindri.json". Aborting.');
return process.exit(1);
}
const circuitName = sindriJson.name;

// Reed in the proof input.
let proofInput: string | undefined;
if (input && fs.existsSync(input)) {
// Read from the specified input file.
proofInput = fs.readFileSync(input, "utf-8");
} else if (!process.stdin.isTTY || input === "-") {
// Read from stdin in a non-TTY context.
proofInput = await readStdin();
} else {
// Try to load from common input filenames.
const defaultInputFiles = [
"input.json",
"example-input.json",
"Prover.toml",
];
for (const file of defaultInputFiles) {
const qualifiedFile = path.join(rootDirectory, file);
if (fs.existsSync(file)) {
proofInput = fs.readFileSync(qualifiedFile, "utf-8");
break;
}
}

if (!proofInput) {
console.error(
"No input file specified, none of the default files found, and not in a non-TTY context.",
);
process.exit(1);
}
}

// Only Circom supports smart contract calldata right now, so we only enable it for that circuit
// type. We'll need to update this as we add support for more circuit types.
const includeSmartContractCalldata =
"circuitType" in sindriJson &&
typeof sindriJson.circuitType === "string" &&
["circom"].includes(sindriJson.circuitType);

const circuitIdentifier = `${circuitName}:${tag}`;
try {
const response = await sindri.proveCircuit(
circuitIdentifier,
proofInput,
!!verify,
includeSmartContractCalldata,
);
print(
JSON.stringify(
{
proofId: response.proof_id,
proof: response.proof,
public: response.public,
// TODO: We need to figure out if this is the format we want to expose.
// smart_contract_calldata: response.smart_contract_calldata,
verification_key: response.verification_key,
},
null,
2,
),
);
} catch (error) {
// TODO: Better error handling.
if (error instanceof ApiError && error.status === 404) {
sindri.logger.error(
`No circuit found with the name "${circuitName}" and tag "${tag}".`,
);
} else {
sindri.logger.fatal("An unknown error occurred.");
sindri.logger.error(error);
return process.exit(1);
}
}
});

export const proofCommand = new Command()
.name("proof")
.description("Commands related to proofs for the current circuit.")
.addCommand(proofCreateCommand);
2 changes: 1 addition & 1 deletion src/lib/api/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class ApiClient {
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) {
this.request = new HttpRequest({
BASE: config?.BASE ?? 'https://sindri.app',
VERSION: config?.VERSION ?? '1.6.7',
VERSION: config?.VERSION ?? '1.6.9',
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? 'include',
TOKEN: config?.TOKEN,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/api/core/OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type OpenAPIConfig = {

export const OpenAPI: OpenAPIConfig = {
BASE: "https://sindri.app",
VERSION: "1.6.7",
VERSION: "1.6.9",
WITH_CREDENTIALS: false,
CREDENTIALS: "include",
TOKEN: undefined,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type { APIKeyResponse } from './models/APIKeyResponse';
export type { CircomCircuitInfoResponse } from './models/CircomCircuitInfoResponse';
export type { CircuitDoesNotExistResponse } from './models/CircuitDoesNotExistResponse';
export type { CircuitInfoResponse } from './models/CircuitInfoResponse';
export type { CircuitIsNotReadyResponse } from './models/CircuitIsNotReadyResponse';
export type { CircuitType } from './models/CircuitType';
export type { ComingSoonResponse } from './models/ComingSoonResponse';
export type { ForgeInternalErrorResponse } from './models/ForgeInternalErrorResponse';
Expand All @@ -31,6 +32,7 @@ export type { ProofCannotBeCreatedResponse } from './models/ProofCannotBeCreated
export type { ProofDoesNotExistResponse } from './models/ProofDoesNotExistResponse';
export type { ProofInfoResponse } from './models/ProofInfoResponse';
export type { Schema } from './models/Schema';
export type { SmartContractVerifierResponse } from './models/SmartContractVerifierResponse';
export type { TeamDetail } from './models/TeamDetail';
export type { TeamMeResponse } from './models/TeamMeResponse';
export type { TokenObtainPairInputSchema } from './models/TokenObtainPairInputSchema';
Expand Down
51 changes: 51 additions & 0 deletions src/lib/api/models/CircomCircuitInfoResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,37 @@ import type { JobStatus } from "./JobStatus";
* Response for getting Circom circuit info.
*/
export type CircomCircuitInfoResponse = {
/**
* A unique identifier generated for the circuit. UUID4 format.
*/
circuit_id: string;
/**
* The name of a circuit. This can be used in place of circuit_id for proving. This is specified during creation in the included sindri.json file.
*/
circuit_name: string;
/**
* The development framework used to write the circuit. This is specified during creation in the included sindri.json file.
*/
circuit_type: "circom";
/**
* The UTC datetime the circuit was uploaded in ISO8601 format.
*/
date_created: string;
/**
* The number of proofs submitted for this circuit.
*/
num_proofs: number;
/**
* The proving scheme for this circuit. This is specified during creation in the included sindri.json file.
*/
proving_scheme: string;
/**
* The status of the circuit job.
*/
status: JobStatus;
/**
* The user/team that owns this circuit.
*/
team: string;
/**
* Total compute time in ISO8601 format. This does not include the Queued time.
Expand All @@ -25,17 +49,44 @@ export type CircomCircuitInfoResponse = {
* Total compute time in seconds. This does not include the Queued time.
*/
compute_time_sec?: number;
/**
* Detailed compute times for the circuit compilation.
*/
compute_times?: any;
/**
* Total size of stored file(s) in bytes.
*/
file_size?: number;
/**
* The name of the uploaded circuit file. Note: the CLI and SDKs create a generic name when a directory is specified for upload.
*/
uploaded_file_name: string;
/**
* The verification key of this circuit.
*/
verification_key?: Record<string, any>;
/**
* The error message for a failed circuit upload.
*/
error?: string;
/**
* The elliptic curve over which the proving protocol takes place.
*/
curve: string;
/**
* The number of constraints in the Rank-1 Constraint System (R1CS) of the circuit.
*/
num_constraints?: number;
/**
* The number of public outputs from the circuit.
*/
num_outputs?: number;
/**
* The number of private inputs for the circuit.
*/
num_private_inputs?: number;
/**
* The number of public inputs for the circuit.
*/
num_public_inputs?: number;
};
14 changes: 14 additions & 0 deletions src/lib/api/models/CircuitIsNotReadyResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

/**
* Action: Attempt to prove with a non-ready circuit (not compiled)
* Error: The circuit is not ready to accept proofs
*/
export type CircuitIsNotReadyResponse = {
error: string;
circuit_id: string;
message?: string;
};
42 changes: 42 additions & 0 deletions src/lib/api/models/GnarkCircuitInfoResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,37 @@ import type { JobStatus } from "./JobStatus";
* Response for getting Gnark circuit info.
*/
export type GnarkCircuitInfoResponse = {
/**
* A unique identifier generated for the circuit. UUID4 format.
*/
circuit_id: string;
/**
* The name of a circuit. This can be used in place of circuit_id for proving. This is specified during creation in the included sindri.json file.
*/
circuit_name: string;
/**
* The development framework used to write the circuit. This is specified during creation in the included sindri.json file.
*/
circuit_type: "gnark";
/**
* The UTC datetime the circuit was uploaded in ISO8601 format.
*/
date_created: string;
/**
* The number of proofs submitted for this circuit.
*/
num_proofs: number;
/**
* The proving scheme for this circuit. This is specified during creation in the included sindri.json file.
*/
proving_scheme: string;
/**
* The status of the circuit job.
*/
status: JobStatus;
/**
* The user/team that owns this circuit.
*/
team: string;
/**
* Total compute time in ISO8601 format. This does not include the Queued time.
Expand All @@ -25,14 +49,32 @@ export type GnarkCircuitInfoResponse = {
* Total compute time in seconds. This does not include the Queued time.
*/
compute_time_sec?: number;
/**
* Detailed compute times for the circuit compilation.
*/
compute_times?: any;
/**
* Total size of stored file(s) in bytes.
*/
file_size?: number;
/**
* The name of the uploaded circuit file. Note: the CLI and SDKs create a generic name when a directory is specified for upload.
*/
uploaded_file_name: string;
/**
* The verification key of this circuit.
*/
verification_key?: Record<string, any>;
/**
* The error message for a failed circuit upload.
*/
error?: string;
/**
* The elliptic curve over which the proving protocol takes place.
*/
curve: string;
/**
* The Gnark frontend version tag.
*/
gnark_version: string;
};
Loading