Building Lattice Reduction (LLL) Intuition
May 30, 2026 · 16 min read
The Lenstra–Lenstra–Lovász (LLL) algorithm efficiently transforms a “bad” (long and non-orthogonal) basis for a lattice into a “pretty good” (short and nearly orthogonal) basis for the same lattice. This transformation is known as lattice reduction, and it has incredibly useful applications. For example, there is a famous attack against ECDSA implementations that leverage biased RNGs that can lead to private key recovery.
However, learning why LLL works can be pretty rough. Most material covering LLL seems targeted towards mathematicians, requiring you to spend a lot of time trying to weasel out the intuition and mechanics of the algorithm. This blog post is a semi organized brain dump of that process, designed to slowly ratchet down the hand waving so you can read until you are happy with your level of understanding.
LLL in Relation to Euclid’s Algorithm
LLL is frequently compared to Euclid’s Algorithm for finding GCDs. While it is an imperfect analogy, at a high-level they share a core similarity: both LLL and Euclid’s algorithm can be broken down into two repeated steps: “reduction” and “swap”.
Consider the pseudocode for both algorithms:
def euclid_gcd(a, b): if b == 0: # base case return a x = a % b # reduction step return euclid_gcd(b, x) # swap step# Don't try to grok this yet...def lll(basis): while k <= n: for j in reversed(range(k)): # reduction step loop mu = calculate_mu(k, j) basis[k] = basis[k] - round(mu) * basis[j] # vector reduction # update orthogonalized basis
if lovasz_condition: k += 1 else: basis[k], basis[k-1] = basis[k-1], basis[k] # swap step # update orthogonalized basis k = max(k-1, 1) return basisIf you squint a bit, you can see the structural similarities. LLL is essentially an extension of Euclid’s algorithm that applies to a set of -dimensional vectors instead of simple integers.
LLL in Relation to Gram-Schmidt
Another closely related process is the Gram-Schmidt (GS) orthogonalization process. At a high level, Gram-Schmidt takes an input basis for a vector space and returns an orthogonalized basis (where all vectors are mutually perpendicular) that spans the same space. It achieves this by leveraging vector projections to “decompose” each vector into related components and removing redundant components.
You might ask: “Why don’t we just use Gram-Schmidt to reduce our lattice basis?”
Unfortunately, we can’t because Gram-Schmidt is not guaranteed to produce orthogonal vectors that lie within our lattice. Let’s look at this comparison:

Notice how the orthogonalized (dashed grey) vectors do not touch actual lattice points? Although we cannot use GS directly to get our lattice basis, LLL still uses Gram-Schmidt as a crucial guide and subroutine.
LLL in Relation to Gaussian Lattice Reduction
Unless you are a sorceress, starting with 2-dimensional lattice basis reduction is highly recommended. Thankfully, we have Gauss’s algorithm for reducing bases in dimension 2, which is perfect for a few reasons:
- Gaussian lattice reduction is structurally similar to LLL.
- It acts as a perfect bridge between Euclid’s algorithm and LLL.
- 2D vectors are easy to graph and visualize.
Gauss’s algorithm is defined as follows:
def gauss_reduction(v1, v2): while True: if v2.norm() < v1.norm(): v1, v2 = v2, v1 # swap step m = round((v1 * v2) / (v1 * v1)) if m == 0: return (v1, v2) v2 = v2 - m * v1 # reduction stepLet’s break this down. gauss_reduction takes two vectors representing our lattice basis. The swap step ensures that the length of is smaller than . This guarantees that the resulting basis is ordered by length, which is crucial for proving the algorithm’s termination and efficiency.
What does represent? It is the scalar projection of onto (the longer vector onto the shorter one). This is the exact same scalar produced during Gram-Schmidt, except we round it to the nearest integer to ensure our reduced vectors remain within the integer span of the lattice.
Let’s visualize this. First, we project onto . Here is what the projected vector looks like before and after rounding:

We then compute our new reduced vector . Here is the reduction step before and after rounding :

