use crate::error::{CommonError, Result};
use crate::paths::ZiskPaths;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum HashMode {
Poseidon1,
Poseidon2,
#[default]
Blake3,
}
impl HashMode {
pub const ALL: [HashMode; 3] = [HashMode::Poseidon1, HashMode::Poseidon2, HashMode::Blake3];
pub fn merkle_tree_arity(&self) -> u64 {
match self {
HashMode::Poseidon1 => 4,
HashMode::Poseidon2 => 4,
HashMode::Blake3 => 2,
}
}
pub fn blowup_factor(&self) -> u64 {
match self {
HashMode::Poseidon1 => 2,
HashMode::Poseidon2 => 2,
HashMode::Blake3 => 2,
}
}
pub fn supports_snark(&self) -> bool {
!matches!(self, HashMode::Blake3)
}
pub fn file_tag(&self) -> &'static str {
match self {
HashMode::Poseidon1 => "poseidon1",
HashMode::Poseidon2 => "poseidon2",
HashMode::Blake3 => "blake3",
}
}
pub fn as_str(&self) -> &'static str {
match self {
HashMode::Poseidon1 => "Poseidon1",
HashMode::Poseidon2 => "Poseidon2",
HashMode::Blake3 => "blake3",
}
}
pub fn from_proving_key(proving_key: &Path) -> Result<Self> {
let path = proving_key.join("pilout.globalInfo.json");
Self::from_proving_key_opt(proving_key)?
.ok_or_else(|| CommonError::Io(format!("failed to read {}: not found", path.display())))
}
pub fn from_proving_key_opt(proving_key: &Path) -> Result<Option<Self>> {
let path = proving_key.join("pilout.globalInfo.json");
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => {
return Err(CommonError::Io(format!("failed to read {}: {e}", path.display())))
}
};
let global_info: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
CommonError::Deserialization(format!("failed to parse {}: {e}", path.display()))
})?;
global_info
.get("hash")
.and_then(|v| v.as_str())
.ok_or_else(|| CommonError::Invalid(format!("no 'hash' in {}", path.display())))?
.parse()
.map(Some)
}
pub fn local() -> Result<Self> {
Self::from_proving_key(&ZiskPaths::global().proving_key)
}
pub fn local_opt() -> Result<Option<Self>> {
Self::from_proving_key_opt(&ZiskPaths::global().proving_key)
}
}
impl std::str::FromStr for HashMode {
type Err = CommonError;
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"poseidon1" => Ok(HashMode::Poseidon1),
"poseidon2" => Ok(HashMode::Poseidon2),
"blake3" => Ok(HashMode::Blake3),
other => Err(CommonError::Invalid(format!("unrecognized HashMode: {other:?}"))),
}
}
}
#[cfg(test)]
mod tests {
use super::HashMode;
use std::path::Path;
use std::str::FromStr;
#[test]
fn hash_mode_from_str_roundtrip() {
assert_eq!(HashMode::from_str("Poseidon1").unwrap(), HashMode::Poseidon1);
assert_eq!(HashMode::from_str("Poseidon2").unwrap(), HashMode::Poseidon2);
}
#[test]
fn hash_mode_from_str_case_insensitive() {
assert_eq!(HashMode::from_str("poseidon1").unwrap(), HashMode::Poseidon1);
assert_eq!(HashMode::from_str("POSEIDON2").unwrap(), HashMode::Poseidon2);
}
#[test]
fn hash_mode_from_str_rejects_garbage() {
assert!(HashMode::from_str("poseidon3").is_err());
assert!(HashMode::from_str("").is_err());
}
fn with_proving_key<T>(contents: Option<&str>, f: impl FnOnce(&Path) -> T) -> T {
let dir = std::env::temp_dir().join(format!(
"zisk-hashmode-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).unwrap();
if let Some(c) = contents {
std::fs::write(dir.join("pilout.globalInfo.json"), c).unwrap();
}
let out = f(&dir);
let _ = std::fs::remove_dir_all(&dir);
out
}
#[test]
fn from_proving_key_opt_separates_absent_from_unusable() {
with_proving_key(None, |d| assert_eq!(HashMode::from_proving_key_opt(d).unwrap(), None));
with_proving_key(Some(r#"{"hash":"blake3"}"#), |d| {
assert_eq!(HashMode::from_proving_key_opt(d).unwrap(), Some(HashMode::Blake3))
});
for unusable in [r#"{"name":"zisk"}"#, r#"{"hash":"poseidon3"}"#, "not json"] {
with_proving_key(Some(unusable), |d| {
assert!(
HashMode::from_proving_key_opt(d).is_err(),
"{unusable:?} must not read as an absent key"
)
});
}
}
#[test]
fn from_proving_key_reads_the_hash_field() {
with_proving_key(Some(r#"{"name":"zisk","hash":"blake3"}"#), |d| {
assert_eq!(HashMode::from_proving_key(d).unwrap(), HashMode::Blake3);
});
}
#[test]
fn from_proving_key_never_falls_back_to_the_default() {
with_proving_key(None, |d| assert!(HashMode::from_proving_key(d).is_err()));
with_proving_key(Some(r#"{"name":"zisk"}"#), |d| {
assert!(HashMode::from_proving_key(d).is_err())
});
with_proving_key(Some(r#"{"hash":"poseidon3"}"#), |d| {
assert!(HashMode::from_proving_key(d).is_err())
});
with_proving_key(Some("not json"), |d| assert!(HashMode::from_proving_key(d).is_err()));
}
#[test]
fn only_the_poseidon_modes_support_a_snark() {
assert!(!HashMode::Blake3.supports_snark());
assert!(HashMode::Poseidon1.supports_snark());
assert!(HashMode::Poseidon2.supports_snark());
}
#[test]
fn the_default_mode_is_proofmans_default_family() {
assert_eq!(HashMode::default().as_str(), proofman_common::hash_family::DEFAULT_HASH_ID);
}
#[test]
fn every_mode_round_trips_through_its_wire_tag() {
for m in HashMode::ALL {
let tag = zisk_verifier::hash_tag(m.as_str())
.unwrap_or_else(|| panic!("{} has no wire tag", m.as_str()));
assert_eq!(zisk_verifier::hash_id_from_tag(tag), Some(m.as_str()));
}
}
#[test]
fn the_modes_match_proofmans_family_list() {
let mut mine: Vec<&str> = HashMode::ALL.iter().map(|m| m.as_str()).collect();
let mut theirs: Vec<&str> = proofman_common::hash_family::FAMILIES.to_vec();
mine.sort_unstable();
theirs.sort_unstable();
assert_eq!(mine, theirs);
}
#[test]
fn hash_mode_as_str_roundtrips_through_from_str() {
for m in HashMode::ALL {
assert_eq!(HashMode::from_str(m.as_str()).unwrap(), m);
}
}
}