continuedfractionfactorization
Version:
continued fraction factorization method for integers
467 lines (441 loc) • 15.1 kB
JavaScript
/*jshint esversion: 11*/
/*global BigInt*/
;
function ngcd(a, b) {
while (b > 0) {
const r = a - Math.floor(a / b) * b;
a = b;
b = r;
}
return a;
}
function gcd(a, b) {
while (BigInt.asUintN(53, b) !== b) {
const r = a % b;
a = b;
b = r;
}
if (b !== 0n) {
if (BigInt.asUintN(53, a) !== a) {
const r = a % b;
a = b;
b = r;
}
if (b !== 0n) {
return BigInt(ngcd(Number(a), Number(b)));
}
}
return a;
}
function log2(x) {
return BigInt(x.toString(16).length * 4);
}
function sqrt(x) {
if (x < BigInt((Number.MAX_SAFE_INTEGER + 1) / 2)) {
return BigInt(Math.floor(Math.sqrt(Number(x) + 0.5)));
}
const q = (log2(x) / 4n);
const initialGuess = ((sqrt(x >> (q * 2n)) + 1n) << q);
let a = initialGuess, b = a+1n;
while(a<b) {
b = a;
a = (b + x/b)/2n;
}
return b;
}
// See https://trizenx.blogspot.com/2018/10/continued-fraction-factorization-method.html
function continuedFractionForSqrt(n) {
function floorDiv(a, b) {
return typeof a === "number" && typeof b === "number" ? Math.floor(a / b) : a / b;
}
n = BigInt(n);
if (n < 0n) {
throw new RangeError();
}
const root = BigInt(sqrt(n));
const remainder = n - root * root;
const i = Number(root) * 2 <= Number.MAX_SAFE_INTEGER ? Number(root) : root;
let state = 0;
// sqrt(n) = floor(sqrt(n)) + 1 / x
// expand continued fraction:
// x_k = floor(x_k) + 1 / x_(k+1)
// each x_k will have form x_k = (sqrt(n) + y_k) / z_k
const one = i / i;
let zprev = one;
let z = typeof i === "number" ? Number(remainder) : remainder;
let y = i;
const iterator = {
next: function continuedFractionForSqrt() {
if (state === 0) {
state = 1;
return {value: i, done: false};
}
if (remainder !== 0n) {
while (state === 1 || zprev !== one) { //TODO: why is it cycling here - ?
state = 2;
const q = floorDiv((i + y), z);
const ynew = q * z - y;
const znew = zprev + q * (y - ynew);
y = ynew;
zprev = z;
z = znew;
//console.assert(one <= q && q <= i + i);
//console.assert(one <= y && y <= i);
//console.assert(one <= z && z <= i + i);
return {value: q, done: false};
}
//console.assert(y === i && BigInt(z) === remainder && zprev === one);
}
return {value: undefined, done: true};
}
};
iterator[globalThis.Symbol.iterator] = function () {
return this;
};
return iterator;
}
function modPowSmall(base, exponent, modulus) {
base = Number(base);
exponent = Number(exponent);
modulus = Number(modulus);
if (Math.max(Math.pow(modulus, 2), Math.pow(base, 2)) > Number.MAX_SAFE_INTEGER) {
throw new RangeError();
}
let accumulator = 1;
while (exponent !== 0) {
if (exponent % 2 === 0) {
exponent /= 2;
base = (base * base) % modulus;
} else {
exponent -= 1;
accumulator = (accumulator * base) % modulus;
}
}
return accumulator;
}
function isQuadraticResidueModuloPrime(a, p) {
a = BigInt(a);
p = Number(p);
if (p === 2) {
// "Modulo 2, every integer is a quadratic residue." - https://en.wikipedia.org/wiki/Quadratic_residue#Prime_modulus
return true;
}
// https://en.wikipedia.org/wiki/Euler%27s_criterion
const amodp = Number(BigInt(a) % BigInt(p));
if (amodp === 0) {
return true;
}
console.assert(p % 2 === 1);
const value = modPowSmall(amodp, (p - 1) / 2, p);
console.assert(value === 1 || value === p - 1);
return value === 1;
}
function log(N) {
const e = Math.max(N.toString(16).length * 4 - 4 * 12, 0);
const lnn = Math.log(Number(N >> BigInt(e))) + Math.log(2) * e;
return lnn;
}
function L(N) { // exp(sqrt(log(n)*log(log(n))))
const lnn = log(N);
return Math.exp(Math.sqrt(lnn * Math.log(lnn)));
}
function product(array) {
const n = array.length;
const m = Math.floor(n / 2);
return n === 0 ? 1n : (n === 1 ? BigInt(array[0]) : product(array.slice(0, m)) * product(array.slice(m)));
}
function isSmoothOverProduct(a, product, product1) {
a = a < 0n ? -a : a;
if (BigInt.asUintN(64, a) !== a) {
const g1 = gcd(a, product1);
if (g1 !== 1n) {
a /= g1;
}
}
if (a === 0n) {
throw new RangeError();
}
// quite test from https://trizenx.blogspot.com/2018/10/continued-fraction-factorization-method.html#:~:text=is_smooth_over_prod
let g = gcd(a, product % a);
while (g !== 1n) {
a /= g;
g = gcd(a, g);
}
return a;
}
function getSmoothFactorization(a, base) {
let value = BigInt(a);
if (value === 0n) {
return [];
}
let result = [];
if (value < 0n) {
result.push(-1);
value = -value;
}
for (let i = 0; i < base.length; i += 1) {
const p = BigInt(base[i]);
while (value % p === 0n) {
value /= p;
result.push(p);
}
}
return value === 1n ? result : null;
}
// (X**2 - Y) % N === 0, where Y is a smooth number
function CongruenceOfsquareOfXminusYmoduloN(X, Y, N, factorization) {
this.X = X;
this.Y = Y;
this.N = N;
this.factorization = factorization;
}
CongruenceOfsquareOfXminusYmoduloN.prototype.toString = function () {
const X = this.X, Y = this.Y, N = this.N;
return 'X**2 ≡ Y (mod N), Y = F'.replaceAll('X', X).replaceAll('Y', Y).replaceAll('N', N).replaceAll('F', this.factorization.join(' * '));
};
function modInverse(a, m) {
a = BigInt(a);
m = BigInt(m);
if (a <= 0n || a >= m || m <= 0n) {
throw new RangeError();
}
// We use the extended Euclidean algorithm:
let b = m;
let [A, C] = [1n, 0n];
while (b > 0n) {
const q = a / b; // floor(a / b)
[a, b] = [b, a - q * b];
[A, C] = [C, A - q * C];
}
if (a !== 1n) {
return 0n;
}
return A < 0n ? A + m : A;
}
function congruencesUsingContinuedFraction(primes, n) {
const USE_LP_STRATEGY = true; // large primes
let largePrimes = USE_LP_STRATEGY ? Object.create(null) : null; // prime -> congruence which needs this prime in base additionaly
const lpStrategy = function (p, X, Y) {
// https://ru.wikipedia.org/wiki/Алгоритм_Диксона#Стратегия_LP
const lp = largePrimes[p];
if (lp == undefined) {
largePrimes[p] = {X: X, Y: Y};
} else {
const s = BigInt(p);
const sInverse = modInverse(s, n);
if (sInverse === 0n) {
return new CongruenceOfsquareOfXminusYmoduloN(s, 0n, n, null);//?
} else {
const X1 = (sInverse * lp.X * X) % n;
if (Y % s === 0n && lp.Y % s === 0n) {
const Y1 = (lp.Y / s) * (Y / s);
const factorization = getSmoothFactorization(Y1, primes);
if (factorization != null) {
return new CongruenceOfsquareOfXminusYmoduloN(X1, Y1, n, factorization);
}
}
}
}
return null;
};
const primesProduct = product(primes);
//const product1 = BigInt(primes.reduce((p, prime) => p * Number(prime) <= Number.MAX_SAFE_INTEGER ? p * Number(prime) : p, 1));
const product1 = product(primes.slice(0, 11));
n = BigInt(n);
const d = ((n - n % 2n) / 2n);
let [hprev, h] = [0n, 1n]; // previout and current convergent numerator modulo n
const fraction = continuedFractionForSqrt(n);
const iterator = {
next: function congruencesUsingContinuedFraction() {
let a = 0n;
while ((a = fraction.next().value) != undefined) { // TODO: why do we stop after the first cycle ?
// https://en.wikipedia.org/wiki/Continued_fraction#:~:text=The%20successive%20convergents%20are%20given%20by%20the%20formula
[hprev, h] = [h, BigInt(a) * h + hprev];
// optimization from the https://trizenx.blogspot.com/2018/10/continued-fraction-factorization-method.html :
h = h % n;
const X = h % n; // A_k mod n
const Y = (X * X + d) % n - d; // (A_k)^2 mod n
//console.log(X, Y);
if (Y === 0n) {
return {value: new CongruenceOfsquareOfXminusYmoduloN(X, 0n, n, null), done: false};
} else {
const s = isSmoothOverProduct(Y, primesProduct, product1);
if (s === 1n) {
return {value: new CongruenceOfsquareOfXminusYmoduloN(X, Y, n, getSmoothFactorization(Y, primes)), done: false};
} else {
if (USE_LP_STRATEGY) {
const B = primes.length === 0 ? 1 : Number(primes[primes.length - 1]);
const lp = Number(s);
if (lp <= Math.min(B * B, Number.MAX_SAFE_INTEGER)) {
// s is prime
//if (!isPrime(s)) {
// throw new RangeError();
//}
const c = lpStrategy(lp, X, Y);
if (c != null) {
return {value: c, done: false};
}
}
}
}
}
}
return {value: undefined, done: true};
}
};
iterator[globalThis.Symbol.iterator] = function () {
return this;
};
return iterator;
}
function BitSet(size) {
this.data = new Array(Math.ceil(size / 32)).fill(0);
this.size = size;
}
BitSet.prototype.has = function (index) {
if (index >= this.size) {
throw new RangeError();
}
return (this.data[Math.floor(index / 32)] & (1 << (index % 32))) !== 0;
};
BitSet.prototype.toggle = function (index) {
if (index >= this.size) {
throw new RangeError();
}
this.data[Math.floor(index / 32)] ^= (1 << (index % 32));
};
BitSet.prototype.xor = function (other) {
const n = other.data.length;
if (n !== this.data.length) {
throw new RangeError();
}
for (let i = 0; i < n; i += 1) {
this.data[i] ^= other.data[i];
}
};
BitSet.prototype.toString = function () {
return this.data.map(x => (x >>> 0).toString(2).padStart(32, '0').split('').reverse().join('')).join('').slice(0, this.size);
};
// pass factorizations with associated values to the next call
// returns linear combinations of vectors which result in zero vector by modulo 2
// (basis of the kernel of the matrix)
function solve(matrixSize) {
// We build the augmented matrix in row-echelon form with permuted rows, which can grow up to matrixSize rows:
const M = new Array(matrixSize).fill(null); // We will fill the matrix so pivot elements will be placed on the diagonal
const associatedValues = new Array(matrixSize).fill(undefined);
let nextSolution = null;
let state = 1;
const iterator = {
next: function solve(tmp) {
while (true) {
if (state === 1) {
state = 0;
return {value: nextSolution, done: false};
}
state = 1;
const [rawRow, associatedValue] = tmp;
let row = new BitSet(matrixSize + matrixSize);
for (let j = 0; j < rawRow.length; j += 1) {
row.toggle(rawRow[j]);
}
// add row to the matrix maintaining it to be in row-echelon form:
for (let pivotColumn = 0; pivotColumn < matrixSize && row != null; pivotColumn += 1) {
if (row.has(pivotColumn)) {
const pivotRow = M[pivotColumn];
if (pivotRow != null) {
// row-reduction:
row.xor(pivotRow);
} else {
row.toggle(matrixSize + pivotColumn);
associatedValues[pivotColumn] = associatedValue;
M[pivotColumn] = row;
row = null;
}
}
}
if (row != null) {
// row has a solution
// extract solution from the augmented part of the matrix:
const solution = [];
for (let i = 0; i < M.length; i += 1) {
if (row.has(matrixSize + i)) {
solution.push(associatedValues[i]);
}
}
solution.push(associatedValue);
nextSolution = solution;
} else {
nextSolution = null;
}
}
//console.log(M.filter(x => x != null).map(x => x.toString()).join('\n'))
}
};
iterator[globalThis.Symbol.iterator] = function () {
return this;
};
return iterator;
}
function primes(MAX) {
let sieve = new Array(MAX + 1).fill(true);
let result = [];
result.push(2);
for (let i = 3; i <= MAX; i += 2) {
if (sieve[i]) {
result.push(i);
for (let j = i * i; j <= MAX; j += 2 * i) {
sieve[j] = false;
}
}
}
return result;
}
function abs(x) {
return x < 0n ? -x : x;
}
function ContinuedFractionFactorization(N) {
N = BigInt(N);
//if (isPrime(N)) {
// return N;
//}
for (let k = 1n;; k += 1n) {
const kN = k * N;
// https://trizenx.blogspot.com/2018/10/continued-fraction-factorization-method.html#:~:text=optimal%20value :
const B = Math.min(Math.floor(Math.sqrt(L(kN))), (1 << 25) - 1);
// as we are check for smoothness the number P_k^2 - N * Q_k^2, where P_k/Q_k is a convergent,
// if p is a factor of this number then P_k^2 - N * Q_k^2 = 0 (mod p),
// after multiplication by Q_k^-2 we have (Q_k^-1*P_k)^2 = N (mod p),
// x^2 = N (mod p), so N is a quadratic residue modulo N
// см. "Теоретико-числовые методы в криптографии" (О.Н. Герман, Ю.В. Нестеренко), страница 202
const primeBase = primes(B).filter(p => isQuadraticResidueModuloPrime(kN, p)).map(p => BigInt(p));
const congruences = congruencesUsingContinuedFraction(primeBase, kN); // congruences X_k^2 = Y_k mod N, where Y_k is smooth over the prime base
const solutions = solve(1 + primeBase.length); // find products of Y_k = Y, so that Y is a perfect square
solutions.next();
let c = null;
while ((c = congruences.next().value) != undefined) {
const solution = c.Y === 0n ? [c] : solutions.next([c.factorization.map(p => p === -1 ? 0 : 1 + primeBase.indexOf(p)), c]).value;
if (solution != null) {
const X = product(solution.map(c => c.X));
const Y = product(solution.map(c => c.Y)); // = sqrt(X**2 % N)
const x = X;
const y = BigInt(sqrt(Y));
console.assert(y * y === BigInt(Y));
const g = gcd(abs(x + y), N);
if (g !== 1n && g !== N) {
return g;
}
}
}
}
}
ContinuedFractionFactorization.testables = {
continuedFractionForSqrt: continuedFractionForSqrt,
primes: primes,
L: L,
getSmoothFactorization: getSmoothFactorization,
CongruenceOfsquareOfXminusYmoduloN: CongruenceOfsquareOfXminusYmoduloN,
solve: solve,
product: product,
isQuadraticResidueModuloPrime: isQuadraticResidueModuloPrime
};
export default ContinuedFractionFactorization;