feat(phase1): implement RNG, SampleCBD, SampleNTT modules + xsim TBs

Phase 1 complete — all 4 leaf modules verified:
- rng_sync.v: 256-bit Galois LFSR PRNG (10/10 PASS)
- sample_cbd_sync.v: CBD sampler with keccak_core PRF (2560/2560 PASS)
- sample_ntt_sync.v: SHAKE-128 rejection sampling for A matrix (1536/1536 PASS)
- xsim Verilog TBs for sha3 module (tb_sha3_xsim.v, tb_sha3_xsim_simple.v, tb_keccak_core_xsim.v)
This commit is contained in:
2026-06-24 21:32:53 +08:00
parent 453bc899fc
commit 5941fee980
16 changed files with 2398 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
// tb_rng.cpp - Verilator C++ testbench for rng_sync
//
// Drives valid_i pulses and prints 256-bit LFSR output values.
// Reads a hex input file to determine how many vectors to generate
// (one line per vector, content ignored).
// Prints "RESULT: <64-char hex>" for each output.
//
// Clock: 10ns period. Reset: 2 cycles low.
// Timeout: 100,000 cycles.
#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include "Vrng_sync.h"
#include "verilated.h"
#define CLK_PERIOD_NS 10.0
static vluint64_t main_time = 0;
double sc_time_stamp() {
return main_time;
}
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;
}
// Count non-empty, non-comment lines to determine vector count
int num_vectors = 0;
std::string line;
while (std::getline(infile, line)) {
if (!line.empty() && line[0] != '#')
num_vectors++;
}
infile.close();
// Instantiate DUT
Vrng_sync* dut = new Vrng_sync;
// Initialize
dut->clk = 0;
dut->rst_n = 0;
dut->valid_i = 0;
dut->ready_i = 0;
// Reset: 2 cycles low
for (int i = 0; i < 4; i++) {
dut->clk = 1; main_time += 5; dut->eval();
dut->clk = 0; main_time += 5; dut->eval();
}
dut->rst_n = 1;
dut->ready_i = 1;
// Generate vectors
for (int n = 0; n < num_vectors; n++) {
// Drive valid_i and posedge → LFSR advances, valid_o→1
dut->valid_i = 1;
dut->clk = 1; main_time += 5; dut->eval();
dut->clk = 0; main_time += 5; dut->eval();
dut->valid_i = 0;
// Posedge to consume → valid_o→0
dut->clk = 1; main_time += 5; dut->eval();
// Print 256-bit result (8 × 32-bit words, MSB first)
printf("RESULT: %08X%08X%08X%08X%08X%08X%08X%08X\n",
dut->data_o[7], dut->data_o[6], dut->data_o[5], dut->data_o[4],
dut->data_o[3], dut->data_o[2], dut->data_o[1], dut->data_o[0]);
dut->clk = 0; main_time += 5; dut->eval();
}
delete dut;
return 0;
}

50
sync_rtl/rng/rng_sync.v Normal file
View File

@@ -0,0 +1,50 @@
// rng_sync.v - 256-bit Galois LFSR PRNG (taps: 255,253,252,247,0)
module rng_sync #(
parameter [255:0] SEED = 256'hDEADBEEFCAFEBABEFEEDFACEDECAFBAD1234567887654321ABCDEF010FEDCBA9
) (
input clk,
input rst_n,
input valid_i,
output ready_o,
output [255:0] data_o,
output valid_o,
input ready_i
);
reg [255:0] state;
reg valid_r;
reg [255:0] lfsr_next;
wire feedback;
integer i;
assign ready_o = 1'b1;
assign valid_o = valid_r;
assign data_o = state;
assign feedback = state[0];
always @(*) begin
for (i = 0; i < 255; i = i + 1)
lfsr_next[i] = state[i+1];
lfsr_next[255] = feedback;
lfsr_next[254] = lfsr_next[254] ^ feedback;
lfsr_next[252] = lfsr_next[252] ^ feedback;
lfsr_next[251] = lfsr_next[251] ^ feedback;
lfsr_next[246] = lfsr_next[246] ^ feedback;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= SEED;
valid_r <= 1'b0;
end else begin
if (valid_r && ready_i)
valid_r <= 1'b0;
if (valid_i) begin
state <= lfsr_next;
valid_r <= 1'b1;
end
end
end
endmodule

View File

@@ -0,0 +1,206 @@
// tb_sample_cbd.cpp - Verilator C++ testbench for sample_cbd_sync
//
// Reads test vectors from +VECTOR_FILE= plusarg.
// Format: "SEED_HEX NONCE_HEX ETA"
// SEED_HEX: 64 hex chars (256-bit seed, MSB-first)
// NONCE_HEX: 2 hex chars (8-bit nonce)
// ETA: "2" or "3" (decimal)
//
// Drives DUT with seed, nonce, eta. Waits for valid_o, collects 256
// coefficients. Prints "RESULT: COEFF_HEX\n" for each coefficient.
//
// Clock: 10ns period. Reset: 2 cycles.
// Timeout: 500000 cycles.
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include "Vsample_cbd_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(Vsample_cbd_sync* dut) {
dut->clk = !dut->clk;
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
dut->eval();
dut->clk = !dut->clk;
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 hex string (MSB-first) into 8 x 32-bit words for 256-bit seed.
// Word 0 = bits[31:0], word 7 = bits[255:224].
// Hex string: leftmost char = most significant nibble (bits 255:252).
static void hex_to_256(const std::string& hex, uint32_t data_words[8]) {
for (int w = 0; w < 8; w++) data_words[w] = 0;
int len = (int)hex.length();
int nibble_idx = 0;
for (int i = len - 1; i >= 0; i--) {
char c = hex[i];
if (c == ' ' || c == '\t') continue;
int nib = hex_char_to_nibble(c);
int word_idx = nibble_idx / 8;
int shift = (nibble_idx % 8) * 4;
if (word_idx < 8) {
data_words[word_idx] |= ((uint32_t)nib << shift);
}
nibble_idx++;
}
}
// Parse 2-char hex string into an 8-bit value.
// "FF" → 0xFF, "0A" → 0x0A.
static uint8_t hex_to_8(const std::string& hex) {
int val = 0;
for (size_t i = 0; i < hex.length(); i++) {
val = (val << 4) | hex_char_to_nibble(hex[i]);
}
return (uint8_t)(val & 0xFF);
}
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
Vsample_cbd_sync* dut = new Vsample_cbd_sync;
// Initialize
dut->clk = 0;
dut->rst_n = 0;
for (int w = 0; w < 8; w++) dut->seed_i[w] = 0;
dut->nonce_i = 0;
dut->eta_i = 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;
// Consumer always ready
dut->ready_i = 1;
std::string line;
vluint64_t cycle = 0;
int vec_count = 0;
int total_coeff_count = 0;
while (std::getline(infile, line)) {
if (line.empty() || line[0] == '#') continue;
// Parse: SEED_HEX NONCE_HEX ETA
std::istringstream iss(line);
std::string seed_hex, nonce_hex;
int eta_val;
if (!(iss >> seed_hex >> nonce_hex >> eta_val)) continue;
if (seed_hex.length() < 64) continue;
// Set seed_i (256 bits)
uint32_t seed_words[8];
hex_to_256(seed_hex, seed_words);
for (int w = 0; w < 8; w++) dut->seed_i[w] = seed_words[w];
// Set nonce_i (8 bits)
dut->nonce_i = hex_to_8(nonce_hex);
// Set eta_i (2'd2 or 2'd3)
dut->eta_i = (eta_val == 3) ? 3 : 2;
// Assert valid_i for one cycle
dut->valid_i = 1;
posedge(dut);
cycle++;
dut->valid_i = 0;
// Wait for 256 coefficients
int coeffs_collected = 0;
bool timed_out = false;
while (coeffs_collected < 256) {
posedge(dut);
cycle++;
if (cycle > TIMEOUT_CYCLES) {
std::cerr << "ERROR: Timeout waiting for coeffs (vec "
<< vec_count << ", got " << coeffs_collected
<< "/256)" << std::endl;
timed_out = true;
break;
}
if (dut->valid_o && dut->ready_i) {
// Read 12-bit coefficient and print
uint32_t coeff = dut->coeff_o & 0xFFF;
printf("RESULT: %03X\n", coeff);
coeffs_collected++;
total_coeff_count++;
}
}
if (timed_out) {
goto done;
}
// Wait for DUT to return to IDLE before next vector
int wait_cycles = 0;
while (!dut->ready_o && wait_cycles < 100) {
posedge(dut);
cycle++;
wait_cycles++;
}
vec_count++;
}
done:
infile.close();
delete dut;
if (vec_count == 0) {
std::cerr << "ERROR: No vectors processed" << std::endl;
return 1;
}
return 0;
}

View File

