use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use halo2curves::bn256::{G1Affine, G2Affine};
use halo2curves::serde::SerdeObject;
use nova_snark::provider::ptau::{NUM_SECTIONS_FULL, NUM_SECTIONS_PRUNED, PTAU_VERSION};
use num_bigint::BigUint;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
const PPOT_BASE_URL: &str =
"https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080";
#[derive(Debug)]
pub enum PrunerError {
InvalidMagic,
UnsupportedVersion(u32),
InvalidNumSections(u32),
InsufficientPower(u32, usize),
Io(io::Error),
Http(String),
SectionNotFound(u32),
}
impl std::fmt::Display for PrunerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrunerError::InvalidMagic => write!(f, "Invalid ptau magic string"),
PrunerError::UnsupportedVersion(v) => write!(f, "Unsupported version: {}", v),
PrunerError::InvalidNumSections(n) => write!(f, "Invalid number of sections: {}", n),
PrunerError::InsufficientPower(p, n) => {
write!(f, "Power {} insufficient for {} G1 points", p, n)
}
PrunerError::Io(e) => write!(f, "IO error: {}", e),
PrunerError::Http(e) => write!(f, "HTTP error: {}", e),
PrunerError::SectionNotFound(s) => write!(f, "Section {} not found", s),
}
}
}
impl std::error::Error for PrunerError {}
impl From<io::Error> for PrunerError {
fn from(e: io::Error) -> Self {
PrunerError::Io(e)
}
}
#[derive(Debug)]
struct PtauMetadata {
power: u32,
pos_tau_g1: u64,
pos_tau_g2: u64,
size_tau_g1: u64,
size_tau_g2: u64,
}
fn get_ptau_filename(power: u32) -> String {
if power == 28 {
"ppot_0080_final.ptau".to_string()
} else {
format!("ppot_0080_{:02}.ptau", power)
}
}
fn download_ptau(power: u32, dest: &PathBuf) -> Result<(), PrunerError> {
let filename = get_ptau_filename(power);
let url = format!("{}/{}", PPOT_BASE_URL, filename);
println!("Downloading {} from {}", filename, url);
let client = reqwest::blocking::Client::new();
let mut response = client
.get(&url)
.send()
.map_err(|e| PrunerError::Http(e.to_string()))?;
if !response.status().is_success() {
return Err(PrunerError::Http(format!(
"HTTP {} for {}",
response.status(),
url
)));
}
let total_size = response.content_length().unwrap_or(0);
println!("Total size: {:.2} MB", total_size as f64 / 1_000_000.0);
let mut file = File::create(dest)?;
let mut downloaded: u64 = 0;
let mut buffer = [0u8; 8192];
let mut last_percent = 0;
loop {
let bytes_read = response
.read(&mut buffer)
.map_err(|e| PrunerError::Http(e.to_string()))?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])?;
downloaded += bytes_read as u64;
if let Some(percent) = (downloaded * 100).checked_div(total_size) {
let percent = percent as u32;
if percent >= last_percent + 10 {
println!(
" {}% downloaded ({:.2} MB)",
percent,
downloaded as f64 / 1_000_000.0
);
last_percent = percent / 10 * 10;
}
}
}
println!("Download complete: {:?}", dest);
Ok(())
}
fn read_ptau_metadata(reader: &mut (impl Read + Seek)) -> Result<PtauMetadata, PrunerError> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if &magic != b"ptau" {
return Err(PrunerError::InvalidMagic);
}
let version = reader.read_u32::<LittleEndian>()?;
if version != PTAU_VERSION {
return Err(PrunerError::UnsupportedVersion(version));
}
let num_sections = reader.read_u32::<LittleEndian>()?;
if num_sections != NUM_SECTIONS_FULL && num_sections != NUM_SECTIONS_PRUNED {
return Err(PrunerError::InvalidNumSections(num_sections));
}
let mut pos_header = 0u64;
let mut pos_tau_g1 = 0u64;
let mut pos_tau_g2 = 0u64;
let mut size_tau_g1 = 0u64;
let mut size_tau_g2 = 0u64;
for _ in 0..num_sections {
let section_id = reader.read_u32::<LittleEndian>()?;
let section_size = reader.read_i64::<LittleEndian>()? as u64;
let section_pos = reader.stream_position()?;
match section_id {
1 => pos_header = section_pos,
2 => {
pos_tau_g1 = section_pos;
size_tau_g1 = section_size;
}
3 => {
pos_tau_g2 = section_pos;
size_tau_g2 = section_size;
}
_ => {}
}
reader.seek(SeekFrom::Current(section_size as i64))?;
}
if pos_header == 0 {
return Err(PrunerError::SectionNotFound(1));
}
if pos_tau_g1 == 0 {
return Err(PrunerError::SectionNotFound(2));
}
if pos_tau_g2 == 0 {
return Err(PrunerError::SectionNotFound(3));
}
reader.seek(SeekFrom::Start(pos_header))?;
let n8 = reader.read_u32::<LittleEndian>()?;
reader.seek(SeekFrom::Current(n8 as i64))?;
let power = reader.read_u32::<LittleEndian>()?;
Ok(PtauMetadata {
power,
pos_tau_g1,
pos_tau_g2,
size_tau_g1,
size_tau_g2,
})
}
fn read_g1_points(reader: &mut impl Read, num: usize) -> Result<Vec<G1Affine>, PrunerError> {
println!("Reading {} G1 points...", num);
let mut points = Vec::with_capacity(num);
for i in 0..num {
let point = G1Affine::read_raw(reader)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
points.push(point);
if num > 10 && i % (num / 10) == 0 {
println!(" {}% read", i * 100 / num);
}
}
println!("Done reading G1 points");
Ok(points)
}
fn read_g2_points(reader: &mut impl Read, num: usize) -> Result<Vec<G2Affine>, PrunerError> {
println!("Reading {} G2 points...", num);
let mut points = Vec::with_capacity(num);
for _ in 0..num {
let point = G2Affine::read_raw(reader)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
points.push(point);
}
println!("Done reading G2 points");
Ok(points)
}
fn write_pruned_ptau(
writer: &mut (impl Write + Seek),
g1_points: &[G1Affine],
g2_points: &[G2Affine],
power: u32,
) -> Result<(), PrunerError> {
const N8: usize = 32;
writer.write_all(b"ptau")?;
writer.write_u32::<LittleEndian>(PTAU_VERSION)?;
writer.write_u32::<LittleEndian>(NUM_SECTIONS_PRUNED)?;
let header_size = 4 + N8 + 4; writer.write_u32::<LittleEndian>(1)?; writer.write_i64::<LittleEndian>(header_size as i64)?;
writer.write_u32::<LittleEndian>(N8 as u32)?;
let modulus = BigUint::parse_bytes(
b"30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47",
16,
)
.unwrap();
let mut modulus_bytes = [0u8; N8];
let bytes = modulus.to_bytes_le();
modulus_bytes[..bytes.len()].copy_from_slice(&bytes);
writer.write_all(&modulus_bytes)?;
writer.write_u32::<LittleEndian>(power)?;
let g1_size = g1_points.len() * 64; writer.write_u32::<LittleEndian>(2)?;
writer.write_i64::<LittleEndian>(g1_size as i64)?;
println!("Writing {} G1 points...", g1_points.len());
for (i, point) in g1_points.iter().enumerate() {
point
.write_raw(writer)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let num = g1_points.len();
if num > 10 && i % (num / 10) == 0 {
println!(" {}% written", i * 100 / num);
}
}
println!("Done writing G1 points");
let g2_size = g2_points.len() * 128; writer.write_u32::<LittleEndian>(3)?;
writer.write_i64::<LittleEndian>(g2_size as i64)?;
println!("Writing {} G2 points...", g2_points.len());
for point in g2_points {
point
.write_raw(writer)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
}
println!("Done writing G2 points");
Ok(())
}
fn prune_ptau(
input_path: &PathBuf,
output_path: &PathBuf,
num_g1: usize,
num_g2: usize,
) -> Result<(), PrunerError> {
println!("Reading metadata from {:?}", input_path);
let mut reader = BufReader::new(File::open(input_path)?);
let metadata = read_ptau_metadata(&mut reader)?;
println!("PTAU file power: {}", metadata.power);
println!("TauG1 section size: {} bytes", metadata.size_tau_g1);
println!("TauG2 section size: {} bytes", metadata.size_tau_g2);
let max_g1 = metadata.size_tau_g1 as usize / 64;
let max_g2 = metadata.size_tau_g2 as usize / 128;
println!("Max G1 points available: {}", max_g1);
println!("Max G2 points available: {}", max_g2);
if num_g1 > max_g1 {
return Err(PrunerError::InsufficientPower(metadata.power, num_g1));
}
if num_g2 > max_g2 {
return Err(PrunerError::InsufficientPower(metadata.power, num_g2));
}
reader.seek(SeekFrom::Start(metadata.pos_tau_g1))?;
let g1_points = read_g1_points(&mut reader, num_g1)?;
reader.seek(SeekFrom::Start(metadata.pos_tau_g2))?;
let g2_points = read_g2_points(&mut reader, num_g2)?;
println!("Writing pruned ptau to {:?}", output_path);
let mut writer = BufWriter::new(File::create(output_path)?);
write_pruned_ptau(&mut writer, &g1_points, &g2_points, metadata.power)?;
let input_size = std::fs::metadata(input_path)?.len();
let output_size = std::fs::metadata(output_path)?.len();
println!("\n=== Summary ===");
println!(
"Input size: {} bytes ({:.2} MB)",
input_size,
input_size as f64 / 1_000_000.0
);
println!(
"Output size: {} bytes ({:.2} MB)",
output_size,
output_size as f64 / 1_000_000.0
);
println!(
"Reduction: {:.1}x smaller",
input_size as f64 / output_size as f64
);
println!("G1 points: {}", num_g1);
println!("G2 points: {}", num_g2);
Ok(())
}
fn print_usage() {
println!("PPOT Pruner - Prune Powers of Tau files for HyperKZG/Mercury");
println!();
println!("Usage: cargo run --example ppot_prune --features io -- [OPTIONS]");
println!();
println!("Options:");
println!(" --power <N> Power of 2 for the ptau file (e.g., 20 for 2^20 constraints)");
println!(" --output <DIR> Output directory for pruned files (default: pruned)");
println!(" --download <DIR> Download directory for original files (default: temp)");
println!(" --num-g1 <N> Number of G1 points to keep (default: 2^power)");
println!(" --skip-download Skip download if file already exists (default: true)");
println!();
println!("Examples:");
println!(" # Prune power 20 (1M constraints, ~64MB output)");
println!(" cargo run --example ppot_prune --features io -- --power 20");
println!();
println!(" # Prune power 23 with custom output");
println!(" cargo run --example ppot_prune --features io -- --power 23 --output ./my_ptau");
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let mut power: Option<u32> = None;
let mut output = PathBuf::from("pruned");
let mut download_dir: Option<PathBuf> = None;
let mut num_g1_override: Option<usize> = None;
let mut skip_existing = true;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--power" | "-p" => {
if i + 1 >= args.len() {
eprintln!("Missing value for --power");
print_usage();
return Ok(());
}
i += 1;
power = Some(args[i].parse()?);
}
"--output" | "-o" => {
if i + 1 >= args.len() {
eprintln!("Missing value for --output");
print_usage();
return Ok(());
}
i += 1;
output = PathBuf::from(&args[i]);
}
"--download" | "-d" => {
if i + 1 >= args.len() {
eprintln!("Missing value for --download");
print_usage();
return Ok(());
}
i += 1;
download_dir = Some(PathBuf::from(&args[i]));
}
"--num-g1" | "-n" => {
if i + 1 >= args.len() {
eprintln!("Missing value for --num-g1");
print_usage();
return Ok(());
}
i += 1;
num_g1_override = Some(args[i].parse()?);
}
"--skip-download" => {
skip_existing = true;
}
"--force-download" => {
skip_existing = false;
}
"--help" | "-h" => {
print_usage();
return Ok(());
}
_ => {
eprintln!("Unknown argument: {}", args[i]);
print_usage();
return Ok(());
}
}
i += 1;
}
let power = match power {
Some(p) => p,
None => {
print_usage();
return Ok(());
}
};
std::fs::create_dir_all(&output)?;
let download_dir = download_dir.unwrap_or_else(std::env::temp_dir);
std::fs::create_dir_all(&download_dir)?;
let ptau_filename = get_ptau_filename(power);
let input_path = download_dir.join(&ptau_filename);
if !input_path.exists() || !skip_existing {
download_ptau(power, &input_path)?;
} else {
println!("Using existing file: {:?}", input_path);
}
let num_g1 = num_g1_override.unwrap_or(1 << power);
let num_g2 = 2;
let output_filename = format!("ppot_pruned_{:02}.ptau", power);
let output_path = output.join(output_filename);
prune_ptau(&input_path, &output_path, num_g1, num_g2)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filename_generation() {
assert_eq!(get_ptau_filename(15), "ppot_0080_15.ptau");
assert_eq!(get_ptau_filename(20), "ppot_0080_20.ptau");
assert_eq!(get_ptau_filename(28), "ppot_0080_final.ptau");
}
}