Skip to main content

vyre_driver/
input_identity.rs

1//! Exact-input identity keys shared by replay and materialized-output caches.
2//!
3//! Backend replay caches need the same collision-resistant,
4//! tuple-boundary-preserving identity for borrowed input slice lists. The key is
5//! only a hot-path filter: cache users must still retain collision-safe exact
6//! byte checks before reusing bytes.
7
8use crate::BackendError;
9
10const DOMAIN_SEPARATED_INPUT_IDENTITY_PREFIX: &[u8] = b"vyre.input-identity.domain.v1";
11
12/// Fixed-width exact-input identity key.
13pub 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
43/// Hash a borrowed input tuple with explicit arity and length prefixes.
44///
45/// # Errors
46///
47/// Returns [`BackendError`] when the input arity or one input length cannot fit
48/// the stable `u64` hash envelope.
49pub 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
55/// Hash a borrowed input tuple under an explicit cache domain and device salt.
56///
57/// Use this for resident/static caches that need the same tuple-boundary
58/// protection as replay keys, but must not alias across different cache users,
59/// logical domains, or backend feature sets.
60///
61/// # Errors
62///
63/// Returns [`BackendError`] when the domain tag is empty, the domain tag cannot
64/// fit the stable `u64` envelope, or an input arity/length cannot fit.
65pub 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}