Skip to main content

ab_riscv_interpreter/rv64/zk/zkn/zkne/
rv64_zkne_helpers.rs

1//! Opaque helpers for RV64 Zkne extension
2
3cfg_select! {
4    all(
5        not(miri),
6        target_arch = "riscv64",
7        target_feature = "zkne"
8    ) => {
9        // Nothing, calling native intrinsics
10    }
11    all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
12        /// x86-64 AES-NI implementation
13        #[expect(
14            clippy::inline_modules,
15            reason = "Small internal API, it is more readable this way"
16        )]
17        mod x86_64 {
18            use core::arch::x86_64::{
19                _mm_aesenclast_si128, _mm_aesenc_si128, _mm_extract_epi64,
20                _mm_set_epi64x, _mm_setzero_si128,
21            };
22
23            /// `_mm_aesenclast_si128(state, zero)` computes ShiftRows + SubBytes then XORs with
24            /// the round key. Zero key -> no-op XOR, matching `aes64es`.
25            #[inline]
26            #[target_feature(enable = "aes,sse4.1")]
27            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
28            pub(super) fn aes64es(rs1: u64, rs2: u64) -> u64 {
29                let state = _mm_set_epi64x(rs2.cast_signed(), rs1.cast_signed());
30                let zero = _mm_setzero_si128();
31                let result = _mm_aesenclast_si128(state, zero);
32                _mm_extract_epi64::<0>(result).cast_unsigned()
33            }
34
35            /// `_mm_aesenc_si128(state, zero)` computes ShiftRows + SubBytes + MixColumns then
36            /// XORs with the round key. Zero key -> no-op XOR, matching `aes64esm`.
37            #[inline]
38            #[target_feature(enable = "aes,sse4.1")]
39            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
40            pub(super) fn aes64esm(rs1: u64, rs2: u64) -> u64 {
41                let state = _mm_set_epi64x(rs2.cast_signed(), rs1.cast_signed());
42                let zero = _mm_setzero_si128();
43                let result = _mm_aesenc_si128(state, zero);
44                _mm_extract_epi64::<0>(result).cast_unsigned()
45            }
46        }
47    }
48    all(target_arch = "aarch64", target_feature = "aes") => {
49        /// AArch64 AES implementation
50        ///
51        /// AESE XORs the round key first, then applies SubBytes + ShiftRows (note: ARM ShiftRows
52        /// direction matches the forward cipher). With a zero round key the XOR is a no-op,
53        /// leaving pure SubBytes + ShiftRows - identical to what `aes64es` requires.
54        #[expect(
55            clippy::inline_modules,
56            reason = "Small internal API, it is more readable this way"
57        )]
58        mod aarch64 {
59            use core::arch::aarch64::{
60                vaeseq_u8, vaesmcq_u8, vcombine_u64, vcreate_u64, vdupq_n_u8, vgetq_lane_u64,
61                vreinterpretq_u8_u64, vreinterpretq_u64_u8,
62            };
63
64            #[inline]
65            #[target_feature(enable = "aes")]
66            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
67            pub(super) fn aes64es(rs1: u64, rs2: u64) -> u64 {
68                let state = vreinterpretq_u8_u64(vcombine_u64(vcreate_u64(rs1), vcreate_u64(rs2)));
69                let zero = vdupq_n_u8(0);
70                let result = vaeseq_u8(state, zero);
71                vgetq_lane_u64::<0>(vreinterpretq_u64_u8(result))
72            }
73
74            /// `vaesmcq_u8(vaeseq_u8(state, zero))` maps exactly to `aes64esm`
75            #[inline]
76            #[target_feature(enable = "aes")]
77            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
78            pub(super) fn aes64esm(rs1: u64, rs2: u64) -> u64 {
79                let state = vreinterpretq_u8_u64(vcombine_u64(vcreate_u64(rs1), vcreate_u64(rs2)));
80                let zero = vdupq_n_u8(0);
81                let after_sub_shift = vaeseq_u8(state, zero);
82                let result = vaesmcq_u8(after_sub_shift);
83                vgetq_lane_u64::<0>(vreinterpretq_u64_u8(result))
84            }
85        }
86    }
87    _ => {
88        /// Software fallback for aes64es, aes64esm
89        #[expect(
90            clippy::inline_modules,
91            reason = "Small internal API, it is more readable this way"
92        )]
93        mod soft {
94            use crate::rv32::zk::zkn::zknd::rv32_zknd_helpers::{SBOX, gmul};
95
96            #[inline(always)]
97            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
98            fn mix_col(col: u32) -> u32 {
99                let s0 = col as u8;
100                let s1 = (col >> 8) as u8;
101                let s2 = (col >> 16) as u8;
102                let s3 = (col >> 24) as u8;
103                let r0 = gmul(s0, 0x02) ^ gmul(s1, 0x03) ^ s2 ^ s3;
104                let r1 = s0 ^ gmul(s1, 0x02) ^ gmul(s2, 0x03) ^ s3;
105                let r2 = s0 ^ s1 ^ gmul(s2, 0x02) ^ gmul(s3, 0x03);
106                let r3 = gmul(s0, 0x03) ^ s1 ^ s2 ^ gmul(s3, 0x02);
107                u32::from(r0) | (u32::from(r1) << 8) | (u32::from(r2) << 16) | (u32::from(r3) << 24)
108            }
109
110            /// Apply ShiftRows + SubBytes to the full 128-bit state `(rs1, rs2)` and return the
111            /// low 64-bit half of the result.
112            ///
113            /// State layout: column-major, little-endian 64-bit halves.
114            /// ShiftRows shifts row `r` left by `r` columns (cyclically over 4).
115            /// Output low half contains post-transform columns 0 and 1.
116            #[inline(always)]
117            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
118            pub(super) fn aes64es(rs1: u64, rs2: u64) -> u64 {
119                let state_byte = |col: usize, row: usize| -> u8 {
120                    let word = if col < 2 { rs1 } else { rs2 };
121                    (word >> ((col % 2) * 32 + row * 8)) as u8
122                };
123
124                let mut out = 0;
125                for c in 0..2usize {
126                    for r in 0..4usize {
127                        let src_col = (c + r) & 3;
128                        let b = SBOX[state_byte(src_col, r) as usize];
129                        out |= u64::from(b) << (c * 32 + r * 8);
130                    }
131                }
132                out
133            }
134
135            #[inline(always)]
136            #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
137            pub(super) fn aes64esm(rs1: u64, rs2: u64) -> u64 {
138                let lo = aes64es(rs1, rs2);
139                let col0 = mix_col(lo as u32);
140                let col1 = mix_col((lo >> 32) as u32);
141                u64::from(col0) | (u64::from(col1) << 32)
142            }
143        }
144    }
145}
146
147#[inline(always)]
148#[doc(hidden)]
149#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
150pub fn aes64es(rs1: u64, rs2: u64) -> u64 {
151    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
152    cfg_select! {
153        all(
154            not(miri),
155            target_arch = "riscv64",
156            target_feature = "zkne"
157        ) => {
158            // SAFETY: Compile-time checked for supported feature
159            unsafe {
160                core::arch::riscv64::aes64es(rs1, rs2)
161            }
162        }
163        all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
164            // SAFETY: Compile-time checked for supported feature
165            unsafe {
166                x86_64::aes64es(rs1, rs2)
167            }
168        }
169        all(target_arch = "aarch64", target_feature = "aes") => {
170            // SAFETY: Compile-time checked for supported feature
171            unsafe {
172                aarch64::aes64es(rs1, rs2)
173            }
174        }
175        _ => { soft::aes64es(rs1, rs2) }
176    }
177}
178
179#[inline(always)]
180#[doc(hidden)]
181#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
182pub fn aes64esm(rs1: u64, rs2: u64) -> u64 {
183    // TODO: Miri is excluded because corresponding intrinsic is not implemented there
184    cfg_select! {
185        all(
186            not(miri),
187            target_arch = "riscv64",
188            target_feature = "zkne"
189        ) => {
190            // SAFETY: Compile-time checked for supported feature
191            unsafe {
192                core::arch::riscv64::aes64esm(rs1, rs2)
193            }
194        }
195        all(target_arch = "x86_64", target_feature = "aes", target_feature = "sse4.1") => {
196            // SAFETY: Compile-time checked for supported feature
197            unsafe {
198                x86_64::aes64esm(rs1, rs2)
199            }
200        }
201        all(target_arch = "aarch64", target_feature = "aes") => {
202            // SAFETY: Compile-time checked for supported feature
203            unsafe {
204                aarch64::aes64esm(rs1, rs2)
205            }
206        }
207        _ => { soft::aes64esm(rs1, rs2) }
208    }
209}