Commit Graph

102 Commits

Author SHA1 Message Date
b7eb59fb5b add design report and timing analysis report 2026-06-30 02:49:40 +08:00
faf15ee113 docs: 新增 mlkem_top RTL 结构图 (SVG)
用模块内真实实例名/信号名绘制的 mlkem_top RTL 框图:输入端口、控制 FSM
+ 状态寄存器、共享 keccak 三路仲裁、叶子算子(sha3/snt/cbd/ntt/pmul/comp)、
五个 sd_bram 存储库、ss_o 隐式拒绝 mux 与各 dbg 回读抽头。三色连线区分
数据通路 / BRAM 端口 / 共享仲裁。
2026-06-30 02:42:31 +08:00
717a9929b6 fix(rtl,scripts): replace combinational divider with Barrett multiplication, add synthesis include_dirs, set 50MHz clock
- Replace / and % operators in comp_decomp_sync with Barrett multiply-by-reciprocal
  (dividend * 5039 >> 24) + correction step. Eliminates ~100 CARRY4 divider chain.
- Add include_dirs for sources_1 fileset so Windows Vivado synthesis finds defines.vh.
- Change CLK_PERIOD from 10.0 (100MHz) to 40.0 (50MHz) to reflect actual target.
2026-06-30 00:23:43 +08:00
ebf1182b6d fix(scripts): add missing RTL files to create_project.tcl, switch default sim to hello_world
- Add sync_rtl/common/pipeline_reg.v and sync_rtl/comp_decomp/comp_decomp_sync.v
  (were missing vs xsim_run.tcl, would cause elaboration failure)
- Change default top module from tb_mlkem_kg_katK_xsim to tb_mlkem_hello_world_xsim
- Remove unused SIM_KP/SIM_CASE variables and pre-compile KAT copy hook
2026-06-29 23:24:03 +08:00
92dafc9696 fix(rtl): add use_dsp="no" attributes, fix duplicate wire declaration
Add (* use_dsp = "no" *) to all modules containing multiplication
operators to force LUT-based multiplication instead of DSP inference:
- barrett_mul.v (3 multipliers x 7 instances = 21 ops)
- comp_decomp_sync.v, sample_ntt_sync*.v, mlkem_top.v

Also fix duplicate wire declaration of ct_bytes_rt in mlkem_top.v.
2026-06-29 23:23:58 +08:00
ffe2e1e0c8 delete image.png 2026-06-29 23:11:10 +08:00
f22165d29e chore: gitignore + untrack AI agent tooling (.claude/.opencode/.trellis/AGENTS.md)
这些是本地 AI 助手 / 工作流工具产物,不应进版本库。加入 .gitignore 并
git rm --cached 取消跟踪(本地文件保留)。.omo/ 此前已忽略。

推送后远程仓库一并删除这些路径。
2026-06-29 23:07:54 +08:00
5ac7a59289 chore: 删除无用的 mod_add 模块
mod_add_sync 未被 mlkem_top 实例化(模加在各 leaf 算子内部完成),整个
sync_rtl/mod_add 目录(RTL + TB + 向量)已无用。同时:
- 更新 mlkem_top.v 顶部 leaf 模块清单注释(去掉 mod_add_sync,并修正为
  '共享 keccak_core' 的现状)
- run_tb.sh 用法示例改用 ntt 模块

KeyGen KAT 烟囱测试通过。
2026-06-29 22:57:46 +08:00
971ce97d50 chore: 删除无用测试文件与 ml-kem-r 痕迹
- 删除 top/TB 下已被 katK 参数化 TB 取代的增量开发 TB:
  tb_mlkem_kg_{2a,2c,2d,2e,2f,kat,katN}_xsim.v
- 删除依赖外部 ml-kem-r Rust 参考的向量生成 / 交叉校验脚本:
  gen_vectors.py / gen_encaps_vectors.py / gen_decaps_vectors.py /
  xcheck_mlkemr.py(KAT/golden 向量已提交,无需运行期依赖)
- 清理源码与文档注释中的 ml-kem-r / Rust 字样

保留的 5 个 TB(kg/enc/dec katK + hello_world + two_inst)即 run_tb.sh /
xsim_run.tcl 实际引用的全部。KeyGen KAT 与 hello_world 烟囱测试通过。
2026-06-29 22:44:58 +08:00
df4d5cd572 refactor: 把 run_hello.sh 合并进 run_tb.sh 的 'hello' 模块
新增 ./run_tb.sh hello [two],复用 'top' 的 RTL 编译列表 + hello_world/
two_inst 自检 TB,删除独立的 run_hello.sh。README 同步更新命令。
2026-06-29 22:34:36 +08:00
6b99e5abba docs: 重写 README,覆盖 KeyGen/Encaps/Decaps 全功能微架构与 mlkem_top I/O
原 README 仅介绍 KeyGen。重写后覆盖:
- 三种操作(op_i=0/1/2)与三种安全等级的运行时选择、尺寸/周期表
- 完整 mlkem_top 端口清单(控制握手、ss_o、ek/dk/c 流式输入、dbg 回读抽头)
- 微架构:叶子算子、共享 keccak、三个系数 BRAM 库 + 字节 BRAM、存储布局
- 顶层 FSM 三条数据通路(含 Decaps 复用 Encaps 重加密 + 隐式拒绝)
- 各操作的 FIPS 203 算法分解与对应状态
- 测试矩阵(KAT 分阶段 + hello_world 单/双实例端到端)与运行脚本
2026-06-29 22:26:01 +08:00
ee2bf1cda8 test(top): two-instance hello_world TB (genenc + dec split)
Add tb_mlkem_two_inst_xsim: runs the hello_world protocol across TWO mlkem_top
instances, modeling the real two-party split:
  * u_genenc: KeyGen THEN Encaps on one instance. KeyGen writes ek into its own
    ek_bram and Encaps reuses it directly (no re-streaming) -> shared_key, ct.
  * u_dec: Decaps on a separate instance, receiving dk + ct streamed over from
    u_genenc via the input ports.

