Skip to article frontmatterSkip to article content

fpylll: A Python Interface to the fplll Lattice Reduction Library

ZKPunk

Overview

fpylll is a Python wrapper for the fplll lattice reduction library, providing efficient implementations of lattice reduction algorithms including:

  • LLL (Lenstra-Lenstra-Lovász) reduction
  • BKZ (Block Korkine-Zolotarev) reduction
  • Enumeration algorithms for SVP/CVP
  • Gram-Schmidt orthogonalization
  • Various utility functions for lattice cryptanalysis

This notebook provides a comprehensive introduction to fpylll with mathematical foundations, algorithm explanations, and practical examples for lattice-based cryptography research.

Table of Contents

  1. Mathematical Background
  2. Installation and Setup
  3. Integer Matrices
  4. Gram-Schmidt Orthogonalization
  5. LLL Algorithm
  6. BKZ Algorithm
  7. Enumeration and SVP/CVP
  8. Lattice Cryptanalysis Examples
  9. Utility Functions and Advanced Features
  10. Advanced Cryptographic Applications
  11. Performance Optimization and Best Practices
  12. Conclusion and Further Resources

1. Mathematical Background

1.1 Lattices

A lattice Λ\Lambda in Rn\mathbb{R}^n is a discrete additive subgroup generated by linearly independent vectors b1,,bdRn\mathbf{b}_1, \ldots, \mathbf{b}_d \in \mathbb{R}^n:

Λ=Λ(b1,,bd)={i=1dxibi:xiZ}\Lambda = \Lambda(\mathbf{b}_1, \ldots, \mathbf{b}_d) = \left\{ \sum_{i=1}^d x_i \mathbf{b}_i : x_i \in \mathbb{Z} \right\}

The vectors b1,,bd\mathbf{b}_1, \ldots, \mathbf{b}_d form a basis of the lattice, represented as a matrix:

B=(b1TbdT)Zd×n\mathbf{B} = \begin{pmatrix} \mathbf{b}_1^T \\ \vdots \\ \mathbf{b}_d^T \end{pmatrix} \in \mathbb{Z}^{d \times n}

Key properties:

  • Rank: rank(Λ)=d\text{rank}(\Lambda) = d (number of linearly independent basis vectors)
  • Determinant: det(Λ)=det(BBT)\det(\Lambda) = \sqrt{\det(\mathbf{B}\mathbf{B}^T)}
  • Fundamental parallelepiped: P(B)={BTx:x[0,1)d}\mathcal{P}(\mathbf{B}) = \{\mathbf{B}^T \mathbf{x} : \mathbf{x} \in [0,1)^d\}

1.2 Hard Lattice Problems

Shortest Vector Problem (SVP)

Given a lattice basis B\mathbf{B}, find the shortest non-zero vector vΛ(B)\mathbf{v} \in \Lambda(\mathbf{B}):

λ1(Λ)=minvΛ{0}v\lambda_1(\Lambda) = \min_{\mathbf{v} \in \Lambda \setminus \{\mathbf{0}\}} \|\mathbf{v}\|

Closest Vector Problem (CVP)

Given a lattice basis B\mathbf{B} and target vector tRn\mathbf{t} \in \mathbb{R}^n, find vΛ(B)\mathbf{v} \in \Lambda(\mathbf{B}) that minimizes tv\|\mathbf{t} - \mathbf{v}\|.

Approximate Variants

  • γ\gamma-SVP: Find vΛ\mathbf{v} \in \Lambda with vγλ1(Λ)\|\mathbf{v}\| \leq \gamma \cdot \lambda_1(\Lambda)
  • γ\gamma-CVP: Find vΛ\mathbf{v} \in \Lambda with tvγdist(t,Λ)\|\mathbf{t} - \mathbf{v}\| \leq \gamma \cdot \text{dist}(\mathbf{t}, \Lambda)

1.3 Gram-Schmidt Orthogonalization

Given basis vectors b1,,bd\mathbf{b}_1, \ldots, \mathbf{b}_d, the Gram-Schmidt process produces orthogonal vectors:

b1=b1\mathbf{b}_1^* = \mathbf{b}_1
bi=bij=1i1μi,jbj,where μi,j=bi,bjbj2\mathbf{b}_i^* = \mathbf{b}_i - \sum_{j=1}^{i-1} \mu_{i,j} \mathbf{b}_j^*, \quad \text{where } \mu_{i,j} = \frac{\langle \mathbf{b}_i, \mathbf{b}_j^* \rangle}{\|\mathbf{b}_j^*\|^2}

This gives the QR decomposition: B=QR\mathbf{B} = \mathbf{Q} \mathbf{R} where:

  • Q\mathbf{Q} has orthonormal columns bi/bi\mathbf{b}_i^*/\|\mathbf{b}_i^*\|
  • R\mathbf{R} is upper triangular with Ri,j=μi,jbjR_{i,j} = \mu_{i,j} \|\mathbf{b}_j^*\| for j<ij < i

1.4 Quality Measures

Hermite Factor

γ=(b1det(Λ)1/d)d\gamma = \left(\frac{\|\mathbf{b}_1\|}{\det(\Lambda)^{1/d}}\right)^d

Root Hermite Factor

δ=(b1det(Λ)1/d)\delta = \left(\frac{\|\mathbf{b}_1\|}{\det(\Lambda)^{1/d}}\right)

For random lattices, we expect δd2πe0.48d\delta \approx \sqrt{\frac{d}{2\pi e}} \approx 0.48\sqrt{d} by the Gaussian heuristic.

2. Installation and Setup

2.1 Installation Options

fpylll can be installed through several methods:

  1. PyPI (recommended):

    pip install fpylll
  2. Conda-Forge:

    conda install -c conda-forge fpylll
  3. SageMath: fpylll comes pre-installed with SageMath

  4. From Source: For development or latest features

    git clone https://github.com/fplll/fpylll.git
    cd fpylll
    pip install -r requirements.txt
    python setup.py install

2.2 Basic Import and Verification

# Import fpylll and related libraries
import fpylll
from fpylll import IntegerMatrix, GSO, LLL, BKZ, FPLLL
from fpylll.algorithms.bkz import BKZReduction
from fpylll.tools.quality import basis_quality
import numpy as np
import matplotlib.pyplot as plt

# Verify installation
print(f"fpylll version: {fpylll.__version__}")
print(f"Available float types: {fpylll.config.float_types}")

# Set random seed for reproducibility
FPLLL.set_random_seed(42)
print("fpylll successfully imported and configured!")
fpylll version: 0.6.3
Available float types: ('d', 'dpe', 'mpfr')
fpylll successfully imported and configured!

3. Integer Matrices

The IntegerMatrix class is the foundation of fpylll, representing lattice bases as dense matrices over the integers. It supports arbitrary precision arithmetic and various construction methods.

3.1 Construction Methods

Basic Construction

# 1. Create empty matrix and populate manually
A = IntegerMatrix(3, 3)
A[0, 0] = 1; A[0, 1] = 2; A[0, 2] = 3
A[1, 0] = 4; A[1, 1] = 5; A[1, 2] = 6  
A[2, 0] = 7; A[2, 1] = 8; A[2, 2] = 9
print("Manual construction:")
print(A)

# 2. Create from Python lists/arrays
B = IntegerMatrix.from_matrix([[1, 0, 2], [0, 1, 3], [0, 0, 1]])
print("\nFrom Python list:")
print(B)

# 3. Create identity matrix
I = IntegerMatrix.identity(4)
print("\nIdentity matrix:")
print(I)

# 4. Matrix properties
print(f"\nMatrix dimensions: {A.nrows} × {A.ncols}")
print(f"Matrix norm of first row: {A[0].norm()}")
print(f"Matrix determinant estimate: {A.get_max_exp()}")
Manual construction:
[ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 9 ]

