binius_hash/vision/
digest.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use std::marker::PhantomData;
4
5use binius_field::{
6	as_packed_field::{PackScalar, PackedType},
7	make_aes_to_binary_packed_transformer, make_binary_to_aes_packed_transformer,
8	packed::set_packed_slice,
9	underlier::{Divisible, WithUnderlier},
10	AESTowerField32b, AesToBinaryTransformation, BinaryField, BinaryField32b, BinaryField8b,
11	BinaryToAesTransformation, ExtensionField, Field, PackedAESBinaryField8x32b,
12	PackedBinaryField8x32b, PackedExtension, PackedExtensionIndexable, PackedField,
13	PackedFieldIndexable,
14};
15use lazy_static::lazy_static;
16
17use super::permutation::PERMUTATION;
18use crate::{permutation::Permutation, FixedLenHasher, HashError};
19
20const RATE_AS_U32: usize = 16;
21const RATE_AS_U8: usize = RATE_AS_U32 * std::mem::size_of::<u32>();
22
23const PADDING_START: u8 = 0x01;
24const PADDING_END: u8 = 0x80;
25
26lazy_static! {
27	static ref TRANS_AES_TO_CANONICAL: AesToBinaryTransformation<PackedAESBinaryField8x32b, PackedBinaryField8x32b> =
28		make_aes_to_binary_packed_transformer::<PackedAESBinaryField8x32b, PackedBinaryField8x32b>();
29	static ref TRANS_CANONICAL_TO_AES: BinaryToAesTransformation<PackedBinaryField8x32b, PackedAESBinaryField8x32b> =
30		make_binary_to_aes_packed_transformer::<PackedBinaryField8x32b, PackedAESBinaryField8x32b>();
31
32	// Padding block for the case when the input is a multiple of the rate.
33	static ref PADDING_BLOCK: [u8; RATE_AS_U8] = {
34		let mut block = [0; RATE_AS_U8];
35		block[0] = PADDING_START;
36		block[RATE_AS_U8 - 1] |= PADDING_END;
37		block
38	};
39}
40
41/// The vision specialization over `BinaryField32b` as per [Vision Mark-32](https://eprint.iacr.org/2024/633)
42pub type Vision32b<P> = VisionHasher<BinaryField32b, P>;
43
44/// This is the struct that implements the Vision hash over `AESTowerField32b` and `BinaryField32b`
45/// isomorphically. Here the generic `P` represents the input type to the `update` function
46#[derive(Clone)]
47pub struct VisionHasher<F, P> {
48	// The hashed state
49	state: [PackedAESBinaryField8x32b; 3],
50	// The length that are committing to hash
51	committed_len: u64,
52	// Current length we have hashed so far
53	current_len: u64,
54	_p_marker: PhantomData<P>,
55	_f_marker: PhantomData<F>,
56}
57
58impl<U, F, P> FixedLenHasher<P> for VisionHasher<F, P>
59where
60	U: PackScalar<F> + Divisible<u32>,
61	F: BinaryField + From<AESTowerField32b> + Into<AESTowerField32b>,
62	P: PackedExtension<F, PackedSubfield: PackedFieldIndexable>,
63	PackedAESBinaryField8x32b: WithUnderlier<Underlier = U>,
64{
65	type Digest = PackedType<U, F>;
66
67	fn new(msg_len: u64) -> Self {
68		let mut this = Self {
69			state: [PackedAESBinaryField8x32b::zero(); 3],
70			committed_len: msg_len,
71			current_len: 0,
72			_p_marker: PhantomData,
73			_f_marker: PhantomData,
74		};
75		this.reset();
76		this
77	}
78
79	fn update(&mut self, msg: impl AsRef<[P]>) {
80		let msg = msg.as_ref();
81		if msg.is_empty() {
82			return;
83		}
84
85		let msg_scalars = P::unpack_base_scalars(msg).iter().copied().map(Into::into);
86
87		let cur_block = (self.current_len as usize * P::WIDTH * P::Scalar::DEGREE) % RATE_AS_U32;
88		for (i, x) in msg_scalars.enumerate() {
89			let block_idx = (cur_block + i) % RATE_AS_U32;
90			let next_block = PackedAESBinaryField8x32b::unpack_scalars_mut(&mut self.state);
91			next_block[block_idx] = x;
92			if block_idx == RATE_AS_U32 - 1 {
93				self.state = PERMUTATION.permute(self.state);
94			}
95		}
96
97		self.current_len = self
98			.current_len
99			.checked_add(msg.len() as u64)
100			.expect("Overflow on message length");
101	}
102
103	fn chain_update(mut self, msg: impl AsRef<[P]>) -> Self {
104		self.update(msg);
105		self
106	}
107
108	fn finalize(mut self) -> Result<Self::Digest, HashError> {
109		// Pad here and output the hash
110		if self.current_len < self.committed_len {
111			return Err(HashError::NotEnoughData {
112				committed: self.committed_len,
113				hashed: self.current_len,
114			});
115		}
116
117		if self.current_len > self.committed_len {
118			return Err(HashError::TooMuchData {
119				committed: self.committed_len,
120				received: self.current_len,
121			});
122		}
123
124		let cur_block = (self.current_len as usize * P::WIDTH * P::Scalar::DEGREE) % RATE_AS_U32;
125		if cur_block != 0 {
126			// Pad and absorb
127			let next_block = PackedFieldIndexable::unpack_scalars_mut(&mut self.state[..2]);
128			next_block[cur_block..].fill(AESTowerField32b::ZERO);
129			self.state = PERMUTATION.permute(self.state);
130		}
131
132		let out_native = self.state[0];
133		Ok(Self::Digest::from_fn(|i| F::from(out_native.get(i))))
134	}
135
136	fn reset(&mut self) {
137		self.state.fill(PackedAESBinaryField8x32b::zero());
138
139		// Write the byte-length of the message into the initial state
140		let bytes_per_elem = P::WIDTH
141			* P::Scalar::DEGREE
142			* <BinaryField32b as ExtensionField<BinaryField8b>>::DEGREE;
143		let msg_len_bytes = self
144			.committed_len
145			.checked_mul(bytes_per_elem as u64)
146			.expect("Overflow on message length");
147		let msg_len_bytes_enc = msg_len_bytes.to_le_bytes();
148		set_packed_slice(
149			&mut self.state,
150			RATE_AS_U32,
151			AESTowerField32b::from(BinaryField32b::new(u32::from_le_bytes(
152				msg_len_bytes_enc[0..4].try_into().unwrap(),
153			))),
154		);
155		set_packed_slice(
156			&mut self.state,
157			RATE_AS_U32 + 1,
158			AESTowerField32b::from(BinaryField32b::new(u32::from_le_bytes(
159				msg_len_bytes_enc[4..8].try_into().unwrap(),
160			))),
161		);
162	}
163}
164
165#[cfg(test)]
166mod tests {
167	use std::array;
168
169	use binius_field::{
170		linear_transformation::Transformation, make_aes_to_binary_packed_transformer,
171		make_binary_to_aes_packed_transformer, BinaryField64b, PackedAESBinaryField4x64b,
172		PackedBinaryField4x64b, PackedBinaryField8x32b,
173	};
174	use hex_literal::hex;
175	use rand::thread_rng;
176
177	use super::*;
178	use crate::{FixedLenHasherDigest, HashDigest};
179
180	fn from_bytes_to_packed_256(elements: &[u8; 32]) -> PackedBinaryField8x32b {
181		PackedBinaryField8x32b::from_fn(|i| {
182			BinaryField32b::new(u32::from_le_bytes(elements[i * 4..i * 4 + 4].try_into().unwrap()))
183		})
184	}
185
186	#[test]
187	fn test_fixed_length_too_much_data() {
188		let mut hasher = Vision32b::new(20);
189		let data = [BinaryField32b::zero(); 30];
190		hasher.update(data);
191		let successful_failure = matches!(
192			hasher.finalize().err().unwrap(),
193			HashError::TooMuchData {
194				committed: 20,
195				received: 30,
196			}
197		);
198		assert!(successful_failure);
199	}
200
201	#[test]
202	fn test_fixed_length_not_enough_data() {
203		let mut hasher = Vision32b::new(20);
204		let data = [BinaryField32b::zero(); 3];
205		hasher.update(data);
206		let successful_failure = matches!(
207			hasher.finalize().err().unwrap(),
208			HashError::NotEnoughData {
209				committed: 20,
210				hashed: 3,
211			}
212		);
213		assert!(successful_failure);
214	}
215
216	#[test]
217	fn test_empty_input_error() {
218		let hasher = Vision32b::<BinaryField32b>::new(0);
219		assert_eq!(hasher.finalize().unwrap(), PackedBinaryField8x32b::zero());
220
221		let hasher: Vision32b<BinaryField32b> = Vision32b::new(25);
222		let successful_failure = matches!(
223			hasher.chain_update([]).finalize().err().unwrap(),
224			HashError::NotEnoughData {
225				committed: 25,
226				hashed: 0
227			}
228		);
229		assert!(successful_failure);
230
231		let hasher: Vision32b<BinaryField32b> = Vision32b::new(25);
232		let successful_failure = matches!(
233			hasher.finalize().err().unwrap(),
234			HashError::NotEnoughData {
235				committed: 25,
236				hashed: 0,
237			}
238		);
239		assert!(successful_failure);
240	}
241
242	#[test]
243	fn test_simple_hash() {
244		let mut hasher: Vision32b<BinaryField32b> = Vision32b::new(1);
245		hasher.update([BinaryField32b::new(u32::from_le_bytes([
246			0xde, 0xad, 0xbe, 0xef,
247		]))]);
248		let out = hasher.finalize().unwrap();
249		// This hash is retrieved from a modified python implementation with the proposed padding and the changed mds matrix.
250		let expected = from_bytes_to_packed_256(&hex!(
251			"69e1764144099730124ab8ef1414570895ae9de0b74dedf364c72d118851cf65"
252		));
253		assert_eq!(expected, out);
254	}
255
256	fn from_bytes_to_b32s(inp: &[u8]) -> Vec<BinaryField32b> {
257		inp.chunks_exact(4)
258			.map(|x| BinaryField32b::new(u32::from_le_bytes(x.try_into().unwrap())))
259			.collect::<Vec<_>>()
260	}
261
262	#[test]
263	fn test_multi_block_aligned() {
264		let mut hasher: Vision32b<BinaryField32b> = Vision32b::new(64);
265		let input = "One part of the mysterious existence of Captain Nemo had been unveiled and, if his identity had not been recognised, at least, the nations united against him were no longer hunting a chimerical creature, but a man who had vowed a deadly hatred against them";
266		hasher.update(from_bytes_to_b32s(input.as_bytes()));
267		let out = hasher.finalize().unwrap();
268
269		let expected = from_bytes_to_packed_256(&hex!(
270			"6ade8ba2a45a070a3abaff6f1bf9483686c78d4afca2d0d8d3c7897fdfe2df91"
271		));
272		assert_eq!(expected, out);
273
274		let mut hasher = Vision32b::new(64);
275		let input_as_b = from_bytes_to_b32s(input.as_bytes());
276		hasher.update(&input_as_b[0..29]);
277		hasher.update(&input_as_b[29..31]);
278		hasher.update(&input_as_b[31..57]);
279		hasher.update(&input_as_b[57..]);
280
281		assert_eq!(expected, hasher.finalize().unwrap());
282	}
283
284	#[test]
285	fn test_extensions_and_packings() {
286		let mut rng = thread_rng();
287		let data_to_hash: [BinaryField32b; 200] =
288			array::from_fn(|_| <BinaryField32b as Field>::random(&mut rng));
289		let expected = FixedLenHasherDigest::<_, Vision32b<_>>::hash(data_to_hash);
290
291		let data_as_u64 = data_to_hash
292			.chunks_exact(2)
293			.map(|x| BinaryField64b::from_bases(x).unwrap())
294			.collect::<Vec<_>>();
295		assert_eq!(FixedLenHasherDigest::<_, Vision32b<_>>::hash(&data_as_u64), expected);
296
297		let l = data_as_u64.len();
298		let data_as_packedu64 = (0..(l / 4))
299			.map(|j| PackedBinaryField4x64b::from_fn(|i| data_as_u64[j * 4 + i]))
300			.collect::<Vec<_>>();
301		assert_eq!(FixedLenHasherDigest::<_, Vision32b<_>>::hash(data_as_packedu64), expected);
302	}
303
304	#[test]
305	fn test_multi_block_unaligned() {
306		let mut hasher = Vision32b::new(23);
307		let input = "You can prove anything you want by coldly logical reason--if you pick the proper postulates.";
308		hasher.update(from_bytes_to_b32s(input.as_bytes()));
309
310		let expected = from_bytes_to_packed_256(&hex!(
311			"2819814fd9da83ab358533900adaf87f4c9e0f88657f572a9a6e83d95b88a9ea"
312		));
313		let out = hasher.finalize().unwrap();
314		assert_eq!(expected, out);
315	}
316
317	#[test]
318	fn test_aes_to_binary_hash() {
319		let mut rng = thread_rng();
320
321		let aes_transformer_1 = make_binary_to_aes_packed_transformer::<
322			PackedBinaryField4x64b,
323			PackedAESBinaryField4x64b,
324		>();
325		let aes_transformer_2 = make_aes_to_binary_packed_transformer::<
326			PackedAESBinaryField8x32b,
327			PackedBinaryField8x32b,
328		>();
329
330		let data_bin: [PackedBinaryField4x64b; 100] =
331			array::from_fn(|_| PackedBinaryField4x64b::random(&mut rng));
332		let data_aes: [PackedAESBinaryField4x64b; 100] =
333			array::from_fn(|i| aes_transformer_1.transform(&data_bin[i]));
334
335		let hasher_32b = Vision32b::new(100);
336		let hasher_aes32b = VisionHasher::<AESTowerField32b, _>::new(100);
337
338		let digest_as_bin = hasher_32b.chain_update(data_bin).finalize().unwrap();
339		let digest_as_aes = hasher_aes32b.chain_update(data_aes).finalize().unwrap();
340
341		assert_eq!(digest_as_bin, aes_transformer_2.transform(&digest_as_aes));
342	}
343}