By rounding prior to subtraction, we “knock over” our new reduced vector so that it lands exactly on a valid lattice point. Crucially, the reduced is shorter than the original , and the resulting basis vectors are “nearly” orthogonal.
Gaussian 2D reduction is guaranteed to terminate and return a short, nearly orthogonal basis.
LLL tl;dr
LLL extends Gauss’s 2D algorithm to work with dimensions.
At a high level, LLL iterates through the input basis vectors and performs a length-reduction (similar to Gauss’s algorithm). However, because we are dealing with vectors, the ordering of the input basis matters. To sort the basis vectors by length, LLL uses a heuristic called the Lovász condition to determine if adjacent vectors need to be swapped. The algorithm terminates once all basis vectors have been successfully size-reduced and ordered.
A Deeper Dive into LLL
To understand the mechanics of LLL, let’s walk through its core implementation details:
def LLL(B, delta=0.75): Q = gram_schmidt(B)
def mu(i, j): v = B[i] u = Q[j] return (v * u) / (u * u)
n, k = B.nrows(), 1 while k < n: # Length reduction step for j in reversed(range(k)): if abs(mu(k, j)) > 0.5: B[k] = B[k] - round(mu(k, j)) * B[j] Q = gram_schmidt(B)
# Swap step (Lovász condition) if Q[k] * Q[k] >= (delta - mu(k, k-1)**2) * (Q[k-1] * Q[k-1]): k = k + 1 else: B[k], B[k-1] = B[k-1], B[k] Q = gram_schmidt(B) k = max(k-1, 1)
return BThere is some seemingly magical logic here. Let’s demystify it piece by piece, starting with .
The Coefficient
The coefficient is the scalar projection of the -th lattice basis vector () onto the -th Gram-Schmidt orthogonalized vector ():
Unlike Gauss’s algorithm, we are not projecting a lattice vector onto another lattice vector; we are projecting it onto a Gram-Schmidt orthogonalized vector, as shown below:

Since we cannot use the “ideal” orthogonalized GS matrix directly (as its vectors don’t lie in the lattice), acts as our reference guide to measure how far our lattice vectors are from being perfectly orthogonal.
Length Reduction
Isolating the size-reduction loop:
for j in reversed(range(k)): if abs(mu(k, j)) > 0.5: B[k] = B[k] - round(mu(k, j)) * B[j] Q = gram_schmidt(B)The loop iterates from down to and checks if the absolute value of is greater than . Since we round to the nearest integer, any coefficient less than rounds to , meaning no reduction would take place.
This loop performs size-reduction on against all preceding vectors. It is mathematically equivalent to the Gram-Schmidt reduction step, but with integer rounding:
After modifying the basis vector, we re-run Gram-Schmidt to keep our reference vectors up to date.
The Lovász Condition and the Swap Step
Once a vector is size-reduced, LLL checks the Lovász condition to decide whether to move forward or swap:
if Q[k] * Q[k] >= (delta - mu(k, k-1)**2) * (Q[k-1] * Q[k-1]): k = k + 1else: B[k], B[k-1] = B[k-1], B[k] Q = gram_schmidt(B) k = max(k-1, 1)Think of LLL as a sorting algorithm: it is a vector sorting algorithm that occasionally shrinks vectors, requiring it to re-sort.
If the Lovász condition is met, the vector at index is in a “good” position relative to , and we increment . If it fails, the vector at is significantly shorter than , so we swap them and decrement to re-evaluate and re-reduce the swapped vector in its new position.
The parameter is a constant chosen between and (usually or ) that determines how strictly we enforce the length sorting.
Things that Stumped Me When Learning LLL
Is the Gaussian length-reduction step guaranteed to provide short and nearly orthogonal vectors?
Yes. Rounding might seem like it would ruin the orthogonality, but the math guarantees that the angle between our reduced vectors will always lie between and .
This guarantee stems from the fact that ‘s projection onto will always have a magnitude of at most because we have removed all possible integer components of from . Using the definition of vector projection:

Since Gaussian reduction ensures that our final vector is shorter than ( when ordered), the value of must be at most , forcing to be between and (nearly orthogonal!).
Why doesn’t Gaussian lattice reduction easily generalize to higher dimensions?
As explained in “Mathematics of Public Key Cryptography”:
_“Choosing the right linear combination to size-reduce using is equivalent to solving the Closest Vector Problem (CVP) in a sublattice, which is NP-hard. Furthermore, there is no guarantee that the resulting basis actually has good properties in high dimensions.”_
LLL overcomes this by breaking the -dimensional problem down into a series of -dimensional sub-problems (adjacent vector pairs) and solving them one pair at a time. This local size-reduction, combined with length-sorting approximations, works incredibly well in practice.
How does LLL use Gram-Schmidt as a guide?
A lattice basis is close to orthogonal if the lengths of its Gram-Schmidt vectors do not decrease too rapidly. By using the Lovász condition to compare adjacent Gram-Schmidt vectors, LLL prevents their lengths from dropping too fast, keeping the entire lattice basis “sufficiently close to orthogonal”.