test(top): two-instance hello_world TB (genenc + dec split)

Add tb_mlkem_two_inst_xsim: runs the hello_world protocol across TWO mlkem_top
instances, modeling the real two-party split:
  * u_genenc: KeyGen THEN Encaps on one instance. KeyGen writes ek into its own
    ek_bram and Encaps reuses it directly (no re-streaming) -> shared_key, ct.
  * u_dec: Decaps on a separate instance, receiving dk + ct streamed over from
    u_genenc via the input ports.

Verifies A.shared_key == B.recovered_key and 'hello world' round-trips. Output
matches the single-instance TB and the Rust reference (key=ced0c031a4bee34a...).

run_hello.sh gains a 'two' arg to select this TB; default stays single-instance.
This commit is contained in:
2026-06-29 22:15:39 +08:00
parent f27922270a
commit ee2bf1cda8
2 changed files with 182 additions and 4 deletions

View File

@@ -1,16 +1,25 @@
#!/usr/bin/env bash
# run_hello.sh - compile + run the ML-KEM hello_world hardware testbench.
# run_hello.sh - compile + run an ML-KEM hello_world hardware testbench.
# ./run_hello.sh single-instance protocol (default)
# ./run_hello.sh two two-instance (genenc + dec) protocol
set -e
cd "$(dirname "$0")"
source /opt/Xilinx/Vivado/2019.2/settings64.sh >/dev/null 2>&1
export LD_PRELOAD="/usr/lib64/libtinfo.so.5"
rm -rf xsim.dir .Xil
if [ "$1" = "two" ]; then
TB=tb_mlkem_two_inst_xsim; SNAP=mlkem_two
else
TB=tb_mlkem_hello_world_xsim; SNAP=mlkem_hello
fi
# compile the RTL (every non-TB xvlog line from the shared tcl)
grep -E '^xvlog ' sync_rtl/top/TB/xsim_run.tcl | grep -v 'TB/tb_' | while read -r cmd; do
eval "$cmd" >/dev/null
done
xvlog -sv --relax sync_rtl/top/TB/tb_mlkem_hello_world_xsim.v >/dev/null
xelab tb_mlkem_hello_world_xsim -s mlkem_hello --timescale 1ns/1ps >/dev/null 2>&1
xsim mlkem_hello -R 2>/dev/null | grep -vE '^(#|INFO|Time resolution|run -all|exit|xsim|Vivado|====.*Simulator|SW Build|IP Build|Copyright|Tool Version|Start of|source |## )' | sed '/^$/N;/^\n$/D'
xvlog -sv --relax sync_rtl/top/TB/$TB.v >/dev/null
xelab $TB -s $SNAP --timescale 1ns/1ps >/dev/null 2>&1
xsim $SNAP -R 2>/dev/null | grep -vE '^(#|INFO|Time resolution|run -all|exit|xsim|Vivado|====.*Simulator|SW Build|IP Build|Copyright|Tool Version|Start of|source |## )' | sed '/^$/N;/^\n$/D'

View File

