🦀 Functional Rust

833: Discrete Logarithm — Baby-Step Giant-Step

Difficulty: 5 Level: Master Solve a^x ≡ b (mod p) in O(√p) time and space — meet-in-the-middle applied to cyclic groups.

The Problem This Solves

The discrete logarithm problem (DLP): given a, b, and prime p, find x such that a^x ≡ b (mod p). This is the computational hard problem underlying Diffie-Hellman key exchange, ElGamal encryption, and the Digital Signature Algorithm (DSA). While modular exponentiation is fast (O(log x)), the inverse — finding x from a^x — has no known polynomial algorithm for large p. Baby-Step Giant-Step (BSGS) is the canonical O(√p) algorithm: impractical for cryptographic primes (p ≈ 2²⁵⁶) but essential for p up to ~10¹² in competitive programming and for pedagogical understanding of why DLP is hard. It also appears in factoring algorithms (Pohlig-Hellman reduces DLP to BSGS on prime-order subgroups). The algorithm returns `Some(x)` if a solution exists, or `None` if b is not a power of a modulo p.

The Intuition

Write x = i · m - j, where m = ⌈√p⌉ and 0 ≤ i, j < m. Then a^x ≡ b becomes a^(im) ≡ b · a^j (mod p). Baby steps: compute b · a^j for j = 0, 1, ..., m-1. Store in a hash map: value → j. Giant steps: compute a^(im) for i = 1, 2, ..., m. If this value is in the hash map, you have i · m - j = x. This is meet-in-the-middle: instead of trying all x from 0 to p-1 (O(p) work), you meet from both sides in O(√p) work and O(√p) space. O(√p) time for both phases. The hash map lookup makes giant steps O(1) per query. OCaml uses `Hashtbl`. Rust uses `HashMap<u64, u64>` — same concept, but with explicit hash function choices affecting performance significantly for large m.

How It Works in Rust

use std::collections::HashMap;

fn mod_pow(mut base: u64, mut exp: u64, modulus: u64) -> u64 {
 let mut result = 1u64;
 base %= modulus;
 while exp > 0 {
     if exp & 1 == 1 { result = result * base % modulus; }
     base = base * base % modulus;
     exp >>= 1;
 }
 result
}

fn baby_step_giant_step(a: u64, b: u64, p: u64) -> Option<u64> {
 let m = (p as f64).sqrt().ceil() as u64 + 1;

 // Baby steps: store b * a^j → j for j in [0, m)
 let mut table: HashMap<u64, u64> = HashMap::with_capacity(m as usize);
 let mut baby = b % p;
 for j in 0..m {
     table.insert(baby, j);
     baby = baby * a % p; // baby = b * a^j
 }

 // Giant steps: compute a^(i*m) for i in [1, m]
 let am = mod_pow(a, m, p); // a^m mod p
 let mut giant = am;        // a^(1*m), a^(2*m), ...
 for i in 1..=m {
     if let Some(&j) = table.get(&giant) {
         // a^(i*m) = b * a^j  =>  x = i*m - j
         let x = i * m - j;
         return Some(x % (p - 1)); // reduce mod group order
     }
     giant = giant * am % p;
 }
 None // b is not a power of a mod p
}
`HashMap::with_capacity(m)` pre-allocates to avoid rehashing during the baby-step phase — a significant speedup for large m (avoid ~log m reallocations). The `mod_pow` function uses fast exponentiation (square-and-multiply): O(log exp) multiplications. For p up to 10¹², intermediate products fit in u64 only if p < 2³² — for larger p, use u128 or modular multiplication via u128 intermediate. The reduction `x % (p - 1)` handles the case where x > p-1 — since a^(p-1) ≡ 1 (mod p) by Fermat's little theorem, solutions repeat with period p-1.

What This Unlocks

Key Differences