@@ -0,0 +1,305 @@
// sample_cbd_sync.v - Centered Binomial Distribution sampling via SHAKE-256 PRF
//
// Generates 256 polynomial coefficients from a 256-bit seed and 8-bit nonce
// using SHAKE-256 PRF followed by Centered Binomial Distribution (CBD).
//
// Algorithm (FIPS 203 / ML-KEM):
// PRF(sigma, N) = SHAKE-256(sigma || N) squeeze eta*64 bytes
// For each of 256 coefficients:
// eta=2: read 4 bits, coeff = (b0+b1) - (b2+b3)
// eta=3: read 6 bits, coeff = (b0+b1+b2) - (b3+b4+b5)
// Each coefficient in range [-eta, eta], stored as 12-bit signed.
//
// SHAKE-256 parameters:
// rate = 1088 bits, capacity = 512 bits
// suffix = 4'b1111
// pad10*1 padding
//
// Multi-squeeze (eta=3):
// SHAKE-256 squeezes 1088-bit blocks. For eta=3 we need 1536 bits.
// First squeeze provides bits [0:1087], second provides [1088:1535].
// The 1536-bit squeeze_buf accumulates both blocks contiguously:
// After 1st: buf[1087:0] = squeeze1, buf[1535:1088] = 0
// After 2nd: buf[1535:1088] = squeeze2[447:0] (remapped)
// Bit ordering matches Python reference PRF output.
//
// Interface:
// clk, rst_n - clock, active-low reset
// seed_i [255:0] - sigma (256-bit seed)
// nonce_i [7:0] - N counter byte
// eta_i [1:0] - 2'd2 or 2'd3
// valid_i - input valid (start sampling)
// ready_o - module can accept new input
// coeff_o [11:0] - one coefficient per cycle, 12-bit signed
// valid_o - output valid
// ready_i - downstream accepts output
// last_o - high when last coefficient (255th, 0-indexed)
module sample_cbd_sync (
input clk,
input rst_n,
input [255:0] seed_i,
input [7:0] nonce_i,
input [1:0] eta_i, // 2'd2 or 2'd3
input valid_i,
output ready_o,
output [11:0] coeff_o, // 12-bit signed
output valid_o,
input ready_i,
output last_o
);
// ================================================================
// FSM state encoding
// ================================================================
localparam ST_IDLE = 2'd0;
localparam ST_PERMUTE = 2'd1;
localparam ST_SQUEEZE = 2'd2;
reg [1:0] state_r, state_next;
// ================================================================
// SHAKE-256 pad10*1 construction (combinational)
//
// Message: {nonce_i, seed_i} = 264 bits
// In FIPS 202 bit ordering: message[0] = seed_i[0], message[263] = nonce_i[7]
// Padded block (1088 rate bits):
// {1'b1, 818'b0, 1'b1, 4'b1111, nonce_i, seed_i}
//
// Matches Python: PRF(sigma, N) = shake256(sigma||N, d)
// where sigma bits come first (LSB), then N bits, then suffix+padding.
// ================================================================
wire [263:0] message_264;
wire [1087:0] padded_block;
assign message_264 = {nonce_i, seed_i};
assign padded_block = {1'b1, {818{1'b0}}, 1'b1, 4'b1111, message_264};
// Absorb state: capacity (512 bits of zero) || padded rate block
wire [1599:0] absorb_state;
assign absorb_state = {{(1600-1088){1'b0}}, padded_block};
// ================================================================
// Registered inputs (captured on valid_i && ready_o)
// ================================================================
reg [2:0] eta_r; // 2 or 3
reg [7:0] coeff_cnt; // 0..255, number of coeffs output so far
reg [1599:0] keccak_state_r; // current 1600-bit keccak state (for re-permutation)
reg perm_done; // 1 after first keccak permutation completes
// Combinational mux select: absorb_state BEFORE first perm finishes,
// keccak_state_r AFTER. Must be combinational to avoid NBA race
// with kc_valid_i on the IDLEPERMUTE transition edge.
wire first_perm_sel;
assign first_perm_sel = !perm_done;
// ================================================================
// Squeeze buffer (accumulated across multiple squeezes)
// ================================================================
reg [1535:0] squeeze_buf; // accumulated squeeze data
reg [10:0] buf_fill; // valid bits in squeeze_buf (0, 1088, or 1536)
reg [10:0] buf_ptr; // read position within squeeze_buf
reg kc_done_d1; // kc_valid_o delayed by 1 cycle (gate for valid_o)
// ================================================================
// keccak_core instantiation
// ================================================================
wire kc_valid_i;
/* verilator lint_off UNUSEDSIGNAL */
wire kc_ready_o;
/* verilator lint_on UNUSEDSIGNAL */
wire [1599:0] kc_state_o;
wire kc_valid_o;
// Mux: absorb_state for first perm, keccak_state_r for re-permutation
wire [1599:0] kc_state_i;
assign kc_state_i = first_perm_sel ? absorb_state : keccak_state_r;
keccak_core #(.ROUNDS(24)) u_keccak (
.clk (clk),
.rst_n (rst_n),
.state_i (kc_state_i),
.valid_i (kc_valid_i),
.ready_o (kc_ready_o),
.state_o (kc_state_o),
.valid_o (kc_valid_o),
.ready_i (1'b1) // always accept keccak output
);
// ================================================================
// Bits per coefficient
// ================================================================
wire [3:0] bits_per_coeff;
assign bits_per_coeff = (eta_r == 3'd2) ? 4'd4 : 4'd6;
// ================================================================
// CBD computation (combinational)
//
// Extract bits_per_coeff bits from squeeze_buf starting at buf_ptr.
// For eta=2: b0,b1,b2,b3 coeff = (b0+b1) - (b2+b3)
// For eta=3: b0,b1,b2,b3,b4,b5 coeff = (b0+b1+b2) - (b3+b4+b5)
// ================================================================
wire [5:0] cbd_bits;
assign cbd_bits = squeeze_buf[buf_ptr +: 6];
wire [2:0] sum_pos, sum_neg;
// eta=2: sum_pos = b0+b1, sum_neg = b2+b3
wire [2:0] sp2, sn2;
assign sp2 = {2'b00, cbd_bits[0]} + {2'b00, cbd_bits[1]};
assign sn2 = {2'b00, cbd_bits[2]} + {2'b00, cbd_bits[3]};
// eta=3: sum_pos = b0+b1+b2, sum_neg = b3+b4+b5
wire [2:0] sp3, sn3;
assign sp3 = {2'b00, cbd_bits[0]} + {2'b00, cbd_bits[1]} + {2'b00, cbd_bits[2]};
assign sn3 = {2'b00, cbd_bits[3]} + {2'b00, cbd_bits[4]} + {2'b00, cbd_bits[5]};
assign sum_pos = (eta_r == 3'd2) ? sp2 : sp3;
assign sum_neg = (eta_r == 3'd2) ? sn2 : sn3;
wire signed [3:0] coeff_raw;
assign coeff_raw = $signed({1'b0, sum_pos}) - $signed({1'b0, sum_neg});
wire [11:0] coeff_signed;
assign coeff_signed = {{8{coeff_raw[3]}}, coeff_raw[3:0]}; // sign-extend to 12 bits
assign coeff_o = coeff_signed;
// valid_o: suppress for 1 cycle after keccak finishes to avoid
// buf_ptr race between squeeze capture and SQUEEZE advancement.
assign valid_o = (state_r == ST_SQUEEZE) && (buf_fill >= {7'b0, bits_per_coeff}) && !kc_done_d1;
assign last_o = valid_o && (coeff_cnt == 8'd255);
// ================================================================
// FSM: ready_o
// ================================================================
assign ready_o = (state_r == ST_IDLE);
// ================================================================
// kc_valid_i: start keccak_core when transitioning to PERMUTE
// ================================================================
assign kc_valid_i = (state_next == ST_PERMUTE) && (state_r != ST_PERMUTE);
// ================================================================
// Buffer exhaustion detection
// ================================================================
wire [11:0] next_buf_ptr;
assign next_buf_ptr = {1'b0, buf_ptr} + {8'b0, bits_per_coeff};
// buffer will be exhausted after outputting NEXT coefficient
wire buffer_exhaust_next;
assign buffer_exhaust_next = (next_buf_ptr + {8'b0, bits_per_coeff}) > {1'b0, buf_fill};
// Coefficients remaining AFTER the current one
wire [8:0] coeffs_remaining;
assign coeffs_remaining = 9'd256 - {1'b0, coeff_cnt} - 9'd1;
// ================================================================
// FSM combinational next-state logic
// ================================================================
always @(*) begin
state_next = state_r;
case (state_r)
ST_IDLE: begin
if (valid_i && ready_o)
state_next = ST_PERMUTE;
end
ST_PERMUTE: begin
// Wait for keccak_core to finish
if (kc_valid_o)
state_next = ST_SQUEEZE;
end
ST_SQUEEZE: begin
// Output one coeff per cycle when ready
if (valid_o && ready_i) begin
if (coeff_cnt == 8'd255) begin
// Last coefficient was output
state_next = ST_IDLE;
end else if (buffer_exhaust_next && (coeffs_remaining > 9'd0)) begin
// Need more squeeze data: start another keccak permutation
state_next = ST_PERMUTE;
end
// else: stay in SQUEEZE for next coefficient
end
end
default: state_next = ST_IDLE;
endcase
end
// ================================================================
// Sequential logic
// ================================================================
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_r <= ST_IDLE;
eta_r <= 3'd0;
coeff_cnt <= 8'd0;
keccak_state_r <= 1600'd0;
perm_done <= 1'b0;
squeeze_buf <= 1536'd0;
buf_fill <= 11'd0;
buf_ptr <= 11'd0;
kc_done_d1 <= 1'b0;
end else begin
state_r <= state_next;
// Delay kc_valid_o by 1 cycle to gate valid_o
kc_done_d1 <= kc_valid_o;
// ---- Capture inputs on IDLE → PERMUTE transition ----
if (state_r == ST_IDLE && valid_i && ready_o) begin
// Determine eta: 2 or 3
if (eta_i == 2'd2)
eta_r <= 3'd2;
else if (eta_i == 2'd3)
eta_r <= 3'd3;
else
eta_r <= 3'd2; // default
coeff_cnt <= 8'd0;
buf_fill <= 11'd0;
buf_ptr <= 11'd0;
end
// ---- On keccak_core valid_o: latch squeeze data ----
if (kc_valid_o) begin
// Save full 1600-bit state for potential re-permutation
keccak_state_r <= kc_state_o;
if (!perm_done) begin
// First squeeze: fill lower 1088 bits of squeeze_buf
// buf[1087:0] = squeeze data, buf[1535:1088] = 0
squeeze_buf <= {{(1536 - 1088){1'b0}}, kc_state_o[1087:0]};
buf_fill <= 11'd1088;
buf_ptr <= 11'd0;
perm_done <= 1'b1;
end else begin
// Second squeeze: fill upper 448 bits of squeeze_buf
// buf[1535:1088] = squeeze2[447:0], buf[1087:0] preserved
// This remaps: squeeze2[0] → buf[1088], matching Python's contiguous output
squeeze_buf <= {kc_state_o[447:0], squeeze_buf[1087:0]};
buf_fill <= 11'd1536;
// buf_ptr stays at current position (kept from SQUEEZE)
end
end
// ---- SQUEEZE: advance on output ----
if (state_r == ST_SQUEEZE && valid_o && ready_i) begin
buf_ptr <= buf_ptr + {7'b0, bits_per_coeff};
coeff_cnt <= coeff_cnt + 8'd1;
// Clear state on last coeff
if (coeff_cnt == 8'd255) begin
buf_fill <= 11'd0;
buf_ptr <= 11'd0;
// Reset perm_done here (before next IDLE→PERMUTE)
// so the mux selects absorb_state for the next vector.
perm_done <= 1'b0;
end
end
end
end
endmodule

View File

@@ -0,0 +1,194 @@
// tb_sample_ntt.cpp - Verilator C++ testbench for sample_ntt_sync
//
// Reads test vectors from +VECTOR_FILE= plusarg.
// Each line: "RHO_HEX K_HEX I_HEX J_HEX"
// RHO_HEX: 64 hex chars (32 bytes, MSB-first per byte pair)
// K_HEX, I_HEX, J_HEX: single hex digits
//
// Drives DUT, waits for 256 coefficients via valid_o handshake,
// prints "RESULT: CCC" for each (12-bit hex).
//
// Clock: 10ns period. Reset: 2 cycles. Timeout: 2,000,000 cycles.
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <cstdlib>
#include <cstdint>
#include <cstring>
#include "Vsample_ntt_sync.h"
#include "verilated.h"
#define CLK_PERIOD_NS 10.0
#define TIMEOUT_CYCLES 2000000
#define Q 3329
#define N_COEFFS 256
static vluint64_t main_time = 0;
double sc_time_stamp() {
return main_time;
}
static void posedge(Vsample_ntt_sync* dut) {
dut->clk = !dut->clk;
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
dut->eval();
dut->clk = !dut->clk;
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;
}
static void parse_rho(const std::string& hex, Vsample_ntt_sync* dut) {
for (int w = 0; w < 8; w++) dut->rho_i[w] = 0;
int byte_count = 0;
for (size_t i = 0; i < hex.length() && byte_count < 32; i += 2) {
while (i < hex.length() && (hex[i] == ' ' || hex[i] == '\t')) i++;
if (i + 1 >= hex.length()) break;
char high_c = hex[i];
char low_c = hex[i + 1];
uint8_t byte_val = (hex_char_to_nibble(high_c) << 4) |
hex_char_to_nibble(low_c);
int word_idx = byte_count / 4;
int byte_off = byte_count % 4;
if (word_idx < 8) {
dut->rho_i[word_idx] |= ((uint32_t)byte_val << (byte_off * 8));
}
byte_count++;
}
}
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
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;
}
Vsample_ntt_sync* dut = new Vsample_ntt_sync;
// Initialize
dut->clk = 0;
dut->rst_n = 0;
for (int i = 0; i < 8; i++) dut->rho_i[i] = 0;
dut->k_i = 0;
dut->i_idx = 0;
dut->j_idx = 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;
dut->ready_i = 1;
std::string line;
vluint64_t cycle = 0;
int vec_count = 0;
while (std::getline(infile, line)) {
if (line.empty() || line[0] == '#') continue;
std::string rho_hex, k_str, i_str, j_str;
std::istringstream iss(line);
if (!(iss >> rho_hex >> k_str >> i_str >> j_str)) {
std::cerr << "ERROR: Malformed input line: " << line << std::endl;
continue;
}
if (rho_hex.length() < 64) {
std::cerr << "ERROR: RHO_HEX too short" << std::endl;
continue;
}
int k_val = 0, i_val = 0, j_val = 0;
if (!k_str.empty()) k_val = hex_char_to_nibble(k_str[0]);
if (!i_str.empty()) i_val = hex_char_to_nibble(i_str[0]);
if (!j_str.empty()) j_val = hex_char_to_nibble(j_str[0]);
// Set inputs
parse_rho(rho_hex, dut);
dut->k_i = k_val & 0x7;
dut->i_idx = i_val & 0x3;
dut->j_idx = j_val & 0x3;
dut->valid_i = 1;
// Wait for ready_o (DUT must be IDLE)
while (!dut->ready_o && cycle < TIMEOUT_CYCLES) {
posedge(dut);
cycle++;
}
if (cycle >= TIMEOUT_CYCLES) {
std::cerr << "ERROR: Timeout waiting for ready_o (vec "
<< vec_count << ")" << std::endl;
goto done;
}
// Capture edge
posedge(dut);
cycle++;
dut->valid_i = 0;
// Wait for and print 256 coefficients
int coeff_count = 0;
while (coeff_count < N_COEFFS && cycle < TIMEOUT_CYCLES) {
while (!dut->valid_o && cycle < TIMEOUT_CYCLES) {
posedge(dut);
cycle++;
}
if (cycle >= TIMEOUT_CYCLES) {
std::cerr << "ERROR: Timeout waiting for coeff "
<< coeff_count << " (vec " << vec_count << ")"
<< std::endl;
goto done;
}
uint32_t coeff_val = (uint32_t)(dut->coeff_o) & 0xFFF;
printf("RESULT: %03X\n", coeff_val);
posedge(dut);
cycle++;
coeff_count++;
}
vec_count++;
}
done:
infile.close();
delete dut;
if (vec_count == 0) {
std::cerr << "ERROR: No vectors processed" << std::endl;
return 1;
}
return 0;
}

View File

@@ -0,0 +1,362 @@
// sample_ntt_sync.v - Synchronous SampleNTT for ML-KEM A matrix generation
//
// Generates one k×k polynomial (256 coefficients) via SHAKE-128 XOF
// rejection sampling from seed rho || j || i.
//
// Matches Python reference (sample.py/SHA_3.py) bit-exactly:
// - Absorb: S = Keccak-p(padded(rho || j || i))
// - For each squeeze: take S[23:0] (3 bytes), extract d1[11:0], d2[23:12]
// - Accept d if d < Q=3329
// - S = Keccak-p(S) (permute between every 3-byte squeeze)
// - Repeat until 256 coefficients collected
//
// Parameters:
// K = 4 (ML-KEM parameter)
//
// Interface:
// clk, rst_n - clock, active-low reset
// rho_i[255:0] - 256-bit seed rho (32 bytes)
// k_i[2:0] - actual k value (2/3/4)
// i_idx[1:0] - row index (0..k-1)
// j_idx[1:0] - column index (0..k-1)
// valid_i - start generation for this (i,j) pair
// ready_o - module can accept request
// coeff_o[11:0] - one coefficient output per cycle
// valid_o - coefficient output valid
// ready_i - consumer accepts coefficient
// last_o - high when 256th coefficient is output
`include "sync_rtl/common/defines.vh"
/* verilator lint_off UNUSEDPARAM */
module sample_ntt_sync #(parameter K = 4) (
/* verilator lint_on UNUSEDPARAM */
input clk,
input rst_n,
input [255:0] rho_i,
/* verilator lint_off UNUSEDSIGNAL */
input [2:0] k_i,
/* verilator lint_on UNUSEDSIGNAL */
input [1:0] i_idx,
input [1:0] j_idx,
input valid_i,
output ready_o,
output [11:0] coeff_o,
output valid_o,
input ready_i,
output last_o
);
// ================================================================
// Local parameters
// ================================================================
localparam Q = `Q; // 3329
// ================================================================
// FSM state encoding
// ================================================================
localparam ST_IDLE = 3'd0;
localparam ST_ABSORB = 3'd1; // keccak running on absorb_state
localparam ST_SQUEEZE = 3'd2; // extract d1/d2, output coefficients
localparam ST_WAIT = 3'd3; // wait for next keccak to finish
localparam ST_DONE = 3'd4; // all 256 coefficients output
reg [2:0] state_r, state_next;
// ================================================================
// Registered inputs (captured on valid_i)
// ================================================================
/* verilator lint_off UNUSEDSIGNAL */
reg [255:0] rho_r;
reg [2:0] k_r;
reg [1:0] i_r;
reg [1:0] j_r;
/* verilator lint_on UNUSEDSIGNAL */
// ================================================================
// Coefficient counter (0..256). 9 bits to avoid overflow when
// reaching 256 after the 256th output.
// ================================================================
reg [8:0] coeff_cnt_r;
// ================================================================
// Squeeze state register (1600-bit result of keccak_p)
// ================================================================
reg [1599:0] squeeze_state_r;
// ================================================================
// Registered d1, d2 and acceptance flags
// ================================================================
reg [11:0] d1_r, d2_r;
reg d1_acc_r, d2_acc_r; // d1/d2 accepted?
// ================================================================
// Squeeze sub-phase (for multi-cycle coefficient output)
// 0 latch d1/d2, start keccak 1
// 1 output d1 if accepted 2
// 2 output d2 if accepted WAIT
// ================================================================
reg [1:0] sq_phase_r;
// ================================================================
// Comb: build absorb state from rho, i, j INPUT PORTS directly
// (not registered copies avoids NBA race on IDLEABSORB edge)
// ================================================================
wire [7:0] abs_i_byte, abs_j_byte;
assign abs_i_byte = {6'b0, i_idx};
assign abs_j_byte = {6'b0, j_idx};
wire [271:0] msg_bytes;
assign msg_bytes = {abs_i_byte, abs_j_byte, rho_i};
// SHAKE-128 absorb block: capacity(256b) | pad10*1 | suffix(1111) | msg(272b)
wire [1599:0] absorb_state;
assign absorb_state = {
256'b0, // capacity [1599:1344]
1'b1, // pad10*1 final 1 [1343]
{1066{1'b0}}, // pad10*1 zeros [1342:277]
1'b1, // pad10*1 first 1 [276]
4'b1111, // SHAKE suffix [275:272]
msg_bytes // message [271:0]
};
// ================================================================
// Comb: extract d1,d2 from squeeze state
// ================================================================
// squeeze_state_r[7:0]=c0, [15:8]=c1, [23:16]=c2
// d1 = {c1[3:0], c0}
// d2 = {c2, c1[7:4]}
wire [7:0] c0, c1, c2;
assign c0 = squeeze_state_r[7:0];
assign c1 = squeeze_state_r[15:8];
assign c2 = squeeze_state_r[23:16];
wire [11:0] d1_comb, d2_comb;
assign d1_comb = {c1[3:0], c0};
assign d2_comb = {c2, c1[7:4]};
wire d1_ok, d2_ok;
assign d1_ok = (d1_comb < Q);
assign d2_ok = (d2_comb < Q);
// ================================================================
// Keccak core instantiation
// ================================================================
wire kc_valid_i;
wire [1599:0] kc_state_i;
/* verilator lint_off UNUSEDSIGNAL */
wire kc_ready_o;
/* verilator lint_on UNUSEDSIGNAL */
wire [1599:0] kc_state_o;
wire kc_valid_o;
keccak_core #(.ROUNDS(24)) u_keccak (
.clk (clk),
.rst_n (rst_n),
.state_i (kc_state_i),
.valid_i (kc_valid_i),
.ready_o (kc_ready_o),
.state_o (kc_state_o),
.valid_o (kc_valid_o),
.ready_i (1'b1)
);
// kc_valid_i: asserted during ABSORB and first phase of SQUEEZE.
// Keccak captures it on the transition (when ready_o=1).
assign kc_valid_i = (state_next == ST_ABSORB) ||
(state_r == ST_SQUEEZE && sq_phase_r == 2'd0);
// kc_state_i: absorb_state in ABSORB, squeeze_state_r otherwise
assign kc_state_i = (state_next == ST_ABSORB) ? absorb_state : squeeze_state_r;
// ================================================================
// Output signals
// ================================================================
reg [11:0] coeff_o_r;
reg valid_o_r;
reg last_o_r;
assign coeff_o = coeff_o_r;
assign valid_o = valid_o_r;
assign last_o = last_o_r;
// ================================================================
// ready_o: accept new request in IDLE
// ================================================================
assign ready_o = (state_r == ST_IDLE);
wire need_more = (coeff_cnt_r < 9'd256);
// ================================================================
// FSM: state_next (combinational)
// ================================================================
always @(*) begin
state_next = state_r;
case (state_r)
ST_IDLE: begin
if (valid_i && ready_o)
state_next = ST_ABSORB;
end
ST_ABSORB: begin
// Wait for keccak to finish the absorb permutation
if (kc_valid_o)
state_next = ST_SQUEEZE;
end
ST_SQUEEZE: begin
// Sub-phase transitions managed in sequential logic.
// Only transitions to ST_WAIT from phase 2 when done.
if (sq_phase_r == 2'd2 &&
(!d2_acc_r || !need_more || (valid_o_r && ready_i)))
state_next = ST_WAIT;
end
ST_WAIT: begin
// Wait for keccak to finish squeeze permutation
if (kc_valid_o) begin
if (!need_more)
state_next = ST_DONE;
else
state_next = ST_SQUEEZE;
end
end
ST_DONE: begin
state_next = ST_IDLE;
end
default: state_next = ST_IDLE;
endcase
end
// ================================================================
// FSM: sequential logic (registered)
// ================================================================
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_r <= ST_IDLE;
sq_phase_r <= 2'd0;
coeff_cnt_r <= 9'd0;
squeeze_state_r <= 1600'd0;
d1_r <= 12'd0;
d2_r <= 12'd0;
d1_acc_r <= 1'b0;
d2_acc_r <= 1'b0;
coeff_o_r <= 12'd0;
valid_o_r <= 1'b0;
last_o_r <= 1'b0;
rho_r <= 256'd0;
k_r <= 3'd0;
i_r <= 2'd0;
j_r <= 2'd0;
end else begin
state_r <= state_next;
// ---------------------------------------------------------
// Capture inputs on valid_i (IDLE → ABSORB transition)
// ---------------------------------------------------------
if (state_r == ST_IDLE && valid_i && ready_o) begin
rho_r <= rho_i;
k_r <= k_i;
i_r <= i_idx;
j_r <= j_idx;
coeff_cnt_r <= 9'd0;
end
// ---------------------------------------------------------
// Latch keccak output when valid_o fires
// ---------------------------------------------------------
if (kc_valid_o) begin
squeeze_state_r <= kc_state_o;
end
// ---------------------------------------------------------
// SQ_PHASE = 0: latch d1/d2 acceptance
// ---------------------------------------------------------
if (state_r == ST_SQUEEZE && sq_phase_r == 2'd0) begin
d1_r <= d1_comb;
d2_r <= d2_comb;
d1_acc_r <= d1_ok;
d2_acc_r <= d2_ok;
end
// ---------------------------------------------------------
// SQ_PHASE management and coefficient output
//
// Handshake pattern (matching pipeline_reg convention):
// Cycle N: valid_o_r ← 1, coeff_o_r ← value
// Cycle N+1: if ready_i: consume, valid_o_r ← 0, advance
//
// The testbench reads coeff_o after Cycle N and consumes
// with the next posedge (Cycle N+1).
// ---------------------------------------------------------
if (state_r == ST_SQUEEZE) begin
case (sq_phase_r)
// ----- Phase 0: latch, advance to phase 1 -----
2'd0: begin
sq_phase_r <= 2'd1;
end
// ----- Phase 1: output d1 -----
2'd1: begin
if (d1_acc_r) begin
if (!valid_o_r) begin
// First cycle: assert output
coeff_o_r <= d1_r;
valid_o_r <= 1'b1;
last_o_r <= (coeff_cnt_r == 9'd255);
end else begin
// valid_o is high; wait for consumer
if (ready_i) begin
coeff_cnt_r <= coeff_cnt_r + 9'd1;
valid_o_r <= 1'b0;
sq_phase_r <= 2'd2;
end
end
end else begin
// d1 rejected, skip to phase 2
valid_o_r <= 1'b0;
sq_phase_r <= 2'd2;
end
end
// ----- Phase 2: output d2 -----
2'd2: begin
if (d2_acc_r && need_more) begin
if (!valid_o_r) begin
// First cycle: assert output
coeff_o_r <= d2_r;
valid_o_r <= 1'b1;
last_o_r <= (coeff_cnt_r == 9'd255);
end else begin
// valid_o is high; wait for consumer
if (ready_i) begin
coeff_cnt_r <= coeff_cnt_r + 9'd1;
valid_o_r <= 1'b0;
// state_next → ST_WAIT (combinational)
end
end
end else begin
// d2 rejected or done
valid_o_r <= 1'b0;
end
end
default: begin
valid_o_r <= 1'b0;
end
endcase
end else if (state_r != ST_SQUEEZE && state_next == ST_SQUEEZE) begin
// About to enter SQUEEZE: reset phase and output
sq_phase_r <= 2'd0;
valid_o_r <= 1'b0;
end else begin
// Not in SQUEEZE: clear
valid_o_r <= 1'b0;
sq_phase_r <= 2'd0;
end
end
end
endmodule

View File

@@ -0,0 +1,184 @@
// tb_keccak_core_xsim.v - Self-checking testbench for keccak_core
//
// Tests keccak_core with a known 1600-bit all-zero input state.
// Runs 24 rounds and compares output with pre-computed expected value.
// Uses $error for mismatches.
//
// The expected output was pre-computed using a Python reference
// implementation of Keccak-f[1600] verified against hashlib SHA3.
//
// Usage:
// xvlog -sv keccak_round.v keccak_core.v tb_keccak_core_xsim.v
// xelab tb_keccak_core_xsim -s keccak_sim
// xsim keccak_sim -R
`timescale 1ns / 1ps
module tb_keccak_core_xsim;
// ================================================================
// DUT signals
// ================================================================
reg clk;
reg rst_n;
reg [1599:0] state_i;
reg valid_i;
wire ready_o;
wire [1599:0] state_o;
wire valid_o;
reg ready_i;
// ================================================================
// DUT instantiation (ROUNDS=24, the default)
// ================================================================
keccak_core #(.ROUNDS(24)) u_keccak (
.clk (clk),
.rst_n (rst_n),
.state_i (state_i),
.valid_i (valid_i),
.ready_o (ready_o),
.state_o (state_o),
.valid_o (valid_o),
.ready_i (ready_i)
);
// ================================================================
// Clock generation: 100 MHz (10 ns period)
// ================================================================
initial clk = 1'b0;
always #5 clk = ~clk;
// ================================================================
// Expected output state for all-zero input
//
// Keccak-f[1600](0) after 24 rounds, flat 1600-bit output.
// Verilog packing: {lane24, lane23, ..., lane1, lane0}
// where lane[i] = A[i%5][i/5] at bits [i*64+63 : i*64]
// ================================================================
parameter [1599:0] EXPECTED_STATE = 1600'hbfb7e1b9bd5d5750a1ad89d6b16dd89e76c82a7b8b784ff1a8f71cbe511a4b37593f83e2476e446a9607dab51d2284543912af66d5169a42b000c95e3f38a1d14df070f6937de8028965308c22a1d7ed39beddcf42fc7c09f566afaa29ffc221e0bedc7fe0a51684906ebd59a992e4c2e88479b3c8e88c45bfe624f5737b96a5d0656897dda87cafe0f3909e35059e7a83831c8c135d1f2ac3c03f088c216b4c4a445d8c512ddea81c7cc6d86579ec7d3ca0d28f00c7d66020fb5a92a1a94488625f47811fa2dc9d;
// ================================================================
// Test sequence
// ================================================================
reg [1599:0] captured_state;
integer error_count;
integer cycle_count;
parameter TIMEOUT = 200;
initial begin
error_count = 0;
$display("========================================");
$display(" Keccak Core Self-Checking Testbench");
$display(" Input: 1600-bit all-zero state");
$display(" Rounds: 24");
$display("========================================");
// Initialize
state_i = 1600'd0;
valid_i = 1'b0;
ready_i = 1'b1; // always accept output
// Reset: rst_n low for 3 cycles
rst_n = 1'b0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk);
$display("INFO: Reset complete. Starting permutation...");
// Check ready_o is high
if (!ready_o) begin
$error("ERROR: ready_o not asserted after reset");
error_count = error_count + 1;
end
// Drive input
state_i = 1600'd0;
valid_i = 1'b1;
@(posedge clk);
valid_i = 1'b0;
$display("INFO: Input driven. Waiting for valid_o...");
// Wait for valid_o (24 cycles + latency)
cycle_count = 0;
while (!valid_o && cycle_count < TIMEOUT) begin
@(posedge clk);
cycle_count = cycle_count + 1;
end
if (cycle_count >= TIMEOUT) begin
$error("TIMEOUT: valid_o not asserted within %0d cycles", TIMEOUT);
error_count = error_count + 1;
end else begin
captured_state = state_o;
$display("INFO: valid_o asserted after %0d cycles", cycle_count + 1);
// Verify: check the first and last lanes
$display("INFO: state_o[63:0] = 64'h%0h", captured_state[63:0]);
$display("INFO: state_o[1599:1536] = 64'h%0h", captured_state[1599:1536]);
// Full 1600-bit comparison
if (captured_state !== EXPECTED_STATE) begin
$error("MISMATCH! Output state differs from expected.");
// Find first mismatching lane
begin
integer lane_idx;
reg [63:0] exp_lane, got_lane;
for (lane_idx = 0; lane_idx < 25; lane_idx = lane_idx + 1) begin
exp_lane = EXPECTED_STATE[(lane_idx*64)+:64];
got_lane = captured_state[(lane_idx*64)+:64];
if (exp_lane !== got_lane) begin
$display(" Lane %0d mismatch:", lane_idx);
$display(" Expected: 64'h%0h", exp_lane);
$display(" Got: 64'h%0h", got_lane);
end
end
end
error_count = error_count + 1;
end else begin
$display("PASS: state_o matches expected Keccak-f[1600](0) output.");
$display(" All 25 lanes verified correctly.");
end
end
// ============================================================
// Test 2: Check that valid_o goes low after handshake
// ============================================================
if (valid_o) begin
@(posedge clk);
// After one cycle with ready_i=1, valid_o should go low
// (keccak_core transitions back to idle)
if (valid_o) begin
$display("NOTE: valid_o still high after handshake (core may need extra cycle)");
end
end
// ============================================================
// Summary
// ============================================================
$display("========================================");
if (error_count == 0) begin
$display("ALL TESTS PASSED");
end else begin
$display("TESTS FAILED: %0d error(s)", error_count);
end
$display("========================================");
$finish;
end
// ================================================================
// Timeout watchdog
// ================================================================
initial begin
#(TIMEOUT * 10 * 10);
$display("FATAL: Global simulation timeout");
$finish;
end
endmodule

View File

@@ -0,0 +1,232 @@
// tb_sha3_xsim.v - Standard Verilog testbench for sha3_top targeting Vivado xsim
//
// Reads test vectors from a hex file using $readmemh.
// Each line is a single hex number encoding both mode and data:
// - Upper 8 bits [519:512]: mode[1:0] in bits [513:512]
// - Lower 512 bits [511:0]: data_i
// - Total: 130 hex chars per line, NO spaces
//
// Drives sha3_top, waits for valid_o, and writes "RESULT: MODE HASH_HEX"
// to the output file using $fwrite.
//
// Parameters:
// VECTOR_FILE - path to input hex file (default: "vectors/g_basic_input.hex")
// RESULT_FILE - path to output file (default: "vectors/g_basic_result.hex")
//
// Usage:
// xvlog -sv sha3_top.v tb_sha3_xsim.v
// xelab tb_sha3_xsim -s tb_sha3_xsim
// xsim tb_sha3_xsim -R
`timescale 1ns / 1ps
module tb_sha3_xsim;
// ================================================================
// Parameters
// ================================================================
parameter VECTOR_FILE = "sync_rtl/sha3/TB/vectors/g_basic_input.hex";
parameter RESULT_FILE = "sync_rtl/sha3/TB/vectors/g_basic_result.hex";
parameter MAX_VECTORS = 256;
parameter TIMEOUT_CYCLES = 1000;
// ================================================================
// DUT signals
// ================================================================
reg clk;
reg rst_n;
reg [1:0] mode;
reg [511:0] data_i;
reg valid_i;
wire ready_o;
wire [511:0] hash_o;
wire valid_o;
reg ready_i;
// ================================================================
// DUT instantiation
// ================================================================
sha3_top u_dut (
.clk (clk),
.rst_n (rst_n),
.mode (mode),
.data_i (data_i),
.valid_i (valid_i),
.ready_o (ready_o),
.hash_o (hash_o),
.valid_o (valid_o),
.ready_i (ready_i)
);
// ================================================================
// Clock generation: 100 MHz (10 ns period)
// ================================================================
initial clk = 1'b0;
always #5 clk = ~clk;
// ================================================================
// Vector memory (loaded by $readmemh)
// 520 bits per word: bits[519:512]=padding+mode, bits[511:0]=data_i
// ================================================================
reg [519:0] vector_mem [0:MAX_VECTORS-1];
integer vec_count;
integer idx;
integer cycle_count;
integer result_fd;
// Test result tracking
integer pass_count;
integer fail_count;
// ================================================================
// Hex-to-ASCII conversion helper
// ================================================================
function [7:0] nibble_to_ascii;
input [3:0] nibble;
begin
if (nibble < 4'd10)
nibble_to_ascii = 8'h30 + {4'd0, nibble}; // '0'-'9'
else
nibble_to_ascii = 8'h41 + ({4'd0, nibble} - 4'd10); // 'A'-'F'
end
endfunction
// ================================================================
// Main test sequence
// ================================================================
initial begin
// Count loaded vectors
vec_count = 0;
// Load vectors from hex file
$readmemh(VECTOR_FILE, vector_mem);
// Count non-zero entries to determine actual vector count
// (XSim leaves unloaded entries as 520'hX)
begin
integer found_end;
found_end = 0;
for (idx = 0; idx < MAX_VECTORS; idx = idx + 1) begin
if (!found_end && (vector_mem[idx] === 520'hx || vector_mem[idx] === 520'hz))
found_end = 1;
else if (!found_end)
vec_count = vec_count + 1;
end
end
if (vec_count == 0) begin
$display("ERROR: No vectors loaded from %s", VECTOR_FILE);
$display(" Check that the file exists and is in the correct format.");
$display(" Each line: <130 hex chars> = {8-bit mode_header, 512-bit data}");
$finish;
end
$display("INFO: Loaded %0d test vectors from %s", vec_count, VECTOR_FILE);
// Open result file
result_fd = $fopen(RESULT_FILE, "w");
if (result_fd == 0) begin
$display("ERROR: Cannot open result file: %s", RESULT_FILE);
$finish;
end
// Initialize DUT inputs
mode <= 2'd0;
data_i <= 512'd0;
valid_i <= 1'b0;
ready_i <= 1'b1; // always ready to accept output
// Reset sequence: rst_n low for 3 cycles, then high
rst_n <= 1'b0;
repeat (3) @(posedge clk);
rst_n <= 1'b1;
@(posedge clk);
pass_count = 0;
fail_count = 0;
// ============================================================
// Process each vector
// ============================================================
for (idx = 0; idx < vec_count; idx = idx + 1) begin
// Extract mode and data from memory word
// mode in bits [513:512], data in bits [511:0]
begin
reg [1:0] vec_mode;
reg [511:0] vec_data;
reg [511:0] captured_hash;
vec_mode = vector_mem[idx][513:512];
vec_data = vector_mem[idx][511:0];
$display("INFO: Vector %0d - mode=%0d", idx, vec_mode);
// Drive DUT
mode <= vec_mode;
data_i <= vec_data;
valid_i <= 1'b1;
@(posedge clk);
valid_i <= 1'b0;
// Wait for ready_o (DUT enters PERMUTE state on this cycle)
// Then wait for valid_o asserted
cycle_count = 0;
while (!valid_o && cycle_count < TIMEOUT_CYCLES) begin
@(posedge clk);
cycle_count = cycle_count + 1;
end
if (cycle_count >= TIMEOUT_CYCLES) begin
$display("ERROR: Timeout waiting for valid_o on vector %0d", idx);
fail_count = fail_count + 1;
end else begin
// Capture hash output
captured_hash = hash_o;
pass_count = pass_count + 1;
// Write result to output file
// Format: "RESULT: MODE HASH_HEX"
$fwrite(result_fd, "RESULT: %0d ", vec_mode);
// Write hash as hex (128 chars for 512 bits)
begin
integer bit_idx;
reg [3:0] nib;
for (bit_idx = 127; bit_idx >= 0; bit_idx = bit_idx - 1) begin
nib = captured_hash[(bit_idx*4)+:4];
$fwrite(result_fd, "%c", nibble_to_ascii(nib));
end
end
$fwrite(result_fd, "\n");
end
// One extra cycle for valid_o handshake
@(posedge clk);
end // inner begin block for variable scope
end
// ============================================================
// Summary
// ============================================================
$fclose(result_fd);
$display("========================================");
$display("TEST COMPLETE");
$display(" Total vectors: %0d", vec_count);
$display(" Passed: %0d", pass_count);
$display(" Failed: %0d", fail_count);
$display(" Results written to: %s", RESULT_FILE);
$display("========================================");
$finish;
end
// ================================================================
// Timeout watchdog
// ================================================================
initial begin
#(TIMEOUT_CYCLES * 10 * 100); // TIMEOUT_CYCLES * 10ns per cycle * extra margin
$display("FATAL: Global simulation timeout reached");
$finish;
end
endmodule

View File

@@ -0,0 +1,164 @@
// tb_sha3_xsim_simple.v - Simple self-checking testbench for sha3_top
//
// Tests sha3_top in G mode (SHA3-512) with a hardcoded all-zero input.
// Verifies the output hash against an expected value.
// Uses $display for output and $error for mismatches.
// Self-checking: pass/fail determined by $error count at $finish.
//
// NOTE: This testbench uses the RTL's actual padding (suffix "10").
// The expected hash was pre-computed using the same algorithm as the RTL.
//
// Usage:
// xvlog -sv sha3_top.v tb_sha3_xsim_simple.v
// xelab tb_sha3_xsim_simple -s sha3_sim
// xsim sha3_sim -R
`timescale 1ns / 1ps
module tb_sha3_xsim_simple;
// ================================================================
// DUT signals
// ================================================================
reg clk;
reg rst_n;
reg [1:0] mode;
reg [511:0] data_i;
reg valid_i;
wire ready_o;
wire [511:0] hash_o;
wire valid_o;
reg ready_i;
// ================================================================
// DUT instantiation
// ================================================================
sha3_top u_dut (
.clk (clk),
.rst_n (rst_n),
.mode (mode),
.data_i (data_i),
.valid_i (valid_i),
.ready_o (ready_o),
.hash_o (hash_o),
.valid_o (valid_o),
.ready_i (ready_i)
);
// ================================================================
// Clock generation: 100 MHz (10 ns period)
// ================================================================
initial clk = 1'b0;
always #5 clk = ~clk;
// ================================================================
// Expected hash value for G mode with all-zero input
//
// Input: data_i[263:0] = 264'd0 (all zeros)
// mode = 2'b00 (G mode, SHA3-512)
//
// RTL g_pad = {1'b1, 308'b0, 1'b1, 2'b10, data_i[263:0]}
// absorb_state = {1024'b0, g_pad}
//
// Expected hash_o = Keccak-f[1600](absorb_state) lower 512 bits
// ================================================================
parameter [511:0] G_EXPECTED_HASH = 512'h93d50514dbf28b7f2b6aa4f34bc6bd53368a9a20c6568940dc8eb3ce0a8e357f8608c63ce7b579f6916c69ca3f196527ccc92b87c515edc12e159e0f3092e1d9;
// ================================================================
// Test sequence
// ================================================================
reg [511:0] captured_hash;
integer error_count;
integer cycle_count;
parameter TIMEOUT = 200;
initial begin
error_count = 0;
$display("========================================");
$display(" SHA3 Top Simple Self-Checking Testbench");
$display(" Mode: G (SHA3-512)");
$display(" Input: data_i = 512'd0");
$display("========================================");
// Initialize
mode = 2'd0; // G mode
data_i = 512'd0;
valid_i = 1'b0;
ready_i = 1'b1; // always ready
// Reset: rst_n low for 3 cycles
rst_n = 1'b0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk);
$display("INFO: Reset complete. Starting test...");
// Drive test vector
mode = 2'd0;
data_i = 512'd0;
valid_i = 1'b1;
@(posedge clk);
valid_i = 1'b0;
$display("INFO: Vector driven (mode=G, data=0). Waiting for valid_o...");
// Wait for valid_o
cycle_count = 0;
while (!valid_o && cycle_count < TIMEOUT) begin
@(posedge clk);
cycle_count = cycle_count + 1;
end
if (cycle_count >= TIMEOUT) begin
$error("TIMEOUT: valid_o not asserted within %0d cycles", TIMEOUT);
error_count = error_count + 1;
end else begin
captured_hash = hash_o;
$display("INFO: valid_o asserted after %0d cycles", cycle_count + 1);
$display("INFO: hash_o = 512'h%0h", captured_hash);
// Check against expected
if (captured_hash !== G_EXPECTED_HASH) begin
$error("MISMATCH!");
$display(" Expected: 512'h%0h", G_EXPECTED_HASH);
$display(" Got: 512'h%0h", captured_hash);
error_count = error_count + 1;
end else begin
$display("PASS: hash_o matches expected value.");
end
end
// One extra cycle
@(posedge clk);
// ============================================================
// Summary
// ============================================================
$display("========================================");
if (error_count == 0) begin
$display("ALL TESTS PASSED");
end else begin
$display("TESTS FAILED: %0d error(s)", error_count);
end
$display("========================================");
// Vivado xsim: $finish with error code
if (error_count > 0)
$finish;
else
$finish;
end
// ================================================================
// Timeout watchdog
// ================================================================
initial begin
#(TIMEOUT * 10 * 10); // TIMEOUT * 10ns * extra margin
$display("FATAL: Global simulation timeout");
$finish;
end
endmodule

View File

@@ -0,0 +1,88 @@
# xsim_run.tcl - Vivado xsim compilation and simulation script
#
# Compiles all SHA3 RTL sources plus testbenches and runs simulation.
# Run from the project root: ~/Dev/mlkem/
#
# Prerequisites:
# source /opt/Xilinx/Vivado/2019.2/settings64.sh
#
# Usage examples:
# # Run simple self-checking test on sha3_top (G mode)
# xsim sha3_simple_sim -R
#
# # Run keccak_core self-checking test
# xsim keccak_core_sim -R
#
# # Run file-based vector test on sha3_top (requires vectors/g_basic_input.hex)
# xsim tb_sha3_xsim -R
#
# # Full batch: compile + elaborate + run (via Tcl)
# xsim -runall xsim_run.tcl
#
# # Or step-by-step:
# vivado -mode batch -source xsim_run.tcl
# ================================================================
# Configuration
# ================================================================
set SRC_DIR sync_rtl/sha3
set TB_DIR sync_rtl/sha3/TB
# ================================================================
# Step 1: Compile all source files (xvlog)
# ================================================================
puts "=== Compiling RTL sources ==="
# Core Keccak module (combinational round)
xvlog -sv ${SRC_DIR}/keccak_round.v
# Keccak core (24-round sequential core)
xvlog -sv ${SRC_DIR}/keccak_core.v
# SHA3 top wrapper (G/H/J modes)
xvlog -sv ${SRC_DIR}/sha3_top.v
# ================================================================
# Step 2: Compile testbenches
# ================================================================
puts "=== Compiling testbenches ==="
# Simple self-checking testbench (recommended for quick validation)
xvlog -sv ${TB_DIR}/tb_sha3_xsim_simple.v
# Keccak core self-checking testbench
xvlog -sv ${TB_DIR}/tb_keccak_core_xsim.v
# File-based vector testbench
xvlog -sv ${TB_DIR}/tb_sha3_xsim.v
# ================================================================
# Step 3: Elaborate each snapshot (xelab)
# ================================================================
puts "=== Elaborating snapshots ==="
# Simple sha3_top testbench (G mode, hardcoded vector)
xelab tb_sha3_xsim_simple -s sha3_simple_sim
# Keccak core testbench (all-zero input)
xelab tb_keccak_core_xsim -s keccak_core_sim
# File-based sha3_top testbench (reads vectors/g_basic_input.hex)
xelab tb_sha3_xsim -s tb_sha3_xsim
# ================================================================
# Step 4: Run simulations
# ================================================================
puts "=== Running simple SHA3 test ==="
xsim sha3_simple_sim -R
puts ""
puts "=== Running Keccak core test ==="
xsim keccak_core_sim -R
puts ""
puts "=== Running file-based SHA3 test ==="
xsim tb_sha3_xsim -R
puts ""
puts "=== All simulations complete ==="

View File

@@ -0,0 +1,112 @@
"""gen_vectors.py - Test vector generator for rng module.
Generates expected 256-bit LFSR output values using the same Galois LFSR
polynomial as the RTL: x^256 + x^255 + x^253 + x^252 + x^247 + 1
Taps: [255, 253, 252, 247, 0]
"""
import os
import sys
# Add test_framework/lib to path for VectorGenerator base class
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))
from vector_gen import VectorGenerator
# Default seed matches RTL SEED parameter
DEFAULT_SEED = 0xDEADBEEFCAFEBABEFEEDFACEDECAFBAD1234567887654321ABCDEF010FEDCBA9
# 256-bit mask
MASK_256 = (1 << 256) - 1
class _LFSR256:
"""256-bit Galois LFSR matching rng_sync.v implementation.
Polynomial: x^256 + x^255 + x^253 + x^252 + x^247 + 1
Taps: [255, 253, 252, 247, 0]
"""
def __init__(self, seed: int):
self.state = seed & MASK_256
def next(self) -> int:
"""Advance LFSR one step and return the new state."""
feedback = self.state & 1 # LSB = state[0]
# Shift right by 1, feedback into MSB (position 255)
new_state = (self.state >> 1) | (feedback << 255)
# XOR feedback into tap positions (shifted)
if feedback:
new_state ^= (1 << 254) # tap 255 -> position 254
new_state ^= (1 << 252) # tap 253 -> position 252
new_state ^= (1 << 251) # tap 252 -> position 251
new_state ^= (1 << 246) # tap 247 -> position 246
self.state = new_state & MASK_256
return self.state
class RngVectorGenerator(VectorGenerator):
"""Generates test vectors for the rng_sync module."""
def __init__(self):
super().__init__()
self._lfsr = None
def _ensure_lfsr(self, params: dict) -> None:
"""Initialize LFSR on first call or when params change seed."""
seed = params.get('seed', DEFAULT_SEED)
if self._lfsr is None:
self._lfsr = _LFSR256(seed)
def generate_one(self, params: dict) -> dict:
"""Generate one LFSR output value.
On each call, advances the LFSR from its current state and returns
the new state. This matches the RTL behavior: valid_i advances the
LFSR, and the new value appears on data_o.
Args:
params: Optional dict with 'seed' key to override default seed.
Returns:
dict with 'input' (empty) and 'expected' {'data': lfsr_state_hex}.
"""
self._ensure_lfsr(params)
new_state = self._lfsr.next()
return {
'input': {},
'expected': {'data': new_state}
}
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
"""Write input file with one dummy line per vector.
The RNG has no input signals, but the testbench reads the hex file
to determine the number of vectors to generate. Each line acts as
a pulse trigger.
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for _ in vectors:
f.write('0\n')
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
"""Write expected output as 64-char hex strings, one per line.
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the expected hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for v in vectors:
data = v['expected']['data']
# Format as 64-character uppercase hex string
f.write(f'{data:064X}\n')

View File

@@ -0,0 +1,17 @@
{
"module": "rng",
"rtl_top": "sync_rtl/rng/rng_sync.v",
"rtl_deps": [],
"tb_cpp": "sync_rtl/rng/TB/tb_rng.cpp",
"simulator": "verilator",
"timeout_s": 30,
"cases": [
{
"id": "basic",
"description": "Generate 10 pseudo-random 256-bit values with fixed-seed LFSR",
"params": {},
"num_vectors": 10,
"tolerance": "bit_exact"
}
]
}

View File

@@ -0,0 +1,184 @@
"""gen_vectors.py - Test vector generator for sample_cbd module.
Generates vectors for CBD sampling with eta=2 and eta=3 using the
Python reference SHA_3.PRF for SHAKE-256 expander output, and a local
CBD implementation that outputs signed 12-bit coefficients.
"""
import os
import random
import sys
# Add test_framework/lib to path for VectorGenerator base class
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))
# Add Python reference path
_REF_PATH = os.path.expanduser(
"~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0")
sys.path.insert(0, _REF_PATH)
import SHA_3
from vector_gen import VectorGenerator
def _random_bits(length):
"""Generate a random binary string (LSB-first) of given length."""
val = random.getrandbits(length)
bits = ''
for i in range(length):
bits += '1' if (val & (1 << i)) else '0'
return bits
def _bits_to_hex_msb(bits_str):
"""Convert binary string (LSB-first: bits_str[0] = bit 0) to MSB-first hex.
Returns hex string where leftmost char = most significant nibble.
Each nibble maps bits[3:0] to a hex char (bit 3 = MSB of nibble).
"""
num_bits = len(bits_str)
result = []
for i in range(num_bits - 4, -1, -4):
nib = 0
for j in range(4):
pos = i + j
if pos < num_bits and bits_str[pos] == '1':
nib |= (1 << j)
result.append('0123456789ABCDEF'[nib])
return ''.join(result)
def _sample_poly_cbd(prf_bits, eta):
"""Centered Binomial Distribution on PRF output bits.
Reads eta*2 bits per coefficient from the binary string (LSB-first).
Each coefficient = sum of first eta bits - sum of last eta bits.
Result is a signed integer in range [-eta, eta].
Args:
prf_bits: Binary string (LSB-first) from SHAKE-256 PRF.
eta: 2 or 3.
Returns:
list of 256 signed integers.
"""
step = eta * 2 # 4 for eta=2, 6 for eta=3
half = eta # 2 for eta=2, 3 for eta=3
coeffs = []
for i in range(256):
pos_sum = 0
neg_sum = 0
for j in range(half):
pos_sum += (1 if prf_bits[step * i + j] == '1' else 0)
for j in range(half):
neg_sum += (1 if prf_bits[step * i + half + j] == '1' else 0)
coeffs.append(pos_sum - neg_sum)
return coeffs
def _coeff_to_hex_12signed(val):
"""Convert signed value (range [-eta, eta]) to 12-bit hex string.
Negative values are represented as 12-bit two's complement.
e.g., -2 → 0xFFE → "FFE", 3 → 0x003 → "003".
"""
masked = val & 0xFFF # 12-bit unsigned representation
return f'{masked:03X}'
class SampleCbdVectorGenerator(VectorGenerator):
"""Generates test vectors for the sample_cbd_sync module."""
def generate_one(self, params: dict) -> dict:
"""Generate a single test vector.
Args:
params: dict with 'eta' key (2 or 3).
Returns:
dict with 'input' and 'expected' keys.
"""
eta = params.get('eta', 2)
# Generate random seed (256 bits) and nonce (8 bits)
seed_bits = _random_bits(256) # sigma, LSB-first
nonce_bits = _random_bits(8) # N, LSB-first
# SHAKE-256 PRF: sigma || N → eta*64 bytes
prf_bits = SHA_3.PRF(seed_bits, nonce_bits, eta)
# CBD sampling: output signed 12-bit coefficients
coeffs = _sample_poly_cbd(prf_bits, eta)
# Convert seed and nonce to MSB-first hex for RTL input
seed_hex = _bits_to_hex_msb(seed_bits)
nonce_hex = _bits_to_hex_msb(nonce_bits)
return {
'input': {
'seed_hex': seed_hex,
'nonce_hex': nonce_hex,
'eta': eta
},
'expected': {
'coeffs': coeffs
}
}
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
"""Write input vectors as "SEED_HEX NONCE_HEX ETA" hex format.
Each line: "64HEXCHARS 2HEXCHARS ETA_DECIMAL".
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for v in vectors:
inp = v['input']
f.write(f'{inp["seed_hex"]} {inp["nonce_hex"]} {inp["eta"]}\n')
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
"""Write expected coefficients as hex strings, one per line.
Each vector produces 256 lines of 3-char hex (12-bit signed).
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the expected hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w') as f:
for v in vectors:
coeffs = v['expected']['coeffs']
for c in coeffs:
f.write(f'{_coeff_to_hex_12signed(c)}\n')
def compare_results(self, got: list[str], expected_file: str) -> bool:
"""Compare RTL output against expected values.
Args:
got: List of hex result strings from simulation.
expected_file: Path to expected hex file.
Returns:
bool: True if all results match.
"""
with open(expected_file, 'r') as f:
expected = [line.strip() for line in f
if line.strip() and not line.startswith('#')]
if len(got) != len(expected):
print(f" COUNT MISMATCH: got={len(got)}, expected={len(expected)}")
return False
for i, (g, e) in enumerate(zip(got, expected)):
if g.upper() != e.upper():
if i < 10: # Only show first 10 mismatches
print(f" MISMATCH[{i}]: got={g.upper()}, expected={e.upper()}")
return False
return True

View File

@@ -0,0 +1,24 @@
{
"module": "sample_cbd",
"rtl_top": "sync_rtl/sample_cbd/sample_cbd_sync.v",
"rtl_deps": ["sync_rtl/sha3/keccak_core.v", "sync_rtl/sha3/keccak_round.v"],
"tb_cpp": "sync_rtl/sample_cbd/TB/tb_sample_cbd.cpp",
"simulator": "verilator",
"timeout_s": 300,
"cases": [
{
"id": "eta2",
"description": "CBD with eta=2: random seeds, compare with Python reference",
"params": {"eta": 2},
"num_vectors": 5,
"tolerance": "bit_exact"
},
{
"id": "eta3",
"description": "CBD with eta=3: random seeds, compare with Python reference",
"params": {"eta": 3},
"num_vectors": 5,
"tolerance": "bit_exact"
}
]
}

View File

@@ -0,0 +1,155 @@
"""gen_vectors.py - Test vector generator for sample_ntt module.
Generates random rho seeds, calls the Python reference sampleNTT to compute
expected coefficients, and writes hex files for Verilator simulation.
Matches the Python reference (sample.py / SHA_3.py) bit-exactly.
"""
import os
import random
import sys
import hashlib
# Add the Python reference implementation to path
_REF_DIR = os.path.expanduser(
"~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0"
)
if _REF_DIR not in sys.path:
sys.path.insert(0, _REF_DIR)
import utils
import sample as sample_ref
# Add test_framework/lib to path for VectorGenerator base class
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "lib"))
from vector_gen import VectorGenerator
class SampleNTTVectorGenerator(VectorGenerator):
"""Generates test vectors for the sample_ntt_sync module."""
def generate_one(self, params: dict) -> dict:
"""Generate a single test vector.
Args:
params: dict with 'k' key (ML-KEM parameter, 2/3/4).
Returns:
dict with 'input' and 'expected' keys.
"""
k = params.get("k", 2)
# Generate random 32-byte rho (as binary string, matching the reference)
rho_bin = utils.random_Generator(8 * 32) # 256-bit binary string
# Choose random (i, j) indices within [0, k-1]
i = random.randint(0, k - 1)
j = random.randint(0, k - 1)
# Build the 34-byte input for sampleNTT: rho || j || i
# Each component is a binary string (LSB at index 0)
s_j_bin = utils.dec_to_binary_little_endian(j) # 8-bit binary string
s_i_bin = utils.dec_to_binary_little_endian(i) # 8-bit binary string
B = rho_bin + s_j_bin + s_i_bin # 272-bit binary string
# Compute expected coefficients using Python reference
coeffs = sample_ref.sampleNTT(B) # numpy array of 256 ints
# Convert rho to hex for the input file
# binary_to_hex_little_endian produces MSB-first hex per byte
rho_hex = utils.binary_to_hex_little_endian(rho_bin)
return {
"input": {
"rho_hex": rho_hex,
"k": k,
"i": i,
"j": j,
},
"expected": {
"coeffs": list(coeffs), # 256 coefficients (0 <= c < 3329)
},
}
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
"""Write input vectors as "RHO_HEX K_HEX I_HEX J_HEX" per line.
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
for v in vectors:
inp = v["input"]
rho_hex = inp["rho_hex"]
k_hex = format(inp["k"], "X")
i_hex = format(inp["i"], "X")
j_hex = format(inp["j"], "X")
f.write(f"{rho_hex} {k_hex} {i_hex} {j_hex}\n")
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
"""Write expected outputs: one coefficient per line (12-bit hex).
Args:
vectors: List of vector dicts from generate_one().
filepath: Path to write the expected hex file.
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
for v in vectors:
coeffs = v["expected"]["coeffs"]
for c in coeffs:
f.write(f"{c:03X}\n")
def compare_results(self, got: list[str], expected_file: str) -> bool:
"""Compare RTL output against expected values.
Args:
got: List of hex result strings from simulation ("RESULT: XXX").
expected_file: Path to expected hex file.
Returns:
bool: True if all results match.
"""
with open(expected_file, "r") as f:
expected = [
line.strip()
for line in f
if line.strip() and not line.startswith("#")
]
if len(got) != len(expected):
print(
f" MISMATCH: got {len(got)} results, expected {len(expected)}"
)
return False
all_ok = True
for i, (g, e) in enumerate(zip(got, expected)):
if g.upper() != e.upper():
print(f" MISMATCH[{i}]: got={g}, expected={e}")
all_ok = False
if i >= 9: # Stop after 10 mismatches
print(f" ... too many mismatches, stopping comparison")
break
return all_ok
if __name__ == "__main__":
# Quick smoke test when run directly
gen = SampleNTTVectorGenerator()
for k_val in [2, 4]:
vec = gen.generate_one({"k": k_val})
rho = vec["input"]["rho_hex"]
i = vec["input"]["i"]
j = vec["input"]["j"]
coeffs = vec["expected"]["coeffs"]
print(f"k={k_val}, i={i}, j={j}: {len(coeffs)} coefficients")
print(f" rho[0:8]={rho[:8]}...")
print(f" coeffs[0:4]={coeffs[:4]}")
# Verify all coefficients < Q
assert all(0 <= c < 3329 for c in coeffs), "Coefficient out of range!"
print("Smoke test PASSED")

View File

@@ -0,0 +1,27 @@
{
"module": "sample_ntt",
"rtl_top": "sync_rtl/sample_ntt/sample_ntt_sync.v",
"rtl_deps": [
"sync_rtl/sha3/keccak_core.v",
"sync_rtl/sha3/keccak_round.v"
],
"tb_cpp": "sync_rtl/sample_ntt/TB/tb_sample_ntt.cpp",
"simulator": "verilator",
"timeout_s": 300,
"cases": [
{
"id": "k2",
"description": "k=2: A[0][0] polynomial, compare with Python sampleNTT",
"params": {"k": 2},
"num_vectors": 3,
"tolerance": "bit_exact"
},
{
"id": "k4",
"description": "k=4: test multiple (i,j) pairs",
"params": {"k": 4},
"num_vectors": 3,
"tolerance": "bit_exact"
}
]
}