use proofman_starks_lib_c::GOLDILOCKS_MERKLE_TREE_ARITY;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
const MERKLE_TREE_ARITY: usize = GOLDILOCKS_MERKLE_TREE_ARITY as usize;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct StarkSettings {
#[serde(default)]
pub verification_hash_type: Option<String>,
#[serde(default)]
pub hash_commits: Option<bool>,
#[serde(default)]
pub blowup_factor: Option<usize>,
#[serde(default)]
pub folding_factor: Option<usize>,
#[serde(default)]
pub final_degree: Option<usize>,
#[serde(default)]
pub merkle_tree_arity: Option<usize>,
#[serde(default)]
pub merkle_tree_custom: Option<bool>,
#[serde(default)]
pub last_level_verification: Option<usize>,
#[serde(default)]
pub pow_bits: Option<usize>,
#[serde(default)]
pub has_compressor: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum ConfigEntry {
Nested(IndexMap<String, StarkSettings>),
Flat(StarkSettings),
}
#[derive(Debug, Clone, Default)]
pub struct StarkStructsConfig {
entries: IndexMap<String, ConfigEntry>,
}
impl StarkStructsConfig {
pub fn from_json_str(data: &str) -> serde_json::Result<Self> {
let entries: IndexMap<String, ConfigEntry> = serde_json::from_str(data)?;
Ok(Self { entries })
}
pub fn resolve(&self, airgroup_name: &str, air_name: &str) -> StarkSettings {
let mut s = self
.lookup_nested(airgroup_name, air_name)
.or_else(|| self.lookup_flat(air_name))
.or_else(|| self.lookup_flat("default"))
.unwrap_or_default();
if s.pow_bits.is_none() {
s.pow_bits = Some(16);
}
s
}
fn lookup_nested(&self, airgroup_name: &str, air_name: &str) -> Option<StarkSettings> {
match self.entries.get(airgroup_name) {
Some(ConfigEntry::Nested(airs)) => airs.get(air_name).cloned(),
_ => None,
}
}
fn lookup_flat(&self, air_name: &str) -> Option<StarkSettings> {
match self.entries.get(air_name) {
Some(ConfigEntry::Flat(s)) => Some(s.clone()),
_ => None,
}
}
pub fn set_has_compressor(&mut self, air_name: &str) {
match self.entries.get_mut(air_name) {
Some(ConfigEntry::Flat(s)) => s.has_compressor = Some(true),
_ => {
self.entries.insert(
air_name.to_string(),
ConfigEntry::Flat(StarkSettings { has_compressor: Some(true), ..Default::default() }),
);
}
}
}
pub fn has_compressor(&self, airgroup_name: &str, air_name: &str) -> bool {
self.lookup_nested(airgroup_name, air_name)
.or_else(|| self.lookup_flat(air_name))
.and_then(|s| s.has_compressor)
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct StarkStruct {
pub n_bits: usize,
pub n_bits_ext: usize,
pub merkle_tree_arity: usize,
pub transcript_arity: usize,
pub merkle_tree_custom: bool,
pub hash_commits: bool,
pub verification_hash_type: String,
pub last_level_verification: usize,
pub pow_bits: usize,
pub steps: Vec<StarkStep>,
#[serde(default)]
pub n_queries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StarkStep {
pub n_bits: usize,
}
pub fn generate_stark_struct(settings: &StarkSettings, n_bits: usize) -> StarkStruct {
let verification_hash_type = settings.verification_hash_type.clone().unwrap_or_else(|| "GL".to_string());
if !["GL", "BN128"].contains(&verification_hash_type.as_str()) {
panic!("Invalid verificationHashType: {}", verification_hash_type);
}
let blowup_factor = settings.blowup_factor.unwrap_or(1);
let folding_factor = settings.folding_factor.unwrap_or(3);
let final_degree = settings.final_degree.unwrap_or(5);
let (merkle_tree_arity, transcript_arity, merkle_tree_custom, hash_commits, last_level_verification, pow_bits) =
if verification_hash_type == "BN128" {
let mta = settings.merkle_tree_arity.unwrap_or(16);
let mtc = settings.merkle_tree_custom.unwrap_or(false);
let pb = settings.pow_bits.unwrap_or(0);
let llv = settings.last_level_verification.unwrap_or(0);
(mta, mta, mtc, false, llv, pb)
} else {
let mta = settings.merkle_tree_arity.unwrap_or(MERKLE_TREE_ARITY);
let pb = settings.pow_bits.unwrap_or(20);
let llv = settings.last_level_verification.unwrap_or(2);
(mta, MERKLE_TREE_ARITY, true, true, llv, pb)
};
let n_bits_ext = n_bits + blowup_factor;
let mut steps = vec![StarkStep { n_bits: n_bits_ext }];
let mut fri_step_bits = n_bits_ext;
while fri_step_bits > final_degree + 1 {
fri_step_bits =
if fri_step_bits > folding_factor + final_degree { fri_step_bits - folding_factor } else { final_degree };
steps.push(StarkStep { n_bits: fri_step_bits });
}
StarkStruct {
n_bits,
n_bits_ext,
merkle_tree_arity,
transcript_arity,
merkle_tree_custom,
hash_commits,
verification_hash_type,
last_level_verification,
pow_bits,
steps,
n_queries: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_stark_struct_defaults() {
let settings = StarkSettings::default();
let ss = generate_stark_struct(&settings, 20);
assert_eq!(ss.n_bits, 20);
assert_eq!(ss.n_bits_ext, 21); assert_eq!(ss.verification_hash_type, "GL");
assert_eq!(ss.merkle_tree_arity, MERKLE_TREE_ARITY);
assert_eq!(ss.transcript_arity, MERKLE_TREE_ARITY);
assert!(ss.merkle_tree_custom);
assert!(ss.hash_commits);
assert_eq!(ss.pow_bits, 20);
assert_eq!(ss.last_level_verification, 2);
assert_eq!(ss.steps[0].n_bits, 21);
assert_eq!(ss.steps.last().unwrap().n_bits, 6);
}
#[test]
fn test_generate_stark_struct_bn128() {
let settings = StarkSettings {
verification_hash_type: Some("BN128".to_string()),
blowup_factor: Some(2),
folding_factor: Some(4),
final_degree: Some(3),
..Default::default()
};
let ss = generate_stark_struct(&settings, 16);
assert_eq!(ss.n_bits, 16);
assert_eq!(ss.n_bits_ext, 18);
assert_eq!(ss.verification_hash_type, "BN128");
assert_eq!(ss.merkle_tree_arity, 16);
assert_eq!(ss.transcript_arity, 16);
assert!(!ss.merkle_tree_custom);
assert!(!ss.hash_commits);
assert_eq!(ss.pow_bits, 0);
assert_eq!(ss.last_level_verification, 0);
assert_eq!(ss.steps[0].n_bits, 18);
}
#[test]
fn test_steps_converge_to_final_degree() {
let settings = StarkSettings {
blowup_factor: Some(2),
folding_factor: Some(3),
final_degree: Some(5),
..Default::default()
};
let ss = generate_stark_struct(&settings, 20);
assert_eq!(ss.steps[0].n_bits, 22);
let last_step = ss.steps.last().unwrap().n_bits;
assert!(
last_step <= settings.final_degree.unwrap() + 1,
"Last step {} should be <= finalDegree + 1 = {}",
last_step,
settings.final_degree.unwrap() + 1
);
}
#[test]
#[should_panic(expected = "Invalid verificationHashType")]
fn test_invalid_hash_type() {
let settings = StarkSettings { verification_hash_type: Some("INVALID".to_string()), ..Default::default() };
generate_stark_struct(&settings, 10);
}
#[test]
fn test_flat_config_resolution() {
let json_str = r#"{
"Keccakf": { "powBits": 23, "lastLevelVerification": 1, "hasCompressor": true },
"Sha256f": { "hasCompressor": true },
"SomeAir": { "blowupFactor": 2 }
}"#;
let cfg = StarkStructsConfig::from_json_str(json_str).unwrap();
let keccak = cfg.resolve("AnyGroup", "Keccakf");
assert_eq!(keccak.pow_bits, Some(23));
assert_eq!(keccak.last_level_verification, Some(1));
assert_eq!(keccak.has_compressor, Some(true));
let some = cfg.resolve("AnyGroup", "SomeAir");
assert_eq!(some.blowup_factor, Some(2));
assert!(cfg.has_compressor("AnyGroup", "Keccakf"));
assert!(cfg.has_compressor("AnyGroup", "Sha256f"));
assert!(!cfg.has_compressor("AnyGroup", "SomeAir"));
}
#[test]
fn test_nested_config_resolution() {
let json_str = r#"{
"Zisk": {
"Poseidon2": { "blowupFactor": 2 },
"Keccakf": { "powBits": 23, "hasCompressor": true }
}
}"#;
let cfg = StarkStructsConfig::from_json_str(json_str).unwrap();
let pos = cfg.resolve("Zisk", "Poseidon2");
assert_eq!(pos.blowup_factor, Some(2));
assert_eq!(generate_stark_struct(&pos, 20).n_bits_ext, 22);
let kec = cfg.resolve("Zisk", "Keccakf");
assert_eq!(kec.pow_bits, Some(23));
assert!(cfg.has_compressor("Zisk", "Keccakf"));
let miss = cfg.resolve("OtherGroup", "Poseidon2");
assert_eq!(miss.blowup_factor, None);
assert_eq!(miss.pow_bits, Some(16));
assert_eq!(generate_stark_struct(&miss, 20).n_bits_ext, 21); }
#[test]
fn test_default_key_fallback() {
let cfg = StarkStructsConfig::from_json_str(r#"{ "default": { "blowupFactor": 3 } }"#).unwrap();
assert_eq!(cfg.resolve("G", "Anything").blowup_factor, Some(3));
}
#[test]
fn test_committed_example_configs_are_nested() {
let prods =
concat!(env!("CARGO_MANIFEST_DIR"), "/../../pil2-components/test/special/intermediate_prods.config.json");
if std::path::Path::new(prods).exists() {
let data = std::fs::read_to_string(prods).unwrap();
let cfg = StarkStructsConfig::from_json_str(&data).unwrap();
assert_eq!(cfg.resolve("Intermediates", "ImDummyAP_24_5").blowup_factor, Some(2));
assert_eq!(cfg.resolve("Intermediates", "ImDummyAP_24_9").blowup_factor, Some(3));
assert_eq!(cfg.resolve("WrongGroup", "ImDummyAP_24_5").blowup_factor, None);
}
}
#[test]
fn test_empty_object_is_harmless() {
let cfg = StarkStructsConfig::from_json_str(r#"{ "EmptyAir": {} }"#).unwrap();
let s = cfg.resolve("G", "EmptyAir");
assert_eq!(s.blowup_factor, None);
assert_eq!(s.pow_bits, Some(16)); assert!(!cfg.has_compressor("G", "EmptyAir"));
}
#[test]
fn test_set_has_compressor_runtime() {
let mut cfg = StarkStructsConfig::from_json_str(r#"{ "Foo": { "blowupFactor": 2 } }"#).unwrap();
cfg.set_has_compressor("Foo");
assert!(cfg.has_compressor("G", "Foo"));
assert_eq!(cfg.resolve("G", "Foo").blowup_factor, Some(2));
cfg.set_has_compressor("Bar"); assert!(cfg.has_compressor("G", "Bar"));
}
}