Skip to main content

krabka_verified/
remote_metadata.rs

1//! KIP-405 remote-log metadata topic partitioning.
2
3use creusot_std::prelude::*;
4
5const MURMUR2_SEED: u32 = 0x9747_b28c;
6const MURMUR2_M: u32 = 0x5bd1_e995;
7
8// Java `int` arithmetic is arithmetic modulo 2^32, so every Java `int` below
9// is carried as its two's-complement bit image in a `u32`, with wrapping
10// `u32` operations standing in for Java's `int` ones.
11
12/// Java `Long.hashCode`, `(int) (value ^ (value >>> 32))`, as its `u32` image.
13// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
14#[cfg(creusot)]
15#[cfg_attr(test, mutants::skip)]
16#[logic]
17fn java_long_hash_model(bits: u64) -> u32 {
18    pearlite! { ((bits ^ (bits >> 32u32)) & 0xffff_ffffu64) as u32 }
19}
20
21#[cfg_attr(creusot, ensures(result == java_long_hash_model(bits)))]
22fn java_long_hash(bits: u64) -> u32 { ((bits ^ (bits >> 32)) & 0xffff_ffff) as u32 }
23
24/// The `u32` image of a Java `int`: the value itself, or the value plus 2^32
25/// (`u32::MAX - (-1 - value)`) when it is negative. Both casts stay in range.
26// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
27#[cfg(creusot)]
28#[cfg_attr(test, mutants::skip)]
29#[logic]
30fn java_int_image_model(value: i32) -> u32 {
31    pearlite! {
32        if value@ >= 0 { value as u32 } else { u32::MAX - (-1i32 - value) as u32 }
33    }
34}
35
36#[cfg_attr(creusot, ensures(result == java_int_image_model(value)))]
37fn java_int_image(value: i32) -> u32 {
38    if value < 0 {
39        // `2^32 + value`, as `u32::MAX - (-1 - value)`.
40        u32::MAX - value.abs_diff(-1)
41    } else {
42        value.abs_diff(0)
43    }
44}
45
46/// Java `Objects.hash(uuid.getLeastSignificantBits(),
47/// uuid.getMostSignificantBits(), partition)`, as its `u32` image.
48// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
49#[cfg(creusot)]
50#[cfg_attr(test, mutants::skip)]
51#[logic]
52fn java_objects_hash_model(topic_id: u128, user_partition: i32) -> u32 {
53    pearlite! {
54        let most_significant_bits = (topic_id >> 64u32) as u64;
55        let least_significant_bits = (topic_id & 0xffff_ffff_ffff_ffffu128) as u64;
56        let first = 1u32 * 31u32 + java_long_hash_model(least_significant_bits);
57        let second = first * 31u32 + java_long_hash_model(most_significant_bits);
58        second * 31u32 + java_int_image_model(user_partition)
59    }
60}
61
62#[cfg_attr(
63    creusot,
64    ensures(result == java_objects_hash_model(topic_id, user_partition))
65)]
66fn java_objects_hash(topic_id: u128, user_partition: i32) -> u32 {
67    let most_significant_bits = (topic_id >> 64) as u64;
68    let least_significant_bits = (topic_id & 0xffff_ffff_ffff_ffff) as u64;
69
70    let mut hash = 1_u32;
71    hash = hash
72        .wrapping_mul(31)
73        .wrapping_add(java_long_hash(least_significant_bits));
74    hash = hash
75        .wrapping_mul(31)
76        .wrapping_add(java_long_hash(most_significant_bits));
77    hash.wrapping_mul(31)
78        .wrapping_add(java_int_image(user_partition))
79}
80
81// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
82#[cfg(creusot)]
83#[cfg_attr(test, mutants::skip)]
84#[logic]
85fn reverse_bytes_model(value: u32) -> u32 {
86    pearlite! {
87        (value >> 24u32)
88            | ((value >> 8u32) & 0x0000_ff00u32)
89            | ((value << 8u32) & 0x00ff_0000u32)
90            | (value << 24u32)
91    }
92}
93
94#[cfg_attr(creusot, ensures(result == reverse_bytes_model(value)))]
95fn reverse_bytes(value: u32) -> u32 {
96    (value >> 24) | ((value >> 8) & 0x0000_ff00) | ((value << 8) & 0x00ff_0000) | (value << 24)
97}
98
99// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
100#[cfg(creusot)]
101#[cfg_attr(test, mutants::skip)]
102#[logic]
103fn kafka_murmur2_int_model(value: u32) -> u32 {
104    pearlite! {
105        let chunk0 = reverse_bytes_model(value);
106        let chunk1 = chunk0 * 0x5bd1_e995u32;
107        let chunk2 = (chunk1 ^ (chunk1 >> 24u32)) * 0x5bd1_e995u32;
108        let hash0 = (0x9747_b28cu32 ^ 4u32) * 0x5bd1_e995u32;
109        let hash1 = hash0 ^ chunk2;
110        let hash2 = (hash1 ^ (hash1 >> 13u32)) * 0x5bd1_e995u32;
111        hash2 ^ (hash2 >> 15u32)
112    }
113}
114
115/// Kafka Murmur2 over the four big-endian bytes of a Java `int`, given as its
116/// `u32` image.
117#[cfg_attr(creusot, ensures(result == kafka_murmur2_int_model(value)))]
118fn kafka_murmur2_int(value: u32) -> u32 {
119    let mut hash = MURMUR2_SEED ^ 4;
120    let mut chunk = reverse_bytes(value);
121    chunk = chunk.wrapping_mul(MURMUR2_M);
122    chunk ^= chunk >> 24;
123    chunk = chunk.wrapping_mul(MURMUR2_M);
124    hash = hash.wrapping_mul(MURMUR2_M);
125    hash ^= chunk;
126    hash ^= hash >> 13;
127    hash = hash.wrapping_mul(MURMUR2_M);
128    hash ^ (hash >> 15)
129}
130
131// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
132#[cfg(creusot)]
133#[cfg_attr(test, mutants::skip)]
134#[logic]
135fn kafka_to_positive_model(hash: u32) -> Int {
136    pearlite! { hash@ % 2_147_483_648 }
137}
138
139/// Kafka `Utils.toPositive`, `number & 0x7fffffff`, which on the `u32` image
140/// is the remainder modulo 2^31.
141#[cfg_attr(creusot, ensures(result@ == kafka_to_positive_model(hash)))]
142#[ensures(0 <= result@)]
143#[ensures(result@ <= i32::MAX@)]
144fn kafka_to_positive(hash: u32) -> i32 { (u64::from(hash) % 0x8000_0000) as i32 }
145
146// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
147#[cfg(creusot)]
148#[cfg_attr(test, mutants::skip)]
149#[logic]
150fn remote_metadata_partition_model(
151    topic_id: u128,
152    user_partition: i32,
153    metadata_partition_count: i32,
154) -> Int {
155    pearlite! {
156        if metadata_partition_count <= 0i32 {
157            0
158        } else {
159            kafka_to_positive_model(kafka_murmur2_int_model(
160                java_objects_hash_model(topic_id, user_partition)
161            )) % metadata_partition_count@
162        }
163    }
164}
165
166/// Map a user topic-id partition to its KIP-405 metadata topic partition.
167///
168/// This is byte-for-byte compatible with Kafka's
169/// `RemoteLogMetadataTopicPartitioner`: Java `Objects.hash` receives the
170/// UUID's least-significant bits, most-significant bits, and user partition;
171/// its big-endian `int` bytes are passed through Kafka Murmur2 and
172/// `Utils.toPositive` before the metadata partition modulo.
173#[cfg_attr(
174    creusot,
175    ensures(metadata_partition_count@ > 0 ==>
176        exists<partition: i32> result == Some(partition)
177            && partition@ == remote_metadata_partition_model(
178                topic_id,
179                user_partition,
180                metadata_partition_count
181            ))
182)]
183#[ensures((result == None) == (metadata_partition_count@ <= 0))]
184#[ensures(forall<partition: i32> result == Some(partition) ==>
185    0 <= partition@ && partition@ < metadata_partition_count@)]
186#[must_use]
187pub fn remote_metadata_partition(
188    topic_id: u128,
189    user_partition: i32,
190    metadata_partition_count: i32,
191) -> Option<i32> {
192    if metadata_partition_count <= 0 {
193        return None;
194    }
195
196    let objects_hash = java_objects_hash(topic_id, user_partition);
197    let murmur2 = kafka_murmur2_int(objects_hash);
198    Some(kafka_to_positive(murmur2) % metadata_partition_count)
199}
200
201/// Select the inclusive metadata-log cursor immediately after a snapshot's
202/// committed offset.
203///
204/// `-1` is the only no-event sentinel and maps to a full replay from zero.
205/// Every real committed offset advances by exactly one. Values below the
206/// sentinel and a committed `i64::MAX` fail closed instead of producing a
207/// negative or wrapped cursor.
208#[cfg_attr(
209    creusot,
210    ensures(match result {
211        Some(cursor) => -1 <= committed@ && committed@ < i64::MAX@ && cursor@ == committed@ + 1,
212        None => committed@ < -1 || committed@ == i64::MAX@,
213    })
214)]
215#[ensures(forall<cursor: i64> result == Some(cursor) ==> 0 <= cursor@)]
216#[must_use]
217pub const fn remote_metadata_resume_cursor(committed: i64) -> Option<i64> {
218    if committed < -1 {
219        return None;
220    }
221    committed.checked_add(1)
222}
223
224#[cfg(test)]
225mod tests {
226    use {super::*, assert2::check};
227
228    #[yare::parameterized(
229        partition_0 = { 0, Some(6) },
230        partition_1 = { 1, Some(36) },
231        partition_3 = { 3, Some(2) },
232        partition_7 = { 7, Some(26) },
233    )]
234    fn matches_jvm_remote_metadata_partitioner_goldens(partition: i32, expected: Option<i32>) {
235        check!(
236            remote_metadata_partition(0x1234_5678_9abc_def0_0011_2233_4455_6677, partition, 50,)
237                == expected
238        );
239    }
240
241    #[yare::parameterized(
242        zero_uuid = { 0_u128, i32::MIN, Some(96) },
243        max_uuid = { u128::MAX, i32::MAX, Some(5) },
244        signed_boundary = { 0xffff_ffff_ffff_ffff_8000_0000_0000_0000, i32::MIN, Some(45) },
245    )]
246    fn matches_java_signed_uuid_and_partition_extremes(
247        topic_id: u128,
248        partition: i32,
249        expected: Option<i32>,
250    ) {
251        check!(remote_metadata_partition(topic_id, partition, 97) == expected);
252    }
253
254    #[test]
255    fn rejects_nonpositive_metadata_partition_counts() {
256        check!(remote_metadata_partition(0, 0, 0) == None);
257        check!(remote_metadata_partition(0, 0, -1) == None);
258    }
259
260    #[test]
261    fn resume_cursor_maps_none_to_zero_and_advances_exactly() {
262        check!(remote_metadata_resume_cursor(-1) == Some(0));
263        check!(remote_metadata_resume_cursor(0) == Some(1));
264        check!(remote_metadata_resume_cursor(41) == Some(42));
265        check!(remote_metadata_resume_cursor(i64::MAX - 1) == Some(i64::MAX));
266    }
267
268    #[test]
269    fn resume_cursor_rejects_malformed_and_overflowing_offsets() {
270        check!(remote_metadata_resume_cursor(-2) == None);
271        check!(remote_metadata_resume_cursor(i64::MIN) == None);
272        check!(remote_metadata_resume_cursor(i64::MAX) == None);
273    }
274}