use core::fmt::{Display, Formatter, Result as FmtResult};
use serde::Serialize;
use thiserror::Error;
use crate::error::ErrorBits;
use crate::fri::FriRegime;
use crate::logup::LOGUP_LABEL;
use crate::report::{BATCH_LABEL, DEEP_LABEL, LDT_COMMIT_LABEL, LDT_QUERY_LABEL};
pub const fn grinding_bits(pow_bits: usize) -> f64 {
pow_bits as f64
}
pub const fn boost(error: ErrorBits, pow_bits: usize) -> ErrorBits {
ErrorBits::from_log2(error.bits() + grinding_bits(pow_bits))
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct GrindingSites {
pub out_of_domain: usize,
pub batch_combination: usize,
pub lookup_challenge: usize,
}
impl GrindingSites {
pub const NONE: Self = Self {
out_of_domain: 0,
batch_combination: 0,
lookup_challenge: 0,
};
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum GrindingSite {
OutOfDomain,
BatchCombination,
LookupChallenge,
LdtCommitPhase,
LdtQueryPhase,
}
impl GrindingSite {
pub const ALL: [Self; 5] = [
Self::OutOfDomain,
Self::BatchCombination,
Self::LookupChallenge,
Self::LdtCommitPhase,
Self::LdtQueryPhase,
];
#[must_use]
pub const fn report_label(self) -> &'static str {
match self {
Self::OutOfDomain => DEEP_LABEL,
Self::BatchCombination => BATCH_LABEL,
Self::LookupChallenge => LOGUP_LABEL,
Self::LdtCommitPhase => LDT_COMMIT_LABEL,
Self::LdtQueryPhase => LDT_QUERY_LABEL,
}
}
const fn index(self) -> usize {
match self {
Self::OutOfDomain => 0,
Self::BatchCombination => 1,
Self::LookupChallenge => 2,
Self::LdtCommitPhase => 3,
Self::LdtQueryPhase => 4,
}
}
}
impl Display for GrindingSite {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::OutOfDomain => write!(f, "out-of-domain"),
Self::BatchCombination => write!(f, "batch-combination"),
Self::LookupChallenge => write!(f, "lookup-challenge"),
Self::LdtCommitPhase => write!(f, "ldt-commit-phase"),
Self::LdtQueryPhase => write!(f, "ldt-query-phase"),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ZeroBitConvention {
Elided,
Always,
WhenPhaseRuns,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct GrindingStep {
pub protocol: &'static str,
pub label: &'static str,
pub site: GrindingSite,
pub zero_bits: ZeroBitConvention,
}
pub const GRINDING_VOCABULARY: [GrindingStep; 10] = [
GrindingStep {
protocol: "p3-circle-pcs",
label: "batch_pow",
site: GrindingSite::BatchCombination,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-circle-pcs",
label: "commit_pow",
site: GrindingSite::LdtCommitPhase,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-circle-pcs",
label: "query_pow",
site: GrindingSite::LdtQueryPhase,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-stir-pcs-batch",
label: "batch_pow",
site: GrindingSite::BatchCombination,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-uni-stark",
label: "ood_pow",
site: GrindingSite::OutOfDomain,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-batch-stark",
label: "ood_pow",
site: GrindingSite::OutOfDomain,
zero_bits: ZeroBitConvention::Always,
},
GrindingStep {
protocol: "p3-batch-stark",
label: "lookup_pow",
site: GrindingSite::LookupChallenge,
zero_bits: ZeroBitConvention::WhenPhaseRuns,
},
GrindingStep {
protocol: "p3-fri-pcs",
label: "batch_pow",
site: GrindingSite::BatchCombination,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-fri",
label: "commit_pow",
site: GrindingSite::LdtCommitPhase,
zero_bits: ZeroBitConvention::Elided,
},
GrindingStep {
protocol: "p3-fri",
label: "query_pow",
site: GrindingSite::LdtQueryPhase,
zero_bits: ZeroBitConvention::Elided,
},
];
pub const UNPRICED_GRINDING_SITES: [(&str, &str); 12] = [
("p3-binary-pcs", "query_pow"),
("p3-whir", "query_pow"),
("p3-whir", "final_query_pow"),
("p3-whir-hvzk", "query_pow"),
("p3-whir-hvzk-base", "base_pow"),
("p3-stir", "folding_pow"),
("p3-stir", "query_pow"),
("p3-stir", "final_folding_pow"),
("p3-stir", "final_pow"),
("p3-sumcheck-quadratic", "round_pow"),
("p3-sumcheck-hvzk", "round_pow"),
("p3-sumcheck-generic-degree", "round_pow"),
];
#[must_use]
pub fn is_unpriced_grinding_site(protocol: &str, label: &str) -> bool {
UNPRICED_GRINDING_SITES
.iter()
.any(|&(p, l)| p == protocol && l == label)
}
#[must_use]
pub fn grinding_step(protocol: &str, label: &str) -> Option<&'static GrindingStep> {
GRINDING_VOCABULARY
.iter()
.find(|step| step.protocol == protocol && step.label == label)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct RecordedGrind {
pub protocol: &'static str,
pub label: &'static str,
pub bits: usize,
}
impl RecordedGrind {
#[must_use]
pub const fn new(protocol: &'static str, label: &'static str, bits: usize) -> Self {
Self {
protocol,
label,
bits,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct GrindingBudget {
bits: [usize; GrindingSite::ALL.len()],
}
impl GrindingBudget {
pub const NONE: Self = Self {
bits: [0; GrindingSite::ALL.len()],
};
#[must_use]
pub const fn from_sites(sites: &GrindingSites) -> Self {
let mut bits = [0; GrindingSite::ALL.len()];
bits[GrindingSite::OutOfDomain.index()] = sites.out_of_domain;
bits[GrindingSite::BatchCombination.index()] = sites.batch_combination;
bits[GrindingSite::LookupChallenge.index()] = sites.lookup_challenge;
Self { bits }
}
#[must_use]
pub const fn with_fri(mut self, ldt: &FriRegime) -> Self {
self.bits[GrindingSite::LdtCommitPhase.index()] = ldt.commit_pow_bits;
self.bits[GrindingSite::LdtQueryPhase.index()] = ldt.query_pow_bits;
self
}
#[must_use]
pub const fn bits(&self, site: GrindingSite) -> usize {
self.bits[site.index()]
}
pub fn check(
&self,
protocols: &[&str],
recorded: &[RecordedGrind],
) -> Result<(), GrindingMismatch> {
for grind in recorded {
if !protocols.contains(&grind.protocol) {
return Err(GrindingMismatch::UnlistedProtocol {
protocol: grind.protocol,
label: grind.label,
recorded: grind.bits,
});
}
if grinding_step(grind.protocol, grind.label).is_none() {
return Err(GrindingMismatch::UnknownStep {
protocol: grind.protocol,
label: grind.label,
recorded: grind.bits,
});
}
}
for step in GRINDING_VOCABULARY
.iter()
.filter(|step| protocols.contains(&step.protocol))
{
let credited = self.bits(step.site);
let mut described = false;
for grind in recorded
.iter()
.filter(|grind| grind.protocol == step.protocol && grind.label == step.label)
{
described = true;
if grind.bits != credited {
return Err(GrindingMismatch::Difficulty {
protocol: step.protocol,
label: step.label,
site: step.site,
credited,
recorded: grind.bits,
});
}
}
match (step.zero_bits, described) {
(ZeroBitConvention::Always | ZeroBitConvention::WhenPhaseRuns, true) => {}
(ZeroBitConvention::Elided, true) if credited > 0 => {}
(ZeroBitConvention::Elided, true) => {
return Err(GrindingMismatch::Unexpected {
protocol: step.protocol,
label: step.label,
site: step.site,
});
}
(ZeroBitConvention::WhenPhaseRuns, false) => {}
(ZeroBitConvention::Elided, false) if credited == 0 => {}
(ZeroBitConvention::Elided | ZeroBitConvention::Always, false) => {
return Err(GrindingMismatch::Missing {
protocol: step.protocol,
label: step.label,
site: step.site,
credited,
});
}
}
}
Ok(())
}
}
#[derive(Copy, Clone, Debug, Error, PartialEq, Eq)]
pub enum GrindingMismatch {
#[error(
"{site} grinding disagrees: the security model credits {credited} bits, `{protocol}`'s `{label}` step demands {recorded}"
)]
Difficulty {
protocol: &'static str,
label: &'static str,
site: GrindingSite,
credited: usize,
recorded: usize,
},
#[error(
"{site} grinding disagrees: the security model credits {credited} bits, `{protocol}` describes no `{label}` step, so 0 are demanded"
)]
Missing {
protocol: &'static str,
label: &'static str,
site: GrindingSite,
credited: usize,
},
#[error(
"{site} grinding disagrees: the security model credits 0 bits, and `{protocol}` describes a zero-bit `{label}` step it should elide"
)]
Unexpected {
protocol: &'static str,
label: &'static str,
site: GrindingSite,
},
#[error(
"`{protocol}`'s `{label}` step demands {recorded} bits at a site `GRINDING_VOCABULARY` does not map"
)]
UnknownStep {
protocol: &'static str,
label: &'static str,
recorded: usize,
},
#[error(
"`{protocol}`'s `{label}` step demands {recorded} bits, and `{protocol}` is not among the protocols checked"
)]
UnlistedProtocol {
protocol: &'static str,
label: &'static str,
recorded: usize,
},
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn grinding_mismatch_is_an_error_naming_site_and_counts() {
fn assert_error<E: core::error::Error>() {}
assert_error::<GrindingMismatch>();
let mismatch = GrindingMismatch::Difficulty {
protocol: "p3-fri",
label: "query_pow",
site: GrindingSite::LdtQueryPhase,
credited: 8,
recorded: 12,
};
assert_eq!(
mismatch.to_string(),
"ldt-query-phase grinding disagrees: the security model credits 8 bits, `p3-fri`'s `query_pow` step demands 12"
);
}
#[test]
fn boost_adds_pow_bits_and_zero_is_neutral() {
let error = ErrorBits::from_log2(80.0);
assert!((boost(error, 0).bits() - 80.0).abs() < 1e-12);
assert!((boost(error, 16).bits() - 96.0).abs() < 1e-12);
}
#[test]
fn default_sites_are_neutral() {
assert_eq!(GrindingSites::default(), GrindingSites::NONE);
}
const fn regime(commit_pow_bits: usize, query_pow_bits: usize) -> FriRegime {
FriRegime {
log_blowup: 1,
num_queries: 64,
log_final_poly_len: 0,
max_log_arity: 3,
commit_pow_bits,
query_pow_bits,
}
}
const UNI_STARK_STACK: [&str; 3] = ["p3-uni-stark", "p3-fri-pcs", "p3-fri"];
#[test]
fn circle_and_stir_batch_grinding_matches_credited_sites() {
let sites = GrindingSites {
batch_combination: 5,
..GrindingSites::NONE
};
let circle = GrindingBudget::from_sites(&sites).with_fri(®ime(3, 7));
let recorded = [
RecordedGrind::new("p3-circle-pcs", "batch_pow", 5),
RecordedGrind::new("p3-circle-pcs", "commit_pow", 3),
RecordedGrind::new("p3-circle-pcs", "query_pow", 7),
];
circle.check(&["p3-circle-pcs"], &recorded).unwrap();
for index in 0..recorded.len() {
let mut mismatched = recorded;
mismatched[index].bits += 1;
assert!(circle.check(&["p3-circle-pcs"], &mismatched).is_err());
}
let stir = GrindingBudget::from_sites(&sites);
stir.check(
&["p3-stir-pcs-batch"],
&[RecordedGrind::new("p3-stir-pcs-batch", "batch_pow", 5)],
)
.unwrap();
assert!(stir.check(&["p3-stir-pcs-batch"], &[]).is_err());
for protocol in ["p3-circle-pcs", "p3-stir-pcs-batch"] {
GrindingBudget::NONE.check(&[protocol], &[]).unwrap();
}
}
#[test]
fn every_site_appears_in_the_vocabulary_exactly_as_often_as_a_protocol_names_it() {
for site in GrindingSite::ALL {
assert!(
GRINDING_VOCABULARY.iter().any(|step| step.site == site),
"{site} is modeled but no protocol's step maps to it"
);
}
for (index, left) in GRINDING_VOCABULARY.iter().enumerate() {
for right in &GRINDING_VOCABULARY[index + 1..] {
assert!(
(left.protocol, left.label) != (right.protocol, right.label),
"`{}`'s `{}` has two vocabulary rows",
left.protocol,
left.label
);
}
}
}
#[test]
fn the_same_label_in_two_protocols_names_one_site_under_two_conventions() {
let uni = grinding_step("p3-uni-stark", "ood_pow").expect("uni-STARK grinds before zeta");
let batch =
grinding_step("p3-batch-stark", "ood_pow").expect("batch-STARK grinds before zeta");
assert_eq!(uni.site, batch.site);
assert_eq!(uni.site, GrindingSite::OutOfDomain);
assert_eq!(uni.zero_bits, ZeroBitConvention::Elided);
assert_eq!(batch.zero_bits, ZeroBitConvention::Always);
}
#[test]
fn each_site_boosts_the_report_term_its_own_module_labels() {
assert_eq!(GrindingSite::OutOfDomain.report_label(), DEEP_LABEL);
assert_eq!(GrindingSite::BatchCombination.report_label(), BATCH_LABEL);
assert_eq!(GrindingSite::LookupChallenge.report_label(), LOGUP_LABEL);
assert_eq!(
GrindingSite::LdtCommitPhase.report_label(),
LDT_COMMIT_LABEL
);
assert_eq!(GrindingSite::LdtQueryPhase.report_label(), LDT_QUERY_LABEL);
}
#[test]
fn the_budget_reads_back_both_halves_of_the_model() {
let sites = GrindingSites {
out_of_domain: 3,
batch_combination: 5,
lookup_challenge: 7,
};
let budget = GrindingBudget::from_sites(&sites).with_fri(®ime(11, 13));
assert_eq!(budget.bits(GrindingSite::OutOfDomain), 3);
assert_eq!(budget.bits(GrindingSite::BatchCombination), 5);
assert_eq!(budget.bits(GrindingSite::LookupChallenge), 7);
assert_eq!(budget.bits(GrindingSite::LdtCommitPhase), 11);
assert_eq!(budget.bits(GrindingSite::LdtQueryPhase), 13);
let without_ldt = GrindingBudget::from_sites(&sites);
assert_eq!(without_ldt.bits(GrindingSite::LdtCommitPhase), 0);
assert_eq!(without_ldt.bits(GrindingSite::LdtQueryPhase), 0);
}
#[test]
fn a_stack_that_grinds_exactly_what_the_model_credits_agrees() {
let sites = GrindingSites {
out_of_domain: 8,
batch_combination: 10,
..GrindingSites::NONE
};
let budget = GrindingBudget::from_sites(&sites).with_fri(®ime(4, 16));
let recorded = [
RecordedGrind::new("p3-uni-stark", "ood_pow", 8),
RecordedGrind::new("p3-fri-pcs", "batch_pow", 10),
RecordedGrind::new("p3-fri", "commit_pow", 4),
RecordedGrind::new("p3-fri", "commit_pow", 4),
RecordedGrind::new("p3-fri", "query_pow", 16),
];
budget
.check(&UNI_STARK_STACK, &recorded)
.expect("the two halves agree");
}
#[test]
fn a_credited_site_the_transcript_never_grinds_for_is_the_overstating_direction() {
let budget = GrindingBudget::from_sites(&GrindingSites {
batch_combination: 10,
..GrindingSites::NONE
})
.with_fri(®ime(0, 0));
let mismatch = budget
.check(&UNI_STARK_STACK, &[])
.expect_err("credited bits nobody pays must be reported");
assert_eq!(
mismatch,
GrindingMismatch::Missing {
protocol: "p3-fri-pcs",
label: "batch_pow",
site: GrindingSite::BatchCombination,
credited: 10,
}
);
}
#[test]
fn a_difficulty_disagreement_names_the_site_and_both_numbers() {
let budget = GrindingBudget::from_sites(&GrindingSites::NONE).with_fri(®ime(0, 16));
let recorded = [RecordedGrind::new("p3-fri", "query_pow", 20)];
let mismatch = budget
.check(&["p3-fri"], &recorded)
.expect_err("20 recorded against 16 credited must be reported");
assert_eq!(
mismatch,
GrindingMismatch::Difficulty {
protocol: "p3-fri",
label: "query_pow",
site: GrindingSite::LdtQueryPhase,
credited: 16,
recorded: 20,
}
);
let rendered = alloc::format!("{mismatch}");
assert!(rendered.contains("ldt-query-phase"), "{rendered}");
assert!(rendered.contains("16"), "{rendered}");
assert!(rendered.contains("20"), "{rendered}");
}
#[test]
fn the_always_convention_requires_a_zero_bit_step_the_elided_one_forbids() {
let budget = GrindingBudget::from_sites(&GrindingSites::NONE);
assert_eq!(
budget
.check(&["p3-batch-stark"], &[])
.expect_err("an always-described step cannot be absent"),
GrindingMismatch::Missing {
protocol: "p3-batch-stark",
label: "ood_pow",
site: GrindingSite::OutOfDomain,
credited: 0,
}
);
budget
.check(
&["p3-batch-stark"],
&[RecordedGrind::new("p3-batch-stark", "ood_pow", 0)],
)
.expect("a zero-bit step is what the convention calls for");
assert_eq!(
budget
.check(
&["p3-uni-stark"],
&[RecordedGrind::new("p3-uni-stark", "ood_pow", 0)],
)
.expect_err("an elided step cannot be described at zero bits"),
GrindingMismatch::Unexpected {
protocol: "p3-uni-stark",
label: "ood_pow",
site: GrindingSite::OutOfDomain,
}
);
}
#[test]
fn a_lookup_phase_that_does_not_run_is_neither_paid_for_nor_credited() {
let budget = GrindingBudget::from_sites(&GrindingSites {
lookup_challenge: 20,
..GrindingSites::NONE
});
budget
.check(
&["p3-batch-stark"],
&[RecordedGrind::new("p3-batch-stark", "ood_pow", 0)],
)
.expect("a phase that does not run credits nothing");
assert_eq!(
budget
.check(
&["p3-batch-stark"],
&[
RecordedGrind::new("p3-batch-stark", "ood_pow", 0),
RecordedGrind::new("p3-batch-stark", "lookup_pow", 8),
],
)
.expect_err("8 recorded against 20 credited must be reported"),
GrindingMismatch::Difficulty {
protocol: "p3-batch-stark",
label: "lookup_pow",
site: GrindingSite::LookupChallenge,
credited: 20,
recorded: 8,
}
);
}
#[test]
fn a_step_outside_the_checked_stack_is_reported_rather_than_ignored() {
let budget = GrindingBudget::NONE;
assert_eq!(
budget
.check(
&["p3-whir"],
&[RecordedGrind::new("p3-whir", "query_pow", 16)]
)
.expect_err("an unmapped site must not pass silently"),
GrindingMismatch::UnknownStep {
protocol: "p3-whir",
label: "query_pow",
recorded: 16,
}
);
assert_eq!(
budget
.check(
&["p3-fri"],
&[RecordedGrind::new("p3-fri-pcs", "batch_pow", 4)]
)
.expect_err("an unlisted protocol must not pass silently"),
GrindingMismatch::UnlistedProtocol {
protocol: "p3-fri-pcs",
label: "batch_pow",
recorded: 4,
}
);
}
}