vyre_driver/
input_identity.rs1use crate::BackendError;
9
10const DOMAIN_SEPARATED_INPUT_IDENTITY_PREFIX: &[u8] = b"vyre.input-identity.domain.v1";
11
12pub type ExactInputKey = [u8; 32];
14
15fn input_identity_count(value: usize, field: &'static str) -> Result<u64, BackendError> {
16 u64::try_from(value).map_err(|source| BackendError::InvalidProgram {
17 fix: format!(
18 "Fix: exact-input key {field} cannot fit u64 while hashing replay inputs: {source}."
19 ),
20 })
21}
22
23fn update_len_prefixed_bytes(
24 hasher: &mut blake3::Hasher,
25 bytes: &[u8],
26 field: &'static str,
27) -> Result<(), BackendError> {
28 let byte_len = input_identity_count(bytes.len(), field)?;
29 hasher.update(&byte_len.to_le_bytes());
30 hasher.update(bytes);
31 Ok(())
32}
33
34fn update_input_tuple(hasher: &mut blake3::Hasher, inputs: &[&[u8]]) -> Result<(), BackendError> {
35 let input_count = input_identity_count(inputs.len(), "input count")?;
36 hasher.update(&input_count.to_le_bytes());
37 for input in inputs {
38 update_len_prefixed_bytes(hasher, input, "input length")?;
39 }
40 Ok(())
41}
42
43pub fn exact_input_key(inputs: &[&[u8]]) -> Result<ExactInputKey, BackendError> {
50 let mut hasher = blake3::Hasher::new();
51 update_input_tuple(&mut hasher, inputs)?;
52 Ok(*hasher.finalize().as_bytes())
53}
54
55pub fn domain_separated_exact_input_key(
66 domain_tag: &[u8],
67 domain_id: u64,
68 feature_key: u64,
69 inputs: &[&[u8]],
70) -> Result<ExactInputKey, BackendError> {
71 if domain_tag.is_empty() {
72 return Err(BackendError::InvalidProgram {
73 fix: "Fix: exact-input domain-separated key requires a non-empty domain tag."
74 .to_string(),
75 });
76 }
77 let mut hasher = blake3::Hasher::new();
78 hasher.update(DOMAIN_SEPARATED_INPUT_IDENTITY_PREFIX);
79 update_len_prefixed_bytes(&mut hasher, domain_tag, "domain tag length")?;
80 hasher.update(&domain_id.to_le_bytes());
81 hasher.update(&feature_key.to_le_bytes());
82 update_input_tuple(&mut hasher, inputs)?;
83 Ok(*hasher.finalize().as_bytes())
84}
85
86#[cfg(test)]
87mod tests {
88 use super::{domain_separated_exact_input_key, exact_input_key};
89
90 #[test]
91 fn exact_input_key_separates_tuple_boundaries_for_4096_generated_cases() {
92 for seed in 0_u32..4096 {
93 let left_len = ((seed.wrapping_mul(17) ^ seed.rotate_left(5)) % 31 + 1) as usize;
94 let right_len = ((seed.wrapping_mul(29) ^ seed.rotate_left(9)) % 31 + 1) as usize;
95 let mut state = seed ^ 0xC0DA_CAFE;
96 let mut left = Vec::with_capacity(left_len);
97 let mut right = Vec::with_capacity(right_len);
98 for index in 0..left_len {
99 state = state
100 .wrapping_mul(1_664_525)
101 .wrapping_add(1_013_904_223)
102 .rotate_left((index as u32) & 15);
103 left.push((state ^ seed.rotate_left(index as u32 & 31)) as u8);
104 }
105 for index in 0..right_len {
106 state = state
107 .wrapping_mul(22_695_477)
108 .wrapping_add(1)
109 .rotate_left((index as u32) & 7);
110 right.push((state ^ seed.rotate_right(index as u32 & 31)) as u8);
111 }
112 let mut concatenated = Vec::with_capacity(left_len + right_len);
113 concatenated.extend_from_slice(&left);
114 concatenated.extend_from_slice(&right);
115
116 let tuple_key = exact_input_key(&[left.as_slice(), right.as_slice()])
117 .expect("Fix: generated tuple exact-input key must fit");
118 let concatenated_key = exact_input_key(&[concatenated.as_slice()])
119 .expect("Fix: generated concatenated exact-input key must fit");
120 let empty_separated_key = exact_input_key(&[left.as_slice(), &[], right.as_slice()])
121 .expect("Fix: generated empty-separated exact-input key must fit");
122
123 assert_ne!(
124 tuple_key, concatenated_key,
125 "Fix: exact-input key must length-prefix slots so tuple boundaries cannot alias for generated case {seed}."
126 );
127 assert_ne!(
128 tuple_key, empty_separated_key,
129 "Fix: exact-input key must include empty input slots instead of collapsing them for generated case {seed}."
130 );
131 }
132 }
133
134 #[test]
135 fn exact_input_key_changes_on_4096_generated_single_byte_mutations() {
136 for seed in 0_u32..4096 {
137 let len = ((seed.wrapping_mul(37) ^ seed.rotate_left(11)) % 96 + 1) as usize;
138 let mut bytes = Vec::with_capacity(len);
139 let mut state = seed ^ 0xA5A5_5A5A;
140 for index in 0..len {
141 state = state
142 .wrapping_mul(1_103_515_245)
143 .wrapping_add(12_345)
144 .rotate_left((index as u32) & 15);
145 bytes.push((state >> ((index & 3) * 8)) as u8);
146 }
147 let mut mutated = bytes.clone();
148 let mutation_index = (seed as usize) % len;
149 mutated[mutation_index] ^= 0x80 | ((seed as u8) & 0x7f);
150
151 let base_key = exact_input_key(&[bytes.as_slice()])
152 .expect("Fix: base generated exact-input key must fit");
153 let mutated_key = exact_input_key(&[mutated.as_slice()])
154 .expect("Fix: mutated generated exact-input key must fit");
155
156 assert_ne!(
157 base_key, mutated_key,
158 "Fix: exact-input key must change when one byte changes for generated case {seed}."
159 );
160 }
161 }
162
163 #[test]
164 fn domain_separated_exact_input_key_preserves_domain_and_tuple_boundaries() {
165 for seed in 0_u32..2048 {
166 let left_len = ((seed.wrapping_mul(19) ^ seed.rotate_left(3)) % 48 + 1) as usize;
167 let right_len = ((seed.wrapping_mul(41) ^ seed.rotate_left(9)) % 48 + 1) as usize;
168 let mut state = seed ^ 0x1D_EA_7E5D;
169 let mut left = Vec::with_capacity(left_len);
170 let mut right = Vec::with_capacity(right_len);
171 for index in 0..left_len {
172 state = state
173 .wrapping_mul(747_796_405)
174 .wrapping_add(2_891_336_453)
175 .rotate_left((index as u32) & 15);
176 left.push((state >> ((index & 3) * 8)) as u8);
177 }
178 for index in 0..right_len {
179 state = state
180 .wrapping_mul(1_664_525)
181 .wrapping_add(1_013_904_223)
182 .rotate_left((index as u32) & 7);
183 right.push((state ^ seed.rotate_right(index as u32 & 31)) as u8);
184 }
185 let mut concatenated = Vec::with_capacity(left_len + right_len);
186 concatenated.extend_from_slice(&left);
187 concatenated.extend_from_slice(&right);
188 let domain_id = u64::from(seed) << 1;
189 let feature_key = u64::from(seed.rotate_left(11)) | 1;
190
191 let key = domain_separated_exact_input_key(
192 b"generated.cache.domain",
193 domain_id,
194 feature_key,
195 &[left.as_slice(), right.as_slice()],
196 )
197 .expect("Fix: generated domain-separated exact-input key must fit");
198 let different_tag = domain_separated_exact_input_key(
199 b"generated.cache.other",
200 domain_id,
201 feature_key,
202 &[left.as_slice(), right.as_slice()],
203 )
204 .expect("Fix: generated domain tag variation must fit");
205 let different_domain = domain_separated_exact_input_key(
206 b"generated.cache.domain",
207 domain_id ^ 0x55AA,
208 feature_key,
209 &[left.as_slice(), right.as_slice()],
210 )
211 .expect("Fix: generated domain id variation must fit");
212 let different_feature = domain_separated_exact_input_key(
213 b"generated.cache.domain",
214 domain_id,
215 feature_key.rotate_left(17),
216 &[left.as_slice(), right.as_slice()],
217 )
218 .expect("Fix: generated feature key variation must fit");
219 let concatenated_key = domain_separated_exact_input_key(
220 b"generated.cache.domain",
221 domain_id,
222 feature_key,
223 &[concatenated.as_slice()],
224 )
225 .expect("Fix: generated concatenated domain key must fit");
226
227 assert_ne!(key, different_tag);
228 assert_ne!(key, different_domain);
229 assert_ne!(key, different_feature);
230 assert_ne!(key, concatenated_key);
231 assert_ne!(
232 key,
233 exact_input_key(&[left.as_slice(), right.as_slice()])
234 .expect("Fix: generated plain exact-input key must fit"),
235 "Fix: domain-separated exact-input keys must not alias plain replay keys."
236 );
237 }
238 }
239
240 #[test]
241 fn domain_separated_exact_input_key_rejects_empty_domain_tag() {
242 let error = domain_separated_exact_input_key(&[], 0, 0, &[b"payload".as_slice()])
243 .expect_err("Fix: empty cache domains must be rejected");
244 assert!(
245 error.to_string().contains("non-empty domain tag"),
246 "Fix: empty domain tag diagnostics must explain the rejected cache-domain contract."
247 );
248 }
249}