1const RHO_PI: [(usize, u32); 25] = [
16 (0, 0),
17 (6, 44),
18 (12, 43),
19 (18, 21),
20 (24, 14),
21 (3, 28),
22 (9, 20),
23 (10, 3),
24 (16, 45),
25 (22, 61),
26 (1, 1),
27 (7, 6),
28 (13, 25),
29 (19, 8),
30 (20, 18),
31 (4, 27),
32 (5, 36),
33 (11, 10),
34 (17, 15),
35 (23, 56),
36 (2, 62),
37 (8, 55),
38 (14, 39),
39 (15, 41),
40 (21, 2),
41];
42
43pub const RC: [u64; 24] = [
45 0x0000000000000001,
46 0x0000000000008082,
47 0x800000000000808a,
48 0x8000000080008000,
49 0x000000000000808b,
50 0x0000000080000001,
51 0x8000000080008081,
52 0x8000000000008009,
53 0x000000000000008a,
54 0x0000000000000088,
55 0x0000000080008009,
56 0x000000008000000a,
57 0x000000008000808b,
58 0x800000000000008b,
59 0x8000000000008089,
60 0x8000000000008003,
61 0x8000000000008002,
62 0x8000000000000080,
63 0x000000000000800a,
64 0x800000008000000a,
65 0x8000000080008081,
66 0x8000000000008080,
67 0x0000000080000001,
68 0x8000000080008008,
69];
70
71fn rotl(expr: &str, n: u32) -> String {
73 if n == 0 {
74 expr.to_string()
75 } else {
76 format!("(({expr} << {n}) | ({expr} >> {}))", 64 - n)
77 }
78}
79
80pub fn emit_keccak_round_verilog() -> String {
83 let mut w = String::new();
84 w.push_str(
85 "// keccak_round — one Keccak-f[1600] round (theta, rho+pi, chi, iota).\n\
86 // GENERATED from the same constants as logicaffeine_system::keccak::keccak_f1600.\n\
87 module keccak_round(input [1599:0] s, input [63:0] rc, output [1599:0] o);\n",
88 );
89 w.push_str(&keccak_theta_wires("s"));
90 w.push_str(&keccak_rest_wires());
91 w.push_str(&keccak_pack("o"));
92 w.push_str("endmodule\n");
93 w
94}
95
96fn keccak_theta_wires(state: &str) -> String {
99 let mut w = String::new();
100 for i in 0..25 {
101 w.push_str(&format!(" wire [63:0] a{i} = {state}[{}*64 +: 64];\n", i));
102 }
103 for col in 0..5 {
104 w.push_str(&format!(
105 " wire [63:0] c{col} = a{}^a{}^a{}^a{}^a{};\n",
106 col,
107 col + 5,
108 col + 10,
109 col + 15,
110 col + 20
111 ));
112 }
113 for col in 0..5 {
114 let left = (col + 4) % 5;
115 let right = (col + 1) % 5;
116 w.push_str(&format!(
117 " wire [63:0] d{col} = c{left} ^ {};\n",
118 rotl(&format!("c{right}"), 1)
119 ));
120 }
121 for i in 0..25 {
122 let col = i % 5;
123 w.push_str(&format!(" wire [63:0] t{i} = a{i} ^ d{col};\n"));
124 }
125 w
126}
127
128fn keccak_rest_wires() -> String {
130 let mut w = String::new();
131 for (dst, &(src, rot)) in RHO_PI.iter().enumerate() {
132 w.push_str(&format!(
133 " wire [63:0] b{dst} = {};\n",
134 rotl(&format!("t{src}"), rot)
135 ));
136 }
137 for r in 0..5 {
138 for c in 0..5 {
139 let i = 5 * r + c;
140 let n1 = 5 * r + (c + 1) % 5;
141 let n2 = 5 * r + (c + 2) % 5;
142 let chi = format!("b{i} ^ (~b{n1} & b{n2})");
143 if i == 0 {
144 w.push_str(&format!(" wire [63:0] o{i} = ({chi}) ^ rc;\n"));
145 } else {
146 w.push_str(&format!(" wire [63:0] o{i} = {chi};\n"));
147 }
148 }
149 }
150 w
151}
152
153fn keccak_pack(out: &str) -> String {
155 let mut w = format!(" assign {out} = {{");
156 for i in (0..25).rev() {
157 w.push_str(&format!("o{i}"));
158 if i != 0 {
159 w.push_str(", ");
160 }
161 }
162 w.push_str("};\n");
163 w
164}
165
166pub fn emit_keccak_core_verilog() -> String {
169 let mut w = String::new();
170 w.push_str(&emit_keccak_round_verilog());
171 w.push('\n');
172 w.push_str(
173 "// keccak_core — 24-cycle iterative Keccak-f[1600] (one keccak_round per clock).\n\
174 module keccak_core(input clk, input rst, input start, input [1599:0] s_in,\n\
175 \x20 output reg done, output reg [1599:0] s_out);\n\
176 \x20 reg [4:0] round;\n\
177 \x20 reg running;\n\
178 \x20 reg [1599:0] st;\n\
179 \x20 reg [63:0] rc;\n\
180 \x20 wire [1599:0] nxt;\n\
181 \x20 keccak_round rnd(.s(st), .rc(rc), .o(nxt));\n\
182 \x20 always @(*) begin\n\
183 \x20 case (round)\n",
184 );
185 for (i, rc) in RC.iter().enumerate() {
186 w.push_str(&format!(" 5'd{i}: rc = 64'h{rc:016x};\n"));
187 }
188 w.push_str(
189 " default: rc = 64'h0;\n\
190 \x20 endcase\n\
191 \x20 end\n\
192 \x20 always @(posedge clk) begin\n\
193 \x20 if (rst) begin running <= 1'b0; done <= 1'b0; round <= 5'd0; end\n\
194 \x20 else begin\n\
195 \x20 done <= 1'b0;\n\
196 \x20 if (start) begin st <= s_in; round <= 5'd0; running <= 1'b1; end\n\
197 \x20 else if (running) begin\n\
198 \x20 st <= nxt;\n\
199 \x20 if (round == 5'd23) begin running <= 1'b0; done <= 1'b1; s_out <= nxt; end\n\
200 \x20 else round <= round + 5'd1;\n\
201 \x20 end\n\
202 \x20 end\n\
203 \x20 end\n\
204 endmodule\n",
205 );
206 w
207}
208
209pub fn emit_keccak_core_half_verilog() -> String {
213 let mut w = String::new();
214 w.push_str("// keccak_theta — theta of one Keccak-f[1600] round.\nmodule keccak_theta(input [1599:0] s, output [1599:0] t);\n");
215 w.push_str(&keccak_theta_wires("s"));
216 w.push_str(" assign t = {");
217 for i in (0..25).rev() {
218 w.push_str(&format!("t{i}"));
219 if i != 0 {
220 w.push_str(", ");
221 }
222 }
223 w.push_str("};\nendmodule\n\n");
224 w.push_str("// keccak_rest — rho, pi, chi and iota of one Keccak-f[1600] round.\nmodule keccak_rest(input [1599:0] tt, input [63:0] rc, output [1599:0] o);\n");
225 for i in 0..25 {
226 w.push_str(&format!(" wire [63:0] t{i} = tt[{}*64 +: 64];\n", i));
227 }
228 w.push_str(&keccak_rest_wires());
229 w.push_str(&keccak_pack("o"));
230 w.push_str("endmodule\n\n");
231 w.push_str(
232 "// keccak_core_half — 48-cycle iterative Keccak-f[1600]: theta, then the rest of the round, a clock each.\n\
233 module keccak_core_half(input clk, input rst, input start, input [1599:0] s_in,\n\
234 \x20 output reg done, output reg [1599:0] s_out);\n\
235 \x20 reg [4:0] round;\n\
236 \x20 reg running, half;\n\
237 \x20 reg [1599:0] st, tt;\n\
238 \x20 reg [63:0] rc;\n\
239 \x20 wire [1599:0] th, nxt;\n\
240 \x20 keccak_theta theta(.s(st), .t(th));\n\
241 \x20 keccak_rest rest(.tt(tt), .rc(rc), .o(nxt));\n\
242 \x20 always @(*) begin\n\
243 \x20 case (round)\n",
244 );
245 for (i, rc) in RC.iter().enumerate() {
246 w.push_str(&format!(" 5'd{i}: rc = 64'h{rc:016x};\n"));
247 }
248 w.push_str(
249 " default: rc = 64'h0;\n\
250 \x20 endcase\n\
251 \x20 end\n\
252 \x20 always @(posedge clk) begin\n\
253 \x20 if (rst) begin running <= 1'b0; done <= 1'b0; round <= 5'd0; half <= 1'b0; end\n\
254 \x20 else begin\n\
255 \x20 done <= 1'b0;\n\
256 \x20 if (start) begin st <= s_in; round <= 5'd0; half <= 1'b0; running <= 1'b1; end\n\
257 \x20 else if (running) begin\n\
258 \x20 if (!half) begin tt <= th; half <= 1'b1; end\n\
259 \x20 else begin\n\
260 \x20 st <= nxt; half <= 1'b0;\n\
261 \x20 if (round == 5'd23) begin running <= 1'b0; done <= 1'b1; s_out <= nxt; end\n\
262 \x20 else round <= round + 5'd1;\n\
263 \x20 end\n\
264 \x20 end\n\
265 \x20 end\n\
266 \x20 end\n\
267 endmodule\n",
268 );
269 w
270}
271
272pub fn emit_keccak_top_verilog() -> String {
279 let mut w = String::new();
280 w.push_str(&emit_keccak_core_verilog());
281 w.push('\n');
282 w.push_str(
283 "// keccak_top — narrow-bus (64-bit lane) wrapper so Keccak-f[1600] place-and-routes.\n\
284 module keccak_top(input clk, input rst, input we, input [4:0] idx,\n\
285 \x20 input [63:0] din, input go, output done, output [63:0] dout);\n\
286 \x20 reg [1599:0] state;\n\
287 \x20 reg launch;\n\
288 \x20 wire core_done;\n\
289 \x20 wire [1599:0] result;\n\
290 \x20 keccak_core core(.clk(clk), .rst(rst), .start(launch), .s_in(state),\n\
291 \x20 .done(core_done), .s_out(result));\n\
292 \x20 assign done = core_done;\n\
293 \x20 assign dout = state[64*idx +: 64];\n\
294 \x20 always @(posedge clk) begin\n\
295 \x20 if (rst) begin launch <= 1'b0; end\n\
296 \x20 else begin\n\
297 \x20 launch <= go;\n\
298 \x20 if (we) state[64*idx +: 64] <= din;\n\
299 \x20 else if (core_done) state <= result;\n\
300 \x20 end\n\
301 \x20 end\n\
302 endmodule\n",
303 );
304 w
305}
306
307pub fn emit_keccak_testbench() -> String {
311 r#"// tb_keccak_core — the RTL Keccak-f[1600] vs the native oracle (keccak_vec.hex).
312`timescale 1ns/1ps
313module tb_keccak_core;
314 reg clk = 0; always #5 clk = ~clk;
315 reg rst = 1, start = 0;
316 reg [63:0] mem [0:49];
317 reg [1599:0] s_in, expect_out;
318 wire done; wire [1599:0] s_out;
319 integer i;
320 reg seen_done = 0;
321 keccak_core dut(.clk(clk), .rst(rst), .start(start), .s_in(s_in), .done(done), .s_out(s_out));
322 always @(posedge clk) if (done) seen_done <= 1'b1;
323 initial begin
324 $readmemh("keccak_vec.hex", mem);
325 for (i = 0; i < 25; i = i + 1) s_in[64*i +: 64] = mem[i];
326 for (i = 0; i < 25; i = i + 1) expect_out[64*i +: 64] = mem[25 + i];
327 rst = 1;
328 repeat (3) @(posedge clk);
329 #1 rst = 0; // deassert OFF the edge — no reset/sample race
330 @(posedge clk); #1 start = 1;
331 @(posedge clk); #1 start = 0;
332 repeat (40) @(posedge clk); // bounded — 24-round permutation + margin, never hangs
333 if (!seen_done)
334 $display("FAIL: Keccak core never asserted done");
335 else if (s_out === expect_out)
336 $display("PASS: Keccak-f[1600] RTL core matches the native oracle bit-exact");
337 else
338 $display("FAIL: Keccak mismatch");
339 $finish;
340 end
341endmodule
342"#
343 .to_string()
344}
345
346pub const KYBER_Q: i32 = 3329;
350pub const KYBER_QINV_I16: i32 = -3327; pub fn montgomery_reduce(a: i32) -> i16 {
355 let t = (a as i16).wrapping_mul(KYBER_QINV_I16 as i16);
356 ((a - (t as i32) * KYBER_Q) >> 16) as i16
357}
358
359pub fn fqmul(a: i16, b: i16) -> i16 {
360 montgomery_reduce(a as i32 * b as i32)
361}
362
363pub fn emit_ntt_butterfly_verilog() -> String {
367 format!(
368 r#"// ntt_butterfly — Montgomery fqmul + one CT butterfly (ML-KEM, q={q}).
369// t = montgomery_reduce(zeta*b); hi = a + t; lo = a - t (16-bit wrapping).
370module ntt_butterfly(input signed [15:0] a, input signed [15:0] b,
371 input signed [15:0] zeta, output signed [15:0] hi,
372 output signed [15:0] lo);
373 wire signed [31:0] prod = zeta * b; // full 32-bit product
374 wire signed [15:0] m = prod[15:0] * (-16'sd{qmag}); // (prod as i16) * qinv (=-{qmag})
375 wire signed [31:0] sub = prod - m * 32'sd{q}; // prod - m*q
376 wire signed [15:0] t = sub[31:16]; // arithmetic >> 16
377 assign hi = a + t;
378 assign lo = a - t;
379endmodule
380"#,
381 q = KYBER_Q,
382 qmag = -KYBER_QINV_I16, )
384}
385
386pub const NTT_STEPS: usize = 9;
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub struct NttCuts(pub [bool; NTT_STEPS]);
394
395impl NttCuts {
396 pub const PER_MULTIPLY: NttCuts = NttCuts([false, false, false, true, false, true, false, false, true]);
398 pub const EVERY_STEP: NttCuts = NttCuts([true; NTT_STEPS]);
400
401 pub fn latency(self) -> u32 {
403 2 + self.0.iter().filter(|&&c| c).count() as u32
404 }
405}
406
407struct NttTerm {
410 name: String,
411 offset: u32,
412 bits: u32,
413 lo: i64,
414 hi: i64,
415}
416
417fn signed_bits(lo: i64, hi: i64) -> u32 {
419 (1..64).find(|&n| -(1i64 << (n - 1)) <= lo && hi < 1i64 << (n - 1)).expect("a range that fits 64 bits")
420}
421
422struct NttVal {
424 bits: u32,
425 stage: u32,
426 need: u32,
427}
428
429struct NttDatapath {
432 vals: std::collections::BTreeMap<String, NttVal>,
433 order: Vec<String>,
434 wires: String,
435}
436
437impl NttDatapath {
438 fn define(&mut self, name: &str, bits: u32, stage: u32, expr: String) {
439 self.wires.push_str(&format!(" wire signed [{}:0] {name}_s{stage} = {expr};\n", bits - 1));
440 self.vals.insert(name.to_string(), NttVal { bits, stage, need: stage });
441 self.order.push(name.to_string());
442 }
443
444 fn at(&mut self, name: &str, stage: u32) -> String {
445 let v = self.vals.get_mut(name).unwrap_or_else(|| panic!("{name} is read before it is defined"));
446 assert!(stage >= v.stage, "{name} is read at stage {stage}, before stage {} computes it", v.stage);
447 v.need = v.need.max(stage);
448 format!("{name}_s{stage}")
449 }
450}
451
452pub fn emit_ntt_butterfly_pipelined_verilog(name: &str, cuts: NttCuts) -> String {
462 let stage_of = |step: usize| 1 + cuts.0[..step].iter().filter(|&&c| c).count() as u32;
463 let last = stage_of(NTT_STEPS);
464 let mut dp = NttDatapath { vals: Default::default(), order: Vec::new(), wires: String::new() };
465 for operand in ["a", "x", "y"] {
466 dp.vals.insert(operand.to_string(), NttVal { bits: 16, stage: 1, need: 1 });
467 dp.order.push(operand.to_string());
468 }
469
470 let s0 = stage_of(0);
471 let (x, y) = (dp.at("x", s0), dp.at("y", s0));
472 let mut terms: Vec<NttTerm> = (0..16u32)
473 .map(|j| {
474 let name = format!("r{j}");
475 let pick = format!("({y}[{j}] ? {{{x}[15], {x}}} : 17'sd0)");
476 dp.define(&name, 17, s0, if j == 15 { format!("-{pick}") } else { pick });
477 let (lo, hi) = if j == 15 { (-32767, 32768) } else { (-32768, 32767) };
478 NttTerm { name, offset: j, bits: 17, lo, hi }
479 })
480 .collect();
481 for level in 0..4 {
482 let st = stage_of(level);
483 terms = terms
484 .chunks(2)
485 .enumerate()
486 .map(|(i, pair)| {
487 let (a, b) = (&pair[0], &pair[1]);
488 let d = b.offset - a.offset;
489 let top = signed_bits((a.lo >> d) + b.lo, (a.hi >> d) + b.hi).min(32 - b.offset);
490 let name = format!("s{level}_{i}");
491 let (av, bv) = (dp.at(&a.name, st), dp.at(&b.name, st));
492 dp.define(&format!("{name}h"), top, st, format!("$signed({av}[{}:{d}]) + {bv}", a.bits - 1));
493 let hv = dp.at(&format!("{name}h"), st);
494 dp.define(&name, d + top, st, format!("{{{hv}, {av}[{}:0]}}", d - 1));
495 NttTerm { name, offset: a.offset, bits: d + top, lo: a.lo + (b.lo << d), hi: a.hi + (b.hi << d) }
496 })
497 .collect();
498 }
499 let p = terms[0].name.clone();
500
501 let st = stage_of(4);
502 let pv = dp.at(&p, st);
503 dp.define("pl", 16, st, format!("{pv}[15:0]"));
504 dp.define("ph", 16, st, format!("{pv}[31:16]"));
505 let pl = dp.at("pl", st);
506 dp.define("u", 8, st, format!("{pl}[7:0] + {{{pl}[5:0], 2'b00}} + {{{pl}[4:0], 3'b000}}"));
507
508 let st = stage_of(5);
509 let (pl, u) = (dp.at("pl", st), dp.at("u", st));
510 dp.define("mh", 8, st, format!("{pl}[15:8] - {u}"));
511 let mh = dp.at("mh", st);
512 dp.define("m", 16, st, format!("{{{mh}, {pl}[7:0]}}"));
513
514 let st = stage_of(6);
515 let m = dp.at("m", st);
516 dp.define("u1", 20, st, format!("{{{{2{{{m}[15]}}}}, {m}, 2'b00}} + {{{{4{{{m}[15]}}}}, {m}}}"));
517 dp.define("u2", 20, st, format!("{{{m}[15], {m}, 3'b000}} + {{{{12{{{m}[15]}}}}, {m}[15:8]}}"));
518
519 let st = stage_of(7);
520 let (u1, u2) = (dp.at("u1", st), dp.at("u2", st));
521 dp.define("mq", 20, st, format!("{u1} + {u2}"));
522
523 let st = stage_of(8);
524 let (ph, mq) = (dp.at("ph", st), dp.at("mq", st));
525 dp.define("t", 16, st, format!("{ph} - {{{{4{{{mq}[19]}}}}, {mq}[19:8]}}"));
526
527 let (a, t) = (dp.at("a", last), dp.at("t", last));
528 dp.define("hi", 16, last, format!("{a} + {t}"));
529 dp.define("lo", 16, last, format!("{a} - {t}"));
530
531 let mut regs = String::new();
532 let mut moves = String::from(" a_s1 <= a; x_s1 <= zeta; y_s1 <= b;\n");
533 for v in &dp.order {
534 let val = &dp.vals[v];
535 let first = if matches!(v.as_str(), "a" | "x" | "y") { 1 } else { val.stage + 1 };
536 for st in first.min(val.need + 1)..=val.need {
537 regs.push_str(&format!(" reg signed [{}:0] {v}_s{st};\n", val.bits - 1));
538 if st > val.stage || matches!(v.as_str(), "a" | "x" | "y") && st > 1 {
539 moves.push_str(&format!(" {v}_s{st} <= {v}_s{};\n", st - 1));
540 }
541 }
542 }
543 let (hi, lo) = (dp.at("hi", last), dp.at("lo", last));
544 format!(
545 "// {name} — Montgomery fqmul + one CT butterfly (ML-KEM, q={q}), one a cycle, latency {lat}.\n\
546 module {name}(input clk, input signed [15:0] a, input signed [15:0] b,\n\
547 {pad}input signed [15:0] zeta, output reg signed [15:0] hi,\n\
548 {pad}output reg signed [15:0] lo);\n\
549 {regs}{wires} always @(posedge clk) begin\n{moves} hi <= {hi}; lo <= {lo};\n end\nendmodule\n",
550 q = KYBER_Q,
551 lat = cuts.latency(),
552 pad = " ".repeat(name.len() + 8),
553 wires = dp.wires,
554 )
555}
556
557#[cfg(test)]
558#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/keccak_rtl/tests.rs"]
559mod tests;