test: support 1000 KAT Vectors
This commit is contained in:
354
run_tb.sh
354
run_tb.sh
@@ -3,14 +3,14 @@
|
|||||||
#
|
#
|
||||||
# Usage: ./run_tb.sh <module> [K] [CASE]
|
# Usage: ./run_tb.sh <module> [K] [CASE]
|
||||||
# ./run_tb.sh ntt # run the module's full xsim_run.tcl
|
# ./run_tb.sh ntt # run the module's full xsim_run.tcl
|
||||||
# ./run_tb.sh top # ML-KEM KeyGen: all K, all cases
|
# ./run_tb.sh top # ML-KEM KeyGen: all K, all KAT vectors
|
||||||
# ./run_tb.sh top 2 # only K=2 (ML-KEM-512), all its cases
|
# ./run_tb.sh top 2 # only K=2 (ML-KEM-512), all KAT vectors
|
||||||
# ./run_tb.sh top 2 3 # only K=2, only CASE=3
|
# ./run_tb.sh top 2 3 # only K=2, only CASE=3
|
||||||
# ./run_tb.sh top 4 0 # only K=4 (ML-KEM-1024), CASE=0
|
# ./run_tb.sh top 4 0 # only K=4 (ML-KEM-1024), CASE=0
|
||||||
# ./run_tb.sh enc # ML-KEM Encaps: all K, all cases (0..2)
|
# ./run_tb.sh enc # ML-KEM Encaps: all K, all KAT vectors
|
||||||
# ./run_tb.sh enc 2 # Encaps K=2, all its cases
|
# ./run_tb.sh enc 2 # Encaps K=2, all KAT vectors
|
||||||
# ./run_tb.sh enc 4 1 # Encaps K=4, only CASE=1
|
# ./run_tb.sh enc 4 1 # Encaps K=4, only CASE=1
|
||||||
# ./run_tb.sh dec # ML-KEM Decaps: all K, all cases (0..2)
|
# ./run_tb.sh dec # ML-KEM Decaps: all K, all KAT vectors
|
||||||
# ./run_tb.sh dec 2 0 # Decaps K=2, only CASE=0
|
# ./run_tb.sh dec 2 0 # Decaps K=2, only CASE=0
|
||||||
# ./run_tb.sh hello [K] # hello_world end-to-end (single instance; K=2/3/4)
|
# ./run_tb.sh hello [K] # hello_world end-to-end (single instance; K=2/3/4)
|
||||||
# ./run_tb.sh hello two [K] # hello_world end-to-end (two instances; K=2/3/4)
|
# ./run_tb.sh hello two [K] # hello_world end-to-end (two instances; K=2/3/4)
|
||||||
@@ -20,8 +20,10 @@
|
|||||||
# all compile the xvlog lines from sync_rtl/top/TB/xsim_run.tcl. They differ only
|
# all compile the xvlog lines from sync_rtl/top/TB/xsim_run.tcl. They differ only
|
||||||
# in the testbench: top -> tb_mlkem_kg_katK_xsim, enc -> tb_mlkem_enc_katK_xsim,
|
# in the testbench: top -> tb_mlkem_kg_katK_xsim, enc -> tb_mlkem_enc_katK_xsim,
|
||||||
# dec -> tb_mlkem_dec_katK_xsim.
|
# dec -> tb_mlkem_dec_katK_xsim.
|
||||||
# For these, K (2/3/4) and CASE select a single KAT run so you can iterate
|
# For these, K (2/3/4) and CASE select a subset of the KAT run so you can
|
||||||
# quickly. Other modules ignore the extra args and run their xsim_run.tcl verbatim.
|
# iterate quickly. Without CASE the script generates/enumerates every vector in
|
||||||
|
# test_data/kat_MLKEM_{512,768,1024}.rsp. Limit xsim parallelism with
|
||||||
|
# RUN_TB_JOBS=N (default: min(nproc, 4)).
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# Vivado 2019.2 at /opt/Xilinx/Vivado/2019.2/
|
# Vivado 2019.2 at /opt/Xilinx/Vivado/2019.2/
|
||||||
@@ -30,6 +32,8 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
VIVADO_SETTINGS="/opt/Xilinx/Vivado/2019.2/settings64.sh"
|
VIVADO_SETTINGS="/opt/Xilinx/Vivado/2019.2/settings64.sh"
|
||||||
|
KAT_VECTOR_DIR="$SCRIPT_DIR/sync_rtl/top/TB/vectors"
|
||||||
|
KAT_RUN_VECTOR_DIR=""
|
||||||
|
|
||||||
if [ "$1" = "--list" ]; then
|
if [ "$1" = "--list" ]; then
|
||||||
echo "Available modules:"
|
echo "Available modules:"
|
||||||
@@ -117,114 +121,240 @@ execute_tcl() {
|
|||||||
# the shared xsim.dir BEFORE any parallel run starts).
|
# the shared xsim.dir BEFORE any parallel run starts).
|
||||||
#
|
#
|
||||||
# run_xsim_jobs <module-tag> <pass-grep-regex> <pass-match-string> <spec>...
|
# run_xsim_jobs <module-tag> <pass-grep-regex> <pass-match-string> <spec>...
|
||||||
# each <spec> = "snapshot:K:CASE". Launches all specs in parallel, waits,
|
# each <spec> = "snapshot:K:CASE". Launches specs with bounded parallelism,
|
||||||
# then prints an ordered summary and returns nonzero if any job failed.
|
# waits, then prints an ordered summary and returns nonzero if any job failed.
|
||||||
PAR_BASE="${TMPDIR:-/tmp}/run_tb_par_$$"
|
PAR_BASE="${TMPDIR:-/tmp}/run_tb_par_$$"
|
||||||
|
KAT_TMP_BASE="${TMPDIR:-/tmp}/run_tb_vectors_$$"
|
||||||
|
trap 'rm -rf "$PAR_BASE" "$KAT_TMP_BASE"' EXIT
|
||||||
|
|
||||||
print_timing_summary() {
|
print_timing_summary() {
|
||||||
local logs=("$@")
|
local logs=("$@")
|
||||||
local found=0 log
|
|
||||||
|
|
||||||
for log in "${logs[@]}"; do
|
|
||||||
if [ -f "$log" ] && grep -q '^TIME ' "$log"; then
|
|
||||||
found=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$found" -eq 0 ] && return 0
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Timing summary (clk=20MHz, period=50ns)"
|
|
||||||
printf " %-7s %-3s %-5s %10s %12s %12s %s\n" "OP" "K" "CASE" "cycles" "time_us" "time_ms" "log"
|
|
||||||
for log in "${logs[@]}"; do
|
|
||||||
[ -f "$log" ] || continue
|
|
||||||
awk -v logfile="$log" '
|
|
||||||
/^TIME / {
|
|
||||||
k = ""; c = ""; op = ""; cycles = ""; runtime = "";
|
|
||||||
for (i = 1; i <= NF; i++) {
|
|
||||||
split($i, a, "=");
|
|
||||||
if (a[1] == "K") k = a[2];
|
|
||||||
else if (a[1] == "CASE") c = a[2];
|
|
||||||
else if (a[1] == "OP") op = a[2];
|
|
||||||
else if (a[1] == "cycles") cycles = a[2];
|
|
||||||
else if (a[1] == "runtime") runtime = a[2];
|
|
||||||
}
|
|
||||||
if (runtime != "") {
|
|
||||||
printf " %-7s %-3s %-5s %10d %12.3f %12.3f %s\n",
|
|
||||||
op, k, c, cycles, runtime / 1000.0, runtime / 1000000.0, logfile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$log"
|
|
||||||
done
|
|
||||||
|
|
||||||
local found_state=0
|
|
||||||
for log in "${logs[@]}"; do
|
|
||||||
if [ -f "$log" ] && grep -q '^ STATE ' "$log"; then
|
|
||||||
found_state=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ "$found_state" -eq 0 ] && return 0
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "State timing breakdown (clk=20MHz, period=50ns)"
|
|
||||||
printf " %-7s %-3s %-5s %-12s %10s %12s\n" "OP" "K" "CASE" "STATE" "cycles" "time_us"
|
|
||||||
for log in "${logs[@]}"; do
|
|
||||||
[ -f "$log" ] || continue
|
|
||||||
awk '
|
awk '
|
||||||
/^TIME_BREAKDOWN / {
|
/^TIME / {
|
||||||
k = ""; c = ""; op = "";
|
cycles = ""; runtime = "";
|
||||||
for (i = 1; i <= NF; i++) {
|
|
||||||
split($i, a, "=");
|
|
||||||
if (a[1] == "K") k = a[2];
|
|
||||||
else if (a[1] == "CASE") c = a[2];
|
|
||||||
else if (a[1] == "OP") op = a[2];
|
|
||||||
}
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
/^ STATE / {
|
|
||||||
state = $2;
|
|
||||||
cycles = "";
|
|
||||||
time_ns = "";
|
|
||||||
for (i = 1; i <= NF; i++) {
|
for (i = 1; i <= NF; i++) {
|
||||||
split($i, a, "=");
|
split($i, a, "=");
|
||||||
if (a[1] == "cycles") cycles = a[2];
|
if (a[1] == "cycles") cycles = a[2];
|
||||||
else if (a[1] == "time_ns") time_ns = a[2];
|
else if (a[1] == "runtime") runtime = a[2];
|
||||||
}
|
}
|
||||||
if (time_ns != "") {
|
if (cycles != "") {
|
||||||
printf " %-7s %-3s %-5s %-12s %10d %12.3f\n",
|
n++;
|
||||||
op, k, c, state, cycles, time_ns / 1000.0;
|
sum_cycles += cycles;
|
||||||
|
if (runtime != "") sum_runtime += runtime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
' "$log"
|
END {
|
||||||
done
|
if (n == 0) {
|
||||||
|
print "Timing average: no TIME records found";
|
||||||
|
} else {
|
||||||
|
printf "Timing average (clk=20MHz, period=50ns): runs=%d avg_cycles=%.1f avg_time_us=%.3f avg_time_ms=%.3f\n",
|
||||||
|
n, sum_cycles / n, (sum_runtime / n) / 1000.0, (sum_runtime / n) / 1000000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "${logs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_kat_selection() {
|
||||||
|
local ksel="$1" csel="$2"
|
||||||
|
case "$ksel" in
|
||||||
|
""|2|3|4) ;;
|
||||||
|
*) echo "ERROR: K must be 2, 3, or 4 (got '$ksel')"; return 1 ;;
|
||||||
|
esac
|
||||||
|
if [ -n "$csel" ] && ! [[ "$csel" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: CASE must be a non-negative integer (got '$csel')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_kat_vectors() {
|
||||||
|
local op="$1" ksel="${2:-}" csel="${3:-}"
|
||||||
|
|
||||||
|
validate_kat_selection "$ksel" "$csel" || return 1
|
||||||
|
KAT_RUN_VECTOR_DIR="$KAT_TMP_BASE/$op"
|
||||||
|
rm -rf "$KAT_RUN_VECTOR_DIR"
|
||||||
|
mkdir -p "$KAT_RUN_VECTOR_DIR"
|
||||||
|
[ -d "$KAT_VECTOR_DIR/encgold" ] && ln -s "$KAT_VECTOR_DIR/encgold" "$KAT_RUN_VECTOR_DIR/encgold"
|
||||||
|
[ -d "$KAT_VECTOR_DIR/decgold" ] && ln -s "$KAT_VECTOR_DIR/decgold" "$KAT_RUN_VECTOR_DIR/decgold"
|
||||||
|
|
||||||
|
python3 - "$SCRIPT_DIR" "$op" "${ksel:-all}" "${csel:-all}" "$KAT_RUN_VECTOR_DIR" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = pathlib.Path(sys.argv[1])
|
||||||
|
op = sys.argv[2]
|
||||||
|
ksel = sys.argv[3]
|
||||||
|
csel = sys.argv[4]
|
||||||
|
out_dir = pathlib.Path(sys.argv[5])
|
||||||
|
|
||||||
|
ks = [2, 3, 4] if ksel == "all" else [int(ksel)]
|
||||||
|
case_filter = None if csel == "all" else int(csel)
|
||||||
|
rsp_by_k = {
|
||||||
|
2: root / "test_data" / "kat_MLKEM_512.rsp",
|
||||||
|
3: root / "test_data" / "kat_MLKEM_768.rsp",
|
||||||
|
4: root / "test_data" / "kat_MLKEM_1024.rsp",
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_rsp(path):
|
||||||
|
records = []
|
||||||
|
cur = {}
|
||||||
|
with path.open("r", encoding="ascii") as f:
|
||||||
|
for raw in f:
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#") or " = " not in line:
|
||||||
|
continue
|
||||||
|
key, val = line.split(" = ", 1)
|
||||||
|
if key == "count":
|
||||||
|
if cur:
|
||||||
|
records.append(cur)
|
||||||
|
cur = {"count": int(val)}
|
||||||
|
else:
|
||||||
|
cur[key] = val.lower()
|
||||||
|
if cur:
|
||||||
|
records.append(cur)
|
||||||
|
return records
|
||||||
|
|
||||||
|
def bytes_of(hexstr, field, count, k):
|
||||||
|
if len(hexstr) % 2:
|
||||||
|
raise SystemExit(f"odd hex length for {field}, K={k}, count={count}")
|
||||||
|
return [hexstr[i:i + 2] for i in range(0, len(hexstr), 2)]
|
||||||
|
|
||||||
|
def write_bytes(path, hexstr, field, count, k):
|
||||||
|
data = bytes_of(hexstr, field, count, k)
|
||||||
|
path.write_text("".join(b + "\n" for b in data), encoding="ascii")
|
||||||
|
|
||||||
|
def write_word256_reversed(path, hexstr, field, count, k):
|
||||||
|
data = bytes_of(hexstr, field, count, k)
|
||||||
|
if len(data) != 32:
|
||||||
|
raise SystemExit(f"{field} must be 32 bytes, got {len(data)}, K={k}, count={count}")
|
||||||
|
path.write_text("".join(reversed(data)) + "\n", encoding="ascii")
|
||||||
|
|
||||||
|
def require(rec, keys, k):
|
||||||
|
missing = [key for key in keys if key not in rec]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing {','.join(missing)} in K={k}, count={rec.get('count')}")
|
||||||
|
|
||||||
|
summary = []
|
||||||
|
for k in ks:
|
||||||
|
records = parse_rsp(rsp_by_k[k])
|
||||||
|
wrote = 0
|
||||||
|
for rec in records:
|
||||||
|
count = rec["count"]
|
||||||
|
if case_filter is not None and count != case_filter:
|
||||||
|
continue
|
||||||
|
tag = f"k{k}"
|
||||||
|
if op == "top":
|
||||||
|
require(rec, ["d", "z", "pk", "sk"], k)
|
||||||
|
base = out_dir / f"kat_{tag}_c{count}"
|
||||||
|
write_word256_reversed(base.with_name(base.name + "_d.hex"), rec["d"], "d", count, k)
|
||||||
|
write_word256_reversed(base.with_name(base.name + "_z.hex"), rec["z"], "z", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ek.hex"), rec["pk"], "pk", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_dk.hex"), rec["sk"], "sk", count, k)
|
||||||
|
elif op == "enc":
|
||||||
|
require(rec, ["pk", "msg", "ct", "ss"], k)
|
||||||
|
base = out_dir / f"enc_{tag}_c{count}"
|
||||||
|
write_bytes(base.with_name(base.name + "_ek.hex"), rec["pk"], "pk", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_m.hex"), rec["msg"], "msg", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ct.hex"), rec["ct"], "ct", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ss.hex"), rec["ss"], "ss", count, k)
|
||||||
|
elif op == "dec":
|
||||||
|
require(rec, ["sk", "ct", "ss", "ct_n", "ss_n"], k)
|
||||||
|
base = out_dir / f"dec_{tag}_c{count}"
|
||||||
|
write_bytes(base.with_name(base.name + "_dk.hex"), rec["sk"], "sk", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ct.hex"), rec["ct"], "ct", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ss.hex"), rec["ss"], "ss", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ctn.hex"), rec["ct_n"], "ct_n", count, k)
|
||||||
|
write_bytes(base.with_name(base.name + "_ssn.hex"), rec["ss_n"], "ss_n", count, k)
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"unknown KAT op: {op}")
|
||||||
|
wrote += 1
|
||||||
|
if case_filter is not None and wrote == 0:
|
||||||
|
raise SystemExit(f"CASE={case_filter} not found in {rsp_by_k[k]}")
|
||||||
|
summary.append(f"K={k}:{wrote}")
|
||||||
|
|
||||||
|
print(f" generated {op} KAT vector files ({', '.join(summary)})")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
kat_cases_for() {
|
||||||
|
local op="$1" k="$2" prefix suffix
|
||||||
|
local dir="${KAT_RUN_VECTOR_DIR:-$KAT_VECTOR_DIR}"
|
||||||
|
case "$op" in
|
||||||
|
top) prefix="kat_k${k}_c"; suffix="_d\\.hex" ;;
|
||||||
|
enc) prefix="enc_k${k}_c"; suffix="_ek\\.hex" ;;
|
||||||
|
dec) prefix="dec_k${k}_c"; suffix="_dk\\.hex" ;;
|
||||||
|
*) echo "ERROR: unknown KAT op '$op'" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
find "$dir" -maxdepth 1 -type f -printf '%f\n' \
|
||||||
|
| sed -n "s/^${prefix}\([0-9][0-9]*\)${suffix}$/\1/p" \
|
||||||
|
| sort -n \
|
||||||
|
| tr '\n' ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
run_xsim_jobs() {
|
run_xsim_jobs() {
|
||||||
local mtag="$1" pgrep="$2" pmatch="$3"; shift 3
|
local mtag="$1" pgrep="$2" pmatch="$3"; shift 3
|
||||||
local specs=("$@")
|
local specs=("$@")
|
||||||
local repo; repo="$(pwd)"
|
local repo; repo="$(pwd)"
|
||||||
|
local max_jobs="${RUN_TB_JOBS:-}"
|
||||||
|
if [ -z "$max_jobs" ]; then
|
||||||
|
if command -v nproc >/dev/null 2>&1; then
|
||||||
|
max_jobs="$(nproc)"
|
||||||
|
[ "$max_jobs" -gt 4 ] && max_jobs=4
|
||||||
|
else
|
||||||
|
max_jobs=4
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
case "$max_jobs" in
|
||||||
|
''|*[!0-9]*|0) echo "ERROR: RUN_TB_JOBS must be a positive integer (got '$max_jobs')"; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
rm -rf "$PAR_BASE"; mkdir -p "$PAR_BASE"
|
rm -rf "$PAR_BASE"; mkdir -p "$PAR_BASE"
|
||||||
|
echo " launching ${#specs[@]} xsim job(s), parallelism=$max_jobs"
|
||||||
|
|
||||||
# launch all jobs in parallel
|
# launch all jobs in parallel
|
||||||
local s snap k c wd
|
local s snap k c wd active=0 launched=0 total_specs=${#specs[@]}
|
||||||
for s in "${specs[@]}"; do
|
for s in "${specs[@]}"; do
|
||||||
IFS=: read -r snap k c <<< "$s"
|
IFS=: read -r snap k c <<< "$s"
|
||||||
|
launched=$((launched + 1))
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
printf "\r running [%d/%d] K=%s CASE=%s" "$launched" "$total_specs" "$k" "$c"
|
||||||
|
else
|
||||||
|
echo " running [$launched/$total_specs] K=$k CASE=$c"
|
||||||
|
fi
|
||||||
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
|
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
|
||||||
mkdir -p "$wd"
|
mkdir -p "$wd"
|
||||||
cp -r "$repo/xsim.dir" "$wd/xsim.dir"
|
cp -r "$repo/xsim.dir" "$wd/xsim.dir"
|
||||||
# The TB loads vectors via repo-relative $readmemh paths
|
# The TB loads vectors via repo-relative $readmemh paths. xsim resolves
|
||||||
# (sync_rtl/top/TB/vectors/...). xsim resolves these against the run
|
# these against the run cwd, so mount the generated vector directory
|
||||||
# cwd, so symlink the repo's sync_rtl tree into each private workdir.
|
# under each private workdir.
|
||||||
|
if [ -n "$KAT_RUN_VECTOR_DIR" ]; then
|
||||||
|
mkdir -p "$wd/sync_rtl/top/TB"
|
||||||
|
ln -sf "$KAT_RUN_VECTOR_DIR" "$wd/sync_rtl/top/TB/vectors"
|
||||||
|
else
|
||||||
ln -sf "$repo/sync_rtl" "$wd/sync_rtl"
|
ln -sf "$repo/sync_rtl" "$wd/sync_rtl"
|
||||||
|
fi
|
||||||
(
|
(
|
||||||
cd "$wd"
|
cd "$wd"
|
||||||
|
{
|
||||||
echo "xsim $snap -R -testplusarg CASE=$c"
|
echo "xsim $snap -R -testplusarg CASE=$c"
|
||||||
xsim "$snap" -R -testplusarg "CASE=$c" --nolog > "$wd/run.out" 2>&1
|
xsim "$snap" -R -testplusarg "CASE=$c" --nolog
|
||||||
|
} > "$wd/run.out" 2>&1
|
||||||
) &
|
) &
|
||||||
|
active=$((active + 1))
|
||||||
|
if [ "$active" -ge "$max_jobs" ]; then
|
||||||
|
wait -n || true
|
||||||
|
active=$((active - 1))
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
wait
|
wait || true
|
||||||
|
[ -t 1 ] && echo ""
|
||||||
|
|
||||||
# ordered summary + per-job log copy to /tmp/run_tb_<mtag>_k*_c*.log
|
# ordered result aggregation + per-job log copy to /tmp/run_tb_<mtag>_k*_c*.log
|
||||||
local fail=0 pf nf log timing_logs=()
|
local fail=0 pf nf log ok
|
||||||
|
local total=0 pass_count=0 fail_count=0 noresult_count=0 fnf_count=0
|
||||||
|
local timing_logs=() failures=()
|
||||||
for s in "${specs[@]}"; do
|
for s in "${specs[@]}"; do
|
||||||
IFS=: read -r snap k c <<< "$s"
|
IFS=: read -r snap k c <<< "$s"
|
||||||
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
|
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
|
||||||
@@ -233,14 +363,38 @@ run_xsim_jobs() {
|
|||||||
timing_logs+=("$log")
|
timing_logs+=("$log")
|
||||||
pf=$(grep -oE "$pgrep" "$log" | tail -1)
|
pf=$(grep -oE "$pgrep" "$log" | tail -1)
|
||||||
nf=$(grep -c 'cannot be opened' "$log")
|
nf=$(grep -c 'cannot be opened' "$log")
|
||||||
echo " K=$k CASE=$c -> ${pf:-NORESULT} (file-not-found=$nf, log: $log)"
|
total=$((total + 1))
|
||||||
|
[ -n "$pf" ] || noresult_count=$((noresult_count + 1))
|
||||||
|
[ "$nf" -eq 0 ] || fnf_count=$((fnf_count + 1))
|
||||||
|
|
||||||
# pmatch == "PASS*" means "any PASS (Dn) stage tag"; else exact match.
|
# pmatch == "PASS*" means "any PASS (Dn) stage tag"; else exact match.
|
||||||
if [ "$pmatch" = "PASS*" ]; then
|
if [ "$pmatch" = "PASS*" ]; then
|
||||||
{ [[ "$pf" == PASS* ]] && [ "$nf" -eq 0 ]; } || fail=1
|
[[ "$pf" == PASS* ]] && [ "$nf" -eq 0 ]; ok=$?
|
||||||
else
|
else
|
||||||
{ [ "$pf" = "$pmatch" ] && [ "$nf" -eq 0 ]; } || fail=1
|
[ "$pf" = "$pmatch" ] && [ "$nf" -eq 0 ]; ok=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ok" -eq 0 ]; then
|
||||||
|
pass_count=$((pass_count + 1))
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fail_count=$((fail_count + 1))
|
||||||
|
if [ "${#failures[@]}" -lt 20 ]; then
|
||||||
|
failures+=("K=$k CASE=$c -> ${pf:-NORESULT} (file-not-found=$nf, log: $log)")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Result summary: total=$total success=$pass_count failure=$fail_count noresult=$noresult_count file-not-found=$fnf_count"
|
||||||
|
if [ "$fail_count" -gt 0 ]; then
|
||||||
|
echo "Failed cases (first ${#failures[@]} of $fail_count):"
|
||||||
|
local item
|
||||||
|
for item in "${failures[@]}"; do
|
||||||
|
echo " $item"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
print_timing_summary "${timing_logs[@]}"
|
print_timing_summary "${timing_logs[@]}"
|
||||||
rm -rf "$PAR_BASE"
|
rm -rf "$PAR_BASE"
|
||||||
return $fail
|
return $fail
|
||||||
@@ -248,11 +402,12 @@ run_xsim_jobs() {
|
|||||||
|
|
||||||
# Fast single-K / single-case path for the 'top' (ML-KEM KeyGen) module.
|
# Fast single-K / single-case path for the 'top' (ML-KEM KeyGen) module.
|
||||||
# Compiles the same RTL as the tcl, but elaborates ONLY the requested K and
|
# Compiles the same RTL as the tcl, but elaborates ONLY the requested K and
|
||||||
# runs the CASE(s) in parallel. Avoids the 3-snapshot + 11-run serial sweep.
|
# runs the generated/enumerated CASE(s) through the bounded parallel launcher.
|
||||||
run_top_selected() {
|
run_top_selected() {
|
||||||
local tcl_file="$1" ksel="$2" csel="$3"
|
local tcl_file="$1" ksel="$2" csel="$3"
|
||||||
set +e # we do our own error handling / result parsing here
|
set +e # we do our own error handling / result parsing here
|
||||||
|
|
||||||
|
generate_kat_vectors top "$ksel" "$csel" || return 1
|
||||||
rm -rf xsim.dir .Xil
|
rm -rf xsim.dir .Xil
|
||||||
|
|
||||||
# Compile every xvlog line from the tcl, verbatim.
|
# Compile every xvlog line from the tcl, verbatim.
|
||||||
@@ -267,10 +422,13 @@ run_top_selected() {
|
|||||||
echo " xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps"
|
echo " xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps"
|
||||||
xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps \
|
xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps \
|
||||||
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
||||||
# Cases per K: K=2 has 0..4, K=3/4 have 0..2.
|
|
||||||
local cases
|
local cases
|
||||||
if [ "$k" = "2" ]; then cases="0 1 2 3 4"; else cases="0 1 2"; fi
|
if [ -n "$csel" ]; then
|
||||||
if [ -n "$csel" ]; then cases="$csel"; fi
|
cases="$csel"
|
||||||
|
else
|
||||||
|
cases="$(kat_cases_for top "$k")"
|
||||||
|
fi
|
||||||
|
[ -n "$cases" ] || { echo "ERROR: no KeyGen KAT cases found for K=$k"; return 1; }
|
||||||
local c
|
local c
|
||||||
for c in $cases; do specs+=("mlkem_kg_k$k:$k:$c"); done
|
for c in $cases; do specs+=("mlkem_kg_k$k:$k:$c"); done
|
||||||
done
|
done
|
||||||
@@ -285,6 +443,7 @@ run_enc_selected() {
|
|||||||
local tcl_file="$1" ksel="$2" csel="$3"
|
local tcl_file="$1" ksel="$2" csel="$3"
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
|
generate_kat_vectors enc "$ksel" "$csel" || return 1
|
||||||
rm -rf xsim.dir .Xil
|
rm -rf xsim.dir .Xil
|
||||||
|
|
||||||
# Compile every xvlog line from the tcl EXCEPT the KeyGen TB (the encaps TB
|
# Compile every xvlog line from the tcl EXCEPT the KeyGen TB (the encaps TB
|
||||||
@@ -306,8 +465,13 @@ run_enc_selected() {
|
|||||||
echo " xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps"
|
echo " xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps"
|
||||||
xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps \
|
xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps \
|
||||||
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
||||||
local cases="0 1 2"
|
local cases
|
||||||
if [ -n "$csel" ]; then cases="$csel"; fi
|
if [ -n "$csel" ]; then
|
||||||
|
cases="$csel"
|
||||||
|
else
|
||||||
|
cases="$(kat_cases_for enc "$k")"
|
||||||
|
fi
|
||||||
|
[ -n "$cases" ] || { echo "ERROR: no Encaps KAT cases found for K=$k"; return 1; }
|
||||||
local c
|
local c
|
||||||
for c in $cases; do specs+=("mlkem_enc_k$k:$k:$c"); done
|
for c in $cases; do specs+=("mlkem_enc_k$k:$k:$c"); done
|
||||||
done
|
done
|
||||||
@@ -321,6 +485,7 @@ run_dec_selected() {
|
|||||||
local tcl_file="$1" ksel="$2" csel="$3"
|
local tcl_file="$1" ksel="$2" csel="$3"
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
|
generate_kat_vectors dec "$ksel" "$csel" || return 1
|
||||||
rm -rf xsim.dir .Xil
|
rm -rf xsim.dir .Xil
|
||||||
|
|
||||||
while read -r cmd; do
|
while read -r cmd; do
|
||||||
@@ -338,8 +503,13 @@ run_dec_selected() {
|
|||||||
echo " xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps"
|
echo " xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps"
|
||||||
xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps \
|
xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps \
|
||||||
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
|| { echo "ELAB FAILED for K=$k"; return 1; }
|
||||||
local cases="0 1 2"
|
local cases
|
||||||
if [ -n "$csel" ]; then cases="$csel"; fi
|
if [ -n "$csel" ]; then
|
||||||
|
cases="$csel"
|
||||||
|
else
|
||||||
|
cases="$(kat_cases_for dec "$k")"
|
||||||
|
fi
|
||||||
|
[ -n "$cases" ] || { echo "ERROR: no Decaps KAT cases found for K=$k"; return 1; }
|
||||||
local c
|
local c
|
||||||
for c in $cases; do specs+=("mlkem_dec_k$k:$k:$c"); done
|
for c in $cases; do specs+=("mlkem_dec_k$k:$k:$c"); done
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""gen_vectors.py - Test vector generator for sample_ntt_sync module.
|
"""Generate FIPS 203 SampleNTT test vectors for sample_ntt_sync.
|
||||||
|
|
||||||
Generates random rho+k+i+j test vectors, computes expected coefficients using
|
The reference oracle is hashlib.shake_128 (FIPS 202). It samples
|
||||||
Keccak-p[1600, 24] matching the RTL's per-permutation squeeze pattern.
|
SHAKE128(rho || j || i) by reading three bytes at a time and accepting
|
||||||
|
d1 = b0 + 256 * (b1 mod 16) and d2 = floor(b1 / 16) + 16 * b2 when d < Q.
|
||||||
|
|
||||||
The RTL sample_ntt_sync absorbs rho || j || i, then repeatedly permutes
|
The RTL feeds rho_i[0] as the first message byte. $readmemh loads
|
||||||
the keccak state, extracting d1,d2 from bits [23:0] of each permuted state
|
rho_i[255:0] MSB-first, so Python passes
|
||||||
with rejection sampling (d < Q=3329).
|
reversed(bytes.fromhex(rho_hex)) + bytes([j, i]) to SHAKE-128.
|
||||||
|
|
||||||
Algorithm (matching RTL exactly):
|
|
||||||
1. Build absorb state: pad10*1 padding for SHAKE-128 (rate=1344) on msg(272b)
|
|
||||||
2. state = keccak_p(absorb_state)
|
|
||||||
3. Extract d1 = (state[11:8] << 8) | state[7:0]
|
|
||||||
d2 = (state[23:16] << 4) | state[15:12]
|
|
||||||
4. If d1 < Q: output d1 (12-bit unsigned)
|
|
||||||
5. If d2 < Q: output d2
|
|
||||||
6. state = keccak_p(state)
|
|
||||||
7. Repeat 3-6 until 256 coefficients collected
|
|
||||||
|
|
||||||
Bit ordering (FIPS 202 / RTL match):
|
|
||||||
The RTL feeds rho_i[0] as the first message bit into SHA3.
|
|
||||||
$readmemh stores hex MSB-first → rho_i[255:0] in MSB-first order.
|
|
||||||
Python: input bytes = reversed(bytes.fromhex(rho_hex)) + bytes([j, i])
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 gen_vectors.py # Generate vectors
|
python3 gen_vectors.py # Generate vectors
|
||||||
python3 gen_vectors.py --verify # Verify results against expected
|
python3 gen_vectors.py --verify # Verify results against expected
|
||||||
|
python3 gen_vectors.py --refresh-expected # Recompute checked-in oracle
|
||||||
python3 gen_vectors.py --selftest # Run Keccak-p self-test
|
python3 gen_vectors.py --selftest # Run Keccak-p self-test
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
@@ -36,6 +24,15 @@ import sys
|
|||||||
Q = 3329
|
Q = 3329
|
||||||
N_COEFFS = 256
|
N_COEFFS = 256
|
||||||
|
|
||||||
|
# This vector accepts its 256th coefficient as d2 from group 168, the final
|
||||||
|
# group of a SHAKE-128 rate block. It guards the no-extra-permutation boundary.
|
||||||
|
RATE_BOUNDARY_VECTOR = {
|
||||||
|
"rho_hex": "7484937160DBF5D93F7D69AEA2DF7C7C77B3A6B02D6B4D579CF975E6F8562097",
|
||||||
|
"k": 2,
|
||||||
|
"i": 1,
|
||||||
|
"j": 1,
|
||||||
|
}
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
# Keccak-p[1600, 24] implementation (stdlib only, matches RTL exactly)
|
# Keccak-p[1600, 24] implementation (stdlib only, matches RTL exactly)
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@@ -269,10 +266,7 @@ def extract_d1_d2(state):
|
|||||||
|
|
||||||
|
|
||||||
def sample_ntt_compute(rho_hex, k, i, j):
|
def sample_ntt_compute(rho_hex, k, i, j):
|
||||||
"""Compute expected coefficients matching RTL sample_ntt_sync.
|
"""Compute the FIPS 203 SampleNTT output for one matrix polynomial.
|
||||||
|
|
||||||
The RTL absorbs rho || j || i, then repeatedly permutes,
|
|
||||||
extracting d1,d2 from each permuted state with rejection sampling.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rho_hex: 64-char hex string.
|
rho_hex: 64-char hex string.
|
||||||
@@ -283,12 +277,19 @@ def sample_ntt_compute(rho_hex, k, i, j):
|
|||||||
Returns:
|
Returns:
|
||||||
list of 256 integers in [0, Q-1].
|
list of 256 integers in [0, Q-1].
|
||||||
"""
|
"""
|
||||||
state = build_absorb_state(rho_hex, j, i)
|
del k
|
||||||
state = keccak_p(state)
|
msg = bytes.fromhex(rho_hex)[::-1] + bytes([j & 0xFF, i & 0xFF])
|
||||||
|
|
||||||
coeffs = []
|
coeffs = []
|
||||||
|
out_len = 168
|
||||||
|
processed = 0
|
||||||
|
|
||||||
while len(coeffs) < N_COEFFS:
|
while len(coeffs) < N_COEFFS:
|
||||||
d1, d2 = extract_d1_d2(state)
|
stream = hashlib.shake_128(msg).digest(out_len)
|
||||||
|
for off in range(processed, len(stream) - 2, 3):
|
||||||
|
b0, b1, b2 = stream[off:off + 3]
|
||||||
|
d1 = b0 + 256 * (b1 & 0x0F)
|
||||||
|
d2 = (b1 >> 4) + 16 * b2
|
||||||
|
|
||||||
if d1 < Q:
|
if d1 < Q:
|
||||||
coeffs.append(d1)
|
coeffs.append(d1)
|
||||||
if len(coeffs) == N_COEFFS:
|
if len(coeffs) == N_COEFFS:
|
||||||
@@ -297,8 +298,8 @@ def sample_ntt_compute(rho_hex, k, i, j):
|
|||||||
coeffs.append(d2)
|
coeffs.append(d2)
|
||||||
if len(coeffs) == N_COEFFS:
|
if len(coeffs) == N_COEFFS:
|
||||||
break
|
break
|
||||||
# Re-permute for next squeeze
|
processed = out_len
|
||||||
state = keccak_p(state)
|
out_len += 168
|
||||||
|
|
||||||
return coeffs
|
return coeffs
|
||||||
|
|
||||||
@@ -367,9 +368,7 @@ def write_expected_hex(vectors, filepath):
|
|||||||
"""Write expected coefficients: one 12-bit hex value per line."""
|
"""Write expected coefficients: one 12-bit hex value per line."""
|
||||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
with open(filepath, "w") as f:
|
with open(filepath, "w") as f:
|
||||||
for idx, v in enumerate(vectors):
|
for v in vectors:
|
||||||
f.write(f"# VECTOR_{idx} k={v['k']} i={v['i']} j={v['j']} "
|
|
||||||
f"rho={v['rho_hex']}\n")
|
|
||||||
for c in v["coeffs"]:
|
for c in v["coeffs"]:
|
||||||
f.write(coeff_to_hex(c) + "\n")
|
f.write(coeff_to_hex(c) + "\n")
|
||||||
|
|
||||||
@@ -424,7 +423,7 @@ def main():
|
|||||||
|
|
||||||
if "--selftest" in sys.argv:
|
if "--selftest" in sys.argv:
|
||||||
keccak_p_selftest()
|
keccak_p_selftest()
|
||||||
print("Running quick smoke test on sample_ntt computation...")
|
print("Running FIPS SampleNTT reference self-test...")
|
||||||
rho = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"
|
rho = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"
|
||||||
coeffs = sample_ntt_compute(rho, k=4, i=0, j=0)
|
coeffs = sample_ntt_compute(rho, k=4, i=0, j=0)
|
||||||
print(f" rho={rho[:16]}...: {len(coeffs)} coefficients")
|
print(f" rho={rho[:16]}...: {len(coeffs)} coefficients")
|
||||||
@@ -467,14 +466,33 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print("VERIFICATION FAILED")
|
print("VERIFICATION FAILED")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
elif "--refresh-expected" in sys.argv:
|
||||||
|
if not os.path.exists(input_file):
|
||||||
|
print(f"ERROR: Input file not found: {input_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
vectors = []
|
||||||
|
with open(input_file, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if len(line) != 66:
|
||||||
|
raise ValueError(f"invalid packed input vector: {line}")
|
||||||
|
header = int(line[:2], 16)
|
||||||
|
vectors.append({
|
||||||
|
"rho_hex": line[2:],
|
||||||
|
"k": header & 0x7,
|
||||||
|
"i": (header >> 3) & 0x3,
|
||||||
|
"j": (header >> 5) & 0x3,
|
||||||
|
})
|
||||||
|
for v in vectors:
|
||||||
|
v["coeffs"] = sample_ntt_compute(v["rho_hex"], v["k"], v["i"], v["j"])
|
||||||
|
write_expected_hex(vectors, expected_file)
|
||||||
|
print(f"Wrote {len(vectors)} FIPS SampleNTT expected vectors to {expected_file}")
|
||||||
else:
|
else:
|
||||||
# Generate mode — only input vectors (no expected outputs).
|
|
||||||
# The RTL uses per-permutation Keccak-p extraction that may not
|
|
||||||
# match standard SHAKE-128 bit-stream output. The TB performs a
|
|
||||||
# smoke test: 256 coefficients, each in [0, Q-1].
|
|
||||||
vector_count = 4
|
vector_count = 4
|
||||||
print(f"Generating {vector_count} random input vectors...")
|
print(f"Generating {vector_count} random input vectors...")
|
||||||
print(f"(Expected outputs are NOT computed — TB uses smoke-test only.)")
|
|
||||||
|
|
||||||
vectors = []
|
vectors = []
|
||||||
for idx in range(vector_count):
|
for idx in range(vector_count):
|
||||||
@@ -490,8 +508,15 @@ def main():
|
|||||||
})
|
})
|
||||||
print(f" Vector {idx}: k={k}, i={i}, j={j}, rho={rho_hex[:8]}...")
|
print(f" Vector {idx}: k={k}, i={i}, j={j}, rho={rho_hex[:8]}...")
|
||||||
|
|
||||||
|
vectors.append(RATE_BOUNDARY_VECTOR.copy())
|
||||||
|
print(" Boundary vector: k=2, i=1, j=1 (256th coefficient is group 168 d2)")
|
||||||
|
|
||||||
write_input_hex(vectors, input_file)
|
write_input_hex(vectors, input_file)
|
||||||
|
for v in vectors:
|
||||||
|
v["coeffs"] = sample_ntt_compute(v["rho_hex"], v["k"], v["i"], v["j"])
|
||||||
|
write_expected_hex(vectors, expected_file)
|
||||||
print(f"Wrote {len(vectors)} vectors to {input_file}")
|
print(f"Wrote {len(vectors)} vectors to {input_file}")
|
||||||
|
print(f"Wrote FIPS SampleNTT expected values to {expected_file}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
// tb_sample_ntt_xsim.v - Vivado xsim smoke-test testbench for sample_ntt_sync
|
// tb_sample_ntt_xsim.v - Vivado xsim FIPS SampleNTT testbench
|
||||||
//
|
//
|
||||||
// Drives the DUT with test vectors from a hex file, then verifies that
|
// Drives the DUT with test vectors from a hex file and checks every coefficient
|
||||||
// the DUT produces exactly 256 coefficients, each in range [0, Q-1].
|
// against the FIPS 203 reference generated by gen_vectors.py.
|
||||||
// This is a smoke test — it does NOT compare against Python expected values
|
|
||||||
// because the RTL uses a per-permutation Keccak-p approach that differs
|
|
||||||
// from standard SHAKE-128 bit-stream semantics.
|
|
||||||
//
|
//
|
||||||
// Vector format (packed hex, 66 chars for $readmemh):
|
// Vector format (packed hex, 66 chars for $readmemh):
|
||||||
// {j_idx[1:0], i_idx[1:0], k_i[2:0], rho_i[255:0]} (264 bits → 66 hex chars)
|
// {j_idx[1:0], i_idx[1:0], k_i[2:0], rho_i[255:0]} (264 bits → 66 hex chars)
|
||||||
@@ -16,7 +13,8 @@
|
|||||||
//
|
//
|
||||||
// Prerequisites:
|
// Prerequisites:
|
||||||
// - Generate vectors: python3 TB/gen_vectors.py
|
// - Generate vectors: python3 TB/gen_vectors.py
|
||||||
// - Output file: vectors/sample_ntt_input.hex
|
// - Or refresh expected vectors: python3 TB/gen_vectors.py --refresh-expected
|
||||||
|
// - Output files: vectors/sample_ntt_input.hex, vectors/sample_ntt_expected.hex
|
||||||
|
|
||||||
`timescale 1ns / 1ps
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
@@ -26,7 +24,9 @@ module tb_sample_ntt_xsim;
|
|||||||
// Parameters
|
// Parameters
|
||||||
// ================================================================
|
// ================================================================
|
||||||
parameter VECTOR_FILE = "sync_rtl/sample_ntt/TB/vectors/sample_ntt_input.hex";
|
parameter VECTOR_FILE = "sync_rtl/sample_ntt/TB/vectors/sample_ntt_input.hex";
|
||||||
parameter MAX_VECTORS = 32; // max lines in input file
|
parameter EXPECTED_FILE = "sync_rtl/sample_ntt/TB/vectors/sample_ntt_expected.hex";
|
||||||
|
parameter MAX_VECTORS = 32; // allocation limit for input vectors
|
||||||
|
parameter VECTOR_COUNT = 5; // vectors present in VECTOR_FILE
|
||||||
parameter Q = 3329; // ML-KEM modulus
|
parameter Q = 3329; // ML-KEM modulus
|
||||||
parameter N_COEFFS = 256; // coefficients per polynomial
|
parameter N_COEFFS = 256; // coefficients per polynomial
|
||||||
parameter TIMEOUT = 500000; // per-vector timeout (cycles)
|
parameter TIMEOUT = 500000; // per-vector timeout (cycles)
|
||||||
@@ -75,6 +75,7 @@ module tb_sample_ntt_xsim;
|
|||||||
// Vector memory (loaded by $readmemh)
|
// Vector memory (loaded by $readmemh)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
reg [263:0] vector_mem [0:MAX_VECTORS-1];
|
reg [263:0] vector_mem [0:MAX_VECTORS-1];
|
||||||
|
reg [11:0] expected_mem [0:MAX_VECTORS*N_COEFFS-1];
|
||||||
integer vec_count;
|
integer vec_count;
|
||||||
integer idx;
|
integer idx;
|
||||||
integer cycle_count;
|
integer cycle_count;
|
||||||
@@ -88,27 +89,14 @@ module tb_sample_ntt_xsim;
|
|||||||
// Main test sequence
|
// Main test sequence
|
||||||
// ================================================================
|
// ================================================================
|
||||||
initial begin
|
initial begin
|
||||||
vec_count = 0;
|
|
||||||
|
|
||||||
// Load vectors from hex file
|
// Load vectors from hex file
|
||||||
$readmemh(VECTOR_FILE, vector_mem);
|
$readmemh(VECTOR_FILE, vector_mem);
|
||||||
|
$readmemh(EXPECTED_FILE, expected_mem);
|
||||||
|
|
||||||
// Count non-zero/X entries to determine actual vector count
|
vec_count = VECTOR_COUNT;
|
||||||
begin
|
if (vec_count < 1 || vec_count > MAX_VECTORS) begin
|
||||||
integer found_end;
|
$display("ERROR: VECTOR_COUNT=%0d must be in [1,%0d]",
|
||||||
found_end = 0;
|
vec_count, MAX_VECTORS);
|
||||||
for (idx = 0; idx < MAX_VECTORS; idx = idx + 1) begin
|
|
||||||
if (!found_end && (vector_mem[idx] === {264{1'bx}} || vector_mem[idx] === {264{1'bz}}))
|
|
||||||
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: <66 hex chars> = {j, i, k, rho}");
|
|
||||||
$finish;
|
$finish;
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -141,6 +129,8 @@ module tb_sample_ntt_xsim;
|
|||||||
reg [1:0] vec_i;
|
reg [1:0] vec_i;
|
||||||
reg [1:0] vec_j;
|
reg [1:0] vec_j;
|
||||||
reg range_ok;
|
reg range_ok;
|
||||||
|
reg coeff_ok;
|
||||||
|
reg last_ok;
|
||||||
|
|
||||||
// Extract fields from packed vector_mem
|
// Extract fields from packed vector_mem
|
||||||
// vector_mem[263:0] = {1'b0, j_idx, i_idx, k_i, rho_i}
|
// vector_mem[263:0] = {1'b0, j_idx, i_idx, k_i, rho_i}
|
||||||
@@ -170,18 +160,31 @@ module tb_sample_ntt_xsim;
|
|||||||
coeff_idx = 0;
|
coeff_idx = 0;
|
||||||
cycle_count = 0;
|
cycle_count = 0;
|
||||||
range_ok = 1'b1; // assume OK until proven otherwise
|
range_ok = 1'b1; // assume OK until proven otherwise
|
||||||
|
coeff_ok = 1'b1;
|
||||||
|
last_ok = 1'b1;
|
||||||
|
|
||||||
while (coeff_idx < N_COEFFS && cycle_count < TIMEOUT) begin
|
while (coeff_idx < N_COEFFS && cycle_count < TIMEOUT) begin
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
cycle_count = cycle_count + 1;
|
cycle_count = cycle_count + 1;
|
||||||
|
|
||||||
if (valid_o) begin
|
if (valid_o) begin
|
||||||
|
if (coeff_o !== expected_mem[idx*N_COEFFS + coeff_idx]) begin
|
||||||
|
$display("ERROR: Vector %0d coeff[%0d] mismatch: got=%03h expected=%03h",
|
||||||
|
idx, coeff_idx, coeff_o,
|
||||||
|
expected_mem[idx*N_COEFFS + coeff_idx]);
|
||||||
|
coeff_ok = 1'b0;
|
||||||
|
end
|
||||||
// Check range: coefficient must be in [0, Q-1]
|
// Check range: coefficient must be in [0, Q-1]
|
||||||
if (coeff_o >= Q) begin
|
if (coeff_o >= Q) begin
|
||||||
$display("ERROR: Vector %0d coeff[%0d]=%0d out of range [0,%0d]",
|
$display("ERROR: Vector %0d coeff[%0d]=%0d out of range [0,%0d]",
|
||||||
idx, coeff_idx, coeff_o, Q-1);
|
idx, coeff_idx, coeff_o, Q-1);
|
||||||
range_ok = 1'b0;
|
range_ok = 1'b0;
|
||||||
end
|
end
|
||||||
|
if (last_o !== (coeff_idx == N_COEFFS-1)) begin
|
||||||
|
$display("ERROR: Vector %0d coeff[%0d] last_o=%b expected=%b",
|
||||||
|
idx, coeff_idx, last_o, coeff_idx == N_COEFFS-1);
|
||||||
|
last_ok = 1'b0;
|
||||||
|
end
|
||||||
coeff_idx = coeff_idx + 1;
|
coeff_idx = coeff_idx + 1;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -199,8 +202,16 @@ module tb_sample_ntt_xsim;
|
|||||||
$display("ERROR: Vector %0d produced out-of-range coefficients",
|
$display("ERROR: Vector %0d produced out-of-range coefficients",
|
||||||
idx);
|
idx);
|
||||||
fail_count = fail_count + 1;
|
fail_count = fail_count + 1;
|
||||||
|
end else if (!coeff_ok) begin
|
||||||
|
$display("ERROR: Vector %0d did not match FIPS SampleNTT",
|
||||||
|
idx);
|
||||||
|
fail_count = fail_count + 1;
|
||||||
|
end else if (!last_ok) begin
|
||||||
|
$display("ERROR: Vector %0d asserted last_o on the wrong coefficient",
|
||||||
|
idx);
|
||||||
|
fail_count = fail_count + 1;
|
||||||
end else begin
|
end else begin
|
||||||
$display("INFO: Vector %0d PASSED (%0d coefficients in %0d cycles, all in range)",
|
$display("INFO: Vector %0d PASSED (%0d FIPS-matching coefficients in %0d cycles)",
|
||||||
idx, coeff_idx, cycle_count);
|
idx, coeff_idx, cycle_count);
|
||||||
pass_count = pass_count + 1;
|
pass_count = pass_count + 1;
|
||||||
end
|
end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@
|
|||||||
12F9652FD859317D04232072A19A8A4E2ABE9A8D5402819FC53C33C337C6797E4B
|
12F9652FD859317D04232072A19A8A4E2ABE9A8D5402819FC53C33C337C6797E4B
|
||||||
52CFFFE52DCB27B795B608BD58C7D52D79367553276B2B54C039E0650A0D86AFDA
|
52CFFFE52DCB27B795B608BD58C7D52D79367553276B2B54C039E0650A0D86AFDA
|
||||||
735E2459D975610B15D89165C144715F9D218BC08EE93CE6AD0B52F8FE751CAA56
|
735E2459D975610B15D89165C144715F9D218BC08EE93CE6AD0B52F8FE751CAA56
|
||||||
|
2A7484937160DBF5D93F7D69AEA2DF7C7C77B3A6B02D6B4D579CF975E6F8562097
|
||||||
|
|||||||
Reference in New Issue
Block a user