diff --git a/DIRECTORY.md b/DIRECTORY.md index b7b8aca45d..54f2743cc7 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -176,6 +176,7 @@ * [AutomorphicNumber](Maths/AutomorphicNumber.js) * [AverageMean](Maths/AverageMean.js) * [AverageMedian](Maths/AverageMedian.js) + * [BabyStepGiantStep](Maths/BabyStepGiantStep.js) * [BinaryConvert](Maths/BinaryConvert.js) * [BinaryExponentiationIterative](Maths/BinaryExponentiationIterative.js) * [BinaryExponentiationRecursive](Maths/BinaryExponentiationRecursive.js) diff --git a/Maths/BabyStepGiantStep.js b/Maths/BabyStepGiantStep.js new file mode 100644 index 0000000000..d457def0cf --- /dev/null +++ b/Maths/BabyStepGiantStep.js @@ -0,0 +1,68 @@ +/** + * Baby-step giant-step discrete logarithm modulo a prime. + * https://en.wikipedia.org/wiki/Baby-step_giant-step + * + * Solves base^x ≡ target (mod modulus) for the smallest non-negative x. + */ + +/** + * @param {number} base + * @param {number} target + * @param {number} modulus prime (or modulus where base is invertible) + * @returns {number} smallest non-negative discrete log + */ +export function babyStepGiantStep(base, target, modulus) { + if ( + typeof base !== 'number' || + typeof target !== 'number' || + typeof modulus !== 'number' || + !Number.isInteger(base) || + !Number.isInteger(target) || + !Number.isInteger(modulus) + ) { + throw new TypeError('Arguments must be integers') + } + if (modulus <= 1) throw new RangeError('modulus must be > 1') + + base = ((base % modulus) + modulus) % modulus + target = ((target % modulus) + modulus) % modulus + + if (target === 1) return 0 + if (base === 0) { + if (target === 0) return 1 + throw new RangeError('no discrete log') + } + + const modPow = (b, e, mod) => { + let r = 1 + b = ((b % mod) + mod) % mod + while (e > 0) { + if (e % 2 === 1) r = (r * b) % mod + b = (b * b) % mod + e = Math.floor(e / 2) + } + return r + } + + const m = Math.ceil(Math.sqrt(modulus - 1)) + const baby = new Map() + let value = 1 + for (let j = 0; j < m; j++) { + if (!baby.has(value)) baby.set(value, j) + value = (value * base) % modulus + } + + // factor = base^{-m} mod modulus (Fermat inverse assumes prime modulus) + const invBase = modPow(base, modulus - 2, modulus) + const factor = modPow(invBase, m, modulus) + + let gamma = target + for (let i = 0; i < m; i++) { + if (baby.has(gamma)) { + return i * m + baby.get(gamma) + } + gamma = (gamma * factor) % modulus + } + + throw new RangeError('no discrete log') +} diff --git a/Maths/test/BabyStepGiantStep.test.js b/Maths/test/BabyStepGiantStep.test.js new file mode 100644 index 0000000000..f03a56e8cb --- /dev/null +++ b/Maths/test/BabyStepGiantStep.test.js @@ -0,0 +1,20 @@ +import { babyStepGiantStep } from '../BabyStepGiantStep' + +describe('babyStepGiantStep', () => { + it.each([ + [2, 1, 5, 0], + [2, 3, 5, 3], // 2^3 = 8 ≡ 3 (mod 5) + [5, 8, 13, 3], // 5^3 = 125 ≡ 8 (mod 13) + [3, 13, 17, 4] // 3^4 = 81 ≡ 13 (mod 17) + ])('solves %i^x ≡ %i (mod %i)', (base, target, modulus, expected) => { + expect(babyStepGiantStep(base, target, modulus)).toBe(expected) + }) + + it('throws when no solution exists', () => { + expect(() => babyStepGiantStep(2, 0, 5)).toThrow(RangeError) + }) + + it('throws for non-integer inputs', () => { + expect(() => babyStepGiantStep('2', 3, 5)).toThrow(TypeError) + }) +})