Skip to main content

binius_core/constraint_system/
proof.rs

1// Copyright 2025 Irreducible Inc.
2use std::borrow::Cow;
3
4use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes};
5use bytes::{Buf, BufMut};
6
7/// A zero-knowledge proof that can be serialized for cross-host verification.
8///
9/// This structure contains the complete proof transcript generated by the prover,
10/// along with information about the challenger type needed for verification.
11/// The proof data represents the Fiat-Shamir transcript that can be deserialized
12/// by the verifier to recreate the interactive protocol.
13///
14/// # Design
15///
16/// The proof contains:
17/// - `data`: The actual proof transcript as bytes (zero-copy with Cow)
18/// - `challenger_type`: String identifying the challenger used (e.g., `"HasherChallenger<Sha256>"`)
19///
20/// This enables complete cross-host verification where a proof generated on one
21/// machine can be serialized, transmitted, and verified on another machine with
22/// the correct challenger configuration.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Proof<'a> {
25	data: Cow<'a, [u8]>,
26	challenger_type: String,
27}
28
29impl<'a> Proof<'a> {
30	/// Serialization format version for compatibility checking
31	pub const SERIALIZATION_VERSION: u32 = 1;
32
33	/// Create a new Proof from borrowed transcript data
34	pub const fn borrowed(data: &'a [u8], challenger_type: String) -> Self {
35		Self {
36			data: Cow::Borrowed(data),
37			challenger_type,
38		}
39	}
40
41	/// Create a new Proof from owned transcript data
42	pub const fn owned(data: Vec<u8>, challenger_type: String) -> Self {
43		Self {
44			data: Cow::Owned(data),
45			challenger_type,
46		}
47	}
48
49	/// Get the proof transcript data as a slice
50	pub fn as_slice(&self) -> &[u8] {
51		&self.data
52	}
53
54	/// Get the challenger type identifier
55	pub fn challenger_type(&self) -> &str {
56		&self.challenger_type
57	}
58
59	/// Get the number of bytes in the proof transcript
60	pub fn len(&self) -> usize {
61		self.data.len()
62	}
63
64	/// Check if the proof transcript is empty
65	pub fn is_empty(&self) -> bool {
66		self.data.is_empty()
67	}
68
69	/// Convert to owned data, consuming self
70	pub fn into_owned(self) -> (Vec<u8>, String) {
71		(self.data.into_owned(), self.challenger_type)
72	}
73
74	/// Convert to owned version of Proof
75	pub fn to_owned(&self) -> Proof<'static> {
76		Proof {
77			data: Cow::Owned(self.data.to_vec()),
78			challenger_type: self.challenger_type.clone(),
79		}
80	}
81}
82
83impl<'a> SerializeBytes for Proof<'a> {
84	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
85		Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?;
86
87		self.challenger_type.serialize(&mut write_buf)?;
88
89		self.data.as_ref().serialize(write_buf)
90	}
91}
92
93impl DeserializeBytes for Proof<'static> {
94	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
95	where
96		Self: Sized,
97	{
98		let version = u32::deserialize(&mut read_buf)?;
99		if version != Self::SERIALIZATION_VERSION {
100			return Err(SerializationError::InvalidConstruction {
101				name: "Proof::version",
102			});
103		}
104
105		let challenger_type = String::deserialize(&mut read_buf)?;
106		let data = Vec::<u8>::deserialize(read_buf)?;
107
108		Ok(Proof::owned(data, challenger_type))
109	}
110}
111
112impl<'a> From<(&'a [u8], String)> for Proof<'a> {
113	fn from((data, challenger_type): (&'a [u8], String)) -> Self {
114		Proof::borrowed(data, challenger_type)
115	}
116}
117
118impl From<(Vec<u8>, String)> for Proof<'static> {
119	fn from((data, challenger_type): (Vec<u8>, String)) -> Self {
120		Proof::owned(data, challenger_type)
121	}
122}
123
124impl<'a> AsRef<[u8]> for Proof<'a> {
125	fn as_ref(&self) -> &[u8] {
126		self.as_slice()
127	}
128}
129
130impl<'a> std::ops::Deref for Proof<'a> {
131	type Target = [u8];
132
133	fn deref(&self) -> &Self::Target {
134		self.as_slice()
135	}
136}
137
138#[cfg(test)]
139mod tests {
140	use rand::prelude::*;
141
142	use super::*;
143
144	#[test]
145	fn test_proof_serialization_round_trip_owned() {
146		let transcript_data = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
147		let challenger_type = "HasherChallenger<Sha256>".to_string();
148		let proof = Proof::owned(transcript_data.clone(), challenger_type.clone());
149
150		let mut buf = Vec::new();
151		proof.serialize(&mut buf).unwrap();
152
153		let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap();
154		assert_eq!(proof, deserialized);
155		assert_eq!(deserialized.as_slice(), transcript_data.as_slice());
156		assert_eq!(deserialized.challenger_type(), &challenger_type);
157	}
158
159	#[test]
160	fn test_proof_serialization_round_trip_borrowed() {
161		let transcript_data = vec![0xAA, 0xBB, 0xCC, 0xDD];
162		let challenger_type = "TestChallenger".to_string();
163		let proof = Proof::borrowed(&transcript_data, challenger_type.clone());
164
165		let mut buf = Vec::new();
166		proof.serialize(&mut buf).unwrap();
167
168		let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap();
169		assert_eq!(proof, deserialized);
170		assert_eq!(deserialized.as_slice(), transcript_data.as_slice());
171		assert_eq!(deserialized.challenger_type(), &challenger_type);
172	}
173
174	#[test]
175	fn test_proof_empty_transcript() {
176		let proof = Proof::owned(vec![], "EmptyProof".to_string());
177		assert!(proof.is_empty());
178		assert_eq!(proof.len(), 0);
179
180		let mut buf = Vec::new();
181		proof.serialize(&mut buf).unwrap();
182
183		let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap();
184		assert_eq!(proof, deserialized);
185		assert!(deserialized.is_empty());
186	}
187
188	#[test]
189	fn test_proof_large_transcript() {
190		let mut rng = StdRng::seed_from_u64(12345);
191		let mut large_data = vec![0u8; 10000];
192		rng.fill_bytes(&mut large_data);
193
194		let challenger_type = "LargeProofChallenger".to_string();
195		let proof = Proof::owned(large_data.clone(), challenger_type.clone());
196
197		let mut buf = Vec::new();
198		proof.serialize(&mut buf).unwrap();
199
200		let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap();
201		assert_eq!(proof, deserialized);
202		assert_eq!(deserialized.len(), 10000);
203		assert_eq!(deserialized.challenger_type(), &challenger_type);
204	}
205
206	#[test]
207	fn test_proof_version_mismatch() {
208		let mut buf = Vec::new();
209		999u32.serialize(&mut buf).unwrap(); // Wrong version
210		"TestChallenger".serialize(&mut buf).unwrap(); // Some challenger type
211		vec![0xAAu8].serialize(&mut buf).unwrap(); // Some data
212
213		let result = Proof::deserialize(&mut buf.as_slice());
214		assert!(result.is_err());
215		match result.unwrap_err() {
216			SerializationError::InvalidConstruction { name } => {
217				assert_eq!(name, "Proof::version");
218			}
219			_ => panic!("Expected version mismatch error"),
220		}
221	}
222
223	#[test]
224	fn test_proof_into_owned() {
225		let original_data = vec![1, 2, 3, 4, 5];
226		let original_challenger = "TestChallenger".to_string();
227		let proof = Proof::owned(original_data.clone(), original_challenger.clone());
228
229		let (data, challenger_type) = proof.into_owned();
230		assert_eq!(data, original_data);
231		assert_eq!(challenger_type, original_challenger);
232	}
233
234	#[test]
235	fn test_proof_to_owned() {
236		let data = vec![0xFF, 0xEE, 0xDD];
237		let challenger_type = "BorrowedChallenger".to_string();
238		let borrowed_proof = Proof::borrowed(&data, challenger_type.clone());
239
240		let owned_proof = borrowed_proof.to_owned();
241		assert_eq!(owned_proof.as_slice(), data);
242		assert_eq!(owned_proof.challenger_type(), &challenger_type);
243		// Verify it's truly owned (not just borrowed)
244		drop(data); // This would fail if owned_proof was still borrowing
245		assert_eq!(owned_proof.len(), 3);
246	}
247
248	#[test]
249	fn test_proof_different_challenger_types() {
250		let data = vec![0x42];
251		let challengers = vec![
252			"HasherChallenger<Sha256>".to_string(),
253			"HasherChallenger<Blake2b>".to_string(),
254			"CustomChallenger".to_string(),
255			"".to_string(), // Empty string should also work
256		];
257
258		for challenger_type in challengers {
259			let proof = Proof::owned(data.clone(), challenger_type.clone());
260			let mut buf = Vec::new();
261			proof.serialize(&mut buf).unwrap();
262
263			let deserialized = Proof::deserialize(&mut buf.as_slice()).unwrap();
264			assert_eq!(deserialized.challenger_type(), &challenger_type);
265		}
266	}
267
268	#[test]
269	fn test_proof_serialization_with_different_sources() {
270		let transcript_data = vec![0x11, 0x22, 0x33, 0x44];
271		let challenger_type = "MultiSourceChallenger".to_string();
272		let original = Proof::owned(transcript_data, challenger_type);
273
274		// Test with Vec<u8> (memory buffer)
275		let mut vec_buf = Vec::new();
276		original.serialize(&mut vec_buf).unwrap();
277		let deserialized1 = Proof::deserialize(&mut vec_buf.as_slice()).unwrap();
278		assert_eq!(original, deserialized1);
279
280		// Test with bytes::BytesMut (another common buffer type)
281		let mut bytes_buf = bytes::BytesMut::new();
282		original.serialize(&mut bytes_buf).unwrap();
283		let deserialized2 = Proof::deserialize(bytes_buf.freeze()).unwrap();
284		assert_eq!(original, deserialized2);
285	}
286
287	/// Helper function to create or update the reference binary file for Proof version
288	/// compatibility testing.
289	#[test]
290	#[ignore] // Use `cargo test -- --ignored create_proof_reference_binary` to run this
291	fn create_proof_reference_binary_file() {
292		let transcript_data = vec![
293			0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54,
294			0x32, 0x10, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
295		];
296		let challenger_type = "HasherChallenger<Sha256>".to_string();
297		let proof = Proof::owned(transcript_data, challenger_type);
298
299		let mut buf = Vec::new();
300		proof.serialize(&mut buf).unwrap();
301
302		let test_data_path = std::path::Path::new("verifier/core/test_data/proof_v1.bin");
303
304		if let Some(parent) = test_data_path.parent() {
305			std::fs::create_dir_all(parent).unwrap();
306		}
307
308		std::fs::write(test_data_path, &buf).unwrap();
309
310		println!("Created Proof reference binary file at: {:?}", test_data_path);
311		println!("Binary data length: {} bytes", buf.len());
312	}
313
314	/// Test deserialization from a reference binary file to ensure Proof version
315	/// compatibility. This test will fail if breaking changes are made without incrementing the
316	/// version.
317	#[test]
318	fn test_proof_deserialize_from_reference_binary_file() {
319		let binary_data = include_bytes!("../../test_data/proof_v1.bin");
320
321		let deserialized = Proof::deserialize(&mut binary_data.as_slice()).unwrap();
322
323		assert_eq!(deserialized.len(), 24); // 24 bytes of transcript data
324		assert_eq!(deserialized.challenger_type(), "HasherChallenger<Sha256>");
325
326		let expected_data = vec![
327			0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54,
328			0x32, 0x10, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
329		];
330		assert_eq!(deserialized.as_slice(), expected_data);
331
332		// Verify that the version is what we expect
333		// This is implicitly checked during deserialization, but we can also verify
334		// the file starts with the correct version bytes
335		let version_bytes = &binary_data[0..4]; // First 4 bytes should be version
336		let expected_version_bytes = 1u32.to_le_bytes(); // Version 1 in little-endian
337		assert_eq!(
338			version_bytes, expected_version_bytes,
339			"Proof binary file version mismatch. If you made breaking changes, increment Proof::SERIALIZATION_VERSION"
340		);
341	}
342}