use clap::{Parser, Subcommand, ValueEnum};
use regex::Regex;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use export_aptos_verifier_core::curves::create_adapter;
use export_aptos_verifier_core::error::{Error, Result};
use export_aptos_verifier_core::formats::{
load_arkworks_inputs, load_compact_bundle, load_snarkjs_json_inputs_with_optional_proof,
};
use export_aptos_verifier_core::local_verify;
use export_aptos_verifier_core::movegen::{
generate_move_package, proof_data_snippet, GenerateMovePackageOptions, MovegenMode,
};
#[derive(Parser)]
#[command(
name = "export-aptos-verifier",
version,
about = "Export Groth16 artifacts to an Aptos Move verifier package"
)]
struct Cli {
#[command(flatten)]
generate: GenerateArgs,
#[command(subcommand)]
command: Option<CliCommand>,
}
#[derive(Subcommand)]
enum CliCommand {
ProofData(ProofDataArgs),
}
#[derive(clap::Args)]
struct GenerateArgs {
#[arg(long)]
vk: Option<PathBuf>,
#[arg(long)]
proof: Option<PathBuf>,
#[arg(long)]
public: Option<PathBuf>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
package_name: Option<String>,
#[arg(long)]
module_name: Option<String>,
#[arg(long, default_value = "0x0")]
account_address: String,
#[arg(long, default_value_t = ModeArg::Entry)]
mode: ModeArg,
#[arg(long, default_value_t = false)]
run_aptos_test: bool,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false)]
skip_local_verify: bool,
#[arg(long, default_value_t = false)]
prepared: bool,
#[arg(long)]
bundle: Option<PathBuf>,
}
#[derive(clap::Args)]
struct ProofDataArgs {
#[arg(long)]
vk: Option<PathBuf>,
#[arg(long)]
proof: Option<PathBuf>,
#[arg(long)]
public: Option<PathBuf>,
#[arg(long)]
bundle: Option<PathBuf>,
#[arg(long, default_value_t = false)]
skip_local_verify: bool,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum ModeArg {
Library,
Entry,
Test,
}
impl ModeArg {
fn into_move_mode(self) -> MovegenMode {
match self {
Self::Library => MovegenMode::Library,
Self::Entry => MovegenMode::Entry,
Self::Test => MovegenMode::Test,
}
}
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Some(CliCommand::ProofData(args)) => run_proof_data(args),
None => run_generate(cli.generate),
};
if let Err(error) = result {
eprintln!("{error}");
std::process::exit(1);
}
}
fn run_proof_data(args: ProofDataArgs) -> Result<()> {
let inputs = load_inputs(
args.bundle.as_ref(),
args.vk.as_ref(),
args.proof.as_ref(),
args.public.as_ref(),
)?;
let requested_curve = inputs.curve.canonical_name().to_string();
let adapter = create_adapter(&requested_curve)?;
if !args.skip_local_verify && inputs.has_test_vectors() {
let ok = local_verify(adapter.as_ref(), &inputs)?;
if !ok {
return Err(Error::LocalProofVerificationFailed(
"local arkworks verification returned false".to_string(),
));
}
}
let snippet = proof_data_snippet(adapter.as_ref(), &inputs)?;
println!("{}", snippet.render_aptos_test_functions());
Ok(())
}
fn run_generate(args: GenerateArgs) -> Result<()> {
let GenerateArgs {
vk,
proof,
public,
out,
package_name,
module_name,
account_address,
mode,
run_aptos_test: should_run_aptos_test,
force,
skip_local_verify,
prepared,
bundle,
} = args;
let out =
out.ok_or_else(|| Error::MissingInput("--out is required for generation".to_string()))?;
let package_name = match package_name {
Some(package_name) => package_name,
None => default_package_name(&out)?,
};
let module_name = module_name.unwrap_or_else(|| "verifier".to_string());
validate_names(&package_name, "package_name")?;
validate_names(&module_name, "module_name")?;
validate_account_address(&account_address)?;
let inputs = load_inputs(
bundle.as_ref(),
vk.as_ref(),
proof.as_ref(),
public.as_ref(),
)?;
let requested_curve = inputs.curve.canonical_name().to_string();
if prepared {
return Err(Error::PreparedNotImplemented);
}
let adapter = create_adapter(&requested_curve)?;
if !skip_local_verify && inputs.has_test_vectors() {
let ok = local_verify(adapter.as_ref(), &inputs)?;
if !ok {
return Err(Error::LocalProofVerificationFailed(
"local arkworks verification returned false".to_string(),
));
}
}
generate_move_package(
&out,
adapter.as_ref(),
&inputs,
&GenerateMovePackageOptions {
package_name: &package_name,
module_name: &module_name,
account_address: &account_address,
mode: mode.into_move_mode(),
force,
},
)?;
if should_run_aptos_test {
run_aptos_test(&out)?;
}
Ok(())
}
fn load_inputs(
bundle: Option<&PathBuf>,
vk: Option<&PathBuf>,
proof: Option<&PathBuf>,
public: Option<&PathBuf>,
) -> Result<export_aptos_verifier_core::model::Groth16VerifierInputs> {
match (bundle, vk) {
(Some(bundle), None) => load_compact_bundle(bundle, None),
(None, Some(vk)) => load_auto_vk_inputs(
vk,
proof.map(PathBuf::as_path),
public.map(PathBuf::as_path),
),
(Some(_), Some(_)) => Err(Error::MissingInput(
"use either --bundle or --vk, not both".to_string(),
)),
(None, None) => Err(Error::MissingInput(
"--vk is required unless --bundle is used".to_string(),
)),
}
}
fn load_auto_vk_inputs(
vk: &Path,
proof: Option<&Path>,
public: Option<&Path>,
) -> Result<export_aptos_verifier_core::model::Groth16VerifierInputs> {
match load_snarkjs_json_inputs_with_optional_proof(vk, proof, public, None) {
Ok(inputs) => Ok(inputs),
Err(snarkjs_err) => match load_arkworks_inputs(vk, proof, public, None) {
Ok(inputs) => Ok(inputs),
Err(arkworks_err) => Err(Error::MissingInput(format!(
"could not auto-detect artifact type: snarkjs failed with {snarkjs_err}; arkworks failed with {arkworks_err}"
))),
},
}
}
impl fmt::Display for ModeArg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Library => "library",
Self::Entry => "entry",
Self::Test => "test",
};
write!(f, "{s}")
}
}
fn default_package_name(out: &Path) -> Result<String> {
let raw = out
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
Error::InvalidPackageName("--out must end with a package directory".to_string())
})?;
let mut name = String::with_capacity(raw.len());
for ch in raw.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
name.push(ch);
} else {
name.push('_');
}
}
if name.is_empty() {
return Err(Error::InvalidPackageName(
"--out must end with a non-empty package directory".to_string(),
));
}
if name.as_bytes()[0].is_ascii_digit() {
name.insert(0, '_');
}
Ok(name)
}
fn validate_names(value: &str, field: &str) -> Result<()> {
let re = Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap();
if !re.is_match(value) {
if field == "module_name" {
return Err(Error::InvalidModuleName(format!(
"{field} must match [A-Za-z_][A-Za-z0-9_]*"
)));
}
return Err(Error::InvalidPackageName(format!(
"{field} must match [A-Za-z_][A-Za-z0-9_]*"
)));
}
Ok(())
}
fn validate_account_address(value: &str) -> Result<()> {
let re = Regex::new(r"^0[xX][0-9a-fA-F]{1,64}$").unwrap();
if !re.is_match(value) {
return Err(Error::InvalidAccountAddress(
"account_address must match 0x[0-9a-fA-F]{1,64}".to_string(),
));
}
Ok(())
}
fn run_aptos_test(out_dir: &std::path::Path) -> Result<()> {
let aptos = ProcessCommand::new("aptos")
.arg("move")
.arg("test")
.arg("--package-dir")
.arg(out_dir)
.output();
match aptos {
Ok(out) => {
if !out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(Error::AptosTestFailed(format!(
"ERR_APTOS_TEST_FAILED: {}\nstdout:\n{}\nstderr:\n{}",
out.status, stdout, stderr
)));
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::AptosTestFailed(
"ERR_APTOS_CLI_NOT_FOUND: install Aptos CLI or run without --run-aptos-test"
.to_string(),
));
}
Err(err) => {
return Err(Error::AptosTestFailed(err.to_string()));
}
}
Ok(())
}