Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ ruint = { version = "1.12.3", features = ["num-traits", "rand"] }
seq-macro = "0.3.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10.9"
sha2 = { version = "0.10.9", features = ["asm"] }
sha3 = "0.11.0-rc.3"
blake3 = "1.5.6"
test-case = "3.3.1"
tikv-jemallocator = "0.6"
toml = "0.8.8"
Expand Down Expand Up @@ -158,7 +160,7 @@ noirc_driver = { git = "https://github.com/noir-lang/noir", rev = "v1.0.0-beta.1
ark-bn254 = { version = "0.5.0", default-features = false, features = [
"scalar_field",
] }
ark-crypto-primitives = { version = "0.5", features = ["merkle_tree"] }
ark-crypto-primitives = { version = "0.5", features = ["merkle_tree", "parallel"] }
ark-ff = { version = "0.5", features = ["asm", "std"] }
ark-poly = "0.5"
ark-serialize = "0.5"
Expand Down
1 change: 1 addition & 0 deletions provekit/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ bytes.workspace = true
hex.workspace = true
itertools.workspace = true
postcard.workspace = true
rand.workspace = true
rand08.workspace = true
rayon.workspace = true
ruint.workspace = true
Expand Down
50 changes: 48 additions & 2 deletions provekit/common/src/file/bin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
super::BufExt as _,
crate::utils::human,
crate::{utils::human, HashConfig},
anyhow::{ensure, Context as _, Result},
bytes::{Buf, BufMut as _, Bytes, BytesMut},
serde::{Deserialize, Serialize},
Expand All @@ -12,8 +12,13 @@ use {
tracing::{info, instrument},
};

const HEADER_SIZE: usize = 20;
/// Header layout: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) =
/// 21 bytes
const HEADER_SIZE: usize = 21;
const MAGIC_BYTES: &[u8] = b"\xDC\xDFOZkp\x01\x00";
/// Byte offset where hash config is stored: MAGIC(8) + FORMAT(8) + MAJOR(2) +
/// MINOR(2) = 20
const HASH_CONFIG_OFFSET: usize = 20;

/// Zstd magic number: `28 B5 2F FD`.
const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
Expand All @@ -36,6 +41,7 @@ pub fn write_bin<T: Serialize>(
format: [u8; 8],
(major, minor): (u16, u16),
compression: Compression,
hash_config: Option<HashConfig>,
) -> Result<()> {
let postcard_data = postcard::to_allocvec(value).context("while encoding to postcard")?;
let uncompressed = postcard_data.len();
Expand All @@ -57,11 +63,14 @@ pub fn write_bin<T: Serialize>(

let mut file = File::create(path).context("while creating output file")?;

// Write header: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1)
let mut header = BytesMut::with_capacity(HEADER_SIZE);
header.put(MAGIC_BYTES);
header.put(&format[..]);
header.put_u16_le(major);
header.put_u16_le(minor);
header.put_u8(hash_config.map(|c| c.to_byte()).unwrap_or(0xff));

file.write_all(&header).context("while writing header")?;

file.write_all(&compressed_data)
Expand All @@ -84,6 +93,40 @@ pub fn write_bin<T: Serialize>(
Ok(())
}

/// Read just the hash_config from the file header (byte 20).
#[instrument(fields(size = path.metadata().map(|m| m.len()).ok()))]
pub fn read_hash_config(
path: &Path,
format: [u8; 8],
(major, minor): (u16, u16),
) -> Result<HashConfig> {
let mut file = File::open(path).context("while opening input file")?;

// Read header
let mut buffer = [0; HEADER_SIZE];
file.read_exact(&mut buffer)
.context("while reading header")?;
let mut header = Bytes::from_owner(buffer);

ensure!(
header.get_bytes::<8>() == MAGIC_BYTES,
"Invalid magic bytes"
);
ensure!(header.get_bytes::<8>() == format, "Invalid format");

let file_major = header.get_u16_le();
let file_minor = header.get_u16_le();

ensure!(file_major == major, "Incompatible format major version");
ensure!(file_minor >= minor, "Incompatible format minor version");

// Read hash_config at HASH_CONFIG_OFFSET (byte 20)
debug_assert_eq!(header.remaining(), HEADER_SIZE - HASH_CONFIG_OFFSET);
let hash_config_byte = header.get_u8();
HashConfig::from_byte(hash_config_byte)
.with_context(|| format!("Invalid hash config byte: 0x{:02X}", hash_config_byte))
}

/// Read a compressed binary file, auto-detecting zstd or XZ compression.
#[instrument(fields(size = path.metadata().map(|m| m.len()).ok()))]
pub fn read_bin<T: for<'a> Deserialize<'a>>(
Expand Down Expand Up @@ -111,6 +154,9 @@ pub fn read_bin<T: for<'a> Deserialize<'a>>(
"Incompatible format minor version"
);

// Skip hash_config byte (can be read separately via read_hash_config if needed)
let _hash_config_byte = header.get_u8();

let uncompressed = decompress_stream(&mut file)?;

postcard::from_bytes(&uncompressed).context("while decoding from postcard")
Expand Down
90 changes: 82 additions & 8 deletions provekit/common/src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ mod json;

use {
self::{
bin::{read_bin, write_bin, Compression},
bin::{read_bin, read_hash_config as read_hash_config_bin, write_bin, Compression},
buf_ext::BufExt,
counting_writer::CountingWriter,
json::{read_json, write_json},
},
crate::{NoirProof, NoirProofScheme, Prover, Verifier},
crate::{HashConfig, NoirProof, NoirProofScheme, Prover, Verifier},
anyhow::Result,
serde::{Deserialize, Serialize},
std::{ffi::OsStr, path::Path},
Expand All @@ -25,41 +25,81 @@ pub trait FileFormat: Serialize + for<'a> Deserialize<'a> {
const COMPRESSION: Compression;
}

/// Helper trait to optionally extract hash config.
pub(crate) trait MaybeHashAware {
fn maybe_hash_config(&self) -> Option<HashConfig>;
}

/// Impl for Prover (has hash config).
impl MaybeHashAware for Prover {
fn maybe_hash_config(&self) -> Option<HashConfig> {
match self {
Prover::Noir(p) => Some(p.hash_config),
Prover::Mavros(p) => Some(p.hash_config),
}
}
}

/// Impl for Verifier (has hash config).
impl MaybeHashAware for Verifier {
fn maybe_hash_config(&self) -> Option<HashConfig> {
Some(self.hash_config)
}
}

/// Impl for NoirProof (no hash config).
impl MaybeHashAware for NoirProof {
fn maybe_hash_config(&self) -> Option<HashConfig> {
None
}
}

/// Impl for NoirProofScheme (has hash config).
impl MaybeHashAware for NoirProofScheme {
fn maybe_hash_config(&self) -> Option<HashConfig> {
match self {
NoirProofScheme::Noir(d) => Some(d.hash_config),
NoirProofScheme::Mavros(d) => Some(d.hash_config),
}
}
}

impl FileFormat for NoirProofScheme {
const FORMAT: [u8; 8] = *b"NrProScm";
const EXTENSION: &'static str = "nps";
const VERSION: (u16, u16) = (1, 1);
const VERSION: (u16, u16) = (1, 2);
const COMPRESSION: Compression = Compression::Zstd;
}

impl FileFormat for Prover {
const FORMAT: [u8; 8] = *b"PrvKitPr";
const EXTENSION: &'static str = "pkp";
const VERSION: (u16, u16) = (1, 1);
const VERSION: (u16, u16) = (1, 2);
const COMPRESSION: Compression = Compression::Xz;
}

impl FileFormat for Verifier {
const FORMAT: [u8; 8] = *b"PrvKitVr";
const EXTENSION: &'static str = "pkv";
const VERSION: (u16, u16) = (1, 2);
const VERSION: (u16, u16) = (1, 3);
const COMPRESSION: Compression = Compression::Zstd;
}

impl FileFormat for NoirProof {
const FORMAT: [u8; 8] = *b"NPSProof";
const EXTENSION: &'static str = "np";
const VERSION: (u16, u16) = (1, 0);
const VERSION: (u16, u16) = (1, 1);
const COMPRESSION: Compression = Compression::Zstd;
}

/// Write a file with format determined from extension.
#[allow(private_bounds)]
#[instrument(skip(value))]
pub fn write<T: FileFormat>(value: &T, path: &Path) -> Result<()> {
pub fn write<T: FileFormat + MaybeHashAware>(value: &T, path: &Path) -> Result<()> {
match path.extension().and_then(OsStr::to_str) {
Some("json") => write_json(value, path),
Some(ext) if ext == T::EXTENSION => {
write_bin(value, path, T::FORMAT, T::VERSION, T::COMPRESSION)
write_bin_with_hash_config(value, path, T::FORMAT, T::VERSION, T::COMPRESSION)
}
_ => Err(anyhow::anyhow!(
"Unsupported file extension, please specify .{} or .json",
Expand All @@ -68,6 +108,19 @@ pub fn write<T: FileFormat>(value: &T, path: &Path) -> Result<()> {
}
}

/// Helper to write binary files with hash_config if T implements
/// MaybeHashAware.
fn write_bin_with_hash_config<T: FileFormat + MaybeHashAware>(
value: &T,
path: &Path,
format: [u8; 8],
version: (u16, u16),
compression: Compression,
) -> Result<()> {
let hash_config = value.maybe_hash_config();
write_bin(value, path, format, version, compression, hash_config)
}

/// Read a file with format determined from extension.
#[instrument()]
pub fn read<T: FileFormat>(path: &Path) -> Result<T> {
Expand All @@ -80,3 +133,24 @@ pub fn read<T: FileFormat>(path: &Path) -> Result<T> {
)),
}
}

/// Read just the hash configuration from a file.
#[instrument()]
pub fn read_hash_config<T: FileFormat>(path: &Path) -> Result<HashConfig> {
match path.extension().and_then(OsStr::to_str) {
Some("json") => {
// For JSON, parse and extract hash_config field (required)
let json_str = std::fs::read_to_string(path)?;
let value: serde_json::Value = serde_json::from_str(&json_str)?;
value
.get("hash_config")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.ok_or_else(|| anyhow::anyhow!("Missing hash_config field in JSON file"))
}
Some(ext) if ext == T::EXTENSION => read_hash_config_bin(path, T::FORMAT, T::VERSION),
_ => Err(anyhow::anyhow!(
"Unsupported file extension, please specify .{} or .json",
T::EXTENSION
)),
}
}
Loading
Loading