use proofman_starks_lib_c::GOLDILOCKS_POSEIDON_MERKLE_TREE_ARITY;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GateRole {
PoseidonSponge,
PoseidonCompression,
TreeSelector,
CMul,
Fft4,
EvPol4,
SelectValArity4,
SelectValArity2,
Blake3Node,
Blake3Compress,
}
pub const FAMILIES: &[&str] = &["Poseidon1", "Poseidon2", "blake3"];
pub const DEFAULT_HASH_ID: &str = "blake3";
pub fn merkle_tree_arity(family: &str) -> u64 {
match family {
"Poseidon1" | "Poseidon2" => GOLDILOCKS_POSEIDON_MERKLE_TREE_ARITY,
"blake3" => 2,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn recursive_bits_threshold(family: &str) -> usize {
match family {
"blake3" => 19,
"Poseidon1" | "Poseidon2" => 17,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn uses_optimal_fri_schedule(family: &str) -> bool {
merkle_tree_arity(family) == 2
}
pub fn compression_block_elements(family: &str) -> usize {
match family {
"blake3" => 8,
"Poseidon1" | "Poseidon2" => 8,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn fri_terminal_degree(family: &str) -> usize {
match family {
"blake3" => 7,
"Poseidon1" | "Poseidon2" => 5,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn recursive_last_level_verification(family: &str) -> Option<usize> {
match family {
"blake3" => Some(5),
"Poseidon1" | "Poseidon2" => None,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn final_grinding_bits(family: &str) -> usize {
match family {
"blake3" => 24,
"Poseidon1" | "Poseidon2" => 22,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn final_n_bits(family: &str) -> Option<usize> {
match family {
"blake3" => Some(19),
"Poseidon1" | "Poseidon2" => None,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn final_blowup_factor(family: &str) -> usize {
match family {
"blake3" => 2,
"Poseidon1" | "Poseidon2" => 4,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn max_constraint_degree_for_blowup(blowup: usize) -> usize {
const CAP: usize = 8;
(2usize.pow(blowup as u32) + 1).min(CAP)
}
pub fn compressed_final_by_default(family: &str) -> bool {
family != "blake3"
}
pub fn default_aggregation_arity(family: &str) -> usize {
match family {
"blake3" => 2,
"Poseidon1" | "Poseidon2" => 3,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn default_grinding_bits(family: &str) -> usize {
match family {
"Poseidon1" | "Poseidon2" => 16,
"blake3" => 24,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn recursive_grinding_bits(family: &str) -> usize {
match family {
"Poseidon1" | "Poseidon2" => 21,
"blake3" => 24,
fam => panic!("Unknown hash family: {fam}"),
}
}
pub fn supports_snark(family: &str) -> bool {
matches!(family, "Poseidon1" | "Poseidon2")
}
pub fn transcript_arity(family: &str) -> u64 {
merkle_tree_arity(family)
}
pub const DIGEST_SIZE: u64 = 4;
pub fn sponge_width(arity: u64) -> u64 {
arity * DIGEST_SIZE
}
pub fn sponge_rate(arity: u64) -> u64 {
sponge_width(arity) - DIGEST_SIZE
}
pub fn transcript_pending_size(arity: u64) -> u64 {
DIGEST_SIZE * (arity - 1)
}
pub fn transcript_out_size(arity: u64) -> u64 {
DIGEST_SIZE * arity
}
pub fn supports_stream_commit(family: &str) -> bool {
matches!(family, "Poseidon1" | "blake3")
}
pub fn has_forced_tree_geometry(family: &str) -> bool {
family == "blake3"
}
const GATES: &[(&str, GateRole, Option<&str>)] = &[
("Poseidon1_16", GateRole::PoseidonSponge, Some("Poseidon1")),
("CustPoseidon1_16", GateRole::PoseidonCompression, Some("Poseidon1")),
("TreeSelector8", GateRole::TreeSelector, None),
("Poseidon2_16", GateRole::PoseidonSponge, Some("Poseidon2")),
("CustPoseidon2_16", GateRole::PoseidonCompression, Some("Poseidon2")),
("TreeSelector4", GateRole::TreeSelector, None),
("CMul", GateRole::CMul, None),
("FFT4", GateRole::Fft4, None),
("EvPol4", GateRole::EvPol4, None),
("SelectValueArity4", GateRole::SelectValArity4, None),
("SelectValueArity2", GateRole::SelectValArity2, None),
("Blake3Node", GateRole::Blake3Node, Some("blake3")),
("Blake3Compress", GateRole::Blake3Compress, Some("blake3")),
];
pub fn lookup_gate(name: &str) -> Option<(GateRole, Option<&'static str>)> {
GATES.iter().find(|(n, _, _)| *n == name).map(|(_, r, f)| (*r, *f))
}
pub fn is_known_family(id: &str) -> bool {
FAMILIES.contains(&id)
}
pub fn supports_native_rust_verifier(family: &str) -> bool {
is_known_family(family)
}
pub fn rust_hash_type(family: &str, arity: u64) -> &'static str {
match (family, arity * 4) {
("Poseidon1", 8) => "Poseidon1_8",
("Poseidon1", 12) => "Poseidon1_12",
("Poseidon1", 16) => "Poseidon1_16",
("Poseidon2", 4) => "Poseidon2_4",
("Poseidon2", 8) => "Poseidon2_8",
("Poseidon2", 12) => "Poseidon2_12",
("Poseidon2", 16) => "Poseidon2_16",
("blake3", 8) => "Blake3_8",
(fam, width) => panic!("Unsupported hash type: {fam}_{width} (arity {arity})"),
}
}
pub fn rust_transcript_type(family: &str, arity: u64) -> String {
match family {
"blake3" => "Blake3Transcript<Goldilocks>".to_string(),
_ => format!("Transcript<Goldilocks, {}>", rust_hash_type(family, arity)),
}
}
pub fn rust_transcript_imports(family: &str, arity: u64) -> Vec<&'static str> {
match family {
"blake3" => vec!["Blake3Transcript"],
_ => vec!["Transcript", rust_hash_type(family, arity)],
}
}
pub fn rust_grinding_type(family: &str) -> &'static str {
match family {
"Poseidon1" => "Poseidon1_8",
"Poseidon2" => "Poseidon2_8",
"blake3" => "Blake3_8",
fam => panic!("Unsupported grinding hash family: {fam}"),
}
}
#[cfg(test)]
mod tests {
#[test]
fn recursive_grinding_bits_exceed_the_basic_defaults() {
assert_eq!(super::recursive_grinding_bits("blake3"), 24);
assert_eq!(super::recursive_grinding_bits("Poseidon1"), 21);
assert_eq!(super::recursive_grinding_bits("Poseidon2"), 21);
for f in super::FAMILIES {
assert!(
super::recursive_grinding_bits(f) >= super::default_grinding_bits(f),
"{f} would grind less in recursion than in its basic airs"
);
}
}
#[test]
fn the_final_air_is_pinned_only_where_a_committed_verifier_encodes_it() {
assert_eq!(super::final_n_bits("blake3"), Some(19));
for f in ["Poseidon1", "Poseidon2"] {
assert_eq!(super::final_n_bits(f), None, "{f}");
}
}
#[test]
fn final_grinding_bits_are_pinned_per_family() {
assert_eq!(super::final_grinding_bits("blake3"), 24);
assert_eq!(super::final_grinding_bits("Poseidon1"), 22);
assert_eq!(super::final_grinding_bits("Poseidon2"), 22);
for f in super::FAMILIES {
assert!(super::final_grinding_bits(f) >= super::default_grinding_bits(f), "{f}");
}
}
#[test]
fn final_blowup_is_per_family_and_carries_the_degree() {
assert_eq!(super::final_blowup_factor("blake3"), 2);
assert_eq!(super::final_blowup_factor("Poseidon2"), 4);
for f in super::FAMILIES {
let b = super::final_blowup_factor(f);
assert!(super::max_constraint_degree_for_blowup(b) >= 5, "{f} at blowup {b} cannot carry degree 5");
}
}
#[test]
fn max_constraint_degree_follows_the_blowup() {
assert_eq!(super::max_constraint_degree_for_blowup(1), 3);
assert_eq!(super::max_constraint_degree_for_blowup(2), 5);
assert_eq!(super::max_constraint_degree_for_blowup(3), 8);
assert_eq!(super::max_constraint_degree_for_blowup(4), 8);
assert_eq!(super::max_constraint_degree_for_blowup(10), 8, "capped");
}
#[test]
fn compressed_final_is_off_only_for_blake3() {
assert!(!super::compressed_final_by_default("blake3"));
assert!(super::compressed_final_by_default("Poseidon1"));
assert!(super::compressed_final_by_default("Poseidon2"));
}
#[test]
fn recursive_bits_threshold_is_the_family_recursion_size() {
assert_eq!(super::recursive_bits_threshold("blake3"), 19);
assert_eq!(super::recursive_bits_threshold("Poseidon1"), 17);
assert_eq!(super::recursive_bits_threshold("Poseidon2"), 17);
}
#[test]
fn default_aggregation_arity_is_two_for_blake3_and_three_for_poseidon() {
assert_eq!(super::default_aggregation_arity("blake3"), 2);
assert_eq!(super::default_aggregation_arity("Poseidon1"), 3);
assert_eq!(super::default_aggregation_arity("Poseidon2"), 3);
for f in super::FAMILIES {
let a = super::default_aggregation_arity(f);
assert!(crate::global_info::is_valid_aggregation_arity(a), "{f} defaults to an invalid arity {a}");
}
}
use super::*;
#[test]
fn blake3_gates_are_registered_to_the_blake3_family() {
assert_eq!(lookup_gate("Blake3Node"), Some((GateRole::Blake3Node, Some("blake3"))));
assert_eq!(lookup_gate("Blake3Compress"), Some((GateRole::Blake3Compress, Some("blake3"))));
}
#[test]
fn a_families_gate_set_resolves_to_exactly_one_family() {
let plausible: &[(&str, &[&str])] = &[
(
"Poseidon1",
&["Poseidon1_16", "CustPoseidon1_16", "TreeSelector8", "CMul", "FFT4", "EvPol4", "SelectValueArity4"],
),
(
"Poseidon2",
&["Poseidon2_16", "CustPoseidon2_16", "TreeSelector4", "CMul", "FFT4", "EvPol4", "SelectValueArity4"],
),
(
"blake3",
&["Blake3Node", "Blake3Compress", "TreeSelector4", "CMul", "FFT4", "EvPol4", "SelectValueArity2"],
),
("blake3", &["Blake3Node", "TreeSelector8", "CMul"]),
("Poseidon2", &["Poseidon2_16", "TreeSelector8", "CMul"]),
];
for (expected, gates) in plausible {
let mut seen: Vec<&str> = Vec::new();
for name in *gates {
let (_, family) = lookup_gate(name).unwrap_or_else(|| panic!("unregistered gate {name}"));
if let Some(f) = family {
if !seen.contains(&f) {
seen.push(f);
}
}
}
assert_eq!(seen, vec![*expected], "gate set {gates:?} does not resolve to {expected} alone");
}
}
#[test]
fn every_family_has_at_least_one_gate() {
for fam in FAMILIES {
assert!(
GATES.iter().any(|(_, _, f)| *f == Some(*fam)),
"family {fam} is in FAMILIES but has no gate in GATES"
);
}
}
#[test]
fn discriminating_gate_names_are_unique_per_family() {
for (i, (name, role, fam)) in GATES.iter().enumerate() {
if fam.is_none() {
continue;
}
if !matches!(role, GateRole::PoseidonSponge | GateRole::PoseidonCompression | GateRole::TreeSelector) {
continue;
}
for (other_name, _, other_fam) in &GATES[i + 1..] {
if name == other_name {
panic!("gate {name:?} claimed by both {fam:?} and {other_fam:?}");
}
}
}
}
#[test]
fn rust_hash_type_tracks_arity_width() {
assert_eq!(rust_hash_type("Poseidon2", 4), "Poseidon2_16");
assert_eq!(rust_hash_type("Poseidon2", 2), "Poseidon2_8");
assert_eq!(rust_hash_type("Poseidon1", 4), "Poseidon1_16");
assert_eq!(rust_hash_type("Poseidon1", 3), "Poseidon1_12");
assert_eq!(rust_hash_type("Poseidon1", 2), "Poseidon1_8");
}
#[test]
fn every_family_has_grinding_bits_and_blake3_grinds_hardest() {
for family in FAMILIES {
assert!(default_grinding_bits(family) > 0, "{family} has no grinding default");
}
assert_eq!(default_grinding_bits("Poseidon1"), default_grinding_bits("Poseidon2"));
assert!(default_grinding_bits("blake3") > default_grinding_bits("Poseidon1"));
}
#[test]
fn the_sponge_width_fits_one_node_of_children() {
for arity in [2, 3, 4] {
assert_eq!(sponge_width(arity), arity * DIGEST_SIZE);
assert_eq!(sponge_rate(arity), sponge_width(arity) - DIGEST_SIZE);
}
assert_eq!((sponge_width(4), sponge_rate(4)), (16, 12));
}
#[test]
fn transcript_sizes_match_the_native_transcript() {
assert_eq!((transcript_pending_size(4), transcript_out_size(4)), (12, 16));
assert_eq!((transcript_pending_size(2), transcript_out_size(2)), (4, 8));
}
#[test]
fn only_the_poseidon_families_can_be_wrapped_in_a_snark() {
assert!(!super::supports_snark("blake3"));
assert!(super::supports_snark("Poseidon1"));
assert!(super::supports_snark("Poseidon2"));
assert!(!super::supports_snark("Keccak"));
}
#[test]
fn default_hash_id_is_registered() {
assert!(is_known_family(DEFAULT_HASH_ID));
}
}