Compare commits

...

3 Commits

Author SHA1 Message Date
4f46c1cd02 build(vivado): point create_project.tcl at shared keccak_core variants
Match the sim flow: use sha3_top_shared + sample_*_sync_shared so the Vivado
project synthesises the single-keccak_core datapath.
2026-06-28 15:36:36 +08:00
460a6ed70c 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.
2026-06-28 15:35:55 +08:00
851630f73c refactor(kg): merge G/H into single shared sha3_top (4->3 keccak_core)
ST_G (single-block G) and ST_H (multi-block H(ek)) are disjoint FSM phases,
so one sha3_top serves both: mb_en and ready_i are phase-muxed, h_hash/h_vo
alias the shared core's outputs. Removes the dedicated u_sha3_h instance and
its keccak_core. 11/11 KAT PASS (K=2 c0-4, K=3/4 c0-2), byte-exact, 0 file-not-found.
2026-06-28 15:23:30 +08:00
4 changed files with 285 additions and 32 deletions

View File

@@ -36,11 +36,11 @@ set_property target_simulator XSim [current_project]
# ── SHA3 / Keccak ──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sha3/keccak_round.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sha3/keccak_core.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sha3/sha3_top.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sha3/sha3_top_shared.v
# ── 采样 ──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sample_ntt/sample_ntt_sync.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sample_cbd/sample_cbd_sync.v
# ── 采样(共享 keccak_core 变体)──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sample_ntt/sample_ntt_sync_shared.v
read_verilog -sv ${PROJECT_DIR}/sync_rtl/sample_cbd/sample_cbd_sync_shared.v
# ── NTT ──
read_verilog -sv ${PROJECT_DIR}/sync_rtl/ntt/barrett_mul.v

View File

@@ -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

View File

@@ -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

View File

@@ -179,20 +179,86 @@ 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
sha3_top u_sha3 (
// ================================================================
// 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),
.mode(2'b00), // G = SHA3-512
.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_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),
.valid_i(sha3_valid),
.ready_o(sha3_ready),
.hash_o(sha3_hash),
.valid_o(sha3_vo),
.ready_i(sha3_ack),
.mb_en(1'b0), .mb_block_i(1088'b0), .mb_valid_i(1'b0),
.mb_last_i(1'b0), .mb_ready_o()
.ready_i(sha3_mb_en ? h_ack : sha3_ack),
.mb_en(sha3_mb_en),
.mb_block_i(h_block_r),
.mb_valid_i(h_mbvalid),
.mb_last_i(h_mblast),
.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)
);
// ---- second sha3_top dedicated to multi-block H(ek) (SHA3-256, 800B->6 blk) ----
// ---- multi-block H(ek) state (SHA3-256, 6/9/12 blocks); fed to shared u_sha3 ----
reg [1087:0] h_block_r; // current pre-padded rate block
reg h_mbvalid;
reg h_mblast;
@@ -213,21 +279,11 @@ module mlkem_top #(
reg [7:0] h_wb_pad; // pad constant to use if g is out of ek range
reg h_wb_inek; // 1 if g is within ek range (use BRAM data)
sha3_top u_sha3_h (
.clk(clk), .rst_n(rst_n),
.mode(2'b01), // unused in mb mode
.data_i(512'b0),
.valid_i(1'b0),
.ready_o(),
.hash_o(h_hash),
.valid_o(h_vo),
.ready_i(h_ack),
.mb_en(1'b1),
.mb_block_i(h_block_r),
.mb_valid_i(h_mbvalid),
.mb_last_i(h_mblast),
.mb_ready_o(h_mbready)
);
// h_hash / h_vo are now served by the shared u_sha3 above (mb_en=1 during
// ST_H). They alias the single core's outputs; the H consumer logic below
// already gates on st==ST_H, and u_sha3's valid_o/hash_o are mb-selected.
assign h_hash = sha3_hash;
assign h_vo = sha3_vo;
// SHA3-256 over ek (ek_bytes_rt bytes): rate=136. Padded length = h_nblk_rt*136.
// pad: byte ek_bytes_rt = 0x06 (domain + first pad bit), last byte |= 0x80.
@@ -266,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),
@@ -277,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)) ----
@@ -288,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),
@@ -298,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