18.18 Discrete Logarithms
Ordinary logarithms answer the question: ```text a^x = b ```
18.18 Discrete Logarithms
Ordinary logarithms answer the question:
a^x = b
What is x?
Discrete logarithms ask the same question in modular arithmetic:
g^x ≡ h (mod p)
Given:
g
h
p
find:
x
Unlike ordinary logarithms, there is no simple closed-form formula. For sufficiently large primes, finding x is computationally difficult. This difficulty forms the foundation of many public-key cryptographic systems.
From an algorithmic perspective, discrete logarithms are a fascinating inverse problem. Modular exponentiation is easy. Recovering the exponent is hard.
Problem
Given:
g^x ≡ h (mod p)
find:
x
Example:
3^x ≡ 5 (mod 7)
Compute powers of 3:
3^0 ≡ 1
3^1 ≡ 3
3^2 ≡ 2
3^3 ≡ 6
3^4 ≡ 4
3^5 ≡ 5
Therefore:
x = 5
This works for tiny examples, but brute force quickly becomes impossible.
Existence of Solutions
Suppose:
g
is a primitive root modulo prime:
p
Then every nonzero residue can be written uniquely as:
g^x
for some:
0 ≤ x < p-1
In this case, a discrete logarithm always exists.
Example:
p = 7
g = 3
Since 3 is a primitive root modulo 7, every nonzero residue has a logarithm base 3.
If g is not a primitive root, only some residues can be represented.
Brute Force
The simplest approach computes powers until the target appears.
def discrete_log_bruteforce(g, h, p):
value = 1
for x in range(p):
if value == h:
return x
value = value * g % p
return None
Example:
print(discrete_log_bruteforce(3, 5, 7))
Output:
5
Complexity:
O(p)
This becomes impractical even for moderate primes.
Why the Problem Is Difficult
Modular exponentiation is easy.
pow(g, x, p)
runs in:
O(log x)
using fast exponentiation.
The inverse problem appears much harder.
Given:
g^x mod p
recovering:
x
has no known polynomial-time algorithm for general groups.
This asymmetry is the basis of discrete-logarithm cryptography.
Baby-Step Giant-Step
The standard practical algorithm for moderate-sized inputs is the Baby-Step Giant-Step algorithm developed by entity["people","Daniel Shanks","American mathematician"].
The idea is to split the exponent:
x = im + j
where:
m ≈ √p
Then:
g^(im+j)
=
g^(im)g^j
Rearrange:
h(g^-m)^i
=
g^j
The algorithm precomputes one side and searches for matches.
Instead of:
O(p)
work, it requires roughly:
O(√p)
time and memory.
Example
Solve:
2^x ≡ 5 (mod 13)
Compute:
m = ⌈√13⌉ = 4
Baby steps:
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
Store:
1 → 0
2 → 1
4 → 2
8 → 3
Now compute giant steps using:
2^-4 mod 13
Since:
2^4 = 16 ≡ 3
and:
3^-1 ≡ 9
we repeatedly multiply by 9.
Eventually a match occurs, yielding:
x = 9
Verification:
2^9 = 512
512 mod 13 = 5
Correct.
Implementation
from math import isqrt
def discrete_log_bsgs(g, h, p):
m = isqrt(p) + 1
baby = {}
value = 1
for j in range(m):
if value not in baby:
baby[value] = j
value = value * g % p
factor = pow(pow(g, m, p), p - 2, p)
gamma = h
for i in range(m):
if gamma in baby:
return i * m + baby[gamma]
gamma = gamma * factor % p
return None
Example:
print(discrete_log_bsgs(2, 5, 13))
Output:
9
Complexity
Let:
n = p-1
Brute force:
O(n)
Baby-Step Giant-Step:
O(√n)
time and:
O(√n)
memory.
This is a dramatic improvement.
For:
n = 10^12
the search space shrinks from:
10^12
to roughly:
10^6
operations.
Pollard's Rho for Logarithms
Baby-Step Giant-Step requires large memory.
Pollard's Rho for discrete logarithms uses pseudorandom walks to reduce memory usage.
Complexity:
Expected O(√n)
time and:
O(1)
memory.
The algorithm is considerably more sophisticated but is often preferred for very large groups.
The same idea appears in Pollard's Rho factorization.
Discrete Logarithms in Finite Fields
For prime modulus:
p
the multiplicative group contains:
p - 1
elements.
If:
g
is primitive, every nonzero residue has a logarithm.
Example:
mod 11
Primitive root:
g = 2
Powers:
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 5
2^5 = 10
2^6 = 9
2^7 = 7
2^8 = 3
2^9 = 6
2^10 = 1
Thus:
log₂(6) = 9
modulo 11.
Logarithm Tables
For small moduli, precomputing logarithms is practical.
def log_table(g, p):
table = {}
value = 1
for x in range(p - 1):
table[value] = x
value = value * g % p
return table
Example:
logs = log_table(2, 11)
print(logs[6])
Output:
9
This turns discrete logarithm queries into constant-time lookups.
Diffie-Hellman
The classic Diffie-Hellman protocol relies directly on the difficulty of discrete logarithms.
Public parameters:
prime p
primitive root g
Alice chooses:
a
and sends:
g^a mod p
Bob chooses:
b
and sends:
g^b mod p
Both compute:
g^(ab) mod p
An attacker sees:
g
g^a
g^b
but must solve discrete logarithms to recover:
a
or:
b
The security depends on that problem being computationally difficult.
ElGamal Encryption
The ElGamal cryptosystem also relies on discrete logarithms.
Public key:
g^x
Private key:
x
Recovering:
x
from:
g^x
is exactly a discrete logarithm problem.
Index Calculus
For very large prime fields, the fastest known algorithms are based on index calculus.
Complexity is roughly:
exp(
√(log p log log p)
)
which is substantially faster than:
√p
for extremely large primes.
This is why cryptographic systems require carefully chosen parameter sizes.
Elliptic Curve Discrete Logarithms
In elliptic curve cryptography, the analogous problem is:
Q = kP
Given:
P
Q
find:
k
This is the elliptic curve discrete logarithm problem.
No index-calculus-style attack is known for general elliptic curves.
As a result, elliptic curve systems achieve comparable security with much smaller key sizes.
Common Mistakes
Assuming Every Residue Has a Logarithm
This is true only when the base generates the entire multiplicative group.
Primitive roots guarantee this property.
Forgetting Modulo p−1
Exponents in a cyclic group are taken modulo:
p-1
for prime modulus p.
Using Brute Force for Large Inputs
The complexity grows linearly.
Use Baby-Step Giant-Step or better algorithms.
Confusing Logarithms and Inverses
The equation:
g^x ≡ h
is fundamentally different from:
ax ≡ b
The latter is a linear congruence.
Ignoring Memory Costs
Baby-Step Giant-Step requires:
O(√n)
storage.
For very large groups, this becomes a limiting factor.
Testing
Basic examples:
def test_discrete_log():
assert discrete_log_bsgs(2, 5, 13) == 9
assert discrete_log_bsgs(3, 5, 7) == 5
Verification:
def verify(g, h, p):
x = discrete_log_bsgs(g, h, p)
if x is None:
return False
return pow(g, x, p) == h
Property test:
for p in [7, 11, 13, 17]:
g = primitive_root_prime(p)
for x in range(p - 1):
h = pow(g, x, p)
assert discrete_log_bsgs(g, h, p) == x
This verifies correctness across all residues generated by the primitive root.
Takeaway
Discrete logarithms reverse modular exponentiation. Given g^x ≡ h (mod p), the goal is to recover the exponent x. While modular exponentiation is easy, no efficient general solution for the inverse problem is known. Baby-Step Giant-Step reduces the search from O(p) to O(√p), while more advanced methods such as Pollard's Rho and index calculus improve performance in specific settings. The hardness of discrete logarithms underpins Diffie-Hellman, ElGamal, and many other cryptographic systems.