Verifies A.shared_key == B.recovered_key and 'hello world' round-trips. Output
matches the single-instance TB and the Rust reference (key=ced0c031a4bee34a...).

run_hello.sh gains a 'two' arg to select this TB; default stays single-instance.
2026-06-29 22:15:39 +08:00
f27922270a test(top): hardware hello_world TB (full KeyGen+Encaps+Decaps protocol)
Mirror of ml-kem-r examples/hello_world.rs on the mlkem_top DUT (ML-KEM-512):
  1. Alice KeyGen(d=0x42.., z=0x77..)        -> ek (800B), dk (1632B)
  2. Bob   Encaps(ek, m=0xDE..)              -> shared_key, kem_ct (768B)
  3. Bob   XOR-encrypt "hello world"
  4. Alice Decaps(dk, kem_ct)                 -> recovered_key
  5. Alice XOR-decrypt -> "hello world"

The whole protocol runs on ONE DUT instance: ek/dk are read out of KeyGen via
the dbg taps and fed back into Encaps/Decaps through the streaming input ports,
just as the keys/ciphertext would cross the wire between Alice and Bob. Each
step prints its inputs and outputs.

Output is byte-identical to the Rust example: shared_key=ced0c031a4bee34a...,
encrypted=a6b5ac5dcb9e9425b9e3b8, decrypted="hello world", keys match.

run_hello.sh compiles the RTL + TB and runs it. Cycle counts (K=2): KeyGen
~22.9k, Encaps ~32.5k, Decaps ~50.8k.
2026-06-29 22:06:32 +08:00
2b70431923 feat(dec): Decaps D7 - implicit-reject compare + end-to-end KAT
Final FO step (FIPS 203 Alg 17 steps 9-11): compare c' to c and select the
shared secret with implicit rejection. Completes full ML-KEM Decaps.

- ST_DEC_CMP walks all ct bytes (no early-out: constant work, matching the
  constant-time spec intent), reading c' (ct_bram) and c (c_in_bram) in
  parallel, OR-ing every byte difference into cmp_neq; the final byte latches
  dec_reject.
