From 460a6ed70c37d33765aebb425e9a0afc026ffe65 Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Sun, 28 Jun 2026 15:35:55 +0800 Subject: [PATCH] refactor(kg): share a single keccak_core across G/H, SampleNTT, CBD (4->1) KeyGen's Keccak consumers (G/H via sha3, A via SampleNTT, C via CBD) run in disjoint top-FSM phases, so one keccak_core suffices. Add sha3_top_shared (keccak_core externalised as kc_* ports, like the existing sample_*_shared variants); mlkem_top now instantiates one keccak_core and a phase mux that routes kc_state_i/kc_valid_i from the active consumer and gates kc_valid_o per consumer (inactive samplers latch squeeze state unconditionally). Cuts the KeyGen datapath from 4 keccak_core (1600-bit state + 24-round logic each) to 1 -- the dominant ASIC area win. 11/11 KAT PASS (K=2 c0-4, K=3/4 c0-2), byte-exact, 0 file-not-found. --- sync_rtl/sha3/sha3_top_shared.v | 183 ++++++++++++++++++++++++++++++++ sync_rtl/top/TB/xsim_run.tcl | 6 +- sync_rtl/top/mlkem_top.v | 83 +++++++++++++-- 3 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 sync_rtl/sha3/sha3_top_shared.v diff --git a/sync_rtl/sha3/sha3_top_shared.v b/sync_rtl/sha3/sha3_top_shared.v new file mode 100644 index 0000000..b62ce97 --- /dev/null +++ b/sync_rtl/sha3/sha3_top_shared.v @@ -0,0 +1,183 @@ +// sha3_top_shared.v - SHA3/SHAKE wrapper with EXTERNAL (shared) keccak_core. +// +// Identical to sha3_top.v except the internal keccak_core instance is +// replaced by explicit kc_* ports, so several Keccak consumers can share a +// single keccak_core via a phase mux (see mlkem_top). The sponge state +// (mb_state_r) and all FSM/squeeze logic stay here; only the 24-round +// permutation is external. Bit-identical to sha3_top when wired to a +// dedicated keccak_core. +// +// Single-block modes (mb_en=0): +// 00 = G (SHA3-512): rate=576, suffix=01, msg_len=264, out=512 +// 01 = H (SHA3-256): rate=1088, suffix=01, msg_len=256, out=256 +// 10 = J (SHAKE-256): rate=1088, suffix=1111,msg_len=512,out=256 +// +// Multi-block SHA3-256 (mb_en=1): streams pre-padded 1088-bit rate blocks; +// the CALLER applies SHA3-256 padding to the final block. +// +// Keccak interface (port names mirror the keccak_core port each connects to): +// kc_state_o[1599:0] - keccak result (input, from keccak_core.state_o) +// kc_valid_o - keccak output valid (input, from keccak_core.valid_o) +// kc_ready_o - keccak ready for input (input, from keccak_core.ready_o) +// kc_state_i[1599:0] - keccak input state (output, to keccak_core.state_i) +// kc_valid_i - request permutation (output, to keccak_core.valid_i) +// kc_ready_i - accept keccak output (output, to keccak_core.ready_i, always 1) + +module sha3_top_shared ( + input clk, + input rst_n, + input [1:0] mode, + input [511:0] data_i, + input valid_i, + output ready_o, + output [511:0] hash_o, + output valid_o, + input ready_i, + // --- multi-block SHA3-256 absorb (tie mb_en=0 to disable) --- + input mb_en, + input [1087:0] mb_block_i, + input mb_valid_i, + input mb_last_i, + output mb_ready_o, + // --- external (shared) keccak_core interface --- + input [1599:0] kc_state_o, + input kc_valid_o, + /* verilator lint_off UNUSEDSIGNAL */ + input kc_ready_o, + /* verilator lint_on UNUSEDSIGNAL */ + output [1599:0] kc_state_i, + output kc_valid_i, + output kc_ready_i +); + + // ================================================================ + // FSM state encoding + // ================================================================ + localparam ST_IDLE = 2'd0; + localparam ST_PERMUTE = 2'd1; + localparam ST_SQUEEZE = 2'd2; + + reg [1:0] state_r, state_next; + + // ================================================================ + // Absorb state: message || suffix || pad10*1 into rate bits + // ================================================================ + wire [575:0] g_pad; + wire [1087:0] h_pad; + wire [1087:0] j_pad; + + assign g_pad = {1'b1, {308{1'b0}}, 1'b1, 2'b10, data_i[263:0]}; + assign h_pad = {1'b1, {828{1'b0}}, 1'b1, 2'b10, data_i[255:0]}; + // J: SHAKE suffix is "1111" — all ones, order irrelevant + assign j_pad = {1'b1, {570{1'b0}}, 1'b1, 4'b1111, data_i[511:0]}; + + wire [1599:0] absorb_state; + assign absorb_state = (mode == 2'b00) ? {{(1600-576){1'b0}}, g_pad} : + (mode == 2'b01) ? {{(1600-1088){1'b0}}, h_pad} : + (mode == 2'b10) ? {{(1600-1088){1'b0}}, j_pad} : + 1600'd0; + + // ================================================================ + // Multi-block SHA3-256 absorb FSM (active only when mb_en=1) + // ================================================================ + localparam MB_IDLE = 2'd0; // ready for a block (or first block) + localparam MB_PERMUTE = 2'd1; // keccak running on xored state + localparam MB_DONE = 2'd2; // squeeze: present 256-bit digest + + reg [1:0] mb_state, mb_state_next; + reg [1599:0] mb_state_r; // running sponge state + reg mb_last_r; // captured last-block flag + reg [255:0] mb_digest_r; // latched 256-bit digest (sticky in MB_DONE) + + // XOR the incoming block into the low 1088 bits (rate) of the state. + wire [1599:0] mb_xored; + assign mb_xored = mb_state_r ^ {{(1600-1088){1'b0}}, mb_block_i}; + + // Accept a block only in MB_IDLE while enabled. + assign mb_ready_o = mb_en && (mb_state == MB_IDLE); + wire mb_accept = mb_en && (mb_state == MB_IDLE) && mb_valid_i; + wire mb_kc_valid = mb_accept; // start keccak on the accept cycle + + always @(*) begin + mb_state_next = mb_state; + case (mb_state) + MB_IDLE: if (mb_accept) mb_state_next = MB_PERMUTE; + MB_PERMUTE: if (kc_valid_o) mb_state_next = mb_last_r ? MB_DONE : MB_IDLE; + MB_DONE: if (ready_i) mb_state_next = MB_IDLE; + default: mb_state_next = MB_IDLE; + endcase + end + + // ================================================================ + // External keccak_core interface (was internal keccak_core) + // ================================================================ + // Keccak input: multi-block xored state when mb_en, else single-block absorb. + assign kc_state_i = mb_en ? mb_xored : absorb_state; + // Always accept keccak output (matches the dedicated-core ready_i=1'b1). + assign kc_ready_i = 1'b1; + // kc_valid_i: single-block start (state_next==PERMUTE) OR multi-block accept. + assign kc_valid_i = mb_en ? mb_kc_valid : (state_next == ST_PERMUTE); + + // ================================================================ + // FSM combinational logic + // ================================================================ + assign ready_o = !mb_en && (state_r == ST_IDLE); + + always @(*) begin + state_next = state_r; + case (state_r) + ST_IDLE: if (valid_i && ready_o) state_next = ST_PERMUTE; + ST_PERMUTE: if (kc_valid_o) state_next = ST_SQUEEZE; + ST_SQUEEZE: if (valid_o && ready_i) state_next = ST_IDLE; + default: state_next = ST_IDLE; + endcase + end + + // ================================================================ + // Output + // ================================================================ + reg [511:0] squeezed_state_r; + + // valid_o / hash_o serve both paths, selected by mb_en. + assign valid_o = mb_en ? (mb_state == MB_DONE) : (state_r == ST_SQUEEZE); + assign hash_o = mb_en ? {256'b0, mb_digest_r} : squeezed_state_r; + + // ================================================================ + // Sequential logic + // ================================================================ + always @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state_r <= ST_IDLE; + squeezed_state_r <= 512'd0; + mb_state <= MB_IDLE; + mb_state_r <= 1600'd0; + mb_last_r <= 1'b0; + mb_digest_r <= 256'd0; + end else begin + state_r <= state_next; + mb_state <= mb_state_next; + + // --- single-block: latch squeezed output --- + if (state_r == ST_PERMUTE && kc_valid_o) begin + squeezed_state_r <= kc_state_o[511:0]; + end + + // --- multi-block: capture last flag on accept --- + if (mb_accept) begin + mb_last_r <= mb_last_i; + end + + // --- multi-block: latch permuted state when keccak finishes --- + if (mb_state == MB_PERMUTE && kc_valid_o) begin + mb_state_r <= kc_state_o; + if (mb_last_r) mb_digest_r <= kc_state_o[255:0]; + end + + // --- multi-block: clear running state after digest consumed --- + if (mb_state == MB_DONE && ready_i) begin + mb_state_r <= 1600'd0; + end + end + end + +endmodule diff --git a/sync_rtl/top/TB/xsim_run.tcl b/sync_rtl/top/TB/xsim_run.tcl index 863ef8c..d426b1c 100644 --- a/sync_rtl/top/TB/xsim_run.tcl +++ b/sync_rtl/top/TB/xsim_run.tcl @@ -9,9 +9,9 @@ # ---- Step 1: compile RTL ---- xvlog -sv --relax -i . sync_rtl/sha3/keccak_round.v xvlog -sv --relax -i . sync_rtl/sha3/keccak_core.v -xvlog -sv --relax -i . sync_rtl/sha3/sha3_top.v -xvlog -sv --relax -i . sync_rtl/sample_ntt/sample_ntt_sync.v -xvlog -sv --relax -i . sync_rtl/sample_cbd/sample_cbd_sync.v +xvlog -sv --relax -i . sync_rtl/sha3/sha3_top_shared.v +xvlog -sv --relax -i . sync_rtl/sample_ntt/sample_ntt_sync_shared.v +xvlog -sv --relax -i . sync_rtl/sample_cbd/sample_cbd_sync_shared.v xvlog -sv --relax -i . sync_rtl/ntt/barrett_mul.v xvlog -sv --relax -i . sync_rtl/ntt/zeta_rom.v xvlog -sv --relax -i . sync_rtl/ntt/butterfly_unit.v diff --git a/sync_rtl/top/mlkem_top.v b/sync_rtl/top/mlkem_top.v index e4b24cb..7d1d514 100644 --- a/sync_rtl/top/mlkem_top.v +++ b/sync_rtl/top/mlkem_top.v @@ -179,13 +179,63 @@ module mlkem_top #( reg sha3_ack; // consumer ready for hash wire [511:0] g_data = {248'b0, 5'b0, k_r, d_i}; // data_i[263:256]=k, [255:0]=d + // ================================================================ + // Shared keccak_core + phase mux (3 consumers -> 1 core) + // + // G/H (u_sha3), SampleNTT (u_snt) and CBD (u_cbd) each need a Keccak + // permutation, but they run in DISJOINT top-FSM phases (ST_G/ST_H, ST_A, + // ST_C respectively), so at most one is ever active. One keccak_core + // serves all three: kc_state_i/kc_valid_i are muxed by phase; kc_valid_o + // is broadcast but GATED per consumer so an inactive consumer never + // latches a permutation result meant for another (the samplers latch + // squeeze state unconditionally on their kc_valid_o input). + // ================================================================ + wire [1599:0] kc_state_o; // shared core output (to all consumers) + wire kc_valid_o; // shared core output-valid + wire kc_ready_o; // shared core input-ready + + // per-consumer drives toward the core + wire [1599:0] sha3_kc_state_i, snt_kc_state_i, cbd_kc_state_i; + wire sha3_kc_valid_i, snt_kc_valid_i, cbd_kc_valid_i; + /* verilator lint_off UNUSEDSIGNAL */ + wire sha3_kc_ready_i, snt_kc_ready_i, cbd_kc_ready_i; // all 1'b1 + /* verilator lint_on UNUSEDSIGNAL */ + + // phase selects (mutually exclusive) + wire sel_sha3 = (st == ST_G) || (st == ST_H); + wire sel_snt = (st == ST_A); + wire sel_cbd = (st == ST_C); + + // gated output-valid: only the active consumer sees kc_valid_o + wire kc_valid_o_sha3 = kc_valid_o & sel_sha3; + wire kc_valid_o_snt = kc_valid_o & sel_snt; + wire kc_valid_o_cbd = kc_valid_o & sel_cbd; + + // input mux: route the active consumer's request to the core + wire [1599:0] kc_state_i_mux = sel_snt ? snt_kc_state_i : + sel_cbd ? cbd_kc_state_i : + sha3_kc_state_i; + wire kc_valid_i_mux = sel_snt ? snt_kc_valid_i : + sel_cbd ? cbd_kc_valid_i : + sel_sha3 ? sha3_kc_valid_i : 1'b0; + + keccak_core #(.ROUNDS(24)) u_keccak ( + .clk(clk), .rst_n(rst_n), + .state_i(kc_state_i_mux), + .valid_i(kc_valid_i_mux), + .ready_o(kc_ready_o), + .state_o(kc_state_o), + .valid_o(kc_valid_o), + .ready_i(1'b1) // consumers always accept (kc_ready_i=1) + ); + // ---- single shared sha3_top serving BOTH G and H ---- // G (ST_G) uses single-block mode (mb_en=0); H(ek) (ST_H) uses the // multi-block absorb path (mb_en=1). These phases are disjoint in the // top FSM, so one sha3_top (one keccak_core) is sufficient. mb_en and // ready_i are muxed by phase; data_i/mode only matter while mb_en=0. wire sha3_mb_en = (st == ST_H); - sha3_top u_sha3 ( + sha3_top_shared u_sha3 ( .clk(clk), .rst_n(rst_n), .mode(2'b00), // G = SHA3-512 (only used when mb_en=0) .data_i(g_data), @@ -198,7 +248,14 @@ module mlkem_top #( .mb_block_i(h_block_r), .mb_valid_i(h_mbvalid), .mb_last_i(h_mblast), - .mb_ready_o(h_mbready) + .mb_ready_o(h_mbready), + // shared keccak_core interface (gated by phase in the mux below) + .kc_state_o(kc_state_o), + .kc_valid_o(kc_valid_o_sha3), + .kc_ready_o(kc_ready_o), + .kc_state_i(sha3_kc_state_i), + .kc_valid_i(sha3_kc_valid_i), + .kc_ready_i(sha3_kc_ready_i) ); // ---- multi-block H(ek) state (SHA3-256, 6/9/12 blocks); fed to shared u_sha3 ---- @@ -265,7 +322,7 @@ module mlkem_top #( wire snt_last; reg snt_ack; // we accept coeffs - sample_ntt_sync #(.K(KMAX)) u_snt ( + sample_ntt_sync_shared #(.K(KMAX)) u_snt ( .clk(clk), .rst_n(rst_n), .rho_i(rho_r), .k_i(k_r), @@ -276,7 +333,14 @@ module mlkem_top #( .coeff_o(snt_coeff), .valid_o(snt_vo), .ready_i(snt_ack), - .last_o(snt_last) + .last_o(snt_last), + // shared keccak_core interface + .kc_state_o(kc_state_o), + .kc_valid_o(kc_valid_o_snt), + .kc_ready_o(kc_ready_o), + .kc_state_i(snt_kc_state_i), + .kc_valid_i(snt_kc_valid_i), + .kc_ready_i(snt_kc_ready_i) ); // ---- sample_cbd_sync: s[i]=CBD3(PRF(sigma,i)), e[i]=CBD3(PRF(sigma,K+i)) ---- @@ -287,7 +351,7 @@ module mlkem_top #( wire cbd_last; reg cbd_ack; - sample_cbd_sync u_cbd ( + sample_cbd_sync_shared u_cbd ( .clk(clk), .rst_n(rst_n), .seed_i(sigma_r), .nonce_i(c_nonce), @@ -297,7 +361,14 @@ module mlkem_top #( .coeff_o(cbd_coeff), .valid_o(cbd_vo), .ready_i(cbd_ack), - .last_o(cbd_last) + .last_o(cbd_last), + // shared keccak_core interface + .kc_state_o(kc_state_o), + .kc_valid_o(kc_valid_o_cbd), + .kc_ready_o(kc_ready_o), + .kc_state_i(cbd_kc_state_i), + .kc_valid_i(cbd_kc_valid_i), + .kc_ready_i(cbd_kc_ready_i) ); // signed (two's complement) -> [0,Q): add Q when negative