From Python list:
[ 1 0 2 ]
[ 0 1 3 ]
[ 0 0 1 ]

Identity matrix:
[ 1 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]

Matrix dimensions: 3 × 3
Matrix norm of first row: 3.7416573867739413
Matrix determinant estimate: 4

3.2 Random Matrix Generation

fpylll provides several algorithms for generating random lattice bases, each with different cryptographic properties:

  • uniform: Random entries with uniform distribution
  • ntrulike: NTRU-style lattices (cryptographically relevant)
  • qary: q-ary lattices (related to SIS/LWE problems)
  • intrel: Integer relation problems
  • simdioph: Simultaneous Diophantine approximation
  • trg: Triangular matrices with geometric progression
# Generate different types of random lattices
print("=== Random Lattice Generation Examples ===\n")

# 1. Uniform random matrix
uniform_lattice = IntegerMatrix.random(4, "uniform", bits=10)
print("1. Uniform random lattice (4×4, 10-bit entries):")
print(uniform_lattice)

# 2. NTRU-like lattice  
ntru_lattice = IntegerMatrix.random(5, "ntrulike", q=127)
print("\n2. NTRU-like lattice (5×10, q=127):")
print(ntru_lattice)

# 3. q-ary lattice (SIS/LWE related)
qary_lattice = IntegerMatrix.random(6, "qary", q=127, k=3)
print("\n3. q-ary lattice (6×6, q=127, k=3):")
print(qary_lattice)

# 4. Integer relation lattice
intrel_lattice = IntegerMatrix.random(4, "intrel", bits=20)
print("\n4. Integer relation lattice (4×5, 20-bit):")
print(intrel_lattice)

# 5. Compute and display basic properties
def analyze_lattice(A, name):
    print(f"\n--- Analysis of {name} ---")
    print(f"Dimensions: {A.nrows}×{A.ncols}")
    print(f"First vector norm: {A[0].norm():.2f}")
    print(f"Last vector norm: {A[A.nrows-1].norm():.2f}")
    if A.nrows == A.ncols:
        # Estimate volume for square matrices
        volume_est = 1
        for i in range(A.nrows):
            volume_est *= A[i].norm()
        print(f"Volume estimate: {volume_est**(1/A.nrows):.2f}")

analyze_lattice(uniform_lattice, "Uniform Lattice")
analyze_lattice(ntru_lattice, "NTRU Lattice")
=== Random Lattice Generation Examples ===

1. Uniform random lattice (4×4, 10-bit entries):
[ 697 665 711 657 ]
[ 412 957 201 883 ]
[ 898 857 990 364 ]
[ 228 783 477 702 ]

2. NTRU-like lattice (5×10, q=127):
[ 1 0 0 0 0   8  99 125  79  70 ]
[ 0 1 0 0 0  70   8  99 125  79 ]
[ 0 0 1 0 0  79  70   8  99 125 ]
[ 0 0 0 1 0 125  79  70   8  99 ]
[ 0 0 0 0 1  99 125  79  70   8 ]
[ 0 0 0 0 0 127   0   0   0   0 ]
[ 0 0 0 0 0   0 127   0   0   0 ]
[ 0 0 0 0 0   0   0 127   0   0 ]
[ 0 0 0 0 0   0   0   0 127   0 ]
[ 0 0 0 0 0   0   0   0   0 127 ]

3. q-ary lattice (6×6, q=127, k=3):
[ 1 0 0   2  42  33 ]
[ 0 1 0 125  52  63 ]
[ 0 0 1  51  99  24 ]
[ 0 0 0 127   0   0 ]
[ 0 0 0   0 127   0 ]
[ 0 0 0   0   0 127 ]

4. Integer relation lattice (4×5, 20-bit):
[   2871 1 0 0 0 ]
[ 613224 0 1 0 0 ]
[ 634839 0 0 1 0 ]
[ 490589 0 0 0 1 ]

--- Analysis of Uniform Lattice ---
Dimensions: 4×4
First vector norm: 1365.72
Last vector norm: 1177.03
Volume estimate: 1378.86

--- Analysis of NTRU Lattice ---
Dimensions: 10×10
First vector norm: 191.39
Last vector norm: 127.00
Volume estimate: 155.91

4. Gram-Schmidt Orthogonalization

The MatGSO class provides Gram-Schmidt orthogonalization with on-demand coefficient computation. It maintains:

  • μ coefficients: μi,j=bi,bjbj2\mu_{i,j} = \frac{\langle \mathbf{b}_i, \mathbf{b}_j^* \rangle}{\|\mathbf{b}_j^*\|^2} for i>ji > j
  • r coefficients: ri,j=bi,bjr_{i,j} = \langle \mathbf{b}_i, \mathbf{b}_j^* \rangle for iji \geq j
  • Gram matrix: Gi,j=bi,bjG_{i,j} = \langle \mathbf{b}_i, \mathbf{b}_j \rangle

4.1 Basic Usage

# Create a lattice basis and GSO object
A = IntegerMatrix.random(5, "uniform", bits=10)
print("Original basis:")
print(A)

# Create GSO object and compute orthogonalization
M = GSO.Mat(A)
M.update_gso()

print("\n=== Gram-Schmidt Analysis ===")

# Access μ coefficients  
print("\nμ coefficients (lower triangular):")
for i in range(1, A.nrows):
    for j in range(i):
        mu_ij = M.get_mu(i, j)
        print(f"μ_{i},{j} = {mu_ij:.4f}", end="  ")
    print()

# Access r coefficients (squared norms of b_i*)
print("\nr coefficients (squared norms):")
for i in range(A.nrows):
    r_ii = M.get_r(i, i)
    print(f"r_{i},{i} = ||b*_{i}||² = {r_ii:.2f}")

# Gram matrix elements
print("\nGram matrix (first 3×3):")
for i in range(min(3, A.nrows)):
    for j in range(min(3, A.ncols)):
        if i >= j:
            gram_ij = M.get_gram(i, j)
            print(f"{gram_ij:8.0f}", end=" ")
        else:
            print(f"{'':8s}", end=" ")
    print()

# Quality measures
print(f"\nLog determinant: {M.get_log_det(0, A.nrows):.4f}")
print(f"Root determinant: {M.get_root_det(0, A.nrows):.4f}")
Original basis:
[ 530  12 490 829  71 ]
[ 907 358 462 770 505 ]
[ 124 259 762 958 915 ]
[ 622 605 302 983 957 ]
[  18 941 801 908 573 ]

=== Gram-Schmidt Analysis ===

μ coefficients (lower triangular):
μ_1,0 = 1.1419  
μ_2,0 = 1.0725  μ_2,1 = 0.6302  
μ_3,0 = 1.1272  μ_3,1 = 1.3921  μ_3,2 = 0.5133  
μ_4,0 = 0.9945  μ_4,1 = 0.7763  μ_4,2 = 0.8758  μ_4,3 = 0.4376  

r coefficients (squared norms):
r_0,0 = ||b*_0||² = 1213426.00
r_1,1 = ||b*_1||² = 430044.32
r_2,2 = ||b*_2||² = 851665.52
r_3,3 = ||b*_3||² = 126695.30
r_4,4 = ||b*_4||² = 543384.81

Gram matrix (first 3×3):
 1213426                   
 1385571  2012182          
 1301355  1756969  2418090 

Log determinant: 65.5907
Root determinant: 497889.2129

5. LLL Algorithm

