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:
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user