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:
112
test_framework/modules/rng/gen_vectors.py
Normal file
112
test_framework/modules/rng/gen_vectors.py
Normal 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')
|
||||
17
test_framework/modules/rng/test_plan.json
Normal file
17
test_framework/modules/rng/test_plan.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
184
test_framework/modules/sample_cbd/gen_vectors.py
Normal file
184
test_framework/modules/sample_cbd/gen_vectors.py
Normal 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
|
||||
24
test_framework/modules/sample_cbd/test_plan.json
Normal file
24
test_framework/modules/sample_cbd/test_plan.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
155
test_framework/modules/sample_ntt/gen_vectors.py
Normal file
155
test_framework/modules/sample_ntt/gen_vectors.py
Normal 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")
|
||||
27
test_framework/modules/sample_ntt/test_plan.json
Normal file
27
test_framework/modules/sample_ntt/test_plan.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user