Skip to main content

nova_snark/provider/
ptau.rs

1//! Powers of Tau (PTAU) file handling for trusted setup.
2//!
3//! This module provides functionality for reading and writing PTAU files,
4//! which contain the structured reference string (SRS) needed for KZG-based
5//! polynomial commitment schemes like HyperKZG and Mercury.
6//!
7//! # Obtaining PTAU Files
8//!
9//! There are two ways to obtain PTAU files:
10//!
11//! ## Option 1: Use Ethereum Powers of Tau Ceremony Files (Recommended for Production)
12//!
13//! For production use, you should use files from the Ethereum [Perpetual Powers of Tau](https://github.com/privacy-ethereum/perpetualpowersoftau)
14//! ceremony. This ceremony has 80+ participants, providing strong security guarantees
15//! (only one honest participant needed).
16//!
17//! ### Downloading Original PPOT Files
18//!
19//! Original PPOT files can be downloaded directly from the PSE S3 bucket:
20//! <https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/>
21//!
22//! Files are named:
23//! - `ppot_0080_15.ptau` through `ppot_0080_27.ptau` (powers 15-27)
24//! - `ppot_0080_final.ptau` (power 28, ~288GB)
25//!
26//! ### Pruning Files (Optional, Reduces Size ~18x)
27//!
28//! To reduce file size, you can prune files using the `ppot_prune` example:
29//!
30//! ```bash
31//! # Prune power 24 (16M constraints, ~1GB output vs ~18GB original)
32//! cargo run --example ppot_prune --features io -- --power 24
33//!
34//! # Prune power 20 with custom output directory
35//! cargo run --example ppot_prune --features io -- --power 20 --output ./my_ptau
36//! ```
37//!
38//! Pruned files are named `ppot_pruned_XX.ptau`.
39//!
40//! ### Using PTAU Files in Your Application
41//!
42//! Place your PTAU files (either original or pruned) in a directory and use:
43//!
44//! ```ignore
45//! use nova_snark::nova::PublicParams;
46//! use std::path::Path;
47//!
48//! // Load commitment key from ptau directory
49//! // Accepts both ppot_pruned_XX.ptau and ppot_0080_XX.ptau naming conventions
50//! let pp = PublicParams::setup_with_ptau_dir(
51//!     &circuit,
52//!     &*S1::ck_floor(),
53//!     &*S2::ck_floor(),
54//!     Path::new("path/to/ptau_files"),
55//! )?;
56//! ```
57//!
58//! The library automatically selects the smallest available file that provides
59//! enough generators for your circuit.
60//!
61//! ## Option 2: Generate Test PTAU Files (Testing Only)
62//!
63//! For testing purposes only, you can generate PTAU files with a random tau.
64//! **These are insecure and must not be used in production.**
65//!
66//! ```bash
67//! # Requires the test-utils feature
68//! cargo run --example ptau_test_setup --features test-utils,io
69//! ```
70//!
71//! # Supported File Naming Conventions
72//!
73//! The library accepts files with these naming patterns:
74//! - `ppot_pruned_XX.ptau` - Pruned files (preferred, smaller)
75//! - `ppot_0080_XX.ptau` - Original PPOT files from PSE
76//! - `ppot_0080_final.ptau` - Power 28 original file
77//!
78//! # File Format
79//!
80//! PTAU files use a binary format with the following structure:
81//!
82//! - **Header**: Magic ("ptau"), version (1), number of sections
83//! - **Section 1**: n8, prime modulus, power
84//! - **Section 2**: TauG1 - N × G1 points (64 bytes each on BN254)
85//! - **Section 3**: TauG2 - M × G2 points (128 bytes each on BN254)
86//!
87//! Both full (11 sections) and pruned (3 sections) formats are supported.
88
89use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
90use ff::PrimeField;
91use halo2curves::CurveAffine;
92use num_bigint::BigUint;
93use std::{
94  fs::File,
95  io::{self, Read, Seek, SeekFrom, Write},
96  path::Path,
97  str::{from_utf8, Utf8Error},
98};
99
100/// Errors that can occur when reading or writing PTAU files.
101#[derive(thiserror::Error, Debug)]
102pub enum PtauFileError {
103  /// Invalid magic string (expected "ptau")
104  #[error("Invalid magic string")]
105  InvalidHead,
106
107  /// Unsupported PTAU file version
108  #[error("Unsupported version")]
109  UnsupportedVersion(u32),
110
111  /// Invalid number of sections in the file
112  #[error("Invalid number of sections")]
113  InvalidNumSections(u32),
114
115  /// Invalid base prime modulus
116  #[error("Invalid base prime")]
117  InvalidPrime(BigUint),
118
119  /// Insufficient power for the requested number of G1 points
120  #[error("Insufficient power for G1")]
121  InsufficientPowerForG1 {
122    /// The power in the file
123    power: u32,
124    /// The required number of points
125    required: usize,
126  },
127
128  /// Insufficient power for the requested number of G2 points
129  #[error("Insufficient power for G2")]
130  InsufficientPowerForG2 {
131    /// The power in the file
132    power: u32,
133    /// The required number of points
134    required: usize,
135  },
136
137  /// IO error during file operations
138  #[error(transparent)]
139  IoError(#[from] io::Error),
140  /// UTF-8 decoding error
141  #[error(transparent)]
142  Utf8Error(#[from] Utf8Error),
143}
144
145#[derive(Debug)]
146struct MetaData {
147  pos_header: u64,
148  pos_tau_g1: u64,
149  pos_tau_g2: u64,
150}
151
152/// PTAU file format version (always 1)
153pub const PTAU_VERSION: u32 = 1;
154/// Number of sections in original PPOT files
155pub const NUM_SECTIONS_FULL: u32 = 11;
156/// Number of sections in pruned files (header, tau_g1, tau_g2 only)
157pub const NUM_SECTIONS_PRUNED: u32 = 3;
158
159/// Maximum power available from the Ethereum PPOT ceremony (2^28 = 256M constraints)
160pub const MAX_PPOT_POWER: u32 = 28;
161
162fn write_header<Base: PrimeField>(
163  writer: &mut impl Write,
164  power: u32,
165) -> Result<(), PtauFileError> {
166  const N8: usize = 32;
167
168  writer.write_all(b"ptau")?;
169
170  writer.write_u32::<LittleEndian>(PTAU_VERSION)?;
171  writer.write_u32::<LittleEndian>(NUM_SECTIONS_FULL)?;
172
173  // * header
174  writer.write_u32::<LittleEndian>(1)?;
175  writer.write_i64::<LittleEndian>(4 + N8 as i64 + 4)?;
176
177  writer.write_u32::<LittleEndian>(N8 as u32)?;
178
179  let modulus = BigUint::parse_bytes(&Base::MODULUS.as_bytes()[2..], 16).unwrap();
180  let mut bytes = [0u8; N8];
181  bytes.copy_from_slice(&modulus.to_bytes_le());
182  writer.write_all(&bytes)?;
183
184  writer.write_u32::<LittleEndian>(power)?;
185
186  Ok(())
187}
188
189pub(crate) fn write_points<G>(
190  mut writer: &mut impl Write,
191  points: Vec<G>,
192) -> Result<(), PtauFileError>
193where
194  G: halo2curves::serde::SerdeObject + CurveAffine,
195{
196  for point in points {
197    point.write_raw(&mut writer)?;
198  }
199  Ok(())
200}
201
202/// Save Ptau File
203pub fn write_ptau<G1, G2>(
204  mut writer: &mut (impl Write + Seek),
205  g1_points: Vec<G1>,
206  g2_points: Vec<G2>,
207  power: u32,
208) -> Result<(), PtauFileError>
209where
210  G1: halo2curves::serde::SerdeObject + CurveAffine,
211  G2: halo2curves::serde::SerdeObject + CurveAffine,
212{
213  write_header::<G1::Base>(&mut writer, power)?;
214
215  writer.write_u32::<LittleEndian>(0)?;
216  writer.write_i64::<LittleEndian>(0)?;
217
218  for id in 4..NUM_SECTIONS_FULL {
219    writer.write_u32::<LittleEndian>(id)?;
220    writer.write_i64::<LittleEndian>(0)?;
221  }
222
223  {
224    writer.write_u32::<LittleEndian>(2)?;
225    let pos = writer.stream_position()?;
226
227    writer.write_i64::<LittleEndian>(0)?;
228    let start = writer.stream_position()?;
229
230    write_points(writer, g1_points)?;
231
232    let size = writer.stream_position()? - start;
233
234    writer.seek(SeekFrom::Start(pos))?;
235    writer.write_i64::<LittleEndian>(size as i64)?;
236
237    writer.seek(SeekFrom::Current(size as i64))?;
238  }
239
240  {
241    writer.write_u32::<LittleEndian>(3)?;
242    let pos = writer.stream_position()?;
243
244    writer.write_i64::<LittleEndian>(0)?;
245    let start = writer.stream_position()?;
246
247    write_points(writer, g2_points)?;
248
249    let size = writer.stream_position()? - start;
250
251    writer.seek(SeekFrom::Start(pos))?;
252    writer.write_i64::<LittleEndian>(size as i64)?;
253  }
254  Ok(())
255}
256
257fn read_meta_data(reader: &mut (impl Read + Seek)) -> Result<MetaData, PtauFileError> {
258  {
259    let mut buf = [0u8; 4];
260    reader.read_exact(&mut buf)?;
261    if from_utf8(&buf)? != "ptau" {
262      return Err(PtauFileError::InvalidHead);
263    }
264  }
265  {
266    let version = reader.read_u32::<LittleEndian>()?;
267    if version != PTAU_VERSION {
268      return Err(PtauFileError::UnsupportedVersion(version));
269    }
270  }
271  let num_sections = {
272    let num_sections = reader.read_u32::<LittleEndian>()?;
273    // Accept both full (11 sections) and pruned (3 sections) ptau files
274    if num_sections != NUM_SECTIONS_FULL && num_sections != NUM_SECTIONS_PRUNED {
275      return Err(PtauFileError::InvalidNumSections(num_sections));
276    }
277    num_sections
278  };
279  let mut pos_header = 0;
280  let mut pos_tau_g1 = 0;
281  let mut pos_tau_g2 = 0;
282
283  for _ in 0..num_sections {
284    let id = reader.read_u32::<LittleEndian>()?;
285    let size = reader.read_i64::<LittleEndian>()?;
286
287    let pos = reader.stream_position()?;
288
289    match id {
290      1 => {
291        pos_header = pos;
292      }
293      2 => {
294        pos_tau_g1 = pos;
295      }
296      3 => {
297        pos_tau_g2 = pos;
298      }
299      _ => {}
300    };
301    reader.seek(SeekFrom::Current(size))?;
302  }
303
304  assert_ne!(pos_header, 0);
305  assert_ne!(pos_tau_g1, 0);
306  assert_ne!(pos_tau_g2, 0);
307
308  Ok(MetaData {
309    pos_header,
310    pos_tau_g1,
311    pos_tau_g2,
312  })
313}
314
315fn read_header<Base: PrimeField>(
316  reader: &mut impl Read,
317  num_g1: usize,
318  num_g2: usize,
319) -> Result<(), PtauFileError> {
320  // * n8
321  let n8 = reader.read_u32::<LittleEndian>()?;
322
323  // * prime
324  {
325    let mut buf = vec![0u8; n8 as usize];
326    reader.read_exact(&mut buf)?;
327
328    let modulus = BigUint::from_bytes_le(&buf);
329
330    let modulus_expected = BigUint::parse_bytes(&Base::MODULUS.as_bytes()[2..], 16).unwrap();
331
332    if modulus != modulus_expected {
333      return Err(PtauFileError::InvalidPrime(modulus));
334    }
335  }
336
337  // * power
338  let power = reader.read_u32::<LittleEndian>()?;
339
340  let max_num_g2 = 1 << power;
341  let max_num_g1 = max_num_g2 * 2 - 1;
342  if num_g1 > max_num_g1 {
343    return Err(PtauFileError::InsufficientPowerForG1 {
344      power,
345      required: max_num_g1,
346    });
347  }
348  if num_g2 > max_num_g2 {
349    return Err(PtauFileError::InsufficientPowerForG2 {
350      power,
351      required: max_num_g2,
352    });
353  }
354
355  Ok(())
356}
357
358pub(crate) fn read_points<G>(
359  mut reader: &mut impl Read,
360  num: usize,
361) -> Result<Vec<G>, PtauFileError>
362where
363  G: halo2curves::serde::SerdeObject + CurveAffine,
364{
365  let mut res = Vec::with_capacity(num);
366  for _ in 0..num {
367    res.push(G::read_raw(&mut reader)?);
368  }
369  Ok(res)
370}
371
372/// Load Ptau File
373pub fn read_ptau<G1, G2>(
374  mut reader: &mut (impl Read + Seek),
375  num_g1: usize,
376  num_g2: usize,
377) -> Result<(Vec<G1>, Vec<G2>), PtauFileError>
378where
379  G1: halo2curves::serde::SerdeObject + CurveAffine,
380  G2: halo2curves::serde::SerdeObject + CurveAffine,
381{
382  let metadata = read_meta_data(&mut reader)?;
383
384  reader.seek(SeekFrom::Start(metadata.pos_header))?;
385  read_header::<G1::Base>(reader, num_g1, num_g2)?;
386
387  reader.seek(SeekFrom::Start(metadata.pos_tau_g1))?;
388  let g1_points = read_points::<G1>(&mut reader, num_g1)?;
389
390  reader.seek(SeekFrom::Start(metadata.pos_tau_g2))?;
391  let g2_points = read_points::<G2>(&mut reader, num_g2)?;
392
393  Ok((g1_points, g2_points))
394}
395
396/// Check the sanity of the ptau file
397pub fn check_sanity_of_ptau_file<G1>(
398  path: impl AsRef<Path>,
399  num_g1: usize,
400  num_g2: usize,
401) -> Result<(), PtauFileError>
402where
403  G1: halo2curves::serde::SerdeObject + CurveAffine,
404{
405  let mut reader = File::open(path)?;
406
407  let metadata = read_meta_data(&mut reader)?;
408
409  reader.seek(SeekFrom::Start(metadata.pos_header))?;
410  read_header::<G1::Base>(&mut reader, num_g1, num_g2)
411}