{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ex46-000",
   "metadata": {},
   "source": [
    "# Chapter 46 — Practical Exercises\n",
    "\n",
    "This sheet accompanies [Chapter 46 — *The Post-Quantum Landscape and the\n",
    "Knapsack Problem*](ch46_pqc_landscape.ipynb). Six exercises from one-liner\n",
    "to small research project.\n",
    "\n",
    "## How to use\n",
    "\n",
    "Each exercise has three parts:\n",
    "\n",
    "1. **Problem statement** — the goal, plus a difficulty marker (★ very easy\n",
    "   to ★★★★★ research project) and a link to the chapter section that\n",
    "   covers the theory.\n",
    "2. **Your turn** — a code cell with `TODO` gaps. Fill them in, run the cell,\n",
    "   and check the assertion at the bottom.\n",
    "3. **Solution** — hidden by default. Click *\"Click to show\"* to reveal a\n",
    "   fully-commented reference implementation that explains every step and\n",
    "   links back to the chapter.\n",
    "\n",
    "The gaps deliberately point at concepts the chapter teaches; doing the\n",
    "exercises is a more durable way to learn the theory than re-reading the\n",
    "text.\n",
    "\n",
    "```{admonition} Working environment\n",
    ":class: tip\n",
    "All exercises run in pure Python with the standard library plus NumPy.\n",
    "Run `pip install numpy matplotlib` once if you have not already.\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-001",
   "metadata": {},
   "source": [
    "## Exercise 46.E1 — ★ Validate a superincreasing sequence\n",
    "\n",
    "**Goal.** Write `is_superincreasing(w)` returning `True` iff\n",
    "$w_k > \\sum_{i<k} w_i$ for every $k \\ge 2$.\n",
    "\n",
    "**Theory.** [§46.5 — Superincreasing knapsacks are easy](ch46_pqc_landscape.ipynb#superincreasing-knapsacks-are-easy).\n",
    "\n",
    "**Why this matters.** Merkle–Hellman keygen requires a superincreasing\n",
    "secret sequence; if you accidentally generate one that is not, the greedy\n",
    "decryption silently returns the wrong plaintext.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ex46-002",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.279439Z",
     "iopub.status.busy": "2026-05-12T08:46:18.279271Z",
     "iopub.status.idle": "2026-05-12T08:46:18.282428Z",
     "shell.execute_reply": "2026-05-12T08:46:18.281956Z"
    }
   },
   "outputs": [],
   "source": [
    "def is_superincreasing(w):\n",
    "    '''Return True iff w_k > sum(w_i for i < k) for every k >= 2.'''\n",
    "    # TODO: walk through w accumulating a running sum and check each w_k.\n",
    "    # If any element fails the condition, return False; otherwise True.\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "# Tests.  Uncomment and run.\n",
    "# assert is_superincreasing([2, 3, 8, 17, 31, 65, 130, 258])\n",
    "# assert not is_superincreasing([2, 3, 4])           # 4 == 2+3 fails strict >\n",
    "# assert not is_superincreasing([5, 3])\n",
    "# assert is_superincreasing([1])                     # singleton trivially yes\n",
    "# print('E1 OK')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ex46-003",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.283684Z",
     "iopub.status.busy": "2026-05-12T08:46:18.283581Z",
     "iopub.status.idle": "2026-05-12T08:46:18.286307Z",
     "shell.execute_reply": "2026-05-12T08:46:18.285949Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "E1 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution.\n",
    "#\n",
    "# We walk through the sequence with a running prefix sum.  At step k we test\n",
    "# whether the current element strictly exceeds the sum of everything before it.\n",
    "#\n",
    "# Reference: §46.5 of the chapter defines the property.  The greedy decoder\n",
    "# in the same section relies critically on this STRICT inequality -- if we had\n",
    "# w_k == sum_{i<k} w_i, two distinct subsets would produce the same target sum\n",
    "# and decryption would be ambiguous.\n",
    "def is_superincreasing(w):\n",
    "    s = 0\n",
    "    for wk in w:\n",
    "        if wk <= s:\n",
    "            return False\n",
    "        s += wk\n",
    "    return True\n",
    "\n",
    "assert is_superincreasing([2, 3, 8, 17, 31, 65, 130, 258])\n",
    "assert not is_superincreasing([2, 3, 4])\n",
    "assert not is_superincreasing([5, 3])\n",
    "assert is_superincreasing([1])\n",
    "print('E1 OK')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-004",
   "metadata": {},
   "source": [
    "## Exercise 46.E2 — ★★ Decrypt a Merkle–Hellman ciphertext\n",
    "\n",
    "**Goal.** You are given a Merkle–Hellman secret key $(w, m, r)$ and a\n",
    "ciphertext $S$. Recover the plaintext bit-vector $\\mathbf{x}$.\n",
    "\n",
    "**Theory.** [§46.6 — The Merkle–Hellman trapdoor](ch46_pqc_landscape.ipynb#the-merklehellman-trapdoor).\n",
    "\n",
    "**Why this matters.** This is exactly what the recipient does on every\n",
    "incoming ciphertext. Two non-trivial steps: (1) modular un-disguise via\n",
    "$r^{-1} \\bmod m$, and (2) the greedy decode.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ex46-005",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.287795Z",
     "iopub.status.busy": "2026-05-12T08:46:18.287704Z",
     "iopub.status.idle": "2026-05-12T08:46:18.290450Z",
     "shell.execute_reply": "2026-05-12T08:46:18.290095Z"
    }
   },
   "outputs": [],
   "source": [
    "# Given key + ciphertext.\n",
    "W = [2, 7, 18, 28, 63, 127, 249, 500]   # secret superincreasing sequence\n",
    "M = 1045                                # secret modulus\n",
    "R = 789                                 # secret multiplier\n",
    "S = 1514                                # ciphertext (an ordinary integer)\n",
    "\n",
    "def egcd(a, b):\n",
    "    if b == 0: return a, 1, 0\n",
    "    g, x1, y1 = egcd(b, a % b)\n",
    "    return g, y1, x1 - (a // b) * y1\n",
    "\n",
    "def modinv(a, m):\n",
    "    g, x, _ = egcd(a, m)\n",
    "    if g != 1: raise ValueError('not invertible')\n",
    "    return x % m\n",
    "\n",
    "\n",
    "def merkle_hellman_decrypt(S, w, m, r):\n",
    "    '''Recover plaintext bits from ciphertext S given secret (w, m, r).'''\n",
    "    # TODO step 1: compute the modular inverse r_inv of r mod m.\n",
    "    # TODO step 2: compute S' = (r_inv * S) mod m.  Note: S' should now\n",
    "    #              equal sum_i x_i * w_i AS AN INTEGER -- the integer sum is\n",
    "    #              less than m by construction, so reducing mod m is benign.\n",
    "    # TODO step 3: greedy decode S' against the SUPERINCREASING w.  Return\n",
    "    #              a list of bits.\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "# Test: round-trip the published values.\n",
    "# bits = merkle_hellman_decrypt(S, W, M, R)\n",
    "# print('Recovered bits:', bits)\n",
    "# assert bits == [1, 1, 0, 1, 0, 0, 1, 1]   # the answer\n",
    "# print('E2 OK')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ex46-006",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.291561Z",
     "iopub.status.busy": "2026-05-12T08:46:18.291460Z",
     "iopub.status.idle": "2026-05-12T08:46:18.294258Z",
     "shell.execute_reply": "2026-05-12T08:46:18.293869Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Recovered bits: [1, 1, 0, 1, 0, 0, 1, 1]\n",
      "E2 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution.\n",
    "#\n",
    "# Step 1.  By construction r and m are coprime, so r has a modular inverse.\n",
    "#          We use the extended Euclidean algorithm (egcd above).  This is\n",
    "#          §46.6 of the chapter.\n",
    "#\n",
    "# Step 2.  S' = r_inv * S mod m exposes the disguise: a_i was r * w_i mod m,\n",
    "#          and S = sum_i x_i * a_i (as a plain integer).  So\n",
    "#                 r_inv * S = sum_i x_i * w_i mod m.\n",
    "#          Crucially, sum w_i < m by construction, so the right-hand side\n",
    "#          taken mod m equals the actual integer sum -- no information lost.\n",
    "#\n",
    "# Step 3.  Greedy decode the easy superincreasing instance: §46.5 again.\n",
    "#          Walk from the largest w_k downwards and include it in the subset\n",
    "#          whenever it does not exceed the remaining target.\n",
    "\n",
    "def merkle_hellman_decrypt(S, w, m, r):\n",
    "    r_inv = modinv(r, m)\n",
    "    Sp = (r_inv * S) % m                              # un-disguised target\n",
    "    n = len(w)\n",
    "    bits = [0] * n\n",
    "    rem = Sp\n",
    "    for k in range(n - 1, -1, -1):\n",
    "        if w[k] <= rem:\n",
    "            bits[k] = 1\n",
    "            rem -= w[k]\n",
    "    assert rem == 0, 'decode failed -- bad key or non-Merkle-Hellman ciphertext'\n",
    "    return bits\n",
    "\n",
    "bits = merkle_hellman_decrypt(S, W, M, R)\n",
    "print('Recovered bits:', bits)\n",
    "assert bits == [1, 1, 0, 1, 0, 0, 1, 1]\n",
    "print('E2 OK')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-007",
   "metadata": {},
   "source": [
    "## Exercise 46.E3 — ★★★ Density vs the Lagarias–Odlyzko threshold\n",
    "\n",
    "**Goal.** Implement `density(public_key)` and predict whether\n",
    "Lagarias–Odlyzko (1985) lattice reduction can break the instance.\n",
    "\n",
    "**Theory.** [§46.7 — Why Merkle–Hellman broke](ch46_pqc_landscape.ipynb#why-merklehellman-broke)\n",
    "and exercise §46.8 Ex 46.2.\n",
    "\n",
    "**Why this matters.** Density is the single most important parameter for\n",
    "predicting whether LLL will recover the plaintext. Anything below\n",
    "$d \\approx 0.94$ is generically broken in polynomial time; above 1, LLL\n",
    "expects multiple-bit collisions and the attack does not directly work.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ex46-008",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.295567Z",
     "iopub.status.busy": "2026-05-12T08:46:18.295501Z",
     "iopub.status.idle": "2026-05-12T08:46:18.297681Z",
     "shell.execute_reply": "2026-05-12T08:46:18.297392Z"
    }
   },
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "def density(a):\n",
    "    '''Knapsack density d = n / log2(max(a_i)).'''\n",
    "    # TODO: return n divided by log2 of the maximum entry of a.\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "def lo_attackable(a, threshold=0.9408):\n",
    "    '''Predict whether Lagarias-Odlyzko (and Coster et al. 1992) succeeds.'''\n",
    "    # TODO: return True iff density(a) < threshold.\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "# Toy public keys at three densities.\n",
    "PK_LOW  = [0xC2DE for _ in range(8)]                                # ~16-bit a_i, n=8 -> d~0.5\n",
    "PK_MED  = [(1 << 12) - 1 - i * 137 for i in range(8)]               # ~12-bit a_i -> d~0.67\n",
    "PK_HIGH = [3 + i for i in range(8)]                                 # tiny a_i -> d ~ 2\n",
    "\n",
    "# print('low  d =', density(PK_LOW),  ' attackable =', lo_attackable(PK_LOW))\n",
    "# print('med  d =', density(PK_MED),  ' attackable =', lo_attackable(PK_MED))\n",
    "# print('high d =', density(PK_HIGH), ' attackable =', lo_attackable(PK_HIGH))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "ex46-009",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.298776Z",
     "iopub.status.busy": "2026-05-12T08:46:18.298705Z",
     "iopub.status.idle": "2026-05-12T08:46:18.301147Z",
     "shell.execute_reply": "2026-05-12T08:46:18.300807Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "low  d = 0.5  attackable = True\n",
      "med  d = 0.667  attackable = True\n",
      "high d = 2.0  attackable = False\n",
      "E3 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution.\n",
    "#\n",
    "# Density is just the bit budget per knapsack entry: n / log_2(max a_i).\n",
    "# Lagarias-Odlyzko 1985 proved an LLL-based attack succeeds for d < 0.6463;\n",
    "# Coster, Joux, LaMacchia, Odlyzko, Schnorr, Stern (1992) raised this to\n",
    "# d < 0.9408.  See the chapter §46.7.\n",
    "#\n",
    "# Note we use ceil(log2) -- a power-of-two max would otherwise be off by one.\n",
    "\n",
    "def density(a):\n",
    "    return len(a) / math.ceil(math.log2(max(a)))\n",
    "\n",
    "def lo_attackable(a, threshold=0.9408):\n",
    "    return density(a) < threshold\n",
    "\n",
    "print('low  d =', round(density(PK_LOW), 3),  ' attackable =', lo_attackable(PK_LOW))\n",
    "print('med  d =', round(density(PK_MED), 3),  ' attackable =', lo_attackable(PK_MED))\n",
    "print('high d =', round(density(PK_HIGH), 3), ' attackable =', lo_attackable(PK_HIGH))\n",
    "print('E3 OK')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-010",
   "metadata": {},
   "source": [
    "## Exercise 46.E4 — ★★★ Brute force vs greedy: timing comparison\n",
    "\n",
    "**Goal.** Implement an exhaustive $O(2^n)$ subset-sum solver and time it\n",
    "against the greedy decoder on superincreasing instances of growing $n$.\n",
    "\n",
    "**Theory.** [§46.4 — The subset-sum / 0-1 knapsack problem](ch46_pqc_landscape.ipynb#the-subset-sum-0-1-knapsack-problem).\n",
    "\n",
    "**Why this matters.** Convince yourself, with concrete timings, that the\n",
    "NP-hard generic problem and the $O(n)$ trapdoored problem really are in\n",
    "totally different complexity classes.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "ex46-011",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.302216Z",
     "iopub.status.busy": "2026-05-12T08:46:18.302143Z",
     "iopub.status.idle": "2026-05-12T08:46:18.304504Z",
     "shell.execute_reply": "2026-05-12T08:46:18.304160Z"
    }
   },
   "outputs": [],
   "source": [
    "import time, itertools\n",
    "\n",
    "def brute_force_subset_sum(a, S):\n",
    "    '''Try every subset; return the bit-vector matching S, or None.'''\n",
    "    # TODO: enumerate all 2^n bit-vectors and pick the first whose dot product\n",
    "    # with `a` equals S.  itertools.product([0,1], repeat=n) is one way.\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "def greedy_subset_sum(w, S):\n",
    "    bits = [0] * len(w)\n",
    "    rem = S\n",
    "    for k in range(len(w) - 1, -1, -1):\n",
    "        if w[k] <= rem:\n",
    "            bits[k] = 1\n",
    "            rem -= w[k]\n",
    "    return bits if rem == 0 else None\n",
    "\n",
    "\n",
    "# Generate a superincreasing sequence and time both solvers for growing n.\n",
    "def superincr(n):\n",
    "    w = [2]\n",
    "    while len(w) < n:\n",
    "        w.append(sum(w) + 1)\n",
    "    return w\n",
    "\n",
    "# for n in (8, 12, 16, 20):\n",
    "#     w = superincr(n)\n",
    "#     S = sum(w[:n:2])           # take the even-indexed weights\n",
    "#     t0 = time.time(); b1 = brute_force_subset_sum(w, S); tb = time.time() - t0\n",
    "#     t0 = time.time(); b2 = greedy_subset_sum(w, S);      tg = time.time() - t0\n",
    "#     print(f'n={n:2d}  brute = {tb*1000:7.1f} ms   greedy = {tg*1000:6.3f} ms   match={b1==b2}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ex46-012",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.305620Z",
     "iopub.status.busy": "2026-05-12T08:46:18.305551Z",
     "iopub.status.idle": "2026-05-12T08:46:18.844104Z",
     "shell.execute_reply": "2026-05-12T08:46:18.843785Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "n= 8  brute =     0.1 ms   greedy =  0.003 ms   match=True\n",
      "n=12  brute =     1.7 ms   greedy =  0.002 ms   match=True\n",
      "n=16  brute =    29.2 ms   greedy =  0.005 ms   match=True\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "n=20  brute =   504.5 ms   greedy =  0.011 ms   match=True\n",
      "E4 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution.\n",
    "#\n",
    "# Brute force enumerates all 2^n subsets.  itertools.product is the cleanest\n",
    "# implementation; for serious benchmarking you would prefer a meet-in-the-\n",
    "# middle algorithm (Schroeppel-Shamir 1981) which runs in O(2^{n/2}) time and\n",
    "# space.\n",
    "#\n",
    "# Empirically you should see brute-force time roughly DOUBLE per +1 in n,\n",
    "# while greedy stays essentially flat -- the textbook exponential vs linear\n",
    "# divide.  This is exactly the gap that motivates the trapdoor-knapsack idea\n",
    "# (chapter §46.6) and exactly what makes that idea fragile (§46.7).\n",
    "\n",
    "def brute_force_subset_sum(a, S):\n",
    "    n = len(a)\n",
    "    for bits in itertools.product([0, 1], repeat=n):\n",
    "        if sum(b * ai for b, ai in zip(bits, a)) == S:\n",
    "            return list(bits)\n",
    "    return None\n",
    "\n",
    "for n in (8, 12, 16, 20):\n",
    "    w = superincr(n)\n",
    "    S = sum(w[:n:2])\n",
    "    t0 = time.time(); b1 = brute_force_subset_sum(w, S); tb = time.time() - t0\n",
    "    t0 = time.time(); b2 = greedy_subset_sum(w, S);      tg = time.time() - t0\n",
    "    print(f'n={n:2d}  brute = {tb*1000:7.1f} ms   greedy = {tg*1000:6.3f} ms   match={b1==b2}')\n",
    "\n",
    "print('E4 OK')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-013",
   "metadata": {},
   "source": [
    "## Exercise 46.E5 — ★★★★ Build the Lagarias–Odlyzko lattice + a tiny LLL\n",
    "\n",
    "**Goal.** Construct the Lagarias–Odlyzko (LO) lattice for a given\n",
    "public-key / ciphertext pair, run a NumPy implementation of LLL on it,\n",
    "and decode the bit-vector from the resulting short vector.\n",
    "\n",
    "**Theory.** [§46.7 — Why Merkle–Hellman broke](ch46_pqc_landscape.ipynb#why-merklehellman-broke)\n",
    "and the *upstream* cryptanalysis chapter Ch 40 (§40.3–40.7) for the LLL\n",
    "algorithm itself.\n",
    "\n",
    "**Why this matters.** This is the W2 lab in miniature. The LO lattice is\n",
    "the bridge from \"knapsack as a hard problem\" to \"LLL as a near-universal\n",
    "cryptanalytic hammer\".\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "ex46-014",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.845667Z",
     "iopub.status.busy": "2026-05-12T08:46:18.845565Z",
     "iopub.status.idle": "2026-05-12T08:46:18.869799Z",
     "shell.execute_reply": "2026-05-12T08:46:18.869304Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "def build_LO_lattice(a, S):\n",
    "    '''Lagarias-Odlyzko lattice: returns an (n+1) x (n+1) integer basis B.\n",
    "\n",
    "    Rows are the basis vectors; the last column carries the sum constraint.\n",
    "    A short vector with last coord 0 has front entries +/- 1 encoding bits.\n",
    "    '''\n",
    "    n = len(a)\n",
    "    # TODO: build B with the following pattern (rows are vectors):\n",
    "    #   for i in 0..n-1:  row i = 2 * e_i  +  2 * a_i * e_n\n",
    "    #   for i = n      :  row n = 1_n      +  2 * S    * e_n\n",
    "    # Return as a 2D NumPy array with dtype=object (so coefficients can be big ints).\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "def lll(B, delta=0.75):\n",
    "    '''Plain LLL on integer-row basis B (NumPy 2D array).  Returns reduced B.'''\n",
    "    B = np.array(B, dtype=object)\n",
    "    n = B.shape[0]\n",
    "    def gso():\n",
    "        Bf = B.astype(float)\n",
    "        Q  = np.zeros_like(Bf)\n",
    "        mu = np.zeros((n, n))\n",
    "        for i in range(n):\n",
    "            Q[i] = Bf[i].copy()\n",
    "            for j in range(i):\n",
    "                denom = float(Q[j] @ Q[j])\n",
    "                mu[i, j] = float(Bf[i] @ Q[j]) / denom if denom else 0.0\n",
    "                Q[i] -= mu[i, j] * Q[j]\n",
    "        return Q, mu\n",
    "    def size_reduce(k, mu):\n",
    "        for j in range(k - 1, -1, -1):\n",
    "            q = round(mu[k, j])\n",
    "            if q:\n",
    "                B[k] = B[k] - q * B[j]\n",
    "                mu[k, :j+1] -= q * mu[j, :j+1]\n",
    "                mu[k, j]   -= q\n",
    "    Q, mu = gso()\n",
    "    k = 1\n",
    "    iters = 0\n",
    "    while k < n and iters < 1000 * n:\n",
    "        size_reduce(k, mu)\n",
    "        if (Q[k] @ Q[k]) >= (delta - mu[k, k-1]**2) * (Q[k-1] @ Q[k-1]):\n",
    "            k += 1\n",
    "        else:\n",
    "            B[[k, k-1]] = B[[k-1, k]]\n",
    "            Q, mu = gso()\n",
    "            k = max(k - 1, 1)\n",
    "        iters += 1\n",
    "    return B\n",
    "\n",
    "\n",
    "def bits_from_short_vec(v, n):\n",
    "    out = []\n",
    "    for x in v[:n]:\n",
    "        if   int(x) ==  1: out.append(0)\n",
    "        elif int(x) == -1: out.append(1)\n",
    "        else: return None\n",
    "    return out\n",
    "\n",
    "\n",
    "def attack_merkle_hellman(a, S):\n",
    "    B = build_LO_lattice(a, S)\n",
    "    Bred = lll(B)\n",
    "    for row in Bred:\n",
    "        if int(row[-1]) != 0: continue\n",
    "        b = bits_from_short_vec(row, len(a))\n",
    "        if b is not None: return b\n",
    "        bneg = bits_from_short_vec(-row, len(a))\n",
    "        if bneg is not None: return bneg\n",
    "    return None\n",
    "\n",
    "\n",
    "# Build a low-density instance and break it.\n",
    "# from random import Random\n",
    "# rnd = Random(42)\n",
    "# n = 10\n",
    "# w = [rnd.randrange(2, 10)]\n",
    "# for _ in range(n - 1):\n",
    "#     w.append(sum(w) + rnd.randrange(1, 10))\n",
    "# m = (1 << 32) + rnd.randrange(1, 1024); r = 17  # tiny example\n",
    "# while True:\n",
    "#     try: r_inv = pow(r, -1, m); break\n",
    "#     except ValueError: r += 2\n",
    "# pk = [(r * wi) % m for wi in w]\n",
    "# bits = [rnd.randrange(2) for _ in range(n)]\n",
    "# S = sum(b * ai for b, ai in zip(bits, pk))\n",
    "# rec = attack_merkle_hellman(pk, S)\n",
    "# print('density =', n / max(pk).bit_length())\n",
    "# print('truth     =', bits)\n",
    "# print('recovered =', rec)\n",
    "# assert rec == bits\n",
    "# print('E5 OK')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ex46-015",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.870911Z",
     "iopub.status.busy": "2026-05-12T08:46:18.870829Z",
     "iopub.status.idle": "2026-05-12T08:46:18.878179Z",
     "shell.execute_reply": "2026-05-12T08:46:18.877848Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "density   = 0.667\n",
      "truth     = [0, 0, 0, 0, 0, 0, 1, 0, 1, 1]\n",
      "recovered = [0, 0, 0, 0, 0, 0, 1, 0, 1, 1]\n",
      "E5 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution.\n",
    "#\n",
    "# The lattice we build is the standard Lagarias-Odlyzko construction (1985):\n",
    "#\n",
    "#     b_i (i<n) = (0,...,2,...,0, 2*a_i)        <- 2 in position i, 2*a_i last\n",
    "#     b_n       = (1, 1, ..., 1, 2*S)\n",
    "#\n",
    "# A vector v in the lattice is a Z-linear combination sum_i c_i b_i.  Setting\n",
    "# the last coordinate to zero forces 2*sum_i c_i a_i + 2*c_n*S = 0 with c_n =\n",
    "# +/- 1; solving gives c_i = -+ x_i where x is the plaintext bit-vector, and\n",
    "# the front entries become 2c_i + c_n = +/- 1, with sign matching whether the\n",
    "# bit is 0 (+1) or 1 (-1).\n",
    "#\n",
    "# The chapter §46.7 sketches this, and the upstream cryptanalysis course\n",
    "# Chapter 40 has the full LLL treatment.\n",
    "\n",
    "def build_LO_lattice(a, S):\n",
    "    n = len(a)\n",
    "    B = np.zeros((n + 1, n + 1), dtype=object)\n",
    "    for i in range(n):\n",
    "        B[i, i] = 2\n",
    "        B[i, n] = 2 * a[i]\n",
    "    for j in range(n):\n",
    "        B[n, j] = 1\n",
    "    B[n, n] = 2 * S\n",
    "    return B\n",
    "\n",
    "# Same lll / bits_from_short_vec / attack helpers as the \"your turn\" cell.\n",
    "\n",
    "from random import Random\n",
    "rnd = Random(42)\n",
    "n = 10\n",
    "w = [rnd.randrange(2, 10)]\n",
    "for _ in range(n - 1):\n",
    "    w.append(sum(w) + rnd.randrange(1, 10))\n",
    "m = (1 << 32) + rnd.randrange(1, 1024); r = 17\n",
    "while True:\n",
    "    try: r_inv = pow(r, -1, m); break\n",
    "    except ValueError: r += 2\n",
    "pk = [(r * wi) % m for wi in w]\n",
    "bits = [rnd.randrange(2) for _ in range(n)]\n",
    "S = sum(b * ai for b, ai in zip(bits, pk))\n",
    "rec = attack_merkle_hellman(pk, S)\n",
    "print('density   =', round(n / max(pk).bit_length(), 3))\n",
    "print('truth     =', bits)\n",
    "print('recovered =', rec)\n",
    "assert rec == bits\n",
    "print('E5 OK')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ex46-016",
   "metadata": {},
   "source": [
    "## Exercise 46.E6 — ★★★★★ Research: density vs LLL success rate\n",
    "\n",
    "**Goal.** Empirically map the **success rate of the Lagarias–Odlyzko\n",
    "attack** as a function of knapsack density. Generate $T = 50$ random\n",
    "Merkle–Hellman instances at each density value $d \\in\n",
    "\\{0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0\\}$, run your `attack_merkle_hellman`\n",
    "from E5, and plot the recovery rate.\n",
    "\n",
    "**Theory.** Lagarias–Odlyzko 1985, refined by Coster–Joux–LaMacchia–Odlyzko–\n",
    "Schnorr–Stern 1992 (chapter §46.7 + the upstream Ch 40).\n",
    "\n",
    "**Why this matters.** This is exactly the kind of empirical question every\n",
    "PQC paper has to answer for its own scheme: \"*at what parameter regime\n",
    "does the best known attack succeed, and where is the margin?*\". The\n",
    "Coster et al. (1992) bound says success below $d \\approx 0.94$, but\n",
    "real-world LLL is implementation-dependent — measure your own.\n",
    "\n",
    "```{admonition} Open-ended\n",
    ":class: note\n",
    "This is a small research project. Variations to try: vary $n$ at fixed\n",
    "$d$; replace LLL with BKZ-2; replace the LO lattice with the\n",
    "Coster et al. 1992 variant. Bring plots to office hours.\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "ex46-017",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.879097Z",
     "iopub.status.busy": "2026-05-12T08:46:18.879027Z",
     "iopub.status.idle": "2026-05-12T08:46:18.880700Z",
     "shell.execute_reply": "2026-05-12T08:46:18.880406Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from random import Random\n",
    "\n",
    "def random_mh_instance(n, density_target, rnd):\n",
    "    '''Return (pk, ct, bits) at a target density.\n",
    "\n",
    "    Density d ~ n / log2(max a_i).  We pick the modulus m ~ 2^(n/d) and\n",
    "    the multiplier r at random coprime to m, then publish a_i = r * w_i mod m.\n",
    "    '''\n",
    "    # TODO step 1: generate a superincreasing secret sequence w of length n.\n",
    "    # TODO step 2: compute the modulus m from the density target.\n",
    "    # TODO step 3: pick a multiplier r coprime to m.\n",
    "    # TODO step 4: build pk and a random plaintext, return (pk, ciphertext, bits).\n",
    "    raise NotImplementedError('your turn')\n",
    "\n",
    "\n",
    "# Scan densities, run your attack from E5, plot recovery rate.\n",
    "# (Reuse `attack_merkle_hellman` from E5 in the same notebook.)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ex46-018",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T08:46:18.881584Z",
     "iopub.status.busy": "2026-05-12T08:46:18.881522Z",
     "iopub.status.idle": "2026-05-12T08:46:21.011025Z",
     "shell.execute_reply": "2026-05-12T08:46:21.010666Z"
    },
    "tags": [
     "hide-cell"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "d = 0.20   success 30/30 = 100%\n",
      "d = 0.30   success 30/30 = 100%\n",
      "d = 0.40   success 30/30 = 100%\n",
      "d = 0.50   success 30/30 = 100%\n",
      "d = 0.60   success 30/30 = 100%\n",
      "d = 0.70   success 30/30 = 100%\n",
      "d = 0.80   success 30/30 = 100%\n",
      "d = 0.90   success 30/30 = 100%\n",
      "d = 1.00   success 30/30 = 100%\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 600x320 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "E6 OK\n"
     ]
    }
   ],
   "source": [
    "# Solution sketch.\n",
    "#\n",
    "# The dominant subtlety is computing the modulus to land at a target density.\n",
    "# For an n-element key with d = n / log2(max(a_i)) and max(a_i) ~ m, we want\n",
    "# m ~ 2^(n/d).  But m must also exceed sum(w); we take m = max(2^(n/d), sum(w))\n",
    "# plus a small random offset.\n",
    "#\n",
    "# Plotting matplotlib in this notebook requires `pip install matplotlib`.\n",
    "\n",
    "import math\n",
    "import matplotlib.pyplot as plt   # noqa: only used in the solution\n",
    "\n",
    "def random_mh_instance(n, density_target, rnd):\n",
    "    # superincreasing secret w\n",
    "    w = [rnd.randrange(2, 10)]\n",
    "    while len(w) < n:\n",
    "        w.append(sum(w) + rnd.randrange(1, 10))\n",
    "    target_max_log = max(int(round(n / density_target)), int(math.log2(sum(w))) + 1)\n",
    "    m = max(sum(w) + rnd.randrange(1, 100), 1 << target_max_log)\n",
    "    while True:\n",
    "        r = rnd.randrange(2, m)\n",
    "        if math.gcd(r, m) == 1: break\n",
    "    pk = [(r * wi) % m for wi in w]\n",
    "    bits = [rnd.randrange(2) for _ in range(n)]\n",
    "    S = sum(b * ai for b, ai in zip(bits, pk))\n",
    "    return pk, S, bits\n",
    "\n",
    "def success_rate(n, d, T, rnd):\n",
    "    wins = 0\n",
    "    for _ in range(T):\n",
    "        pk, S, bits = random_mh_instance(n, d, rnd)\n",
    "        rec = attack_merkle_hellman(pk, S)\n",
    "        if rec == bits:\n",
    "            wins += 1\n",
    "    return wins / T\n",
    "\n",
    "# Reduce T at higher n for runtime.\n",
    "n = 10\n",
    "T = 30\n",
    "ds  = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]\n",
    "rnd = Random(2026)\n",
    "rates = [success_rate(n, d, T, rnd) for d in ds]\n",
    "for d, p in zip(ds, rates):\n",
    "    print(f'd = {d:.2f}   success {int(p*T)}/{T} = {p:.0%}')\n",
    "\n",
    "# Plot.\n",
    "fig, ax = plt.subplots(figsize=(6, 3.2))\n",
    "ax.plot(ds, rates, marker='o', color='#4f46e5')\n",
    "ax.axvline(0.9408, color='#dc2626', linestyle='--', label='Coster et al. 1992 threshold')\n",
    "ax.set_xlabel('density d')\n",
    "ax.set_ylabel(f'LLL recovery rate (n={n}, T={T})')\n",
    "ax.set_ylim(-0.05, 1.05); ax.grid(True, alpha=0.3); ax.legend()\n",
    "ax.set_title('Lagarias-Odlyzko attack success vs. knapsack density')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "print('E6 OK')\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
