Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions maths/pollard_rho_discrete_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import math
import random


def pollards_rho_discrete_log(base: int, target: int, modulus: int) -> int | None:
"""
Solve for x in the discrete logarithm problem: base^x ≡ target (mod modulus)
using Pollard's Rho algorithm.

This is a probabilistic algorithm that finds discrete logarithms in
O(√modulus) time.
The algorithm may not always find a solution in a single run due to its
probabilistic nature, but it will find the correct answer when it succeeds.

More info: https://en.wikipedia.org/wiki/Pollard%27s_rho_algorithm_for_logarithms

Parameters
----------
base : int
The generator (base of the exponential).
target : int
The target value (target ≡ base^x mod modulus).
modulus : int
Comment on lines +19 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not repeat the datatypes (int) from above, because if one is changed and the other is not, then readers become confused

A prime modulus.

Returns
-------
int | None
The discrete log x if found, otherwise None.

Examples
--------
>>> result = pollards_rho_discrete_log(2, 22, 29)
>>> result is not None and pow(2, result, 29) == 22
True

>>> result = pollards_rho_discrete_log(3, 9, 11)
>>> result is not None and pow(3, result, 11) == 9
True

>>> result = pollards_rho_discrete_log(5, 3, 7)
>>> result is not None and pow(5, result, 7) == 3
True

>>> # Case with no solution should return None or fail verification
>>> result = pollards_rho_discrete_log(3, 7, 11)
>>> result is None or pow(3, result, 11) != 7
True
"""

def pseudo_random_function(
current_value: int, exponent_base: int, exponent_target: int
) -> tuple[int, int, int]:
"""
Pseudo-random function that partitions the search space into 3 sets.

Returns a tuple of (new_value, new_exponent_base, new_exponent_target).
"""
if current_value % 3 == 0:
# Multiply by base
return (
(current_value * base) % modulus,
(exponent_base + 1) % (modulus - 1),
exponent_target,
)
elif current_value % 3 == 1:
# Square
return (
(current_value * current_value) % modulus,
(2 * exponent_base) % (modulus - 1),
(2 * exponent_target) % (modulus - 1),
)
else:
# Multiply by target
return (
(current_value * target) % modulus,
exponent_base,
(exponent_target + 1) % (modulus - 1),
)

# Try multiple random starting points to avoid immediate collisions
max_attempts = 50 # Increased attempts for better reliability

for _attempt in range(max_attempts):
# Use different starting values to avoid trivial collisions
# current_value represents base^exponent_base * target^exponent_target
random.seed() # Ensure truly random values
exponent_base = random.randint(0, modulus - 2)
exponent_target = random.randint(0, modulus - 2)

# Ensure current_value = base^exponent_base * target^exponent_target mod modulus
current_value = (
pow(base, exponent_base, modulus) * pow(target, exponent_target, modulus)
) % modulus

# Skip if current_value is 0 or 1 (problematic starting points)
if current_value <= 1:
continue

# Tortoise and hare start at same position
tortoise_value, tortoise_exp_base, tortoise_exp_target = (
current_value,
exponent_base,
exponent_target,
)
hare_value, hare_exp_base, hare_exp_target = (
current_value,
exponent_base,
exponent_target,
)