The Lenstra-Lenstra-Lovász (LLL) algorithm finds a reduced basis in polynomial time. A basis is LLL-reduced with parameters (δ,η)(\delta, \eta) if:

  1. Size reduction: μi,jη|\mu_{i,j}| \leq \eta for all i>ji > j (typically η=0.5\eta = 0.5)
  2. Lovász condition: δbi12bi+μi,i1bi12\delta \|\mathbf{b}_{i-1}^*\|^2 \leq \|\mathbf{b}_i^* + \mu_{i,i-1}\mathbf{b}_{i-1}^*\|^2 for all ii (typically δ=0.99\delta = 0.99)

5.1 LLL Reduction Performance

The LLL algorithm guarantees:

  • Approximation factor: b12(d1)/4λ1(Λ)\|\mathbf{b}_1\| \leq 2^{(d-1)/4} \lambda_1(\Lambda)
  • Running time: O(d5nlogB)O(d^5 n \log B) where BB is the max entry size

5.2 Basic LLL Usage

# Create a random lattice for LLL demonstration
print("=== LLL Reduction Example ===\n")

# Generate a challenging lattice (larger entries, less structured)
A = IntegerMatrix.random(8, "uniform", bits=30)
print("Original basis vectors norms:")
for i in range(A.nrows):
    print(f"||b_{i}|| = {A[i].norm():.2f}")

# Compute initial quality measures
def compute_quality(B, name=""):
    """Compute and display basis quality measures"""
    M = GSO.Mat(B)
    M.update_gso()
    
    # Compute Hermite factor
    det = M.get_root_det(0, B.nrows)
    hermite_factor = B[0].norm() / (det ** (1.0/B.nrows))
    
    # Compute orthogonality defect
    volume_basis = 1.0
    volume_gso = 1.0
    for i in range(B.nrows):
        volume_basis *= B[i].norm()
        volume_gso *= M.get_r(i, i) ** 0.5
    
    orthogonality_defect = volume_basis / volume_gso
    
    print(f"\n{name} Quality Measures:")
    print(f"  First vector norm: {B[0].norm():.2f}")
    print(f"  Root determinant: {det:.2f}")
    print(f"  Hermite factor: {hermite_factor:.4f}")
    print(f"  Orthogonality defect: {orthogonality_defect:.2f}")
    
    return hermite_factor, orthogonality_defect

# Measure quality before reduction
hf_before, od_before = compute_quality(A, "BEFORE LLL")

# Perform LLL reduction
print(f"\nPerforming LLL reduction with δ=0.99, η=0.51...")
A_copy = IntegerMatrix(A)  # Make a copy
LLL.reduction(A_copy)

# Measure quality after reduction  
hf_after, od_after = compute_quality(A_copy, "AFTER LLL")

# Show improvement
print(f"\n=== Improvement ===")
print(f"Hermite factor: {hf_before:.4f} → {hf_after:.4f} (factor {hf_before/hf_after:.2f})")
print(f"Orthogonality defect: {od_before:.2f} → {od_after:.2f} (factor {od_before/od_after:.2f})")

# Verify LLL-reducedness
is_reduced = LLL.is_reduced(A_copy)
print(f"Is LLL-reduced: {is_reduced}")

print("\nReduced basis vector norms:")
for i in range(A_copy.nrows):
    print(f"||b_{i}|| = {A_copy[i].norm():.2f}")
=== LLL Reduction Example ===

Original basis vectors norms:
||b_0|| = 1660827349.28
||b_1|| = 1521079728.42
||b_2|| = 1911032678.14
||b_3|| = 1970698185.30
||b_4|| = 1783494983.56
||b_5|| = 1881569976.10
||b_6|| = 1959094268.40
||b_7|| = 1883362263.01

BEFORE LLL Quality Measures:
  First vector norm: 1660827349.28
  Root determinant: 496311780760631552.00
  Hermite factor: 10194247.3008
  Orthogonality defect: 1941.46

Performing LLL reduction with δ=0.99, η=0.51...

AFTER LLL Quality Measures:
  First vector norm: 566792349.48
  Root determinant: 496311780760624512.00
  Hermite factor: 3479001.8248
  Orthogonality defect: 1.91

=== Improvement ===
Hermite factor: 10194247.3008 → 3479001.8248 (factor 2.93)
Orthogonality defect: 1941.46 → 1.91 (factor 1016.79)
Is LLL-reduced: True

Reduced basis vector norms:
||b_0|| = 566792349.48
||b_1|| = 681307914.82
||b_2|| = 805173269.83
||b_3|| = 794442421.31
||b_4|| = 864034234.95
||b_5|| = 815319592.50
||b_6|| = 801468449.48
||b_7|| = 830714504.33

6. BKZ Algorithm

Block Korkine-Zolotarev (BKZ) is a generalization of LLL that provides stronger reduction guarantees at higher computational cost. BKZ with block size β\beta alternates between:

  1. Size reduction in the current block
  2. SVP solving in projected sublattices of dimension β\beta

6.1 BKZ Properties

For BKZ with block size β\beta:

  • Approximation factor: b1γβd/(2β)λ1(Λ)\|\mathbf{b}_1\| \leq \gamma_\beta^{d/(2\beta)} \lambda_1(\Lambda)
  • Hermite factor: δ(β2πe(πβ)1/β)1/(2(β1))\delta \approx \left(\frac{\beta}{2\pi e}(\pi\beta)^{1/\beta}\right)^{1/(2(\beta-1))}
  • Running time: Polynomial in input size, exponential in β\beta

Common block sizes:

  • β=2\beta = 2: Equivalent to LLL
  • β=1030\beta = 10-30: Practical for most applications
  • β>50\beta > 50: Computationally expensive but high quality

6.2 BKZ Implementation

# BKZ reduction example with different block sizes
print("=== BKZ Reduction Comparison ===\n")

# Create a more challenging lattice 
A = IntegerMatrix.random(20, "qary", q=1024, k=10)
print(f"Generated {A.nrows}×{A.ncols} q-ary lattice with q=1024, k=10")

# Helper function for BKZ analysis
def run_bkz_analysis(basis, block_size, max_tours=10):
    """Run BKZ and analyze results"""
    from fpylll.algorithms.bkz2 import BKZReduction as BKZ2
    
    # Create copy for reduction
    B = IntegerMatrix(basis)
    
    # First apply LLL for fair comparison
    LLL.reduction(B)
    
    print(f"\n--- BKZ-{block_size} Analysis ---")
    
    # Measure initial quality after LLL
    M_before = GSO.Mat(B)
    M_before.update_gso()
    det = M_before.get_root_det(0, B.nrows)
    hf_before = B[0].norm() / (det ** (1.0/B.nrows))
    
    print(f"After LLL preprocessing:")
    print(f"  First vector norm: {B[0].norm():.2f}")
    print(f"  Hermite factor: {hf_before:.6f}")
    
    if block_size > 2:
        # Apply BKZ reduction
        from fpylll import BKZ as BKZ_param
        param = BKZ_param.Param(
            block_size=block_size,
            max_loops=max_tours,
            strategies=BKZ_param.DEFAULT_STRATEGY
        )
        
        bkz = BKZ2(GSO.Mat(B))
        bkz(param)
    
    # Measure final quality
    M_after = GSO.Mat(B) 
    M_after.update_gso()
    hf_after = B[0].norm() / (det ** (1.0/B.nrows))
    
    print(f"After BKZ-{block_size}:")
    print(f"  First vector norm: {B[0].norm():.2f}")
    print(f"  Hermite factor: {hf_after:.6f}")
    print(f"  Improvement factor: {hf_before/hf_after:.3f}")
    
    return hf_after, B

# Compare different block sizes
block_sizes = [2, 10, 20]  # Start with smaller block sizes
results = {}

for beta in block_sizes:
    try:
        hf, reduced_basis = run_bkz_analysis(A, beta)
        results[beta] = hf
    except Exception as e:
        print(f"BKZ-{beta} failed: {e}")
        results[beta] = float('inf')

