Chapter 7: Lattices for programmers
A lattice is the set of integer combinations of a basis. Fix linearly independent vectors in and the lattice is the set of all sums with integer coefficients . The basis is a presentation of the lattice; the lattice is the object the presentation names. In dimension at least two, the same lattice has infinitely many different bases; in dimension one, admits only and as bases.
Every lattice-based scheme in Part II rests on a claim about the lattice as an object. The shortest nonzero vector is hard to find, a noisy version of the closest vector is hard to find, or the lattice carries enough structure that a fast transform is available. The geometric quantities in this chapter, determinant, dual, successive minima, SVP, and CVP, are properties of the lattice rather than of a particular basis (Micciancio & Goldwasser, 2002). Cryptographic instances still come with representations: a public basis may be hard to use, a trapdoor basis may make sampling or decoding easy, and the ring or module representations of Chapter 9 expose the fast arithmetic used by schemes such as ML-KEM. Chapter 7 builds the algebra that makes the basis-invariant side precise: bases over , change of basis, the determinant, the dual lattice, and Minkowski’s bound on how short the shortest vector can be.
A lattice two ways
Section titled “A lattice two ways”Consider the 2D lattice with basis vectors and . Writing these as the columns of a 2-by-2 integer matrix gives the basis
Every point in is of the form for some integer vector . The integer combinations , , , and all sit in . The vector does not: solving gives and , which are not integers.
Now fix a second basis
with columns and . The claim is that and are the same lattice. The quick argument is that sits in , and sits in . Every basis vector on each side is an integer combination of the other side’s basis vectors, so the two lattices share their generating sets and are equal.
The reader can check this in Python:
import numpy as np
# A lattice in Z^2, stored as its basis B with columns b_1, b_2.B1 = np.array([[3, 1], [1, 2]], dtype=np.int64)B2 = np.array([[3, 4], [1, 3]], dtype=np.int64)
def lattice_point(B, x): return tuple(int(a) for a in B @ np.asarray(x, dtype=np.int64))
print("b_1 =", lattice_point(B1, [1, 0]))# ==> b_1 = (3, 1)print("b_2 =", lattice_point(B1, [0, 1]))# ==> b_2 = (1, 2)print("b'_2 =", lattice_point(B2, [0, 1]))# ==> b'_2 = (4, 3)print("b_1 + b_2 (in L(B1)) =", lattice_point(B1, [1, 1]))# ==> b_1 + b_2 (in L(B1)) = (4, 3)Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch07/, one file per block. Appendix C covers the clone and the environment they run on.
The two bases generate the same set. The next section names the algebraic structure underlying this equality. The matrix with is an integer matrix with determinant , called a unimodular matrix, and any two bases of the same lattice are related by some such .
The algebra
Section titled “The algebra”Basis over Z
Section titled “Basis over Z”A basis of a full-rank lattice is a set of linearly independent vectors in such that every element of is a unique integer combination of the . The skeleton of this definition matches Chapter 2’s basis over a field : independent vectors that span every element of the structure exactly once. The important difference is that the coefficients come from , not from a field (Micciancio & Goldwasser, 2002).
is a commutative ring with identity but not a field, so “basis over ” means something strictly more restricted than “basis over or ”. The restriction shows up as soon as the reader tries to rescale a basis vector. Over , any nonzero multiple of generates the same line; over , only does. The vector generates the strict sublattice of even-coefficient combinations and has index 2 in the original lattice (Micciancio & Goldwasser, 2002). This is the narrower meaning of basis that Chapter 2 forward-pointed to.
Unimodular change of basis
Section titled “Unimodular change of basis”An -by- integer matrix is unimodular when its determinant is . Equivalently, is unimodular when both and are integer matrices, which follows from the adjugate formula applied to an integer (Micciancio & Goldwasser, 2002). The group of unimodular matrices is denoted .
The central theorem of this section is that two bases generate the same lattice exactly when they are related by a unimodular change of basis.
Theorem (basis equivalence). Let be two full-rank matrices. Then if and only if there exists a unimodular matrix with (Micciancio & Goldwasser, 2002).
The proof fits in two paragraphs. Suppose first that for some unimodular . Every column of is then an integer combination of the columns of (the coefficients are the columns of , which are integers), so . The reverse inclusion follows by writing and observing that is also an integer matrix because is unimodular.
Conversely, suppose . Every column of sits in , so there is an integer matrix with . Running the same argument with the roles of and swapped produces an integer matrix with , hence and . Taking determinants of gives . Both factors are integers, so each is . So is unimodular.
Concretely, the change-of-basis matrix for the two bases above is
with . So is unimodular and the claim that and generate the same lattice is formally correct.
The determinant and the fundamental parallelepiped
Section titled “The determinant and the fundamental parallelepiped”The determinant of a full-rank lattice is defined by for any basis of . Basis-invariance follows from the equivalence theorem: if with unimodular , then , and taking absolute values gives . So is a property of alone, not of the choice of basis (Micciancio & Goldwasser, 2002).
The determinant has a geometric interpretation. The fundamental parallelepiped of a basis is the half-open region
Its -dimensional volume is by the Jacobian of the linear map , so is the volume of the fundamental parallelepiped of any basis (Micciancio & Goldwasser, 2002). An equivalent way to think about this: is the amount of -volume per lattice point. A lattice with a small determinant is dense, and a lattice with a large determinant is sparse.
The running example has , so . Every fundamental parallelepiped of has area , regardless of which basis the reader uses to draw it.
Figure 7.1 shows with and drawn as arrows and the fundamental parallelepiped shaded.
The interior of the fundamental parallelepiped contains no lattice point. Half-open translates of by lattice vectors tile the plane with exactly one lattice point per tile. Closing the parallelogram on every side would double-count boundary points.
The dual lattice
Section titled “The dual lattice”Given a full-rank lattice , the dual lattice is
For a lattice with basis , the dual is itself a full-rank lattice, and a basis for it is given by (Micciancio & Goldwasser, 2002). The derivation is two lines. A vector sits in exactly when is an integer for every . Evaluating the condition at shows that every entry of is an integer, so , which is equivalent to for .
Two facts about the dual matter in the rest of Part II. First, the dual-of-dual identity gives . The double dual returns the original lattice. Second, when is an integer matrix, the dual basis is generally rational rather than integer, so need not sit inside even when . The special case is self-dual, and every other full-rank acquires fractional dual vectors: forces , while rules out . The dual lives in the same real span as but at a different scale. Chapter 9 uses the dual lattice to state the Ring-LWE and Module-LWE hardness assumptions; Chapter 7 supplies the definition, the computational identity, and nothing more. Pontryagin duality and the algebraic dual are out of scope.
Successive minima and Minkowski’s bound
Section titled “Successive minima and Minkowski’s bound”The successive minima of a full-rank lattice are a sequence of real numbers
The value is the smallest real such that contains linearly independent vectors of Euclidean norm at most . So is the length of the shortest nonzero vector in , and is the smallest radius that captures linearly independent lattice vectors (Micciancio & Goldwasser, 2002).
Minkowski’s theorem (first bound). For every full-rank lattice ,
This is the form of Minkowski’s bound cited throughout the rest of this book (Micciancio & Goldwasser, 2002). The proof applies Minkowski’s convex body theorem to a suitably scaled centrally symmetric convex body, such as a cube, and then converts the resulting coordinate bound into the Euclidean norm bound. The full argument is out of scope here. Micciancio and Goldwasser give it in full (Micciancio & Goldwasser, 2002). What matters downstream is the algorithmic gap the bound leaves open, which Chapter 13 takes up.
Minkowski’s bound is an existence statement: it guarantees that a short vector is present in , but it does not identify which one. Finding the shortest vector, or finding a vector within a factor of , is the shortest vector problem and its approximate variant, stated later in this chapter.
The four shortest nonzero lattice vectors of the running example are and , all of length , and no lattice vector is shorter. The argument is a finite case check on the Gram matrix
which gives . The nine values over attain minimum nonzero value exactly at . Whenever or , the form is at least , so no lattice vector is shorter than . This gives , comfortably below Minkowski’s bound (Micciancio & Goldwasser, 2002).
Building a lattice in Python
Section titled “Building a lattice in Python”The full implementation with a pytest suite lives at solutions/ch07-lattices/. The snippets below show the core idea of each operation in numpy.
Start with the lattice itself. A full-rank lattice in dimension is stored as an integer basis matrix whose columns are the basis vectors, and a lattice point is just the matrix-vector product for some integer vector :
import numpy as np
# The basis B_1 from the start of the chapter, columns are b_1 and b_2.B1 = np.array([[3, 1], [1, 2]], dtype=np.int64)
def lattice_point(B, x): return tuple(int(a) for a in B @ np.asarray(x, dtype=np.int64))
# A handful of lattice points in L(B_1).print(lattice_point(B1, [ 0, 0]))# ==> (0, 0)print(lattice_point(B1, [ 1, 1]))# ==> (4, 3)print(lattice_point(B1, [ 2, -1]))# ==> (5, 0)print(lattice_point(B1, [-1, 2]))# ==> (-1, 3)The change-of-basis check reduces to solving the integer linear system and testing whether is unimodular:
import numpy as np
B1 = np.array([[3, 1], [1, 2]], dtype=np.int64)B2 = np.array([[3, 4], [1, 3]], dtype=np.int64)
# Solve B_1 U = B_2 for U over the rationals; unimodular iff integer and det +/-1.U = np.linalg.solve(B1.astype(float), B2.astype(float))U_int = np.round(U).astype(np.int64)
print("U =", U_int.tolist())# ==> U = [[1, 1], [0, 1]]print("U is integer?", bool(np.allclose(U, U_int)))# ==> U is integer? Trueprint("det U =", int(round(np.linalg.det(U_int.astype(float)))))# ==> det U = 1The determinant of the lattice is the absolute value of the determinant of any basis, and the equivalence theorem guarantees it is invariant under unimodular change of basis:
import numpy as np
B1 = np.array([[3, 1], [1, 2]], dtype=np.int64)U = np.array([[1, 1], [0, 1]], dtype=np.int64)B2 = B1 @ U
det_B1 = abs(int(round(np.linalg.det(B1.astype(float)))))det_B2 = abs(int(round(np.linalg.det(B2.astype(float)))))print("|det B1| =", det_B1)# ==> |det B1| = 5print("|det B2| =", det_B2)# ==> |det B2| = 5The dual basis is , computed via numpy.linalg.inv and a transpose:
import numpy as np
B = np.array([[3, 1], [1, 2]], dtype=float)D = np.linalg.inv(B).T
# The dual basis is rational; displaying 5 * D shows it as a small integer matrix.print("5 * D =", np.round(5 * D).astype(np.int64).tolist())# ==> 5 * D = [[2, -1], [-1, 3]]
# B^T @ D should equal I: every (dual, primal) pair has the right inner product.identity = B.T @ Dprint("B^T @ D rounded =", np.round(identity, 9).tolist())# ==> B^T @ D rounded = [[1.0, 0.0], [0.0, 1.0]]The dual-of-dual identity is worth seeing explicitly:
import numpy as np
B = np.array([[3, 1], [1, 2]], dtype=float)D = np.linalg.inv(B).TDD = np.linalg.inv(D).Tprint("dual-of-dual equals B?", bool(np.allclose(DD, B)))# ==> dual-of-dual equals B? TrueFinally, Minkowski’s bound for the running example is . An exhaustive search over small integer coefficients finds an explicit lattice vector under the bound (in fact, well under it):
import mathimport numpy as np
B = np.array([[3, 1], [1, 2]], dtype=np.int64)n = B.shape[1]det_L = abs(int(round(np.linalg.det(B.astype(float)))))bound = math.sqrt(n) * (det_L ** (1.0 / n))print("det(L) =", det_L)# ==> det(L) = 5print("Minkowski bound =", round(bound, 4))# ==> Minkowski bound = 3.1623
best_len = float("inf")best_coef = Nonefor i in range(-3, 4): for j in range(-3, 4): if (i, j) == (0, 0): continue v = B @ np.array([i, j], dtype=np.int64) length = float(np.linalg.norm(v)) if length < best_len: best_len = length best_coef = (i, j)
v = B @ np.array(best_coef, dtype=np.int64)print("shortest coefficients:", best_coef)# ==> shortest coefficients: (-1, 1)print("shortest vector:", tuple(int(x) for x in v))# ==> shortest vector: (-2, 1)print("length =", round(best_len, 4))# ==> length = 2.2361The shortest vector has length , safely below the Minkowski bound of . The chapter’s pytest suite at tests/ch07/ runs the same checks on larger 2D and 3D examples and confirms the bound holds on every one of them.
SVP, CVP, and what the attacks look like
Section titled “SVP, CVP, and what the attacks look like”The two algorithmic problems Part II cares about are stated directly in terms of the successive minima. The first asks for a shortest vector, and the second asks for a lattice vector closest to a target point.
Shortest vector problem (SVP). The input is a basis of a full-rank lattice . The output is a nonzero vector with (Micciancio & Goldwasser, 2002).
Closest vector problem (CVP). The input is a basis of a full-rank lattice together with a target point . The output is a vector minimizing (Micciancio & Goldwasser, 2002).
Both problems have approximate and gap variants. Fix a slack factor .
- The approximate shortest vector problem asks for a nonzero with (Micciancio & Goldwasser, 2002).
- The gap shortest vector problem is the promise decision problem: given and a threshold , decide whether or , promised one case holds (Micciancio & Goldwasser, 2002).
- The approximate closest vector problem asks for with .
- The gap closest vector problem is the corresponding promise decision problem: decide whether or .
- The shortest independent vectors problem asks for linearly independent lattice vectors of maximum norm at most (Micciancio & Regev, 2009, sec. 2).
These approximate and gap problems are the language used in worst-case-to-average-case reductions. Regev’s 2009 result says, roughly, that efficient algorithms for suitable LWE instances would yield quantum algorithms for worst-case lattice problems such as and , at an approximation factor of , which grows with the dimension and with the reciprocal of the LWE noise rate . The exact statement depends on the modulus and error rate. Chapter 8 states the parameterized version rather than treating as an unspecified constant (Regev, 2009).
Minkowski’s bound is an upper bound on , which is the output norm of SVP. The bound shows that a short vector exists; it does not show how to find one. Closing the gap between “a short vector exists by Minkowski” and “here is an algorithm that produces one” is what lattice cryptanalysis is about. If an efficient algorithm closed that gap for the approximation factors and structured instances used in deployed schemes, the standard hardness assumptions behind Part II would be seriously undermined. Chapter 13 walks BKZ and its derivatives, explains how concrete attack estimates depend on a block-size parameter, and shows how ML-KEM’s parameters are evaluated against those estimates (Albrecht et al., 2024; Avanzi et al., 2021; National Institute of Standards and Technology, 2024).
One observation closes the section. SVP in dimension two is easy. A handful of integer combinations of the basis vectors can be checked by brute force, and a two-dimensional reduction algorithm (Lagrange’s method, a predecessor of the Lenstra-Lenstra-Lovász (LLL) algorithm) finds the shortest vector in polynomial time. ML-KEM-512 fixes a degree- polynomial ring with and a module of rank , giving coefficients over with (National Institute of Standards and Technology, 2024). The concrete attacks are not simply “SVP in dimension 512”. They reduce the Module-LWE instance to -ary lattice problems whose attack dimensions and costs depend on the chosen primal, dual, or hybrid attack model. Chapter 13 explains how BKZ-style estimates turn those structured Module-LWE instances into concrete security estimates.
Tradeoffs inside Part II
Section titled “Tradeoffs inside Part II”Lattices, bases, determinants, and dual lattices are the ambient objects every subsequent Part II chapter rests on.
| Chapter | What it adds to the objects built here |
|---|---|
| 8 | Learning with errors (LWE), a computational assumption tied to -ary lattices |
| 9 | Ring-LWE and Module-LWE: from plain integer lattices to structured modules |
| 10 | Regev encryption, the cleanest lattice public-key encryption scheme |
| 11 | ML-KEM, from Module-LWE plus the Fujisaki-Okamoto transform of Chapter 5 |
| 12 | ML-DSA, which adds Module-SIS to Module-LWE and signs by Fiat-Shamir with aborts |
| 13 | Lattice cryptanalysis: BKZ, reduction, and the estimates that set ML-KEM’s parameters |
Two of those rows are easy to misread. Read LWE as noisy linear algebra modulo . Geometrically, a batch of LWE samples is a bounded-distance-decoding or noisy-CVP-style instance: the observed vector sits close to a structured -ary lattice point, and the adversary must either recover the secret (search LWE) or tell the noisy samples from uniform (decisional LWE) (Regev, 2009). The ring that Chapter 9 works over is not itself a lattice; it is the modular arithmetic layer used to describe Ring-LWE and Module-LWE instances whose underlying hardness is analyzed through structured -ary lattices.
The dual lattice earns its keep once the original lattice acquires ring structure. In the Ring-LWE and Module-LWE setting, the dual-lattice story is usually formulated before reduction modulo , in the number-field and ring-of-integers setting: there the dual of the ring of integers is a fractional ideal related to the inverse different. Chapter 9 picks that up where it states the Ring-LWE hardness reduction, whose Gaussian widths and dual ideals are stated in the canonical embedding rather than in the coefficient representation concrete schemes compute with modulo . The general definition from the algebra section above is what Chapter 9 draws on.
Chapter 8 is the immediate next step: it turns the objects built here into the LWE hardness assumption.
Exercises
Section titled “Exercises”Exercise 1. Verify a unimodular change of basis. Take and from the start of the chapter. Compute by hand, check that every entry is an integer, and compute . Then construct a second way: express each column of as an integer combination of the columns of directly, and collect the coefficients into a matrix. The two constructions must agree.
Exercise 2. Dual-of-dual on a 3D lattice. Fix the basis
Compute the dual basis in Python using numpy.linalg.inv and a transpose. Compute the dual of in the same way, call it , and verify that equals up to floating-point error. Explain in one sentence why the entries of are in general rational rather than integer.
Exercise 3. Minkowski’s bound on a diagonal lattice. Take as a basis of a full-rank lattice . Compute and Minkowski’s bound in Python. Then find an explicit nonzero lattice vector whose Euclidean norm is below the bound, and report its integer coefficients in the given basis.
Exercise 4. State SVP and CVP from memory. Without looking at the section above, write the shortest vector problem and the closest vector problem with their inputs and outputs. Then state the approximate versions and , and the promise-decision versions and . Check your statements against the definitions in Micciancio-Goldwasser 2002 (Micciancio & Goldwasser, 2002) and Regev 2009 (Regev, 2009), and note any differences.
Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 7. A separate track, for rebuilding rather than reading: the package exercises/ch07-lattices has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch07 to grade your version against the suite that proves the reference one.
References
Section titled “References”Last updated: