Four-scheme jamming comparison and a fully generated refresh table

Give the orthogonal-access jammer its own Rayleigh channel in
oma_ser_jammed, matching the convention every simulated scheme already
used. Without it the closed-form curve faced a jammer at full power in
every frame while the Monte Carlo curves faced a fading one, which
inverted the ordering of the comparison.

Measure the outsider error rate for the fixed-key and naive-refresh
cases as well, and emit the two refresh tables from make_tables.py, so
no cell of the paper is hand-typed.
This commit is contained in:
KiHoLee
2026-08-13 22:22:18 +09:00
parent 057c555374
commit c31e6a3fe0
17 changed files with 192 additions and 61 deletions
+63
View File
@@ -645,6 +645,68 @@ def csv_rows(path):
yield from _csv.DictReader(f)
def oma_ser_jammed(snr_db, jsr_db_list, bits=16, U=4, n_grid=4096):
"""OMA under a jammer that concentrates on the victim's slots.
An OMA user occupies d/U exclusive real dimensions that are public,
so a jammer needs no key to put all of its power there. With unit
energy per real dimension and a total jammer energy of rho times the
frame energy, concentrating on d/U of the d dimensions gives a
per-dimension jammer variance of U*rho.
The jammer reaches the victim through its own Rayleigh channel, the
same convention eval_scheme uses for every simulated scheme, so the
victim sees an effective noise variance of 1/snr + U*rho*hJ**2 with
E[hJ**2]=1. Averaging over the independent signal and jammer gains
uses a product of exponential quantile grids.
"""
q = (torch.arange(n_grid, dtype=torch.float64) + 0.5) / n_grid
h2 = -torch.log1p(-q) # |h|^2 ~ Exp(1)
hj2 = h2.clone() # |hJ|^2 ~ Exp(1), independent
h = h2.sqrt()[:, None] # (n,1) signal amplitude
snr = 10.0 ** (snr_db / 10.0)
out = []
for jsr_db in jsr_db_list:
rho = 10.0 ** (jsr_db / 10.0)
var = (1.0 / snr + U * rho * hj2)[None, :] # (1,n)
arg = (h / var.sqrt()).clamp(0, 38)
pe = 0.5 * torch.erfc(arg / math.sqrt(2.0)) # per-bit error
out.append(float((1.0 - (1.0 - pe) ** bits).mean()))
return out
def stage_L():
"""Jamming comparison across schemes at 10 dB.
proposed blind : the strongest jammer the proposed scheme admits
while the key stays secret
public matched : the jammer a public-mask scheme always faces
permutation blind: the shuffling-style scheme, whose secret
permutation also denies the jammer a target
OMA targeted : the jammer an orthogonal scheme faces, since its
slot assignment is public and needs no key
"""
print("[L] jamming across schemes ...")
m = get_model(iters=4000)
F = 300_000
d = m.P * m.L
gp = torch.Generator().manual_seed(11)
perms = torch.randperm(d, generator=gp)[None].repeat(m.users, 1)
jsr = [-10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0]
oma = oma_ser_jammed(10.0, jsr, bits=int(math.log2(m.V)), U=m.users)
rows = []
for i, j in enumerate(jsr):
blind = eval_scheme(m, 10.0, F, jam_w="blind", jsr_db=j)
matched = eval_scheme(m, 10.0, F, jam_w="matched", jsr_db=j)
perm = eval_scheme(m, 10.0, F, perms=perms, jam_w="blind", jsr_db=j)
rows.append((j, blind, matched, perm, oma[i]))
print(f" JSR={j:6.1f} blind={blind:.4f} matched={matched:.4f} "
f"perm={perm:.4f} oma={oma[i]:.4f}")
write_csv(DATA / "sec_jam_cmp.csv",
["jsr_db", "blind", "matched", "perm_blind", "oma_targeted"],
rows)
def main():
print(f"device={DEVICE}")
stage_A()
@@ -655,6 +717,7 @@ def main():
stage_F()
stage_I()
stage_J()
stage_L()
print("[done] full-scale security CSVs in", DATA)
+23 -4
View File
@@ -109,6 +109,11 @@ def main():
B0 = m.B.detach().clone().cpu()
ew = eve_wrong_mask(U, Lp, seed=20260813)
# the no-refresh reference: the trained keys, held for every block
install(m, K0, B0)
lg_fixed = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
ev_fixed = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
rows = []
for t in range(BLOCKS):
signs, colperm, userperm = kdf_invariant(SEED, t, U, Lp)
@@ -117,18 +122,32 @@ def main():
ev = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
install(m, kdf_naive(SEED, t, U, Lp), B0)
lg_naive = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
rows.append((t, lg, lg_naive, ev))
ev_naive = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
rows.append((t, lg, lg_naive, ev, ev_naive))
if t < 3 or t == BLOCKS - 1:
print(f" block {t:3d} invariant={lg:.4f} naive={lg_naive:.4f} "
f"eve={ev:.4f}")
write_csv(DATA / "refresh.csv",
["block", "legit_invariant", "legit_naive", "eve_ser"], rows)
["block", "legit_invariant", "legit_naive", "eve_invariant",
"eve_naive"], rows)
inv = [r[1] for r in rows]; nai = [r[2] for r in rows]
ev = [r[3] for r in rows]
ev = [r[3] for r in rows]; evn = [r[4] for r in rows]
print(f" invariant refresh: mean={np.mean(inv):.4f} "
f"min={min(inv):.4f} max={max(inv):.4f}")
print(f" naive refresh : mean={np.mean(nai):.4f}")
print(f" eavesdropper : mean={np.mean(ev):.5f}")
print(f" eavesdropper : mean={np.mean(ev):.5f} "
f"min={min(ev):.5f} max={max(ev):.5f}")
# the three rows of the refresh table, so no cell is hand-typed. Both
# fixed and naive draw U of the L-1 non-constant Hadamard rows.
fam = math.lgamma(Lp) / math.log(2.0) - math.lgamma(Lp - U) / math.log(2.0)
write_csv(DATA / "refresh_summary.csv",
["scheme", "legit", "eve", "entropy_bits"],
[("None (fixed key)", lg_fixed, ev_fixed, fam),
("Fresh orthogonal keys", float(np.mean(nai)),
float(np.mean(evn)), fam),
("Invariant", float(np.mean(inv)), float(np.mean(ev)),
entropy_bits(U, Lp))])
print("[K] known plaintext across a refresh ...")
kpa_rows = []
+25 -2
View File
@@ -23,7 +23,7 @@ NAME = {
}
RECEIVER = {
"legit": "Legitimate", "oma": "OMA",
"insider": "Insider", "eve": "Outsider eavesdropper",
"insider": "Insider", "eve": "Outsider",
}
@@ -73,7 +73,30 @@ def real_table():
print(f"{RECEIVER[key]} & {cells}" + r" \\")
def refresh_tables():
print("% Table: key refresh (from refresh_summary.csv)")
for r in csv.DictReader(open(DATA / "refresh_summary.csv")):
b = r["scheme"] == "Invariant"
name = r"\textbf{Invariant}" if b else r["scheme"]
f = (lambda t: r"\mathbf{" + t + "}") if b else (lambda t: t)
print(f"{name} & ${f(format(float(r['legit']), '.3f'))}$ & "
f"${f(format(float(r['eve']), '.4f'))}$ & "
f"${f(format(float(r['entropy_bits']), '.1f'))}$~bits" + r" \\")
print()
print("% Table: known plaintext across a refresh (from refresh_kpa.csv)")
rows = {r["n_frames"]: r for r in csv.DictReader(open(DATA / "refresh_kpa.csv"))}
keep = ["2", "8", "64"]
print("Frames used by the attacker & "
+ " & ".join(f"${k}$" for k in keep) + r" \\")
for lbl, key in (("Same block", "ser_same_block"),
("Next block", "ser_next_block")):
print(f"{lbl} & "
+ " & ".join(f"${float(rows[k][key]):.3f}$" for k in keep)
+ r" \\")
if __name__ == "__main__":
compare_table(); print()
maskfam_table(); print()
real_table()
real_table(); print()
refresh_tables()
+26 -15
View File
@@ -3,12 +3,16 @@ from ../data/*.csv and writes paper-ready PDFs to ../fig/. No experiment
is rerun. All result plots share one canvas and axes rectangle (8:6 box).
Label dictionary is fixed here and copied verbatim into tables and prose.
fig_sec_snr.pdf : legitimate vs eavesdropper SER vs SNR (Fig. 2)
fig_sec_snr.pdf : legitimate and outsider SER vs SNR (Fig. 2)
fig_sec_keylen.pdf : SER vs key length L (Fig. 3)
fig_sec_jam.pdf : target-user SER vs JSR (Fig. 4)
fig_sec_sens.pdf : Eve SER vs key correlation (Fig. 5)
fig_sec_brute.pdf : Eve SER vs number of key guesses (Fig. 6)
fig_sec_brute_rho.pdf : best key correlation vs guesses (Fig. 7)
fig_sec_jam.pdf : target-user SER vs JSR, four schemes (Fig. 4)
fig_sec_sens.pdf : outsider SER vs fraction of key held (Fig. 5)
fig_sec_brute.pdf : outsider SER vs number of key guesses (Fig. 6)
fig_sec_kpa.pdf : outsider SER vs known-plaintext frames (Fig. 7)
fig_sec_real.pdf : token error rate on real streams (Fig. 8)
fig_sec_brute_rho.pdf is also emitted as a diagnostic and is not used in
the paper.
"""
from __future__ import annotations
from pathlib import Path
@@ -138,19 +142,26 @@ def fig_keylen():
def fig_jam():
# the target-user SER spans 0.3 to 1.0, less than one decade, so a
# linear axis is used: a log axis here produces wide minor tick
# labels (6x10^-1) that crowd out the y label under the fixed
# axes rectangle
r = load("sec_jam.csv")
"""Target-user SER against JSR for four schemes. A linear axis is
used because the range spans less than one decade, where a log axis
would print wide minor tick labels that crowd out the y label."""
r = load("sec_jam_cmp.csv")
x = col(r, "jsr_db")
fig, ax = plt.subplots()
ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":",
label="OMA, targeted")
ax.plot(x, col(r, "matched"), color=C_MATCH, marker="P", ls="--",
label=LBL["jam_m"])
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-",
label=LBL["jam_b"])
nojam = col(r, "nojam")[0]
ax.axhline(nojam, color=C_OMA, ls=":", lw=0.9, label=LBL["nojam"])
label="Public masks, matched")
# the two blind curves agree to 0.0015, so the proposed one is drawn
# first and wide and the permutation key rides on top with open
# markers, otherwise one legend entry would have no visible curve
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-", lw=2.6,
ms=7, alpha=0.85, label="Proposed, blind")
ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.",
lw=1.2, ms=4.5, mfc="none", label="Permutation key, blind")
nojam = float(load("sec_jam.csv")[0]["nojam"])
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9,
label=LBL["nojam"])
ax.set_xlabel("JSR (dB)")
ax.set_ylabel("SER")
ax.set_xlim(min(x), max(x))