ConceptOCamlRust
Hash map`Hashtbl.create m``HashMap::with_capacity(m)` — pre-allocated
Modular arithmetic`Int64` or native (63-bit, risky)`u64` for p < 2³², u128 for larger
Fast exponentiationRecursive or iterative with `Int64``mod_pow` with `base %= modulus` guard
Square root ceiling`int_of_float (sqrt (float p)) + 1``(p as f64).sqrt().ceil() as u64 + 1`
Group order reductionManual `mod (p-1)``x % (p - 1)` — same, but type-checked u64
/// Discrete Logarithm: Baby-Step Giant-Step (BSGS).
///
/// Solves a^x ≡ b (mod p) for prime p in O(√p) time and space.
/// Write x = i·m - j, precompute baby steps, check giant steps.

use std::collections::HashMap;

fn mulmod(a: u64, b: u64, m: u64) -> u64 {
    (a as u128 * b as u128 % m as u128) as u64
}

fn pow_mod(mut base: u64, mut exp: u64, m: u64) -> u64 {
    let mut result = 1u64;
    base %= m;
    while exp > 0 {
        if exp & 1 == 1 { result = mulmod(result, base, m); }
        base = mulmod(base, base, m);
        exp >>= 1;
    }
    result
}

/// BSGS: find smallest x ≥ 0 such that a^x ≡ b (mod p).
/// p must be prime (or at least gcd(a, p) = 1).
/// Returns None if no solution exists.
pub fn bsgs(a: u64, b: u64, p: u64) -> Option<u64> {
    let m = (p as f64).sqrt().ceil() as u64;

    // Baby steps: store a^j mod p → j for j = 0..m
    let mut table: HashMap<u64, u64> = HashMap::with_capacity(m as usize + 1);
    let mut aj = 1u64;
    for j in 0..=m {
        // Only store first occurrence (smallest j wins)
        table.entry(aj).or_insert(j);
        aj = mulmod(aj, a, p);
    }

    // Giant steps: check b * (a^m)^i mod p for i = 1..=m
    let am = pow_mod(a, m, p);
    let mut giant = b;
    for i in 1..=m {
        giant = mulmod(giant, am, p);
        if let Some(&j) = table.get(&giant) {
            let x = i * m - j;
            // Verify: x must be non-negative (it always is here since i*m ≥ j)
            return Some(x);
        }
    }
    None
}

/// Brute-force for verification and small p.
fn discrete_log_brute(a: u64, b: u64, p: u64) -> Option<u64> {
    let mut cur = 1u64;
    for x in 0..p {
        if cur == b { return Some(x); }
        cur = mulmod(cur, a, p);
    }
    None
}