print(f"\n=== BKZ Comparison Summary ===")
print("Block Size | Hermite Factor")
print("-" * 25)
for beta, hf in results.items():
    if hf != float('inf'):
        print(f"BKZ-{beta:2d}     | {hf:.6f}")
    else:
        print(f"BKZ-{beta:2d}     | Failed")

# Theoretical Hermite factors for comparison
print(f"\nTheoretical asymptotic Hermite factors:")
for beta in block_sizes:
    if beta == 2:
        theoretical_hf = float(2**(1/4))  # LLL bound
    else:
        # Simplified BKZ bound - convert to float for formatting
        import math
        theoretical_hf = float((beta/(2*math.pi*math.e) * (math.pi*beta)**(1/beta))**(1/(2*(beta-1))))
    print(f"BKZ-{beta}: δ ≈ {theoretical_hf:.6f}")
=== BKZ Reduction Comparison ===

Generated 20×20 q-ary lattice with q=1024, k=10

--- BKZ-2 Analysis ---
After LLL preprocessing:
  First vector norm: 39.89
  Hermite factor: 28.204610
After BKZ-2:
  First vector norm: 39.89
  Hermite factor: 28.204610
  Improvement factor: 1.000

--- BKZ-10 Analysis ---
After LLL preprocessing:
  First vector norm: 39.89
  Hermite factor: 28.204610
After BKZ-10:
  First vector norm: 39.89
  Hermite factor: 28.204610
  Improvement factor: 1.000

--- BKZ-20 Analysis ---
After LLL preprocessing:
  First vector norm: 39.89
  Hermite factor: 28.204610
After BKZ-20:
  First vector norm: 39.89
  Hermite factor: 28.204610
  Improvement factor: 1.000

=== BKZ Comparison Summary ===
Block Size | Hermite Factor
-------------------------
BKZ- 2     | 28.204610
BKZ-10     | 28.204610
BKZ-20     | 28.204610

Theoretical asymptotic Hermite factors:
BKZ-2: δ ≈ 1.189207
BKZ-10: δ ≈ 0.989469
BKZ-20: δ ≈ 1.009648

7. Enumeration and SVP/CVP

Enumeration is an exact algorithm for solving SVP and CVP by systematically searching through lattice points within a given radius. fpylll implements Kannan’s enumeration with pruning techniques.

7.1 Enumeration Algorithm

The enumeration algorithm:

  1. Uses the Gram-Schmidt basis B\mathbf{B}^*
  2. Searches for lattice vectors v=i=0d1xibi\mathbf{v} = \sum_{i=0}^{d-1} x_i \mathbf{b}_i with v2R2\|\mathbf{v}\|^2 \leq R^2
  3. Employs pruning to reduce the search space

7.2 Babai’s Nearest Plane Algorithm

For CVP approximation, fpylll implements Babai’s nearest plane algorithm:

w=i=0d1t~i+0.5bi\mathbf{w} = \sum_{i=0}^{d-1} \lfloor \tilde{t}_i + 0.5 \rfloor \mathbf{b}_i

where t~=(B)1t\tilde{\mathbf{t}} = (\mathbf{B}^*)^{-1} \mathbf{t} are the coordinates of t\mathbf{t} in the GSO basis.

7.3 Practical Examples

# SVP and CVP examples
print("=== SVP and CVP Examples ===\n")

# Create a small lattice for exact SVP solving
A = IntegerMatrix.random(6, "uniform", bits=8)
LLL.reduction(A)  # Preprocess with LLL

print("Working with 6-dimensional lattice:")
print(A)

# 1. Babai's algorithm for approximate CVP
print(f"\n1. Babai's Nearest Plane Algorithm")
M = GSO.Mat(A)
M.update_gso()

# Choose a random target vector
target = [100, 50, 75, 25, 125, 80]
print(f"Target vector: {target}")

# Apply Babai's algorithm
closest_coeff = M.babai(target)
closest_vector = A.multiply_left(closest_coeff)

print(f"Closest lattice vector: {closest_vector}")
print(f"Distance: {sum((target[i] - closest_vector[i])**2 for i in range(len(target)))**0.5:.2f}")

# 2. Using built-in CVP solver
from fpylll import CVP
closest_vector_cvp = CVP.babai(A, target)
print(f"CVP.babai result: {closest_vector_cvp}")

# 3. Enumeration for small lattices
print(f"\n2. Enumeration Example")
from fpylll import Enumeration

# Create enumeration object
enum = Enumeration(M)

# Set enumeration radius (use first vector as bound)
radius = A[0].norm()**2
print(f"Enumeration radius: {radius**.5:.2f}")

try:
    # Find short vectors (this may be computationally intensive)
    # We use a smaller radius to find multiple solutions
    solutions = enum.enumerate(0, A.nrows, 0.8 * radius, 0)
    
    print(f"Found {len(solutions)} vectors within radius {(0.8 * radius)**0.5:.2f}")
    
    # Display shortest vectors found
    if solutions:
        solutions.sort(key=lambda x: x[1])  # Sort by length
        print("Shortest vectors found:")
        for i, (coeff, length) in enumerate(solutions[:3]):
            vector = A.multiply_left(coeff)
            print(f"  Vector {i+1}: {vector}, length: {length**0.5:.2f}")
            
except Exception as e:
    print(f"Enumeration failed or took too long: {e}")

# 4. Gaussian Heuristic prediction
print(f"\n3. Gaussian Heuristic Analysis")
from fpylll.util import gaussian_heuristic

# Compute Gaussian heuristic prediction
gh_length = gaussian_heuristic(M.r())**0.5
print(f"Gaussian heuristic prediction: {gh_length:.2f}")
print(f"Actual first vector length: {A[0].norm():.2f}")
print(f"Ratio (actual/predicted): {A[0].norm()/gh_length:.3f}")

# 5. Volume and determinant analysis
det = M.get_root_det(0, A.nrows)
print(f"Lattice determinant: {det:.2e}")
print(f"Volume per lattice point: {det:.2e}")
=== SVP and CVP Examples ===

Working with 6-dimensional lattice:
[  -4  -32  -17 -11  -52 -24 ]
[  90   48   -2  42  -36 -83 ]
[   8  -33 -139 -22   95  37 ]
[  52 -101  -20 115   63 -17 ]
[  11   36    5   9 -102 165 ]
[ 135  -90   14 -90   14  44 ]

1. Babai's Nearest Plane Algorithm
Target vector: [100, 50, 75, 25, 125, 80]
Closest lattice vector: (12, 96, 51, 33, 156, 72)
Distance: 107.35
CVP.babai result: (12, 96, 51, 33, 156, 72)

2. Enumeration Example
Enumeration radius: 68.77
Enumeration failed or took too long: No solution found.

3. Gaussian Heuristic Analysis
Gaussian heuristic prediction: 108.57
Actual first vector length: 68.77
Ratio (actual/predicted): 0.633
Lattice determinant: 2.04e+04
Volume per lattice point: 2.04e+04

8. Lattice Cryptanalysis Examples

This section demonstrates practical applications of fpylll in cryptanalysis, including attacks on cryptographic schemes based on lattice problems.

8.1 Knapsack Cryptosystem Attack

The knapsack problem can be solved using lattice reduction when the density is low. Given weights a1,,ana_1, \ldots, a_n and target sum ss, we construct the lattice:

B=(100a1010a2001an000s)\mathbf{B} = \begin{pmatrix} 1 & 0 & \cdots & 0 & a_1 \\ 0 & 1 & \cdots & 0 & a_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \cdots & 1 & a_n \\ 0 & 0 & \cdots & 0 & s \end{pmatrix}

The solution vector (x1,,xn,0)(x_1, \ldots, x_n, 0) where xiai=s\sum x_i a_i = s is unusually short.

