export-sui-verifier-core 0.3.0

Load Groth16 artifacts from snarkjs JSON or Arkworks bundles and generate Sui Move verifier packages.
Documentation
mod context;
mod render;

use crate::bytes::move_hex_literal;
use crate::curves::CurveAdapter;
use crate::error::{Error, Result};
use crate::model::Groth16VerifierInputs;
pub use context::{MovegenMode, MovegenTemplateInput};
use handlebars::Handlebars;
use std::env;
use std::fs::{self, create_dir_all, write};
use std::path::{Component, Path};

#[derive(Debug, Clone)]
pub struct GenerateMovePackageOptions<'a> {
    pub package_name: &'a str,
    pub module_name: &'a str,
    pub mode: MovegenMode,
    pub force: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProofDataSnippet {
    pub proof_bytes: String,
    pub public_inputs_bytes: String,
}

impl ProofDataSnippet {
    pub fn render_sui_test_functions(&self) -> String {
        format!(
            r#"fun proof_bytes(): vector<u8> {{
    {}
}}

fun public_inputs_bytes(): vector<u8> {{
    {}
}}"#,
            self.proof_bytes, self.public_inputs_bytes
        )
    }
}

pub fn proof_data_snippet(
    adapter: &dyn CurveAdapter,
    inputs: &Groth16VerifierInputs,
) -> Result<ProofDataSnippet> {
    let proof = inputs.proof.as_ref().ok_or_else(|| {
        Error::MissingInput("proof-data requires proof input; VK-only inputs have no proof".into())
    })?;

    Ok(ProofDataSnippet {
        proof_bytes: move_hex_literal(&adapter.serialize_proof(proof)?),
        public_inputs_bytes: move_hex_literal(&serialize_public_inputs(adapter, inputs)?),
    })
}

pub fn generate_move_package(
    out_dir: &Path,
    adapter: &dyn CurveAdapter,
    inputs: &Groth16VerifierInputs,
    options: &GenerateMovePackageOptions<'_>,
) -> Result<()> {
    if options.force {
        validate_safe_force_output_dir(out_dir)?;
    }

    if out_dir.exists() && !options.force {
        return Err(Error::OutputExists(out_dir.to_path_buf()));
    }

    if out_dir.exists() {
        fs::remove_dir_all(out_dir).map_err(|e| Error::Io {
            source: e,
            context: format!("failed to clear existing output dir {}", out_dir.display()),
        })?;
    }

    create_dir_all(out_dir).map_err(|e| Error::Io {
        source: e,
        context: format!("create output dir {}", out_dir.display()),
    })?;

    create_dir_all(out_dir.join("sources")).map_err(|e| Error::Io {
        source: e,
        context: format!("create sources dir {}", out_dir.join("sources").display()),
    })?;
    let mut reg = Handlebars::new();
    register_templates(&mut reg)?;

    let verifying_key_bytes =
        move_hex_literal(&adapter.serialize_verifying_key(&inputs.verifying_key)?);
    let proof_bytes = match inputs.proof.as_ref() {
        Some(proof) => move_hex_literal(&adapter.serialize_proof(proof)?),
        None => "x\"\"".to_string(),
    };
    let public_inputs_bytes = move_hex_literal(&serialize_public_inputs(adapter, inputs)?);

    let input = MovegenTemplateInput {
        package_name: options.package_name.to_string(),
        module_name: options.module_name.to_string(),
        curve_function: adapter.sui_curve_function().to_string(),
        verifying_key_bytes,
        proof_bytes,
        public_inputs_bytes,
        include_test_vectors: inputs.has_test_vectors(),
        include_entry: options.mode.include_entry(),
    };

    let move_toml = reg
        .render("move_toml", &input)
        .map_err(|e| Error::TemplateRender(e.to_string()))?;
    fs::write(out_dir.join("Move.toml"), move_toml).map_err(|e| Error::Io {
        source: e,
        context: "write Move.toml".to_string(),
    })?;

    let verifier_source = reg
        .render("verifier", &input)
        .map_err(|e| Error::TemplateRender(e.to_string()))?;
    fs::write(
        out_dir.join("sources").join("verifier.move"),
        verifier_source,
    )
    .map_err(|e| Error::Io {
        source: e,
        context: "write verifier.move".to_string(),
    })?;

    if input.include_test_vectors {
        create_dir_all(out_dir.join("tests")).map_err(|e| Error::Io {
            source: e,
            context: format!("create tests dir {}", out_dir.join("tests").display()),
        })?;
        let tests = reg
            .render("move_tests", &input)
            .map_err(|e| Error::TemplateRender(e.to_string()))?;
        fs::write(out_dir.join("tests").join("verifier_tests.move"), tests).map_err(|e| {
            Error::Io {
                source: e,
                context: "write verifier_tests.move".to_string(),
            }
        })?;
    }

    write(
        out_dir.join("README.md"),
        render::readme_content(
            options.package_name,
            options.module_name,
            &input.curve_function,
            input.include_test_vectors,
            input.include_entry,
        ),
    )
    .map_err(|e| Error::Io {
        source: e,
        context: "write README.md".to_string(),
    })?;

    Ok(())
}

fn serialize_public_inputs(
    adapter: &dyn CurveAdapter,
    inputs: &Groth16VerifierInputs,
) -> Result<Vec<u8>> {
    let mut out = Vec::with_capacity(inputs.public_inputs.len() * 32);
    for value in &inputs.public_inputs {
        out.extend_from_slice(&adapter.serialize_fr_public_input(value)?);
    }
    Ok(out)
}

fn validate_safe_force_output_dir(out_dir: &Path) -> Result<()> {
    if out_dir
        .components()
        .any(|component| matches!(component, Component::ParentDir))
    {
        return Err(Error::UnsafeOutputDirectory(out_dir.to_path_buf()));
    }

    if !out_dir.exists() {
        return Ok(());
    }

    let target = out_dir.canonicalize().map_err(|e| Error::Io {
        source: e,
        context: format!("canonicalize output dir {}", out_dir.display()),
    })?;
    if target.parent().is_none() {
        return Err(Error::UnsafeOutputDirectory(target));
    }

    let cwd = env::current_dir().map_err(|e| Error::Io {
        source: e,
        context: "get current working directory".to_string(),
    })?;
    let cwd = cwd.canonicalize().map_err(|e| Error::Io {
        source: e,
        context: format!("canonicalize current working directory {}", cwd.display()),
    })?;
    if target == cwd || cwd.starts_with(&target) {
        return Err(Error::UnsafeOutputDirectory(target));
    }

    Ok(())
}

fn register_templates(handlebars: &mut Handlebars) -> Result<()> {
    let move_toml = include_str!("../../templates/Move.toml.hbs");
    let verifier = include_str!("../../templates/verifier.move.hbs");
    let tests = include_str!("../../templates/tests.move.hbs");

    handlebars
        .register_template_string("move_toml", move_toml)
        .map_err(|e| Error::TemplateRender(e.to_string()))?;
    handlebars
        .register_template_string("verifier", verifier)
        .map_err(|e| Error::TemplateRender(e.to_string()))?;
    handlebars
        .register_template_string("move_tests", tests)
        .map_err(|e| Error::TemplateRender(e.to_string()))?;
    Ok(())
}