From c4cd10c2c12e932471422f2bf17a6058db81447d Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Wed, 24 Jun 2026 22:51:14 +0800 Subject: [PATCH] feat(ntt): implement synchronous NTT core with Barrett modular reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- sync_rtl/ntt/TB/tb_ntt.cpp | 195 ++++++++++++++++++++++ sync_rtl/ntt/barrett_mul.v | 53 ++++++ sync_rtl/ntt/butterfly_unit.v | 67 ++++++++ sync_rtl/ntt/ntt_core.v | 132 +++++++++++++++ sync_rtl/ntt/ntt_sync.v | 19 +++ sync_rtl/ntt/zeta_rom.v | 141 ++++++++++++++++ test_framework/modules/ntt/gen_vectors.py | 122 ++++++++++++++ test_framework/modules/ntt/test_plan.json | 36 ++++ 8 files changed, 765 insertions(+) create mode 100644 sync_rtl/ntt/TB/tb_ntt.cpp create mode 100644 sync_rtl/ntt/barrett_mul.v create mode 100644 sync_rtl/ntt/butterfly_unit.v create mode 100644 sync_rtl/ntt/ntt_core.v create mode 100644 sync_rtl/ntt/ntt_sync.v create mode 100644 sync_rtl/ntt/zeta_rom.v create mode 100644 test_framework/modules/ntt/gen_vectors.py create mode 100644 test_framework/modules/ntt/test_plan.json diff --git a/sync_rtl/ntt/TB/tb_ntt.cpp b/sync_rtl/ntt/TB/tb_ntt.cpp new file mode 100644 index 0000000..e70c3fd --- /dev/null +++ b/sync_rtl/ntt/TB/tb_ntt.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#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 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; +} diff --git a/sync_rtl/ntt/barrett_mul.v b/sync_rtl/ntt/barrett_mul.v new file mode 100644 index 0000000..2bf7745 --- /dev/null +++ b/sync_rtl/ntt/barrett_mul.v @@ -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 diff --git a/sync_rtl/ntt/butterfly_unit.v b/sync_rtl/ntt/butterfly_unit.v new file mode 100644 index 0000000..8f616a8 --- /dev/null +++ b/sync_rtl/ntt/butterfly_unit.v @@ -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 diff --git a/sync_rtl/ntt/ntt_core.v b/sync_rtl/ntt/ntt_core.v new file mode 100644 index 0000000..f5146ce --- /dev/null +++ b/sync_rtl/ntt/ntt_core.v @@ -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= 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 diff --git a/sync_rtl/ntt/ntt_sync.v b/sync_rtl/ntt/ntt_sync.v new file mode 100644 index 0000000..90f2329 --- /dev/null +++ b/sync_rtl/ntt/ntt_sync.v @@ -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 diff --git a/sync_rtl/ntt/zeta_rom.v b/sync_rtl/ntt/zeta_rom.v new file mode 100644 index 0000000..09eeafd --- /dev/null +++ b/sync_rtl/ntt/zeta_rom.v @@ -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 diff --git a/test_framework/modules/ntt/gen_vectors.py b/test_framework/modules/ntt/gen_vectors.py new file mode 100644 index 0000000..b97315f --- /dev/null +++ b/test_framework/modules/ntt/gen_vectors.py @@ -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') diff --git a/test_framework/modules/ntt/test_plan.json b/test_framework/modules/ntt/test_plan.json new file mode 100644 index 0000000..28c78e0 --- /dev/null +++ b/test_framework/modules/ntt/test_plan.json @@ -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" + } + ] +}