feat(ntt): implement synchronous NTT core with Barrett modular reduction

Phase 2.1: Merged Path00+Path01 NTT engine.
- barrett_mul.v: Barrett modular multiplication (a·b mod 3329)
- butterfly_unit.v: Cooley-Tukey/Gentleman-Sande butterfly
- zeta_rom.v: 128-entry ROM with bit-reversed roots of unity
- ntt_core.v: 7-layer NTT FSM, 256×12-bit register file
- ntt_sync.v: valid/ready streaming wrapper

Verified: 13/13 vectors bit-exact vs Python NTT/NTTInverse
This commit is contained in:
2026-06-24 22:51:14 +08:00
parent 5941fee980
commit c4cd10c2c1
8 changed files with 765 additions and 0 deletions

195
sync_rtl/ntt/TB/tb_ntt.cpp Normal file
View File

@@ -0,0 +1,195 @@
// tb_ntt.cpp - Verilator C++ testbench for ntt_sync
//
// Reads test vectors from +VECTOR_FILE= plusarg.
// Format: "MODE COEFF0 COEFF1 ... COEFF255"
// MODE: "F" = forward NTT, "I" = inverse NTT
// COEFFx: 3-digit hex (12-bit, 000..CFF)
//
// Drives DUT with coefficients one per cycle, waits for output,
// prints "RESULT: COEFF0 COEFF1 ... COEFF255\n" to stdout.
//
// Clock: 10ns period. Reset: 2 cycles.
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include "Vntt_sync.h"
#include "verilated.h"
#define CLK_PERIOD_NS 10.0
#define TIMEOUT_CYCLES 500000
static vluint64_t main_time = 0;
double sc_time_stamp() {
return main_time;
}
// Toggle clock: both edges + eval (one full cycle)
static void posedge(Vntt_sync* dut) {
dut->clk = 1;
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
dut->eval();
dut->clk = 0;
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
dut->eval();
}
static int hex_char_to_nibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return 0;
}
// Parse 3-char hex token to 12-bit value.
static uint16_t hex3_to_val(const std::string& tok) {
uint16_t val = 0;
for (size_t i = 0; i < tok.length() && i < 3; i++) {
val = (val << 4) | hex_char_to_nibble(tok[i]);
}
return val & 0xFFF;
}
// Format 12-bit value as 3-char hex (lowercase for consistency).
static std::string val_to_hex3(uint16_t val) {
char buf[4];
snprintf(buf, sizeof(buf), "%03X", val & 0xFFF);
return std::string(buf);
}
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
// Parse +VECTOR_FILE= plusarg
const char* vector_file = NULL;
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg.rfind("+VECTOR_FILE=", 0) == 0) {
vector_file = argv[i] + 13;
}
}
if (!vector_file) {
std::cerr << "ERROR: +VECTOR_FILE= not specified" << std::endl;
return 1;
}
std::ifstream infile(vector_file);
if (!infile.is_open()) {
std::cerr << "ERROR: Cannot open vector file: " << vector_file << std::endl;
return 1;
}
// Instantiate DUT
Vntt_sync* dut = new Vntt_sync;
// Initialize
dut->clk = 0;
dut->rst_n = 0;
dut->mode = 0;
dut->coeff_in = 0;
dut->valid_i = 0;
dut->ready_i = 0;
// Reset: 2 full cycles
for (int i = 0; i < 2; i++) posedge(dut);
dut->rst_n = 1;
std::string line;
vluint64_t cycle = 0;
int vec_count = 0;
while (std::getline(infile, line)) {
if (line.empty() || line[0] == '#') continue;
// Parse: MODE COEFF0 COEFF1 ... COEFF255
std::istringstream iss(line);
std::string mode_str;
if (!(iss >> mode_str)) continue;
int mode_val = 0;
if (mode_str == "I") mode_val = 1;
// Parse 256 coefficients into vector
std::vector<uint16_t> input_coeffs(256);
std::string tok;
int coeff_idx = 0;
while (iss >> tok && coeff_idx < 256) {
input_coeffs[coeff_idx] = hex3_to_val(tok);
coeff_idx++;
}
if (coeff_idx != 256) {
std::cerr << "ERROR: Expected 256 coefficients, got " << coeff_idx
<< " (vec " << vec_count << ")" << std::endl;
continue;
}
// Set mode
dut->mode = mode_val;
// ---- Load 256 coefficients ----
while (!dut->ready_o) {
posedge(dut); cycle++;
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
}
for (int i = 0; i < 256; i++) {
dut->coeff_in = input_coeffs[i];
dut->valid_i = 1;
posedge(dut); cycle++;
dut->valid_i = 0;
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
}
// At this point, the DUT has captured all 256 coeffs and
// transitioned to S_COMPUTE_RD (ready_o went low).
// ---- Wait for valid_o (DUT computing) ----
dut->ready_i = 1;
while (!dut->valid_o) {
posedge(dut);
cycle++;
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
}
// ---- Read 256 output coefficients ----
printf("RESULT: ");
for (int i = 0; i < 256; i++) {
// Wait for valid_o to be asserted (data is valid NOW)
while (!dut->valid_o) {
posedge(dut);
cycle++;
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
}
// Capture coefficient BEFORE consuming posedge
uint16_t coeff_val = (uint16_t)(dut->coeff_out & 0xFFF);
printf("%s%s", val_to_hex3(coeff_val).c_str(),
(i < 255) ? " " : "");
// Consume this coefficient: posedge with ready_i=1
posedge(dut);
cycle++;
}
printf("\n");
vec_count++;
}
std::cout << "Processed " << vec_count << " vectors" << std::endl;
infile.close();
delete dut;
return (vec_count > 0) ? 0 : 1;
timeout_err:
std::cerr << "ERROR: Timeout at cycle " << cycle
<< " (vec " << vec_count << ")" << std::endl;
infile.close();
delete dut;
return 1;
}

View File

@@ -0,0 +1,53 @@
// barrett_mul.v - Barrett modular multiplication (a * b mod Q)
//
// Computes product = a * b mod Q using Barrett reduction.
// Q = 3329, k = floor(2^24 / Q) = 5039
//
// Pure combinational, single-cycle latency.
// All multiplication widths explicitly controlled to avoid Verilog truncation.
module barrett_mul (
input [11:0] a,
input [11:0] b,
output [11:0] product
);
localparam Q = 3329;
localparam K = 5039; // floor(2^24 / 3329)
localparam R = 24;
// Full product: a * b (both < 3329, product < 11,082,241 < 2^24)
// Force 24-bit evaluation by extending operand
wire [23:0] p;
assign p = {12'd0, a} * b;
// Extend p to 37 bits for multiplication with K
wire [36:0] p_ext;
assign p_ext = {13'd0, p};
// Compute t_shifted = (p * K) >> 24
// Use explicit wire for the product to control width
/* verilator lint_off UNUSEDSIGNAL */
wire [36:0] t_product;
/* verilator lint_on UNUSEDSIGNAL */
assign t_product = p_ext * K;
wire [12:0] t_shifted;
assign t_shifted = t_product[36:R];
// q_approx = t_shifted * Q
wire [24:0] q_approx;
assign q_approx = t_shifted * Q;
// r = p - q_approx
wire [24:0] r0;
assign r0 = {1'b0, p} - q_approx;
// Conditional subtract Q (at most twice)
wire [24:0] r1;
assign r1 = (r0 >= Q) ? (r0 - Q) : r0;
wire [11:0] r2;
assign r2 = (r1[11:0] >= Q) ? (r1[11:0] - Q) : r1[11:0];
assign product = (r1 >= Q) ? r2 : r1[11:0];
endmodule

View File

@@ -0,0 +1,67 @@
// butterfly_unit.v - Cooley-Tukey / Gentleman-Sande butterfly
//
// Computes one butterfly operation for NTT or inverse NTT.
//
// Parameters:
// Q = 3329 (prime modulus)
//
// Forward NTT (mode=0):
// t = zeta * b mod Q (via barrett_mul)
// a_out = (a + t) mod Q
// b_out = (a - t) mod Q
//
// Inverse NTT (mode=1):
// a_out = (a + b) mod Q
// diff = (b - a) mod Q (handled as: if b >= a: b-a; else: b-a+Q)
// b_out = zeta * diff mod Q (via barrett_mul)
//
// Pure combinational.
module butterfly_unit (
input [11:0] a,
input [11:0] b,
input [11:0] zeta,
input mode, // 0 = forward NTT, 1 = inverse NTT
output [11:0] a_out,
output [11:0] b_out
);
localparam Q = 3329;
// Barrett modular multiplication: zeta * mul_data mod Q
wire [11:0] mul_data; // what to multiply with zeta
wire [11:0] mul_result; // result of barrett_mul(zeta, mul_data)
// Forward: mul_data = b, t = zeta * b mod Q
// Inverse: mul_data = (b - a) mod Q positive
assign mul_data = (mode == 1'b0) ? b : ((b >= a) ? (b - a) : (b - a + Q));
barrett_mul u_barrett (
.a (zeta),
.b (mul_data),
.product (mul_result)
);
// ---- a_out computation ----
// Forward: a_out = (a + t) mod Q
// Inverse: a_out = (a + b) mod Q
wire [12:0] a_sum;
wire [11:0] add_val;
assign add_val = (mode == 1'b0) ? mul_result : b;
assign a_sum = {1'b0, a} + {1'b0, add_val};
// a_sum - Q produces 13-bit result; we only need lower 12 bits since
// a_sum >= Q guarantees the result < Q < 2^12
wire [11:0] a_sub_12;
assign a_sub_12 = a_sum[11:0] - Q[11:0];
wire [11:0] a_result = (a_sum >= Q) ? a_sub_12 : a_sum[11:0];
assign a_out = a_result;
// ---- b_out computation ----
// Forward: b_out = (a - t) mod Q if a >= t: a-t; else: a-t+Q
// Inverse: b_out = t (mul_result, which is zeta * (b-a) mod Q)
wire [11:0] sub_val;
assign sub_val = mul_result;
wire [11:0] sub_result;
assign sub_result = (a >= sub_val) ? (a - sub_val) : (a - sub_val + Q);
assign b_out = (mode == 1'b0) ? sub_result : mul_result;
endmodule

132
sync_rtl/ntt/ntt_core.v Normal file
View File

@@ -0,0 +1,132 @@
// ntt_core.v - NTT core with individual coefficient registers
//
// Uses 256 individual 12-bit registers and generate-based muxing
// to avoid any part-select simulation issues.
// 3-cycle butterfly: SetAddr -> Read -> Compute+Write
module ntt_core (
input clk, rst_n,
input [11:0] coeff_in,
input valid_i,
output ready_o,
input mode,
output [11:0] coeff_out,
output valid_o,
input ready_i,
output done_o
);
localparam N = 256, LAYERS = 7, DW = 12;
// Individual coefficient registers
reg [DW-1:0] cr [0:N-1];
integer ci;
// State machine
localparam S_IDLE=3'd0, S_LOAD=3'd1, S_CMP_A=3'd2, S_CMP_B=3'd3,
S_CMP_C=3'd4, S_OUTPUT=3'd5, S_DONE=3'd6;
reg [2:0] state, next_state;
reg [7:0] load_cnt, out_cnt;
reg [7:0] j, start, layer_len;
reg [6:0] zeta_idx;
reg [2:0] layer;
reg bf_done;
// Pipeline registers
reg [DW-1:0] r_a, r_b;
reg [7:0] r_wa, r_wb;
// Zeta
wire [DW-1:0] zeta;
zeta_rom u_z (.addr(zeta_idx), .zeta(zeta));
// Butterfly
wire [DW-1:0] bf_a_out, bf_b_out;
butterfly_unit u_bf (
.a(r_a), .b(r_b), .zeta(zeta), .mode(mode),
.a_out(bf_a_out), .b_out(bf_b_out));
// Output scaling
wire [DW-1:0] coeff_scaled;
barrett_mul u_scl (.a(cr[out_cnt]), .b(12'd3303), .product(coeff_scaled));
assign coeff_out = mode ? coeff_scaled : cr[out_cnt];
assign ready_o = (state == S_IDLE) || (state == S_LOAD);
assign valid_o = (state == S_OUTPUT);
assign done_o = (state == S_DONE);
always @* begin
next_state = state;
case (state)
S_IDLE: if (valid_i) next_state = S_LOAD;
S_LOAD: if (load_cnt >= 255 && valid_i) next_state = S_CMP_A;
S_CMP_A: if (bf_done) next_state = S_OUTPUT; else next_state = S_CMP_B;
S_CMP_B: if (bf_done) next_state = S_OUTPUT; else next_state = S_CMP_C;
S_CMP_C: if (bf_done) next_state = S_OUTPUT; else next_state = S_CMP_A;
S_OUTPUT:if (out_cnt >= 255 && ready_i) next_state = S_DONE;
S_DONE: next_state = S_IDLE;
default: next_state = S_IDLE;
endcase
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state<=S_IDLE; load_cnt<=0; out_cnt<=0; j<=0; start<=0; layer_len<=0;
zeta_idx<=0; layer<=0; bf_done<=0; r_a<=0; r_b<=0; r_wa<=0; r_wb<=0;
for (ci=0; ci<N; ci=ci+1) cr[ci] <= 0;
end else begin
state <= next_state;
if (state == S_IDLE && valid_i) begin
cr[0] <= coeff_in;
load_cnt<=1; out_cnt<=0; j<=0; start<=0; layer<=0; bf_done<=0;
if (!mode) begin layer_len<=128; zeta_idx<=1; end
else begin layer_len<=2; zeta_idx<=127; end
end
if (state == S_LOAD && valid_i) begin
cr[load_cnt] <= coeff_in;
load_cnt <= load_cnt + 8'd1;
end
// S_CMP_A: set read addresses (j, j+len)
if (state == S_CMP_A) begin
r_wa <= j;
r_wb <= j + layer_len;
end
// S_CMP_B: capture read data
if (state == S_CMP_B) begin
r_a <= cr[j];
r_b <= cr[j + layer_len];
end
// S_CMP_C: write butterfly results, advance counters
if (state == S_CMP_C) begin
cr[r_wa] <= bf_a_out;
cr[r_wb] <= bf_b_out;
j <= j + 8'd1;
if (j + 8'd1 >= start + layer_len) begin
if (!mode) zeta_idx <= zeta_idx + 7'd1;
else zeta_idx <= zeta_idx - 7'd1;
if ({1'b0,start} + {1'b0,layer_len} + {1'b0,layer_len} >= 256) begin
layer <= layer + 3'd1;
layer_len <= mode ? (layer_len<<1) : (layer_len>>1);
start <= 0; j <= 0;
if (layer + 3'd1 >= LAYERS) bf_done <= 1'b1;
end else begin
start <= start + layer_len + layer_len;
j <= start + layer_len + layer_len;
end
end
end
if (state == S_OUTPUT && ready_i)
out_cnt <= (out_cnt>=255) ? 0 : (out_cnt+8'd1);
end
end
endmodule

19
sync_rtl/ntt/ntt_sync.v Normal file
View File

@@ -0,0 +1,19 @@
module ntt_sync (
input clk, rst_n,
input [11:0] coeff_in,
input valid_i,
output ready_o,
output [11:0] coeff_out,
output valid_o,
input ready_i,
input mode,
output done_o
);
ntt_core u_ntt_core (
.clk(clk), .rst_n(rst_n),
.coeff_in(coeff_in), .valid_i(valid_i), .ready_o(ready_o),
.mode(mode),
.coeff_out(coeff_out), .valid_o(valid_o), .ready_i(ready_i),
.done_o(done_o)
);
endmodule

141
sync_rtl/ntt/zeta_rom.v Normal file
View File

@@ -0,0 +1,141 @@
// zeta_rom.v - Precomputed Zeta ROM for ML-KEM NTT
//
// Stores 128 bit-reversed roots of unity for Z_q (q=3329).
// Address 0..127, each entry is a 12-bit value.
// Pure combinational, single continuous assignment.
module zeta_rom (
input [6:0] addr, // 0..127
output [11:0] zeta
);
assign zeta =
(addr == 7'd0) ? 12'd1 :
(addr == 7'd1) ? 12'd1729 :
(addr == 7'd2) ? 12'd2580 :
(addr == 7'd3) ? 12'd3289 :
(addr == 7'd4) ? 12'd2642 :
(addr == 7'd5) ? 12'd630 :
(addr == 7'd6) ? 12'd1897 :
(addr == 7'd7) ? 12'd848 :
(addr == 7'd8) ? 12'd1062 :
(addr == 7'd9) ? 12'd1919 :
(addr == 7'd10) ? 12'd193 :
(addr == 7'd11) ? 12'd797 :
(addr == 7'd12) ? 12'd2786 :
(addr == 7'd13) ? 12'd3260 :
(addr == 7'd14) ? 12'd569 :
(addr == 7'd15) ? 12'd1746 :
(addr == 7'd16) ? 12'd296 :
(addr == 7'd17) ? 12'd2447 :
(addr == 7'd18) ? 12'd1339 :
(addr == 7'd19) ? 12'd1476 :
(addr == 7'd20) ? 12'd3046 :
(addr == 7'd21) ? 12'd56 :
(addr == 7'd22) ? 12'd2240 :
(addr == 7'd23) ? 12'd1333 :
(addr == 7'd24) ? 12'd1426 :
(addr == 7'd25) ? 12'd2094 :
(addr == 7'd26) ? 12'd535 :
(addr == 7'd27) ? 12'd2882 :
(addr == 7'd28) ? 12'd2393 :
(addr == 7'd29) ? 12'd2879 :
(addr == 7'd30) ? 12'd1974 :
(addr == 7'd31) ? 12'd821 :
(addr == 7'd32) ? 12'd289 :
(addr == 7'd33) ? 12'd331 :
(addr == 7'd34) ? 12'd3253 :
(addr == 7'd35) ? 12'd1756 :
(addr == 7'd36) ? 12'd1197 :
(addr == 7'd37) ? 12'd2304 :
(addr == 7'd38) ? 12'd2277 :
(addr == 7'd39) ? 12'd2055 :
(addr == 7'd40) ? 12'd650 :
(addr == 7'd41) ? 12'd1977 :
(addr == 7'd42) ? 12'd2513 :
(addr == 7'd43) ? 12'd632 :
(addr == 7'd44) ? 12'd2865 :
(addr == 7'd45) ? 12'd33 :
(addr == 7'd46) ? 12'd1320 :
(addr == 7'd47) ? 12'd1915 :
(addr == 7'd48) ? 12'd2319 :
(addr == 7'd49) ? 12'd1435 :
(addr == 7'd50) ? 12'd807 :
(addr == 7'd51) ? 12'd452 :
(addr == 7'd52) ? 12'd1438 :
(addr == 7'd53) ? 12'd2868 :
(addr == 7'd54) ? 12'd1534 :
(addr == 7'd55) ? 12'd2402 :
(addr == 7'd56) ? 12'd2647 :
(addr == 7'd57) ? 12'd2617 :
(addr == 7'd58) ? 12'd1481 :
(addr == 7'd59) ? 12'd648 :
(addr == 7'd60) ? 12'd2474 :
(addr == 7'd61) ? 12'd3110 :
(addr == 7'd62) ? 12'd1227 :
(addr == 7'd63) ? 12'd910 :
(addr == 7'd64) ? 12'd17 :
(addr == 7'd65) ? 12'd2761 :
(addr == 7'd66) ? 12'd583 :
(addr == 7'd67) ? 12'd2649 :
(addr == 7'd68) ? 12'd1637 :
(addr == 7'd69) ? 12'd723 :
(addr == 7'd70) ? 12'd2288 :
(addr == 7'd71) ? 12'd1100 :
(addr == 7'd72) ? 12'd1409 :
(addr == 7'd73) ? 12'd2662 :
(addr == 7'd74) ? 12'd3281 :
(addr == 7'd75) ? 12'd233 :
(addr == 7'd76) ? 12'd756 :
(addr == 7'd77) ? 12'd2156 :
(addr == 7'd78) ? 12'd3015 :
(addr == 7'd79) ? 12'd3050 :
(addr == 7'd80) ? 12'd1703 :
(addr == 7'd81) ? 12'd1651 :
(addr == 7'd82) ? 12'd2789 :
(addr == 7'd83) ? 12'd1789 :
(addr == 7'd84) ? 12'd1847 :
(addr == 7'd85) ? 12'd952 :
(addr == 7'd86) ? 12'd1461 :
(addr == 7'd87) ? 12'd2687 :
(addr == 7'd88) ? 12'd939 :
(addr == 7'd89) ? 12'd2308 :
(addr == 7'd90) ? 12'd2437 :
(addr == 7'd91) ? 12'd2388 :
(addr == 7'd92) ? 12'd733 :
(addr == 7'd93) ? 12'd2337 :
(addr == 7'd94) ? 12'd268 :
(addr == 7'd95) ? 12'd641 :
(addr == 7'd96) ? 12'd1584 :
(addr == 7'd97) ? 12'd2298 :
(addr == 7'd98) ? 12'd2037 :
(addr == 7'd99) ? 12'd3220 :
(addr == 7'd100) ? 12'd375 :
(addr == 7'd101) ? 12'd2549 :
(addr == 7'd102) ? 12'd2090 :
(addr == 7'd103) ? 12'd1645 :
(addr == 7'd104) ? 12'd1063 :
(addr == 7'd105) ? 12'd319 :
(addr == 7'd106) ? 12'd2773 :
(addr == 7'd107) ? 12'd757 :
(addr == 7'd108) ? 12'd2099 :
(addr == 7'd109) ? 12'd561 :
(addr == 7'd110) ? 12'd2466 :
(addr == 7'd111) ? 12'd2594 :
(addr == 7'd112) ? 12'd2804 :
(addr == 7'd113) ? 12'd1092 :
(addr == 7'd114) ? 12'd403 :
(addr == 7'd115) ? 12'd1026 :
(addr == 7'd116) ? 12'd1143 :
(addr == 7'd117) ? 12'd2150 :
(addr == 7'd118) ? 12'd2775 :
(addr == 7'd119) ? 12'd886 :
(addr == 7'd120) ? 12'd1722 :
(addr == 7'd121) ? 12'd1212 :
(addr == 7'd122) ? 12'd1874 :
(addr == 7'd123) ? 12'd1029 :
(addr == 7'd124) ? 12'd2110 :
(addr == 7'd125) ? 12'd2935 :
(addr == 7'd126) ? 12'd885 :
12'd2154;
endmodule

View File

@@ -0,0 +1,122 @@
"""gen_vectors.py - Test vector generator for ntt module.
Generates random polynomials and computes expected NTT results
using the Python reference implementation (embedded to avoid
side-effect imports from the original module).
"""
import os
import random
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))
from vector_gen import VectorGenerator
Q = 3329
N = 256
N_INV = 3303
zeta_bitRev = [1, 1729, 2580, 3289, 2642, 630, 1897, 848,
1062, 1919, 193, 797, 2786, 3260, 569, 1746,
296, 2447, 1339, 1476, 3046, 56, 2240, 1333,
1426, 2094, 535, 2882, 2393, 2879, 1974, 821,
289, 331, 3253, 1756, 1197, 2304, 2277, 2055,
650, 1977, 2513, 632, 2865, 33, 1320, 1915,
2319, 1435, 807, 452, 1438, 2868, 1534, 2402,
2647, 2617, 1481, 648, 2474, 3110, 1227, 910,
17, 2761, 583, 2649, 1637, 723, 2288, 1100,
1409, 2662, 3281, 233, 756, 2156, 3015, 3050,
1703, 1651, 2789, 1789, 1847, 952, 1461, 2687,
939, 2308, 2437, 2388, 733, 2337, 268, 641,
1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
1063, 319, 2773, 757, 2099, 561, 2466, 2594,
2804, 1092, 403, 1026, 1143, 2150, 2775, 886,
1722, 1212, 1874, 1029, 2110, 2935, 885, 2154]
def NTT(f):
"""Forward NTT using Cooley-Tukey, bit-exact with reference."""
f_hat = f.copy()
i = 1
for len_NTT in [128, 64, 32, 16, 8, 4, 2]:
for start in range(0, 256, 2 * len_NTT):
zeta = zeta_bitRev[i]
i += 1
for j in range(start, start + len_NTT):
t = (zeta * f_hat[j + len_NTT]) % Q
f_hat[j + len_NTT] = (f_hat[j] - t) % Q
f_hat[j] = (f_hat[j] + t) % Q
return f_hat
def NTTInverse(f_hat):
"""Inverse NTT using Gentleman-Sande, bit-exact with reference."""
f = f_hat.copy()
i = 127
for len_NTT in [2, 4, 8, 16, 32, 64, 128]:
for start in range(0, 256, 2 * len_NTT):
zeta = zeta_bitRev[i]
i -= 1
for j in range(start, start + len_NTT):
t = f[j]
f[j] = (t + f[j + len_NTT]) % Q
f[j + len_NTT] = (zeta * (f[j + len_NTT] - t)) % Q
for i in range(len(f)):
f[i] = (f[i] * N_INV) % Q
return f
class NttVectorGenerator(VectorGenerator):
"""Generates test vectors for the ntt_sync module."""
def generate_one(self, params: dict) -> dict:
mode = params.get('mode', 'forward')
if mode == 'forward':
f = [random.randint(0, Q - 1) for _ in range(N)]
f_hat = NTT(f)
return {
'input': {'mode': 'F', 'coeffs': f},
'expected': {'coeffs': f_hat}
}
elif mode == 'inverse':
f_hat = [random.randint(0, Q - 1) for _ in range(N)]
f = NTTInverse(f_hat)
return {
'input': {'mode': 'I', 'coeffs': f_hat},
'expected': {'coeffs': f}
}
elif mode == 'roundtrip':
f = [random.randint(0, Q - 1) for _ in range(N)]
f_hat = NTT(f)
f_recover = NTTInverse(f_hat)
assert f_recover == f, "Round-trip invariant failed!"
return {
'input': {'mode': 'I', 'coeffs': f_hat},
'expected': {'coeffs': f}
}
else:
raise ValueError(f"Unknown mode: {mode}")
def _format_coeffs(self, coeffs: list[int]) -> str:
return ' '.join(f'{c:03X}' for c in coeffs)
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for v in vectors:
mode = v['input']['mode']
coeffs = v['input']['coeffs']
hex_str = self._format_coeffs(coeffs)
f.write(f'{mode} {hex_str}\n')
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for v in vectors:
coeffs = v['expected']['coeffs']
hex_str = self._format_coeffs(coeffs)
f.write(f'{hex_str}\n')

View File

@@ -0,0 +1,36 @@
{
"module": "ntt",
"rtl_top": "sync_rtl/ntt/ntt_sync.v",
"rtl_deps": [
"sync_rtl/ntt/ntt_core.v",
"sync_rtl/ntt/butterfly_unit.v",
"sync_rtl/ntt/barrett_mul.v",
"sync_rtl/ntt/zeta_rom.v"
],
"tb_cpp": "sync_rtl/ntt/TB/tb_ntt.cpp",
"simulator": "verilator",
"timeout_s": 300,
"cases": [
{
"id": "forward",
"description": "Forward NTT: 256-coeff polynomial -> NTT domain, vs Python NTT()",
"params": {"mode": "forward"},
"num_vectors": 5,
"tolerance": "bit_exact"
},
{
"id": "inverse",
"description": "Inverse NTT: 256-coeff NTT-domain -> normal domain, vs Python NTTInverse()",
"params": {"mode": "inverse"},
"num_vectors": 5,
"tolerance": "bit_exact"
},
{
"id": "roundtrip",
"description": "Round-trip: NTT(INTT(f)) == f",
"params": {"mode": "roundtrip"},
"num_vectors": 3,
"tolerance": "bit_exact"
}
]
}