From 7547fbf0b10ac8cdc4b4884fe95b5eabb66dac26 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:03 +0000 Subject: [PATCH 1/6] Add serialization support to the ContraintsSystem (#871) ### TL;DR Add serialization support for constraint systems to enable persistence and cross-process sharing. ### What changed? - Implemented `SerializeBytes` and `DeserializeBytes` traits for `ConstraintSystem` and its components - Added version tracking with `SERIALIZATION_VERSION` constant for compatibility - Created binary reference file for testing serialization compatibility - Added documentation for the serialization format and testing approach - Added new dependencies: `bytes` for serialization and `rand` for testing ### How to test? - Run the existing test suite which includes comprehensive serialization tests - To regenerate the reference binary file: - To verify compatibility with the reference binary: ### Why make this change? Serialization support enables: 1. Persistence of constraint systems to disk 2. Sharing constraint systems between different processes 3. Caching compiled circuits to avoid recomputation 4. Versioning to ensure backward compatibility as the format evolves The implementation includes careful validation during deserialization and version checking to maintain compatibility over time. --- crates/core/Cargo.toml | 2 + .../core/test_data/constraint_system_v1.bin | Bin 0 -> 216 bytes crates/core/src/constraint_system.rs | 618 ++++++++++++++++++ crates/core/src/word.rs | 18 + crates/core/test_data/README.md | 41 ++ 5 files changed, 679 insertions(+) create mode 100644 crates/core/crates/core/test_data/constraint_system_v1.bin create mode 100644 crates/core/test_data/README.md 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/crates/core/test_data/constraint_system_v1.bin b/crates/core/crates/core/test_data/constraint_system_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..fbe68fb3c08b6daf1a2bb76b69a80bcc302318b0 GIT binary patch literal 216 zcmX|5!3}^g5TaER<45OkcUN!}S8xU4ERYbO<$7%ZCbE;;@H2HKkC7WyIDf%=UyrP2 j#K1+! 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 +51,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 +131,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 +198,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 +227,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 +258,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 +329,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 +419,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. @@ -312,3 +520,413 @@ impl IndexMut for ValueVec { &mut self.data[index.0 as usize] } } + +#[cfg(test)] +mod serialization_tests { + use rand::{RngCore, SeedableRng, rngs::StdRng}; + + use super::*; + + 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()); + + // Check individual elements + 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 test_data_path = std::path::Path::new("crates/core/test_data/constraint_system_v1.bin"); + + // If the reference file doesn't exist, create it first + if !test_data_path.exists() { + let constraint_system = create_test_constraint_system(); + let mut buf = Vec::new(); + constraint_system.serialize(&mut buf).unwrap(); + + // 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(); + } + + // Read the reference binary file + let binary_data = std::fs::read(test_data_path).unwrap(); + + // Deserialize and verify it works + let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap(); + + // Verify the deserialized data has expected structure + 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 that demonstrates what happens when there's a version mismatch. + /// This serves as documentation for the version compatibility system. + #[test] + fn test_version_compatibility_documentation() { + // Create a constraint system and serialize it + let constraint_system = create_test_constraint_system(); + let mut buf = Vec::new(); + constraint_system.serialize(&mut buf).unwrap(); + + // Verify current version works + let deserialized = ConstraintSystem::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(constraint_system.constants.len(), deserialized.constants.len()); + + // Now simulate an older version by modifying the version bytes + let mut modified_buf = buf.clone(); + let old_version = 0u32; // Simulate version 0 + modified_buf[0..4].copy_from_slice(&old_version.to_le_bytes()); + + // This should fail with version mismatch + let result = ConstraintSystem::deserialize(&mut modified_buf.as_slice()); + assert!(result.is_err()); + match result.unwrap_err() { + SerializationError::InvalidConstruction { name } => { + assert_eq!(name, "ConstraintSystem::version"); + } + _ => panic!("Expected version mismatch error"), + } + } +} 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..54acc458f --- /dev/null +++ b/crates/core/test_data/README.md @@ -0,0 +1,41 @@ +# 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. + +## 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: + +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 + +## Binary Format + +The binary format uses little-endian encoding and follows this structure: + +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 + +All data uses the platform-independent `SerializeBytes`/`DeserializeBytes` traits from `binius-utils`. \ No newline at end of file From 579a01f51b630b59bf12aa073961c20d054636d0 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:04 +0000 Subject: [PATCH 2/6] Add public witness serialization (#872) # Add PublicWitness struct for zero-knowledge proof verification ### TL;DR Introduces a new `PublicWitness` type to represent the public portion of witness data in zero-knowledge proofs. ### What changed? - Added a new `PublicWitness<'a>` struct that uses `Cow<'a, [Word]>` to efficiently store public witness data - Implemented serialization/deserialization with version compatibility - Added conversion methods from various types (`Vec`, `&[Word]`, `&ValueVec`) - Added utility methods for accessing and manipulating the witness data - Created a reference binary file `public_witness_v1.bin` for serialization testing - Updated test documentation to include information about the new reference file ### How to test? - Run the test suite to verify the implementation works correctly: - The tests include serialization round-trips, version compatibility checks, and conversion tests ### Why make this change? Public witness data is a critical component in zero-knowledge proof systems, representing the public inputs and constants that both provers and verifiers need to agree on. This implementation provides a standardized way to handle this data with efficient memory usage through `Cow`, allowing both borrowed and owned variants depending on the use case. --- .../core/test_data/public_witness_v1.bin | Bin 0 -> 40 bytes crates/core/src/constraint_system.rs | 251 +++++++++++++++--- crates/core/test_data/README.md | 11 + 3 files changed, 230 insertions(+), 32 deletions(-) create mode 100644 crates/core/crates/core/test_data/public_witness_v1.bin diff --git a/crates/core/crates/core/test_data/public_witness_v1.bin b/crates/core/crates/core/test_data/public_witness_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..11ef86674a8b6e4aa9d0524ff2bc11587e1a451c GIT binary patch literal 40 kcmZQ%U|?VYVn!ea0WAo{@P6Ogdm!ffv#Tdmgqa8d080M{bN~PV literal 0 HcmV?d00001 diff --git a/crates/core/src/constraint_system.rs b/crates/core/src/constraint_system.rs index 231ee0767..a762085be 100644 --- a/crates/core/src/constraint_system.rs +++ b/crates/core/src/constraint_system.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, cmp, ops::{Index, IndexMut}, }; @@ -521,6 +522,124 @@ impl IndexMut for ValueVec { } } +/// Public witness data for zero-knowledge proofs. +/// +/// This structure holds the public portion of witness data that needs to be shared +/// with verifiers. It uses `Cow<[Word]>` to avoid unnecessary clones while supporting +/// both borrowed and owned data. +/// +/// The public witness consists of: +/// - Constants: Fixed values defined in the constraint system +/// - Inputs/Outputs: Public values that are part of the statement being proven +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicWitness<'a> { + data: Cow<'a, [Word]>, +} + +impl<'a> PublicWitness<'a> { + /// Serialization format version for compatibility checking + pub const SERIALIZATION_VERSION: u32 = 1; + + /// Create a new PublicWitness from borrowed data + pub fn borrowed(data: &'a [Word]) -> Self { + Self { + data: Cow::Borrowed(data), + } + } + + /// Create a new PublicWitness from owned data + pub fn owned(data: Vec) -> Self { + Self { + data: Cow::Owned(data), + } + } + + /// Get the public witness data as a slice + pub fn as_slice(&self) -> &[Word] { + &self.data + } + + /// Get the number of words in the public witness + pub fn len(&self) -> usize { + self.data.len() + } + + /// Check if the public 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 PublicWitness + pub fn to_owned(&self) -> PublicWitness<'static> { + PublicWitness { + data: Cow::Owned(self.data.to_vec()), + } + } +} + +impl<'a> SerializeBytes for PublicWitness<'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 PublicWitness<'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: "PublicWitness::version", + }); + } + + let data = Vec::::deserialize(read_buf)?; + + Ok(PublicWitness::owned(data)) + } +} + +impl<'a> From<&'a [Word]> for PublicWitness<'a> { + fn from(data: &'a [Word]) -> Self { + PublicWitness::borrowed(data) + } +} + +impl From> for PublicWitness<'static> { + fn from(data: Vec) -> Self { + PublicWitness::owned(data) + } +} + +impl<'a> From<&'a ValueVec> for PublicWitness<'a> { + fn from(value_vec: &'a ValueVec) -> Self { + PublicWitness::borrowed(value_vec.public()) + } +} + +impl<'a> AsRef<[Word]> for PublicWitness<'a> { + fn as_ref(&self) -> &[Word] { + self.as_slice() + } +} + +impl<'a> std::ops::Deref for PublicWitness<'a> { + type Target = [Word]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + #[cfg(test)] mod serialization_tests { use rand::{RngCore, SeedableRng, rngs::StdRng}; @@ -853,27 +972,10 @@ mod serialization_tests { fn test_deserialize_from_reference_binary_file() { let test_data_path = std::path::Path::new("crates/core/test_data/constraint_system_v1.bin"); - // If the reference file doesn't exist, create it first - if !test_data_path.exists() { - let constraint_system = create_test_constraint_system(); - let mut buf = Vec::new(); - constraint_system.serialize(&mut buf).unwrap(); - - // 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(); - } - - // Read the reference binary file let binary_data = std::fs::read(test_data_path).unwrap(); - // Deserialize and verify it works let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap(); - // Verify the deserialized data has expected structure 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); @@ -901,32 +1003,117 @@ mod serialization_tests { ); } - /// Test that demonstrates what happens when there's a version mismatch. - /// This serves as documentation for the version compatibility system. #[test] - fn test_version_compatibility_documentation() { - // Create a constraint system and serialize it + fn test_public_witness_from_value_vec() { let constraint_system = create_test_constraint_system(); + let value_vec = constraint_system.new_value_vec(); + + let public_witness: PublicWitness = (&value_vec).into(); + + assert_eq!(public_witness.len(), value_vec.public().len()); + assert_eq!(public_witness.as_slice(), value_vec.public()); + } + + #[test] + fn test_public_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 = PublicWitness::owned(data.clone()); + let mut buf = Vec::new(); - constraint_system.serialize(&mut buf).unwrap(); + witness.serialize(&mut buf).unwrap(); - // Verify current version works - let deserialized = ConstraintSystem::deserialize(&mut buf.as_slice()).unwrap(); - assert_eq!(constraint_system.constants.len(), deserialized.constants.len()); + let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(witness, deserialized); + assert_eq!(deserialized.as_slice(), data.as_slice()); + } + + #[test] + fn test_public_witness_serialization_round_trip_borrowed() { + let data = vec![Word::from_u64(123), Word::from_u64(456)]; + let witness = PublicWitness::borrowed(&data); + + let mut buf = Vec::new(); + witness.serialize(&mut buf).unwrap(); - // Now simulate an older version by modifying the version bytes - let mut modified_buf = buf.clone(); - let old_version = 0u32; // Simulate version 0 - modified_buf[0..4].copy_from_slice(&old_version.to_le_bytes()); + let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).unwrap(); + assert_eq!(witness, deserialized); + assert_eq!(deserialized.as_slice(), data.as_slice()); + } + + #[test] + fn test_public_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 - // This should fail with version mismatch - let result = ConstraintSystem::deserialize(&mut modified_buf.as_slice()); + let result = PublicWitness::deserialize(&mut buf.as_slice()); assert!(result.is_err()); match result.unwrap_err() { SerializationError::InvalidConstruction { name } => { - assert_eq!(name, "ConstraintSystem::version"); + assert_eq!(name, "PublicWitness::version"); } _ => panic!("Expected version mismatch error"), } } + + /// Helper function to create or update the reference binary file for PublicWitness version + /// compatibility testing. + #[test] + #[ignore] // Use `cargo test -- --ignored create_public_witness_reference_binary` to run this + fn create_public_witness_reference_binary_file() { + let data = vec![ + Word::from_u64(1), + Word::from_u64(42), + Word::from_u64(0xDEADBEEF), + Word::from_u64(0x1234567890ABCDEF), + ]; + let public_witness = PublicWitness::owned(data); + + let mut buf = Vec::new(); + public_witness.serialize(&mut buf).unwrap(); + + let test_data_path = std::path::Path::new("crates/core/test_data/public_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 PublicWitness reference binary file at: {:?}", test_data_path); + println!("Binary data length: {} bytes", buf.len()); + } + + /// Test deserialization from a reference binary file to ensure PublicWitness version + /// compatibility. This test will fail if breaking changes are made without incrementing the + /// version. + #[test] + fn test_public_witness_deserialize_from_reference_binary_file() { + let test_data_path = std::path::Path::new("crates/core/test_data/public_witness_v1.bin"); + + let binary_data = std::fs::read(test_data_path).unwrap(); + + let deserialized = PublicWitness::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, + "PublicWitness binary file version mismatch. If you made breaking changes, increment PublicWitness::SERIALIZATION_VERSION" + ); + } } diff --git a/crates/core/test_data/README.md b/crates/core/test_data/README.md index 54acc458f..bd282aba4 100644 --- a/crates/core/test_data/README.md +++ b/crates/core/test_data/README.md @@ -5,6 +5,7 @@ This directory contains binary reference files used for testing serialization fo ## 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. ## Purpose @@ -20,6 +21,7 @@ These binary files serve as regression tests to ensure that: 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 @@ -28,6 +30,15 @@ If you make intentional breaking changes to the serialization format: 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 + ## Binary Format The binary format uses little-endian encoding and follows this structure: From ddfcba204984c774b3f9f00c262d0685b27187b4 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:05 +0000 Subject: [PATCH 3/6] Add serializatoin support for the proof (#873) ### TL;DR Added new data structures for zero-knowledge proof serialization and improved value vector handling. ### What changed? - Renamed `PublicWitness` to `ValuesData` to make it more generic for both public and non-public values - Added `ValueVec::new_from_data()` method to create a value vector from separate public and private parts - Added `ValueVec::non_public()` method to access non-public values - Added new `Proof` data structure for serializing zero-knowledge proofs with challenger type information - Fixed test data paths by moving files to the correct location and using `include_bytes!` instead of file I/O - Updated documentation in test_data/README.md to reflect the new structures ### How to test? - Run the test suite to verify serialization/deserialization works correctly: ``` cargo test -p binius-core ``` - The tests include round-trip serialization tests for the new `Proof` structure and the renamed `ValuesData` structure - Reference binary files are included for compatibility testing ### Why make this change? These changes support cross-host verification of zero-knowledge proofs by providing proper serialization of proof transcripts along with challenger type information. The renamed `ValuesData` structure is more flexible, allowing it to represent both public and non-public values. The new methods on `ValueVec` enable splitting and recombining value vectors, which is useful for distributed proving scenarios where public and private inputs need to be handled separately. --- crates/core/src/constraint_system.rs | 490 +++++++++++++++--- crates/core/src/error.rs | 2 + crates/core/test_data/README.md | 20 + .../test_data/constraint_system_v1.bin | Bin crates/core/test_data/proof_v1.bin | Bin 0 -> 60 bytes .../witness_v1.bin} | Bin 6 files changed, 441 insertions(+), 71 deletions(-) rename crates/core/{crates/core => }/test_data/constraint_system_v1.bin (100%) create mode 100644 crates/core/test_data/proof_v1.bin rename crates/core/{crates/core/test_data/public_witness_v1.bin => test_data/witness_v1.bin} (100%) diff --git a/crates/core/src/constraint_system.rs b/crates/core/src/constraint_system.rs index a762085be..6a6dc1a82 100644 --- a/crates/core/src/constraint_system.rs +++ b/crates/core/src/constraint_system.rs @@ -477,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() @@ -495,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; @@ -522,49 +546,45 @@ impl IndexMut for ValueVec { } } -/// Public witness data for zero-knowledge proofs. +/// Values data for zero-knowledge proofs (either public witness or non-public part - private inputs +/// and internal values). /// -/// This structure holds the public portion of witness data that needs to be shared -/// with verifiers. It uses `Cow<[Word]>` to avoid unnecessary clones while supporting +/// It uses `Cow<[Word]>` to avoid unnecessary clones while supporting /// both borrowed and owned data. -/// -/// The public witness consists of: -/// - Constants: Fixed values defined in the constraint system -/// - Inputs/Outputs: Public values that are part of the statement being proven #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PublicWitness<'a> { +pub struct ValuesData<'a> { data: Cow<'a, [Word]>, } -impl<'a> PublicWitness<'a> { +impl<'a> ValuesData<'a> { /// Serialization format version for compatibility checking pub const SERIALIZATION_VERSION: u32 = 1; - /// Create a new PublicWitness from borrowed data + /// Create a new ValuesData from borrowed data pub fn borrowed(data: &'a [Word]) -> Self { Self { data: Cow::Borrowed(data), } } - /// Create a new PublicWitness from owned data + /// Create a new ValuesData from owned data pub fn owned(data: Vec) -> Self { Self { data: Cow::Owned(data), } } - /// Get the public witness data as a slice + /// Get the values data as a slice pub fn as_slice(&self) -> &[Word] { &self.data } - /// Get the number of words in the public witness + /// Get the number of words in the values data pub fn len(&self) -> usize { self.data.len() } - /// Check if the public witness is empty + /// Check if the witness is empty pub fn is_empty(&self) -> bool { self.data.is_empty() } @@ -574,15 +594,15 @@ impl<'a> PublicWitness<'a> { self.data.into_owned() } - /// Convert to owned version of PublicWitness - pub fn to_owned(&self) -> PublicWitness<'static> { - PublicWitness { + /// 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 PublicWitness<'a> { +impl<'a> SerializeBytes for ValuesData<'a> { fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> { Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?; @@ -590,7 +610,7 @@ impl<'a> SerializeBytes for PublicWitness<'a> { } } -impl DeserializeBytes for PublicWitness<'static> { +impl DeserializeBytes for ValuesData<'static> { fn deserialize(mut read_buf: impl Buf) -> Result where Self: Sized, @@ -598,42 +618,167 @@ impl DeserializeBytes for PublicWitness<'static> { let version = u32::deserialize(&mut read_buf)?; if version != Self::SERIALIZATION_VERSION { return Err(SerializationError::InvalidConstruction { - name: "PublicWitness::version", + name: "Witness::version", }); } let data = Vec::::deserialize(read_buf)?; - Ok(PublicWitness::owned(data)) + Ok(ValuesData::owned(data)) } } -impl<'a> From<&'a [Word]> for PublicWitness<'a> { +impl<'a> From<&'a [Word]> for ValuesData<'a> { fn from(data: &'a [Word]) -> Self { - PublicWitness::borrowed(data) + ValuesData::borrowed(data) } } -impl From> for PublicWitness<'static> { +impl From> for ValuesData<'static> { fn from(data: Vec) -> Self { - PublicWitness::owned(data) + ValuesData::owned(data) } } -impl<'a> From<&'a ValueVec> for PublicWitness<'a> { - fn from(value_vec: &'a ValueVec) -> Self { - PublicWitness::borrowed(value_vec.public()) +impl<'a> AsRef<[Word]> for ValuesData<'a> { + fn as_ref(&self) -> &[Word] { + self.as_slice() } } -impl<'a> AsRef<[Word]> for PublicWitness<'a> { - fn as_ref(&self) -> &[Word] { +impl<'a> std::ops::Deref for ValuesData<'a> { + type Target = [Word]; + + fn deref(&self) -> &Self::Target { self.as_slice() } } -impl<'a> std::ops::Deref for PublicWitness<'a> { - type Target = [Word]; +/// 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() @@ -646,7 +791,7 @@ mod serialization_tests { use super::*; - fn create_test_constraint_system() -> ConstraintSystem { + pub(crate) fn create_test_constraint_system() -> ConstraintSystem { let constants = vec![ Word::from_u64(1), Word::from_u64(42), @@ -797,7 +942,6 @@ mod serialization_tests { assert_eq!(constraint.b.len(), deserialized.b.len()); assert_eq!(constraint.c.len(), deserialized.c.len()); - // Check individual elements 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); @@ -970,9 +1114,7 @@ mod serialization_tests { /// This test will fail if breaking changes are made without incrementing the version. #[test] fn test_deserialize_from_reference_binary_file() { - let test_data_path = std::path::Path::new("crates/core/test_data/constraint_system_v1.bin"); - - let binary_data = std::fs::read(test_data_path).unwrap(); + let binary_data = include_bytes!("../test_data/constraint_system_v1.bin"); let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap(); @@ -1004,80 +1146,69 @@ mod serialization_tests { } #[test] - fn test_public_witness_from_value_vec() { - let constraint_system = create_test_constraint_system(); - let value_vec = constraint_system.new_value_vec(); - - let public_witness: PublicWitness = (&value_vec).into(); - - assert_eq!(public_witness.len(), value_vec.public().len()); - assert_eq!(public_witness.as_slice(), value_vec.public()); - } - - #[test] - fn test_public_witness_serialization_round_trip_owned() { + 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 = PublicWitness::owned(data.clone()); + let witness = ValuesData::owned(data.clone()); let mut buf = Vec::new(); witness.serialize(&mut buf).unwrap(); - let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).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_public_witness_serialization_round_trip_borrowed() { + fn test_witness_serialization_round_trip_borrowed() { let data = vec![Word::from_u64(123), Word::from_u64(456)]; - let witness = PublicWitness::borrowed(&data); + let witness = ValuesData::borrowed(&data); let mut buf = Vec::new(); witness.serialize(&mut buf).unwrap(); - let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).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_public_witness_version_mismatch() { + 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 = PublicWitness::deserialize(&mut buf.as_slice()); + let result = ValuesData::deserialize(&mut buf.as_slice()); assert!(result.is_err()); match result.unwrap_err() { SerializationError::InvalidConstruction { name } => { - assert_eq!(name, "PublicWitness::version"); + assert_eq!(name, "Witness::version"); } _ => panic!("Expected version mismatch error"), } } - /// Helper function to create or update the reference binary file for PublicWitness version + /// Helper function to create or update the reference binary file for Witness version /// compatibility testing. #[test] - #[ignore] // Use `cargo test -- --ignored create_public_witness_reference_binary` to run this - fn create_public_witness_reference_binary_file() { + #[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 public_witness = PublicWitness::owned(data); + let witness = ValuesData::owned(data); let mut buf = Vec::new(); - public_witness.serialize(&mut buf).unwrap(); + witness.serialize(&mut buf).unwrap(); - let test_data_path = std::path::Path::new("crates/core/test_data/public_witness_v1.bin"); + 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(); @@ -1085,20 +1216,18 @@ mod serialization_tests { std::fs::write(test_data_path, &buf).unwrap(); - println!("Created PublicWitness reference binary file at: {:?}", test_data_path); + 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 PublicWitness version + /// 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_public_witness_deserialize_from_reference_binary_file() { - let test_data_path = std::path::Path::new("crates/core/test_data/public_witness_v1.bin"); - - let binary_data = std::fs::read(test_data_path).unwrap(); + fn test_witness_deserialize_from_reference_binary_file() { + let binary_data = include_bytes!("../test_data/witness_v1.bin"); - let deserialized = PublicWitness::deserialize(&mut binary_data.as_slice()).unwrap(); + let deserialized = ValuesData::deserialize(&mut binary_data.as_slice()).unwrap(); assert_eq!(deserialized.len(), 4); assert_eq!(deserialized.as_slice()[0].as_u64(), 1); @@ -1113,7 +1242,226 @@ mod serialization_tests { let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian assert_eq!( version_bytes, expected_version_bytes, - "PublicWitness binary file version mismatch. If you made breaking changes, increment PublicWitness::SERIALIZATION_VERSION" + "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/test_data/README.md b/crates/core/test_data/README.md index bd282aba4..21dd2c97d 100644 --- a/crates/core/test_data/README.md +++ b/crates/core/test_data/README.md @@ -6,6 +6,7 @@ This directory contains binary reference files used for testing serialization fo - `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 @@ -39,14 +40,33 @@ If you make intentional breaking changes to the serialization format: 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/crates/core/test_data/constraint_system_v1.bin b/crates/core/test_data/constraint_system_v1.bin similarity index 100% rename from crates/core/crates/core/test_data/constraint_system_v1.bin rename to crates/core/test_data/constraint_system_v1.bin diff --git a/crates/core/test_data/proof_v1.bin b/crates/core/test_data/proof_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..70ab9a326f7d0ce0908aaeb9901bd15c11734c82 GIT binary patch literal 60 zcmZQ%U|^5{Vvoe)jMO6MjKrLr)V%c6BAeihL?cr(JCGcsvTJ(h>a*|v-PtvxEW}9Q N-r9ZdPyO4q4*&+<7bO4y literal 0 HcmV?d00001 diff --git a/crates/core/crates/core/test_data/public_witness_v1.bin b/crates/core/test_data/witness_v1.bin similarity index 100% rename from crates/core/crates/core/test_data/public_witness_v1.bin rename to crates/core/test_data/witness_v1.bin From aff67880f66e991a670685d86067e34da7d7cfb7 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:07 +0000 Subject: [PATCH 4/6] Add save command to the examples (#874) # Add save subcommand to example CLI for artifact export ### TL;DR Adds a new `save` subcommand to the examples CLI that allows exporting circuit artifacts to files. ### What changed? - Added a new `save` subcommand to the example CLI that can export: - Constraint system binary - Public witness values - Non-public data values - Implemented a helper function `write_serialized` to handle serialization and file writing - Updated the README with documentation for all CLI subcommands, including detailed usage examples for the new `save` command ### How to test? Run any example with the new save subcommand: ``` # 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 artifacts 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 ``` ### Why make this change? This change enables developers to export circuit artifacts for external analysis, debugging, or integration with other tools. The ability to save constraint systems and witness data separately provides flexibility for different workflows and testing scenarios. --- crates/core/src/constraint_system.rs | 2 +- crates/examples/README.md | 42 +++++++++ crates/examples/src/cli.rs | 122 ++++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 2 deletions(-) diff --git a/crates/core/src/constraint_system.rs b/crates/core/src/constraint_system.rs index 6a6dc1a82..8334d27ba 100644 --- a/crates/core/src/constraint_system.rs +++ b/crates/core/src/constraint_system.rs @@ -665,7 +665,7 @@ impl<'a> std::ops::Deref for ValuesData<'a> { /// /// The proof contains: /// - `data`: The actual proof transcript as bytes (zero-copy with Cow) -/// - `challenger_type`: String identifying the challenger used (e.g., "HasherChallenger") +/// - `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 diff --git a/crates/examples/README.md b/crates/examples/README.md index fad6ea85e..08c3b4199 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`: 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: From 4b1cfa19c4610ec2932c1f22678808722a761fc9 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:08 +0000 Subject: [PATCH 5/6] Add prover binary (#875) ### TL;DR Added a standalone prover binary example that can generate proofs from serialized constraint systems and witnesses. ### What changed? - Added a new `prover` example binary that reads a constraint system and witness data from disk and produces a serialized proof - Implemented a `From>` trait for `Vec` to simplify conversion of witness data - Updated the examples README with documentation for the new prover binary, including usage instructions - Added the binary to the examples crate manifest ### How to test? 1. Generate artifacts from an example circuit: ``` 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 ``` 1. Produce a proof using the new prover binary: ``` 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 ``` ### Why make this change? This change enables cross-host proof generation pipelines by providing a standalone binary that can generate proofs from serialized inputs. This is useful for scenarios where the constraint system is generated on one machine but the proof needs to be generated on another, potentially more powerful machine optimized for proving. --- crates/core/src/constraint_system.rs | 6 ++ crates/examples/Cargo.toml | 6 +- crates/examples/README.md | 29 ++++++++ crates/examples/examples/prover.rs | 103 +++++++++++++++++++++++++++ crates/verifier/src/config.rs | 11 ++- 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 crates/examples/examples/prover.rs diff --git a/crates/core/src/constraint_system.rs b/crates/core/src/constraint_system.rs index 8334d27ba..34f7c5b31 100644 --- a/crates/core/src/constraint_system.rs +++ b/crates/core/src/constraint_system.rs @@ -654,6 +654,12 @@ impl<'a> std::ops::Deref for ValuesData<'a> { } } +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, diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 009ee2003..c385c8ce2 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -38,4 +38,8 @@ 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" diff --git a/crates/examples/README.md b/crates/examples/README.md index 08c3b4199..371102dc0 100644 --- a/crates/examples/README.md +++ b/crates/examples/README.md @@ -332,6 +332,35 @@ 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 +``` + ## 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/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; From 43c8daf1f2361b9f893e101c7c0da2a079d709f4 Mon Sep 17 00:00:00 2001 From: GraDKh <23423065+GraDKh@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:13:09 +0000 Subject: [PATCH 6/6] Add verifier binary (#876) ### TL;DR Add a verifier binary to verify the serialized proofs using the serialized constraint system and public witness data. ### What changed? - Added a new `verifier` example binary that reads a constraint system, public witness, and proof from disk and verifies the proof - Updated `Cargo.toml` to include the new verifier example - Added documentation for the verifier binary in the README, including usage instructions and command-line arguments ### How to test? Run the verifier on a proof generated by the prover example: ``` # First generate a proof with the prover cargo run --release --example prover -- \ --cs-path out/sha256/cs.bin \ --pub-witness-path out/sha256/public.bin \ --priv-witness-path out/sha256/private.bin \ --proof-path out/sha256/proof.bin \ --log-inv-rate 1 # Then verify the proof 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 ``` ### Why make this change? This completes the example workflow by providing a way to verify proofs generated by the prover example. The verifier demonstrates how to load and verify proofs, including checking that the challenger type in the proof matches the verifier's expected challenger type (HasherChallenger\). --- crates/examples/Cargo.toml | 4 ++ crates/examples/README.md | 25 ++++++++ crates/examples/examples/verifier.rs | 92 ++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 crates/examples/examples/verifier.rs diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index c385c8ce2..e029343d2 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -43,3 +43,7 @@ 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 371102dc0..bce7f7a36 100644 --- a/crates/examples/README.md +++ b/crates/examples/README.md @@ -361,6 +361,31 @@ cargo run --release --example prover -- \ --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/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(()) +}