# Breaking a subset sum (knapsack) problem using lattice reduction

# Generate a subset sum problem instance
import random
random.seed(int(42))  # Explicitly convert to Python int

# Parameters for the subset sum problem
n = int(20)  # Explicitly convert to Python int
density = float(0.9)  # Density of the knapsack (affects difficulty)

# Generate random weights
weights = [random.randint(1, 2**(int(n*density))) for _ in range(n)]
print(f"Weights: {weights[:5]}...")  # Show first 5 weights

# Generate a random subset and compute the target sum
subset_indices = random.sample(range(n), n//2)
target = sum(weights[i] for i in subset_indices)
print(f"Target sum: {target}")
print(f"Subset indices: {subset_indices}")

# Construct the lattice for subset sum attack
# We use the standard lattice construction for subset sum
from fpylll import IntegerMatrix

# Create the lattice matrix
# The lattice has dimension (n+1) x (n+1)
M = IntegerMatrix(n+1, n+1)

# Fill the matrix according to the subset sum lattice construction
scaling_factor = int(2)  # Scaling factor to balance the lattice

for i in range(n):
    M[i, i] = int(2)  # Identity part scaled
    M[i, n] = int(weights[i])  # Weights in the last column

M[n, n] = int(target)  # Target in the bottom-right corner

print(f"Lattice dimension: {M.nrows} x {M.ncols}")
# Note: determinant computation omitted for efficiency

# Apply BKZ reduction to find short vectors
from fpylll import BKZ

# Use a moderate block size for efficiency
block_size = int(10)
param = BKZ.Param(block_size=block_size, strategies=BKZ.DEFAULT_STRATEGY)

# Reduce the lattice
print(f"\nApplying BKZ-{block_size} reduction...")
BKZ.reduction(M, param)

# Check if we found a solution
print("\nChecking for solutions in reduced basis:")
for i in range(min(int(5), M.nrows)):  # Check first few vectors
    vector = [M[i, j] for j in range(M.ncols)]
    
    # Check if this vector corresponds to a subset sum solution
    if abs(vector[-1]) <= int(1):  # The last coordinate should be 0 or ±1
        # Extract the subset from the vector
        subset_vector = vector[:-1]
        
        # Check if this gives a valid subset (binary vector after scaling)
        if all(abs(x) <= int(1) for x in subset_vector):
            # Compute the corresponding subset sum
            computed_sum = sum(int(weights[j]) * abs(subset_vector[j]) for j in range(n))
            
            if abs(computed_sum - target) <= abs(vector[-1]):
                print(f"Found solution vector: {vector}")
                solution_indices = [j for j in range(n) if abs(subset_vector[j]) > int(0)]
                print(f"Solution subset indices: {solution_indices}")
                print(f"Original subset indices: {subset_indices}")
                print(f"Match: {set(solution_indices) == set(subset_indices)}")
                break
else:
    print("No clear solution found in the first few vectors")
Weights: [58370, 13113, 144195, 128394, 117027]...
Target sum: 1126190
Subset indices: [0, 5, 13, 10, 8, 2, 3, 12, 18, 1]
Lattice dimension: 21 x 21

Applying BKZ-10 reduction...

Checking for solutions in reduced basis:
No clear solution found in the first few vectors

9. Utility Functions and Advanced Features

Fpylll provides many utility functions for working with lattices, including:

9.1 Random Lattice Generation

Fpylll includes functions to generate various types of random lattices for testing and research:

  • Uniform random lattices: random_matrix() with different distributions
  • q-ary lattices: Common in cryptography, where lattice points are congruent to 0 mod q
  • NTRU-like lattices: Used in NTRU cryptosystem
  • Integer relation lattices: For finding linear dependencies

9.2 Lattice Basis Quality Measures

Several functions help evaluate the quality of a lattice basis:

  • Hermite factor: δ = (||b₁||/vol(L)^(1/n))^n
  • Root Hermite factor: δ^(1/n)
  • Orthogonality defect: ∏||bᵢ||/vol(L)
  • Condition number: ||B|| · ||B⁻¹||

9.3 Precision and Floating Point Options

Fpylll supports different floating point precisions for better accuracy or performance:

  • Double precision: Default, fastest
  • Long double: Extended precision
  • Multiple precision: Arbitrary precision using MPFR
# Demonstration of fpylll utility functions

from fpylll import IntegerMatrix, LLL, BKZ, GSO
from fpylll.util import gaussian_heuristic
import math

# Generate different types of random lattices
print("=== Random Lattice Generation ===")

# 1. Uniform random lattice
n = 30
A = IntegerMatrix.random(n, "uniform", bits=30)
print(f"Uniform random lattice: {A.nrows}x{A.ncols}, bits=30")

# 2. q-ary lattice (important in cryptography)
q = 97  # Small prime
m = 40  # Number of samples
A_qary = IntegerMatrix.random(m, "qary", k=n, bits=30, q=q)
print(f"q-ary lattice: q={q}, dimension={A_qary.nrows}x{A_qary.ncols}")

# 3. NTRU-like lattice
A_ntru = IntegerMatrix.random(n, "ntrulike", bits=10, q=q)
print(f"NTRU-like lattice: {A_ntru.nrows}x{A_ntru.ncols}, q={q}")

print("\n=== Quality Measures ===")

# Work with a small example for clear demonstration
B = IntegerMatrix.random(15, "uniform", bits=20)
print(f"Original lattice dimension: {B.nrows}x{B.ncols}")

# Apply LLL reduction
LLL.reduction(B)

# Create GSO object to compute quality measures
M = GSO.Mat(B)
M.update_gso()

# Compute various quality measures
n = B.nrows
vol = B.det()

# First vector length
first_length = sum(B[0, j]**2 for j in range(B.ncols))**0.5
print(f"First vector length: {first_length:.3f}")

# Theoretical shortest vector (Gaussian heuristic)
gh = gaussian_heuristic([M.get_r(i, i) for i in range(n)])
print(f"Gaussian heuristic: {gh:.3f}")

# Hermite factor
hermite_factor = (first_length / abs(vol)**(1.0/n))**n
print(f"Hermite factor: {hermite_factor:.6f}")

# Root Hermite factor  
root_hermite = hermite_factor**(1.0/n)
print(f"Root Hermite factor: {root_hermite:.6f}")

# Orthogonality defect
product_lengths = 1.0
for i in range(n):
    length = sum(B[i, j]**2 for j in range(B.ncols))**0.5
    product_lengths *= length

orthogonality_defect = product_lengths / abs(vol)
print(f"Orthogonality defect: {orthogonality_defect:.3e}")

print("\n=== Precision Options ===")

# Demonstrate different precision options
from fpylll import FPLLL

# Check current precision
print(f"Current default precision: {FPLLL.get_precision()} bits")

# You can set higher precision for more accurate computations
# FPLLL.set_precision(100)  # Uncomment to set 100-bit precision
# print(f"New precision: {FPLLL.get_precision()} bits")

# Different precision modes affect GSO computations
print("GSO with different floating point types:")
print("- FP_NR<double>: Fast, double precision")
print("- FP_NR<long double>: Extended precision") 
print("- FP_NR<mpfr_t>: Arbitrary precision")

print("\n=== Performance Comparison ===")

import time

# Compare LLL vs BKZ performance on same lattice
test_lattice = IntegerMatrix.random(25, "uniform", bits=25)

# LLL timing
test_copy = IntegerMatrix.from_matrix(test_lattice)
start_time = time.time()
LLL.reduction(test_copy)
lll_time = time.time() - start_time

# BKZ-10 timing  
test_copy = IntegerMatrix.from_matrix(test_lattice)
start_time = time.time()
BKZ.reduction(test_copy, BKZ.Param(block_size=10))
bkz10_time = time.time() - start_time

# BKZ-20 timing
test_copy = IntegerMatrix.from_matrix(test_lattice)
start_time = time.time()
BKZ.reduction(test_copy, BKZ.Param(block_size=20))
bkz20_time = time.time() - start_time

print(f"LLL reduction time: {lll_time:.4f} seconds")
print(f"BKZ-10 reduction time: {bkz10_time:.4f} seconds")  
print(f"BKZ-20 reduction time: {bkz20_time:.4f} seconds")
print(f"BKZ-20 vs LLL slowdown: {bkz20_time/lll_time:.1f}x")

10. Advanced Cryptographic Applications

Lattice reduction is a powerful tool in cryptanalysis. Here we explore several important applications:

9.1 RSA with Small Private Exponents (Wiener’s Attack)

When RSA uses a small private exponent d, continued fractions can recover it. However, lattice methods extend this attack beyond the continued fraction bound.

9.2 Coppersmith’s Method

Coppersmith’s method uses lattice reduction to find small roots of polynomials modulo large integers. This has applications in:

  • Factoring with partial key exposure: When some bits of RSA factors are known
  • Broadcast attacks: When the same message is encrypted with different moduli
  • Stereotyped messages: When messages have known structure

9.3 Learning With Errors (LWE)

LWE is a fundamental problem underlying many post-quantum cryptographic schemes. The security relies on the difficulty of solving lattice problems.

9.4 NTRU Cryptanalysis

NTRU is a lattice-based cryptosystem. Understanding how to attack it provides insights into lattice-based security.

# Example: Coppersmith's Method for Small Roots

# This demonstrates the basic idea behind Coppersmith's method
# We'll solve f(x) = 0 mod N where we know x is small

from fpylll import IntegerMatrix, LLL
import math

def coppersmith_univariate_example():
    """
    Example of using lattice reduction to find small roots of 
    a polynomial modulo a large integer (simplified Coppersmith)
    """
    
    # Example: f(x) = x^2 + a*x + b mod N
    # We want to find small root x₀ such that f(x₀) ≡ 0 (mod N)
    
    # Parameters
    N = 1009 * 1013  # Small composite for demonstration
    a = 123
    b = 456
    
    # The polynomial f(x) = x^2 + 123*x + 456
    # Let's say we know the root is small (< 100)
    
    print(f"Finding small roots of f(x) = x² + {a}x + {b} mod {N}")
    
    # Generate a small root for testing
    true_root = 37  # This should be unknown in practice
    check = (true_root**2 + a*true_root + b) % N
    print(f"Verification: f({true_root}) ≡ {check} (mod {N})")
    
    # Coppersmith's method construction
    # We construct a lattice where short vectors correspond to
    # polynomials that have the same small root but are zero over integers
    
    d = 2  # Degree of polynomial
    m = 2  # Parameter controlling lattice dimension
    X = 100  # Bound on the root size
    
    # Construct lattice for Coppersmith's method
    # This is a simplified version - real implementation needs more care
    
    dim = m + d  # Lattice dimension
    L = IntegerMatrix(dim, dim)
    
    # Fill the lattice matrix
    # Each row corresponds to a polynomial of the form x^i * f(x)^j * N^(m-j)
    row = 0
    
    for j in range(m):
        for i in range(d):
            if row < dim:
                # Polynomial: x^i * f(x)^j * N^(m-j)
                # Coefficient of x^k in this polynomial
                L[row, i] = X**i * N**(m-j)  # Simplified
                row += 1
    
    # Add some structure to make it more realistic
    for i in range(dim):
        if L[i, i] == 0:
            L[i, i] = X**i
    
    print(f"Lattice dimension: {dim}x{dim}")
    print(f"Original determinant: {abs(L.det())}")
    
    # Apply LLL reduction
    LLL.reduction(L)
    print(f"Reduced determinant: {abs(L.det())}")
    
    # In a real implementation, we would:
    # 1. Extract polynomials from short lattice vectors
    # 2. Compute their GCD over integers
    # 3. Factor the resulting polynomial to find roots
    
    print("In practice, short vectors give polynomials with small integer roots")
    print("These can be factored to recover the original small root")
    
    return true_root

def simple_rsa_example():
    """
    Simplified example showing how lattices can attack RSA with partial information
    """
    print("\n=== RSA Partial Key Exposure Example ===")
    
    # Very small RSA parameters for demonstration
    p, q = 61, 67
    N = p * q
    phi = (p-1) * (q-1)
    e = 17  # Public exponent
    
    # Compute private exponent
    d = pow(e, -1, phi)  # Modular inverse
    
    print(f"RSA parameters: N={N}, e={e}, d={d}")
    
    # Suppose we know some bits of d (partial key exposure)
    # In practice, this might come from side-channel attacks
    
    known_bits = 6  # We know the lower 6 bits of d
    d_low = d & ((1 << known_bits) - 1)  # Extract lower bits
    d_high = d >> known_bits  # Unknown high bits
    
    print(f"Known low bits of d: {d_low} (correct: {d & ((1 << known_bits) - 1)})")
    print(f"Unknown high bits: {d_high}")
    
    # Set up lattice to find the unknown high bits
    # This uses the relation: e*d ≡ 1 (mod φ(N))
    # So e*d = 1 + k*φ(N) for some integer k
    
    # In practice, we'd use Coppersmith's method here
    # For now, we just verify our approach would work
    
    # The equation becomes: e*(d_high * 2^known_bits + d_low) = 1 + k*φ(N)
    # Rearranging: e*d_high * 2^known_bits = 1 + k*φ(N) - e*d_low
    
    scale = 2**known_bits
    target = 1 - e * d_low  # This should be ≡ k*φ(N) (mod something)
    
    print(f"Attack would solve for d_high such that:")
    print(f"e * d_high * {scale} ≡ {target} + k*φ(N)")
    print(f"This can be solved using Coppersmith's method with appropriate lattice")
    
    return d

# Run the examples
print("=== Coppersmith Method Demonstration ===")
root = coppersmith_univariate_example()
private_key = simple_rsa_example()

print(f"\n=== Summary ===")
print("These examples show the basic principles behind lattice-based cryptanalysis:")
print("1. Transform the problem into finding small solutions")
print("2. Construct a lattice where short vectors encode the solutions") 
print("3. Use LLL/BKZ to find short vectors")
print("4. Extract and verify the solutions")

11. Performance Optimization and Best Practices

10.1 Algorithm Selection Guidelines

When to use LLL:

  • Quick reduction for moderate dimensions (n < 100)
  • Preprocessing before other algorithms
  • When speed is more important than quality

When to use BKZ:

  • Better reduction quality needed
  • Cryptanalytic applications requiring strong reduction
  • When willing to trade time for quality

Block size selection for BKZ:

  • BKZ-10 to BKZ-20: Good balance of speed/quality
  • BKZ-50+: High quality, significant time cost
  • BKZ-60+: Research-level, very slow

10.2 Memory and Precision Considerations

Floating point precision:

  • Double precision: Default, fastest
  • Long double: Better numerical stability
  • MPFR: Highest precision, slower

Memory usage:

  • GSO objects store O(n²) values
  • BKZ enumeration can use significant memory
  • Consider dimension limits for available RAM

10.3 Parallel Processing

Fpylll supports some parallelization:

  • Multiple threads in BKZ enumeration
  • Parallel preprocessing strategies
  • Custom reduction strategies
# Performance optimization examples

from fpylll import IntegerMatrix, LLL, BKZ, GSO
import time

def benchmark_algorithms():
    """Compare performance of different reduction algorithms"""
    
    print("=== Performance Benchmarking ===")
    
    # Test on various lattice dimensions
    dimensions = [20, 30, 40, 50]
    
    for n in dimensions:
        print(f"\nDimension {n}:")
        
        # Generate test lattice
        A = IntegerMatrix.random(n, "uniform", bits=30)
        
        # LLL benchmark
        A_copy = IntegerMatrix.from_matrix(A)
        start = time.time()
        LLL.reduction(A_copy)
        lll_time = time.time() - start
        
        # Compute quality after LLL
        M_lll = GSO.Mat(A_copy)
        M_lll.update_gso()
        lll_quality = M_lll.get_r(0, 0)**0.5 / (abs(A_copy.det())**(1.0/n))
        
        # BKZ-10 benchmark
        A_copy = IntegerMatrix.from_matrix(A)
        start = time.time()
        BKZ.reduction(A_copy, BKZ.Param(block_size=10))
        bkz10_time = time.time() - start
        
        # Compute quality after BKZ-10
        M_bkz = GSO.Mat(A_copy)
        M_bkz.update_gso()
        bkz10_quality = M_bkz.get_r(0, 0)**0.5 / (abs(A_copy.det())**(1.0/n))
        
        # BKZ-20 benchmark (only for smaller dimensions)
        if n <= 40:
            A_copy = IntegerMatrix.from_matrix(A)
            start = time.time()
            BKZ.reduction(A_copy, BKZ.Param(block_size=20))
            bkz20_time = time.time() - start
            
            M_bkz20 = GSO.Mat(A_copy)
            M_bkz20.update_gso()
            bkz20_quality = M_bkz20.get_r(0, 0)**0.5 / (abs(A_copy.det())**(1.0/n))
            
            print(f"  LLL:    {lll_time:.3f}s, quality: {lll_quality:.4f}")
            print(f"  BKZ-10: {bkz10_time:.3f}s, quality: {bkz10_quality:.4f}")
            print(f"  BKZ-20: {bkz20_time:.3f}s, quality: {bkz20_quality:.4f}")
        else:
            print(f"  LLL:    {lll_time:.3f}s, quality: {lll_quality:.4f}")
            print(f"  BKZ-10: {bkz10_time:.3f}s, quality: {bkz10_quality:.4f}")
            print(f"  BKZ-20: Skipped (too slow for demo)")

def demonstrate_strategies():
    """Show different BKZ strategies and their effects"""
    
    print("\n=== BKZ Strategy Comparison ===")
    
    # Create test lattice
    n = 30
    A = IntegerMatrix.random(n, "uniform", bits=25)
    
    # Default strategy
    A_copy = IntegerMatrix.from_matrix(A)
    start = time.time()
    param_default = BKZ.Param(block_size=15, strategies=BKZ.DEFAULT_STRATEGY)
    BKZ.reduction(A_copy, param_default)
    default_time = time.time() - start
    
    M_default = GSO.Mat(A_copy)
    M_default.update_gso()
    default_quality = M_default.get_r(0, 0)**0.5
    
    print(f"Default strategy: {default_time:.3f}s, ||b₁|| = {default_quality:.2f}")
    
    # Custom strategy with more enumeration
    A_copy = IntegerMatrix.from_matrix(A)
    start = time.time()
    
    # Create custom strategy (simplified example)
    param_custom = BKZ.Param(block_size=15)
    param_custom.strategies = BKZ.DEFAULT_STRATEGY  # In practice, you'd customize this
    
    BKZ.reduction(A_copy, param_custom)
    custom_time = time.time() - start
    
    M_custom = GSO.Mat(A_copy)
    M_custom.update_gso()
    custom_quality = M_custom.get_r(0, 0)**0.5
    
    print(f"Custom strategy:  {custom_time:.3f}s, ||b₁|| = {custom_quality:.2f}")

def precision_examples():
    """Demonstrate different precision settings"""
    
    print("\n=== Precision Settings ===")
    
    from fpylll import FPLLL
    
    # Show current precision
    current_prec = FPLLL.get_precision()
    print(f"Current precision: {current_prec} bits")
    
    # For very ill-conditioned lattices, higher precision helps
    # Create an ill-conditioned example
    n = 15
    A = IntegerMatrix(n, n)
    
    # Create identity matrix with exponentially growing diagonal
    for i in range(n):
        A[i, i] = 2**(i*2)  # Exponentially growing
    
    print(f"Condition number estimate: ~2^{(n-1)*2}")
    
    # Standard precision reduction
    A_copy = IntegerMatrix.from_matrix(A)
    LLL.reduction(A_copy)
    
    M = GSO.Mat(A_copy)
    M.update_gso()
    standard_quality = M.get_r(0, 0)**0.5
    
    print(f"Standard precision result: ||b₁|| = {standard_quality:.3e}")
    
    # Note: In practice, you might set higher precision for ill-conditioned cases
    # FPLLL.set_precision(100)  # Uncomment for higher precision
    # Then repeat the reduction...
    
    print("For ill-conditioned lattices, consider:")
    print("- Using higher precision (FPLLL.set_precision)")
    print("- Checking numerical stability")
    print("- Using exact arithmetic when possible")

def memory_optimization():
    """Tips for memory-efficient lattice reduction"""
    
    print("\n=== Memory Optimization Tips ===")
    
    # Memory usage scales quadratically with dimension for GSO
    n = 50
    A = IntegerMatrix.random(n, "uniform", bits=20)
    
    print(f"For dimension {n}:")
    print(f"- GSO matrix requires ~{n*n*8} bytes for double precision")
    print(f"- IntegerMatrix requires ~{n*n*8} bytes for the basis")
    print(f"- BKZ enumeration may need additional temporary storage")
    
    # Memory-efficient approach: avoid keeping multiple GSO objects
    print("\nMemory-efficient practices:")
    print("1. Reuse GSO objects when possible")
    print("2. Use in-place operations")
    print("3. Consider block-wise processing for very large lattices")
    print("4. Free intermediate objects explicitly if needed")
    
    # Example of reusing GSO object
    M = GSO.Mat(A)
    M.update_gso()
    
    # Work with the GSO object...
    quality_before = M.get_r(0, 0)**0.5
    
    # Apply reduction (this modifies A in-place)
    BKZ.reduction(A, BKZ.Param(block_size=10))
    
    # Update the same GSO object
    M.update_gso()
    quality_after = M.get_r(0, 0)**0.5
    
    print(f"\nReusing GSO object:")
    print(f"Quality before BKZ: {quality_before:.3f}")
    print(f"Quality after BKZ:  {quality_after:.3f}")

# Run all optimization examples
benchmark_algorithms()
demonstrate_strategies()
precision_examples()
memory_optimization()

12. Conclusion and Further Resources

11.1 Summary of Key Concepts

This notebook has covered the essential aspects of using fpylll for lattice-based computations:

Mathematical Foundations:

  • Lattices as discrete subgroups of ℝⁿ
  • Shortest Vector Problem (SVP) and Closest Vector Problem (CVP)
  • Gram-Schmidt orthogonalization and basis quality measures
  • Theoretical guarantees of LLL and BKZ algorithms

Practical Implementation:

  • IntegerMatrix construction and manipulation
  • GSO computations for basis analysis
  • LLL reduction for fast, good-quality bases
  • BKZ reduction for high-quality bases
  • Enumeration for exact SVP/CVP solving

Advanced Applications:

  • Cryptanalytic attacks (subset sum, RSA, Coppersmith’s method)
  • Random lattice generation for testing
  • Performance optimization and precision control
  • Memory management for large-scale problems

11.2 Performance Guidelines

AlgorithmTime ComplexityWhen to Use
LLLO(n⁴ log B)Fast reduction, preprocessing
BKZ-β2^O(β) · poly(n)Quality reduction, cryptanalysis
Enumeration2^O(n)Exact solutions, small dimensions

11.3 Further Reading

Classical Papers:

  • Lenstra, Lenstra, Lovász (1982): The LLL algorithm
  • Schnorr & Euchner (1994): Lattice basis reduction and enumeration
  • Coppersmith (1997): Small solutions to polynomial equations

Modern Developments:

  • Gama & Nguyen (2008): Predicting lattice reduction
  • Chen & Nguyen (2011): BKZ 2.0 improvements
  • Albrecht et al. (2017): Concrete hardness of LWE

Software Documentation:

11.4 Common Pitfalls and Debugging

Numerical Issues:

  • Use higher precision for ill-conditioned lattices
  • Check for overflow in large integer computations
  • Verify GSO updates after basis changes

Performance Problems:

  • Choose appropriate block sizes for BKZ
  • Consider preprocessing with LLL
  • Monitor memory usage for large dimensions

Cryptanalytic Applications:

  • Validate lattice constructions carefully
  • Check theoretical bounds vs. practical performance
  • Test on smaller examples first

This notebook provides a foundation for understanding and using fpylll. The library’s flexibility and performance make it an excellent tool for both research and practical applications in lattice-based cryptography and computational number theory.

# Final comprehensive example: Complete lattice analysis workflow

from fpylll import IntegerMatrix, LLL, BKZ, GSO, Enumeration
from fpylll.util import gaussian_heuristic
import time
import math

def complete_lattice_analysis(dimension=25, bits=30):
    """
    Complete workflow demonstrating all major fpylll capabilities
    """
    print("=" * 60)
    print(f"COMPLETE LATTICE ANALYSIS WORKFLOW")
    print("=" * 60)
    
    # Step 1: Generate and analyze initial lattice
    print(f"\n1. LATTICE GENERATION (dimension {dimension}, {bits} bits)")
    print("-" * 40)
    
    A = IntegerMatrix.random(dimension, "uniform", bits=bits)
    print(f"Generated {A.nrows}×{A.ncols} random lattice")
    print(f"Determinant: {A.det()}")
    
    # Initial quality analysis
    M = GSO.Mat(A)
    M.update_gso()
    
    initial_length = M.get_r(0, 0)**0.5
    vol = abs(A.det())
    initial_hermite = (initial_length / vol**(1.0/dimension))**dimension
    
    print(f"Initial first vector length: {initial_length:.3f}")
    print(f"Initial Hermite factor: {initial_hermite:.6f}")
    
    # Step 2: LLL Reduction
    print(f"\n2. LLL REDUCTION")
    print("-" * 40)
    
    start_time = time.time()
    LLL.reduction(A)
    lll_time = time.time() - start_time
    
    M.update_gso()
    lll_length = M.get_r(0, 0)**0.5
    lll_hermite = (lll_length / vol**(1.0/dimension))**dimension
    
    print(f"LLL time: {lll_time:.4f} seconds")
    print(f"LLL first vector length: {lll_length:.3f}")
    print(f"LLL Hermite factor: {lll_hermite:.6f}")
    print(f"Improvement factor: {initial_length/lll_length:.2f}")
    
    # Step 3: BKZ Reduction
    print(f"\n3. BKZ REDUCTION")
    print("-" * 40)
    
    block_size = min(20, dimension//2)
    print(f"Applying BKZ-{block_size}...")
    
    start_time = time.time()
    param = BKZ.Param(block_size=block_size, strategies=BKZ.DEFAULT_STRATEGY)
    BKZ.reduction(A, param)
    bkz_time = time.time() - start_time
    
    M.update_gso()
    bkz_length = M.get_r(0, 0)**0.5
    bkz_hermite = (bkz_length / vol**(1.0/dimension))**dimension
    
    print(f"BKZ-{block_size} time: {bkz_time:.4f} seconds")
    print(f"BKZ first vector length: {bkz_length:.3f}")
    print(f"BKZ Hermite factor: {bkz_hermite:.6f}")
    print(f"Improvement over LLL: {lll_length/bkz_length:.2f}")
    
    # Step 4: Quality Analysis
    print(f"\n4. QUALITY ANALYSIS")
    print("-" * 40)
    
    # Gaussian heuristic
    gh = gaussian_heuristic([M.get_r(i, i) for i in range(dimension)])
    print(f"Gaussian heuristic: {gh:.3f}")
    print(f"First vector vs GH: {bkz_length/gh:.3f}")
    
    # Orthogonality defect
    product_lengths = 1.0
    for i in range(dimension):
        row_length = sum(A[i, j]**2 for j in range(A.ncols))**0.5
        product_lengths *= row_length
    
    orthogonality_defect = product_lengths / vol
    print(f"Orthogonality defect: {orthogonality_defect:.3e}")
    
    # Successive minima approximation
    print(f"Successive minima approximation:")
    for i in range(min(5, dimension)):
        length_i = M.get_r(i, i)**0.5
        print(f"  λ_{i+1} ≈ {length_i:.3f}")
    
    # Step 5: Short Vector Search (for small dimensions)
    if dimension <= 20:
        print(f"\n5. ENUMERATION (SVP)")
        print("-" * 40)
        
        try:
            # Enumerate short vectors
            start_time = time.time()
            enum = Enumeration(M)
            
            # Find vectors within 1.1 times the first vector length
            radius = bkz_length * 1.1
            solutions = enum.enumerate(0, dimension, radius**2, 0)
            enum_time = time.time() - start_time
            
            print(f"Enumeration time: {enum_time:.4f} seconds")
            print(f"Search radius: {radius:.3f}")
            print(f"Vectors found: {len(solutions)}")
            
            if solutions:
                shortest = min(solutions, key=lambda x: x[0])
                shortest_length = shortest[0]**0.5
                print(f"Shortest vector length: {shortest_length:.3f}")
                print(f"Ratio to first basis vector: {shortest_length/bkz_length:.3f}")
        
        except Exception as e:
            print(f"Enumeration failed or too slow: {e}")
    
    # Step 6: Summary
    print(f"\n6. SUMMARY")
    print("-" * 40)
    
    print(f"Lattice dimension: {dimension}")
    print(f"Total reduction time: {lll_time + bkz_time:.4f} seconds")
    print(f"")
    print(f"Quality progression:")
    print(f"  Initial:  ||b₁|| = {initial_length:.3f}, δ = {initial_hermite:.6f}")
    print(f"  LLL:      ||b₁|| = {lll_length:.3f}, δ = {lll_hermite:.6f}")
    print(f"  BKZ-{block_size}:   ||b₁|| = {bkz_length:.3f}, δ = {bkz_hermite:.6f}")
    print(f"")
    print(f"Theoretical comparison:")
    print(f"  LLL guarantee: δ ≤ (4/3)^(n-1)/4 = {(4/3)**((dimension-1)/4):.6f}")
    print(f"  Achieved: δ = {bkz_hermite:.6f}")
    print(f"  Gap to GH: {bkz_length/gh:.3f}")
    
    return A, M

# Run the complete analysis
print("Running complete lattice analysis workflow...")
print("This demonstrates all major fpylll capabilities in one example.")

try:
    lattice, gso = complete_lattice_analysis(dimension=20, bits=25)
    print(f"\n✓ Analysis completed successfully!")
    print(f"✓ Final lattice basis stored in 'lattice'")
    print(f"✓ GSO object stored in 'gso'")
    
except Exception as e:
    print(f"\n✗ Analysis failed: {e}")
    print("This might be due to dimension too large or other computational limits")

print(f"\n" + "="*60)
print("FPYLLL TUTORIAL COMPLETE")
print("="*60)
print("You now have a comprehensive understanding of:")
print("• Lattice theory and computational problems")
print("• fpylll library structure and capabilities") 
print("• LLL and BKZ reduction algorithms")
print("• Cryptanalytic applications")
print("• Performance optimization techniques")
print("")
print("Ready for advanced lattice-based research and applications!")