cuprate_cryptonight/
hash_v4.rs

1use std::cmp::max;
2
3use seq_macro::seq;
4use InstructionList::{Add, Mul, Ret, Rol, Ror, Sub, Xor};
5
6use crate::{
7    blake256::{Blake256, Digest},
8    util::subarray_copy,
9};
10
11const TOTAL_LATENCY: usize = 15 * 3;
12const NUM_INSTRUCTIONS_MIN: usize = 60;
13pub(crate) const NUM_INSTRUCTIONS_MAX: usize = 70;
14const ALU_COUNT_MUL: usize = 1;
15const ALU_COUNT: usize = 3;
16
17#[repr(u8)]
18#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub(crate) enum InstructionList {
20    Mul, // a*b
21    Add, // a+b + C, C is an unsigned 32-bit constant
22    Sub, // a-b
23    Ror, // rotate right "a" by "b & 31" bits
24    Rol, // rotate left "a" by "b & 31" bits
25    Xor, // a^b
26    #[default]
27    Ret, // finish execution
28}
29
30const INSTRUCTION_COUNT: usize = Ret as usize;
31
32/// INSTRUCTION_* constants are used to generate code from random data.
33/// Every random sequence of bytes is a valid code.
34///
35/// There are 9 registers in total:
36/// - 4 variable registers
37/// - 5 constant registers initialized from loop variables
38const INSTRUCTION_OPCODE_BITS: usize = 3;
39const INSTRUCTION_DST_INDEX_BITS: usize = 2;
40const INSTRUCTION_SRC_INDEX_BITS: usize = 3;
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub(crate) struct Instruction {
44    pub(crate) opcode: InstructionList,
45    pub(crate) dst_index: u8,
46    pub(crate) src_index: u8,
47    pub(crate) c: u32,
48}
49
50/// If we don't have enough data available, generate more.
51/// Original C code:
52/// <https://github.com/monero-project/monero/blob/v0.18.3.4/src/crypto/variant4_random_math.h#L171-L178>
53fn check_data(data_index: &mut usize, bytes_needed: usize, data: &mut [u8]) {
54    if *data_index + bytes_needed > data.len() {
55        let output = Blake256::digest(&data);
56        data.copy_from_slice(&output);
57        *data_index = 0;
58    }
59}
60
61/// Generates as many random math operations as possible with given latency and
62/// ALU restrictions.
63///
64/// Original C code:
65/// <https://github.com/monero-project/monero/blob/v0.18.3.4/src/crypto/variant4_random_math.h#L180-L439>
66///
67#[expect(clippy::cast_sign_loss)]
68#[expect(clippy::cast_possible_wrap)]
69#[expect(clippy::cast_possible_truncation)]
70pub(crate) fn random_math_init(
71    code: &mut [Instruction; NUM_INSTRUCTIONS_MAX + 1],
72    height: u64,
73) -> usize {
74    // MUL is 3 cycles, 3-way addition and rotations are 2 cycles, SUB/XOR are 1
75    // cycle These latencies match real-life instruction latencies for Intel
76    // CPUs starting from Sandy Bridge and up to Skylake/Coffee lake
77    //
78    // AMD Ryzen has the same latencies except 1-cycle ROR/ROL, so it'll be a bit
79    // faster than Intel Sandy Bridge and newer processors Surprisingly, Intel
80    // Nehalem also has 1-cycle ROR/ROL, so it'll also be faster than Intel Sandy
81    // Bridge and newer processors AMD Bulldozer has 4 cycles latency for MUL
82    // (slower than Intel) and 1 cycle for ROR/ROL (faster than Intel), so average
83    // performance will be the same Source: https://www.agner.org/optimize/instruction_tables.pdf
84    const OP_LATENCY: [usize; INSTRUCTION_COUNT] = [3, 2, 1, 2, 2, 1];
85
86    // Instruction latencies for theoretical ASIC implementation
87    const ASIC_OP_LATENCY: [usize; INSTRUCTION_COUNT] = [3, 1, 1, 1, 1, 1];
88
89    // Available ALUs for each instruction
90    const OP_ALUS: [usize; INSTRUCTION_COUNT] = [
91        ALU_COUNT_MUL,
92        ALU_COUNT,
93        ALU_COUNT,
94        ALU_COUNT,
95        ALU_COUNT,
96        ALU_COUNT,
97    ];
98
99    let mut data = [0_u8; 32];
100    data[0..8].copy_from_slice(&height.to_le_bytes());
101
102    data[20] = -38_i8 as u8; // change seed
103
104    // Set data_index past the last byte in data
105    // to trigger full data update with blake hash
106    // before we start using it
107    let mut data_index: usize = data.len();
108
109    let mut code_size: usize;
110
111    // There is a small chance (1.8%) that register R8 won't be used in the
112    // generated program, so we keep track of it and try again if it's not used
113    loop {
114        let mut latency = [0_usize; 9];
115        let mut asic_latency = [0_usize; 9];
116
117        // Tracks previous instruction and value of the source operand for
118        // registers R0-R3 throughout code execution:
119        // byte 0: current value of the destination register
120        // byte 1: instruction opcode
121        // byte 2: current value of the source register
122        //
123        // Registers R4-R8 are constant and are treated as having the same
124        // value, because when we do the same operation twice with two constant
125        // source registers, it can be optimized into a single operation.
126        let mut inst_data: [usize; 9] =
127            [0, 1, 2, 3, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF];
128
129        let mut alu_busy = [[false; ALU_COUNT]; TOTAL_LATENCY + 1];
130        let mut is_rotation = [false; INSTRUCTION_COUNT];
131        is_rotation[Ror as usize] = true;
132        is_rotation[Rol as usize] = true;
133        let mut rotated = [false; 4];
134        let mut rotate_count = 0_usize;
135
136        let mut num_retries = 0_usize;
137        code_size = 0;
138
139        let mut total_iterations = 0_usize;
140        let mut r8_used = false;
141
142        // Generate random code to achieve minimal required latency for our abstract CPU
143        // Try to get this latency for all 4 registers
144        while (latency[0] < TOTAL_LATENCY
145            || latency[1] < TOTAL_LATENCY
146            || latency[2] < TOTAL_LATENCY
147            || latency[3] < TOTAL_LATENCY)
148            && num_retries < 64
149        {
150            // Fail-safe to guarantee loop termination
151            total_iterations += 1;
152            if total_iterations > 256 {
153                break;
154            }
155
156            check_data(&mut data_index, 1, &mut data);
157
158            let c = data[data_index];
159            data_index += 1;
160
161            // MUL = opcodes 0-2
162            // ADD = opcode 3
163            // SUB = opcode 4
164            // ROR/ROL = opcode 5, shift direction is selected randomly
165            // XOR = opcodes 6-7
166            let opcode_bits = c & ((1 << INSTRUCTION_OPCODE_BITS) - 1);
167            let opcode: InstructionList;
168            if opcode_bits == 5 {
169                check_data(&mut data_index, 1, &mut data);
170                opcode = if data[data_index] as i8 >= 0 {
171                    Ror
172                } else {
173                    Rol
174                };
175                data_index += 1;
176            } else if opcode_bits >= 6 {
177                opcode = Xor;
178            } else if opcode_bits <= 2 {
179                opcode = Mul;
180            } else {
181                // remaining values are 3-4
182                opcode = match opcode_bits {
183                    3 => Add,
184                    4 => Sub,
185                    _ => unreachable!(),
186                };
187            }
188
189            let dst_index =
190                (c >> INSTRUCTION_OPCODE_BITS) & ((1 << INSTRUCTION_DST_INDEX_BITS) - 1);
191            let mut src_index = (c >> (INSTRUCTION_OPCODE_BITS + INSTRUCTION_DST_INDEX_BITS))
192                & ((1 << INSTRUCTION_SRC_INDEX_BITS) - 1);
193
194            let a = dst_index as usize;
195            let mut b = src_index as usize;
196
197            // Don't do ADD/SUB/XOR with the same register
198            if matches!(opcode, Add | Sub | Xor) && a == b {
199                b = 8;
200                src_index = 8;
201            }
202
203            // Don't do rotation with the same destination twice because it's equal to a
204            // single rotation
205            if is_rotation[opcode as usize] && rotated[a] {
206                continue;
207            }
208
209            // Don't do the same instruction (except MUL) with the same source value twice,
210            // because all other cases can be optimized:
211            //      2xADD(a, b, C) = ADD(a,b*2, C1+C2),
212            // Same for SUB and rotations:
213            //      2xXOR(a, b) = NOP
214            if opcode != Mul
215                && inst_data[a] & 0xFFFF00
216                    == ((opcode as usize) << 8) + ((inst_data[b] & 255) << 16)
217            {
218                continue;
219            }
220
221            // Find which ALU is available (and when) for this instruction
222            let mut next_latency = if latency[a] > latency[b] {
223                latency[a]
224            } else {
225                latency[b]
226            };
227            let mut alu_index = -1;
228            while next_latency < TOTAL_LATENCY {
229                for i in (0..OP_ALUS[opcode as usize]).rev() {
230                    if alu_busy[next_latency][i] {
231                        continue;
232                    }
233
234                    if opcode == Add && alu_busy[next_latency + 1][i] {
235                        continue;
236                    }
237
238                    if is_rotation[opcode as usize]
239                        && next_latency < (rotate_count * OP_LATENCY[opcode as usize])
240                    {
241                        continue;
242                    }
243
244                    alu_index = i as isize;
245                    break;
246                }
247                if alu_index >= 0 {
248                    break;
249                }
250                next_latency += 1;
251            }
252
253            // Don't generate instructions that leave some register unchanged for more than
254            // 7 cycles
255            if next_latency > latency[a] + 7 {
256                continue;
257            }
258
259            next_latency += OP_LATENCY[opcode as usize];
260
261            if next_latency <= TOTAL_LATENCY {
262                if is_rotation[opcode as usize] {
263                    rotate_count += 1;
264                }
265
266                // Mark ALU as busy only for the first cycle when it starts executing the
267                // instruction because ALUs are fully pipelined.
268                alu_busy[next_latency - OP_LATENCY[opcode as usize]][alu_index as usize] = true;
269                latency[a] = next_latency;
270
271                // ASIC is supposed to have enough ALUs to run as many independent instructions
272                // per cycle as possible, so latency calculation for ASIC is straightforward.
273                asic_latency[a] =
274                    max(asic_latency[a], asic_latency[b]) + ASIC_OP_LATENCY[opcode as usize];
275
276                rotated[a] = is_rotation[opcode as usize];
277
278                inst_data[a] = code_size + ((opcode as usize) << 8) + ((inst_data[b] & 255) << 16);
279
280                code[code_size].opcode = opcode;
281                code[code_size].dst_index = dst_index;
282                code[code_size].src_index = src_index;
283                code[code_size].c = 0;
284
285                if src_index == 8 {
286                    r8_used = true;
287                }
288
289                if opcode == Add {
290                    alu_busy[next_latency - OP_LATENCY[opcode as usize] + 1][alu_index as usize] =
291                        true;
292
293                    check_data(&mut data_index, size_of::<u32>(), &mut data);
294                    code[code_size].c = u32::from_le_bytes(subarray_copy(&data, data_index));
295                    data_index += 4;
296                }
297                code_size += 1;
298                if code_size >= NUM_INSTRUCTIONS_MIN {
299                    break;
300                }
301            } else {
302                num_retries += 1;
303            }
304        }
305
306        // ASIC has more execution resources and can extract as much parallelism
307        // from the code as possible. We need to add a few more MUL and ROR
308        // instructions to achieve minimal required latency for ASIC. Get this
309        // latency for at least 1 of the 4 registers.
310        let prev_code_size = code_size;
311        while code_size < NUM_INSTRUCTIONS_MAX
312            && asic_latency.iter().take(4).all(|&lat| lat < TOTAL_LATENCY)
313        {
314            let mut min_idx: usize = 0;
315            let mut max_idx: usize = 0;
316            for i in 1..4 {
317                if asic_latency[i] < asic_latency[min_idx] {
318                    min_idx = i;
319                }
320                if asic_latency[i] > asic_latency[max_idx] {
321                    max_idx = i;
322                }
323            }
324
325            let pattern = [Ror, Mul, Mul];
326            let opcode = pattern[(code_size - prev_code_size) % 3];
327            latency[min_idx] = latency[max_idx] + OP_LATENCY[opcode as usize];
328            asic_latency[min_idx] = asic_latency[max_idx] + ASIC_OP_LATENCY[opcode as usize];
329
330            code[code_size] = Instruction {
331                opcode,
332                dst_index: min_idx as u8,
333                src_index: max_idx as u8,
334                c: 0,
335            };
336            code_size += 1;
337        }
338
339        // There is ~98.15% chance that loop condition is false, so this loop will
340        // execute only 1 iteration most of the time. It never does more than 4
341        // iterations for all block heights < 10,000,000.
342
343        if r8_used && (NUM_INSTRUCTIONS_MIN..=NUM_INSTRUCTIONS_MAX).contains(&code_size) {
344            break;
345        }
346    }
347
348    // It's guaranteed that NUM_INSTRUCTIONS_MIN <= code_size <=
349    // NUM_INSTRUCTIONS_MAX here. Add final instruction to stop the interpreter.
350    code[code_size].opcode = Ret;
351    code[code_size].dst_index = 0;
352    code[code_size].src_index = 0;
353    code[code_size].c = 0;
354
355    code_size
356}
357
358/// Original C code:
359/// <https://github.com/monero-project/monero/blob/v0.18.3.4/src/crypto/variant4_random_math.h#L81-L168>
360#[expect(clippy::needless_return, reason = "last iteration of unrolled loop")]
361#[expect(clippy::unnecessary_semicolon, reason = "macro")]
362pub(crate) fn v4_random_math(code: &[Instruction; NUM_INSTRUCTIONS_MAX + 1], r: &mut [u32; 9]) {
363    const REG_BITS: u32 = 32;
364
365    debug_assert_eq!(NUM_INSTRUCTIONS_MAX, 70);
366    seq!(i in 0..70 {
367        let op = &code[i];
368        let src = r[op.src_index as usize];
369        let dst = &mut r[op.dst_index as usize];
370        match op.opcode {
371            Mul => *dst = dst.wrapping_mul(src),
372            Add => *dst = dst.wrapping_add(src).wrapping_add(op.c),
373            Sub => *dst = dst.wrapping_sub(src),
374            Ror => *dst = dst.rotate_right(src % REG_BITS),
375            Rol => *dst = dst.rotate_left(src % REG_BITS),
376            Xor => *dst ^= src,
377            Ret => return,
378        }
379    });
380}
381
382/// Original C code:
383/// <https://github.com/monero-project/monero/blob/v0.18.3.4/src/crypto/slow-hash.c#L336-L370>
384/// To match the C code organization, this function would be in `slow_hash.rs`, but
385/// the test code for it is so large, that it was moved here.
386#[expect(clippy::cast_possible_truncation)]
387pub(crate) fn variant4_random_math(
388    a1: &mut u128,
389    c2: &mut u128,
390    r: &mut [u32; 9],
391    b: &[u128; 2],
392    code: &[Instruction; 71],
393) {
394    let t64 = u64::from(r[0].wrapping_add(r[1])) | (u64::from(r[2].wrapping_add(r[3])) << 32);
395    *c2 ^= u128::from(t64);
396
397    r[4] = *a1 as u32;
398    r[5] = (*a1 >> 64) as u32;
399    r[6] = b[0] as u32;
400    r[7] = b[1] as u32;
401    r[8] = (b[1] >> 64) as u32;
402
403    v4_random_math(code, r);
404
405    *a1 ^= u128::from(r[2])
406        | (u128::from(r[3]) << 32)
407        | (u128::from(r[0]) << 64)
408        | (u128::from(r[1]) << 96);
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::util::hex_to_array;
415
416    #[rustfmt::skip]
417    const CODE: [Instruction; 71] = [
418        Instruction { opcode: Rol, dst_index: 0, src_index: 7, c: 0 },
419        Instruction { opcode: Mul, dst_index: 3, src_index: 1, c: 0 },
420        Instruction { opcode: Add, dst_index: 2, src_index: 7, c: 3553557725 },
421        Instruction { opcode: Sub, dst_index: 0, src_index: 8, c: 0 },
422        Instruction { opcode: Add, dst_index: 3, src_index: 4, c: 3590470404 },
423        Instruction { opcode: Xor, dst_index: 1, src_index: 0, c: 0 },
424        Instruction { opcode: Xor, dst_index: 1, src_index: 5, c: 0 },
425        Instruction { opcode: Xor, dst_index: 1, src_index: 0, c: 0 },
426        Instruction { opcode: Mul, dst_index: 0, src_index: 7, c: 0 },
427        Instruction { opcode: Mul, dst_index: 2, src_index: 1, c: 0 },
428        Instruction { opcode: Mul, dst_index: 2, src_index: 4, c: 0 },
429        Instruction { opcode: Mul, dst_index: 2, src_index: 7, c: 0 },
430        Instruction { opcode: Sub, dst_index: 1, src_index: 8, c: 0 },
431        Instruction { opcode: Add, dst_index: 0, src_index: 6, c: 1516169632 },
432        Instruction { opcode: Add, dst_index: 2, src_index: 0, c: 1587456779 },
433        Instruction { opcode: Mul, dst_index: 3, src_index: 5, c: 0 },
434        Instruction { opcode: Mul, dst_index: 1, src_index: 0, c: 0 },
435        Instruction { opcode: Xor, dst_index: 2, src_index: 0, c: 0 },
436        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
437        Instruction { opcode: Sub, dst_index: 3, src_index: 6, c: 0 },
438        Instruction { opcode: Rol, dst_index: 3, src_index: 0, c: 0 },
439        Instruction { opcode: Xor, dst_index: 2, src_index: 4, c: 0 },
440        Instruction { opcode: Mul, dst_index: 3, src_index: 5, c: 0 },
441        Instruction { opcode: Xor, dst_index: 2, src_index: 0, c: 0 },
442        Instruction { opcode: Rol, dst_index: 2, src_index: 4, c: 0 },
443        Instruction { opcode: Xor, dst_index: 3, src_index: 8, c: 0 },
444        Instruction { opcode: Mul, dst_index: 0, src_index: 4, c: 0 },
445        Instruction { opcode: Add, dst_index: 2, src_index: 3, c: 2235486112 },
446        Instruction { opcode: Xor, dst_index: 0, src_index: 3, c: 0 },
447        Instruction { opcode: Mul, dst_index: 0, src_index: 2, c: 0 },
448        Instruction { opcode: Xor, dst_index: 2, src_index: 7, c: 0 },
449        Instruction { opcode: Mul, dst_index: 0, src_index: 7, c: 0 },
450        Instruction { opcode: Ror, dst_index: 0, src_index: 4, c: 0 },
451        Instruction { opcode: Mul, dst_index: 3, src_index: 2, c: 0 },
452        Instruction { opcode: Add, dst_index: 2, src_index: 3, c: 382729823 },
453        Instruction { opcode: Mul, dst_index: 1, src_index: 4, c: 0 },
454        Instruction { opcode: Sub, dst_index: 3, src_index: 5, c: 0 },
455        Instruction { opcode: Add, dst_index: 3, src_index: 7, c: 446636115 },
456        Instruction { opcode: Sub, dst_index: 0, src_index: 5, c: 0 },
457        Instruction { opcode: Add, dst_index: 1, src_index: 8, c: 1136500848 },
458        Instruction { opcode: Xor, dst_index: 3, src_index: 8, c: 0 },
459        Instruction { opcode: Mul, dst_index: 0, src_index: 4, c: 0 },
460        Instruction { opcode: Ror, dst_index: 3, src_index: 5, c: 0 },
461        Instruction { opcode: Mul, dst_index: 2, src_index: 0, c: 0 },
462        Instruction { opcode: Ror, dst_index: 0, src_index: 1, c: 0 },
463        Instruction { opcode: Add, dst_index: 0, src_index: 7, c: 4221005163 },
464        Instruction { opcode: Rol, dst_index: 0, src_index: 2, c: 0 },
465        Instruction { opcode: Add, dst_index: 0, src_index: 7, c: 1789679560 },
466        Instruction { opcode: Xor, dst_index: 0, src_index: 3, c: 0 },
467        Instruction { opcode: Add, dst_index: 2, src_index: 8, c: 2725270475 },
468        Instruction { opcode: Xor, dst_index: 1, src_index: 4, c: 0 },
469        Instruction { opcode: Sub, dst_index: 3, src_index: 8, c: 0 },
470        Instruction { opcode: Xor, dst_index: 3, src_index: 5, c: 0 },
471        Instruction { opcode: Sub, dst_index: 3, src_index: 2, c: 0 },
472        Instruction { opcode: Rol, dst_index: 2, src_index: 2, c: 0 },
473        Instruction { opcode: Add, dst_index: 3, src_index: 6, c: 4110965463 },
474        Instruction { opcode: Xor, dst_index: 2, src_index: 6, c: 0 },
475        Instruction { opcode: Sub, dst_index: 2, src_index: 7, c: 0 },
476        Instruction { opcode: Sub, dst_index: 3, src_index: 1, c: 0 },
477        Instruction { opcode: Sub, dst_index: 1, src_index: 8, c: 0 },
478        Instruction { opcode: Ror, dst_index: 1, src_index: 2, c: 0 },
479        Instruction { opcode: Mul, dst_index: 0, src_index: 1, c: 0 },
480        Instruction { opcode: Mul, dst_index: 2, src_index: 0, c: 0 },
481        Instruction { opcode: Ret, dst_index: 0, src_index: 0, c: 0 },
482        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
483        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
484        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
485        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
486        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
487        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
488        Instruction { opcode: Mul, dst_index: 0, src_index: 0, c: 0 },
489    ];
490
491    #[test]
492    fn test1_variant4_random_math() {
493        let mut a1 = u128::from_le_bytes(hex_to_array("969ecd223474a6bb3be76c637db7457b"));
494        let mut c2 = u128::from_le_bytes(hex_to_array("dbd1f6404d4826c52f209951334e6ea7"));
495        let mut r: [u32; 9] = [1336109178, 464004736, 1552145461, 3528897376, 0, 0, 0, 0, 0];
496        let b_bytes: [u8; 32] =
497            hex_to_array("8dfa6d2c82e1806367b844c15f0439ced99c9a4bae0badfb8a8cf8504b813b7d");
498        let b: [u128; 2] = [
499            u128::from_le_bytes(subarray_copy(&b_bytes, 0)),
500            u128::from_le_bytes(subarray_copy(&b_bytes, 16)),
501        ];
502
503        variant4_random_math(&mut a1, &mut c2, &mut r, &b, &CODE);
504
505        assert_eq!(
506            hex::encode(a1.to_le_bytes()),
507            "1cb6fe7738de9e764dd73ea37c438056"
508        );
509        assert_eq!(
510            hex::encode(c2.to_le_bytes()),
511            "215fbd2bd8c7fceb2f209951334e6ea7"
512        );
513        #[rustfmt::skip]
514        assert_eq!(r, [
515            3226611830, 767947777, 1429416074, 3443042828, 583900822, 1668081467, 745405069,
516            1268423897, 1358466186
517        ]);
518    }
519
520    #[test]
521    fn test2_variant4_random_math() {
522        let mut a1 = u128::from_le_bytes(hex_to_array("643955bde578c845e4898703c3ce5eaa"));
523        let mut c2 = u128::from_le_bytes(hex_to_array("787e2613b8fd0a2dadad16d4ec189035"));
524        let mut r: [u32; 9] = [
525            3226611830, 767947777, 1429416074, 3443042828, 583900822, 1668081467, 745405069,
526            1268423897, 1358466186,
527        ];
528        let b_bytes: [u8; 32] =
529            hex_to_array("d4d1e70f7da4089ae53b2e7545e4242a8dfa6d2c82e1806367b844c15f0439ce");
530        let b: [u128; 2] = [
531            u128::from_le_bytes(subarray_copy(&b_bytes, 0)),
532            u128::from_le_bytes(subarray_copy(&b_bytes, 16)),
533        ];
534
535        variant4_random_math(&mut a1, &mut c2, &mut r, &b, &CODE);
536
537        assert_eq!(
538            hex::encode(a1.to_le_bytes()),
539            "c40cb4b3a3640a958cc919ccb4ff29e6"
540        );
541        assert_eq!(
542            hex::encode(c2.to_le_bytes()),
543            "0f5a3efd2e2f610fadad16d4ec189035"
544        );
545        #[rustfmt::skip]
546        assert_eq!(r, [
547            3483254888_u32, 1282879863, 249640352, 3502382150, 3176479076, 59214308, 266850772,
548            745405069, 3242506343
549        ]);
550    }
551}