diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 7d2b76ef7..641899394 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,7 +11,9 @@ workspace = true binius-field = { path = "../field" } binius-utils = { path = "../utils" } bytemuck = { workspace = true } +bytes = { workspace = true } thiserror = { workspace = true } [dev-dependencies] proptest = { workspace = true } +rand = { workspace = true } diff --git a/crates/core/src/constraint_system.rs b/crates/core/src/constraint_system.rs index 198e77bbf..34f7c5b31 100644 --- a/crates/core/src/constraint_system.rs +++ b/crates/core/src/constraint_system.rs @@ -1,8 +1,12 @@ use std::{ + borrow::Cow, cmp, ops::{Index, IndexMut}, }; +use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes}; +use bytes::{Buf, BufMut}; + use crate::{consts, error::ConstraintSystemError, word::Word}; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] @@ -20,6 +24,21 @@ impl Default for ValueIndex { } } +impl SerializeBytes for ValueIndex { + fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> { + self.0.serialize(write_buf) + } +} + +impl DeserializeBytes for ValueIndex { + fn deserialize(read_buf: impl Buf) -> Result + where + Self: Sized, + { + Ok(ValueIndex(u32::deserialize(read_buf)?)) + } +} + /// A different variants of shifting a value. /// /// Note that there is no shift left arithmetic because it is redundant. @@ -33,6 +52,35 @@ pub enum ShiftVariant { Sar, } +impl SerializeBytes for ShiftVariant { + fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> { + let index = match self { + ShiftVariant::Sll => 0u8, + ShiftVariant::Slr => 1u8, + ShiftVariant::Sar => 2u8, + }; + index.serialize(write_buf) + } +} + +impl DeserializeBytes for ShiftVariant { + fn deserialize(read_buf: impl Buf) -> Result + where + Self: Sized, + { + let index = u8::deserialize(read_buf)?; + match index { + 0 => Ok(ShiftVariant::Sll), + 1 => Ok(ShiftVariant::Slr), + 2 => Ok(ShiftVariant::Sar), + _ => Err(SerializationError::UnknownEnumVariant { + name: "ShiftVariant", + index, + }), + } + } +} + #[derive(Copy, Clone, Debug)] pub struct ShiftedValueIndex { /// The index of this value in the input values vector `z`. @@ -84,6 +132,38 @@ impl ShiftedValueIndex { } } +impl SerializeBytes for ShiftedValueIndex { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + self.value_index.serialize(&mut write_buf)?; + self.shift_variant.serialize(&mut write_buf)?; + self.amount.serialize(write_buf) + } +} + +impl DeserializeBytes for ShiftedValueIndex { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let value_index = ValueIndex::deserialize(&mut read_buf)?; + let shift_variant = ShiftVariant::deserialize(&mut read_buf)?; + let amount = usize::deserialize(read_buf)?; + + // Validate that amount is within valid range + if amount >= 64 { + return Err(SerializationError::InvalidConstruction { + name: "ShiftedValueIndex::amount", + }); + } + + Ok(ShiftedValueIndex { + value_index, + shift_variant, + amount, + }) + } +} + pub type Operand = Vec; #[derive(Debug, Clone, Default)] @@ -119,6 +199,27 @@ impl AndConstraint { } } +impl SerializeBytes for AndConstraint { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + self.a.serialize(&mut write_buf)?; + self.b.serialize(&mut write_buf)?; + self.c.serialize(write_buf) + } +} + +impl DeserializeBytes for AndConstraint { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let a = Vec::::deserialize(&mut read_buf)?; + let b = Vec::::deserialize(&mut read_buf)?; + let c = Vec::::deserialize(read_buf)?; + + Ok(AndConstraint { a, b, c }) + } +} + #[derive(Debug, Clone, Default)] pub struct MulConstraint { pub a: Operand, @@ -127,6 +228,29 @@ pub struct MulConstraint { pub lo: Operand, } +impl SerializeBytes for MulConstraint { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + self.a.serialize(&mut write_buf)?; + self.b.serialize(&mut write_buf)?; + self.hi.serialize(&mut write_buf)?; + self.lo.serialize(write_buf) + } +} + +impl DeserializeBytes for MulConstraint { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let a = Vec::::deserialize(&mut read_buf)?; + let b = Vec::::deserialize(&mut read_buf)?; + let hi = Vec::::deserialize(&mut read_buf)?; + let lo = Vec::::deserialize(read_buf)?; + + Ok(MulConstraint { a, b, hi, lo }) + } +} + #[derive(Debug, Clone)] pub struct ConstraintSystem { pub value_vec_layout: ValueVecLayout, @@ -135,6 +259,11 @@ pub struct ConstraintSystem { pub mul_constraints: Vec, } +impl ConstraintSystem { + /// Serialization format version for compatibility checking + pub const SERIALIZATION_VERSION: u32 = 1; +} + impl ConstraintSystem { pub fn new( constants: Vec, @@ -201,6 +330,49 @@ impl ConstraintSystem { } } +impl SerializeBytes for ConstraintSystem { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?; + + self.value_vec_layout.serialize(&mut write_buf)?; + self.constants.serialize(&mut write_buf)?; + self.and_constraints.serialize(&mut write_buf)?; + self.mul_constraints.serialize(write_buf) + } +} + +impl DeserializeBytes for ConstraintSystem { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let version = u32::deserialize(&mut read_buf)?; + if version != Self::SERIALIZATION_VERSION { + return Err(SerializationError::InvalidConstruction { + name: "ConstraintSystem::version", + }); + } + + let value_vec_layout = ValueVecLayout::deserialize(&mut read_buf)?; + let constants = Vec::::deserialize(&mut read_buf)?; + let and_constraints = Vec::::deserialize(&mut read_buf)?; + let mul_constraints = Vec::::deserialize(read_buf)?; + + if constants.len() != value_vec_layout.n_const { + return Err(SerializationError::InvalidConstruction { + name: "ConstraintSystem::constants", + }); + } + + Ok(ConstraintSystem { + value_vec_layout, + constants, + and_constraints, + mul_constraints, + }) + } +} + /// Description of a layout of the value vector for a particular circuit. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ValueVecLayout { @@ -248,6 +420,43 @@ impl ValueVecLayout { } } +impl SerializeBytes for ValueVecLayout { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + self.n_const.serialize(&mut write_buf)?; + self.n_inout.serialize(&mut write_buf)?; + self.n_witness.serialize(&mut write_buf)?; + self.n_internal.serialize(&mut write_buf)?; + self.offset_inout.serialize(&mut write_buf)?; + self.offset_witness.serialize(&mut write_buf)?; + self.total_len.serialize(write_buf) + } +} + +impl DeserializeBytes for ValueVecLayout { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let n_const = usize::deserialize(&mut read_buf)?; + let n_inout = usize::deserialize(&mut read_buf)?; + let n_witness = usize::deserialize(&mut read_buf)?; + let n_internal = usize::deserialize(&mut read_buf)?; + let offset_inout = usize::deserialize(&mut read_buf)?; + let offset_witness = usize::deserialize(&mut read_buf)?; + let total_len = usize::deserialize(read_buf)?; + + Ok(ValueVecLayout { + n_const, + n_inout, + n_witness, + n_internal, + offset_inout, + offset_witness, + total_len, + }) + } +} + /// The vector of values. /// /// This is a prover-only structure. @@ -268,6 +477,25 @@ impl ValueVec { } } + pub fn new_from_data( + layout: ValueVecLayout, + mut public: Vec, + private: Vec, + ) -> Result { + public.extend_from_slice(&private); + if public.len() != layout.total_len { + return Err(ConstraintSystemError::ValueVecLenMismatch { + expected: layout.total_len, + actual: public.len(), + }); + } + + Ok(ValueVec { + layout, + data: public, + }) + } + /// The total size of the vector. pub fn size(&self) -> usize { self.data.len() @@ -286,6 +514,11 @@ impl ValueVec { &self.data[..self.layout.offset_witness] } + /// Return all non-public values (witness + internal). + pub fn non_public(&self) -> &[Word] { + &self.data[self.layout.offset_witness..] + } + /// Returns the witness portion of the values vector. pub fn witness(&self) -> &[Word] { let start = self.layout.offset_witness; @@ -312,3 +545,929 @@ impl IndexMut for ValueVec { &mut self.data[index.0 as usize] } } + +/// Values data for zero-knowledge proofs (either public witness or non-public part - private inputs +/// and internal values). +/// +/// It uses `Cow<[Word]>` to avoid unnecessary clones while supporting +/// both borrowed and owned data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValuesData<'a> { + data: Cow<'a, [Word]>, +} + +impl<'a> ValuesData<'a> { + /// Serialization format version for compatibility checking + pub const SERIALIZATION_VERSION: u32 = 1; + + /// Create a new ValuesData from borrowed data + pub fn borrowed(data: &'a [Word]) -> Self { + Self { + data: Cow::Borrowed(data), + } + } + + /// Create a new ValuesData from owned data + pub fn owned(data: Vec) -> Self { + Self { + data: Cow::Owned(data), + } + } + + /// Get the values data as a slice + pub fn as_slice(&self) -> &[Word] { + &self.data + } + + /// Get the number of words in the values data + pub fn len(&self) -> usize { + self.data.len() + } + + /// Check if the witness is empty + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Convert to owned data, consuming self + pub fn into_owned(self) -> Vec { + self.data.into_owned() + } + + /// Convert to owned version of ValuesData + pub fn to_owned(&self) -> ValuesData<'static> { + ValuesData { + data: Cow::Owned(self.data.to_vec()), + } + } +} + +impl<'a> SerializeBytes for ValuesData<'a> { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?; + + self.data.as_ref().serialize(write_buf) + } +} + +impl DeserializeBytes for ValuesData<'static> { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let version = u32::deserialize(&mut read_buf)?; + if version != Self::SERIALIZATION_VERSION { + return Err(SerializationError::InvalidConstruction { + name: "Witness::version", + }); + } + + let data = Vec::::deserialize(read_buf)?; + + Ok(ValuesData::owned(data)) + } +} + +impl<'a> From<&'a [Word]> for ValuesData<'a> { + fn from(data: &'a [Word]) -> Self { + ValuesData::borrowed(data) + } +} + +impl From> for ValuesData<'static> { + fn from(data: Vec) -> Self { + ValuesData::owned(data) + } +} + +impl<'a> AsRef<[Word]> for ValuesData<'a> { + fn as_ref(&self) -> &[Word] { + self.as_slice() + } +} + +impl<'a> std::ops::Deref for ValuesData<'a> { + type Target = [Word]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl<'a> From> for Vec { + fn from(value: ValuesData<'a>) -> Self { + value.into_owned() + } +} + +/// A zero-knowledge proof that can be serialized for cross-host verification. +/// +/// This structure contains the complete proof transcript generated by the prover, +/// along with information about the challenger type needed for verification. +/// The proof data represents the Fiat-Shamir transcript that can be deserialized +/// by the verifier to recreate the interactive protocol. +/// +/// # Design +/// +/// The proof contains: +/// - `data`: The actual proof transcript as bytes (zero-copy with Cow) +/// - `challenger_type`: String identifying the challenger used (e.g., `"HasherChallenger"`) +/// +/// This enables complete cross-host verification where a proof generated on one +/// machine can be serialized, transmitted, and verified on another machine with +/// the correct challenger configuration. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Proof<'a> { + data: Cow<'a, [u8]>, + challenger_type: String, +} + +impl<'a> Proof<'a> { + /// Serialization format version for compatibility checking + pub const SERIALIZATION_VERSION: u32 = 1; + + /// Create a new Proof from borrowed transcript data + pub fn borrowed(data: &'a [u8], challenger_type: String) -> Self { + Self { + data: Cow::Borrowed(data), + challenger_type, + } + } + + /// Create a new Proof from owned transcript data + pub fn owned(data: Vec, challenger_type: String) -> Self { + Self { + data: Cow::Owned(data), + challenger_type, + } + } + + /// Get the proof transcript data as a slice + pub fn as_slice(&self) -> &[u8] { + &self.data + } + + /// Get the challenger type identifier + pub fn challenger_type(&self) -> &str { + &self.challenger_type + } + + /// Get the number of bytes in the proof transcript + pub fn len(&self) -> usize { + self.data.len() + } + + /// Check if the proof transcript is empty + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Convert to owned data, consuming self + pub fn into_owned(self) -> (Vec, String) { + (self.data.into_owned(), self.challenger_type) + } + + /// Convert to owned version of Proof + pub fn to_owned(&self) -> Proof<'static> { + Proof { + data: Cow::Owned(self.data.to_vec()), + challenger_type: self.challenger_type.clone(), + } + } +} + +impl<'a> SerializeBytes for Proof<'a> { + fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { + Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?; + + self.challenger_type.serialize(&mut write_buf)?; + + self.data.as_ref().serialize(write_buf) + } +} + +impl DeserializeBytes for Proof<'static> { + fn deserialize(mut read_buf: impl Buf) -> Result + where + Self: Sized, + { + let version = u32::deserialize(&mut read_buf)?; + if version != Self::SERIALIZATION_VERSION { + return Err(SerializationError::InvalidConstruction { + name: "Proof::version", + }); + } + + let challenger_type = String::deserialize(&mut read_buf)?; + let data = Vec::::deserialize(read_buf)?; + + Ok(Proof::owned(data, challenger_type)) + } +} + +impl<'a> From<(&'a [u8], String)> for Proof<'a> { + fn from((data, challenger_type): (&'a [u8], String)) -> Self { + Proof::borrowed(data, challenger_type) + } +} + +impl From<(Vec, String)> for Proof<'static> { + fn from((data, challenger_type): (Vec, String)) -> Self { + Proof::owned(data, challenger_type) + } +} + +impl<'a> AsRef<[u8]> for Proof<'a> { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +impl<'a> std::ops::Deref for Proof<'a> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +#[cfg(test)] +mod serialization_tests { + use rand::{RngCore, SeedableRng, rngs::StdRng}; + + use super::*; + + pub(crate) fn create_test_constraint_system() -> ConstraintSystem { + let constants = vec![ + Word::from_u64(1), + Word::from_u64(42), + Word::from_u64(0xDEADBEEF), + ]; + + let value_vec_layout = ValueVecLayout { + n_const: 3, + n_inout: 2, + n_witness: 10, + n_internal: 3, + offset_inout: 4, // Must be power of 2 and >= n_const + offset_witness: 8, // Must be power of 2 and >= offset_inout + n_inout + total_len: 16, // Must be power of 2 and >= offset_witness + n_witness + }; + + let and_constraints = vec![ + AndConstraint::plain_abc( + vec![ValueIndex(0), ValueIndex(1)], + vec![ValueIndex(2)], + vec![ValueIndex(3), ValueIndex(4)], + ), + AndConstraint::abc( + vec![ShiftedValueIndex::sll(ValueIndex(0), 5)], + vec![ShiftedValueIndex::srl(ValueIndex(1), 10)], + vec![ShiftedValueIndex::sar(ValueIndex(2), 15)], + ), + ]; + + let mul_constraints = vec![MulConstraint { + a: vec![ShiftedValueIndex::plain(ValueIndex(0))], + b: vec![ShiftedValueIndex::plain(ValueIndex(1))], + hi: vec![ShiftedValueIndex::plain(ValueIndex(2))], + lo: vec![ShiftedValueIndex::plain(ValueIndex(3))], + }]; + + ConstraintSystem::new(constants, value_vec_layout, and_constraints, mul_constraints) + } + + #[test] + fn test_word_serialization_round_trip() { + let mut rng = StdRng::seed_from_u64(0); + let word = Word::from_u64(rng.next_u64()); + + let mut buf = Vec::new(); + word.serialize(&mut buf).unwrap(); + + let deserialized = Word::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(word, deserialized); + } + + #[test] + fn test_shift_variant_serialization_round_trip() { + let variants = [ShiftVariant::Sll, ShiftVariant::Slr, ShiftVariant::Sar]; + + for variant in variants { + let mut buf = Vec::new(); + variant.serialize(&mut buf).unwrap(); + + let deserialized = ShiftVariant::deserialize(&mut buf.as_slice()).unwrap(); + match (variant, deserialized) { + (ShiftVariant::Sll, ShiftVariant::Sll) + | (ShiftVariant::Slr, ShiftVariant::Slr) + | (ShiftVariant::Sar, ShiftVariant::Sar) => {} + _ => panic!("ShiftVariant round trip failed: {:?} != {:?}", variant, deserialized), + } + } + } + + #[test] + fn test_shift_variant_unknown_variant() { + // Create invalid variant index + let mut buf = Vec::new(); + 255u8.serialize(&mut buf).unwrap(); + + let result = ShiftVariant::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::UnknownEnumVariant { name, index } => { + assert_eq!(name, "ShiftVariant"); + assert_eq!(index, 255); + } + _ => panic!("Expected UnknownEnumVariant error"), + } + } + + #[test] + fn test_value_index_serialization_round_trip() { + let value_index = ValueIndex(12345); + + let mut buf = Vec::new(); + value_index.serialize(&mut buf).unwrap(); + + let deserialized = ValueIndex::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(value_index, deserialized); + } + + #[test] + fn test_shifted_value_index_serialization_round_trip() { + let shifted_value_index = ShiftedValueIndex::srl(ValueIndex(42), 23); + + let mut buf = Vec::new(); + shifted_value_index.serialize(&mut buf).unwrap(); + + let deserialized = ShiftedValueIndex::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(shifted_value_index.value_index, deserialized.value_index); + assert_eq!(shifted_value_index.amount, deserialized.amount); + match (shifted_value_index.shift_variant, deserialized.shift_variant) { + (ShiftVariant::Slr, ShiftVariant::Slr) => {} + _ => panic!("ShiftVariant mismatch"), + } + } + + #[test] + fn test_shifted_value_index_invalid_amount() { + // Create a buffer with invalid shift amount (>= 64) + let mut buf = Vec::new(); + ValueIndex(0).serialize(&mut buf).unwrap(); + ShiftVariant::Sll.serialize(&mut buf).unwrap(); + 64usize.serialize(&mut buf).unwrap(); // Invalid amount + + let result = ShiftedValueIndex::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "ShiftedValueIndex::amount"); + } + _ => panic!("Expected InvalidConstruction error"), + } + } + + #[test] + fn test_and_constraint_serialization_round_trip() { + let constraint = AndConstraint::abc( + vec![ShiftedValueIndex::sll(ValueIndex(1), 5)], + vec![ShiftedValueIndex::srl(ValueIndex(2), 10)], + vec![ + ShiftedValueIndex::sar(ValueIndex(3), 15), + ShiftedValueIndex::plain(ValueIndex(4)), + ], + ); + + let mut buf = Vec::new(); + constraint.serialize(&mut buf).unwrap(); + + let deserialized = AndConstraint::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(constraint.a.len(), deserialized.a.len()); + assert_eq!(constraint.b.len(), deserialized.b.len()); + assert_eq!(constraint.c.len(), deserialized.c.len()); + + for (orig, deser) in constraint.a.iter().zip(deserialized.a.iter()) { + assert_eq!(orig.value_index, deser.value_index); + assert_eq!(orig.amount, deser.amount); + } + } + + #[test] + fn test_mul_constraint_serialization_round_trip() { + let constraint = MulConstraint { + a: vec![ShiftedValueIndex::plain(ValueIndex(0))], + b: vec![ShiftedValueIndex::srl(ValueIndex(1), 32)], + hi: vec![ShiftedValueIndex::plain(ValueIndex(2))], + lo: vec![ShiftedValueIndex::plain(ValueIndex(3))], + }; + + let mut buf = Vec::new(); + constraint.serialize(&mut buf).unwrap(); + + let deserialized = MulConstraint::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(constraint.a.len(), deserialized.a.len()); + assert_eq!(constraint.b.len(), deserialized.b.len()); + assert_eq!(constraint.hi.len(), deserialized.hi.len()); + assert_eq!(constraint.lo.len(), deserialized.lo.len()); + } + + #[test] + fn test_value_vec_layout_serialization_round_trip() { + let layout = ValueVecLayout { + n_const: 5, + n_inout: 3, + n_witness: 12, + n_internal: 7, + offset_inout: 8, + offset_witness: 16, + total_len: 32, + }; + + let mut buf = Vec::new(); + layout.serialize(&mut buf).unwrap(); + + let deserialized = ValueVecLayout::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(layout, deserialized); + } + + #[test] + fn test_constraint_system_serialization_round_trip() { + let original = create_test_constraint_system(); + + let mut buf = Vec::new(); + original.serialize(&mut buf).unwrap(); + + let deserialized = ConstraintSystem::deserialize(&mut buf.as_slice()).unwrap(); + + // Check version + assert_eq!(ConstraintSystem::SERIALIZATION_VERSION, 1); + + // Check value_vec_layout + assert_eq!(original.value_vec_layout, deserialized.value_vec_layout); + + // Check constants + assert_eq!(original.constants.len(), deserialized.constants.len()); + for (orig, deser) in original.constants.iter().zip(deserialized.constants.iter()) { + assert_eq!(orig, deser); + } + + // Check and_constraints + assert_eq!(original.and_constraints.len(), deserialized.and_constraints.len()); + + // Check mul_constraints + assert_eq!(original.mul_constraints.len(), deserialized.mul_constraints.len()); + } + + #[test] + fn test_constraint_system_version_mismatch() { + // Create a buffer with wrong version + let mut buf = Vec::new(); + 999u32.serialize(&mut buf).unwrap(); // Wrong version + + let result = ConstraintSystem::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "ConstraintSystem::version"); + } + _ => panic!("Expected InvalidConstruction error"), + } + } + + #[test] + fn test_constraint_system_constants_length_mismatch() { + // Create valid components but with mismatched constants length + let value_vec_layout = ValueVecLayout { + n_const: 5, // Expect 5 constants + n_inout: 2, + n_witness: 10, + n_internal: 3, + offset_inout: 8, + offset_witness: 16, + total_len: 32, + }; + + let constants = vec![Word::from_u64(1), Word::from_u64(2)]; // Only 2 constants + let and_constraints: Vec = vec![]; + let mul_constraints: Vec = vec![]; + + // Serialize components manually + let mut buf = Vec::new(); + ConstraintSystem::SERIALIZATION_VERSION + .serialize(&mut buf) + .unwrap(); + value_vec_layout.serialize(&mut buf).unwrap(); + constants.serialize(&mut buf).unwrap(); + and_constraints.serialize(&mut buf).unwrap(); + mul_constraints.serialize(&mut buf).unwrap(); + + let result = ConstraintSystem::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "ConstraintSystem::constants"); + } + _ => panic!("Expected InvalidConstruction error"), + } + } + + #[test] + fn test_serialization_with_different_sources() { + let original = create_test_constraint_system(); + + // Test with Vec (memory buffer) + let mut vec_buf = Vec::new(); + original.serialize(&mut vec_buf).unwrap(); + let deserialized1 = ConstraintSystem::deserialize(&mut vec_buf.as_slice()).unwrap(); + assert_eq!(original.constants.len(), deserialized1.constants.len()); + + // Test with bytes::BytesMut (another common buffer type) + let mut bytes_buf = bytes::BytesMut::new(); + original.serialize(&mut bytes_buf).unwrap(); + let deserialized2 = ConstraintSystem::deserialize(bytes_buf.freeze()).unwrap(); + assert_eq!(original.constants.len(), deserialized2.constants.len()); + } + + /// Helper function to create or update the reference binary file for version compatibility + /// testing. This is not run automatically but can be used to regenerate the reference file + /// when needed. + #[test] + #[ignore] // Use `cargo test -- --ignored create_reference_binary` to run this + fn create_reference_binary_file() { + let constraint_system = create_test_constraint_system(); + + // Serialize to binary data + let mut buf = Vec::new(); + constraint_system.serialize(&mut buf).unwrap(); + + // Write to reference file + let test_data_path = std::path::Path::new("crates/core/test_data/constraint_system_v1.bin"); + + // Create directory if it doesn't exist + if let Some(parent) = test_data_path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + + std::fs::write(test_data_path, &buf).unwrap(); + + println!("Created reference binary file at: {:?}", test_data_path); + println!("Binary data length: {} bytes", buf.len()); + } + + /// Test deserialization from a reference binary file to ensure version compatibility. + /// This test will fail if breaking changes are made without incrementing the version. + #[test] + fn test_deserialize_from_reference_binary_file() { + let binary_data = include_bytes!("../test_data/constraint_system_v1.bin"); + + let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap(); + + assert_eq!(deserialized.value_vec_layout.n_const, 3); + assert_eq!(deserialized.value_vec_layout.n_inout, 2); + assert_eq!(deserialized.value_vec_layout.n_witness, 10); + assert_eq!(deserialized.value_vec_layout.n_internal, 3); + assert_eq!(deserialized.value_vec_layout.offset_inout, 4); + assert_eq!(deserialized.value_vec_layout.offset_witness, 8); + assert_eq!(deserialized.value_vec_layout.total_len, 16); + + assert_eq!(deserialized.constants.len(), 3); + assert_eq!(deserialized.constants[0].as_u64(), 1); + assert_eq!(deserialized.constants[1].as_u64(), 42); + assert_eq!(deserialized.constants[2].as_u64(), 0xDEADBEEF); + + assert_eq!(deserialized.and_constraints.len(), 2); + assert_eq!(deserialized.mul_constraints.len(), 1); + + // Verify that the version is what we expect + // This is implicitly checked during deserialization, but we can also verify + // the file starts with the correct version bytes + let version_bytes = &binary_data[0..4]; // First 4 bytes should be version + let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian + assert_eq!( + version_bytes, expected_version_bytes, + "Binary file version mismatch. If you made breaking changes, increment ConstraintSystem::SERIALIZATION_VERSION" + ); + } + + #[test] + fn test_witness_serialization_round_trip_owned() { + let data = vec![ + Word::from_u64(1), + Word::from_u64(42), + Word::from_u64(0xDEADBEEF), + Word::from_u64(0x1234567890ABCDEF), + ]; + let witness = ValuesData::owned(data.clone()); + + let mut buf = Vec::new(); + witness.serialize(&mut buf).unwrap(); + + let deserialized = ValuesData::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(witness, deserialized); + assert_eq!(deserialized.as_slice(), data.as_slice()); + } + + #[test] + fn test_witness_serialization_round_trip_borrowed() { + let data = vec![Word::from_u64(123), Word::from_u64(456)]; + let witness = ValuesData::borrowed(&data); + + let mut buf = Vec::new(); + witness.serialize(&mut buf).unwrap(); + + let deserialized = ValuesData::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(witness, deserialized); + assert_eq!(deserialized.as_slice(), data.as_slice()); + } + + #[test] + fn test_witness_version_mismatch() { + let mut buf = Vec::new(); + 999u32.serialize(&mut buf).unwrap(); // Wrong version + vec![Word::from_u64(1)].serialize(&mut buf).unwrap(); // Some data + + let result = ValuesData::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "Witness::version"); + } + _ => panic!("Expected version mismatch error"), + } + } + + /// Helper function to create or update the reference binary file for Witness version + /// compatibility testing. + #[test] + #[ignore] // Use `cargo test -- --ignored create_witness_reference_binary` to run this + fn create_witness_reference_binary_file() { + let data = vec![ + Word::from_u64(1), + Word::from_u64(42), + Word::from_u64(0xDEADBEEF), + Word::from_u64(0x1234567890ABCDEF), + ]; + let witness = ValuesData::owned(data); + + let mut buf = Vec::new(); + witness.serialize(&mut buf).unwrap(); + + let test_data_path = std::path::Path::new("crates/core/test_data/witness_v1.bin"); + + if let Some(parent) = test_data_path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + + std::fs::write(test_data_path, &buf).unwrap(); + + println!("Created Witness reference binary file at: {:?}", test_data_path); + println!("Binary data length: {} bytes", buf.len()); + } + + /// Test deserialization from a reference binary file to ensure Witness version + /// compatibility. This test will fail if breaking changes are made without incrementing the + /// version. + #[test] + fn test_witness_deserialize_from_reference_binary_file() { + let binary_data = include_bytes!("../test_data/witness_v1.bin"); + + let deserialized = ValuesData::deserialize(&mut binary_data.as_slice()).unwrap(); + + assert_eq!(deserialized.len(), 4); + assert_eq!(deserialized.as_slice()[0].as_u64(), 1); + assert_eq!(deserialized.as_slice()[1].as_u64(), 42); + assert_eq!(deserialized.as_slice()[2].as_u64(), 0xDEADBEEF); + assert_eq!(deserialized.as_slice()[3].as_u64(), 0x1234567890ABCDEF); + + // Verify that the version is what we expect + // This is implicitly checked during deserialization, but we can also verify + // the file starts with the correct version bytes + let version_bytes = &binary_data[0..4]; // First 4 bytes should be version + let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian + assert_eq!( + version_bytes, expected_version_bytes, + "WitnessData binary file version mismatch. If you made breaking changes, increment WitnessData::SERIALIZATION_VERSION" + ); + } + + #[test] + fn test_proof_serialization_round_trip_owned() { + let transcript_data = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]; + let challenger_type = "HasherChallenger".to_string(); + let proof = Proof::owned(transcript_data.clone(), challenger_type.clone()); + + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(proof, deserialized); + assert_eq!(deserialized.as_slice(), transcript_data.as_slice()); + assert_eq!(deserialized.challenger_type(), &challenger_type); + } + + #[test] + fn test_proof_serialization_round_trip_borrowed() { + let transcript_data = vec![0xAA, 0xBB, 0xCC, 0xDD]; + let challenger_type = "TestChallenger".to_string(); + let proof = Proof::borrowed(&transcript_data, challenger_type.clone()); + + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(proof, deserialized); + assert_eq!(deserialized.as_slice(), transcript_data.as_slice()); + assert_eq!(deserialized.challenger_type(), &challenger_type); + } + + #[test] + fn test_proof_empty_transcript() { + let proof = Proof::owned(vec![], "EmptyProof".to_string()); + assert!(proof.is_empty()); + assert_eq!(proof.len(), 0); + + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(proof, deserialized); + assert!(deserialized.is_empty()); + } + + #[test] + fn test_proof_large_transcript() { + let mut rng = StdRng::seed_from_u64(12345); + let mut large_data = vec![0u8; 10000]; + rng.fill_bytes(&mut large_data); + + let challenger_type = "LargeProofChallenger".to_string(); + let proof = Proof::owned(large_data.clone(), challenger_type.clone()); + + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(proof, deserialized); + assert_eq!(deserialized.len(), 10000); + assert_eq!(deserialized.challenger_type(), &challenger_type); + } + + #[test] + fn test_proof_version_mismatch() { + let mut buf = Vec::new(); + 999u32.serialize(&mut buf).unwrap(); // Wrong version + "TestChallenger".serialize(&mut buf).unwrap(); // Some challenger type + vec![0xAAu8].serialize(&mut buf).unwrap(); // Some data + + let result = Proof::deserialize(&mut buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "Proof::version"); + } + _ => panic!("Expected version mismatch error"), + } + } + + #[test] + fn test_proof_into_owned() { + let original_data = vec![1, 2, 3, 4, 5]; + let original_challenger = "TestChallenger".to_string(); + let proof = Proof::owned(original_data.clone(), original_challenger.clone()); + + let (data, challenger_type) = proof.into_owned(); + assert_eq!(data, original_data); + assert_eq!(challenger_type, original_challenger); + } + + #[test] + fn test_proof_to_owned() { + let data = vec![0xFF, 0xEE, 0xDD]; + let challenger_type = "BorrowedChallenger".to_string(); + let borrowed_proof = Proof::borrowed(&data, challenger_type.clone()); + + let owned_proof = borrowed_proof.to_owned(); + assert_eq!(owned_proof.as_slice(), data); + assert_eq!(owned_proof.challenger_type(), &challenger_type); + // Verify it's truly owned (not just borrowed) + drop(data); // This would fail if owned_proof was still borrowing + assert_eq!(owned_proof.len(), 3); + } + + #[test] + fn test_proof_different_challenger_types() { + let data = vec![0x42]; + let challengers = vec![ + "HasherChallenger".to_string(), + "HasherChallenger".to_string(), + "CustomChallenger".to_string(), + "".to_string(), // Empty string should also work + ]; + + for challenger_type in challengers { + let proof = Proof::owned(data.clone(), challenger_type.clone()); + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(deserialized.challenger_type(), &challenger_type); + } + } + + #[test] + fn test_proof_serialization_with_different_sources() { + let transcript_data = vec![0x11, 0x22, 0x33, 0x44]; + let challenger_type = "MultiSourceChallenger".to_string(); + let original = Proof::owned(transcript_data, challenger_type); + + // Test with Vec (memory buffer) + let mut vec_buf = Vec::new(); + original.serialize(&mut vec_buf).unwrap(); + let deserialized1 = Proof::deserialize(&mut vec_buf.as_slice()).unwrap(); + assert_eq!(original, deserialized1); + + // Test with bytes::BytesMut (another common buffer type) + let mut bytes_buf = bytes::BytesMut::new(); + original.serialize(&mut bytes_buf).unwrap(); + let deserialized2 = Proof::deserialize(bytes_buf.freeze()).unwrap(); + assert_eq!(original, deserialized2); + } + + /// Helper function to create or update the reference binary file for Proof version + /// compatibility testing. + #[test] + #[ignore] // Use `cargo test -- --ignored create_proof_reference_binary` to run this + fn create_proof_reference_binary_file() { + let transcript_data = vec![ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, + 0x32, 0x10, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, + ]; + let challenger_type = "HasherChallenger".to_string(); + let proof = Proof::owned(transcript_data, challenger_type); + + let mut buf = Vec::new(); + proof.serialize(&mut buf).unwrap(); + + let test_data_path = std::path::Path::new("crates/core/test_data/proof_v1.bin"); + + if let Some(parent) = test_data_path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + + std::fs::write(test_data_path, &buf).unwrap(); + + println!("Created Proof reference binary file at: {:?}", test_data_path); + println!("Binary data length: {} bytes", buf.len()); + } + + /// Test deserialization from a reference binary file to ensure Proof version + /// compatibility. This test will fail if breaking changes are made without incrementing the + /// version. + #[test] + fn test_proof_deserialize_from_reference_binary_file() { + let binary_data = include_bytes!("../test_data/proof_v1.bin"); + + let deserialized = Proof::deserialize(&mut binary_data.as_slice()).unwrap(); + + assert_eq!(deserialized.len(), 24); // 24 bytes of transcript data + assert_eq!(deserialized.challenger_type(), "HasherChallenger"); + + let expected_data = vec![ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, + 0x32, 0x10, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, + ]; + assert_eq!(deserialized.as_slice(), expected_data); + + // Verify that the version is what we expect + // This is implicitly checked during deserialization, but we can also verify + // the file starts with the correct version bytes + let version_bytes = &binary_data[0..4]; // First 4 bytes should be version + let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian + assert_eq!( + version_bytes, expected_version_bytes, + "Proof binary file version mismatch. If you made breaking changes, increment Proof::SERIALIZATION_VERSION" + ); + } + + #[test] + fn split_values_vec_and_combine() { + let values = ValueVec::new(ValueVecLayout { + n_const: 2, + n_inout: 2, + n_witness: 2, + n_internal: 2, + offset_inout: 2, + offset_witness: 4, + total_len: 8, + }); + + let public = values.public(); + let non_public = values.non_public(); + let combined = + ValueVec::new_from_data(values.layout.clone(), public.to_vec(), non_public.to_vec()) + .unwrap(); + assert_eq!(combined.combined_witness(), values.combined_witness()); + } +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index b7618595f..b23a56e1b 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -10,4 +10,6 @@ pub enum ConstraintSystemError { "the public input segment must be at least {MIN_WORDS_PER_SEGMENT} words, got: {pub_input_size}" )] PublicInputTooShort { pub_input_size: usize }, + #[error("the data length doesn't match layout")] + ValueVecLenMismatch { expected: usize, actual: usize }, } diff --git a/crates/core/src/word.rs b/crates/core/src/word.rs index b5d511107..494521ead 100644 --- a/crates/core/src/word.rs +++ b/crates/core/src/word.rs @@ -3,6 +3,9 @@ use std::{ ops::{BitAnd, BitOr, BitXor, Not, Shl, Shr}, }; +use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes}; +use bytes::{Buf, BufMut}; + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Word(pub u64); @@ -195,6 +198,21 @@ impl Word { } } +impl SerializeBytes for Word { + fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> { + self.0.serialize(write_buf) + } +} + +impl DeserializeBytes for Word { + fn deserialize(read_buf: impl Buf) -> Result + where + Self: Sized, + { + Ok(Word(u64::deserialize(read_buf)?)) + } +} + #[cfg(test)] mod tests { use proptest::prelude::*; diff --git a/crates/core/test_data/README.md b/crates/core/test_data/README.md new file mode 100644 index 000000000..21dd2c97d --- /dev/null +++ b/crates/core/test_data/README.md @@ -0,0 +1,72 @@ +# Test Data for Serialization Compatibility + +This directory contains binary reference files used for testing serialization format compatibility. + +## Files + +- `constraint_system_v1.bin`: Reference binary serialization of a `ConstraintSystem` using serialization version 1. +- `public_witness_v1.bin`: Reference binary serialization of a `PublicWitness` using serialization version 1. +- `proof_v1.bin`: Reference binary serialization of a `Proof` using serialization version 1. + +## Purpose + +These binary files serve as regression tests to ensure that: + +1. **Backward compatibility**: Future changes to the serialization format don't accidentally break the ability to deserialize existing data. + +2. **Version enforcement**: If breaking changes are made to the serialization format, developers are forced to increment the `SERIALIZATION_VERSION` constant, which will cause the compatibility tests to fail until the version is updated. + +3. **Format validation**: The tests verify both the structure and content of deserialized data to ensure the format remains consistent. + +## Updating Reference Files + +If you make intentional breaking changes to the serialization format: + +### For ConstraintSystem +1. Increment `ConstraintSystem::SERIALIZATION_VERSION` +2. Run the ignored test to regenerate the reference file: + ```bash + cargo test -p binius-core -- --ignored create_reference_binary + ``` +3. Rename the new file to include the new version number +4. Update test paths to reference the new file + +### For PublicWitness +1. Increment `PublicWitness::SERIALIZATION_VERSION` +2. Run the ignored test to regenerate the reference file: + ```bash + cargo test -p binius-core -- --ignored create_public_witness_reference_binary + ``` +3. Rename the new file to include the new version number +4. Update test paths to reference the new file + +### For Proof +1. Increment `Proof::SERIALIZATION_VERSION` +2. Run the ignored test to regenerate the reference file: + ```bash + cargo test -p binius-core -- --ignored create_proof_reference_binary + ``` +3. Rename the new file to include the new version number +4. Update test paths to reference the new file + +## Binary Format + +The binary format uses little-endian encoding and follows this structure: + +### ConstraintSystem Format +1. **Version header** (4 bytes): `u32` serialization version +2. **ValueVecLayout**: Layout configuration +3. **Constants**: Vector of `Word` values +4. **AND constraints**: Vector of `AndConstraint` structures +5. **MUL constraints**: Vector of `MulConstraint` structures + +### PublicWitness Format +1. **Version header** (4 bytes): `u32` serialization version +2. **Data**: Vector of `Word` values representing the public witness + +### Proof Format +1. **Version header** (4 bytes): `u32` serialization version +2. **Challenger type**: String identifying the challenger (e.g., "HasherChallenger") +3. **Transcript data**: Vector of bytes containing the proof transcript + +All data uses the platform-independent `SerializeBytes`/`DeserializeBytes` traits from `binius-utils`. \ No newline at end of file diff --git a/crates/core/test_data/constraint_system_v1.bin b/crates/core/test_data/constraint_system_v1.bin new file mode 100644 index 000000000..fbe68fb3c Binary files /dev/null and b/crates/core/test_data/constraint_system_v1.bin differ diff --git a/crates/core/test_data/proof_v1.bin b/crates/core/test_data/proof_v1.bin new file mode 100644 index 000000000..70ab9a326 Binary files /dev/null and b/crates/core/test_data/proof_v1.bin differ diff --git a/crates/core/test_data/witness_v1.bin b/crates/core/test_data/witness_v1.bin new file mode 100644 index 000000000..11ef86674 Binary files /dev/null and b/crates/core/test_data/witness_v1.bin differ diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 009ee2003..e029343d2 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -38,4 +38,12 @@ harness = false [features] default = ["rayon"] perfetto = ["tracing-profile/perfetto"] -rayon = ["binius-prover/rayon"] \ No newline at end of file +rayon = ["binius-prover/rayon"] + +[[example]] +name = "prover" +path = "examples/prover.rs" + +[[example]] +name = "verifier" +path = "examples/verifier.rs" diff --git a/crates/examples/README.md b/crates/examples/README.md index fad6ea85e..bce7f7a36 100644 --- a/crates/examples/README.md +++ b/crates/examples/README.md @@ -274,6 +274,48 @@ cargo run --release --example my_circuit -- --help RUST_LOG=info cargo run --release --example my_circuit ``` +## CLI subcommands + +All example binaries share a common CLI with these subcommands: + +- prove (default): build the circuit, generate witness, create and verify a proof +- stat: print circuit statistics +- composition: output circuit composition as JSON +- check-snapshot: compare current stats with the stored snapshot +- bless-snapshot: update the stored snapshot with current stats +- save: save artifacts to files (only those explicitly requested) + +### Save artifacts + +Use the save subcommand to write selected artifacts to disk. Nothing is written unless a corresponding path is provided. + +Flags: +- --cs-path PATH: write the constraint system binary +- --pub-witness-path PATH: write the public witness values binary +- --non-pub-data-path PATH: write the non-public witness values binary + +Examples: + +```bash +# Save only the constraint system +cargo run --release --example my_circuit -- save --cs-path out/cs.bin + +# Save public values and non-public values +cargo run --release --example my_circuit -- save \ + --pub-witness-path out/public.bin \ + --non-pub-data-path out/non_public.bin + +# Save all three +cargo run --release --example my_circuit -- save \ + --cs-path out/cs.bin \ + --pub-witness-path out/public.bin \ + --non-pub-data-path out/non_public.bin +``` + +Notes: +- Public and non-public outputs are serialized using the versioned ValuesData format from core. +- Parent directories are created automatically if they don’t exist. + ## Adding to Cargo.toml Add your example to `crates/examples/Cargo.toml`: @@ -290,6 +332,60 @@ Look at these examples for reference: - `sha256.rs` - Shows parameter/instance separation, random data generation - `zklogin.rs` - Shows complex witness population with external data generation +## Prover binary + +The `prover` example binary reads a constraint system and witnesses from disk and produces a serialized proof. This is useful for cross-host proof generation pipelines. + +Arguments: +- `--cs-path PATH`: path to the constraint system binary +- `--pub-witness-path PATH`: path to the public values binary (ValuesData) +- `--non-pub-data-path PATH`: path to the non-public values binary (ValuesData) +- `--proof-path PATH`: path to write the proof binary +- `-l, --log-inv-rate N`: log of the inverse rate (default: 1) + +Usage: + +```bash +# 1) Generate artifacts from an example circuit (e.g., sha256) +cargo run --release --example sha256 -- save \ + --cs-path out/sha256/cs.bin \ + --pub-witness-path out/sha256/public.bin \ + --non-pub-data-path out/sha256/non_public.bin + +# 2) Produce a proof from those files +cargo run --release --example prover -- \ + --cs-path out/sha256/cs.bin \ + --pub-witness-path out/sha256/public.bin \ + --non-pub-data-path out/sha256/non_public.bin \ + --proof-path out/sha256/proof.bin \ + --log-inv-rate 1 +``` + +## Verifier binary + +The `verifier` example binary reads a constraint system, a public witness, and a proof from disk and verifies the proof. It also checks that the challenger type embedded in the proof matches the verifier’s expected challenger (HasherChallenger), returning an error if it doesn’t. + +Arguments: +- `--cs-path PATH`: path to the constraint system binary +- `--pub-witness-path PATH`: path to the public values binary (ValuesData) +- `--proof-path PATH`: path to the proof binary +- `-l, --log-inv-rate N`: log of the inverse rate (default: 1) + +Usage: + +```bash +# Verify the proof generated above +cargo run --release --example verifier -- \ + --cs-path out/sha256/cs.bin \ + --pub-witness-path out/sha256/public.bin \ + --proof-path out/sha256/proof.bin \ + --log-inv-rate 1 +``` + +Notes: +- The verifier fails if the challenger type in the proof is not `HasherChallenger`. +- The public witness must match the constraint system and the proof’s statement. + ## Tips 1. **Keep it simple**: The main function should just create the CLI and run it diff --git a/crates/examples/examples/prover.rs b/crates/examples/examples/prover.rs new file mode 100644 index 000000000..ccc0d6cf8 --- /dev/null +++ b/crates/examples/examples/prover.rs @@ -0,0 +1,103 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{Context, Result}; +use binius_core::constraint_system::{ConstraintSystem, Proof, ValueVec, ValuesData}; +use binius_examples::setup; +use binius_utils::serialization::{DeserializeBytes, SerializeBytes}; +use binius_verifier::{ + config::{ChallengerWithName, StdChallenger}, + transcript::ProverTranscript, +}; +use clap::Parser; + +/// Prover CLI: generate a proof from a serialized constraint system and witnesses. +#[derive(Debug, Parser)] +#[command( + name = "prover", + about = "Generate and save a proof from CS and witnesses" +)] +struct Args { + /// Path to the constraint system binary + #[arg(long = "cs-path")] + cs_path: PathBuf, + + /// Path to the public values (ValuesData) binary + #[arg(long = "pub-witness-path")] + pub_witness_path: PathBuf, + + /// Path to the non-public values (ValuesData) binary + #[arg(long = "non-pub-data-path")] + non_pub_data_path: PathBuf, + + /// Path to write the proof binary + #[arg(long = "proof-path")] + proof_path: PathBuf, + + /// Log of the inverse rate for the proof system + #[arg(short = 'l', long = "log-inv-rate", default_value_t = 1, value_parser = clap::value_parser!(u32).range(1..))] + log_inv_rate: u32, +} + +fn main() -> Result<()> { + let _tracing_guard = tracing_profile::init_tracing().ok(); + let args = Args::parse(); + + // Read and deserialize constraint system + let cs_bytes = fs::read(&args.cs_path).with_context(|| { + format!("Failed to read constraint system from {}", args.cs_path.display()) + })?; + let cs = ConstraintSystem::deserialize(&mut cs_bytes.as_slice()) + .context("Failed to deserialize ConstraintSystem")?; + + // Read and deserialize public values + let pub_bytes = fs::read(&args.pub_witness_path).with_context(|| { + format!("Failed to read public values from {}", args.pub_witness_path.display()) + })?; + let public = ValuesData::deserialize(&mut pub_bytes.as_slice()) + .context("Failed to deserialize public ValuesData")?; + + // Read and deserialize non-public values + let non_pub_bytes = fs::read(&args.non_pub_data_path).with_context(|| { + format!("Failed to read non-public values from {}", args.non_pub_data_path.display()) + })?; + let non_public = ValuesData::deserialize(&mut non_pub_bytes.as_slice()) + .context("Failed to deserialize non-public ValuesData")?; + + // Reconstruct the full ValueVec + // Take ownership of the underlying vectors without extra copies + let public: Vec<_> = public.into(); + let non_public: Vec<_> = non_public.into(); + let witness = ValueVec::new_from_data(cs.value_vec_layout.clone(), public, non_public) + .context("Failed to reconstruct ValueVec from provided values")?; + + // Setup prover (verifier is not used here) + let (_verifier, prover) = setup(cs, args.log_inv_rate as usize)?; + + // Prove + let mut prover_transcript = ProverTranscript::new(StdChallenger::default()); + prover + .prove(witness, &mut prover_transcript) + .context("Proving failed")?; + let transcript = prover_transcript.finalize(); + + // Wrap into serializable Proof with a stable challenger type identifier. + // NOTE: Avoid std::any::type_name for cross-platform stability; use a constant instead. + let proof = Proof::owned(transcript, StdChallenger::NAME.to_string()); + + // Serialize and save the proof + if let Some(parent) = args.proof_path.parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create parent directory {}", parent.display()))?; + } + let mut buf = Vec::new(); + proof + .serialize(&mut buf) + .context("Failed to serialize proof")?; + fs::write(&args.proof_path, &buf) + .with_context(|| format!("Failed to write proof to {}", args.proof_path.display()))?; + + tracing::info!("Saved proof to {} ({} bytes)", args.proof_path.display(), buf.len()); + Ok(()) +} diff --git a/crates/examples/examples/verifier.rs b/crates/examples/examples/verifier.rs new file mode 100644 index 000000000..d321659aa --- /dev/null +++ b/crates/examples/examples/verifier.rs @@ -0,0 +1,92 @@ +use std::{fs, path::PathBuf}; + +use anyhow::{Context, Result, bail}; +use binius_core::constraint_system::{ConstraintSystem, Proof, ValuesData}; +use binius_examples::StdVerifier; +use binius_utils::serialization::DeserializeBytes; +use binius_verifier::{ + Verifier, + config::{ChallengerWithName, StdChallenger}, + hash::StdCompression, + transcript::VerifierTranscript, +}; +use clap::Parser; + +/// Verifier CLI: load CS, public witness and proof, then verify. +#[derive(Debug, Parser)] +#[command( + name = "verifier", + about = "Verify a proof from a constraint system, public witness, and proof binary" +)] +struct Args { + /// Path to the constraint system binary + #[arg(long = "cs-path")] + cs_path: PathBuf, + + /// Path to the public values (ValuesData) binary + #[arg(long = "pub-witness-path")] + pub_witness_path: PathBuf, + + /// Path to the proof binary + #[arg(long = "proof-path")] + proof_path: PathBuf, + + /// Log of the inverse rate for the proof system (must match what was used for proving) + #[arg(short = 'l', long = "log-inv-rate", default_value_t = 1, value_parser = clap::value_parser!(u32).range(1..))] + log_inv_rate: u32, +} + +fn main() -> Result<()> { + let _tracing_guard = tracing_profile::init_tracing().ok(); + let args = Args::parse(); + + // Read and deserialize constraint system + let cs_bytes = fs::read(&args.cs_path).with_context(|| { + format!("Failed to read constraint system from {}", args.cs_path.display()) + })?; + let cs = ConstraintSystem::deserialize(&mut cs_bytes.as_slice()) + .context("Failed to deserialize ConstraintSystem")?; + + // Read and deserialize public values + let pub_bytes = fs::read(&args.pub_witness_path).with_context(|| { + format!("Failed to read public values from {}", args.pub_witness_path.display()) + })?; + let public = ValuesData::deserialize(&mut pub_bytes.as_slice()) + .context("Failed to deserialize public ValuesData")?; + + // Read and deserialize proof + let proof_bytes = fs::read(&args.proof_path) + .with_context(|| format!("Failed to read proof from {}", args.proof_path.display()))?; + let proof = + Proof::deserialize(&mut proof_bytes.as_slice()).context("Failed to deserialize Proof")?; + + // Validate challenger type matches our verifier configuration + let expected_challenger = StdChallenger::NAME; + if proof.challenger_type() != expected_challenger { + bail!( + "Challenger type mismatch: expected '{}', found '{}'", + expected_challenger, + proof.challenger_type() + ); + } + + // Set up the verifier + let verifier: StdVerifier = + Verifier::setup(cs, args.log_inv_rate as usize, StdCompression::default()) + .context("Failed to setup verifier")?; + + // Create a verifier transcript from the serialized proof data + let (data, _) = proof.into_owned(); + let mut verifier_transcript = VerifierTranscript::new(StdChallenger::default(), data); + + // Verify + verifier + .verify(public.as_slice(), &mut verifier_transcript) + .context("Verification failed")?; + verifier_transcript + .finalize() + .context("Transcript not fully consumed after verification")?; + + tracing::info!("Proof verified successfully"); + Ok(()) +} diff --git a/crates/examples/src/cli.rs b/crates/examples/src/cli.rs index 46cb05c5d..86fd5dd77 100644 --- a/crates/examples/src/cli.rs +++ b/crates/examples/src/cli.rs @@ -1,9 +1,29 @@ +use std::{fs, path::Path}; + use anyhow::Result; +use binius_core::constraint_system::{ValueVec, ValuesData}; use binius_frontend::{compiler::CircuitBuilder, stat::CircuitStat}; +use binius_utils::serialization::SerializeBytes; use clap::{Arg, Args, Command, FromArgMatches, Subcommand}; use crate::{ExampleCircuit, prove_verify, setup}; +/// Serialize a value implementing `SerializeBytes` and write it to the given path. +fn write_serialized(value: &T, path: &str) -> Result<()> { + if let Some(parent) = Path::new(path).parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent).map_err(|e| { + anyhow::anyhow!("Failed to create directory '{}': {}", parent.display(), e) + })?; + } + let mut buf: Vec = Vec::new(); + value.serialize(&mut buf)?; + fs::write(path, &buf) + .map_err(|e| anyhow::anyhow!("Failed to write serialized data to '{}': {}", path, e))?; + Ok(()) +} + /// A CLI builder for circuit examples that handles all command-line parsing and execution. /// /// This provides a clean API for circuit examples where developers only need to: @@ -74,6 +94,27 @@ enum Commands { #[command(flatten)] params: CommandArgs, }, + + /// Save constraint system, public witness, and non-public data to files if paths are provided + Save { + /// Output path for the constraint system binary + #[arg(long = "cs-path")] + cs_path: Option, + + /// Output path for the public witness binary + #[arg(long = "pub-witness-path")] + pub_witness_path: Option, + + /// Output path for the non-public data (witness + internal) binary + #[arg(long = "non-pub-data-path")] + non_pub_data_path: Option, + + #[command(flatten)] + params: CommandArgs, + + #[command(flatten)] + instance: CommandArgs, + }, } /// Wrapper for dynamic command arguments @@ -102,13 +143,15 @@ where let composition_cmd = Self::build_composition_subcommand(); let check_snapshot_cmd = Self::build_check_snapshot_subcommand(); let bless_snapshot_cmd = Self::build_bless_snapshot_subcommand(); + let save_cmd = Self::build_save_subcommand(); let command = command .subcommand(prove_cmd) .subcommand(stat_cmd) .subcommand(composition_cmd) .subcommand(check_snapshot_cmd) - .subcommand(bless_snapshot_cmd); + .subcommand(bless_snapshot_cmd) + .subcommand(save_cmd); // Also add top-level args for default prove behavior let command = command.arg( @@ -171,6 +214,34 @@ where E::Params::augment_args(cmd) } + fn build_save_subcommand() -> Command { + let mut cmd = Command::new("save").about( + "Save constraint system, public witness, and non-public data to files if paths are provided", + ); + cmd = cmd + .arg( + Arg::new("cs_path") + .long("cs-path") + .value_name("PATH") + .help("Output path for the constraint system binary"), + ) + .arg( + Arg::new("pub_witness_path") + .long("pub-witness-path") + .value_name("PATH") + .help("Output path for the public witness binary"), + ) + .arg( + Arg::new("non_pub_data_path") + .long("non-pub-data-path") + .value_name("PATH") + .help("Output path for the non-public data (witness + internal) binary"), + ); + cmd = E::Params::augment_args(cmd); + cmd = E::Instance::augment_args(cmd); + cmd + } + /// Set the about/description text for the command. /// /// This appears in the help output. @@ -212,6 +283,7 @@ where Some(("bless-snapshot", sub_matches)) => { Self::run_bless_snapshot_impl(sub_matches.clone(), circuit_name) } + Some(("save", sub_matches)) => Self::run_save(sub_matches.clone()), Some((cmd, _)) => anyhow::bail!("Unknown subcommand: {}", cmd), None => { // No subcommand - default to prove behavior for backward compatibility @@ -319,6 +391,54 @@ where Ok(()) } + fn run_save(matches: clap::ArgMatches) -> Result<()> { + // Extract optional output paths + let cs_path = matches.get_one::("cs_path").cloned(); + let pub_witness_path = matches.get_one::("pub_witness_path").cloned(); + let non_pub_data_path = matches.get_one::("non_pub_data_path").cloned(); + + // If nothing to save, exit early + if cs_path.is_none() && pub_witness_path.is_none() && non_pub_data_path.is_none() { + tracing::info!("No output paths provided; nothing to save"); + return Ok(()); + } + + // Parse Params and Instance + let params = E::Params::from_arg_matches(&matches)?; + let instance = E::Instance::from_arg_matches(&matches)?; + + // Build circuit + let mut builder = CircuitBuilder::new(); + let example = E::build(params, &mut builder)?; + let circuit = builder.build(); + + // Generate witness + let mut filler = circuit.new_witness_filler(); + example.populate_witness(instance, &mut filler)?; + circuit.populate_wire_witness(&mut filler)?; + let witness: ValueVec = filler.into_value_vec(); + + // Conditionally write artifacts + if let Some(path) = cs_path.as_deref() { + write_serialized(circuit.constraint_system(), path)?; + tracing::info!("Constraint system saved to '{}'", path); + } + + if let Some(path) = pub_witness_path.as_deref() { + let data = ValuesData::from(witness.public()); + write_serialized(&data, path)?; + tracing::info!("Public witness saved to '{}'", path); + } + + if let Some(path) = non_pub_data_path.as_deref() { + let data = ValuesData::from(witness.non_public()); + write_serialized(&data, path)?; + tracing::info!("Non-public witness saved to '{}'", path); + } + + Ok(()) + } + /// Parse arguments and run the circuit example. /// /// This orchestrates the entire flow: diff --git a/crates/verifier/src/config.rs b/crates/verifier/src/config.rs index 46b85f73c..788bef51a 100644 --- a/crates/verifier/src/config.rs +++ b/crates/verifier/src/config.rs @@ -1,7 +1,7 @@ //! Specifies standard trait implementations and parameters. use binius_field::{AESTowerField8b, BinaryField, BinaryField1b, BinaryField128bGhash}; -use binius_transcript::fiat_shamir::HasherChallenger; +use binius_transcript::fiat_shamir::{Challenger, HasherChallenger}; use binius_utils::checked_arithmetics::{checked_int_div, checked_log_2}; use super::hash::StdDigest; @@ -10,6 +10,15 @@ use super::hash::StdDigest; pub type B1 = BinaryField1b; pub type B128 = BinaryField128bGhash; +/// The intention of this trait is to capture the moment when a StandardChallenger type is changed. +pub trait ChallengerWithName: Challenger { + const NAME: &'static str; +} + +impl ChallengerWithName for HasherChallenger { + const NAME: &'static str = "HasherChallenger"; +} + /// The default [`binius_transcript::fiat_shamir::Challenger`] implementation. pub type StdChallenger = HasherChallenger;