Skip to content

Commit 8e0f8ff

Browse files
GraDKhlockedloop
authored andcommitted
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>`, `&[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.
1 parent 41e3f85 commit 8e0f8ff

File tree

3 files changed

+230
-32
lines changed

3 files changed

+230
-32
lines changed
40 Bytes
Binary file not shown.

crates/core/src/constraint_system.rs

Lines changed: 219 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
borrow::Cow,
23
cmp,
34
ops::{Index, IndexMut},
45
};
@@ -521,6 +522,124 @@ impl IndexMut<ValueIndex> for ValueVec {
521522
}
522523
}
523524

525+
/// Public witness data for zero-knowledge proofs.
526+
///
527+
/// This structure holds the public portion of witness data that needs to be shared
528+
/// with verifiers. It uses `Cow<[Word]>` to avoid unnecessary clones while supporting
529+
/// both borrowed and owned data.
530+
///
531+
/// The public witness consists of:
532+
/// - Constants: Fixed values defined in the constraint system
533+
/// - Inputs/Outputs: Public values that are part of the statement being proven
534+
#[derive(Clone, Debug, PartialEq, Eq)]
535+
pub struct PublicWitness<'a> {
536+
data: Cow<'a, [Word]>,
537+
}
538+
539+
impl<'a> PublicWitness<'a> {
540+
/// Serialization format version for compatibility checking
541+
pub const SERIALIZATION_VERSION: u32 = 1;
542+
543+
/// Create a new PublicWitness from borrowed data
544+
pub fn borrowed(data: &'a [Word]) -> Self {
545+
Self {
546+
data: Cow::Borrowed(data),
547+
}
548+
}
549+
550+
/// Create a new PublicWitness from owned data
551+
pub fn owned(data: Vec<Word>) -> Self {
552+
Self {
553+
data: Cow::Owned(data),
554+
}
555+
}
556+
557+
/// Get the public witness data as a slice
558+
pub fn as_slice(&self) -> &[Word] {
559+
&self.data
560+
}
561+
562+
/// Get the number of words in the public witness
563+
pub fn len(&self) -> usize {
564+
self.data.len()
565+
}
566+
567+
/// Check if the public witness is empty
568+
pub fn is_empty(&self) -> bool {
569+
self.data.is_empty()
570+
}
571+
572+
/// Convert to owned data, consuming self
573+
pub fn into_owned(self) -> Vec<Word> {
574+
self.data.into_owned()
575+
}
576+
577+
/// Convert to owned version of PublicWitness
578+
pub fn to_owned(&self) -> PublicWitness<'static> {
579+
PublicWitness {
580+
data: Cow::Owned(self.data.to_vec()),
581+
}
582+
}
583+
}
584+
585+
impl<'a> SerializeBytes for PublicWitness<'a> {
586+
fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
587+
Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?;
588+
589+
self.data.as_ref().serialize(write_buf)
590+
}
591+
}
592+
593+
impl DeserializeBytes for PublicWitness<'static> {
594+
fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
595+
where
596+
Self: Sized,
597+
{
598+
let version = u32::deserialize(&mut read_buf)?;
599+
if version != Self::SERIALIZATION_VERSION {
600+
return Err(SerializationError::InvalidConstruction {
601+
name: "PublicWitness::version",
602+
});
603+
}
604+
605+
let data = Vec::<Word>::deserialize(read_buf)?;
606+
607+
Ok(PublicWitness::owned(data))
608+
}
609+
}
610+
611+
impl<'a> From<&'a [Word]> for PublicWitness<'a> {
612+
fn from(data: &'a [Word]) -> Self {
613+
PublicWitness::borrowed(data)
614+
}
615+
}
616+
617+
impl From<Vec<Word>> for PublicWitness<'static> {
618+
fn from(data: Vec<Word>) -> Self {
619+
PublicWitness::owned(data)
620+
}
621+
}
622+
623+
impl<'a> From<&'a ValueVec> for PublicWitness<'a> {
624+
fn from(value_vec: &'a ValueVec) -> Self {
625+
PublicWitness::borrowed(value_vec.public())
626+
}
627+
}
628+
629+
impl<'a> AsRef<[Word]> for PublicWitness<'a> {
630+
fn as_ref(&self) -> &[Word] {
631+
self.as_slice()
632+
}
633+
}
634+
635+
impl<'a> std::ops::Deref for PublicWitness<'a> {
636+
type Target = [Word];
637+
638+
fn deref(&self) -> &Self::Target {
639+
self.as_slice()
640+
}
641+
}
642+
524643
#[cfg(test)]
525644
mod serialization_tests {
526645
use rand::{RngCore, SeedableRng, rngs::StdRng};
@@ -853,27 +972,10 @@ mod serialization_tests {
853972
fn test_deserialize_from_reference_binary_file() {
854973
let test_data_path = std::path::Path::new("crates/core/test_data/constraint_system_v1.bin");
855974

856-
// If the reference file doesn't exist, create it first
857-
if !test_data_path.exists() {
858-
let constraint_system = create_test_constraint_system();
859-
let mut buf = Vec::new();
860-
constraint_system.serialize(&mut buf).unwrap();
861-
862-
// Create directory if it doesn't exist
863-
if let Some(parent) = test_data_path.parent() {
864-
std::fs::create_dir_all(parent).unwrap();
865-
}
866-
867-
std::fs::write(test_data_path, &buf).unwrap();
868-
}
869-
870-
// Read the reference binary file
871975
let binary_data = std::fs::read(test_data_path).unwrap();
872976

873-
// Deserialize and verify it works
874977
let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap();
875978

876-
// Verify the deserialized data has expected structure
877979
assert_eq!(deserialized.value_vec_layout.n_const, 3);
878980
assert_eq!(deserialized.value_vec_layout.n_inout, 2);
879981
assert_eq!(deserialized.value_vec_layout.n_witness, 10);
@@ -901,32 +1003,117 @@ mod serialization_tests {
9011003
);
9021004
}
9031005

904-
/// Test that demonstrates what happens when there's a version mismatch.
905-
/// This serves as documentation for the version compatibility system.
9061006
#[test]
907-
fn test_version_compatibility_documentation() {
908-
// Create a constraint system and serialize it
1007+
fn test_public_witness_from_value_vec() {
9091008
let constraint_system = create_test_constraint_system();
1009+
let value_vec = constraint_system.new_value_vec();
1010+
1011+
let public_witness: PublicWitness = (&value_vec).into();
1012+
1013+
assert_eq!(public_witness.len(), value_vec.public().len());
1014+
assert_eq!(public_witness.as_slice(), value_vec.public());
1015+
}
1016+
1017+
#[test]
1018+
fn test_public_witness_serialization_round_trip_owned() {
1019+
let data = vec![
1020+
Word::from_u64(1),
1021+
Word::from_u64(42),
1022+
Word::from_u64(0xDEADBEEF),
1023+
Word::from_u64(0x1234567890ABCDEF),
1024+
];
1025+
let witness = PublicWitness::owned(data.clone());
1026+
9101027
let mut buf = Vec::new();
911-
constraint_system.serialize(&mut buf).unwrap();
1028+
witness.serialize(&mut buf).unwrap();
9121029

913-
// Verify current version works
914-
let deserialized = ConstraintSystem::deserialize(&mut buf.as_slice()).unwrap();
915-
assert_eq!(constraint_system.constants.len(), deserialized.constants.len());
1030+
let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).unwrap();
1031+
assert_eq!(witness, deserialized);
1032+
assert_eq!(deserialized.as_slice(), data.as_slice());
1033+
}
1034+
1035+
#[test]
1036+
fn test_public_witness_serialization_round_trip_borrowed() {
1037+
let data = vec![Word::from_u64(123), Word::from_u64(456)];
1038+
let witness = PublicWitness::borrowed(&data);
1039+
1040+
let mut buf = Vec::new();
1041+
witness.serialize(&mut buf).unwrap();
9161042

917-
// Now simulate an older version by modifying the version bytes
918-
let mut modified_buf = buf.clone();
919-
let old_version = 0u32; // Simulate version 0
920-
modified_buf[0..4].copy_from_slice(&old_version.to_le_bytes());
1043+
let deserialized = PublicWitness::deserialize(&mut buf.as_slice()).unwrap();
1044+
assert_eq!(witness, deserialized);
1045+
assert_eq!(deserialized.as_slice(), data.as_slice());
1046+
}
1047+
1048+
#[test]
1049+
fn test_public_witness_version_mismatch() {
1050+
let mut buf = Vec::new();
1051+
999u32.serialize(&mut buf).unwrap(); // Wrong version
1052+
vec![Word::from_u64(1)].serialize(&mut buf).unwrap(); // Some data
9211053

922-
// This should fail with version mismatch
923-
let result = ConstraintSystem::deserialize(&mut modified_buf.as_slice());
1054+
let result = PublicWitness::deserialize(&mut buf.as_slice());
9241055
assert!(result.is_err());
9251056
match result.unwrap_err() {
9261057
SerializationError::InvalidConstruction { name } => {
927-
assert_eq!(name, "ConstraintSystem::version");
1058+
assert_eq!(name, "PublicWitness::version");
9281059
}
9291060
_ => panic!("Expected version mismatch error"),
9301061
}
9311062
}
1063+
1064+
/// Helper function to create or update the reference binary file for PublicWitness version
1065+
/// compatibility testing.
1066+
#[test]
1067+
#[ignore] // Use `cargo test -- --ignored create_public_witness_reference_binary` to run this
1068+
fn create_public_witness_reference_binary_file() {
1069+
let data = vec![
1070+
Word::from_u64(1),
1071+
Word::from_u64(42),
1072+
Word::from_u64(0xDEADBEEF),
1073+
Word::from_u64(0x1234567890ABCDEF),
1074+
];
1075+
let public_witness = PublicWitness::owned(data);
1076+
1077+
let mut buf = Vec::new();
1078+
public_witness.serialize(&mut buf).unwrap();
1079+
1080+
let test_data_path = std::path::Path::new("crates/core/test_data/public_witness_v1.bin");
1081+
1082+
if let Some(parent) = test_data_path.parent() {
1083+
std::fs::create_dir_all(parent).unwrap();
1084+
}
1085+
1086+
std::fs::write(test_data_path, &buf).unwrap();
1087+
1088+
println!("Created PublicWitness reference binary file at: {:?}", test_data_path);
1089+
println!("Binary data length: {} bytes", buf.len());
1090+
}
1091+
1092+
/// Test deserialization from a reference binary file to ensure PublicWitness version
1093+
/// compatibility. This test will fail if breaking changes are made without incrementing the
1094+
/// version.
1095+
#[test]
1096+
fn test_public_witness_deserialize_from_reference_binary_file() {
1097+
let test_data_path = std::path::Path::new("crates/core/test_data/public_witness_v1.bin");
1098+
1099+
let binary_data = std::fs::read(test_data_path).unwrap();
1100+
1101+
let deserialized = PublicWitness::deserialize(&mut binary_data.as_slice()).unwrap();
1102+
1103+
assert_eq!(deserialized.len(), 4);
1104+
assert_eq!(deserialized.as_slice()[0].as_u64(), 1);
1105+
assert_eq!(deserialized.as_slice()[1].as_u64(), 42);
1106+
assert_eq!(deserialized.as_slice()[2].as_u64(), 0xDEADBEEF);
1107+
assert_eq!(deserialized.as_slice()[3].as_u64(), 0x1234567890ABCDEF);
1108+
1109+
// Verify that the version is what we expect
1110+
// This is implicitly checked during deserialization, but we can also verify
1111+
// the file starts with the correct version bytes
1112+
let version_bytes = &binary_data[0..4]; // First 4 bytes should be version
1113+
let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian
1114+
assert_eq!(
1115+
version_bytes, expected_version_bytes,
1116+
"PublicWitness binary file version mismatch. If you made breaking changes, increment PublicWitness::SERIALIZATION_VERSION"
1117+
);
1118+
}
9321119
}

crates/core/test_data/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This directory contains binary reference files used for testing serialization fo
55
## Files
66

77
- `constraint_system_v1.bin`: Reference binary serialization of a `ConstraintSystem` using serialization version 1.
8+
- `public_witness_v1.bin`: Reference binary serialization of a `PublicWitness` using serialization version 1.
89

910
## Purpose
1011

@@ -20,6 +21,7 @@ These binary files serve as regression tests to ensure that:
2021

2122
If you make intentional breaking changes to the serialization format:
2223

24+
### For ConstraintSystem
2325
1. Increment `ConstraintSystem::SERIALIZATION_VERSION`
2426
2. Run the ignored test to regenerate the reference file:
2527
```bash
@@ -28,6 +30,15 @@ If you make intentional breaking changes to the serialization format:
2830
3. Rename the new file to include the new version number
2931
4. Update test paths to reference the new file
3032

33+
### For PublicWitness
34+
1. Increment `PublicWitness::SERIALIZATION_VERSION`
35+
2. Run the ignored test to regenerate the reference file:
36+
```bash
37+
cargo test -p binius-core -- --ignored create_public_witness_reference_binary
38+
```
39+
3. Rename the new file to include the new version number
40+
4. Update test paths to reference the new file
41+
3142
## Binary Format
3243

3344
The binary format uses little-endian encoding and follows this structure:

0 commit comments

Comments
 (0)