# Increased iteration limit for better coverage
max_iterations = max(int(math.sqrt(modulus)) * 2, modulus // 2)
for i in range(1, max_iterations):
# Tortoise: one step
(
tortoise_value,
tortoise_exp_base,
tortoise_exp_target,
) = pseudo_random_function(
tortoise_value, tortoise_exp_base, tortoise_exp_target
)
# Hare: two steps
hare_value, hare_exp_base, hare_exp_target = pseudo_random_function(
*pseudo_random_function(hare_value, hare_exp_base, hare_exp_target)
)

if tortoise_value == hare_value and i > 1: # Avoid immediate collision
# Collision found
exponent_difference = (tortoise_exp_base - hare_exp_base) % (
modulus - 1
)
target_difference = (hare_exp_target - tortoise_exp_target) % (
modulus - 1
)

if target_difference == 0:
break # Try with different starting point

try:
# Compute modular inverse using extended Euclidean algorithm
inverse_target_diff = pow(target_difference, -1, modulus - 1)
except ValueError:
break # No inverse, try different starting point

discrete_log = (exponent_difference * inverse_target_diff) % (
modulus - 1
)

# Verify the solution
if pow(base, discrete_log, modulus) == target:
return discrete_log
break # This attempt failed, try with different starting point

return None


if __name__ == "__main__":
import doctest

# Run doctests
doctest.testmod(verbose=True)

# Also run the main example
result = pollards_rho_discrete_log(2, 22, 29)
print(f"pollards_rho_discrete_log(2, 22, 29) = {result}")
if result is not None:
print(f"Verification: 2^{result} mod 29 = {pow(2, result, 29)}")
119 changes: 119 additions & 0 deletions maths/test_pollard_rho_discrete_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Test suite for Pollard's Rho Discrete Logarithm Algorithm.

This module contains comprehensive tests for the pollard_rho_discrete_log module,
including basic functionality tests, edge cases, and performance validation.
"""

import os
import sys
import unittest

# Add the parent directory to sys.path to import maths module
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from maths.pollard_rho_discrete_log import pollards_rho_discrete_log


class TestPollardRhoDiscreteLog(unittest.TestCase):
"""Test cases for Pollard's Rho Discrete Logarithm Algorithm."""

def test_basic_example(self):
"""Test the basic example from the GitHub issue."""
# Since the algorithm is probabilistic, try multiple times
found_solution = False
for _attempt in range(5): # Try up to 5 times
result = pollards_rho_discrete_log(2, 22, 29)
if result is not None:
# Verify the result is correct
assert pow(2, result, 29) == 22
found_solution = True
break

assert found_solution, "Algorithm should find a solution within 5 attempts"

def test_simple_cases(self):
"""Test simple discrete log cases with known answers."""
test_cases = [
(2, 8, 17), # 2^3 ≡ 8 (mod 17)
(5, 3, 7), # 5^5 ≡ 3 (mod 7)
(3, 9, 11), # 3^2 ≡ 9 (mod 11)
]

for g, h, p in test_cases:
# Try multiple times due to probabilistic nature
for _attempt in range(3):
result = pollards_rho_discrete_log(g, h, p)
if result is not None:
assert pow(g, result, p) == h
break
# Not all cases may have solutions, so we don't check for success

def test_no_solution_case(self):
"""Test case where no solution exists."""
# 3^x ≡ 7 (mod 11) has no solution (verified by brute force)
# The algorithm should return None or fail to find a solution
if (result := pollards_rho_discrete_log(3, 7, 11)) is not None:
# If it returns a result, it must be wrong since no solution exists
assert pow(3, result, 11) != 7

def test_edge_cases(self):
"""Test edge cases and input validation scenarios."""
# g = 1: 1^x ≡ h (mod p) only has solution if h = 1
result = pollards_rho_discrete_log(1, 1, 7)
if result is not None:
assert pow(1, result, 7) == 1

# h = 1: g^x ≡ 1 (mod p) - looking for the multiplicative order
result = pollards_rho_discrete_log(3, 1, 7)
if result is not None:
assert pow(3, result, 7) == 1

def test_small_primes(self):
"""Test with small prime moduli."""
test_cases = [
(2, 4, 5), # 2^2 ≡ 4 (mod 5)
(2, 3, 5), # 2^? ≡ 3 (mod 5)
(2, 1, 3), # 2^2 ≡ 1 (mod 3)
(3, 2, 5), # 3^3 ≡ 2 (mod 5)
]

for g, h, p in test_cases:
result = pollards_rho_discrete_log(g, h, p)
if result is not None:
# Verify the result is mathematically correct
assert pow(g, result, p) == h

def test_larger_examples(self):
"""Test with larger numbers to ensure algorithm scales."""
# Test cases with larger primes
test_cases = [
(2, 15, 31), # Find x where 2^x ≡ 15 (mod 31)
(3, 10, 37), # Find x where 3^x ≡ 10 (mod 37)
(5, 17, 41), # Find x where 5^x ≡ 17 (mod 41)
]

for g, h, p in test_cases:
result = pollards_rho_discrete_log(g, h, p)
if result is not None:
assert pow(g, result, p) == h

def test_multiple_runs_consistency(self):
"""Test that multiple runs give consistent results."""
# Since the algorithm is probabilistic, run it multiple times
# and ensure any returned result is mathematically correct
g, h, p = 2, 22, 29
results = []

for _ in range(10): # Run 10 times
result = pollards_rho_discrete_log(g, h, p)
if result is not None:
results.append(result)
assert pow(g, result, p) == h

# Should find at least one solution in 10 attempts
assert len(results) > 0, "Algorithm should find solution in multiple attempts"


if __name__ == "__main__":
unittest.main(verbosity=2)