fix(sample_ntt): suppress spurious 257th valid_o after last_o

Phase-1 (d1 output) lacked the 'need_more' guard that phase-2 (d2) had, so
when the 256th accepted coefficient was a d1 whose group then advanced, the
FSM could fire one extra valid_o after last_o for certain seeds (e.g. KAT
count=0 rho, seed i=0/j=1 emitted 257 pulses). In mlkem_top KeyGen this
trailing pulse leaked into the next poly's index 0, shifting the stream.

Fix: gate phase-1 d1 output with 'd1_acc_r && need_more' (mirrors phase-2).
Applied to both sample_ntt_sync and sample_ntt_sync_shared.

Standalone TB had a coverage blind spot (stopped reading at 256, never
checked valid_o stayed low after last_o). Added a regression assertion:
counts spurious post-last_o pulses and fails if any. Verified the assertion
catches the bug on a reverted-fix copy (3 spurious) and passes on the fix.

Verified:
- 40-seed audit (sync) + 24-seed audit (shared): all exactly 256 pulses,
  last_o@256, zero post-last pulses.
- Verilator vs hashlib oracle: 1536/1536 (no real coeff dropped).
- Full framework regression: 4334/4334.
- mlkem_top KeyGen Stage 2c: 2048/2048 A_hat+s+e coeffs exact.
This commit is contained in:
2026-06-28 01:35:35 +08:00
parent 106b2925a8
commit 6db3c7cc5e
3 changed files with 20 additions and 6 deletions

View File

@@ -208,8 +208,22 @@ module tb_sample_ntt_xsim;
// Wait for DUT to return to IDLE before next vector.
// The DUT may still be processing its last Keccak permutation
// (ST_WAIT ST_DONE ST_IDLE), which takes ~20+ cycles.
while (!ready_o) begin
@(posedge clk);
// ASSERTION: after the 256th coeff (last_o), valid_o must stay
// low until IDLE any extra pulse is a spurious 257th output
// (regression guard for the phase-1 need_more fix).
begin
integer extra_pulses;
extra_pulses = 0;
while (!ready_o) begin
@(posedge clk);
if (valid_o) extra_pulses = extra_pulses + 1;
end
if (extra_pulses != 0) begin
$display("ERROR: Vector %0d emitted %0d spurious valid_o pulse(s) after last_o",
idx, extra_pulses);
fail_count = fail_count + 1;
if (pass_count > 0) pass_count = pass_count - 1; // revoke the earlier PASS
end
end
end // inner begin block
end

View File

@@ -334,7 +334,7 @@ module sample_ntt_sync #(parameter K = 4) (
// ----- Phase 1: output d1 -----
2'd1: begin
if (d1_acc_r) begin
if (d1_acc_r && need_more) begin
if (!valid_o_r) begin
// First cycle: assert output
coeff_o_r <= d1_r;
@@ -349,7 +349,7 @@ module sample_ntt_sync #(parameter K = 4) (
end
end
end else begin
// d1 rejected, skip to phase 2
// d1 rejected or no longer needed: skip to phase 2
valid_o_r <= 1'b0;
sq_phase_r <= 2'd2;
end

View File

@@ -348,7 +348,7 @@ module sample_ntt_sync_shared #(parameter K = 4) (
// ----- Phase 1: output d1 -----
2'd1: begin
if (d1_acc_r) begin
if (d1_acc_r && need_more) begin
if (!valid_o_r) begin
// First cycle: assert output
coeff_o_r <= d1_r;
@@ -363,7 +363,7 @@ module sample_ntt_sync_shared #(parameter K = 4) (
end
end
end else begin
// d1 rejected, skip to phase 2
// d1 rejected or no longer needed: skip to phase 2
valid_o_r <= 1'b0;
sq_phase_r <= 2'd2;
end