Practical Index Calculus for Programmers
Coding in C and Python: Row Reduction Over Finite Fields and Integer Rings
Quick Intro
LeetArxiv is Leetcode for implementing Arxiv and other research papers.
This is part of our series on Practical Index Calculus for Computer Programmers.
Part 1: Discrete Logarithms and the Index Calculus Solution.
Part 2: Solving Pell Equations with Index Calculus and Algebraic Numbers.
Part 3 (We are here): Solving Index Calculus Equations over Integers and Finite Fields
Feel free to email me for sponsorships: murage.kibicho@leetarxiv.com
1.0 Introduction
Password hacking, satellite communications, and solving Pell equations all depend on solving a matrix system over a finite field or an integer ring. Finding such a solution is equivalent to tranforming a matrix into its Reduced Row Echelon Form (Chasnov, 2025)1.
Our past implementation of (Lenstra, 2008)2 as well as our coding guide to discrete logarithms needed us solve such systems of equations:
This is a coding guide designed for programmers interested in rolling their own index calculus solvers over finite fields and rings of integers.
1.1 Index Calculus and its Use Cases
Informally, index calculus is an algorithm for solving discrete logarithm problems in finite and algebraic fields.
More formally, the index calculus is a fast lifting algorithm designed to solve the classical (DLP) Discrete Logarithm Problem (Silverman, 2009)3. The index calculus is built upon the idea: one can lift the DLP from 𝔽p to ℤ, solve the problem in ℤ, and then reduce the solution modulo p.
One uses index calculus to break encryption (Howell, 1998)4, correct errors in messages (Welch & Berlekamp)5, and even solve Pell equations (Lenstra, 2008).
2.0 Solving a Matrix System in Reduced Row Echelon Form
The Reduced Row Echelon Form (RREF) of a matrix is the form we desire to find.
A pivot is the first non-zero element in the current column. This is important to note.
A matrix in RREF has these properties (Kahan, 1998)6 and (Kun, 2011)7:
The Reduced Row-Echelon Form is unique.
This means, no two matrices have the same RREF.
The first nonzero element in any nonzero row is “1”.
This means, you should see always zeros then a one in a row.
Each nonzero row's leading “1” comes in a column whose every other element is “0”.
This means, everything below the leading one is a zero.
Each pivot is the only nonzero entry in its column (Kottke, 2025).
This condition is the main difference between RE
2.1 Putting Matrices in RREF over Finite Fields
To put a matrix in RREF we perform these elementary row operations (Kun, 2011) and (Kottke, 2025)8:
swap the positions of any two rows,
multiply any row by a nonzero constant, and
add a nonzero multiple of one row to another row.
We shall solve the problem in Python and C for the remainder of this article.
We solve this matrix over a finite field in these steps:
First, we rewrite the matrix in augmented form:
A = [ [3, 0, 1, 2, 0, 346], [6, 0, 0, 1, 1, 171], [0, 3, 1, 0, 1, 153], [0, 4, 1, 0, 0, 442], [3, 5, 1, 0, 0, 458] ] p = 7043 # Prime modulus A_rref = rref_mod(A, p) printMatrix(A_rref)
Next, we write a modular matrix inversion function using Fermat’s little theorem.
def modinv(a, p): # Fermat's Little Theorem return pow(a, p-2, p) def printMatrix(m): for row in m: print(str(row))
Finally, we shall write a function to perform row operations on the augmented matrix to find RREF:
First, we find our matrix dimensions and loop through rows and columns:
def rref_mod(matrix, p): if not matrix: return numRows = len(matrix) numCols = len(matrix[0]) i, j = 0, 0 while i < numRows and j < numCols:
Next, we find the pivot row (the row in the current column with the largest non-zero element) and swap if it appears in the wrong position.
# Find the pivot row (swap if necessary) if matrix[i][j] == 0: for k in range(i+1, numRows): if matrix[k][j] != 0: matrix[i], matrix[k] = matrix[k], matrix[i] break if matrix[i][j] == 0: j += 1 continue
Once we find the pivot, we need to transform the entire row to start with one. This is done by multiplying all row elements by the pivot’s modular inverse:
# Normalize the pivot row (using modular inverse) pivot = matrix[i][j] inv_pivot = modinv(pivot, p) matrix[i] = [(x * inv_pivot) % p for x in matrix[i]]
Finally, we get rid of all other column elements that appear below the pivot:
# Eliminate other rows for k in range(numRows): if k != i and matrix[k][j] != 0: factor = matrix[k][j] matrix[k] = [(y - factor * x) % p for x, y in zip(matrix[i], matrix[k])]
Running this code gives the expected solution:
Your code in Python should resemble this gist:
#!/usr/bin/python
import time, random
def modinv(a, p):
# Fermat's Little Theorem
return pow(a, p-2, p)
def printMatrix(m):
for row in m:
print(str(row))
def rref_mod(matrix, p):
if not matrix: return
numRows = len(matrix)
numCols = len(matrix[0])
i, j = 0, 0
while i < numRows and j < numCols:
# Find the pivot row (swap if necessary)
if matrix[i][j] == 0:
for k in range(i+1, numRows):
if matrix[k][j] != 0:
matrix[i], matrix[k] = matrix[k], matrix[i]
break
if matrix[i][j] == 0:
j += 1
continue
# Normalize the pivot row (using modular inverse)
pivot = matrix[i][j]
inv_pivot = modinv(pivot, p)
matrix[i] = [(x * inv_pivot) % p for x in matrix[i]]
# Eliminate other rows
for k in range(numRows):
if k != i and matrix[k][j] != 0:
factor = matrix[k][j]
matrix[k] = [(y - factor * x) % p
for x, y in zip(matrix[i], matrix[k])]
i += 1
j += 1
return matrix
A = [
[3, 0, 1, 2, 0, 346],
[6, 0, 0, 1, 1, 171],
[0, 3, 1, 0, 1, 153],
[0, 4, 1, 0, 0, 442],
[3, 5, 1, 0, 0, 458]
]
p = 7043 # Prime modulus
A_rref = rref_mod(A, p)
printMatrix(A_rref)
and in C, it resembles this gist:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Function to compute modular inverse using Fermat's Little Theorem
long modinv(long a, long p)
{
long result = 1;
long power = p - 2;
a = a % p;
while (power > 0)
{
if (power % 2 == 1)
result = (result * a) % p;
a = (a * a) % p;
power /= 2;
}
return result;
}
// Function to print a matrix
void printMatrix(long **matrix, int numRows, int numCols)
{
for (int i = 0; i < numRows; i++)
{
for (int j = 0; j < numCols; j++)
{
printf("%ld ", matrix[i][j]);
}
printf("\n");
}
}
// Function to perform RREF modulo p
void rref_mod(long **matrix, int numRows, int numCols, long p)
{
int i = 0, j = 0;
while (i < numRows && j < numCols)
{
// Find the pivot row
if (matrix[i][j] == 0)
{
for (int k = i + 1; k < numRows; k++)
{
if (matrix[k][j] != 0)
{
// Swap rows
long *temp = matrix[i];
matrix[i] = matrix[k];
matrix[k] = temp;
break;
}
}
}
if (matrix[i][j] == 0)
{
j++;
continue;
}
// Normalize the pivot row
long pivot = matrix[i][j];
long inv_pivot = modinv(pivot, p);
for (int col = 0; col < numCols; col++)
{
matrix[i][col] = (matrix[i][col] * inv_pivot) % p;
}
// Eliminate other rows
for (int k = 0; k < numRows; k++)
{
if (k != i && matrix[k][j] != 0)
{
long factor = matrix[k][j];
for (int col = 0; col < numCols; col++)
{
matrix[k][col] = (matrix[k][col] - factor * matrix[i][col]) % p;
// Ensure positive result
if (matrix[k][col] < 0) matrix[k][col] += p;
}
}
}
i++;
j++;
}
}
int main() {
// Initialize matrix
int numRows = 5;
int numCols = 6;
long p = 7043;
// Allocate memory for matrix
long **A = (long **)malloc(numRows * sizeof(long *));
for (int i = 0; i < numRows; i++) {
A[i] = (long *)malloc(numCols * sizeof(long));
}
// Initialize matrix values
long values[5][6] = {
{3, 0, 1, 2, 0, 346},
{6, 0, 0, 1, 1, 171},
{0, 3, 1, 0, 1, 153},
{0, 4, 1, 0, 0, 442},
{3, 5, 1, 0, 0, 458}
};
for (int i = 0; i < numRows; i++) {
for (int j = 0; j < numCols; j++) {
A[i][j] = values[i][j];
}
}
printf("Original matrix:\n");
printMatrix(A, numRows, numCols);
// Perform RREF
rref_mod(A, numRows, numCols, p);
printf("\nRREF matrix (mod %ld):\n", p);
printMatrix(A, numRows, numCols);
// Free memory
for (int i = 0; i < numRows; i++) {
free(A[i]);
}
free(A);
return 0;
}
2.2 Putting Matrices in RREF over Integers
The previous section demonstrated how to put matrices in RREF over finite fields. This section covers the same operation but over the integers.
We take this problem from our Pell equations:
We can only use integers, no fractions and no floats.
We get rid of modular inverses and field addition and subtraction.
We normalize using the GCD. Here’s the code
#include <stdio.h> #include <stdlib.h> // Helper to get/set elements in a 1D matrix (row-major) #define MAT(m, row, col, numCols) ((m)[(row)*(numCols)+(col)]) // Swap two rows in a 1D array void swapRows(int *matrix, int row1, int row2, int numCols) { for (int col = 0; col < numCols; col++) { int temp = MAT(matrix, row1, col, numCols); MAT(matrix, row1, col, numCols) = MAT(matrix, row2, col, numCols); MAT(matrix, row2, col, numCols) = temp; } } // Compute GCD int gcd(int a, int b) { if (b == 0) return a > 0 ? a : -a; return gcd(b, a % b); } // Perform integer row reduction (not normalized RREF) void rref_integer(int *matrix, int numRows, int numCols) { int lead = 0; for (int r = 0; r < numRows; r++) { if (lead >= numCols) return; int i = r; while (MAT(matrix, i, lead, numCols) == 0) { i++; if (i == numRows) { i = r; lead++; if (lead == numCols) return; } } swapRows(matrix, i, r, numCols); for (int j = 0; j < numRows; j++) { if (j != r && MAT(matrix, j, lead, numCols) != 0) { int a = MAT(matrix, r, lead, numCols); int b = MAT(matrix, j, lead, numCols); int g = gcd(a, b); int factor_r = b / g; int factor_j = a / g; for (int col = 0; col < numCols; col++) { MAT(matrix, j, col, numCols) = MAT(matrix, j, col, numCols) * factor_j - MAT(matrix, r, col, numCols) * factor_r; } } } lead++; } } // Print matrix from 1D array void printMatrix(int *matrix, int numRows, int numCols) { for (int i = 0; i < numRows; i++) { for (int j = 0; j < numCols; j++) { printf("%4d ", MAT(matrix, i, j, numCols)); } printf("\n"); } } int main() { const int numRows = 9; const int numCols = 14; int test[9][14] = { { 2, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, { 6, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0}, { 0, 0, 2, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0}, { 0, -2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, { 2, 2, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}, { 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0}, { 3, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0}, { 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0}, { 3, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1} }; // Allocate 1D matrix int *A = (int *)malloc(numRows * numCols * sizeof(int)); if (!A) { printf("Memory allocation failed\n"); return 1; } // Copy data for (int i = 0; i < numRows; i++) for (int j = 0; j < numCols; j++) MAT(A, i, j, numCols) = test[i][j]; printf("Original matrix:\n"); printMatrix(A, numRows, numCols); rref_integer(A, numRows, numCols); printf("\nInteger row-reduced matrix:\n"); printMatrix(A, numRows, numCols); free(A); return 0; }
This yields the matrix:
We are done! We RREF’d over the ring of integers and over the finite fields!
References
Chasnov, R,. J,. (2025). Reduced Row Echelon Form. LibreTexts.
Lenstra, H,. (2008). Solving the Pell Equation. Algorithmic Number Theory Journal. Link.
Silverman, J,. H,. (2009). Lifting and Elliptic Curve Discrete Logarithms. In: Avanzi, R.M., Keliher, L., Sica, F. (eds) Selected Areas in Cryptography. SAC 2008. Lecture Notes in Computer Science, vol 5381. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-04159-4_6
Howell, S,. J,. (1998). The Index Calculus Algorithm for Discrete Logarithms. Clemson University Masters Thesis.