sindri
Version:
The Sindri Labs JavaScript SDK and CLI tool.
196 lines (182 loc) • 6.61 kB
text/typescript
import fs from "fs";
import path from "path";
import process from "process";
import { Command } from "@commander-js/extra-typings";
import { collectMetaWithLogger, findFileUpwards } from "cli/utils";
import sindri from "lib";
import { ApiError } from "lib/api";
import { print } from "lib/logging";
import { getDefaultMeta } from "lib/utils";
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 if non-TTY; " +
"`input.json`, `example-input.json`, or `Prover.toml` otherwise).",
)
.option(
"-m, --meta <key=value>",
"Metadata key/value to attach to the proof.",
collectMetaWithLogger(sindri.logger),
getDefaultMeta({ logger: sindri.logger, raiseExceptions: false }),
)
.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, meta, 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();
}
if (!proofInput || !proofInput.trim()) {
// 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 {
// Poll for proof generation to complete.
const startTime = Date.now();
const response = await sindri.proveCircuit(
circuitIdentifier,
proofInput,
!!verify,
includeSmartContractCalldata,
meta,
);
const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
// Check that the status is "Ready" or log an error.
if (response.status === "Ready") {
sindri.logger.info(
`Proof generated successfully after ${elapsedSeconds} seconds.`,
);
} else if (response.status === "Failed") {
sindri.logger.error(
`Proof generation failed after ${elapsedSeconds} seconds: ` +
(response.error ?? "Unknown error."),
);
return process.exit(1);
} else {
sindri.logger.fatal(`Unexpected response status: ${response.status}`);
return process.exit(1);
}
// Print out the formatted proof response.
print(
JSON.stringify(
{
proofId: response.proof_id,
meta: response.meta,
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);