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