Skip to content

Chapter 2: Mathematical preliminaries

The algebra-heavy parts of post-quantum cryptography in this book repeatedly use four kinds of algebraic object: integers modulo nn, finite fields, polynomial quotient rings, and vectors or matrices over those rings. Lattice schemes (Part II), code-based schemes (Part IV), and the post-quantum zero-knowledge layer (Part VI) all reach into this toolbox. Hash-based signatures (Part III) do not. They are built on hash functions and Merkle trees, and Chapter 14 introduces the small set of primitives they need on its own terms. Chapter 2 defines each algebraic object, states the one or two theorems the later chapters cite, and implements the basic operations in Python. Readers who want to verify they have the assumed background should see Prerequisites.

Every code block in this chapter is self-contained and runnable in isolation: the chapter’s block verifier runs each block in a fresh Python namespace, so every block that depends on an earlier helper restates that helper at the top. A reader copying a single block into a file gets a script that runs without any external imports. The trailing # ==> marker is the expected-output assertion checked by tools/verify_code_blocks.py. There is no error handling on any helper. A helper given bad input crashes loudly, which is the intended behavior for a toy implementation. Asserts are used as narration of preconditions, not as error recovery.

One kind of calculation dominates what later chapters do with integers: raise a base to a large power and reduce the result modulo another integer. Here is the smallest instance that shows up. Take the prime 13 and the base 7, and compute 7200mod137^{200} \bmod 13.

Python’s built-in pow does this in time logarithmic in the exponent by repeated squaring:

direct = pow(7, 200, 13)
print(direct)
# ==> 3

Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch02/, one file per block. Appendix C covers the clone and the environment they run on.

Fermat’s little theorem gives a shortcut (Shoup, 2009). For any prime pp and any integer aa not divisible by pp, ap11(modp)a^{p-1} \equiv 1 \pmod{p}. For p=13p = 13 and a=7a = 7, that means 7121(mod13)7^{12} \equiv 1 \pmod{13}. Since 200=1612+8200 = 16 \cdot 12 + 8, the exponent 200200 collapses modulo 12 to 8, so 720078(mod13)7^{200} \equiv 7^{8} \pmod{13}:

by_fermat = pow(7, 8, 13)
print(by_fermat)
# ==> 3

Both routes return the same element of Z/13Z\mathbb{Z}/13\mathbb{Z}, because they are computing the same thing by different paths. Everything in the rest of the chapter is machinery for doing this kind of calculation carefully in richer algebraic settings. Integers modulo nn are the first setting. Finite fields, polynomial quotient rings, and matrices over those rings are the rest of the catalog.

Fix a positive integer nn. Two integers aa and bb are congruent modulo nn, written ab(modn)a \equiv b \pmod{n}, when nn divides aba - b. The set of equivalence classes under congruence is Z/nZ\mathbb{Z}/n\mathbb{Z}, the integers modulo nn. Addition and multiplication pass to equivalence classes without modification, so Z/nZ\mathbb{Z}/n\mathbb{Z} is a commutative ring with identity (Shoup, 2009).

An element aa of Z/nZ\mathbb{Z}/n\mathbb{Z} is a unit (has a multiplicative inverse) exactly when gcd(a,n)=1\gcd(a, n) = 1. The extended Euclidean algorithm computes gcd(a,n)\gcd(a, n) together with integers xx and yy satisfying ax+ny=gcd(a,n)a x + n y = \gcd(a, n). When gcd(a,n)=1\gcd(a, n) = 1, the coefficient xx reduced modulo nn is the inverse of aa (Shoup, 2009). The code below implements this.

Two theorems about powers come up in the number-theoretic chapters. Fermat’s little theorem says that for a prime pp and an integer aa not divisible by pp, ap11(modp)a^{p-1} \equiv 1 \pmod{p} (Shoup, 2009). Euler’s generalization says that for n2n \geq 2 and any aa with gcd(a,n)=1\gcd(a, n) = 1, aφ(n)1(modn)a^{\varphi(n)} \equiv 1 \pmod{n} (Shoup, 2009). The function φ(n)\varphi(n) is the number of integers aa with 1an1 \leq a \leq n and gcd(a,n)=1\gcd(a, n) = 1. For a prime pp the count φ(p)\varphi(p) equals p1p - 1, and Euler collapses to Fermat.

