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}