@@ -0,0 +1,169 @@
// tb_mlkem_two_inst_xsim.v - ML-KEM hello_world on TWO mlkem_top instances.
//
// Models the real two-party split:
// * Instance A (u_genenc): Alice/Bob side that does KeyGen THEN Encaps.
// - KeyGen(d,z) writes ek into A's own ek_bram and produces dk.
// - Encaps(m) reuses that same ek_bram (no re-streaming) -> shared_key, ct.
// * Instance B (u_dec): Alice side that does Decaps.
// - receives dk (read out of A via dbg_dk) and ct (read out of A via
// dbg_ct), streamed into B's input ports -> recovered_key.
//
// Then XOR-encrypt/decrypt "hello world" and verify both keys + message match.
// Each step prints its inputs and outputs.
//
// xelab tb_mlkem_two_inst_xsim ; xsim
`timescale 1ns/1ps
module tb_mlkem_two_inst_xsim;
localparam KP = 2; // ML-KEM-512
localparam EKB = 384*KP + 32; // 800
localparam DKB = 768*KP + 96; // 1632
localparam CTB = 32*(10*KP + 4); // 768
localparam MLEN = 11; // "hello world"
reg clk=0;
always #5 clk = ~clk;
// ---------------- Instance A: KeyGen + Encaps ----------------
reg a_rst_n=0, a_start=0;
reg [1:0] a_op=0;
reg [255:0] a_d=0, a_z=0, a_msg=0;
wire a_busy, a_done;
wire [255:0] a_ss;
reg [10:0] a_ct_idx=0; wire [7:0] a_ct_o;
reg a_byte_sel=0; reg [10:0] a_byte_idx=0; wire [7:0] a_byte_o;
reg [11:0] a_dk_idx=0; wire [7:0] a_dk_o;
wire [255:0] a_rho,a_sigma,a_r,a_hek,a_mp,a_kbar,a_dz,a_dh;
mlkem_top u_genenc (
.clk(clk), .rst_n(a_rst_n), .k_i(KP[2:0]), .op_i(a_op),
.d_i(a_d), .z_i(a_z), .msg_i(a_msg), .start_i(a_start),
.busy_o(a_busy), .done_o(a_done),
.ek_in_we(1'b0), .ek_in_addr(11'd0), .ek_in_byte(8'd0),
.dk_in_we(1'b0), .dk_in_addr(12'd0), .dk_in_byte(8'd0),
.c_in_we(1'b0), .c_in_addr(11'd0), .c_in_byte(8'd0),
.ss_o(a_ss), .dbg_ct_idx_i(a_ct_idx), .dbg_ct_o(a_ct_o),
.dbg_slot_i(6'd0), .dbg_idx_i(8'd0), .dbg_coeff_o(),
.dbg_byte_sel_i(a_byte_sel), .dbg_byte_idx_i(a_byte_idx), .dbg_byte_o(a_byte_o),
.dbg_dk_idx_i(a_dk_idx), .dbg_dk_o(a_dk_o),
.dbg_rho_o(a_rho), .dbg_sigma_o(a_sigma), .dbg_r_o(a_r), .dbg_hek_o(a_hek),
.dbg_mprime_o(a_mp), .dbg_kbar_o(a_kbar), .dbg_decz_o(a_dz), .dbg_dech_o(a_dh)
);
// ---------------- Instance B: Decaps ----------------
reg b_rst_n=0, b_start=0;
wire b_busy, b_done;
wire [255:0] b_ss;
reg b_dk_we=0; reg [11:0] b_dk_addr=0; reg [7:0] b_dk_byte=0;
reg b_c_we=0; reg [10:0] b_c_addr=0; reg [7:0] b_c_byte=0;
wire [255:0] b_rho,b_sigma,b_r,b_hek,b_mp,b_kbar,b_dz,b_dh;
wire [7:0] b_byte_o, b_dk_o, b_ct_o;
mlkem_top u_dec (
.clk(clk), .rst_n(b_rst_n), .k_i(KP[2:0]), .op_i(2'd2),
.d_i(256'd0), .z_i(256'd0), .msg_i(256'd0), .start_i(b_start),
.busy_o(b_busy), .done_o(b_done),
.ek_in_we(1'b0), .ek_in_addr(11'd0), .ek_in_byte(8'd0),
.dk_in_we(b_dk_we), .dk_in_addr(b_dk_addr), .dk_in_byte(b_dk_byte),
.c_in_we(b_c_we), .c_in_addr(b_c_addr), .c_in_byte(b_c_byte),
.ss_o(b_ss), .dbg_ct_idx_i(11'd0), .dbg_ct_o(b_ct_o),
.dbg_slot_i(6'd0), .dbg_idx_i(8'd0), .dbg_coeff_o(),
.dbg_byte_sel_i(1'b0), .dbg_byte_idx_i(11'd0), .dbg_byte_o(b_byte_o),
.dbg_dk_idx_i(12'd0), .dbg_dk_o(b_dk_o),
.dbg_rho_o(b_rho), .dbg_sigma_o(b_sigma), .dbg_r_o(b_r), .dbg_hek_o(b_hek),
.dbg_mprime_o(b_mp), .dbg_kbar_o(b_kbar), .dbg_decz_o(b_dz), .dbg_dech_o(b_dh)
);
// shuttle storage (the wire between the two instances)
reg [7:0] dk_b [0:DKB-1];
reg [7:0] ct_b [0:CTB-1];
reg [7:0] msg_b [0:MLEN-1];
reg [7:0] enc_b [0:MLEN-1];
reg [7:0] dec_b [0:MLEN-1];
reg [255:0] shared_key, recovered_key;
integer c, i, j, errors;
task wait_a; begin
c=0; while(!a_done && c<4000000) begin @(posedge clk); c=c+1; end
if(!a_done) begin $display("FAIL: instance A timeout (op=%0d)", a_op); $finish; end
end endtask
task wait_b; begin
c=0; while(!b_done && c<4000000) begin @(posedge clk); c=c+1; end
if(!b_done) begin $display("FAIL: instance B timeout"); $finish; end
end endtask
task print_hex32; input [255:0] v; begin
for (j=0;j<32;j=j+1) $write("%02x", v[8*j +: 8]); $write("\n");
end endtask
initial begin
errors = 0;
msg_b[0]="h"; msg_b[1]="e"; msg_b[2]="l"; msg_b[3]="l"; msg_b[4]="o";
msg_b[5]=" "; msg_b[6]="w"; msg_b[7]="o"; msg_b[8]="r"; msg_b[9]="l"; msg_b[10]="d";
$display("=== ML-KEM hello_world (TWO instances: genenc + dec, ML-KEM-512) ===");
$write("Original: \""); for(i=0;i<MLEN;i=i+1) $write("%c", msg_b[i]); $display("\"\n");
a_rst_n=0; b_rst_n=0; repeat(4) @(posedge clk); a_rst_n=1; b_rst_n=1; @(posedge clk);
// ===== Step 1: Instance A KeyGen(d, z) =====
a_d = {32{8'h42}}; a_z = {32{8'h77}};
$display("[A.1] KeyGen");
$write(" in d = "); print_hex32(a_d);
$write(" in z = "); print_hex32(a_z);
a_op=2'd0; a_start=1; @(posedge clk); a_start=0; wait_a;
// read dk out of A (to hand to instance B)
for (i=0;i<DKB;i=i+1) begin a_dk_idx=i[11:0]; @(posedge clk); @(posedge clk); dk_b[i]=a_dk_o; end
$display(" out ek = %0d B (stays in A's ek_bram); dk = %0d B (dk[0:8]=%02x %02x %02x %02x %02x %02x %02x %02x ...)",
EKB, DKB, dk_b[0],dk_b[1],dk_b[2],dk_b[3],dk_b[4],dk_b[5],dk_b[6],dk_b[7]);
$display(" (cycles: %0d)\n", c);
// ===== Step 2: Instance A Encaps(ek, m) =====
// ek already lives in A's ek_bram from KeyGen; no re-streaming needed.
a_msg = {32{8'hDE}};
$display("[A.2] Encaps (reuses ek_bram from KeyGen)");
$write(" in m = "); print_hex32(a_msg);
a_op=2'd1; a_start=1; @(posedge clk); a_start=0; wait_a;
shared_key = a_ss;
for (i=0;i<CTB;i=i+1) begin a_ct_idx=i[10:0]; @(posedge clk); @(posedge clk); ct_b[i]=a_ct_o; end
$write(" out shared_key = "); print_hex32(shared_key);
$display(" out kem_ct = %0d B (ct[0:8]=%02x %02x %02x %02x %02x %02x %02x %02x ...)",
CTB, ct_b[0],ct_b[1],ct_b[2],ct_b[3],ct_b[4],ct_b[5],ct_b[6],ct_b[7]);
$display(" (cycles: %0d)\n", c);
// ===== Step 3: Bob XOR-encrypt =====
for (i=0;i<MLEN;i=i+1) enc_b[i] = msg_b[i] ^ shared_key[8*(i%32) +: 8];
$display("[3] encrypt \"hello world\" (XOR stream, demo)");
$write(" out encrypted = "); for(i=0;i<MLEN;i=i+1) $write("%02x ", enc_b[i]); $write("\n\n");
// ===== Step 4: Instance B Decaps(dk, kem_ct) =====
// hand dk + ct from A to B over the streaming input ports
for (i=0;i<DKB;i=i+1) begin b_dk_we=1; b_dk_addr=i[11:0]; b_dk_byte=dk_b[i]; @(posedge clk); end
b_dk_we=0;
for (i=0;i<CTB;i=i+1) begin b_c_we=1; b_c_addr=i[10:0]; b_c_byte=ct_b[i]; @(posedge clk); end
b_c_we=0; @(posedge clk);
$display("[B.1] Decaps (separate instance)");
$display(" in dk = %0d B, kem_ct = %0d B (received from instance A)", DKB, CTB);
b_start=1; @(posedge clk); b_start=0; wait_b;
recovered_key = b_ss;
$write(" out recovered_key = "); print_hex32(recovered_key);
$display(" (cycles: %0d)\n", c);
// ===== Step 5: Alice XOR-decrypt =====
for (i=0;i<MLEN;i=i+1) dec_b[i] = enc_b[i] ^ recovered_key[8*(i%32) +: 8];
$write("[5] decrypt -> \""); for(i=0;i<MLEN;i=i+1) $write("%c", dec_b[i]); $display("\"\n");
// ===== Step 6: Verify =====
if (shared_key !== recovered_key) begin
$display("FAIL: shared_key (A) != recovered_key (B)"); errors=errors+1;
end
for (i=0;i<MLEN;i=i+1) if (dec_b[i] !== msg_b[i]) errors=errors+1;
if (errors==0)
$display("Success: A.shared_key == B.recovered_key, message recovered across two instances.");
else
$display("FAILURE: %0d mismatches", errors);
$finish;
end
initial begin #400000000; $display("FAIL: global timeout"); $finish; end
endmodule