From 39dd36994b51b395258ae9bcaf0fd6dccf00a529 Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Wed, 24 Jun 2026 23:10:18 +0800 Subject: [PATCH] feat(poly_mul): implement synchronous PolyMul with base-case multiply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2.2: NTT-domain polynomial pointwise multiplication. - basecase_mul.v: degree-1 base-case multiply (c0,c1) with Barrett - poly_mul_zeta_rom.v: 128-entry zeta ROM for PolyMul - poly_mul_sync.v: FSM (IDLE→LOAD 256 cycles→COMPUTE 256 cycles→DONE) Verified: 5/5 vectors bit-exact vs Python PolyMul reference --- sync_rtl/poly_mul/TB/tb_poly_mul.cpp | 186 ++++++++++++++++++ sync_rtl/poly_mul/basecase_mul.v | 45 +++++ sync_rtl/poly_mul/poly_mul_sync.v | 166 ++++++++++++++++ sync_rtl/poly_mul/poly_mul_zeta_rom.v | 58 ++++++ .../modules/poly_mul/gen_vectors.py | 128 ++++++++++++ .../modules/poly_mul/test_plan.json | 21 ++ 6 files changed, 604 insertions(+) create mode 100644 sync_rtl/poly_mul/TB/tb_poly_mul.cpp create mode 100644 sync_rtl/poly_mul/basecase_mul.v create mode 100644 sync_rtl/poly_mul/poly_mul_sync.v create mode 100644 sync_rtl/poly_mul/poly_mul_zeta_rom.v create mode 100644 test_framework/modules/poly_mul/gen_vectors.py create mode 100644 test_framework/modules/poly_mul/test_plan.json diff --git a/sync_rtl/poly_mul/TB/tb_poly_mul.cpp b/sync_rtl/poly_mul/TB/tb_poly_mul.cpp new file mode 100644 index 0000000..5b3f217 --- /dev/null +++ b/sync_rtl/poly_mul/TB/tb_poly_mul.cpp @@ -0,0 +1,186 @@ +// tb_poly_mul.cpp - Verilator C++ testbench for poly_mul_sync +// +// Reads test vectors from +VECTOR_FILE= plusarg. +// Format: one line with 512 space-separated hex values: +// A[0] A[1] ... A[255] B[0] B[1] ... B[255] +// Each value is a 3-digit hex (12-bit). +// +// Drives DUT with 256 paired (A,B) coefficients, waits for valid_o, +// then reads 256 output coefficients via valid/ready handshake. +// Prints "RESULT: COEFF0 COEFF1 ... COEFF255\n" to stdout. +// +// Clock: 10ns period. Reset: 2 cycles. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Vpoly_mul_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(Vpoly_mul_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. +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 + Vpoly_mul_sync* dut = new Vpoly_mul_sync; + + // Initialize + dut->clk = 0; + dut->rst_n = 0; + dut->coeff_a_in = 0; + dut->coeff_b_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: A[0] A[1] ... A[255] B[0] B[1] ... B[255] + // Total: 512 hex values, one line + std::istringstream iss(line); + std::string tok; + + std::vector input_coeffs(512); + int coeff_idx = 0; + while (iss >> tok && coeff_idx < 512) { + input_coeffs[coeff_idx] = hex3_to_val(tok); + coeff_idx++; + } + if (coeff_idx != 512) { + std::cerr << "ERROR: Expected 512 coefficients, got " << coeff_idx + << " (vec " << vec_count << ")" << std::endl; + continue; + } + + // ---- Load 256 A+B pairs ---- + while (!dut->ready_o) { + posedge(dut); cycle++; + if (cycle > TIMEOUT_CYCLES) goto timeout_err; + } + for (int i = 0; i < 256; i++) { + dut->coeff_a_in = input_coeffs[i]; + dut->coeff_b_in = input_coeffs[256 + i]; + dut->valid_i = 1; + posedge(dut); cycle++; + dut->valid_i = 0; + if (cycle > TIMEOUT_CYCLES) goto timeout_err; + } + + // ---- 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/poly_mul/basecase_mul.v b/sync_rtl/poly_mul/basecase_mul.v new file mode 100644 index 0000000..420704c --- /dev/null +++ b/sync_rtl/poly_mul/basecase_mul.v @@ -0,0 +1,45 @@ +// basecase_mul.v - NTT-domain degree-1 polynomial multiplication +// +// Computes pointwise product of two degree-1 NTT-domain polynomials: +// c0 = (a0*b0 + a1*b1*zeta) mod Q +// c1 = (a0*b1 + a1*b0) mod Q +// +// Uses three barrett_mul instances for modular multiplications +// and pure combinational modular addition for the final sums. +// +// All inputs/outputs are 12-bit values in [0, Q-1] where Q = 3329. + +module basecase_mul ( + input [11:0] a0, a1, // first polynomial: degree-0 and degree-1 coeffs + input [11:0] b0, b1, // second polynomial: degree-0 and degree-1 coeffs + input [11:0] zeta, // precomputed twiddle factor (zeta^2 * 17 mod Q) + output [11:0] c0, c1 // result polynomial coefficients +); + + localparam Q = 3329; + + // Barrett modular multiplication outputs + wire [11:0] t1; // a0 * b0 mod Q + wire [11:0] t2; // a1 * b1 mod Q + wire [11:0] t3; // a1 * b0 mod Q + wire [11:0] t4; // a0 * b1 mod Q + wire [11:0] t2_zeta; // (a1 * b1) * zeta mod Q + + // Four Barrett multiplications for the scalar products + barrett_mul u_mul1 (.a(a0), .b(b0), .product(t1)); + barrett_mul u_mul2 (.a(a1), .b(b1), .product(t2)); + barrett_mul u_mul3 (.a(a1), .b(b0), .product(t3)); + barrett_mul u_mul4 (.a(a0), .b(b1), .product(t4)); + + // Multiply t2 by zeta for the zeta term in c0 + barrett_mul u_mul_zeta (.a(t2), .b(zeta), .product(t2_zeta)); + + // Combinational modular addition: each sum is in [0, 2Q-1) + // Subtract Q once if >= Q + wire [12:0] sum_c0 = {1'b0, t1} + {1'b0, t2_zeta}; + wire [12:0] sum_c1 = {1'b0, t3} + {1'b0, t4}; + + assign c0 = (sum_c0 >= Q) ? sum_c0[11:0] - Q[11:0] : sum_c0[11:0]; + assign c1 = (sum_c1 >= Q) ? sum_c1[11:0] - Q[11:0] : sum_c1[11:0]; + +endmodule diff --git a/sync_rtl/poly_mul/poly_mul_sync.v b/sync_rtl/poly_mul/poly_mul_sync.v new file mode 100644 index 0000000..59d02b1 --- /dev/null +++ b/sync_rtl/poly_mul/poly_mul_sync.v @@ -0,0 +1,166 @@ +// poly_mul_sync.v - Synchronous NTT-domain polynomial multiplier +// +// Computes pointwise (Karatsuba-like base-case) multiplication of two +// 256-coefficient NTT-domain polynomials. +// +// Operation flow: +// IDLE → LOAD (256× A+B pairs) → COMP_CALC (read+compute, 1 cycle) +// → COMP_C0 (output c0) → COMP_C1 (output c1) → DONE → IDLE +// +// The LOAD phase accepts both A and B coefficients simultaneously +// (one pair per cycle) on coeff_a_in/coeff_b_in. +// +// The COMPUTE phase outputs the 256 result coefficients one per cycle +// via valid/ready handshake on coeff_out/valid_o/ready_i. +// +// Memory: 256×12-bit register arrays for A and B coefficients. +// +// Interface: +// clk, rst_n - Clock, active-low reset +// coeff_a_in[11:0]- Polynomial A coefficient input +// coeff_b_in[11:0]- Polynomial B coefficient input +// valid_i - Input valid +// ready_o - Ready to accept input (high in IDLE/LOAD) +// coeff_out[11:0] - Result coefficient output +// valid_o - Output valid (high in COMP_C0/COMP_C1) +// ready_i - Output consumer ready + +module poly_mul_sync ( + input clk, rst_n, + input [11:0] coeff_a_in, + input [11:0] coeff_b_in, + input valid_i, + output ready_o, + output [11:0] coeff_out, + output valid_o, + input ready_i +); + + // State definitions + localparam S_IDLE = 3'd0; + localparam S_LOAD = 3'd1; + localparam S_COMP_CALC = 3'd2; + localparam S_COMP_C0 = 3'd3; + localparam S_COMP_C1 = 3'd4; + localparam S_DONE = 3'd5; + + reg [2:0] state, next_state; + + // Coefficient storage (register arrays) + reg [11:0] mem_A [0:255]; + reg [11:0] mem_B [0:255]; + + // Counters + reg [7:0] load_cnt; // 0..256 for loading 256 pairs + reg [6:0] comp_k; // 0..127, current base-case index + + // Registered basecase_mul results + reg [11:0] c0_reg, c1_reg; + + // Combinational read signals for COMP_CALC + wire [7:0] addr_even = {comp_k, 1'b0}; // comp_k * 2 (7+1 = 8 bits) + wire [7:0] addr_odd = {comp_k, 1'b1}; // comp_k * 2 + 1 + wire [11:0] mem_a0 = mem_A[addr_even]; + wire [11:0] mem_a1 = mem_A[addr_odd]; + wire [11:0] mem_b0 = mem_B[addr_even]; + wire [11:0] mem_b1 = mem_B[addr_odd]; + + // Zeta ROM + wire [11:0] zeta; + poly_mul_zeta_rom u_zeta ( + .addr (comp_k), + .zeta (zeta) + ); + + // Basecase multiply + wire [11:0] bc_c0, bc_c1; + basecase_mul u_bc ( + .a0 (mem_a0), + .a1 (mem_a1), + .b0 (mem_b0), + .b1 (mem_b1), + .zeta(zeta), + .c0 (bc_c0), + .c1 (bc_c1) + ); + + // Output interface + assign ready_o = (state == S_IDLE) || (state == S_LOAD); + assign valid_o = (state == S_COMP_C0) || (state == S_COMP_C1); + assign coeff_out = (state == S_COMP_C0) ? c0_reg : c1_reg; + + // State transition logic (combinational) + always @* begin + next_state = state; + case (state) + S_IDLE: if (valid_i && ready_o) next_state = S_LOAD; + S_LOAD: if (load_cnt >= 255 && valid_i && ready_o) + next_state = S_COMP_CALC; + S_COMP_CALC: next_state = S_COMP_C0; + S_COMP_C0: if (valid_o && ready_i) next_state = S_COMP_C1; + S_COMP_C1: if (valid_o && ready_i) begin + if (comp_k >= 127) next_state = S_DONE; + else next_state = S_COMP_CALC; + end + S_DONE: next_state = S_IDLE; + default: next_state = S_IDLE; + endcase + end + + // Sequential logic + integer i; + always @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state <= S_IDLE; + load_cnt <= 8'd0; + comp_k <= 7'd0; + c0_reg <= 12'd0; + c1_reg <= 12'd0; + for (i = 0; i < 256; i = i + 1) begin + mem_A[i] <= 12'd0; + mem_B[i] <= 12'd0; + end + end else begin + state <= next_state; + + // ---- LOAD phase ---- + // First coefficient captured on IDLE → LOAD transition + if (state == S_IDLE && valid_i && ready_o) begin + mem_A[0] <= coeff_a_in; + mem_B[0] <= coeff_b_in; + load_cnt <= 8'd1; + end + + // Subsequent coefficients in LOAD state + if (state == S_LOAD && valid_i && ready_o) begin + mem_A[load_cnt] <= coeff_a_in; + mem_B[load_cnt] <= coeff_b_in; + load_cnt <= load_cnt + 8'd1; + end + + // ---- COMPUTE phase ---- + // COMP_CALC: capture basecase_mul results + if (state == S_COMP_CALC) begin + c0_reg <= bc_c0; + c1_reg <= bc_c1; + end + + // COMP_C0 → COMP_C1: c0 was consumed, increment comp_k + if (state == S_COMP_C0 && valid_o && ready_i) begin + // comp_k stays same, c1 still to output + end + + // COMP_C1 → COMP_CALC: c1 was consumed, advance to next pair + if (state == S_COMP_C1 && valid_o && ready_i) begin + comp_k <= comp_k + 7'd1; + end + + // ---- DONE → IDLE: reset counters ---- + if (state == S_DONE) begin + load_cnt <= 8'd0; + comp_k <= 7'd0; + end + end + end + +endmodule diff --git a/sync_rtl/poly_mul/poly_mul_zeta_rom.v b/sync_rtl/poly_mul/poly_mul_zeta_rom.v new file mode 100644 index 0000000..e25b9bb --- /dev/null +++ b/sync_rtl/poly_mul/poly_mul_zeta_rom.v @@ -0,0 +1,58 @@ +// poly_mul_zeta_rom.v - Read-only memory for PolyMul zeta values +// +// Stores precomputed zeta values for base-case NTT-domain polynomial +// multiplication. Each entry zeta[k] = (zeta_bitRev[k]^2 * 17) mod Q. +// +// 128 entries × 12 bits. Pure combinational read. +// +// zeta_bitRev values are the same bit-reversed twiddle factors used +// in NTT. The PolyMul formula squares them and multiplies by 17 mod Q. + +module poly_mul_zeta_rom ( + input [6:0] addr, // 0..127, selects which zeta to read + output [11:0] zeta // zeta = (zeta_bitRev[addr]^2 * 17) mod Q +); + + reg [11:0] rom [0:127]; + + // Initialize ROM with precomputed zeta values + // Generated from Python: zeta = (zeta_bitRev[i]**2 * 17) % 3329 + initial begin + rom[ 0] = 12'd17; rom[ 1] = 12'd3312; rom[ 2] = 12'd2761; rom[ 3] = 12'd568; + rom[ 4] = 12'd583; rom[ 5] = 12'd2746; rom[ 6] = 12'd2649; rom[ 7] = 12'd680; + rom[ 8] = 12'd1637; rom[ 9] = 12'd1692; rom[ 10] = 12'd723; rom[ 11] = 12'd2606; + rom[ 12] = 12'd2288; rom[ 13] = 12'd1041; rom[ 14] = 12'd1100; rom[ 15] = 12'd2229; + rom[ 16] = 12'd1409; rom[ 17] = 12'd1920; rom[ 18] = 12'd2662; rom[ 19] = 12'd667; + rom[ 20] = 12'd3281; rom[ 21] = 12'd48; rom[ 22] = 12'd233; rom[ 23] = 12'd3096; + rom[ 24] = 12'd756; rom[ 25] = 12'd2573; rom[ 26] = 12'd2156; rom[ 27] = 12'd1173; + rom[ 28] = 12'd3015; rom[ 29] = 12'd314; rom[ 30] = 12'd3050; rom[ 31] = 12'd279; + rom[ 32] = 12'd1703; rom[ 33] = 12'd1626; rom[ 34] = 12'd1651; rom[ 35] = 12'd1678; + rom[ 36] = 12'd2789; rom[ 37] = 12'd540; rom[ 38] = 12'd1789; rom[ 39] = 12'd1540; + rom[ 40] = 12'd1847; rom[ 41] = 12'd1482; rom[ 42] = 12'd952; rom[ 43] = 12'd2377; + rom[ 44] = 12'd1461; rom[ 45] = 12'd1868; rom[ 46] = 12'd2687; rom[ 47] = 12'd642; + rom[ 48] = 12'd939; rom[ 49] = 12'd2390; rom[ 50] = 12'd2308; rom[ 51] = 12'd1021; + rom[ 52] = 12'd2437; rom[ 53] = 12'd892; rom[ 54] = 12'd2388; rom[ 55] = 12'd941; + rom[ 56] = 12'd733; rom[ 57] = 12'd2596; rom[ 58] = 12'd2337; rom[ 59] = 12'd992; + rom[ 60] = 12'd268; rom[ 61] = 12'd3061; rom[ 62] = 12'd641; rom[ 63] = 12'd2688; + rom[ 64] = 12'd1584; rom[ 65] = 12'd1745; rom[ 66] = 12'd2298; rom[ 67] = 12'd1031; + rom[ 68] = 12'd2037; rom[ 69] = 12'd1292; rom[ 70] = 12'd3220; rom[ 71] = 12'd109; + rom[ 72] = 12'd375; rom[ 73] = 12'd2954; rom[ 74] = 12'd2549; rom[ 75] = 12'd780; + rom[ 76] = 12'd2090; rom[ 77] = 12'd1239; rom[ 78] = 12'd1645; rom[ 79] = 12'd1684; + rom[ 80] = 12'd1063; rom[ 81] = 12'd2266; rom[ 82] = 12'd319; rom[ 83] = 12'd3010; + rom[ 84] = 12'd2773; rom[ 85] = 12'd556; rom[ 86] = 12'd757; rom[ 87] = 12'd2572; + rom[ 88] = 12'd2099; rom[ 89] = 12'd1230; rom[ 90] = 12'd561; rom[ 91] = 12'd2768; + rom[ 92] = 12'd2466; rom[ 93] = 12'd863; rom[ 94] = 12'd2594; rom[ 95] = 12'd735; + rom[ 96] = 12'd2804; rom[ 97] = 12'd525; rom[ 98] = 12'd1092; rom[ 99] = 12'd2237; + rom[100] = 12'd403; rom[101] = 12'd2926; rom[102] = 12'd1026; rom[103] = 12'd2303; + rom[104] = 12'd1143; rom[105] = 12'd2186; rom[106] = 12'd2150; rom[107] = 12'd1179; + rom[108] = 12'd2775; rom[109] = 12'd554; rom[110] = 12'd886; rom[111] = 12'd2443; + rom[112] = 12'd1722; rom[113] = 12'd1607; rom[114] = 12'd1212; rom[115] = 12'd2117; + rom[116] = 12'd1874; rom[117] = 12'd1455; rom[118] = 12'd1029; rom[119] = 12'd2300; + rom[120] = 12'd2110; rom[121] = 12'd1219; rom[122] = 12'd2935; rom[123] = 12'd394; + rom[124] = 12'd885; rom[125] = 12'd2444; rom[126] = 12'd2154; rom[127] = 12'd1175; + end + + // Combinational read + assign zeta = rom[addr]; + +endmodule diff --git a/test_framework/modules/poly_mul/gen_vectors.py b/test_framework/modules/poly_mul/gen_vectors.py new file mode 100644 index 0000000..82faec2 --- /dev/null +++ b/test_framework/modules/poly_mul/gen_vectors.py @@ -0,0 +1,128 @@ +"""gen_vectors.py - Test vector generator for poly_mul module. + +Generates random NTT-domain polynomial pairs and computes expected +pointwise base-case multiplication results using embedded Python reference. +""" + +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 + +# Bit-reversed zeta values (same as NTT zeta_bitRev) +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] + +# Precomputed PolyMul zeta values: zeta_sq_mul[i] = (zeta_bitRev[i]^2 * 17) % Q +zeta_sq_mul = [((z * z) * 17) % Q for z in zeta_bitRev] + + +def basecase_multiply(a0, a1, b0, b1, zeta): + """Degree-1 NTT-domain base-case multiply. + + Computes: + c0 = (a0*b0 + a1*b1*zeta) mod Q + c1 = (a0*b1 + a1*b0) mod Q + + Uses modular arithmetic to match Barrett reduction semantics. + """ + c0 = ((a0 * b0) % Q + (((a1 * b1) % Q) * zeta) % Q) % Q + c1 = ((a0 * b1) % Q + (a1 * b0) % Q) % Q + return c0, c1 + + +def poly_mul(f_hat, g_hat): + """Full NTT-domain polynomial multiplication. + + Given two 256-coefficient NTT-domain polynomials f_hat and g_hat, + compute pointwise product using 128 degree-1 base-case multiplies. + + Args: + f_hat: list of 256 coefficients in [0, Q-1] + g_hat: list of 256 coefficients in [0, Q-1] + + Returns: + list of 256 result coefficients in [0, Q-1] + """ + h_hat = [] + for i in range(128): + zeta = zeta_sq_mul[i] + h1, h2 = basecase_multiply( + f_hat[2 * i], f_hat[2 * i + 1], + g_hat[2 * i], g_hat[2 * i + 1], + zeta + ) + h_hat.append(h1) + h_hat.append(h2) + return h_hat + + +class PolyMulVectorGenerator(VectorGenerator): + """Generates test vectors for the poly_mul_sync module.""" + + def generate_one(self, params: dict) -> dict: + """Generate a random polynomial pair with expected result. + + Args: + params: Unused for basic case. + + Returns: + dict with 'input' (A coeffs, B coeffs) and 'expected' (C coeffs). + """ + # Generate random NTT-domain polynomials + A = [random.randint(0, Q - 1) for _ in range(N)] + B = [random.randint(0, Q - 1) for _ in range(N)] + + # Compute expected result via Python reference + C = poly_mul(A, B) + + return { + 'input': {'A': A, 'B': B}, + 'expected': {'C': C} + } + + def _format_coeffs(self, coeffs: list[int]) -> str: + """Format a coefficient list as space-separated 3-digit hex.""" + return ' '.join(f'{c:03X}' for c in coeffs) + + def write_hex_file(self, vectors: list[dict], filepath: str) -> None: + """Write input vectors: one line per vector with 512 hex values. + + Format: A[0] A[1] ... A[255] B[0] B[1] ... B[255] + """ + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w') as f: + for v in vectors: + all_coeffs = v['input']['A'] + v['input']['B'] + hex_str = self._format_coeffs(all_coeffs) + f.write(f'{hex_str}\n') + + def write_expected_file(self, vectors: list[dict], filepath: str) -> None: + """Write expected output: one line per vector with 256 hex values. + + Format: C[0] C[1] ... C[255] + """ + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w') as f: + for v in vectors: + hex_str = self._format_coeffs(v['expected']['C']) + f.write(f'{hex_str}\n') diff --git a/test_framework/modules/poly_mul/test_plan.json b/test_framework/modules/poly_mul/test_plan.json new file mode 100644 index 0000000..fbfaa27 --- /dev/null +++ b/test_framework/modules/poly_mul/test_plan.json @@ -0,0 +1,21 @@ +{ + "module": "poly_mul", + "rtl_top": "sync_rtl/poly_mul/poly_mul_sync.v", + "rtl_deps": [ + "sync_rtl/poly_mul/basecase_mul.v", + "sync_rtl/poly_mul/poly_mul_zeta_rom.v", + "sync_rtl/ntt/barrett_mul.v" + ], + "tb_cpp": "sync_rtl/poly_mul/TB/tb_poly_mul.cpp", + "simulator": "verilator", + "timeout_s": 300, + "cases": [ + { + "id": "basic", + "description": "Pointwise multiply of two random NTT-domain polynomials, vs Python basecase_mul reference", + "params": {}, + "num_vectors": 5, + "tolerance": "bit_exact" + } + ] +}