fn main() {
    let cases = [
        (2u64, 22, 29),  // 2^x ≡ 22 (mod 29)
        (3u64, 1, 7),    // 3^x ≡ 1 (mod 7): x=6 (order of 3)
        (5u64, 3, 23),   // 5^x ≡ 3 (mod 23)
        (2u64, 3, 7),    // 2^x ≡ 3 (mod 7)
        (7u64, 2, 11),   // 7^x ≡ 2 (mod 11)
    ];

    for (a, b, p) in cases {
        let result = bsgs(a, b, p);
        let brute = discrete_log_brute(a, b, p);
        let verify = result.map(|x| pow_mod(a, x, p) == b).unwrap_or(true);
        println!("{a}^x ≡ {b} (mod {p}): bsgs={result:?}, brute={brute:?}, verified={verify}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn check(a: u64, b: u64, p: u64) {
        let bsgs_result = bsgs(a, b, p);
        let brute_result = discrete_log_brute(a, b, p);
        // Both should agree on existence
        assert_eq!(bsgs_result.is_some(), brute_result.is_some(),
            "existence mismatch: {a}^x≡{b} mod {p}");
        // If solution exists, verify it
        if let Some(x) = bsgs_result {
            assert_eq!(pow_mod(a, x, p), b,
                "wrong answer: {a}^{x} mod {p} ≠ {b}");
        }
    }

    #[test]
    fn test_basic_cases() {
        check(2, 22, 29);
        check(3, 1, 7);
        check(5, 3, 23);
        check(2, 3, 7);
    }

    #[test]
    fn test_identity() {
        // a^0 = 1
        let result = bsgs(5, 1, 23);
        // Could be 0 or 22 (the order)
        assert!(result.is_some());
        let x = result.unwrap();
        assert_eq!(pow_mod(5, x, 23), 1);
    }

    #[test]
    fn test_large_prime() {
        // 2^x ≡ 100 (mod 1009)
        check(2, 100, 1009);
    }

    #[test]
    fn test_matches_brute_all_small() {
        let p = 29u64; // Small prime
        for b in 1..p {
            check(2, b, p);
        }
    }

    #[test]
    fn test_no_solution() {
        // If a has order d < p-1, some b values have no log
        // 2 is not a primitive root mod 9 (which is composite), but for primes
        // we can still have no solution if b is not in the subgroup generated by a
        // (When a is a primitive root, all b have a log.)
        // Here we just verify our function doesn't panic or lie.
        let result = bsgs(3, 5, 7); // check
        let brute = discrete_log_brute(3, 5, 7);
        assert_eq!(result.is_some(), brute.is_some());
    }
}
(* Discrete Logarithm: Baby-Step Giant-Step in OCaml *)

let mulmod a b m =
  Int64.(to_int (rem (mul (of_int a) (of_int b)) (of_int m)))

let rec pow_mod base exp m =
  if exp = 0 then 1 mod m
  else if exp land 1 = 1 then mulmod base (pow_mod base (exp-1) m) m
  else let h = pow_mod base (exp/2) m in mulmod h h m

(* Baby-step giant-step: find x s.t. a^x ≡ b (mod p), 0 ≤ x < p *)
(* Returns Some x or None if no solution *)
let bsgs (a : int) (b : int) (p : int) : int option =
  let m = int_of_float (ceil (sqrt (float_of_int p))) in

  (* Baby steps: table[a^j mod p] = j, for j = 0..m-1 *)
  let table = Hashtbl.create m in
  let aj = ref 1 in
  for j = 0 to m - 1 do
    Hashtbl.replace table !aj j;
    aj := mulmod !aj a p
  done;

  (* Giant steps: check b * (a^m)^i mod p, for i = 1..m *)
  let am = pow_mod a m p in
  let giant = ref b in
  let result = ref None in
  let i = ref 1 in
  while !i <= m && !result = None do
    giant := mulmod !giant am p;
    (match Hashtbl.find_opt table !giant with
     | Some j ->
       let x = !i * m - j in
       if x >= 0 then result := Some x
     | None -> ());
    incr i
  done;
  !result

(* Brute-force for verification *)
let discrete_log_brute (a : int) (b : int) (p : int) : int option =
  let x = ref 0 and cur = ref 1 in
  let found = ref None in
  while !x < p && !found = None do
    if !cur = b then found := Some !x;
    cur := mulmod !cur a p;
    incr x
  done;
  !found

let () =
  let cases = [
    (2, 22, 29);    (* 2^x ≡ 22 (mod 29) *)
    (3, 1, 7);      (* 3^x ≡ 1 (mod 7), x=6 *)
    (5, 3, 23);     (* 5^x ≡ 3 (mod 23) *)
    (2, 3, 7);      (* 2^x ≡ 3 (mod 7) *)
  ] in
  List.iter (fun (a, b, p) ->
    let bsgs_result = bsgs a b p in
    let brute_result = discrete_log_brute a b p in
    Printf.printf "%d^x ≡ %d (mod %d): bsgs=%s, brute=%s\n"
      a b p
      (match bsgs_result with Some x -> string_of_int x | None -> "None")
      (match brute_result with Some x -> string_of_int x | None -> "None")
  ) cases