- ss_o = dec_reject ? kbar_r (K-bar) : ss_r (K'). KeyGen/Encaps leave
  dec_reject=0 so ss_r passes through unchanged.
- ST_ENC_C2 terminal branches on op_r: Decaps -> ST_DEC_CMP, Encaps -> DONE.

The dec TB now runs end-to-end twice per case (dk loaded once):
  - valid ct  : ss_o == KAT ss   (c'==c -> K'),            dec_reject==0
  - corrupt ct: ss_o == KAT ss_n (c'!=c -> K-bar=J(z||ct_n)), dec_reject==1
exercising both the accept and implicit-reject paths against the KAT's own
ct_n/ss_n reject vectors.

Verified: dec D7 K=2/3/4 all cases PASS (accept + reject); KeyGen + Encaps
unregressed. Full ML-KEM (KeyGen + Encaps + Decaps w/ implicit rejection) now
works in hardware across all three parameter sets.
2026-06-29 21:56:07 +08:00
a734eb2cad feat(dec): Decaps D6 - c' = K-PKE.Encrypt(ek_pke, m', r')
Re-encryption step of the FO transform (FIPS 203 Alg 17 step 8), done by
reusing the ENTIRE Encaps E1-E7 pipeline rather than duplicating it:

- FSM: ST_DEC_J (D5) -> ST_ENC_LOAD, then the existing Encaps chain
  LOAD->A->C->N->U->C1->TDEC->E2MV->V->C2 runs unchanged and writes c' to
  ct_bram. The reuse preconditions are all in place: rho loads from ek_bram's
  ek_pke region (same 384k offset Encaps uses; populated at D0 load via
  dk_ld_ekpke), the CBD seed is r_r (r' from D5), and ek_pke is in ek_bram.
- D4 now packs the recovered message directly into m_r (dropping the separate
  mprime_r register): Encaps V's mu reads m_r[idx] and dbg_mprime_o now aliases
  m_r, so the re-encrypt sees m' with no extra plumbing.
- ST_ENC_LOAD arming generalized to fire when entered from ST_ENC_G (Encaps)
  or ST_DEC_J (Decaps re-encrypt).

The re-encrypt overwrites bank_a/bank_se/bank_t, so the bank-based stage checks
(D1 v', D2 s_hat/u_hat, D3 w) are no longer valid at end-of-run. The dec TB now
verifies the surviving register/BRAM artifacts: dk parse (D0), m' (D4, in m_r),
K'/r'/K-bar (D5), and the 768/1088/1568-byte c' against golden (D6). Earlier
stages remain proven by their per-stage builds and transitively by c'.

Verified: dec D6 K=2/3/4 all cases PASS (c' == golden == valid ciphertext c);
KeyGen + Encaps unregressed.
2026-06-29 21:19:38 +08:00
189411e8d1 feat(dec): Decaps D5 - (K',r')=G(m'||h) + K-bar=J(z||c)
FO transform hash derivations (FIPS 203 Alg 17 steps 6-7), reusing the
shared SHA3 core:

- ST_DEC_G: (K',r') = G(m'||h) via the single-block SHA3-512 path (mode 11,
  dec_g_data = {hek_r, mprime_r}; h was captured into hek_r at D0 load).
  K' -> ss_r (candidate shared secret, ss_o), r' -> r_r (PRF seed for D6).
- ST_DEC_J: K-bar = J(z||c) via the multi-block absorb port (mb_en=1),
  modeled on the H(ek) machine: assemble 136-byte blocks, byte source is
  z_r (g<32), c_in_bram (32<=g<msglen), or SHAKE256 pad (0x1F suffix, last
  byte |=0x80 -- the only difference from H's 0x06). mb_* inputs muxed
  between H and J by state. K-bar -> kbar_r (dbg_kbar_o).
- FSM: MENC -> G -> J -> DONE.

Bring-up note: c_in_bram read through cin_rd_addr_r (a register) plus the
registered BRAM is 2-cycle latency, but the assemble/writeback pipeline only
budgets 1 -- so the first c byte at the z->c boundary read X and poisoned the
whole sponge. Fixed by driving cin_rd_addr combinationally from dj_c_idx
during ST_DEC_J (dropping the register stage) so data lands the next cycle.

Verified: dec D5 K=2/3/4 all cases PASS (ct 768/1088/1568B -> 6/9/12 J blocks);
K' matches the KAT shared secret for valid ciphertexts; KeyGen + Encaps
unregressed.
2026-06-29 20:37:03 +08:00
7f519fe826 feat(dec): Decaps D3+D4 - w = v'-INTT(s.u_hat) + m' recovery
Completes K-PKE.Decrypt (FIPS 203 Alg 15) in hardware: m' is recovered.

D3 (ST_DEC_W) reuses the Encaps V MAC/INTT machine (u_row tied to 0):
- MAC s_hat[j] (bank_a slot j*K) o u_hat[j] (bank_se rel j) -> psum bank_t[UPSUM]
  -- identical addressing to Encaps V (t_hat[j] o y_hat[j]), so free reuse.
- INTT(psum) in place.
- SUB: w = v' - psum mod Q (negative -> +Q), written to bank_t[UPSUM].
  To read v' and psum in parallel during SUB (one read port per bank), D1's v'
  write was relocated from bank_t to bank_a slot DEC_VASLOT=1 (always free:
  s_hat occupies j*K, slot 1 is unused for K>=2). This mirrors V-ADD reading
  psum (bank_t) + e2 (bank_a) simultaneously.

D4 (ST_DEC_MENC): m' = byteEncode_1(Compress_1(w)). Compress_1(w)=1 iff
832 < w <= 2496 (Q=3329); bits packed LSB-first into mprime_r, exposed on
dbg_mprime_o (was a placeholder tied to m_r).

Added ST_DEC_W to the u_* machine muxes/sub-phases and the FSM chain
NTT->W->MENC->DONE. TB verify_d3 checks w (bank_t UPSUM); verify_d4 checks the
32-byte m' against golden (== the KAT-decrypted m == original message).

Verified: dec D1-D4 K=2/3/4 all cases PASS; KeyGen + Encaps unregressed.
2026-06-29 18:57:29 +08:00
940946f30c feat(dec): Decaps D2 - s_hat=byteDecode12(dk_pke) + u_hat=NTT(u')
K-PKE.Decrypt steps 2-3 (FIPS 203 Alg 15), both by reusing Encaps machines:

- ST_DEC_SDEC reuses the Encaps TDEC (byteDecode12) machine: only the byte
  source changes (td_byte mux -> dkp_rd_data; dkp_rd_addr driven by td_ekaddr
  in SDEC). Decodes dk_pke -> s_hat[j] into bank_a slot j*K, the same layout
  t_hat uses, so the D3 MAC can read s_hat[j] with the existing addressing.
- ST_DEC_NTT reuses the forward-NTT machine (n_slot_max=k_r) to transform
  u'[i] in place in bank_se rel slots 0..K-1 -> u_hat[i]. Added ST_DEC_NTT to
  the bank_se read/write muxes and the NTT load/process/arm blocks alongside
  ST_N/ST_ENC_N.
- FSM: DECOMP -> SDEC -> NTT -> DONE.

TB verify_d2 checks s_hat[i] (bank_a slot i*K) and u_hat[i] (bank_se rel i)
against golden. verify_d1 narrowed to v' only: D2's in-place NTT overwrites u'
in bank_se, so u' correctness is now proven transitively via u_hat==NTT(u').

Verified: dec D2 K=2/3/4 all cases PASS; KeyGen + Encaps unregressed.
2026-06-29 18:00:44 +08:00
ecc00d6dd5 feat(dec): Decaps D1 - byteDecode_d + Decompress -> u'/v'
K-PKE.Decrypt step 1 (FIPS 203 Alg 15): decode+decompress the ciphertext.

- comp_decomp_sync instance made mode-selectable: Encaps C1/C2 still compress
  (mode 0), Decaps ST_DEC_DECOMP decompresses (mode 1) with d=du/dv.
- New ST_DEC_DECOMP state with an inline byteDecode_d walker (reverse of the
  C1/C2 bit-packer): walks c_in_bram bytes, accumulates LSB-first into a bit
  buffer, extracts d-bit symbols, feeds comp_decomp, writes each decompressed
  coeff (mod q) to a bank.
    c1 = K polys, d=du -> u'[i] in bank_se rel slot i (0..K-1)
    c2 = 1 poly,  d=dv -> v'   in bank_t rel slot DEC_VSLOT=2 (avoids UPSUM=1)
- dbg_slot_i widened 4->6 bits so the TB can read v' (abs slot 26 at K=4).
- bse/bt write muxes gain the DECOMP writeback paths (dec_u_we / dec_v_we).

Verification: examples/dump_decaps.rs (ml-kem-r worktree) emits per-stage
golden (u'/v'/s_hat/u_hat/w/m') into vectors/decgold/. TB verify_d1 reads back
u'[i] and v' and compares all 256 coeffs each.

Bring-up note: dbg coeff readback latency is bank(1)+dbg_coeff_r(1); the TB's
rdcoeff initially waited 2 cyc and saw data shifted by one index -> fixed to 3.

Verified: dec D1 K=2/3/4 all cases PASS; KeyGen + Encaps unregressed.
2026-06-29 17:32:03 +08:00
e46d2258d9 chore(tb): remove Verilator TBs + framework; parallelize XSIM runs
Verilator is no longer used (all verification is via Vivado XSIM). Remove:
- 10 per-module tb_*.cpp Verilator testbenches
- the entire test_framework/ Verilator harness (lib/, run_all.py, config.json,
  per-module test_plan.json/gen_vectors.py, golden vectors, reports)
- stale specs: verilator-conventions.md, test_framework/structure.md
  (index.md updated to drop the Verilator entry)

Parallelize run_tb.sh K x case execution (modules stay serial):
- new run_xsim_jobs helper: compile+elaborate once (serial, populates the
  shared xsim.dir), then run each (K,case) xsim in its own private workdir
  with a COPY of xsim.dir (~1MB) so concurrent same-snapshot runs don't clobber
  each other's runtime logs. Each workdir symlinks the repo sync_rtl tree so
  the TB's repo-relative $readmemh vector paths resolve.
- top/enc/dec runners refactored to build a (snapshot:K:case) spec list and
  hand it to run_xsim_jobs; ordered PASS/FAIL summary + per-job /tmp logs
  preserved. Bare './run_tb.sh top' now also takes the parallel path.

Speedup (20 cores): top full sweep 2:11 -> 0:51 (~2.6x), ~320% CPU.
Verified: top (11) / enc (9) / dec (9) all PASS; missing-vector runs still
fail (file-not-found guard -> exit 1).
2026-06-29 16:05:06 +08:00
030931d4e5 feat(dec): Decaps D0 - op_i widen + dk/c load + parse
Scaffolding for ML-KEM Decaps (FIPS 203 Alg 18):
- op_i widened to 2-bit: 00=KeyGen, 01=Encaps, 10=Decaps (op_r too).
- New ST_DEC_LOAD state (D0: settles to DONE so load/parse is dbg-checkable).
- dk (=sk) streamed via dk_in_*; load logic routes each byte by region:
  [0,384K)->dk_pke (dkp_bram), [384K,768K+32)->ek_pke (ek_bram),
  [768K+32,+32)->H(ek) (hek_r), [768K+64,+32)->z (z_r). Routing uses the
  LIVE k_i input, not start-captured k_r (dk is streamed before start_i).
- c (=ct) streamed via c_in_* into a SEPARATE c_in_bram, so the computed c'
  (ct_bram) can later be compared against original c and J(z||c) can read c.
- New dbg taps: dbg_mprime_o/dbg_kbar_o/dbg_decz_o/dbg_dech_o.

TB: tb_mlkem_dec_katK_xsim verifies dk parse (H(ek), z, ek_pke/dk_pke BRAM
round-trip). gen_decaps_vectors.py emits dec_k{K}_c{N}_{dk,ct,ss,ctn,ssn}.hex
from the NIST KAT. run_tb.sh gains a 'dec' module (mirrors 'enc').

Regression fix: old KeyGen/Encaps TBs didn't connect the new input ports,
floating them to X and corrupting the ek/dkp write muxes -> tied off
dk_in_*/c_in_*/new dbg taps in both.

Verified: dec D0 K=2/3/4 PASS; KeyGen K=2 + Encaps K=2 unregressed.
2026-06-29 15:22:34 +08:00
4091fd0676 chore(enc): merge run_enc.sh into run_tb.sh; TB dumps hardware ct
run_tb.sh gains an 'enc' module that shares the 'top' KeyGen tcl compile
list (same RTL datapath) and swaps in tb_mlkem_enc_katK_xsim. Usage mirrors
'top': ./run_tb.sh enc [K] [CASE]; no args -> full sweep K=2/3/4 cases 0..2.
--list shows enc; per-case summary lines parse PASS (E7). run_enc.sh deleted.

tb_mlkem_enc_katK_xsim verify_e7 now dumps the hardware-produced ct (read
from ct_bram via the dbg_ct tap) on one line (byte 0 first), same format as
ml-kem-r's encaps_io example, so ss/ct can be eyeballed and diffed. On
mismatch it re-scans to print the first 8 differing byte positions.

Verified: ./run_tb.sh enc -> 9/9 PASS (E7) (ct==KAT.ct && ss==KAT.ss);
./run_tb.sh top 3 0 KeyGen unregressed.
2026-06-29 12:32:29 +08:00
7228bebb78 feat(enc): Encaps E7 - c2 = byteEncode_dv(Compress_dv(v)) + end-to-end KAT
Reuses the E5 bit-packer FSM for ST_ENC_C2, generalized over region:
- coeff source: cp_coeff_src = bank_t[UPSUM] (v) for C2, bank_se (u) for C1
- bit width: cp_d already = dv_rt for C2 else du_rt
- poly count: cp_poly_max = 1 (single v) for C2, K for C1
- ct write pointer cp_wa CONTINUES from c1_bytes into C2 (not reset), so c2
  lands right after c1. c1 ends on a whole-byte/poly boundary (256*du/8
  integral), so cp_buf/nbits are empty at the C1->C2 handoff.

FSM tail: ST_ENC_V -> ST_ENC_C2 -> ST_DONE.

TB: verify_e7 compares the full ct (c1||c2, CTB bytes) to KAT.ct byte-exact
via the dbg_ct tap. Combined with the E0 ss==KAT.ss check this is the full
end-to-end Encaps KAT (ct==KAT.ct && ss==KAT.ss).

Verified end-to-end for K=2/3/4, cases 0-2 (K2) / 0-1 (K3,K4):
ct==KAT.ct && ss==KAT.ss. ML-KEM Encaps complete. KeyGen unregressed.
2026-06-29 11:18:58 +08:00
e114bec5ee feat(enc): Encaps E6 - v = INTT(sum t_hat o y_hat) + e2 + mu
Storage choreography (per plan risk notes):
- TDEC now decodes t_hat[j] into bank_a slot j*K (was bank_t). This makes
  V's MAC reuse E4's u_aslot=u_j*K+u_row addressing with u_row=0, no mux
  change. bank_t has no room for K=4 (t_hat would fill all 4 slots vs
  psum's UPSUM slot), hence bank_a (16 slots, A_hat dead after E4).
- New ST_ENC_E2MV state relocates e2 (bank_t[0]) -> bank_a[1] so V-ADD
  reads psum (bank_t) and e2 (bank_a) from different banks (no port
  conflict). bank_se (y_hat + u) stays intact -> verify_e2/e3/e4 unaffected.
- V reuses the u_* MAC/INTT/ADD machine with u_row tied to 0 (u_row_max=1).
  ADD computes psum + e2 + mu mod Q -> bank_t[UPSUM] in place;
  mu[w] = m_r[w] ? 1665 : 0 (Decompress_1). FSM: C1->TDEC->E2MV->V->DONE.

Bug found+fixed during bring-up: e2 relocation was off-by-one (wrote
e2[i+1] into slot i) because em_we/em_widx were registered an extra cycle
past the bram read. Fixed: em_widx==em_ridx, write scheduled for the cycle
bt_rd_data presents e2[em_ridx].

TB: verify_e6 compares v (bank_t dbg slot 9, K=2) to ml-kem-r golden.
verify_e1 dropped (TDEC overwrites bank_a A_hat slots; A_hat transitively
verified by E4). Verified: K=2 E2/E3/E4/E6 == golden, E5 c1 == KAT prefix;
K=3/4 E0+E5 pass; KeyGen K=2 unregressed.
2026-06-29 11:03:33 +08:00
4fee8bded3 fix(enc): compile comp_decomp_sync + pipeline_reg in KeyGen tcl
mlkem_top now instantiates comp_decomp_sync (E5), so the shared
xsim_run.tcl must compile it (+ its pipeline_reg dep) before mlkem_top.
This unbreaks 'run_tb.sh top' which failed elaboration with
'Module comp_decomp_sync not found'. run_enc.sh simplified to reuse the
tcl's now-complete compile list (drops its duplicate leaf block).
2026-06-29 03:06:11 +08:00
3bc46f9640 feat(enc): Encaps E5 - c1 = byteEncode_du(Compress_du(u))
ST_ENC_C1: per-coeff Compress_du via comp_decomp_sync (mode 0) then
LSB-first byte packing into ct_bram. 5-phase micro-seq reads u[cp_poly]
from bank_se (rel K+poly), feeds the compressor (1-cyc pipe), appends du
bits to cp_buf, and drains whole bytes. Each poly = 256*du bits (whole
bytes) so the bit buffer empties at every poly boundary.

ST_ENC_U now advances to ST_ENC_C1 (was ST_DONE).

TB: verify_e5 compares ct_bram[0..c1_bytes-1] to the KAT.ct prefix via
the dbg_ct tap. run_enc.sh: encaps TB runner (compiles comp_decomp_sync
which the KeyGen tcl omits).

Verified K=2/3/4 c1 == KAT.ct prefix (640/960/1408 B; K=4 du=11
cross-byte path), K=2 cases 0-2.
2026-06-29 02:59:12 +08:00
ee875d2ff7 feat(enc): Encaps E4 - u = INTT(sum A^T o y_hat) + e1
ST_ENC_U computes u[i] per row in 3 sub-phases reusing shared u_pmul + u_ntt:
  sub0 MAC : sum_j A_hat[j][i] o y_hat[j] (TRANSPOSE: slot=j*K+i) -> NTT-domain
             psum in bank_t rel slot UPSUM=1 (e2 in slot 0), init 0 at j==0
  sub1 INTT: INTT(psum) mode=1 (built-in x3303) in place in bank_t[UPSUM]
  sub2 ADD : u[i][w] = psum[w] + e1[i][w] mod Q -> bank_se rel (K+i), over e1
y_hat (bank_se 0..K-1) preserved for V. ntt_core mode + input muxed for the
INTT sub-phase; bank_a/se/t read+write ports extended for all 3 sub-phases.

Fixed a duplicate 'assign bse_we' (stale + new both present -> ADD writes
X-dropped); collapsed to one. Verified (K=2 c0) u[0..1] == ml-kem-r golden
(transpose + INTT + e1 all correct); E0/E1/E3 pass, E2 trimmed to e2 (e1
consumed into u, transitively checked by E4); K=3/4 no timeout.
2026-06-29 02:26:01 +08:00
8ed4d59546 feat(enc): Encaps E3 - y_hat = NTT(y) in place
ST_ENC_N reuses the ST_N forward-NTT datapath (mode=0, read-ahead load,
in-place writeback to bank_se). Slot count parameterized: KeyGen 2K (s,e),
Encaps K (y only; e1/e2 stay time-domain) via n_slot_max. bse rd/wr muxes
and the C->N arming extended for ST_ENC_N.

Verified (K=2 c0) y_hat[0..1] == ml-kem-r golden; E0/E1/E2 still pass
(E2 y check dropped since NTT overwrites y in place -> covered by E3);
K=3/4 run through E3 without timeout.
2026-06-29 02:01:37 +08:00
cdc5ce25b1 feat(enc): Encaps E2 - sample y/e1/e2 (CBD eta1/eta2, r seed)
ST_ENC_C reuses the ST_C CBD datapath with Encaps muxes: seed=r (not sigma),
eta=eta1 for y[0..K-1] then eta2 for e1[0..K-1]/e2, nonce 0..2K. 2K+1 polys
(vs KeyGen 2K). y/e1 -> bank_se rel slots 0..2K-1; e2 -> bank_t rel slot 0
(free during C/N/U since TDEC is deferred to V-prep so the 28-slot banks hold
all of A+y_hat+e1+e2 at K=4 without resizing).

Bring-up golden via ml-kem-r dump_encaps_full (working-tree example):
vectors/encgold/ec_k2_c0_{y,e1,e2}.hex. Verified (K=2 c0) y[0..1],e1[0..1],e2
== ml-kem-r; A_hat (E1) and ss (E0) still pass; K=3/4 no timeout.
2026-06-29 01:55:47 +08:00
31c967c8a4 feat(enc): Encaps E1 - rho load + A regen + byteDecode12 t_hat
ST_ENC_LOAD: stream rho (32B) from ek_bram[384k..] into rho_r (read-ahead,
1-cyc bram latency). ST_ENC_A: regenerate A_hat via SampleNTT into bank_a
(reuses ST_A datapath, gated on st==ST_A||ST_ENC_A). ST_ENC_TDEC:
byteDecode12 ek[i*384..] -> t_hat[i] into bank_t (5-cycle micro-phase per
3-byte/2-coeff triple; bt write port muxed with ST_M).

Verified (K=2 c0) A_hat (1024 coeffs) + t_hat (512) == KeyGen golden via
dbg_coeff_o; E0 ss==KAT.ss still passes all K/cases (no timeout).
2026-06-29 01:44:50 +08:00
0a8b3dae69 feat(enc): Encaps E0 - op_i/msg_i/ek-load scaffold + H(ek)+G(m||H(ek))
Extend mlkem_top with a runtime op_i select (0=KeyGen, 1=Encaps) and the
first Encaps stages, reusing the shared keccak_core and the ST_H multi-block
SHA3-256 machinery:
  ST_ENC_H: H(ek) over preloaded ek_bram (same FSM as KeyGen ST_H)
  ST_ENC_G: (K,r) = G(m||H(ek)) via new 64-byte single-block SHA3-512

- sha3_top_shared: add mode=2'b11 = SHA3-512 over a full 512-bit message
  (g512_pad). Standalone tb_sha3_g512 confirms it byte-exact.
- mlkem_top: new ports op_i, msg_i, ek_in_{we,addr,byte} (ek preload), ss_o,
  dbg_ct_*, dbg_r_o/dbg_hek_o. st widened 4->5 bits; ST_ENC_* states added.
  Renamed message port to msg_i to avoid collision with ST_M counter m_i.
- TB tb_mlkem_enc_katK + gen_encaps_vectors.py (per-byte ek/m/ct/ss vectors).

Verified ss==KAT.ss for K=2/3/4, cases 0-2 (all PASS). KeyGen unaffected
(K=2 c0 still ek==pk, dk==sk byte-exact).
2026-06-29 01:00:47 +08:00
c4669480d1 test(top): add ml-kem-r cross-validation script (xcheck_mlkemr.py)
Cross-checks the hardware KAT vectors against the ml-kem-r Rust reference's
NIST .rsp files, accounting for the d/z byte-order convention: the hardware
hashes G(reverse(d)||K), so its vector files store d/z reversed vs the NIST
.rsp. The script applies that reversal for d/z and compares ek==pk, dk==sk
directly; 11/11 cases consistent.

ml-kem-r path via $ML_KEM_R (default ~/Dev/ml-kem-r); friendly errors on
missing files. Documents the convention that tripped up logging.rs (which
pasted hardware-order d into the literal-order Rust reference).
2026-06-28 23:54:08 +08:00
af9ecb20b7 test(top): dump KeyGen d/z inputs and ek/dk outputs per case
The KAT TB now prints, for each run: the d and z seeds (32-byte hex, MSB-first
== vector file order) before start, and the full ek/dk byte strings read back
from the DUT (32 bytes/line, offset-prefixed) after the byte-compare. Inputs
verified to match the kat_*_d/z.hex files exactly; dk[768..] echoes ek[0..] as
expected (dk_pke || ek || H(ek) || z). Pass/fail logic unchanged.
2026-06-28 22:08:50 +08:00
a38c41a1f5 refactor(kg): bank_se -> sd_bram instance; Phase 2 complete (polymem all BRAM)
Final bank promoted to sd_bram (the busiest: 5 read sites, 2 write sites).
Read port phase-muxed: ST_N load / ST_M load (pm_b s_hat[j]) vs accumulate
(e_hat, selected by m_loading) / ST_E dk-half / dbg. Write port combinational:
ST_C CBD store vs ST_N NTT writeback (disjoint states). All explicit consumer
read registers (n_rd_data, pm_b_rd, m_eacc_rd, e_se_rd) collapsed into the
sd_bram internal read register; m_acc_src and e_rd_coeff now select between
two registered sd_bram outputs (same 1-cycle latency).

mlkem_top now contains ZERO behavioural RAM arrays: all coefficient storage is
3 sd_bram banks (a/se/t) + ek/dkp byte buffers = 5 sd_bram instances total,
each inferring BRAM (ASIC: compiled SRAM). 11/11 KAT PASS, byte-exact.
2026-06-28 22:00:41 +08:00
5cfe8c74ca refactor(kg): bank_t -> sd_bram instance (1R+1W RMW, real BRAM)
Second bank promoted to sd_bram. Read port muxed by phase (ST_M acc t_hat
addr / ST_E ek-half byteEncode addr / dbg). The shared mux-registers that fed
both bank_se and bank_t were split: the bank_se half stays a manual reg
(m_eacc_rd / e_se_rd), the bank_t half uses sd_bram's internal read register
(bt_rd_data) -- same 1-cycle latency, so the j-select (m_jq) and ST_E coeff
select (e_rd_coeff) just pick between the two registered outputs. RMW safe:
acc read addr (m_oidx+1) leads write addr (m_oidx), no same-cycle alias.
Write port combinational. 11/11 KAT PASS incl. K=3/4 deep accumulate.
2026-06-28 21:41:51 +08:00
d1b409f65f refactor(kg): bank_a -> sd_bram instance (1R+1W, real BRAM)
First of 3 banks promoted from async reg array to an sd_bram. Read port
muxed by phase (ST_M load drives pm_a_full, else dbg index); sd_bram's
internal rd_addr_r replaces the explicit pm_a_rd reg (identical 1-cyc
latency). Write port driven combinationally so the posedge write matches the
old nonblocking reg-array timing. 11/11 KAT PASS, byte-exact.
2026-06-28 21:26:35 +08:00
4afa3a4998 refactor(kg): registered single-port read for ST_E byteEncode (bank_se/bank_t)
Final per-consumer step of stage 2b. ST_E read two coeffs of a pair from one
bank combinationally; now serialized through a single registered read port
(e_rd_data_r) over a 4-cycle micro-phase e_ph: ph0 fetch c0, ph1 write b0 +
fetch c1, ph2 write b1, ph3 write b2. Matches sd_bram timing; +512 cyc on K=2
(one extra fetch per pair-stream). All polymem read consumers (ST_N, ST_M
load, ST_M acc, ST_E) are now registered-read. 11/11 KAT PASS, byte-exact.
2026-06-28 21:13:39 +08:00
0fa7f48ed4 build(vivado): add sd_bram to create_project.tcl, fix run instructions
mlkem_top instantiates sd_bram (ek/dk byte buffers) but create_project.tcl
was missing it -- elaboration would fail. Add sync_rtl/storage/sd_bram.v to
match xsim_run.tcl exactly. Also clarify the run flow: runtime=all means
launch_simulation runs to the TB $finish; a manual 'run all' afterwards trips
the 120ms watchdog and prints a spurious timeout.

Verified: vivado -mode batch builds the project (0 errors) and the simulation
prints 'K=2 CASE 0 PASS: ek==pk, dk==sk'.
2026-06-28 17:14:14 +08:00
f4493966ac test(top): stream raw xsim output to terminal (tee to log)
The fast top runner was hiding xsim stdout in /tmp and only echoing PASS/FAIL,
which buried the TB $display diagnostics needed when a case fails. Now xsim
output is tee'd to both terminal and log, with the PASS/FAIL summary appended.
2026-06-28 16:52:27 +08:00
74d8f021c9 test(top): add fast single-K / single-case runner to run_tb.sh
./run_tb.sh top [K] [CASE] now elaborates only the requested K and runs only
the requested CASE(s), parsing PASS/FAIL + file-not-found per run. Avoids the
full 3-snapshot, 11-case sweep when iterating. 'top' with no extra args and
all other modules keep their original full-tcl behaviour.
2026-06-28 16:46:33 +08:00
75c350c1e4 refactor(kg): registered read-ahead for ST_M accumulate (bank_se/bank_t)
Third per-consumer step of stage 2b, the read-modify-write one. m_acc_src is
now a registered read (m_acc_rd) whose address leads by pm_vo (m_oidx+1 when
consuming) so the value lands exactly on the pm_vo cycle. Same-index RMW on
bank_t is read-old: the running-t_hat source was written by the previous term
and is long settled; read addr (m_oidx+1) leads write addr (m_oidx).
11/11 KAT PASS incl. K=3/4 j>0 paths, byte-exact.
2026-06-28 16:37:07 +08:00
9c08273c5f refactor(kg): registered read-ahead for ST_M load (bank_a + bank_se)
Second per-consumer step of stage 2b. m_ld becomes a read-ahead pointer; both
poly_mul load reads (bank_a A_hat, bank_se s_hat) are registered into
pm_a_rd/pm_b_rd and fed one cycle later (pm_valid delayed 1 cyc), same pattern
as ST_N. 11/11 KAT PASS, byte-exact.
2026-06-28 16:27:00 +08:00
45e07c28e8 refactor(kg): registered read-ahead for ST_N (bank_se NTT load)
First per-consumer step of stage 2b. n_ridx becomes a read-ahead pointer
leading the consume index by 1; bank_se read is registered into n_rd_data and
fed to ntt_core one cycle later (n_valid delayed 1 cyc). Cores hold ready_o
high through LOAD so a fixed 1-cycle skew suffices. Matches sd_bram registered
timing for this read port. 11/11 KAT PASS, byte-exact.
2026-06-28 16:17:30 +08:00
4d3adc6b57 refactor(kg): split polymem into 3 banks {a, se, t} (async, stage 2a)
Replace the single async-read polymem[0:28*256-1] with 3 polynomial-indexed
banks (bank_a A_hat / bank_se s_hat||e_hat / bank_t t_hat), addressed by
abs_slot - base_slot. Still async-read here -- a pure refactor that validates
bank sizing and base-relative addressing with zero timing change before
stage 2b converts them to registered sd_bram + read-ahead pipelines.

11/11 KAT PASS, byte-exact, 0 file-not-found.
2026-06-28 15:55:26 +08:00
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
5a7d5d6a47 refactor(kg): move ek/dk_pke byte storage into BRAM (sd_bram)
Phase 1 of migrating mlkem_top's large arrays to inferable RAM. ek_mem
and dkp_mem reg arrays are replaced by two sd_bram instances (1R/1W,
registered read). Datapath changes to fit single-port-per-cycle BRAM:

  - ST_E writes 1 byte/cycle (was 3): added e_byte sub-counter; ST_E
    length ~3x (K=2 KeyGen 21403->22433 cyc, ~5%).
  - ST_H ek read is now registered: assemble phase presents the address
    one cycle ahead and writes back the byte that arrived (h_wb_* pipe),
    h_byte runs 0..136 to flush the final byte. Pad bytes via h_padconst.
  - dbg_byte_o/dbg_dk_o read combinationally off the BRAM registered
    output (net 1-cycle latency, within the TB's 2-cycle read wait);
    region decode for dk readback unchanged.

Add sd_bram.v to the top TB compile list. Verified byte-exact vs NIST
KAT: K=2 c0-4, K=3 c0-2, K=4 c0-2 -> 11/11 PASS, 0 file-not-found.
2026-06-28 14:49:05 +08:00
8774e03a0e build(vivado): rewrite create_project.tcl for current KeyGen flow
The old script referenced 5 non-existent files (keccak_arbiter,
sha3_chain_top_shared, tb_mlkem_top_xsim, tb_kg/en/de) and stale
vectors, so read_verilog/elaborate failed outright.

Rewrite to mirror the verified XSIM flow (sync_rtl/top/TB/xsim_run.tcl):
  - load exactly the 14 sources mlkem_top depends on
  - sim top = tb_mlkem_kg_katK_xsim, runtime K via generic KP, case via
    -testplusarg CASE
  - copy KAT vectors into the xsim working dir via xsim.compile.tcl.pre
    (the only hook in 2019.2 that runs before $readmemh; an appended
    -tclbatch runs after Vivado's own 'run all', too late)
  - drop duplicate --relax (XSim adds it; passing again is an error)

Verified through the actual Vivado batch project flow:
  K=2 CASE 0 -> PASS (21403 cyc), K=4 CASE 2 -> PASS (54059 cyc),
  0 file-not-found warnings. gitignore the generated vivado_prj/.

Also rewrite README.md in Chinese documenting the mlkem_top workflow
and test flow.
2026-06-28 03:43:56 +08:00
3a53993754 refactor(kg): make ML-KEM K a runtime input k_i instead of a parameter
mlkem_top now sizes storage for KMAX=4 (worst case) and selects the
active ML-KEM parameter set at start_i via the k_i input. All K-derived
quantities (eta1, slot bases, ek/dk byte counts, H(ek) block count, FSM
bounds) are computed at runtime from the captured k_r.

Verified byte-exact against NIST KAT for all three parameter sets:
  K=2 (512)  cases 0-4, K=3 (768) cases 0-2, K=4 (1024) cases 0-2
  -> 11/11 PASS (ek==pk, dk==sk).
2026-06-28 03:24:58 +08:00