use proofman_common::hash_family::{sponge_rate, transcript_out_size, transcript_pending_size, DIGEST_SIZE};
const BLAKE3_ABSORB_WORDS: u64 = 8;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HashCounts {
pub leaf: u64,
pub merkle: u64,
pub fri: u64,
pub transcript: u64,
pub grinding: u64,
}
impl HashCounts {
pub fn total(&self) -> u64 {
self.leaf + self.merkle + self.fri + self.transcript + self.grinding
}
}
#[derive(Debug, Clone, Default)]
pub struct VerifierGeometry {
pub n_bits_ext: u64,
pub arity: u64,
pub transcript_arity: u64,
pub last_level_verification: u64,
pub n_queries: u64,
pub pow_bits: u64,
pub hash_commits: bool,
pub stage_widths: Vec<u64>,
pub n_constants: u64,
pub custom_commit_widths: Vec<u64>,
pub step_n_bits: Vec<u64>,
pub n_publics: u64,
pub n_evals: u64,
pub stage_challenges: Vec<u64>,
pub stage_air_values: Vec<u64>,
pub final_pol_size: u64,
}
pub fn blake3_compressions(bytes: u64) -> u64 {
let blocks = bytes.div_ceil(64).max(1);
let chunks = bytes.div_ceil(1024).max(1);
blocks + (chunks - 1)
}
pub fn leaf_hashes(family: &str, arity: u64, width: u64) -> u64 {
match family {
"blake3" => blake3_compressions(width * 8),
_ if arity < 2 => 0,
_ => width.div_ceil(sponge_rate(arity)),
}
}
pub fn merkle_path_permutations(n_bits_height: u64, arity: u64, last_level_verification: u64) -> u64 {
if n_bits_height == 0 {
return 0;
}
let levels = (n_bits_height as f64 / (arity as f64).log2()).ceil() as u64;
levels.saturating_sub(last_level_verification)
}
pub fn root_reduction_hashes(arity: u64, last_level_verification: u64, n_bits_height: u64) -> u64 {
if arity < 2 || n_bits_height >= 64 {
return 0;
}
let Some(stop) = arity.checked_pow(last_level_verification.min(u32::MAX as u64) as u32) else {
return 0;
};
let mut pending = 1u64 << n_bits_height;
while pending > stop {
pending = pending.div_ceil(arity);
}
let mut hashes = 0;
while pending > 1 {
pending = pending.div_ceil(arity);
hashes += pending;
}
hashes
}
#[derive(Debug)]
pub struct TranscriptSim {
pending: u64,
out: u64,
pending_size: u64,
out_size: u64,
pub hashes: u64,
}
impl TranscriptSim {
pub fn new(family: &str, arity: u64) -> Self {
Self {
pending: 0,
out: 0,
pending_size: if family == "blake3" { BLAKE3_ABSORB_WORDS } else { transcript_pending_size(arity) },
out_size: transcript_out_size(arity),
hashes: 0,
}
}
fn update_state(&mut self) {
self.hashes += 1;
self.pending = 0;
self.out = self.out_size;
}
pub fn put(&mut self, n: u64) {
for _ in 0..n {
self.pending += 1;
self.out = 0;
if self.pending == self.pending_size {
self.update_state();
}
}
}
fn get_fields1(&mut self) {
if self.out == 0 {
self.update_state();
}
self.out -= 1;
}
pub fn get_field(&mut self) {
for _ in 0..3 {
self.get_fields1();
}
}
pub fn get_state(&mut self) {
if self.pending > 0 {
self.update_state();
}
}
pub fn get_permutations(&mut self, n: u64, n_bits: u64) {
let total_bits = n * n_bits;
let n_fields = if total_bits == 0 { 0 } else { (total_bits - 1) / 63 + 1 };
for _ in 0..n_fields {
self.get_fields1();
}
}
}
pub fn geometry_for_family(
stark_struct: &crate::types::stark_struct::StarkStruct,
setup: &crate::types::pilout_info::SetupResult,
n_evals: usize,
) -> VerifierGeometry {
use crate::types::security;
use crate::types::security::pcs::{Batching, Fri, FriConfig};
use crate::types::security::regimes::DecodingRegime;
let arity = stark_struct.merkle_tree_arity as u64;
let fri = Fri::new(FriConfig {
field_size: security::goldilocks_safe_extension_field_size(),
trace_length: 1u32 << stark_struct.n_bits,
rate: 1.0 / (1u64 << (stark_struct.n_bits_ext - stark_struct.n_bits)) as f64,
batch_size: n_evals.max(1) as u64,
batching: Batching::Powers,
log_folding_factors: crate::output::stark_info::compute_log_folding_factors(stark_struct),
max_grinding_bits_query: stark_struct.pow_bits as u64,
use_max_grinding_bits_query: true,
tree_arity: arity,
hash_size_bits: 256,
target_security_bits: 128,
regime: DecodingRegime::Jbr,
});
let security = fri.security_params();
let width = |section: &str| setup.map_sections_n.get(section).copied().unwrap_or(0) as u64;
VerifierGeometry {
n_bits_ext: stark_struct.n_bits_ext as u64,
arity,
transcript_arity: stark_struct.transcript_arity as u64,
last_level_verification: stark_struct.last_level_verification as u64,
n_queries: security.n_queries,
pow_bits: security.grinding_bits_query as u64,
hash_commits: stark_struct.hash_commits,
stage_widths: (1..=setup.n_stages + 1).map(|s| width(&format!("cm{s}"))).collect(),
n_constants: setup.n_constants as u64,
custom_commit_widths: setup.custom_commits.iter().map(|c| width(&format!("{}0", c.name))).collect(),
step_n_bits: stark_struct.steps.iter().map(|s| s.n_bits as u64).collect(),
n_publics: setup.n_publics as u64,
n_evals: n_evals as u64,
stage_challenges: (2..=setup.n_stages + 1)
.map(|s| setup.challenges_map.iter().filter(|c| c.stage == Some(s)).count() as u64)
.collect(),
stage_air_values: (2..=setup.n_stages + 1)
.map(|s| setup.air_values_map.iter().filter(|v| v.stage == Some(s)).count() as u64)
.collect(),
final_pol_size: 1u64 << stark_struct.steps.last().map_or(0, |s| s.n_bits),
}
}
pub fn verifier_hashes(geom: &VerifierGeometry, family: &str) -> HashCounts {
let mut counts = HashCounts::default();
let open = |width: u64, n_bits_height: u64, leaf: &mut u64, merkle: &mut u64| {
*leaf += geom.n_queries * leaf_hashes(family, geom.arity, width);
*merkle += geom.n_queries * merkle_path_permutations(n_bits_height, geom.arity, geom.last_level_verification);
*merkle += root_reduction_hashes(geom.arity, geom.last_level_verification, n_bits_height);
};
for &width in &geom.stage_widths {
open(width, geom.n_bits_ext, &mut counts.leaf, &mut counts.merkle);
}
open(geom.n_constants, geom.n_bits_ext, &mut counts.leaf, &mut counts.merkle);
for &width in &geom.custom_commit_widths {
open(width, geom.n_bits_ext, &mut counts.leaf, &mut counts.merkle);
}
for step in 1..geom.step_n_bits.len() {
let n_bits = geom.step_n_bits[step];
let group_size = 1u64 << geom.step_n_bits[step - 1].saturating_sub(n_bits).min(63);
let (mut leaf, mut merkle) = (0, 0);
open(group_size * FIELD_EXTENSION, n_bits, &mut leaf, &mut merkle);
counts.fri += leaf + merkle;
}
counts.transcript = transcript_hashes(geom, family);
counts.grinding = 1;
counts
}
const FIELD_EXTENSION: u64 = 3;
fn transcript_hashes(geom: &VerifierGeometry, family: &str) -> u64 {
let mut t = TranscriptSim::new(family, geom.transcript_arity);
let put_values = |t: &mut TranscriptSim, n: u64| {
if geom.hash_commits {
let mut inner = TranscriptSim::new(family, geom.transcript_arity);
inner.put(n);
inner.get_state();
t.hashes += inner.hashes;
t.put(DIGEST_SIZE);
} else {
t.put(n);
}
};
t.put(DIGEST_SIZE); if geom.n_publics > 0 {
put_values(&mut t, geom.n_publics);
}
t.put(DIGEST_SIZE);
for (i, &n_challenges) in geom.stage_challenges.iter().enumerate() {
for _ in 0..n_challenges {
t.get_field();
}
t.put(DIGEST_SIZE);
t.put(geom.stage_air_values.get(i).copied().unwrap_or(0) * FIELD_EXTENSION);
}
t.get_field(); put_values(&mut t, geom.n_evals * FIELD_EXTENSION);
t.get_field(); t.get_field();
for step in 0..geom.step_n_bits.len() {
if step > 0 {
t.get_field();
}
if step + 1 < geom.step_n_bits.len() {
t.put(DIGEST_SIZE);
} else {
put_values(&mut t, geom.final_pol_size * FIELD_EXTENSION);
}
}
t.get_field();
let mut queries = TranscriptSim::new(family, geom.transcript_arity);
queries.put(FIELD_EXTENSION);
queries.put(1);
queries.get_permutations(geom.n_queries, geom.step_n_bits.first().copied().unwrap_or(0));
t.hashes + queries.hashes
}
#[derive(Debug, Clone, Copy)]
pub struct Blake3RecursionFit {
pub blocks: usize,
pub capacity: usize,
pub needs_compressor: bool,
}
pub fn blake3_recursion_fit(counts: &HashCounts, lanes: usize) -> Blake3RecursionFit {
use pil2_stark_recurser::plonk2pil::setups::blake3::blake3_max_blocks;
let blocks = (counts.total() as usize).div_ceil(lanes.max(1));
let capacity = blake3_max_blocks(1 << proofman_common::hash_family::recursive_bits_threshold("blake3"));
Blake3RecursionFit { blocks, capacity, needs_compressor: blocks > capacity }
}
#[cfg(test)]
mod tests {
use super::*;
use proofman_common::hash_family::merkle_tree_arity;
#[test]
fn a_blake3_node_is_one_compression() {
assert_eq!(blake3_compressions(8 * DIGEST_SIZE * 2), 1);
}
#[test]
fn blake3_counts_blocks_plus_chunk_parents() {
assert_eq!(blake3_compressions(0), 1);
assert_eq!(blake3_compressions(1), 1);
assert_eq!(blake3_compressions(1024), 16);
assert_eq!(blake3_compressions(1025), 18); }
#[test]
fn a_poseidon_leaf_costs_one_permutation_per_rate() {
assert_eq!(sponge_rate(4), 12);
assert_eq!(leaf_hashes("Poseidon1", 4, 0), 0);
assert_eq!(leaf_hashes("Poseidon1", 4, 1), 1);
assert_eq!(leaf_hashes("Poseidon1", 4, 12), 1);
assert_eq!(leaf_hashes("Poseidon1", 4, 13), 2);
assert_eq!(leaf_hashes("Poseidon2", 4, 100), 9);
}
#[test]
fn a_blake3_leaf_hashes_the_whole_row() {
assert_eq!(leaf_hashes("blake3", 2, 8), 1); assert_eq!(leaf_hashes("blake3", 2, 128), 16); }
#[test]
fn a_merkle_path_is_one_hash_per_level() {
assert_eq!(merkle_path_permutations(20, 4, 0), 10);
assert_eq!(merkle_path_permutations(20, 2, 0), 20);
assert_eq!(merkle_path_permutations(20, 4, 2), 8);
assert_eq!(merkle_path_permutations(0, 4, 0), 0, "a height-1 tree has no path");
}
#[test]
fn the_root_reduction_is_paid_once_per_tree() {
assert_eq!(root_reduction_hashes(4, 0, 20), 0);
assert_eq!(root_reduction_hashes(4, 1, 20), 1, "4 nodes -> 1");
assert_eq!(root_reduction_hashes(4, 2, 20), 5, "16 -> 4 -> 1");
assert_eq!(root_reduction_hashes(2, 3, 20), 7, "8 -> 4 -> 2 -> 1");
}
#[test]
fn an_odd_height_keeps_a_narrower_level() {
assert_eq!(root_reduction_hashes(4, 2, 22), 5, "2^22 folds to exactly 16");
assert_eq!(root_reduction_hashes(4, 2, 19), 3, "2^19 folds to 8, not 16");
assert_eq!(root_reduction_hashes(2, 2, 19), 3, "arity 2 always lands on 4");
}
#[test]
fn the_default_binary_geometry_trades_path_levels_for_root_reductions() {
assert_eq!(merkle_path_permutations(22, 2, 4), 18, "22 levels less the 4 the kept level replaces");
assert_eq!(merkle_path_permutations(22, 2, 2), 20, "the same tree at the old llv");
assert_eq!(root_reduction_hashes(2, 4, 22), 15, "a 16-node kept level folds in 15");
assert_eq!(root_reduction_hashes(2, 2, 22), 3, "a 4-node one in 3");
}
#[test]
fn the_transcript_permutes_when_its_buffer_fills() {
let mut t = TranscriptSim::new("Poseidon1", 4);
t.put(12);
assert_eq!(t.hashes, 1);
let mut t = TranscriptSim::new("Poseidon1", 4);
t.put(5);
assert_eq!(t.hashes, 0, "a partial buffer has not permuted yet");
t.get_state();
assert_eq!(t.hashes, 1, "getState flushes it");
}
#[test]
fn an_absorb_invalidates_the_squeezed_output() {
let mut t = TranscriptSim::new("Poseidon1", 4);
t.put(12); assert_eq!(t.hashes, 1);
t.get_field(); assert_eq!(t.hashes, 1);
let mut t = TranscriptSim::new("Poseidon1", 4);
t.put(12);
t.put(1); t.get_field();
assert_eq!(t.hashes, 2, "the squeeze had to permute again");
}
#[test]
fn squeezing_past_the_fifo_permutes_again() {
let mut t = TranscriptSim::new("Poseidon1", 4);
t.put(12);
for _ in 0..5 {
t.get_field(); }
assert_eq!(t.hashes, 1);
t.get_field(); assert_eq!(t.hashes, 2);
}
fn minimal_geometry() -> VerifierGeometry {
VerifierGeometry {
n_bits_ext: 4,
arity: 4,
transcript_arity: 4,
n_queries: 1,
stage_widths: vec![12],
n_constants: 12,
step_n_bits: vec![4],
..Default::default()
}
}
#[test]
fn the_query_phase_is_leaf_plus_path_per_tree() {
let counts = verifier_hashes(&minimal_geometry(), "Poseidon1");
assert_eq!(counts.leaf, 2, "one leaf hash per tree");
assert_eq!(counts.merkle, 4, "two levels per tree");
assert_eq!(counts.fri, 0, "a single FRI step has no folding tree");
}
#[test]
fn the_query_phase_scales_with_the_query_count() {
let one = verifier_hashes(&minimal_geometry(), "Poseidon1");
let mut geom = minimal_geometry();
geom.n_queries = 8;
let eight = verifier_hashes(&geom, "Poseidon1");
assert_eq!(eight.leaf, one.leaf * 8);
assert_eq!(eight.merkle, one.merkle * 8);
}
#[test]
fn each_fri_step_adds_a_folding_tree() {
let mut geom = minimal_geometry();
geom.step_n_bits = vec![4, 2];
let counts = verifier_hashes(&geom, "Poseidon1");
assert_eq!(counts.fri, 2, "one leaf hash and one path level");
}
#[test]
fn grinding_costs_one_hash_whatever_the_threshold() {
let mut geom = minimal_geometry();
assert_eq!(verifier_hashes(&geom, "Poseidon1").grinding, 1);
geom.pow_bits = 20;
assert_eq!(verifier_hashes(&geom, "Poseidon1").grinding, 1);
}
#[test]
fn the_measured_blake3_air_matches_the_native_verifier() {
let geom = VerifierGeometry {
n_bits_ext: 22,
arity: 2,
transcript_arity: 2,
last_level_verification: 2,
n_queries: 114,
pow_bits: 16,
hash_commits: true,
stage_widths: vec![214, 174, 6],
n_constants: 8,
custom_commit_widths: vec![],
step_n_bits: vec![22, 19, 16, 13, 10, 7, 5],
n_publics: 0,
n_evals: 531,
stage_challenges: vec![2, 1],
stage_air_values: vec![0, 0],
final_pol_size: 1 << 5,
};
let counts = verifier_hashes(&geom, "blake3");
assert_eq!(counts.leaf, 6042, "114 queries x 53 compressions");
assert_eq!(counts.merkle, 9132, "4 trees x 114 x 20 levels + 4 root reductions");
assert_eq!(counts.fri, 8568, "6 folding trees");
assert_eq!(counts.transcript, 228, "replayed absorb/squeeze sequence");
assert_eq!(counts.grinding, 1);
assert_eq!(counts.total(), 23971);
}
#[test]
fn the_measured_air_under_poseidon_matches_the_native_verifier() {
let geom = VerifierGeometry {
n_bits_ext: 22,
arity: 4,
transcript_arity: 4,
last_level_verification: 2,
n_queries: 114,
pow_bits: 16,
hash_commits: true,
stage_widths: vec![214, 174, 6],
n_constants: 8,
custom_commit_widths: vec![],
step_n_bits: vec![22, 19, 16, 13, 10, 7, 5],
n_publics: 0,
n_evals: 531,
stage_challenges: vec![2, 1],
stage_air_values: vec![0, 0],
final_pol_size: 1 << 5,
};
for family in ["Poseidon1", "Poseidon2"] {
let counts = verifier_hashes(&geom, family);
assert_eq!(counts.leaf, 3990, "{family}");
assert_eq!(counts.merkle, 4124, "{family}");
assert_eq!(counts.fri, 4126, "{family}");
assert_eq!(counts.transcript, 155, "{family}");
assert_eq!(counts.total(), 12396, "{family}: what the native verifier reported");
}
}
#[test]
fn the_family_changes_the_count_not_just_the_price() {
let poseidon = verifier_hashes(&minimal_geometry(), "Poseidon1");
let blake3_arity = merkle_tree_arity("blake3");
let blake3 = verifier_hashes(
&VerifierGeometry { arity: blake3_arity, transcript_arity: blake3_arity, ..minimal_geometry() },
"blake3",
);
assert_eq!(blake3.merkle, poseidon.merkle * 2, "binary paths are twice as long");
}
#[test]
fn the_threshold_is_the_pinned_airs_block_capacity() {
let fit = |total: u64, lanes| blake3_recursion_fit(&HashCounts { leaf: total, ..Default::default() }, lanes);
assert_eq!(fit(37_444, 4).capacity, 9361);
assert_eq!(fit(37_444, 4).blocks, 9361);
assert!(!fit(37_444, 4).needs_compressor);
assert!(fit(37_445, 4).needs_compressor);
assert!(fit(37_444, 1).needs_compressor);
}
#[test]
fn the_block_boundary_is_the_n_bits_boundary() {
use pil2_stark_recurser::plonk2pil::setups::blake3::{BLAKE3_CLOCKS, CLOCK_WRAP_ROWS};
let n_bits = |blocks: usize| (blocks * BLAKE3_CLOCKS + CLOCK_WRAP_ROWS).next_power_of_two().trailing_zeros();
let capacity = blake3_recursion_fit(&HashCounts::default(), 4).capacity;
assert_eq!(n_bits(capacity), 19);
assert_eq!(n_bits(capacity + 1), 20);
}
#[test]
fn the_recorded_airs_land_on_the_right_side() {
let hashes_example = HashCounts { leaf: 6042, merkle: 9132, fri: 8568, transcript: 440, grinding: 1 };
let fit = blake3_recursion_fit(&hashes_example, 4);
assert_eq!(fit.blocks, 6046);
assert!(!fit.needs_compressor);
assert!(blake3_recursion_fit(&HashCounts { leaf: 11_283 * 4, ..Default::default() }, 4).needs_compressor);
}
}