The Chinese remainder theorem handles multiple moduli at once. Suppose n1,n2,,nkn_1, n_2, \ldots, n_k are pairwise coprime, and let nn be their product. The map Z/nZ(Z/n1Z)××(Z/nkZ)\mathbb{Z}/n\mathbb{Z} \to (\mathbb{Z}/n_1\mathbb{Z}) \times \cdots \times (\mathbb{Z}/n_k\mathbb{Z}) that sends aa to the tuple (amodn1,,amodnk)(a \bmod n_1, \ldots, a \bmod n_k) is a ring isomorphism (Shoup, 2009). Concretely, arithmetic in Z/nZ\mathbb{Z}/n\mathbb{Z} becomes arithmetic in each factor Z/niZ\mathbb{Z}/n_i\mathbb{Z} in parallel, and the reverse map reassembles a result in Z/nZ\mathbb{Z}/n\mathbb{Z} from its images in the factors.

Fast exponentiation (repeated squaring) computes akmodna^k \bmod n in O(logk)O(\log k) modular multiplications by walking the binary representation of the exponent from low bit to high bit (Shoup, 2009). Chapter 4 uses it to build toy RSA, and Chapter 9 uses it to raise a primitive root of unity to the powers the number theoretic transform needs.

Raise a base to a non-negative exponent modulo nn by repeated squaring. The loop walks the exponent from low bit to high bit, squaring the running base each step and multiplying it into the result whenever the current bit is 1.

def mod_pow(base, exponent, modulus):
assert exponent >= 0, "exponent must be non-negative"
assert modulus >= 1, "modulus must be at least 1"
result = 1 % modulus
base = base % modulus
while exponent > 0:
if exponent % 2 == 1:
result = (result * base) % modulus
base = (base * base) % modulus
exponent //= 2
return result
# 7^200 mod 13, matching the opening calculation.
print(mod_pow(7, 200, 13))
# ==> 3

Python’s built-in pow(base, exponent, modulus) does the same computation with a more careful C implementation, and the later chapters use it wherever speed matters. The explicit version here exists to show what the built-in is doing inside.

Given non-negative integers aa and bb, find their greatest common divisor gg together with coefficients xx and yy satisfying ax+by=ga x + b y = g. When b=0b = 0 the gcd is aa and the trivial coefficients (1,0)(1, 0) work; otherwise, solve recursively on (b,amodb)(b, a \bmod b) and repackage the coefficients by the usual substitution. Non-negativity is a real precondition, not a formality: on a negative argument the recursion can bottom out on a negative value and hand back a negative gg. mod_inv is unaffected, because it reduces its argument modulo nn before calling.

