feat(poly_mul): implement synchronous PolyMul with base-case multiply

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
This commit is contained in:
2026-06-24 23:10:18 +08:00
parent c4cd10c2c1
commit 39dd36994b
6 changed files with 604 additions and 0 deletions

View File

@@ -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 <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#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<uint16_t> 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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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"
}
]
}