diff --git a/maths/pollard_rho_discrete_log.py b/maths/pollard_rho_discrete_log.py new file mode 100644 index 000000000000..c25258bcceb2 --- /dev/null +++ b/maths/pollard_rho_discrete_log.py @@ -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 + 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)}") diff --git a/maths/test_pollard_rho_discrete_log.py b/maths/test_pollard_rho_discrete_log.py new file mode 100644 index 000000000000..647896d0b8c2 --- /dev/null +++ b/maths/test_pollard_rho_discrete_log.py @@ -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)