diff --git a/.claude/plans/encaps_plan.md b/.claude/plans/encaps_plan.md index eaab2e3..7812279 100644 --- a/.claude/plans/encaps_plan.md +++ b/.claude/plans/encaps_plan.md @@ -71,7 +71,7 @@ ct_bytes_rt = c1_bytes_rt + c2_bytes_rt; // 768 / 1088 / 1568 - **E2 — y/e1/e2 采样 (η1/η2)**:ST_ENC_C,nonce 0..2K,eta 在 e1/e2 切 2。dbg 验证 y/e1/e2(对 ml-kem-r)。 - **E3 — ŷ = NTT(y)**:ST_ENC_N,mode=0,就地。dbg 对 y_hat。 - **E4 — u = INTT(Σ Â[j][i]∘ŷ[j]) + e1**:ST_ENC_U,poly_mul + 累加 + INTT(mode=1) + 加 e1。**转置寻址 slot=j·K+i**。dbg 对 u。 -- **E5 — Compress_du + byteEncode_du → c1**:comp_decomp + 通用打包器写 ct_bram c1 区。dbg 对 ct[0..c1_bytes]==KAT.ct 前缀。 +- **E5 — Compress_du + byteEncode_du → c1** ✅:comp_decomp(mode0,d=du) + 通用 LSB-first 打包器(ST_ENC_C1)写 ct_bram c1 区。dbg_ct tap 比 ct[0..c1_bytes]==KAT.ct 前缀,K=2/3/4 全过(含 K=4 du=11 跨字节)。runner = `./run_enc.sh K CASE`。 - **E6 — v = INTT(Σ t̂[j]∘ŷ[j]) + e2 + mu**:ST_ENC_V,mu 流内由 m bit 生成。dbg 对 v。 - **E7 — Compress_dv + byteEncode_dv → c2 + 端到端 KAT**:写 ct c2 区。干净 TB 喂 ek/m,比 ct(全长)==KAT.ct 且 ss==KAT.ss,K=2/3/4 各 count=0..N。 diff --git a/run_enc.sh b/run_enc.sh new file mode 100755 index 0000000..42e6225 --- /dev/null +++ b/run_enc.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# run_enc.sh - Run Vivado XSIM for the ML-KEM Encaps testbench (tb_mlkem_enc_katK). +# +# Usage: ./run_enc.sh [K] [CASE] +# ./run_enc.sh # K=2 CASE=0 +# ./run_enc.sh 3 1 # K=3 CASE=1 +# +# Compiles the same RTL as the KeyGen tcl (mlkem_top + leaves), elaborates the +# encaps TB for the requested K, and runs the requested CASE. + +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VIVADO_SETTINGS="/opt/Xilinx/Vivado/2019.2/settings64.sh" + +SEL_K="${1:-2}" +SEL_CASE="${2:-0}" + +source "$VIVADO_SETTINGS" +export LD_PRELOAD="${LD_PRELOAD}${LD_PRELOAD:+:}/usr/lib64/libtinfo.so.5" + +cd "$SCRIPT_DIR" +rm -rf xsim.dir .Xil + +# Compile every xvlog RTL line from the KeyGen tcl (same datapath), verbatim. +TCL="sync_rtl/top/TB/xsim_run.tcl" +while read -r cmd; do + [[ "$cmd" == *tb_mlkem_kg_katK* ]] && continue # skip the KeyGen TB + [[ "$cmd" == *mlkem_top.v* ]] && continue # defer top until comp_decomp compiled + echo " $cmd" + eval "$cmd" || { echo "COMPILE FAILED: $cmd"; exit 1; } +done < <(grep -E '^xvlog ' "$TCL") + +# Encaps-only leaf: comp_decomp_sync (compress) + its pipeline_reg dep. +for f in sync_rtl/common/pipeline_reg.v sync_rtl/comp_decomp/comp_decomp_sync.v; do + [[ -f "$f" ]] || continue + echo " xvlog -sv --relax -i . $f" + xvlog -sv --relax -i . "$f" || { echo "COMPILE FAILED: $f"; exit 1; } +done + +# Now compile the top (depends on comp_decomp_sync). +echo " xvlog -sv --relax -i . sync_rtl/top/mlkem_top.v" +xvlog -sv --relax -i . sync_rtl/top/mlkem_top.v \ + || { echo "TOP COMPILE FAILED"; exit 1; } + +# Compile the encaps TB +echo " xvlog -sv --relax sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v" +xvlog -sv --relax sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v \ + || { echo "TB COMPILE FAILED"; exit 1; } + +echo " xelab tb_mlkem_enc_katK_xsim -generic_top KP=$SEL_K -s mlkem_enc_k$SEL_K --timescale 1ns/1ps" +xelab tb_mlkem_enc_katK_xsim -generic_top KP=$SEL_K -s mlkem_enc_k$SEL_K --timescale 1ns/1ps \ + || { echo "ELAB FAILED for K=$SEL_K"; exit 1; } + +LOG="/tmp/run_enc_k${SEL_K}_c${SEL_CASE}.log" +echo " xsim mlkem_enc_k$SEL_K -R -testplusarg CASE=$SEL_CASE" +xsim "mlkem_enc_k$SEL_K" -R -testplusarg "CASE=$SEL_CASE" 2>&1 | tee "$LOG" diff --git a/sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v b/sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v index 8b5763b..c8853f0 100644 --- a/sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v +++ b/sync_rtl/top/TB/tb_mlkem_enc_katK_xsim.v @@ -10,6 +10,8 @@ module tb_mlkem_enc_katK_xsim; parameter KP = 2; localparam EKB = 384*KP + 32; // ek (=pk) bytes localparam CTB = (KP==4) ? 1568 : (32*(10*KP+4)); // ct bytes: K2 768,K3 1088,K4 1568 + localparam DU = (KP==4) ? 11 : 10; // compression du + localparam C1B = 32*DU*KP; // c1 byte count: K2 640,K3 960,K4 1408 reg clk=0, rst_n=0, start_i=0; reg [2:0] k_i; @@ -41,8 +43,9 @@ module tb_mlkem_enc_katK_xsim; reg [7:0] ek_b [0:EKB-1]; reg [7:0] m_b [0:31]; reg [7:0] ss_b [0:31]; + reg [7:0] ct_b [0:CTB-1]; integer c, i, errors, casenum, j; - reg [8*80-1:0] tag, ekfile, mfile, ssfile; + reg [8*80-1:0] tag, ekfile, mfile, ssfile, ctfile; initial begin if (!$value$plusargs("CASE=%d", casenum)) casenum = 0; @@ -50,9 +53,11 @@ module tb_mlkem_enc_katK_xsim; $sformat(ekfile, "sync_rtl/top/TB/vectors/enc_%0s_c%0d_ek.hex", tag, casenum); $sformat(mfile, "sync_rtl/top/TB/vectors/enc_%0s_c%0d_m.hex", tag, casenum); $sformat(ssfile, "sync_rtl/top/TB/vectors/enc_%0s_c%0d_ss.hex", tag, casenum); + $sformat(ctfile, "sync_rtl/top/TB/vectors/enc_%0s_c%0d_ct.hex", tag, casenum); $readmemh(ekfile, ek_b); $readmemh(mfile, m_b); $readmemh(ssfile, ss_b); + $readmemh(ctfile, ct_b); // build m_i: byte i in m_i[8*i +: 8] m_i = 256'd0; @@ -102,6 +107,9 @@ module tb_mlkem_enc_katK_xsim; verify_e3; verify_e4; end + // E5: c1 = byteEncode_du(Compress_du(u)) must equal KAT.ct[0..C1B-1]. + // Runs for every K/case (ct_b is the full KAT ciphertext). + verify_e5; $finish; end @@ -193,4 +201,24 @@ module tb_mlkem_enc_katK_xsim; end endtask initial begin #120000000; $display("FAIL: global timeout"); $finish; end + + // E5: read ct_bram bytes 0..C1B-1 via dbg_ct tap; compare to KAT.ct prefix. + // dbg_ct_idx_i -> ct_rd_addr (1-cyc registered read) -> dbg_ct_o (comb tap): + // wait 3 cycles per byte (same cadence as the coeff readback tasks). + task verify_e5; + integer be; + begin + be = 0; + for (i = 0; i < C1B; i = i + 1) begin + dbg_ct_idx_i = i[10:0]; + @(posedge clk); @(posedge clk); @(posedge clk); + if (dbg_ct_o !== ct_b[i]) begin + if (be < 8) $display(" C1[%0d] got=%02x exp=%02x", i, dbg_ct_o, ct_b[i]); + be = be + 1; + end + end + if (be == 0) $display("K=%0d CASE %0d PASS (E5): c1 (%0d B) == KAT.ct prefix", KP, casenum, C1B); + else $display("K=%0d CASE %0d FAIL (E5): %0d c1 byte mismatches", KP, casenum, be); + end + endtask endmodule diff --git a/sync_rtl/top/mlkem_top.v b/sync_rtl/top/mlkem_top.v index 3826422..b8dc470 100644 --- a/sync_rtl/top/mlkem_top.v +++ b/sync_rtl/top/mlkem_top.v @@ -128,11 +128,21 @@ module mlkem_top #( reg td_we; reg [9:0] td_wa; // PT_AW=10 (declared below; literal here) reg [11:0] td_wd; - // ct readback tap (ct_bram added in E5/E7); tied off until then. - /* verilator lint_off UNUSEDSIGNAL */ - wire [10:0] dbg_ct_idx_unused = dbg_ct_idx_i; - /* verilator lint_on UNUSEDSIGNAL */ - assign dbg_ct_o = 8'd0; + + // ---- ct_bram: ciphertext byte buffer (<=1568 B). Written by E5/E7 + // (compress + byteEncode_du/dv), read back via dbg_ct tap. ---- + wire [10:0] ct_rd_addr; + wire [7:0] ct_rd_data; + reg ct_we; + reg [10:0] ct_wa; + reg [7:0] ct_wd; + sd_bram #(.W(8), .D(2048), .A(11)) u_ct_bram ( + .clk(clk), + .rd_addr(ct_rd_addr), .rd_data(ct_rd_data), + .wr_en(ct_we), .wr_addr(ct_wa), .wr_data(ct_wd) + ); + assign ct_rd_addr = dbg_ct_idx_i; + assign dbg_ct_o = ct_rd_data; // ================================================================ // Polynomial storage, sized for KMAX (worst case). Runtime k uses a @@ -263,6 +273,7 @@ module mlkem_top #( (st == ST_ENC_U) ? ((u_sub == 2'd0) ? u_pm_b_full[PSE_AW-1:0] : // MAC: y_hat[u_j] u_add_e1rd[PSE_AW-1:0]) : // ADD: e1[u_row] read + (st == ST_ENC_C1) ? cp_se_full[PSE_AW-1:0] : // C1: u[cp_poly] dbg_se_addr[PSE_AW-1:0]; // bank_se write: KeyGen ST_C CBD (s/e), Encaps ST_ENC_C CBD (y/e1, c_poly<2K; // e2 at c_poly==2K goes to bank_t instead), ST_N/ST_ENC_N NTT writeback, @@ -743,6 +754,45 @@ module mlkem_top #( wire [12:0] u_usum = {1'b0, bt_rd_data} + {1'b0, bse_rd_data}; // psum + e1 wire [11:0] u_uq = (u_usum >= 13'(Q)) ? (u_usum - 13'(Q)) : u_usum[11:0]; + // ================================================================ + // E5/E7: Compress_d + byteEncode_d -> ciphertext (Encaps ST_ENC_C1/C2). + // Per coeff: read poly coeff -> comp_decomp (mode 0 compress, d=du/dv) -> + // bit-packer (LSB-first) -> emit bytes to ct_bram. c1 = K polys of u + // (d=du), then c2 = 1 poly of v (d=dv). Per poly = 256 coeffs -> 32*d + // bytes (whole), so the bit buffer empties at each poly boundary. + // micro-phase cp_ph: 0 present coeff addr; 1 feed comp_decomp (cd_valid); + // 2 wait pipe; 3 capture compressed + accumulate bits; 4..n drain bytes. + // ================================================================ + wire cd_active = (st == ST_ENC_C1) || (st == ST_ENC_C2); + reg [11:0] cd_coeff; // coeff presented to comp_decomp + reg cd_valid; // 1-cyc pulse to comp_decomp + wire cd_ready; + wire [11:0] cd_out; // compressed value (low d bits valid) + wire cd_vo; + wire [4:0] cp_d = (st == ST_ENC_C2) ? dv_rt : du_rt; // compress width + comp_decomp_sync u_comp ( + .clk(clk), .rst_n(rst_n), + .coeff_in(cd_coeff), + .d(cp_d), + .mode(1'b0), // compress + .valid_i(cd_valid), + .ready_o(cd_ready), + .coeff_out(cd_out), + .valid_o(cd_vo), + .ready_i(1'b1) + ); + // bit-packer / ct walk bookkeeping + reg [2:0] cp_poly; // c1: 0..K-1 (u rows); c2: single v + reg [7:0] cp_idx; // coeff 0..255 within poly + reg [2:0] cp_ph; // micro-phase + reg [24:0] cp_buf; // bit accumulator (LSB-first) + reg [5:0] cp_nbits; // valid bits in cp_buf + reg [11:0] cp_wa; // ct_bram byte write address (runs c1 then c2) + reg cp_done; // serialization complete (this region) + // coeff source: c1 reads u[cp_poly] in bank_se rel (K+cp_poly); c2 reads v + // (lands in bank_t rel slot V_SLOT -- defined in E6). For E5 only c1 path. + wire [13:0] cp_se_full = ({2'b0,k_r}+{2'b0,cp_poly})*256 + cp_idx; // bank_se u[cp_poly] + reg pm_valid; wire pm_ready; wire [11:0] pm_coeff; @@ -804,7 +854,8 @@ module mlkem_top #( ST_ENC_A: if (a_pair >= kk_rt) st_next = ST_ENC_C; ST_ENC_C: if (c_poly >= {k_r, 1'b1}) st_next = ST_ENC_N; // 2K+1 polys done ST_ENC_N: if (n_slot >= {2'b0, k_r}) st_next = ST_ENC_U; // K slots (y_hat) - ST_ENC_U: if (u_row >= k_r) st_next = ST_DONE; // E4: u[0..K-1] done + ST_ENC_U: if (u_row >= k_r) st_next = ST_ENC_C1; // u[0..K-1] done + ST_ENC_C1: if (cp_done) st_next = ST_DONE; // E5: c1 packed ST_ENC_TDEC: if (td_done) st_next = ST_DONE; // (TDEC deferred to V-prep later) ST_DONE: st_next = ST_IDLE; default: st_next = ST_IDLE; @@ -878,6 +929,18 @@ module mlkem_top #( u_aidx <= 9'd0; u_awidx <= 8'd0; u_avalid <= 1'b0; + cd_coeff <= 12'd0; + cd_valid <= 1'b0; + cp_poly <= 3'd0; + cp_idx <= 8'd0; + cp_ph <= 3'd0; + cp_buf <= 25'd0; + cp_nbits <= 6'd0; + cp_wa <= 12'd0; + cp_done <= 1'b0; + ct_we <= 1'b0; + ct_wa <= 11'd0; + ct_wd <= 8'd0; e_poly <= 3'd0; e_pair <= 8'd0; e_ph <= 2'd0; @@ -911,6 +974,7 @@ module mlkem_top #( ek_we <= 1'b0; dkp_we <= 1'b0; td_we <= 1'b0; // TDEC bank_t write default low + ct_we <= 1'b0; // ct_bram byte write default low (E5/E7) // Kick off when entering from IDLE: KeyGen starts G; Encaps captures // op/m and arms the H(ek) machinery (ST_ENC_H reuses the ST_H FSM). @@ -1241,11 +1305,75 @@ module mlkem_top #( u_pending <= 1'b0; pm_valid <= 1'b0; end else begin - u_row <= u_row + 3'd1; // == K -> ST_DONE + u_row <= u_row + 3'd1; // == K -> ST_ENC_C1 end end end + // Arm E5 (ST_ENC_C1) when U finishes: c1 = byteEncode_du(Compress_du(u)). + // Walk K polys * 256 coeffs; reset bit-packer + ct write pointer. + if (st == ST_ENC_U && st_next == ST_ENC_C1) begin + cp_poly <= 3'd0; + cp_idx <= 8'd0; + cp_ph <= 3'd0; + cp_buf <= 25'd0; + cp_nbits <= 6'd0; + cp_wa <= 12'd0; + cp_done <= 1'b0; + cd_valid <= 1'b0; + end + + // ---- ST_ENC_C1: Compress_du(u[i]) -> byteEncode_du -> ct c1 region ---- + // Per coeff, 5-phase micro-sequence (read-ahead 1 cyc bram + 1 cyc + // comp_decomp pipe), then a drain sub-phase emitting whole bytes: + // ph0: present u[cp_poly][cp_idx] addr to bank_se (cp_se_full). + // ph1: coeff arrives (bse_rd_data) -> latch into cd_coeff, pulse cd_valid. + // ph2: drop cd_valid (1-cyc pulse); comp_decomp captures. + // ph3: cd_vo high -> cd_out (low du bits) valid; append LSB-first to cp_buf. + // ph4: drain: while >=8 bits buffered, emit one ct byte/cycle; then advance. + // Each poly = 256 coeffs = 32*du bytes (whole), so cp_buf empties at + // each poly boundary (no carry across polys). + if (st == ST_ENC_C1 && !cp_done) begin + case (cp_ph) + 3'd0: cp_ph <= 3'd1; // addr presented; wait read + 3'd1: begin + cd_coeff <= bse_rd_data; // u coeff (registered read) + cd_valid <= 1'b1; // feed comp_decomp (1-cyc pulse) + cp_ph <= 3'd2; + end + 3'd2: begin + cd_valid <= 1'b0; // comp_decomp captured this cyc + cp_ph <= 3'd3; + end + 3'd3: begin + // cd_out valid (cd_vo): append du bits LSB-first at bit cp_nbits + cp_buf <= cp_buf | (({13'd0, cd_out} & ((25'd1 << du_rt) - 25'd1)) << cp_nbits); + cp_nbits <= cp_nbits + {1'b0, du_rt}; + cp_ph <= 3'd4; + end + default: begin // 3'd4: drain whole bytes + if (cp_nbits >= 6'd8) begin + ct_we <= 1'b1; + ct_wa <= cp_wa; + ct_wd <= cp_buf[7:0]; + cp_wa <= cp_wa + 12'd1; + cp_buf <= cp_buf >> 8; + cp_nbits <= cp_nbits - 6'd8; + end else begin + // coeff fully packed; advance coeff / poly + if (cp_idx == 8'd255) begin + cp_idx <= 8'd0; + if (cp_poly + 3'd1 < k_r) cp_poly <= cp_poly + 3'd1; + else cp_done <= 1'b1; // c1 complete -> DONE + end else begin + cp_idx <= cp_idx + 8'd1; + end + cp_ph <= 3'd0; + end + end + endcase + end + // Arm E stage when M finishes if (st == ST_M && st_next == ST_E) begin e_poly <= 3'd0;