Skip to main content

binius_core/constraint_system/
values_data.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::ops::Deref;
4
5use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes};
6use bytes::{Buf, BufMut};
7
8use crate::word::Word;
9
10/// A run of value-vector words, decoded from its versioned on-disk form.
11///
12/// The words of one proving job travel as two files, holding what the circuit itself does not fix:
13///
14/// ```text
15///     file        | holds                        | who reads it
16///     ------------+------------------------------+------------------
17///     inout       | inputs and outputs           | prover, verifier
18///     non-public  | witness and internal values  | prover only
19/// ```
20///
21/// Neither file holds the circuit's constants.
22/// Those are fixed for every instance, so they stay in the constraint system.
23/// Rebuilding the public segment puts them back in front of the words a file carries.
24///
25/// Those two files plus the circuit's constraint system are all another host needs.
26/// From the three it rebuilds the witness and proves against it.
27///
28/// This is the owned end of the format, the one decoding produces.
29/// It owns its words because the byte buffer they came from need not outlive the call.
30/// Writing starts from the borrowed counterpart below, which copies nothing.
31#[derive(Clone, Debug)]
32pub struct ValuesData(Vec<Word>);
33
34impl ValuesData {
35	/// Version of the byte layout, written ahead of the words in both directions.
36	///
37	/// # Why this exists
38	///
39	/// An older layout would decode into plausible but wrong words.
40	/// A wrong witness proves nothing, so the mismatch has to surface here.
41	/// Bumping this on any layout change turns silent corruption into a hard error.
42	pub const SERIALIZATION_VERSION: u32 = 1;
43}
44
45impl Deref for ValuesData {
46	type Target = [Word];
47
48	fn deref(&self) -> &[Word] {
49		// A segment is read-only once decoded, so it is handed out as a plain word slice.
50		&self.0
51	}
52}
53
54impl DeserializeBytes for ValuesData {
55	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError> {
56		// A mismatched tag means the words that follow were written to another layout.
57		let version = u32::deserialize(&mut read_buf)?;
58		if version != Self::SERIALIZATION_VERSION {
59			return Err(SerializationError::InvalidConstruction {
60				name: "ValuesData::version",
61			});
62		}
63
64		// The word count leads the words, so a short buffer fails instead of truncating.
65		Ok(Self(Vec::deserialize(read_buf)?))
66	}
67}
68
69/// A segment of a value vector borrowed straight from a witness, ready to write.
70///
71/// This is the borrowed end of the format.
72/// It relates to the owned counterpart above as a string slice relates to an owned string.
73///
74/// Borrowing is what keeps writing cheap.
75/// A witness segment runs to tens of megabytes, and none of it is copied to reach the buffer.
76pub struct ValuesRef<'a>(&'a [Word]);
77
78impl<'a> ValuesRef<'a> {
79	/// Wraps one segment of a value vector for writing.
80	pub const fn new(words: &'a [Word]) -> Self {
81		Self(words)
82	}
83}
84
85impl SerializeBytes for ValuesRef<'_> {
86	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
87		// The tag leads the bytes, so a reader can reject the layout before decoding any word.
88		ValuesData::SERIALIZATION_VERSION.serialize(&mut write_buf)?;
89
90		// Then a word count, then that many little-endian words.
91		self.0.serialize(write_buf)
92	}
93}
94
95#[cfg(test)]
96mod tests {
97	use std::{fs, path::Path};
98
99	use proptest::{collection, prelude::any, prop_assert_eq, proptest};
100
101	use super::*;
102
103	// The four words held by the committed reference file.
104	fn reference_words() -> [Word; 4] {
105		[
106			Word::from_u64(1),
107			Word::from_u64(42),
108			Word::from_u64(0xDEAD_BEEF),
109			Word::from_u64(0x1234_5678_90AB_CDEF),
110		]
111	}
112
113	proptest! {
114		#[test]
115		fn round_trip_preserves_words(words in collection::vec(any::<u64>(), 0..64usize)) {
116			// Invariant: the words read back are the words written, in order.
117			//
118			// Fixture state: 0 to 63 arbitrary words, so the empty segment is covered too.
119			let words: Vec<Word> = words.into_iter().map(Word).collect();
120
121			// Writing borrows the slice, reading returns an owned segment:
122			//
123			//     words in --write--> [ 1 | n | word_0 .. word_n-1 ] --read--> words out
124			let mut buf = Vec::new();
125			ValuesRef::new(&words).serialize(&mut buf).unwrap();
126			let read = ValuesData::deserialize(buf.as_slice()).unwrap();
127
128			prop_assert_eq!(&*read, &words[..]);
129		}
130	}
131
132	#[test]
133	fn deserialize_rejects_version_mismatch() {
134		// Invariant: a segment written to an unknown layout is rejected, never decoded.
135		//
136		// Fixture state: one word, tagged one version past the current one.
137		//
138		//     on disk:  [ 2 | 1 | word_0 ]
139		//     expected:   1
140		//     -> reject without reading word_0
141		let mut buf = Vec::new();
142		(ValuesData::SERIALIZATION_VERSION + 1)
143			.serialize(&mut buf)
144			.unwrap();
145		vec![Word::ONE].serialize(&mut buf).unwrap();
146
147		match ValuesData::deserialize(buf.as_slice()).unwrap_err() {
148			SerializationError::InvalidConstruction { name } => {
149				assert_eq!(name, "ValuesData::version");
150			}
151			other => panic!("Expected InvalidConstruction, got: {other:?}"),
152		}
153	}
154
155	#[test]
156	fn deserialize_rejects_truncated_segment() {
157		// Invariant: a file cut short fails, rather than yielding a shorter segment.
158		//
159		// Fixture state: two words written, then one byte dropped.
160		//
161		//     written:  [ 1 | 2 | word_0 | word_1 ]   4 + 4 + 8 + 8 = 24 bytes
162		//     on disk:  same, minus one byte          23 bytes
163		//     -> the count promises 16 bytes of words, 15 remain
164		let mut buf = Vec::new();
165		ValuesRef::new(&[Word::ONE, Word::ALL_ONE])
166			.serialize(&mut buf)
167			.unwrap();
168		buf.truncate(buf.len() - 1);
169
170		match ValuesData::deserialize(buf.as_slice()).unwrap_err() {
171			SerializationError::NotEnoughBytes => {}
172			other => panic!("Expected NotEnoughBytes, got: {other:?}"),
173		}
174	}
175
176	#[test]
177	fn reference_binary_deserializes_at_current_version() {
178		// Invariant: the committed file still decodes to the words it was written from.
179		//
180		// This is what forces a layout change to bump the version tag.
181		// Change the bytes without touching the tag, and the words stop matching.
182		let bytes = include_bytes!("../../test_data/values_data_v1.bin");
183
184		// The tag occupies the leading four bytes, little-endian.
185		assert_eq!(
186			&bytes[..4],
187			&ValuesData::SERIALIZATION_VERSION.to_le_bytes(),
188			"reference file version mismatch: regenerate it with the ignored test below"
189		);
190
191		let read = ValuesData::deserialize(bytes.as_slice()).unwrap();
192		assert_eq!(&*read, &reference_words()[..]);
193	}
194
195	// Regenerates the reference file after an intentional layout change.
196	// Run: `cargo test -p binius-core -- --ignored create_values_data_reference_binary`.
197	#[test]
198	#[ignore]
199	fn create_values_data_reference_binary_file() {
200		let mut buf = Vec::new();
201		ValuesRef::new(&reference_words())
202			.serialize(&mut buf)
203			.unwrap();
204
205		// Relative to the crate root, which is the working directory of a test run.
206		let path = Path::new("test_data/values_data_v1.bin");
207		fs::write(path, &buf).unwrap();
208
209		println!("Wrote {} bytes to {}", buf.len(), path.display());
210	}
211}