def ext_gcd(a, b):
if b == 0:
return a, 1, 0
g, x1, y1 = ext_gcd(b, a % b)
return g, y1, x1 - (a // b) * y1
def mod_inv(a, modulus):
assert modulus > 1, "modulus must be greater than 1"
g, x, _ = ext_gcd(a % modulus, modulus)
assert g == 1, "inverse does not exist: a and modulus share a factor"
return x % modulus
# Compute 17^-1 modulo 43 and verify the inverse.
inverse = mod_inv(17, 43)
product = (17 * inverse) % 43
print(inverse)
print(product)
# ==> 38
# ==> 1

The assert is narration, not error handling. It marks a precondition for the toy reader. A production implementation would raise a specific exception, but a toy helper that quietly returns garbage on invalid input teaches the wrong lesson.

When nn is a prime pp, every nonzero element of Z/pZ\mathbb{Z}/p\mathbb{Z} has a multiplicative inverse, so Z/pZ\mathbb{Z}/p\mathbb{Z} is a field. Write it Fp\mathbb{F}_p when treating it as a field, and Fp\mathbb{F}_p^* for the set of its nonzero elements under multiplication.

Fp\mathbb{F}_p^* is a cyclic group of order p1p - 1 (Lidl & Niederreiter, 1997). Cyclic means there is an element gg with the property that every nonzero element of Fp\mathbb{F}_p equals gig^i for some integer ii in {0,1,,p2}\{0, 1, \ldots, p - 2\}. Such a gg is called a primitive element or a generator of Fp\mathbb{F}_p^*. For the concrete primes this book uses, a generator is found by trial, as Exercise 2 walks through for p=17p = 17.

The prime fields Fp\mathbb{F}_p are the only finite fields Part II builds from scratch. Chapter 11’s partial number theoretic transform computes inside quadratic extensions of Zq\mathbb{Z}_q, which that chapter introduces where it needs them. Finite fields of the form F2m\mathbb{F}_{2^m} also exist and appear in the Classic McEliece treatment in Chapter 20. They are constructed as F2[x]/(f(x))\mathbb{F}_2[x] / (f(x)) for an irreducible polynomial ff of degree mm, which is an instance of the polynomial-quotient construction in the next subsection (Lidl & Niederreiter, 1997). Chapter 20 writes this field GF(2m)\mathrm{GF}(2^m), where mm is the extension degree and the field has 2m2^m elements. The Goppa-code dimension parameter kk is a separate quantity and lives at the code level, not at the field level.

Fix a prime pp. The polynomial ring Fp[x]\mathbb{F}_p[x] is the set of polynomials in one variable with coefficients in Fp\mathbb{F}_p, under the usual addition and multiplication of polynomials. Fp[x]\mathbb{F}_p[x] is a Euclidean domain: for any f,gf, g in Fp[x]\mathbb{F}_p[x] with gg nonzero there exist unique q,rq, r in Fp[x]\mathbb{F}_p[x] with deg(r)<deg(g)\deg(r) < \deg(g) and f=qg+rf = q g + r (Lang, 2002; Shoup, 2009). The zero polynomial has degree -\infty by convention, which is what lets a zero remainder satisfy that bound. Finding qq and rr is polynomial long division, which the code below implements.

When qq is prime, the symbols Zq\mathbb{Z}_q, Z/qZ\mathbb{Z}/q\mathbb{Z}, and Fq\mathbb{F}_q all refer to the same field. The book follows the convention used in NIST’s lattice specifications and writes Zq\mathbb{Z}_q when the modulus is the integer qq used by a scheme parameter set, and Fp\mathbb{F}_p when the prime pp is generic.

Given a polynomial ff in Fp[x]\mathbb{F}_p[x] of degree kk, the quotient ring Fp[x]/(f(x))\mathbb{F}_p[x] / (f(x)) consists of equivalence classes of polynomials modulo ff. A canonical representative of each class is the polynomial of degree strictly less than kk obtained by taking the remainder on long division by ff. Addition and multiplication in the quotient are performed in Fp[x]\mathbb{F}_p[x] and then reduced modulo ff (Lang, 2002).

When ff is irreducible over Fp\mathbb{F}_p, every nonzero element of Fp[x]/(f(x))\mathbb{F}_p[x] / (f(x)) has a multiplicative inverse, and the quotient is a field with pkp^k elements (Lidl & Niederreiter, 1997). This is how the finite fields Fpk\mathbb{F}_{p^k} are built. When ff is nonconstant and reducible, the quotient is still a commutative ring with identity, but it is not a field and it has zero divisors. It is still usable for arithmetic.

The lattice-based schemes in Part II work in quotient rings of the form Zq[x]/(xn+1)\mathbb{Z}_q[x] / (x^n + 1), where qq is a prime and nn is a power of two. With nn a power of two, xn+1x^n + 1 is the cyclotomic polynomial Φ2n(x)\Phi_{2n}(x), and the quotient is the negacyclic ring of polynomials of degree less than nn in which xnx^n is identified with 1-1. The parameter qq is then chosen so that multiplication in this ring admits an efficient number theoretic transform.

ML-KEM uses q=3329q = 3329 and n=256n = 256 (National Institute of Standards and Technology, 2024). The prime 3329 satisfies q1=2813q - 1 = 2^8 \cdot 13. So Zq\mathbb{Z}_q contains a primitive 256256-th root of unity but not a primitive 512512-th root. The polynomial x256+1x^{256} + 1 factors over Zq\mathbb{Z}_q into 128 degree-two irreducible polynomials rather than 256 linear factors (FIPS 203 §4.3, equation 4.10) (National Institute of Standards and Technology, 2024). Chapter 9 derives the condition behind this partial split, and Chapter 11 builds the corresponding transform.

For Chapter 2 it is enough to know that Fp[x]/(f(x))\mathbb{F}_p[x] / (f(x)) is a ring, that its elements are polynomials of bounded degree, and that multiplication is ordinary polynomial multiplication followed by reduction modulo ff. Figure 2.1 shows that reduction in the negacyclic case at n=4n = 4.

Reduction modulo x^4 + 1 A row of seven boxes labeled x^0 through x^6, the coefficient slots of a product of two degree-3 polynomials. A dashed vertical divider sits between the x^3 and x^4 boxes; the four boxes to its left are green and marked "kept", the three to its right are outlined in dashed amber and marked "out of range". Four dashed vertical lines carry slots 0 through 3 straight down to a lower row of four green boxes labeled x^0 through x^3. Three amber curved arrows carry slot 4 to slot 0, slot 5 to slot 1 and slot 6 to slot 2, each passing through a small circled minus sign. Text at the right states that x^4 is congruent to -1, so x^(4+i) is congruent to minus x^i, that every term of degree 4 or more folds onto degree i with its sign flipped, and that nothing is discarded. Reduction modulo x4+ 1 kept out of range x0 x1 x2 x3 x4 x5 x6 x0 x1 x2 x3 Seven coefficient slots in, four out: the canonical representative has degree at most 3. x4≡ −1 so x4+i≡ −xi Every term of degree 4 or more folds onto degree i with its sign flipped. Nothing is discarded.
Figure 2.1. Reduction modulo x4+1x^4 + 1. The product of two degree-3 polynomials occupies seven coefficient slots. The three slots past degree 3 fold back onto degrees 0, 1, and 2 with their signs flipped, which is what makes the ring negacyclic.

Represent a polynomial as a list of coefficients in ascending order, so the list [2, 1, 3] stands for 2+x+3x22 + x + 3 x^2. Multiplication is the textbook O(n2)O(n^2) double loop. The result has degree at most deg(f)+deg(g)\deg(f) + \deg(g), and every entry is reduced modulo pp as it is accumulated. Chapter 9 replaces this with the number theoretic transform for the specific rings ML-KEM uses, but the naive version here is what the rest of Part II’s exposition assumes the reader has in mind.

def poly_mul(f, g, p):
result = [0] * (len(f) + len(g) - 1)
for i, a in enumerate(f):
for j, b in enumerate(g):
result[i + j] = (result[i + j] + a * b) % p
return result
# Multiply (2 + x) by (3 + x^2) in F_5[x].
f = [2, 1]
g = [3, 0, 1]
print(poly_mul(f, g, 5))
# ==> [1, 3, 2, 1]

The constant term is 231(mod5)2 \cdot 3 \equiv 1 \pmod{5}. The xx coefficient is 20+13=32 \cdot 0 + 1 \cdot 3 = 3. The x2x^2 coefficient is 21+10=22 \cdot 1 + 1 \cdot 0 = 2. The x3x^3 coefficient is 11=11 \cdot 1 = 1. Reading the list back as a polynomial gives 1+3x+2x2+x31 + 3 x + 2 x^2 + x^3 in F5[x]\mathbb{F}_5[x].

Long division of a polynomial aa by a monic polynomial ff over Fp\mathbb{F}_p produces a remainder rr with deg(r)<deg(f)\deg(r) < \deg(f). Repeatedly peel off the top coefficient of aa, subtract the appropriate multiple of ff from the tail, and drop the now-zero top coefficient. This helper assumes ff is monic for simplicity; a production helper would handle a general leading coefficient by dividing it out first. The first line reduces every incoming coefficient modulo pp, so a caller that passes unreduced integers gets coefficients back in {0,,p1}\{0, \ldots, p-1\}. That is not the same as a canonical representative, because the trailing zero coefficients are still left in place.

def poly_mul(f, g, p):
result = [0] * (len(f) + len(g) - 1)
for i, a in enumerate(f):
for j, b in enumerate(g):
result[i + j] = (result[i + j] + a * b) % p
return result
def poly_mod(a, f, p):
assert f[-1] == 1, "poly_mod requires f to be monic"
a = [c % p for c in a]
deg_f = len(f) - 1
while len(a) - 1 >= deg_f:
lead = a[-1]
if lead != 0:
for i in range(deg_f + 1):
a[-1 - i] = (a[-1 - i] - lead * f[deg_f - i]) % p
a.pop()
return a
# In F_7[x], reduce (1 + x + x^3) * (2 + x^2) modulo x^3 + x + 1.
# Any multiple of the modulus reduces to the zero polynomial.
f_mod = [1, 1, 0, 1]
g = [2, 0, 1]
product = poly_mul(f_mod, g, 7)
print(poly_mod(product, f_mod, 7))
# ==> [0, 0, 0]

The modulus is a factor of the product by construction, so the remainder is the zero polynomial. poly_mod does not trim trailing zero coefficients, so the zero polynomial comes back as [0, 0, 0] rather than the shorter [0] or []. A caller that wants the shortest representation has to trim the result itself.

A vector in Fpn\mathbb{F}_p^n is an ordered tuple of nn elements from Fp\mathbb{F}_p. An mm-by-nn matrix over Fp\mathbb{F}_p is a rectangular array of elements of Fp\mathbb{F}_p with mm rows and nn columns. Addition is componentwise; scalar multiplication scales every entry by a fixed element of Fp\mathbb{F}_p; matrix multiplication is the usual row-times-column rule. These operations make Fpn\mathbb{F}_p^n a vector space over Fp\mathbb{F}_p (Dummit & Foote, 2004).

Two ideas from linear algebra carry into the later chapters. The first is rank. The rank of a matrix AA is the dimension of its row space. Equivalently, it is the dimension of its column space. The two dimensions are equal, even though the row and column spaces live in different ambient vector spaces when AA is rectangular. Gaussian elimination by row operations reduces AA to reduced row echelon form without changing its rank. The rank is the number of nonzero rows in the reduced form (Dummit & Foote, 2004). The code below implements this algorithm.

The second is a basis. A basis of Fpn\mathbb{F}_p^n is a set of nn vectors that spans Fpn\mathbb{F}_p^n and is linearly independent. Over a field, every spanning set contains a basis and every linearly independent set extends to a basis (Dummit & Foote, 2004). Over Z\mathbb{Z}, which is not a field, the word “basis” has a narrower meaning tied to the structure of integer lattices. Chapter 7 develops that narrower meaning in full; for this chapter, “basis over Fp\mathbb{F}_p” is the only version in scope.

The standard dot product over Fp\mathbb{F}_p defines a symmetric bilinear form, which coding theory routinely calls an inner product, and it is used algorithmically (for example, in the syndrome calculation of a linear code). It is not positive definite: finite fields carry no compatible ordering, and a nonzero vector can be orthogonal to itself. Over F2\mathbb{F}_2, the nonzero vector (1,1)(1, 1) has self-inner-product 11+11=01 \cdot 1 + 1 \cdot 1 = 0. What does carry over is the part coding theory needs. Orthogonality and orthogonal complements remain well defined, which is what a parity-check matrix relies on. Chapter 19 returns to this form over F2\mathbb{F}_2 when setting up linear codes.

Reduce a matrix over Fp\mathbb{F}_p to reduced row echelon form by processing one column at a time. The rank of the matrix is the number of pivots the algorithm places. Over a field this is straightforward because every nonzero scalar is invertible. Over a ring that is not a field, the algorithm breaks in informative ways. The lattice and code-based chapters each return to row reduction in a more delicate form. Inside the pivot-normalization step this block uses pow(a, p - 2, p) as the modular inverse, because the modulus is a prime and Fermat’s little theorem gives the inverse in one line. This is equivalent to the extended-Euclidean mod_inv from the earlier block. The Fermat form is just shorter when the modulus is known to be prime.

def gauss_eliminate(matrix, p):
assert p > 1, "p must be prime; this helper does not test primality"
m = [row[:] for row in matrix]
rows = len(m)
cols = len(m[0]) if m else 0
rank = 0
for col in range(cols):
pivot = None
for r in range(rank, rows):
if m[r][col] % p != 0:
pivot = r
break
if pivot is None:
continue
m[rank], m[pivot] = m[pivot], m[rank]
# Fermat: for prime p, the inverse of a nonzero element is a^(p-2).
inv = pow(m[rank][col], p - 2, p)
m[rank] = [(x * inv) % p for x in m[rank]]
for r in range(rows):
if r != rank and m[r][col] % p != 0:
factor = m[r][col]
m[r] = [(m[r][c] - factor * m[rank][c]) % p for c in range(cols)]
rank += 1
return m, rank
matrix = [
[1, 2, 3],
[0, 1, 4],
[2, 0, 0],
]
reduced, rank = gauss_eliminate(matrix, 5)
for row in reduced:
print(row)
print("rank =", rank)
# ==> [1, 0, 0]
# ==> [0, 1, 4]
# ==> [0, 0, 0]
# ==> rank = 2

The third row of the input is twice the first row plus the second row, all in F5\mathbb{F}_5: 2[1,2,3]+[0,1,4]=[2,5,10][2,0,0](mod5)2 \cdot [1, 2, 3] + [0, 1, 4] = [2, 5, 10] \equiv [2, 0, 0] \pmod{5}. So the three rows span a two-dimensional subspace, the algorithm correctly reports rank 2, and the third row reduces to zero.

Each of the four algebraic objects in this chapter has a home in a later part of the book.

ObjectSetting forChapters
Integers modulo nnClassical RSA and classical Diffie-HellmanChapter 4 (toy RSA) and Chapter 5 (toy Diffie-Hellman), both of them the foil for the post-quantum chapters that follow
Finite fields and polynomial quotient ringsThe lattice-based schemes: the ring Zq[x]/(xn+1)\mathbb{Z}_q[x] / (x^n + 1) that ML-KEM works in, where the number theoretic transform gives a fast multiplication algorithmChapter 9
Matrices over F2\mathbb{F}_2The code-based schemes: linear codes as subspaces of F2n\mathbb{F}_2^n, binary Goppa codes over the extension field F2m\mathbb{F}_{2^m} in Classic McEliece, and HQC, whose security rests on quasi-cyclic syndrome decoding, a structured variant of the same decoding problemChapter 19 (linear codes), Chapter 20 (Classic McEliece), Chapter 21 (HQC)
Linear algebra over Z\mathbb{Z}The lattice chapters: lattices as the integer combinations of a basis of Rn\mathbb{R}^n, then basis reductionChapter 7 (building lattices), Chapter 13 (basis reduction)

Hash-based signatures in Part III sit outside this map entirely: they need hash functions and Merkle trees, not rings or matrices.

Where Chapter 2 ends and Chapter 3 picks up

Section titled “Where Chapter 2 ends and Chapter 3 picks up”

This chapter answered “what is the object and how do I compute in it”. It said nothing about why anyone would build a cryptosystem on one. Chapter 3 asks the other question: given these objects, which computational problems over them are believed to be hard, and hard for a quantum adversary in particular.

The map from this chapter to that one is partial. Two of Chapter 3’s four families rest directly on the algebra above: short and close vectors in lattices, and syndrome decoding of a random linear code. A third rests on isogeny and endomorphism-ring problems over supersingular elliptic curves, which are not built from anything defined here. Chapter 3 defines the vocabulary it needs on the spot and leaves the full structure to Chapter 22. The fourth needs none of this chapter’s algebra at all, resting instead on the preimage and collision properties of hash functions.

  1. Use the chapter’s mod_pow helper to compute 7200mod137^{200} \bmod 13. Verify the answer by computing 78mod137^{8} \bmod 13 (Fermat’s little theorem says 7121(mod13)7^{12} \equiv 1 \pmod{13}, and 200=1612+8200 = 16 \cdot 12 + 8, so the exponent reduces to 8 modulo 12). Confirm the two answers agree and state what the common answer is.

  2. Find a generator of F17\mathbb{F}_{17}^*. Try g=2g = 2 first: compute 21,22,,2162^1, 2^2, \ldots, 2^{16} modulo 17 and check whether the sequence hits every nonzero element of F17\mathbb{F}_{17} exactly once. If g=2g = 2 does not work, try g=3g = 3, then g=5g = 5, and so on. Write out the full power sequence for the first generator you find.

  3. Write a Python function that returns every root a polynomial in Fp[x]\mathbb{F}_p[x] has in Fp\mathbb{F}_p, by evaluating it at every element of Fp\mathbb{F}_p in turn. Apply the function to f(x)=x3+x+1f(x) = x^3 + x + 1 over F7\mathbb{F}_7. List the values f(0),f(1),,f(6)f(0), f(1), \ldots, f(6) modulo 7. A polynomial of degree 2 or 3 over a field is irreducible if and only if it has no root in the field. (Any proper factorization of such a polynomial would include a linear factor, and linear factors correspond to roots.) State whether ff is irreducible and justify the answer from the values. The equivalence does not extend to degree 4 and higher: (x2+1)2(x^2 + 1)^2 over F3\mathbb{F}_3 is reducible but has no root in F3\mathbb{F}_3.

  4. Apply the chapter’s gauss_eliminate helper to the 4-by-4 matrix

    [[1, 2, 3, 4],
    [2, 3, 4, 5],
    [3, 4, 5, 6],
    [4, 5, 6, 7]]

    over F7\mathbb{F}_7. State the rank of the matrix and write one sentence explaining why the rank is what it is, in terms of a linear dependence among the rows of the original matrix. Hint: the rows form an arithmetic progression.

Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 2. A separate track, for rebuilding rather than reading: the package exercises/ch02-algebra has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch02 to grade your version against the suite that proves the reference one.

Dummit, D. S., & Foote, R. M. (2004). Abstract Algebra (3rd ed.). Wiley. https://www.wiley.com/en-us/Abstract+Algebra%252C+3rd+Edition-p-9780471433347
Fan, K. (Steve), & Pollack, P. (2025). Counting primes with a given primitive root, uniformly. Mathematika, 71(4), e70055. https://doi.org/10.1112/mtk.70055
Gupta, R., & Murty, M. R. (1984). A remark on Artin’s conjecture. Inventiones Mathematicae, 78, 127–130. https://doi.org/10.1007/BF01388719
Heath-Brown, D. R. (1986). Artin’s conjecture for primitive roots. Quarterly Journal of Mathematics, 37(1), 27–38. https://doi.org/10.1093/qmath/37.1.27
Hooley, C. (1967). On Artin’s conjecture. Journal Für Die Reine Und Angewandte Mathematik, 225, 209–220. https://doi.org/10.1515/crll.1967.225.209
Lang, S. (2002). Algebra (Revised 3rd, Vol. 211). Springer. https://link.springer.com/book/10.1007/978-1-4613-0041-0
Lidl, R., & Niederreiter, H. (1997). Finite Fields (2nd ed., Vol. 20). Cambridge University Press. https://www.cambridge.org/core/books/finite-fields/75BDAA74ABAE713196E718392B9E5E72
Moree, P. (2012). Artin’s primitive root conjecture: a survey. Integers, 12(6), 1305–1416. https://doi.org/10.1515/integers-2012-0043
National Institute of Standards and Technology. (2024). FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.203
Shoup, V. (2009). A Computational Introduction to Number Theory and Algebra (2nd ed.). Cambridge University Press. https://doi.org/10.1017/cbo9780511814549

Last updated: