binius_hash/vision_4/
digest.rs

1// Copyright 2025 Irreducible Inc.
2
3use binius_field::{BinaryField128bGhash as Ghash, Field};
4use binius_utils::{DeserializeBytes, SerializeBytes};
5use digest::{
6	FixedOutput, FixedOutputReset, HashMarker, OutputSizeUser, Reset, Update, consts::U32,
7	core_api::BlockSizeUser,
8};
9
10use super::{constants::M, permutation::permutation};
11
12pub const RATE_AS_U128: usize = 2;
13pub const RATE_AS_U8: usize = RATE_AS_U128 * std::mem::size_of::<u128>();
14
15const PADDING_START: u8 = 0x80;
16const PADDING_END: u8 = 0x01;
17
18pub const PADDING_BLOCK: [u8; RATE_AS_U8] = {
19	let mut block = [0; RATE_AS_U8];
20	block[0] = PADDING_START;
21	block[RATE_AS_U8 - 1] |= PADDING_END;
22	block
23};
24
25/// Fill the data using Keccak padding scheme.
26#[inline(always)]
27pub fn fill_padding(data: &mut [u8]) {
28	debug_assert!(!data.is_empty() && data.len() <= RATE_AS_U8);
29
30	data.fill(0);
31	data[0] |= PADDING_START;
32	data[data.len() - 1] |= PADDING_END;
33}
34
35/// An implementation of the Vision permutation with 4 Ghash elements for state.
36#[derive(Clone)]
37pub struct VisionHasherDigest {
38	state: [Ghash; M],
39	buffer: [u8; RATE_AS_U8],
40	filled_bytes: usize,
41}
42
43impl Default for VisionHasherDigest {
44	fn default() -> Self {
45		Self {
46			state: [Ghash::ZERO; M],
47			buffer: [0; RATE_AS_U8],
48			filled_bytes: 0,
49		}
50	}
51}
52
53impl VisionHasherDigest {
54	pub fn permute(state: &mut [Ghash; M], data: &[u8]) {
55		debug_assert_eq!(data.len(), RATE_AS_U8);
56
57		// Overwrite first RATE_AS_U128 elements of state with data
58		for i in 0..RATE_AS_U128 {
59			state[i] = Ghash::deserialize(&data[i * 16..]).expect("data len checked");
60		}
61
62		permutation(state);
63	}
64
65	fn finalize(&mut self, out: &mut digest::Output<Self>) {
66		if self.filled_bytes != 0 {
67			fill_padding(&mut self.buffer[self.filled_bytes..]);
68			Self::permute(&mut self.state, &self.buffer);
69		} else {
70			Self::permute(&mut self.state, &PADDING_BLOCK);
71		}
72
73		// Serialize first two state elements to output (32 bytes total)
74		let (state0, state1) = out.as_mut_slice().split_at_mut(16);
75		self.state[0].serialize(state0).expect("fits in 16 bytes");
76		self.state[1].serialize(state1).expect("fits in 16 bytes");
77	}
78}
79
80impl HashMarker for VisionHasherDigest {}
81
82impl Update for VisionHasherDigest {
83	fn update(&mut self, mut data: &[u8]) {
84		if self.filled_bytes != 0 {
85			let to_copy = std::cmp::min(data.len(), RATE_AS_U8 - self.filled_bytes);
86			self.buffer[self.filled_bytes..self.filled_bytes + to_copy]
87				.copy_from_slice(&data[..to_copy]);
88			data = &data[to_copy..];
89			self.filled_bytes += to_copy;
90
91			if self.filled_bytes == RATE_AS_U8 {
92				Self::permute(&mut self.state, &self.buffer);
93				self.filled_bytes = 0;
94			}
95		}
96
97		let mut chunks = data.chunks_exact(RATE_AS_U8);
98		for chunk in &mut chunks {
99			Self::permute(&mut self.state, chunk);
100		}
101
102		let remaining = chunks.remainder();
103		if !remaining.is_empty() {
104			self.buffer[..remaining.len()].copy_from_slice(remaining);
105			self.filled_bytes = remaining.len();
106		}
107	}
108}
109
110impl OutputSizeUser for VisionHasherDigest {
111	type OutputSize = U32;
112}
113
114impl BlockSizeUser for VisionHasherDigest {
115	type BlockSize = U32;
116}
117
118impl FixedOutput for VisionHasherDigest {
119	fn finalize_into(mut self, out: &mut digest::Output<Self>) {
120		Self::finalize(&mut self, out);
121	}
122}
123
124impl FixedOutputReset for VisionHasherDigest {
125	fn finalize_into_reset(&mut self, out: &mut digest::Output<Self>) {
126		Self::finalize(self, out);
127		Reset::reset(self);
128	}
129}
130
131impl Reset for VisionHasherDigest {
132	fn reset(&mut self) {
133		self.state = [Ghash::ZERO; M];
134		self.buffer = [0; RATE_AS_U8];
135		self.filled_bytes = 0;
136	}
137}
138
139#[cfg(test)]
140mod tests {
141	use digest::Digest;
142
143	use super::VisionHasherDigest;
144
145	const INPUT: &[u8] = "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".as_bytes();
146
147	#[test]
148	fn test_multi_block_aligned() {
149		let mut hasher = VisionHasherDigest::default();
150
151		hasher.update(INPUT);
152		let out = hasher.finalize();
153
154		let mut hasher = VisionHasherDigest::default();
155		let input_as_b = INPUT;
156		hasher.update(&input_as_b[0..63]);
157		hasher.update(&input_as_b[63..128]);
158		hasher.update(&input_as_b[128..163]);
159		hasher.update(&input_as_b[163..]);
160
161		assert_eq!(out, hasher.finalize());
162	}
163
164	#[test]
165	fn test_multi_block_unaligned() {
166		let mut hasher = VisionHasherDigest::default();
167		hasher.update(INPUT);
168		let out = hasher.finalize();
169
170		let mut hasher = VisionHasherDigest::default();
171		let input_as_b = INPUT;
172		hasher.update(&input_as_b[0..1]);
173		hasher.update(&input_as_b[1..120]);
174		hasher.update(&input_as_b[120..120]);
175		hasher.update(&input_as_b[120..]);
176
177		assert_eq!(out, hasher.finalize());
178	}
179}