├── .flake8 ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── benchmark.py ├── benchmark_small_dim.py ├── flatter_conversion.py ├── max_q.py └── optimal_segment_size.py ├── core ├── blaster.pyx ├── decl.pxd ├── eigen_matmul.cpp ├── enumeration.cpp ├── enumeration.hpp ├── lattice_reduction.cpp ├── pruning_params.cpp └── types.hpp ├── find_pruning_params.py ├── setup.py └── src ├── app.py ├── blaster.py ├── lattice_io.py ├── size_reduction.py └── stats.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache 2 | __pycache__ 3 | 4 | # Shared libraries 5 | *.so 6 | 7 | # Swap files 8 | *.swp 9 | 10 | # Input/output lattice bases 11 | /input/ 12 | /output/ 13 | 14 | # Cython related files 15 | /build/ 16 | 17 | # Local eigen files 18 | /eigen* 19 | 20 | # Local venv 21 | /activate 22 | /python3 23 | /venv/ 24 | 25 | # cysignals creates this directory when Ctrl+C'ing during execution 26 | cysignals_crash_logs/ 27 | 28 | # Benchmarks generate big logs 29 | /logs/ 30 | /logs-flatter/ 31 | /benchmark/results/ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Leo Ducas, Ludo Pulles, Marc Stevens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile compiles Cython 2 | .POSIX: 3 | 4 | .PHONY: cython cython-gdb venv-clean eigen3-clean 5 | 6 | # if virtualenv directory venv exists then prefer venv/bin/python3 over system python3 7 | VENV := venv 8 | PYTHONV := python3 9 | PYTHON = $(or $(wildcard $(VENV)/bin/$(PYTHONV)), $(shell command -v $(PYTHONV)), $(PYTHONV)) 10 | 11 | 12 | all: cython 13 | 14 | clean: 15 | $(PYTHON) setup.py clean 16 | rm -rf build cysignals_crash_logs 17 | 18 | # Run 19 | cython: 20 | $(PYTHON) setup.py build_ext 21 | 22 | # Debug 23 | # The LD_PRELOAD trick is from https://github.com/grpc/grpc/issues/25819 24 | cython-gdb: 25 | $(PYTHON) setup.py build_ext --cython-gdb 26 | 27 | 28 | ### Rules to install Eigen C++ template library for linear algebra locally 29 | 30 | # Default version of Eigen C++ template library for linear algebra 31 | EIGEN_VERSION := 3.4.0 32 | 33 | eigen3: 34 | $(MAKE) eigen-$(EIGEN_VERSION) 35 | if [ -d eigen3 ]; then rm eigen3; fi 36 | ln -s eigen-$(EIGEN_VERSION) eigen3 37 | 38 | eigen-%: eigen-%.tar.gz 39 | tar -xzvf $< >/dev/null 40 | 41 | eigen-%.tar.gz: 42 | wget -nv "https://gitlab.com/libeigen/eigen/-/archive/$*/$@" -O "$@" 43 | 44 | eigen3-clean: 45 | rm -rf eigen3 eigen-$(EIGEN_VERSION) eigen-$(EIGEN_VERSION).tar.gz 46 | 47 | 48 | 49 | ### Rules to create a virtual environment with up-to-date numpy and cython 50 | 51 | # Default requirements (maybe set a specific version of numpy & cython?) 52 | PIP_REQUIREMENTS := pip cython cysignals numpy setuptools matplotlib 53 | 54 | venv: 55 | @if [ "$(VIRTUAL_ENV)" != "" ]; then echo "Active virtual environment detected. Please run 'deactivate' first!"; false; fi 56 | $(PYTHON) -m pip install --upgrade pip 2> /dev/null || echo "ERROR: Upgrading pip failed!" 57 | $(PYTHON) -m pip install virtualenv 2> /dev/null || echo "ERROR: Installing pip package virtualenv failed!" 58 | $(PYTHON) -m virtualenv $(VENV) 59 | -@rm activate 2>/dev/null 60 | ln -s $(VENV)/bin/activate . 61 | printf "#!/usr/bin/env bash\nOPENBLAS_NUM_THREADS=1 $(VENV)/bin/$(PYTHONV) \$$*\n" > ./$(PYTHONV) 62 | chmod +x ./$(PYTHONV) 63 | $(VENV)/bin/$(PYTHONV) -m pip install --upgrade $(PIP_REQUIREMENTS) 64 | @echo "==================================================================" 65 | @echo "=== NOTE: Please use 'source activate' to activate environment ===" 66 | @echo "=== Or use './python3' to use environment ===" 67 | @echo "==================================================================" 68 | 69 | venv-clean: 70 | @if [ "$(VIRTUAL_ENV)" != "" ]; then echo "Active virtual environment detected. Please run 'deactivate' first!"; false; fi 71 | rm -rf venv 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BLASter 2 | 3 | BLASter is a proof of concept of an LLL-like lattice reduction algorithm that uses: 4 | 5 | - parallelization, 6 | - segmentation, 7 | - Seysen's reduction instead of size reduction, and 8 | - a linear algebra library. 9 | 10 | ## Disclaimer 11 | 12 | The goal of this software is to showcase speed ups that are possible in lattice reduction software. 13 | This software is a *proof of concept*! 14 | 15 | In particular, we **do not**: 16 | 17 | - guarantee the algorithm terminates, nor claim its output is correct on all lattices, 18 | - support lattices with large entries, 19 | - consider issues / PRs that improve efficiency or robustness, 20 | - actively maintain this software. 21 | 22 | We **do**: 23 | 24 | - happily answer any questions to explain design choices phrased as: *"Why is X done in Y way?"*. The answer may, in many cases, be: "because it is faster in practice". 25 | - encourage the cryptographic community to build a new robust lattice reduction library incorporating the ideas in this proof of concept. 26 | 27 | ## Requirements 28 | 29 | - python3 30 | - Cython version 3.0 or later 31 | - Python modules: `cysignals numpy setuptools` (installed system-wide or in locally through `make venv`) 32 | - The Eigen library version 3 or later (installed system-wide or locally through `make eigen3`) 33 | 34 | Optional: 35 | 36 | - Python module: virtualenv (for creating a local virtual environment to install python3 modules) 37 | - fplll (for generating q-ary lattices with the `latticegen` command) 38 | 39 | ## Building 40 | 41 | - Optional: Run `make eigen3` to install libeigen3 library in a subdirectory. 42 | - Optional: Run `make venv` to create a local virtual environment and install the required python3 modules. 43 | - Run `make` to compile all the Cython files in `core/`. 44 | 45 | ## Debugging 46 | 47 | - Debug the C++/Cython code with the `libasan` and `libubsan` sanitizers by running `make cython-gdb`. 48 | These sanitizers check for memory leaks, out of bounds accesses, and undefined behaviour. 49 | - When executing the `src/app.py`, preload libasan like this: 50 | `LD_PRELOAD=$(gcc -print-file-name=libasan.so) src/app.py -pvi INPUTFILE` 51 | - If you want to run the program with the `gdb` debugger, read the [Cython documentation](https://cython.readthedocs.io/en/stable/src/userguide/debugging.html#running-the-debugger), for more info. 52 | 53 | ## Running 54 | 55 | *Note: you first need to build the software, see above.* 56 | 57 | Run the command by e.g. typing `./python3 src/app.py -pvi INPUTFILE`. 58 | Add `-h` for seeing all available command line arguments. 59 | 60 | ## Example 61 | 62 | Command: `time latticegen q 128 64 20 p | src/app.py -pq`. 63 | 64 | Expected output: 65 | ``` 66 | Profile: [9.38 9.39 9.30 9.28 9.18 ... 4.27 4.33 4.31 4.29 4.35] 67 | Root Hermite factor: 1.020447, ∥b_1∥ = 11906.636 68 | 69 | real 0m0,754s 70 | user 0m2,271s 71 | sys 0m0,105s 72 | ``` 73 | 74 | To run deep-LLL with depth `4`, run `src/app.py -pq -i {lattice} -d4`. 75 | 76 | To run progressive BKZ-60 (with 4-deep-LLL) with `1` tours and block size increments of `2`, run `src/app.py -pq -i {lattice} -b60 -t1 -P2`. 77 | -------------------------------------------------------------------------------- /benchmark/benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | from flatter_conversion import convert_logfiles 7 | from blaster.lattice_io import read_qary_lattice 8 | from blaster.stats import get_profile, rhf, slope 9 | 10 | 11 | # Specify which lattices we want to test: 12 | mqs = [ 13 | (128, 631), # latticegen q 2 1 10 p 14 | (256, 829561), # latticegen q 2 1 20 p 15 | (512, 968665207), # latticegen q 2 1 30 p 16 | (1024, 968665207), # latticegen q 2 1 30 p 17 | ] 18 | seeds = range(10) 19 | cmd_blaster = "../python3 ../src/app.py -q" 20 | temp_lat = "../output/temp.lat" 21 | other_logs = {m: open(f'./logs_other_{m}.csv', mode='w', encoding='utf8') for (m, q) in mqs} 22 | 23 | 24 | def is_float(x): 25 | try: 26 | x = float(x) 27 | except: 28 | return False 29 | return True 30 | 31 | def parse_time_usage(time_output): 32 | times = time_output.strip().split(" ") 33 | # parts = time_output.split("\n")[1:4] 34 | # times = [part.split("\t")[1] for part in parts] 35 | return {'real': times[0], 'user': times[1], 'sys': times[2]} 36 | 37 | 38 | def run_command(cmd, logfile=None, capture_time=False, flatter_fail=False): 39 | print(f"Executing \"{cmd}\".", flush=True) 40 | if capture_time: 41 | result = subprocess.run( 42 | f"/usr/bin/time -f \"%e %U %S\" {cmd}", 43 | text=True, shell=True, capture_output=True 44 | ) 45 | else: 46 | result = subprocess.run(cmd, shell=True) 47 | if not flatter_fail and result.returncode != 0: 48 | print(result.stderr) 49 | if logfile: 50 | os.remove(logfile) 51 | exit(1) 52 | 53 | if capture_time: 54 | return parse_time_usage(result.stderr) 55 | else: 56 | return None 57 | 58 | 59 | def gen_lattice(m, q, seed, path): 60 | n = m//2 61 | run_command(f"latticegen -randseed {seed} q {m} {n} {q} q > {path}") 62 | 63 | 64 | def run_blaster(m, q, seed, path): 65 | logfile = f"../logs/lll_{m}_{q}_{seed}.csv" 66 | run_command(f"{cmd_blaster} -i {path} -l {logfile}", logfile) 67 | 68 | 69 | def run_blaster_deeplll(m, q, seed, path, depth): 70 | logfile = f"../logs/deeplll{depth}_{m}_{q}_{seed}.csv" 71 | # outfile = path.replace('input/', f'output/d{depth}_') 72 | # result = run_command(f"{cmd_blaster} -i {path} -o {outfile} -l {logfile} -d{depth}") 73 | run_command(f"{cmd_blaster} -i {path} -l {logfile} -d{depth}", logfile) 74 | 75 | 76 | def run_blaster_bkz(m, q, seed, path, beta, bkz_prog=2): 77 | logfile = f"../logs/progbkz{beta}_{m}_{q}_{seed}.csv" 78 | run_command(f"{cmd_blaster} -i {path} -l {logfile} -b{beta} -P{bkz_prog} -t1", logfile) 79 | 80 | 81 | def run_flatter(m, q, seed, path, num_threads, alpha=None): 82 | flogfile = f"../logs-flatter/{m}_{q}_{seed}.log" 83 | cmd = f"OMP_NUM_THREADS={num_threads} FLATTER_LOG={flogfile} flatter -q {path}" 84 | if alpha: 85 | cmd = f"{cmd} -alpha {alpha}" 86 | run_command(cmd, flogfile, flatter_fail=True) 87 | 88 | plogfile = f"../logs/flatter_{m}_{q}_{seed}.csv" 89 | convert_logfiles(flogfile, plogfile) 90 | 91 | 92 | def run_fplll(m, q, seed, path): 93 | cmd = f"fplll {path} > {temp_lat}" 94 | t = run_command(cmd, capture_time=True) 95 | prof = get_profile(read_qary_lattice(temp_lat)) 96 | data = { 97 | 'seed': seed,'type': f"fpLLL", 98 | 'slope': f"{slope(prof):.6f}", 'rhf': f"{rhf(prof):.5f}" 99 | } 100 | print(','.join(str(v) for k, v in (data | t).items()), file=other_logs[m], flush=True) 101 | other_logs[m].flush() 102 | 103 | 104 | def run_KEF21(m, q, seed, path, num_threads): 105 | cmd = f"optlll -p {num_threads} < {path} > {temp_lat}" 106 | t = run_command(cmd, capture_time=True) 107 | prof = get_profile(read_qary_lattice(temp_lat)) 108 | data = { 109 | 'seed': seed,'type': f"KEF21 ({num_threads} threads)", 110 | 'slope': f"{slope(prof):.6f}", 'rhf': f"{rhf(prof):.5f}" 111 | } 112 | print(','.join(str(v) for k, v in (data | t).items()), file=other_logs[m], flush=True) 113 | 114 | 115 | def __main__(): 116 | global mqs 117 | 118 | lattices = [(m, q, seed, f"../input/{m}_{q}_{seed}") for (m, q) in mqs for seed in seeds] 119 | 120 | 121 | for f in other_logs.values(): 122 | print("seed,type,slope,rhf,real (s),user (s),sys (s)", file=f, flush=True) 123 | 124 | has_cmd = False 125 | for i, arg in enumerate(sys.argv[1:]): 126 | is_cmd = True 127 | if arg == 'dim': 128 | assert 2 + i < len(sys.argv), "dim param expected!" 129 | dim = int(sys.argv[2 + i]) 130 | assert dim in [m for (m, q) in mqs], "Unknown dimension" 131 | curq = [q for (m, q) in mqs if m == dim] 132 | assert len(curq) == 1 133 | curq = curq[0] 134 | lattices = [(dim, curq, seed, f"../input/{dim}_{curq}_{seed}") for seed in seeds] 135 | elif arg == 'lattices': 136 | for lat in lattices: 137 | gen_lattice(*lat) 138 | elif arg == 'lll': 139 | for lat in lattices: 140 | run_blaster(*lat) 141 | elif arg == 'deeplll': 142 | assert 2 + i < len(sys.argv), "depth param expected!" 143 | depth = int(sys.argv[2 + i]) 144 | for lat in lattices: 145 | run_blaster_deeplll(*lat, depth) 146 | elif arg == 'pbkz': 147 | assert 2 + i < len(sys.argv), "beta param expected!" 148 | beta = int(sys.argv[2 + i]) 149 | for lat in lattices: 150 | run_blaster_bkz(*lat, beta) 151 | elif arg == 'flatter': 152 | assert 2 + i < len(sys.argv), "num_threads param expected!" 153 | num_threads = int(sys.argv[2 + i]) 154 | alpha = None 155 | if 3 + i < len(sys.argv) and is_float(sys.argv[3 + i]): 156 | alpha = float(sys.argv[3 + i]) 157 | for lat in lattices: 158 | run_flatter(*lat, num_threads, alpha) 159 | elif arg == 'fplll': 160 | for lat in lattices: 161 | run_fplll(*lat) 162 | elif arg == 'KEF21': 163 | assert 2 + i < len(sys.argv), "num_threads param expected!" 164 | num_threads = int(sys.argv[2 + i]) 165 | for lat in lattices: 166 | run_KEF21(*lat, num_threads) 167 | else: 168 | is_cmd = False 169 | has_cmd = has_cmd or is_cmd 170 | 171 | for f in other_logs.values(): 172 | f.close() 173 | 174 | if not has_cmd: 175 | print(f"Usage: {sys.argv[0]} [dim d|lattices|lll|deeplll `depth`|" 176 | f"pbkz `beta`|flatter `num_threads`]") 177 | 178 | if __name__ == "__main__": 179 | __main__() 180 | -------------------------------------------------------------------------------- /benchmark/benchmark_small_dim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from math import ceil 3 | from multiprocessing import cpu_count 4 | import subprocess 5 | import sys 6 | from time import time 7 | 8 | from flatter_conversion import convert_logfiles 9 | from blaster.lattice_io import read_qary_lattice 10 | from blaster.stats import get_profile, rhf, slope 11 | from blaster.blaster import reduce 12 | 13 | 14 | def is_prime(x): 15 | if x <= 2: 16 | return x == 2 17 | for y in range(2, x): 18 | if y*y > x: 19 | return True 20 | if x % y == 0: 21 | return False 22 | return True 23 | # return all(x % i for i in range(2, x)) 24 | 25 | 26 | def next_prime(x): 27 | while not is_prime(x): 28 | x += 1 29 | return x 30 | 31 | 32 | # Specify which lattices we want to test: 33 | mqs = [(m, next_prime(2**(m//8))) for m in range(48, 256, 16)] 34 | seeds = range(10) 35 | lattice_output = "../output/temp.lat" 36 | output_file = None 37 | 38 | 39 | def parse_time_usage(time_output): 40 | times = time_output.strip().split(" ") 41 | # parts = time_output.split("\n")[1:4] 42 | # times = [part.split("\t")[1] for part in parts] 43 | return {'real': times[0], 'user': times[1], 'sys': times[2]} 44 | 45 | 46 | def output_data(data): 47 | global output_file 48 | print(','.join(str(v) for k, v in data.items()), file=output_file) 49 | print(','.join(str(v) for k, v in data.items())) 50 | 51 | 52 | def run_command(cmd, instance, env=None): 53 | # print(f"Executing \"time {cmd}\".", flush=True) 54 | if not env: 55 | env = "" 56 | result = subprocess.run(f"{env} /usr/bin/time -f \"%e %U %S\" {cmd}", text=True, shell=True, capture_output=True) 57 | if result.returncode != 0: 58 | print("ERROR WHILE EXECUTING COMMAND!") 59 | print(result.stderr) 60 | exit(1) 61 | 62 | prof = get_profile(read_qary_lattice(lattice_output)) 63 | data = instance | {'slope': f"{slope(prof):.6f}", 'rhf': f"{rhf(prof):.5f}"} 64 | output_data(data | parse_time_usage(result.stderr)) 65 | 66 | 67 | def exec_blaster(inputfile, depth, instance): 68 | T0 = time() 69 | 70 | # Read lattice 71 | B = read_qary_lattice(inputfile) 72 | 73 | # Based on src/app.py: 74 | cores = max(1, min(ceil(B.shape[1] / 64), cpu_count() // 2)) 75 | 76 | # Run lattice reduction 77 | U, B_red, tprof = reduce(B, depth=depth) 78 | 79 | T1 = time() 80 | prof = get_profile(B_red) 81 | data = instance | {'slope': f"{slope(prof):.6f}", 'rhf': f"{rhf(prof):.5f}"} 82 | output_data(data | {'real': (T1 - T0), 'user': '0', 'sys': 0}) 83 | 84 | def gen_lattice(m, q, seed, path): 85 | n = m//2 86 | run_command(f"latticegen -randseed {seed} q {m} {n} {q} q > {path}") 87 | 88 | 89 | def run_blaster(m, q, seed, path): 90 | exec_blaster(path, 0, {'m': m, 'q': q, 'seed': seed, 'type': 'LLL'}) 91 | 92 | 93 | def run_blaster_deeplll(m, q, seed, path, depth): 94 | exec_blaster(path, depth, {'m': m, 'q': q, 'seed': seed, 'type': f'DeepLLL{depth}'}) 95 | 96 | 97 | def run_flatter(m, q, seed, path, num_threads): 98 | run_command( 99 | f"~/.local/bin/flatter {path} {lattice_output}", 100 | {'m': m, 'q': q, 'seed': seed, 'type': f'Flatter ({num_threads} threads)'}, 101 | env=f"OMP_NUM_THREADS={num_threads}") 102 | 103 | 104 | def run_fplll(m, q, seed, path): 105 | cmd = f"~/.local/bin/fplll {path} > {lattice_output}" 106 | run_command(cmd, {'m': m, 'q': q, 'seed': seed, 'type': f'fpLLL'}) 107 | 108 | 109 | def run_KEF21(m, q, seed, path, num_threads): 110 | cmd = f"optlll -p {num_threads} < {path} > {lattice_output}" 111 | run_command(cmd, {'m': m, 'q': q, 'seed': seed, 'type': f'KEF21 ({num_threads} threads)'}) 112 | 113 | 114 | def __main__(): 115 | global mqs, output_file 116 | 117 | output_file = open("./small-dimension.csv", mode='w', encoding="utf8") 118 | print("m,q,seed,type,slope,rhf,real (s),user (s),sys (s)", file=output_file) 119 | 120 | lattices = [(m, q, seed, f"../input/{m}_{q}_{seed}") for (m, q) in mqs for seed in seeds] 121 | commands_executed = 0 122 | for i, arg in enumerate(sys.argv[1:]): 123 | if arg == 'lattices': 124 | for lat in lattices: 125 | gen_lattice(*lat) 126 | elif arg == 'lll': 127 | for lat in lattices: 128 | run_blaster(*lat) 129 | elif arg == 'deeplll': 130 | assert 2 + i < len(sys.argv), "depth param expected!" 131 | depth = int(sys.argv[2 + i]) 132 | for lat in lattices: 133 | run_blaster_deeplll(*lat, depth) 134 | elif arg == 'flatter': 135 | assert 2 + i < len(sys.argv), "num_threads param expected!" 136 | num_threads = int(sys.argv[2 + i]) 137 | for lat in lattices: 138 | run_flatter(*lat, num_threads) 139 | elif arg == 'fplll': 140 | for lat in lattices: 141 | run_fplll(*lat) 142 | elif arg == 'KEF21': 143 | assert 2 + i < len(sys.argv), "num_threads param expected!" 144 | num_threads = int(sys.argv[2 + i]) 145 | for lat in lattices: 146 | run_KEF21(*lat, num_threads) 147 | elif arg == 'all': 148 | assert 3 + i < len(sys.argv), "depth & num_threads param expected!" 149 | depth = int(sys.argv[2 + i]) 150 | num_threads = int(sys.argv[3 + i]) 151 | 152 | for lat in lattices: 153 | run_blaster(*lat) 154 | run_blaster_deeplll(*lat, depth) 155 | run_flatter(*lat, num_threads) 156 | run_fplll(*lat) 157 | run_KEF21(*lat, num_threads) 158 | else: 159 | commands_executed -= 1 160 | commands_executed += 1 161 | 162 | if commands_executed == 0: 163 | print(f"Usage: {sys.argv[0]} [lattices|lll|deeplll `depth`|flatter `num_threads`]|fplll|KEF21") 164 | 165 | output_file.close() 166 | 167 | 168 | if __name__ == "__main__": 169 | __main__() 170 | -------------------------------------------------------------------------------- /benchmark/flatter_conversion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | from blaster.stats import rhf, slope, potential 4 | 5 | # Adaption of: 6 | # https://github.com/keeganryan/flatter/blob/main/scripts/visualize_profile.py 7 | # 8 | # This script: 9 | # 1) reads a flatter logfile 10 | # 2) extracts the evolution of root hermite factor as time progresses 11 | # 3) outputs this into a csv file in format "TT,rhf,slope", where TT is the elapsed wall time. 12 | class Delta: 13 | def __init__(self, start, end, data, prev_data=None, is_reset=False): 14 | self.start = start 15 | self.end = end 16 | self.data = data.copy() 17 | self.prev = prev_data.copy() if prev_data is not None else None 18 | self.is_reset = is_reset 19 | 20 | 21 | class ProfileSet: 22 | ''' Model the sequence of Gram-Schmidt vector lengths ''' 23 | 24 | def __init__(self, fname): 25 | self.reset() 26 | self.parse_logfile(fname) 27 | 28 | def reset(self): 29 | self.__n = 0 30 | self.__cur_data = np.zeros(1) 31 | self.__deltas = [] 32 | self.__times = [] 33 | self.__pos = 0 34 | self._follow = True 35 | self._expect_reset = False 36 | self._max_time = None 37 | 38 | def parse_logfile(self, fname): 39 | with open(fname) as f: 40 | for line in f: 41 | if "profile" in line: 42 | self.parse_profile_line(line) 43 | self.position = 1 44 | 45 | def parse_profile_line(self, ln): 46 | ln = ln.rstrip() 47 | end_i = ln.find(")") 48 | start_i = ln.find("profile") 49 | if "," not in ln[start_i + 8:end_i]: 50 | # This is a reset line 51 | dim = int(ln[start_i+8:end_i]) 52 | self.reset_profile(dim) 53 | else: 54 | bounds = ln[start_i+8:end_i].split(",") 55 | start = int(bounds[0]) 56 | end = int(bounds[1]) 57 | 58 | ts = ln[ln.find("[")+1:ln.find("]")] 59 | ts = float(ts) 60 | 61 | end_i = ln.find("]") 62 | 63 | val_pairs = [c.split("+") for c in ln[end_i+1:].split(" ") if len(c) > 0] 64 | 65 | if len(val_pairs) != end - start: 66 | raise ValueError("Unexpected number of values for profile line") 67 | 68 | vals = [float(v[0])+float(v[1]) for v in val_pairs] 69 | 70 | self.append(start, end, vals, ts) 71 | 72 | def reset_profile(self, dim): 73 | self._expect_reset = True 74 | 75 | def append(self, start, end, values, time): 76 | # Expand if necessary 77 | # assert end <= self.__n 78 | 79 | delta = Delta(start, end, values, is_reset=self._expect_reset) 80 | self._expect_reset = False 81 | 82 | # Is this the first delta? Apply it. 83 | if len(self.__deltas) == 0: 84 | self.__n = end 85 | self.__cur_data = np.zeros(end) 86 | self.__cur_data[start:end] = values 87 | self.__deltas.append(delta) 88 | self.__times.append(time) 89 | if self._follow: 90 | to_adv = len(self.__deltas) - self.__pos 91 | assert to_adv > 0 92 | self.advance(to_adv) 93 | 94 | def get_data(self): 95 | return self.__cur_data.copy() 96 | 97 | def get_time(self): 98 | return self.__times[self.__pos] 99 | 100 | def get_times(self): 101 | return self.__times[:] 102 | 103 | def _step_forward(self): 104 | if self.__pos >= len(self.__deltas) - 1: 105 | self._follow = True 106 | return 107 | 108 | delta = self.__deltas[self.__pos + 1] 109 | s = delta.start 110 | e = delta.end 111 | if delta.is_reset: 112 | if delta.prev is None: 113 | delta.prev = self.__cur_data.copy() 114 | self.__cur_data = np.array(delta.data) 115 | else: 116 | if delta.prev is None: 117 | delta.prev = self.__cur_data[s:e].copy() 118 | self.__cur_data[s:e] = delta.data 119 | self.__pos += 1 120 | 121 | def _step_backward(self): 122 | self._follow = False 123 | if self.__pos <= 0: 124 | return 125 | 126 | delta = self.__deltas[self.__pos] 127 | assert delta.prev is not None 128 | s = delta.start 129 | e = delta.end 130 | 131 | if delta.is_reset: 132 | self.__cur_data = delta.prev.copy() 133 | else: 134 | self.__cur_data[s:e] = delta.prev 135 | self.__pos -= 1 136 | 137 | def advance(self, steps): 138 | if steps < 0: 139 | steps = -steps 140 | while steps > 0: 141 | self._step_backward() 142 | steps -= 1 143 | else: 144 | while steps > 0: 145 | self._step_forward() 146 | steps -= 1 147 | 148 | def advance_to_time(self, t): 149 | # Which index is closest to the specified time? 150 | ts = np.array(self.__times) 151 | 152 | i = np.argmax(ts > t) 153 | self.position = i + 1 154 | 155 | @property 156 | def max_time(self): 157 | if self._max_time is None: 158 | self._max_time = np.max(self.__times) 159 | return self._max_time 160 | 161 | @property 162 | def position(self): 163 | return self.__pos + 1 164 | 165 | @position.setter 166 | def position(self, p): 167 | delta = p - self.position 168 | self.advance(delta) 169 | 170 | @property 171 | def count(self): 172 | return len(self.__deltas) 173 | 174 | def log_profile(self, fname): 175 | """ 176 | Output the RHF and slope of the basis profile, as a function of elapsed wall time. 177 | """ 178 | pairs = [] 179 | while self.__pos + 1 < len(self.__deltas): 180 | prof = self.get_data() 181 | pairs.append((self.get_time(), rhf(prof), slope(prof), potential(prof))) 182 | self._step_forward() 183 | 184 | npairs, sparse_pairs = len(pairs), [pairs[0]] 185 | for i in range(1, npairs): 186 | if i == npairs - 1 or abs(pairs[i][1] - pairs[i-1][1]) > 1e-6: 187 | sparse_pairs.append(pairs[i]) 188 | 189 | with open(fname, "w") as f: 190 | print("it,walltime,rhf,slope,potential", file=f) 191 | it = 1 192 | for tt, _rhf, _slope, _pot in sparse_pairs: 193 | print(f"{it:4d},{tt:.6f},{_rhf:.6f},{_slope:.6f},{_pot:.3f}", file=f) 194 | it += 1 195 | 196 | 197 | def convert_logfiles(logfile, outfile): 198 | prof_set = ProfileSet(logfile) 199 | prof_set.log_profile(outfile) 200 | 201 | 202 | def __main__(): 203 | parser = argparse.ArgumentParser() 204 | parser.add_argument("logfile", type=str, help="Input: flatter logfile") 205 | parser.add_argument("outfile", type=str, help="Output: RHF as function of elapsed wall time") 206 | args = parser.parse_args() 207 | convert_logfiles(args.logfile, args.outfile) 208 | 209 | 210 | if __name__ == "__main__": 211 | __main__() 212 | -------------------------------------------------------------------------------- /benchmark/max_q.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import signal 4 | import subprocess 5 | 6 | ERROR_FILE = open("results/max_q_errors.txt", "w", encoding='utf8') 7 | NUM_TRIALS = 10 8 | 9 | 10 | def gen_lattice(m, lgq, seed, path): 11 | cmd = f"latticegen -randseed {seed} q {m} {m // 2} {lgq} p > {path}" 12 | result = subprocess.run(cmd, text=True, shell=True, capture_output=True) 13 | if result.returncode != 0: 14 | print("Error encountered during lattice generation.") 15 | print(result.stderr) 16 | exit(1) 17 | 18 | 19 | # https://stackoverflow.com/questions/4789837/how-to-terminate-a-python-subprocess-launched-with-shell-true 20 | def run_command(cmd, timeout, description): 21 | # The os.setsid() is passed in the argument preexec_fn so 22 | # it's run after the fork() and before exec() to run the shell. 23 | cmd = f"/usr/bin/time -f \"%e\" {cmd}" 24 | with subprocess.Popen(cmd, text=True, shell=True, preexec_fn=os.setsid, 25 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: 26 | try: 27 | stdout, stderr = process.communicate(None, timeout=timeout) 28 | except subprocess.TimeoutExpired as exc: 29 | process.kill() 30 | os.killpg(os.getpgid(process.pid), signal.SIGTERM) 31 | # Send the signal to all the process groups 32 | 33 | # POSIX _communicate already populated the output so 34 | # far into the TimeoutExpired exception. 35 | process.wait() 36 | print(f"Timeout expired for command \"{cmd}\"", file=ERROR_FILE) 37 | if exc.stderr is not None: 38 | print(exc.stderr.decode("utf-8"), file=ERROR_FILE, flush=True) 39 | return None 40 | except: # Including KeyboardInterrupt, communicate handled that. 41 | process.kill() 42 | os.killpg(os.getpgid(process.pid), signal.SIGTERM) 43 | # We don't call process.wait() as .__exit__ does that for us. 44 | raise 45 | retcode = process.poll() 46 | result = subprocess.CompletedProcess(process.args, retcode, stdout, stderr) 47 | if result.returncode != 0: 48 | print(f"{description} finished with error code {result.returncode}.", file=ERROR_FILE) 49 | print(result.stderr, file=ERROR_FILE, flush=True) 50 | return None 51 | return float(result.stderr.strip()) 52 | 53 | 54 | def run_blaster(m, lgq, seed, path): 55 | timeout = 5 if m < 256 else 15 56 | desc = f"BLASter(m={m}, lgq={lgq}, seed={seed})" 57 | return run_command(f"../python3 ../src/app.py -qi {path}", timeout, desc) 58 | 59 | 60 | def count_successes(m, lgq, path): 61 | wt = [] 62 | for seed in range(NUM_TRIALS): 63 | gen_lattice(m, lgq, seed, path) 64 | result = run_blaster(m, lgq, seed, path) 65 | if result: 66 | wt.append(result) 67 | 68 | # Report times 69 | mint, avgt, maxt = (min(wt), sum(wt)/len(wt), max(wt)) if wt else (0,0,0) 70 | print(f"{m:4d},{lgq:2d},{len(wt):2d},{mint:6.2f},{avgt:6.2f},{maxt:6.2f}", flush=True) 71 | 72 | # Return number of successes 73 | return len(wt) 74 | 75 | 76 | def __main__(): 77 | # Print CSV header 78 | print("m,lgq,num_success,time_min,time_avg,time_max", flush=True) 79 | 80 | path = "../input.lat" 81 | for m in range(2**4, 2**9 + 1, 2**4): 82 | # Decrement lgq until at least one run is successful. 83 | for lgq in range(30, 65): 84 | if count_successes(m, lgq, path) == 0: 85 | break 86 | 87 | 88 | if __name__ == "__main__": 89 | __main__() 90 | -------------------------------------------------------------------------------- /benchmark/optimal_segment_size.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import signal 4 | import subprocess 5 | 6 | mqs = [ 7 | (128, 631), # latticegen q 2 1 10 p 8 | (256, 829561), # latticegen q 2 1 20 p 9 | (512, 968665207), # latticegen q 2 1 30 p 10 | (1024, 968665207), # latticegen q 2 1 30 p 11 | ] 12 | seeds = range(10) 13 | cmd_blaster = "../python3 ../src/app.py -q" 14 | error_file = open("results/optimal_segment_size-errors.txt", "a", encoding='utf8') 15 | 16 | 17 | def parse_time_usage(time_output): 18 | """ 19 | Returns (real, user, sys). 20 | """ 21 | times = time_output.strip().split(" ") 22 | return (float(times[0]), float(times[1]), float(times[2])) 23 | 24 | 25 | # https://stackoverflow.com/questions/4789837/how-to-terminate-a-python-subprocess-launched-with-shell-true 26 | def time_run_and_timeout(cmd): 27 | timeout = 300 # 5 minutes 28 | 29 | # The os.setsid() is passed in the argument preexec_fn so 30 | # it's run after the fork() and before exec() to run the shell. 31 | with subprocess.Popen(cmd, text=True, shell=True, preexec_fn=os.setsid, 32 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: 33 | try: 34 | stdout, stderr = process.communicate(None, timeout=timeout) 35 | except subprocess.TimeoutExpired as exc: 36 | process.kill() 37 | os.killpg(os.getpgid(process.pid), signal.SIGTERM) 38 | # Send the signal to all the process groups 39 | 40 | # POSIX _communicate already populated the output so 41 | # far into the TimeoutExpired exception. 42 | process.wait() 43 | raise 44 | except: # Including KeyboardInterrupt, communicate handled that. 45 | process.kill() 46 | os.killpg(os.getpgid(process.pid), signal.SIGTERM) 47 | # We don't call process.wait() as .__exit__ does that for us. 48 | raise 49 | retcode = process.poll() 50 | return subprocess.CompletedProcess(process.args, retcode, stdout, stderr) 51 | 52 | 53 | def run_command(cmd): 54 | cmd = f"/usr/bin/time -f \"%e %U %S\" {cmd}" 55 | try: 56 | result = time_run_and_timeout(cmd) 57 | except subprocess.TimeoutExpired as exc: 58 | print(f"Timeout expired for command \"{cmd}\"", file=error_file) 59 | if exc.stderr is not None: 60 | print(exc.stderr.decode("utf-8"), file=error_file, flush=True) 61 | return None 62 | if result.returncode != 0: 63 | print(f"Non zero return code encountered for command \"{cmd}\"", file=error_file) 64 | print(result.stderr, file=error_file, flush=True) 65 | return None 66 | 67 | return parse_time_usage(result.stderr) 68 | 69 | 70 | def run_blaster(m, q, seed, LLLsize): 71 | logfile = f"../logs/optimal_segment_size/lll_{m}_{q}_{seed}_{LLLsize}.csv" 72 | path = f"../input/{m}_{q}_{seed}" 73 | return run_command(f"{cmd_blaster} -i {path} -l {logfile} -L {LLLsize}") 74 | 75 | 76 | def run_blaster_deeplll(m, q, seed, depth): 77 | logfile = f"../logs/optimal_segment_size/deeplll{depth}_{m}_{q}_{seed}_{LLLsize}.csv" 78 | path = f"../input/{m}_{q}_{seed}" 79 | return run_command(f"{cmd_blaster} -i {path} -l {logfile} -L {LLLsize} -d{depth}") 80 | 81 | 82 | def __main__(): 83 | # Print CSV header 84 | print("m,q,LLLsize,seed,real,user,sys", flush=True) 85 | for (m, q) in mqs: 86 | for LLLsize in range(32, 130, 2): 87 | if (m < 512 88 | or (m == 512 and LLLsize not in [92, 94, 98, 104]) 89 | or (m == 1024 and LLLsize not in [76, 86, 90, 104, 106, 108, 110]) 90 | ): 91 | continue 92 | if LLLsize > m: 93 | break 94 | for seed in range(10): 95 | res = run_blaster(m, q, seed, LLLsize) 96 | if res: 97 | print(f"{m:4d},{q:9d},{LLLsize},{seed:1d}," 98 | f"{res[0]:6.2f},{res[1]:6.2f},{res[2]:6.2f}", flush=True) 99 | 100 | 101 | if __name__ == "__main__": 102 | __main__() 103 | -------------------------------------------------------------------------------- /core/blaster.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | 3 | import numpy as np 4 | cimport cython 5 | cimport numpy as cnp 6 | 7 | from cysignals.signals cimport sig_on, sig_off 8 | from cython.parallel cimport prange 9 | from libc.string cimport memcpy 10 | from openmp cimport omp_set_num_threads, omp_get_num_threads, omp_get_thread_num 11 | 12 | from decl cimport FT, ZZ, lll_reduce, deeplll_reduce, bkz_reduce, \ 13 | eigen_init, eigen_matmul, eigen_left_matmul, eigen_right_matmul 14 | 15 | 16 | cnp.import_array() # http://docs.cython.org/en/latest/src/tutorial/numpy.html#adding-types 17 | NP_FT = np.float64 # floating-point type 18 | NP_ZZ = np.int64 # integer type 19 | 20 | 21 | cdef int debug_size_reduction = 0 22 | 23 | 24 | def set_debug_flag(int flag): 25 | global debug_size_reduction 26 | debug_size_reduction = flag 27 | 28 | 29 | def set_num_cores(int num_cores): 30 | omp_set_num_threads(num_cores) # used by `prange` in block_X 31 | eigen_init(num_cores) 32 | 33 | 34 | # 35 | # Lattice reduction 36 | # 37 | @cython.boundscheck(False) 38 | @cython.wraparound(False) 39 | def block_lll( 40 | cnp.ndarray[FT, ndim=2] R, cnp.ndarray[ZZ, ndim=2] B_red, cnp.ndarray[ZZ, ndim=2] U, 41 | FT delta, int offset, int block_size) -> None: 42 | global debug_size_reduction 43 | 44 | # Variables 45 | cdef: 46 | Py_ssize_t n = R.shape[0] 47 | int i, j, w, num_blocks = int((n - offset + block_size - 1) / block_size), block_id 48 | FT[:, ::1] R_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_FT) 49 | ZZ[:, ::1] U_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_ZZ) 50 | 51 | # Check that these are of the correct type: 52 | assert R.dtype == NP_FT and U.dtype == NP_ZZ 53 | 54 | sig_on() 55 | for block_id in prange(num_blocks, nogil=True): 56 | i = offset + block_size * block_id 57 | w = min(n - i, block_size) 58 | 59 | for j in range(w): 60 | memcpy(&R_sub[block_id, j * w], &R[i + j, i], w * sizeof(FT)); 61 | 62 | # Step 1: run LLL on block [i, i + w). 63 | lll_reduce(w, &R_sub[block_id, 0], &U_sub[block_id, 0], delta) 64 | 65 | if debug_size_reduction != 0: 66 | for j in range(w): 67 | memcpy(&R[i + j, i], &R_sub[block_id, j * w], w * sizeof(FT)); 68 | 69 | sig_off() 70 | 71 | # Step 2: Update U and B_red locally by multiplying with U_sub[block_id]. 72 | for block_id in range(num_blocks): 73 | i = offset + block_size * block_id 74 | w = min(n - i, block_size) 75 | 76 | ZZ_right_matmul_strided(U[:, i:i+w], U_sub[block_id, 0:w*w]) 77 | ZZ_right_matmul_strided(B_red[:, i:i+w], U_sub[block_id, 0:w*w]) 78 | 79 | 80 | @cython.boundscheck(False) 81 | @cython.wraparound(False) 82 | def block_deep_lll(int depth, 83 | cnp.ndarray[FT, ndim=2] R, cnp.ndarray[ZZ, ndim=2] B_red, cnp.ndarray[ZZ, ndim=2] U, 84 | FT delta, int offset, int block_size) -> None: 85 | global debug_size_reduction 86 | 87 | # Variables 88 | cdef: 89 | Py_ssize_t n = R.shape[0] 90 | int i, j, w, num_blocks = int((n - offset + block_size - 1) / block_size), block_id 91 | FT[:, ::1] R_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_FT) 92 | ZZ[:, ::1] U_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_ZZ) 93 | 94 | # Check that these are of the correct type: 95 | assert R.dtype == NP_FT and U.dtype == NP_ZZ 96 | 97 | sig_on() 98 | for block_id in prange(num_blocks, nogil=True): 99 | i = offset + block_size * block_id 100 | w = min(n - i, block_size) 101 | 102 | for j in range(w): 103 | memcpy(&R_sub[block_id, j * w], &R[i + j, i], w * sizeof(FT)); 104 | 105 | # Step 1: run deep-LLL on block [i, i + w). 106 | deeplll_reduce(w, &R_sub[block_id, 0], &U_sub[block_id, 0], delta, depth) 107 | 108 | if debug_size_reduction != 0: 109 | for j in range(w): 110 | memcpy(&R[i + j, i], &R_sub[block_id, j * w], w * sizeof(FT)); 111 | 112 | sig_off() 113 | 114 | # Step 2: Update U and B_red locally by multiplying with U_sub[block_id]. 115 | for block_id in range(num_blocks): 116 | i = offset + block_size * block_id 117 | w = min(n - i, block_size) 118 | 119 | ZZ_right_matmul_strided(U[:, i:i+w], U_sub[block_id, 0:w*w]) 120 | ZZ_right_matmul_strided(B_red[:, i:i+w], U_sub[block_id, 0:w*w]) 121 | 122 | 123 | @cython.boundscheck(False) 124 | @cython.wraparound(False) 125 | def block_bkz(int beta, 126 | cnp.ndarray[FT, ndim=2] R, cnp.ndarray[ZZ, ndim=2] B_red, cnp.ndarray[ZZ, ndim=2] U, 127 | FT delta, int offset, int block_size) -> None: 128 | global debug_size_reduction 129 | 130 | # Variables 131 | cdef: 132 | Py_ssize_t n = R.shape[0] 133 | int i, j, w, num_blocks = int((n - offset + block_size - 1) / block_size), block_id 134 | FT[:, ::1] R_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_FT) 135 | ZZ[:, ::1] U_sub = np.empty(shape=(num_blocks, block_size**2), dtype=NP_ZZ) 136 | 137 | # Check that these are of the correct type: 138 | assert R.dtype == NP_FT and U.dtype == NP_ZZ 139 | 140 | sig_on() 141 | for block_id in prange(num_blocks, nogil=True): 142 | i = offset + block_size * block_id 143 | w = min(n - i, block_size) 144 | 145 | for j in range(w): 146 | memcpy(&R_sub[block_id, j * w], &R[i + j, i], w * sizeof(FT)); 147 | 148 | # Step 1: run BKZ on block [i, i + w). 149 | bkz_reduce(w, &R_sub[block_id, 0], &U_sub[block_id, 0], delta, beta) 150 | 151 | if debug_size_reduction != 0: 152 | for j in range(w): 153 | memcpy(&R[i + j, i], &R_sub[block_id, j * w], w * sizeof(FT)); 154 | 155 | sig_off() 156 | 157 | # Step 2: Update U and B_red locally by multiplying with U_sub[block_id]. 158 | for block_id in range(num_blocks): 159 | i = offset + block_size * block_id 160 | w = min(n - i, block_size) 161 | 162 | ZZ_right_matmul_strided(U[:, i:i+w], U_sub[block_id, 0:w*w]) 163 | ZZ_right_matmul_strided(B_red[:, i:i+w], U_sub[block_id, 0:w*w]) 164 | 165 | 166 | # 167 | # Integer (int64) Matrix Multiplication using Eigen, which internally uses OpenMP. 168 | # 169 | def ZZ_matmul(const ZZ[:, ::1] A, const ZZ[:, ::1] B) -> cnp.ndarray[ZZ]: 170 | """ 171 | Return A * B. 172 | A and B should be C-contiguous. 173 | """ 174 | cdef: 175 | int n = A.shape[0], m = A.shape[1], k = B.shape[1] 176 | ZZ[:, ::1] C = np.empty(shape=(n, k), dtype=NP_ZZ) 177 | 178 | assert B.shape[0] == m, "Dimension mismatch" 179 | eigen_matmul(&A[0, 0], &B[0, 0], &C[0, 0], n, m, k) 180 | return np.asarray(C) 181 | 182 | 183 | def ZZ_left_matmul_strided(const ZZ[:, :] A, ZZ[:, :] B) -> None: 184 | """ 185 | Compute B <- A * B. 186 | A and B may have a row-stride. 187 | """ 188 | cdef: 189 | int n = B.shape[0], m = B.shape[1] 190 | int stride_a = A.strides[0] // sizeof(ZZ), stride_b = B.strides[0] // sizeof(ZZ) 191 | 192 | assert A.strides[1] == sizeof(ZZ), "Array A not C-contiguous" 193 | assert B.strides[1] == sizeof(ZZ), "Array B not C-contiguous" 194 | eigen_left_matmul(&A[0, 0], &B[0, 0], n, m, stride_a, stride_b) 195 | 196 | 197 | def ZZ_right_matmul(ZZ[:, ::1] A, const ZZ[:, ::1] B) -> None: 198 | """ 199 | Compute A <- A * B. 200 | A and B should be C-contiguous. 201 | """ 202 | cdef: 203 | int n = A.shape[0], m = A.shape[1] 204 | 205 | assert B.shape[0] == m and B.shape[1] == m, "Dimension mismatch" 206 | eigen_right_matmul(&A[0, 0], &B[0, 0], n, m) 207 | 208 | 209 | def ZZ_right_matmul_strided(ZZ[:, :] A, const ZZ[:] B) -> None: 210 | """ 211 | Compute A <- A * B. 212 | A may have a row-stride. B should be a 1-dimensional array of length m^2, 213 | where m is the number of columns of A. 214 | """ 215 | cdef: 216 | int n = A.shape[0], m = A.shape[1], stride_a = A.strides[0] // sizeof(ZZ) 217 | 218 | assert A.strides[1] == sizeof(ZZ), "Array A not C-contiguous" 219 | eigen_right_matmul(&A[0, 0], &B[0], n, m, stride_a) 220 | 221 | 222 | # 223 | # Floating-point (double) Matrix Multiplication using NumPy, which internally uses BLAS. 224 | # 225 | def FT_matmul(cnp.ndarray[FT, ndim=2] A, cnp.ndarray[FT, ndim=2] B) -> cnp.ndarray[FT]: 226 | """ 227 | Return A * B. 228 | """ 229 | return A @ B 230 | 231 | -------------------------------------------------------------------------------- /core/decl.pxd: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | 3 | cdef extern from "types.hpp": 4 | int MAX_ENUM_N 5 | ctypedef double FT # floating-point type 6 | ctypedef long long ZZ # integer type 7 | 8 | cdef extern from "lattice_reduction.cpp" nogil: 9 | void lll_reduce(const int N, FT *R, ZZ *U, const FT delta) 10 | void deeplll_reduce(const int N, FT *R, ZZ *U, const FT delta, const int depth) 11 | void bkz_reduce(const int N, FT *R, ZZ *U, const FT delta, const int beta) 12 | 13 | cdef extern from "eigen_matmul.cpp" nogil: 14 | void eigen_init(int num_cores) 15 | 16 | # c = a * b 17 | void eigen_matmul(const ZZ *a, const ZZ *b, ZZ *c, int n, int m, int k) 18 | 19 | # b = a * b 20 | void eigen_left_matmul(const ZZ *a, ZZ *b, int n, int m, int stride_a, int stride_b) 21 | 22 | # a = a * b 23 | void eigen_right_matmul(ZZ *a, const ZZ *b, int n, int m) 24 | void eigen_right_matmul(ZZ *a, const ZZ *b, int n, int m, int stride_a) 25 | -------------------------------------------------------------------------------- /core/eigen_matmul.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "types.hpp" 5 | 6 | 7 | using Matrix = Eigen::Matrix; 8 | typedef Eigen::Stride Stride; 9 | 10 | 11 | void eigen_init(int num_cores) { 12 | // See https://eigen.tuxfamily.org/dox/TopicMultiThreading.html 13 | Eigen::initParallel(); 14 | Eigen::setNbThreads(num_cores); 15 | } 16 | 17 | 18 | // See: https://eigen.tuxfamily.org/dox/group__TutorialMapClass.html 19 | 20 | /** 21 | * Compute the matrix product between a and b, and store the result `a * b` in `c`. 22 | * Dimensions of `a`, `b` and `c` are assumed to be `n x m`, `m x k` and `n x k` respectively. 23 | */ 24 | void eigen_matmul(const ZZ *a, const ZZ *b, ZZ *c, int n, int m, int k) { 25 | Eigen::Map ma(a, n, m), mb(b, m, k); 26 | Eigen::Map mc(c, n, k); 27 | 28 | mc = ma * mb; 29 | } 30 | 31 | /** 32 | * Compute the matrix product between a and b, and store the result `a * b` in `b`. 33 | * Dimensions of `a` and `b` are assumed to be `n x n` and `n x m` respectively. 34 | */ 35 | void eigen_left_matmul(const ZZ *a, ZZ *b, int n, int m, int stride_a, int stride_b) { 36 | Eigen::Map ma(a, n, n, Stride(stride_a, 1)); 37 | Eigen::Map mb(b, n, m, Stride(stride_b, 1)); 38 | 39 | mb = ma * mb; 40 | } 41 | 42 | /** 43 | * Compute the matrix product between a and b, and store the result `a * b` in `a`. 44 | * Dimensions of `a` and `b` are assumed to be `n x m` and `m x m` respectively. 45 | */ 46 | void eigen_right_matmul(ZZ *a, const ZZ *b, int n, int m) { 47 | Eigen::Map ma(a, n, m); 48 | Eigen::Map mb(b, m, m); 49 | 50 | ma *= mb; 51 | } 52 | 53 | void eigen_right_matmul(ZZ *a, const ZZ *b, int n, int m, int stride_a) { 54 | Eigen::Map ma(a, n, m, Stride(stride_a, 1)); 55 | Eigen::Map mb(b, m, m); 56 | 57 | ma *= mb; 58 | } 59 | -------------------------------------------------------------------------------- /core/enumeration.cpp: -------------------------------------------------------------------------------- 1 | #ifndef ENUMLIB_WRAPPER_ENUMERATION_CPP 2 | #define ENUMLIB_WRAPPER_ENUMERATION_CPP 3 | 4 | #include 5 | 6 | #include "enumeration.hpp" 7 | 8 | /* 9 | * Perform enumeration to solve SVP in dimension N, using the enumlib library by Marc Stevens. 10 | * 11 | * @param N is dimension 12 | * @param R: upper-diagonal matrix of dimension N*N. B=Q*R 13 | * @param rowstride: rowstride of R. R(row,col) = R[rowstride*row + col] 14 | * @param pruningvector: vector of dimension N containing *relative* bounds for the squared norm within the projected sublattices. 15 | * @param enumeration_radius: expected norm squared of the shortest nonzero vector in this lattice 16 | * @param sol: return param: integer vector solution with respect to current basis, or the 0 vector otherwise 17 | * 18 | * Complexity: super-exponential in N. 19 | */ 20 | FT enumeration(const int N, const FT *R, const int rowstride, const FT *pruningvector, FT enumeration_radius, ZZ* sol) 21 | { 22 | // ensure we always return the 0-vector in sol, unless a valid solution is found 23 | std::fill(sol, sol+N, ZZ(0)); 24 | 25 | // enumeration is only supported up to MAX_ENUM_N dimensions 26 | if (N > MAX_ENUM_N || N <= 0) 27 | return 0.0; 28 | 29 | // we pad the enumeration tree with virtual basis vectors up to dim MAX_ENUM_N 30 | // these virtual basis vectors have very high length 31 | // thus these will have zero coefficients in any found solution 32 | lattice_enum_t enumobj; 33 | 34 | // initialize enumobj.muT 35 | // assumption: enumobj.muT is all-zero 36 | for (int i = 0; i < N-1; ++i) { 37 | FT* muTi = &enumobj.muT[i][0]; 38 | const FT* Ri = R+(i*rowstride); 39 | FT Rii_inv = FT(1.0) / Ri[i]; 40 | for (int j = i+1; j < N; ++j) { 41 | // muT[i][j] = / ||bi*||^2 42 | muTi[j] = Ri[j] * Rii_inv; 43 | } 44 | } 45 | 46 | // initialize enumobj.risq 47 | for (int i = 0; i < N; ++i) { 48 | // risq[i] = ||bi*||^2 49 | enumobj.risq[i] = R[i*rowstride+i] * R[i*rowstride+i]; 50 | } 51 | 52 | // set the pruning vectors 53 | if (pruningvector == nullptr) 54 | for (int i = 0; i < N; i++) 55 | enumobj.pr[i] = 1.0; 56 | else 57 | std::copy(&pruningvector[0], &pruningvector[N], &enumobj.pr[0]); 58 | 59 | // pad enumeration tree to MAX_ENUM_N dimension using virtual basis vectors of length above enumeration bound 60 | for (int i = N; i < MAX_ENUM_N; ++i) { 61 | // ensure these virtual basis vectors are never used 62 | enumobj.risq[i] = 2.0 * enumobj.risq[0]; // = 2 * ||b0*||^2 63 | enumobj.pr[i] = 1.0; 64 | } 65 | 66 | // set the initial norm bound 67 | enumobj._A = enumeration_radius; 68 | 69 | 70 | // perform enumeration 71 | enumobj.enumerate_recursive(); 72 | 73 | // the virtual basis vectors should never be used 74 | // if sol is non-zero for these positions then there is an internal error 75 | for (int i = N; i < MAX_ENUM_N; ++i) { 76 | if (enumobj._sol[i] != 0) { 77 | std::cerr << "[enum]: dim=" << N << ": internal error _sol[" << i << "] != 0." << std::endl; 78 | return 0.0; 79 | } 80 | } 81 | 82 | // write enumeration solution to sol 83 | for (int i = 0; i < N; ++i) { 84 | sol[i] = enumobj._sol[i]; 85 | } 86 | 87 | // return the squared norm of the solution found 88 | return (enumobj._A < enumeration_radius) ? enumobj._A : 0.0; 89 | } 90 | 91 | #endif // ENUMLIB_WRAPPER_ENUMERATION_CPP 92 | -------------------------------------------------------------------------------- /core/enumeration.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Marc Stevens 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #ifndef ENUMLIB_ENUMERATION_HPP 26 | #define ENUMLIB_ENUMERATION_HPP 27 | 28 | #include 29 | #include 30 | #include 31 | 32 | #include "types.hpp" 33 | 34 | #define NOCOUNTS 1 35 | 36 | template 37 | struct lattice_enum_t 38 | { 39 | typedef std::array fltrow_t; 40 | typedef std::array introw_t; 41 | 42 | /* inputs */ 43 | // mu^T corresponds to R (B=Q*R) with multiplicative corrections: muT[i][j] = R[i][j] / R[i][i] 44 | // mu^T is the transposed mu in fplll (see also: An LLL Algorithm with Quadratic Complexity, Nguyen, Stehle, 2009.) 45 | FT muT[N][N]; 46 | // risq[i] is ||bi*||^2, or R[i][i]*R[i][i] 47 | fltrow_t risq; 48 | // the *relative* pruning bounds for the squared norm within the projected sublattices. 49 | fltrow_t pr; 50 | 51 | /* internals */ 52 | FT _A; // overall enumeration bound 53 | fltrow_t _AA; // enumeration pruning bounds 54 | introw_t _x, _Dx, _D2x; 55 | fltrow_t _sol; // to pass to fplll 56 | fltrow_t _c; 57 | introw_t _r; 58 | std::array _l; 59 | std::array _counts; 60 | 61 | FT _sigT[N][N]; 62 | 63 | lattice_enum_t() 64 | : muT(), risq(), pr() 65 | { 66 | } 67 | 68 | inline void _update_AA() 69 | { 70 | // ensure that the pruning bounds are non-increasing from a basis perspective. 71 | for (int k = 0; k < N; ++k) { 72 | _AA[k] = _A * pr[k]; 73 | } 74 | } 75 | 76 | // compile time parameters for enumerate_recur (without ANY runtime overhead) 77 | // allows specialization for certain specific cases, e.g., i=0 78 | template struct i_tag {}; 79 | 80 | template 81 | inline void enumerate_recur(i_tag) 82 | { 83 | if (_r[i] > _r[i - 1]) 84 | _r[i - 1] = _r[i]; 85 | FT ci = _sigT[i][i]; 86 | FT yi = round(ci); 87 | ZZ xi = (ZZ)(yi); 88 | yi = ci - yi; 89 | FT li = _l[i + 1] + (yi * yi * risq[i]); 90 | #ifndef NOCOUNTS 91 | ++_counts[i]; 92 | #endif 93 | 94 | if (li > _AA[i]) 95 | return; 96 | 97 | _Dx[i] = _D2x[i] = (((int)(yi >= 0) & 1) << 1) - 1; 98 | _c[i] = ci; 99 | _x[i] = xi; 100 | _l[i] = li; 101 | 102 | for (int j = _r[i - 1]; j > i - 1; --j) 103 | _sigT[i - 1][j - 1] = _sigT[i - 1][j] - _x[j] * muT[i - 1][j]; 104 | 105 | while (true) 106 | { 107 | enumerate_recur(i_tag()); 108 | 109 | if (_l[i + 1] == 0.0) { 110 | ++_x[i]; 111 | } else { 112 | _x[i] += _Dx[i]; 113 | _D2x[i] = -_D2x[i]; 114 | _Dx[i] = _D2x[i] - _Dx[i]; 115 | } 116 | 117 | _r[i - 1] = i; 118 | FT yi2 = _c[i] - _x[i]; 119 | FT li2 = _l[i + 1] + (yi2 * yi2 * risq[i]); 120 | if (li2 > _AA[i]) 121 | return; 122 | _l[i] = li2; 123 | _sigT[i - 1][i - 1] = _sigT[i - 1][i] - _x[i] * muT[i - 1][i]; 124 | } 125 | } 126 | 127 | inline void enumerate_recur(i_tag<0>) 128 | { 129 | static constexpr int i = 0; 130 | FT ci = _sigT[i][i]; 131 | FT yi = round(ci); 132 | ZZ xi = (ZZ)(yi); 133 | yi = ci - yi; 134 | FT li = _l[i + 1] + (yi * yi * risq[i]); 135 | #ifndef NOCOUNTS 136 | ++_counts[i]; 137 | #endif 138 | 139 | if (li > _AA[i]) 140 | return; 141 | 142 | _Dx[i] = _D2x[i] = (((int)(yi >= 0) & 1) << 1) - 1; 143 | _c[i] = ci; 144 | _x[i] = xi; 145 | _l[i] = li; 146 | 147 | while (true) 148 | { 149 | enumerate_recur(i_tag()); 150 | 151 | if (_l[i + 1] == 0.0) { 152 | ++_x[i]; 153 | } else { 154 | _x[i] += _Dx[i]; 155 | _D2x[i] = -_D2x[i]; 156 | _Dx[i] = _D2x[i] - _Dx[i]; 157 | } 158 | 159 | FT yi2 = _c[i] - _x[i]; 160 | FT li2 = _l[i + 1] + (yi2 * yi2 * risq[i]); 161 | if (li2 > _AA[i]) 162 | return; 163 | _l[i] = li2; 164 | } 165 | } 166 | 167 | 168 | inline void enumerate_recur(i_tag<-1>) 169 | { 170 | if (_l[0] > _A || _l[0] == 0.0) 171 | return; 172 | 173 | for (int j = 0; j < N; ++j) 174 | _sol[j] = _x[j]; 175 | 176 | _A = _l[0]; 177 | _update_AA(); 178 | } 179 | 180 | inline void enumerate_recursive() 181 | { 182 | _update_AA(); 183 | 184 | std::fill(_l.begin(), _l.end(), 0.0); 185 | std::fill(_x.begin(), _x.end(), 0); 186 | std::fill(_Dx.begin(), _Dx.end(), 0); 187 | std::fill(_D2x.begin(), _D2x.end(), -1); 188 | std::fill(_c.begin(), _c.end(), 0.0); 189 | 190 | std::fill(_r.begin(), _r.end(), N-1); 191 | std::fill_n(&_sigT[0][0], N * N, 0.0); 192 | 193 | std::fill(_sol.begin(), _sol.end(), 0); 194 | std::fill(_counts.begin(), _counts.end(), 0); 195 | 196 | enumerate_recur(i_tag()); 197 | } 198 | }; 199 | 200 | #endif // ENUMLIB_ENUMERATION_HPP 201 | -------------------------------------------------------------------------------- /core/lattice_reduction.cpp: -------------------------------------------------------------------------------- 1 | #include // std::swap, std::fill_n 2 | #include // llround, sqrt 3 | 4 | #include "enumeration.cpp" 5 | #include "pruning_params.cpp" 6 | 7 | 8 | /******************************************************************************* 9 | * Helper functions to access the matrices R and U at row 'row' and column 'col' 10 | ******************************************************************************/ 11 | #define RR(row, col) R[(row) * N + (col)] 12 | #define RSQ(row, col) (RR(row, col) * RR(row, col)) 13 | 14 | #define UU(row, col) U[(row) * N + (col)] 15 | 16 | /* 17 | * Replace `b_j` by `b_j + number * b_i`, and 18 | * update R-factor and transformation matrix U accordingly. 19 | * Assumes i < j. 20 | * 21 | * Complexity: O(N) 22 | */ 23 | inline void alter_basis(const int N, FT *R, ZZ *U, int i, int j, ZZ number) 24 | { 25 | if (number == 0) { 26 | return; 27 | } 28 | 29 | // R_j += number * R_i. 30 | for (int k = 0; k <= i; k++) { 31 | RR(k, j) += number * RR(k, i); 32 | } 33 | 34 | // U_i += number * U_i. 35 | for (int k = 0; k < N; k++) { 36 | UU(k, j) += number * UU(k, i); 37 | } 38 | } 39 | 40 | /* 41 | * Size reduce column j with respect to column i (i < j), and 42 | * update the R-factor and transformation matrix U accordingly. 43 | * 44 | * Complexity: O(N) 45 | */ 46 | inline void size_reduce(const int N, FT *R, ZZ *U, int i, int j) 47 | { 48 | alter_basis(N, R, U, i, j, llround(-RR(i, j) / RR(i, i))); 49 | } 50 | 51 | /* 52 | * Swap the adjacent basis vectors b_k and b_{k+1} and update the R-factor and transformation 53 | * matrix U correspondingly. R is updated by performing a Givens rotation. 54 | * 55 | * Complexity: O(N) 56 | */ 57 | void swap_basis_vectors(const int N, FT *R, ZZ *U, const int k) 58 | { 59 | // a. Perform Givens rotation on coordinates {k, k+1}, and update R. 60 | FT c = RR(k, k + 1), s = RR(k + 1, k + 1), norm = sqrt(c * c + s * s); 61 | c /= norm; 62 | s /= norm; 63 | 64 | RR(k, k + 1) = c * RR(k, k); 65 | RR(k + 1, k + 1) = s * RR(k, k); 66 | RR(k, k) = norm; 67 | 68 | for (int i = k + 2; i < N; i++) { 69 | FT new_value = c * RR(k, i) + s * RR(k + 1, i); 70 | RR(k + 1, i) = s * RR(k, i) - c * RR(k + 1, i); 71 | RR(k, i) = new_value; 72 | } 73 | 74 | // b. Swap R_k and R_{k+1}, except the already processed 2x2 block. 75 | for (int i = 0; i < k; i++) { 76 | std::swap(RR(i, k), RR(i, k + 1)); 77 | } 78 | 79 | // c. Swap U_k and U_{k+1}. 80 | for (int i = 0; i < N; i++) { 81 | std::swap(UU(i, k), UU(i, k + 1)); 82 | } 83 | } 84 | 85 | inline void init_U(const int N, ZZ *U) 86 | { 87 | // Initialize U with the identity matrix 88 | std::fill_n(U, N * N, ZZ(0)); 89 | for (int i = 0; i < N; i++) { 90 | UU(i, i) = 1; 91 | } 92 | } 93 | 94 | /******************************************************************************* 95 | * LLL reduction 96 | ******************************************************************************/ 97 | void _lll_reduce(const int N, FT *R, ZZ *U, const FT delta, const int limit_k) 98 | { 99 | // Loop invariant: [0, k) is LLL-reduced (size reduced and Lovász' condition holds). 100 | for (int k = 1; k < limit_k; ) { 101 | // 1. Size-reduce b_k wrt b_0, ..., b_{k-1}. 102 | for (int i = k - 1; i >= 0; --i) { 103 | size_reduce(N, R, U, i, k); 104 | } 105 | 106 | // 2. Check Lovász’ condition: `\delta ||\pi(b_{k-1})||^2 <= ||\pi(b_k)||^2`. 107 | if (delta * RSQ(k - 1, k - 1) <= RSQ(k - 1, k) + RSQ(k, k)) { 108 | // Lovász’ condition is satisfied at `k`, so increment `k`. 109 | k++; 110 | } else { 111 | // Lovász’ condition is not satisfied at `k`. 112 | // 3. Swap b_{k - 1} and b_k. 113 | swap_basis_vectors(N, R, U, k - 1); 114 | 115 | // 4. Decrease `k` if possible. 116 | if (k > 1) k--; 117 | } 118 | } 119 | } 120 | 121 | /* 122 | * Perform LLL reduction on the basis R, and return the transformation matrix U such that RU is 123 | * LLL-reduced. 124 | * 125 | * @param R upper-triangular matrix representing the R-factor from QR decomposing the basis. 126 | * @param U transformation matrix, that was applied to R to LLL-reduce it. 127 | * 128 | * Complexity: poly(N) (for a fixed delta < 1). 129 | */ 130 | void lll_reduce(const int N, FT *R, ZZ *U, const FT delta) 131 | { 132 | init_U(N, U); 133 | _lll_reduce(N, R, U, delta, N); 134 | } 135 | 136 | /******************************************************************************* 137 | * LLL reduction with deep insertions 138 | ******************************************************************************/ 139 | void _deeplll_reduce(const int N, FT *R, ZZ *U, const FT delta, const int depth, const int limit_k) 140 | { 141 | // First run LLL, because this makes deep-LLL faster, see [2]. 142 | // [2] https://doi.org/10.1007/s10623-014-9918-8 143 | _lll_reduce(N, R, U, delta, limit_k); 144 | 145 | // Loop invariant: [0, k) is depth-deep-LLL-reduced. 146 | for (int k = 1; k < limit_k; ) { 147 | // 1. Size-reduce R_k wrt R_0, ..., R_{k - 1}. 148 | for (int i = k - 1; i >= 0; i--) { 149 | size_reduce(N, R, U, i, k); 150 | } 151 | 152 | // 2. Determine ||b_k||^2 153 | FT proj_norm_sq = 0.0; 154 | for (int i = 0; i <= k; i++) { 155 | proj_norm_sq += RSQ(i, k); 156 | } 157 | 158 | // 3. Look for an i < k such that ||pi_i(b_k)||^2 <= delta ||b_i*||^2. 159 | // Loop invariant: proj_norm_sq = ||pi_i(b_k)||^2. 160 | bool swap_performed = false; 161 | for (int i = 0; i < k; i++) { 162 | if ((i < depth || i >= k - depth) && proj_norm_sq <= delta * RSQ(i, i)) { 163 | // 3a. Deep insert b_k at position i and move b_i, ..., b_{k-1} 164 | // one position forward. Complexity: O(N * (k - i)) 165 | while (k > i) { 166 | swap_basis_vectors(N, R, U, --k); 167 | } 168 | if (k == 0) k++; 169 | swap_performed = true; 170 | break; 171 | } 172 | 173 | // 3b. Increment i and update ||pi_i(b_k)||^2. 174 | proj_norm_sq -= RSQ(i, k); 175 | } 176 | 177 | if (!swap_performed) k++; 178 | } 179 | } 180 | 181 | /* 182 | * Perform depth-deep-LLL reduction on the basis R, and return the transformation matrix U such 183 | * that RU is depth-deep-LLL-reduced. 184 | * 185 | * @param R upper-triangular matrix representing the R-factor from QR decomposing the basis. 186 | * @param U transformation matrix, that was applied to R to deep-LLL-reduce it. 187 | * @param depth maximum number of positions allowed for 'deep insertions' 188 | * 189 | * Complexity: poly(N) (for a fixed delta < 1 and a fixed depth). 190 | */ 191 | void deeplll_reduce(const int N, FT *R, ZZ *U, const FT delta, const int depth) 192 | { 193 | init_U(N, U); 194 | _deeplll_reduce(N, R, U, delta, depth, N); 195 | } 196 | 197 | /******************************************************************************* 198 | * BKZ reduction 199 | ******************************************************************************/ 200 | 201 | /* 202 | * Compute and return the square of the Gaussian Heuristic, i.e. (the square 203 | * of) the expected length of the shortest nonzero vector, for a lattice of 204 | * dimension `dimension` with a determinant (covolume) of `exp(log_volume)`. 205 | * 206 | * @param dimension dimension of lattice 207 | * @param log_volume logarithm of volume of the lattice, natural base. 208 | * @return GH(L)^2, for a lattice L of rank `dimension` and `det(L) = exp(log_volume)`. 209 | */ 210 | FT gh_squared(int dimension, FT log_volume) 211 | { 212 | // GH(n) = Gamma(n / 2 + 1) / (pi)^{n / 2} so 213 | // GH(n)^2 = exp(log(Gamma(n / 2 + 1)) / n) / pi. 214 | FT log_gamma = lgamma(dimension / 2.0 + 1); 215 | return exp(2.0 * (log_gamma + log_volume) / dimension) / M_PI; 216 | } 217 | 218 | FT safe_gh_squared(int dim, FT log_volume) 219 | { 220 | // Loosely based on Figure 2 from [3]: 221 | FT gh2 = gh_squared(dim, log_volume); 222 | FT gh_factor = std::max(1.05, std::min(2.0, 1.0 + 4.0 / dim)); 223 | return gh2 * gh_factor * gh_factor; 224 | } 225 | 226 | /* 227 | * Solve SVP on b_[0, N), and 228 | * put the result somewhere in the basis where the coefficient is +1/-1, and 229 | * run LLL on b_0, ..., b_{N-1}. 230 | * 231 | * Based on Algorithm 1 from [3]. 232 | * [3] https://doi.org/10.1007/978-3-642-25385-0_1 233 | */ 234 | void svp(const int N, FT *R, ZZ *U, const FT delta, int i, int w, ZZ *sol) 235 | { 236 | // Solve SVP on block [i, i + w). 237 | FT log_volume = 0.0; 238 | for (int j = 0; j < w; j++) { 239 | log_volume += log(RSQ(i + j, i + j)); 240 | } 241 | 242 | // Find a solution that is shorter than current basis vector and (1 + eps)·GH 243 | FT expected_normsq = std::min((1023.0 / 1024) * RSQ(i, i), safe_gh_squared(w, log_volume)); 244 | 245 | // Pick the pruning parameters for `pr[0 ... w - 1]`. 246 | const FT *pr = get_pruning_coefficients(w); 247 | 248 | // [3] Algorithm 1, line 4: 249 | // Perform enumeration to find the shortest vector. 250 | FT sol_square_norm = enumeration(w, &RR(i, i), N, pr, expected_normsq, sol); 251 | 252 | // [3] Algorithm 1, line 5: 253 | // Check if a shorter, nonzero vector is found. 254 | if (sol_square_norm > 0.0) { 255 | // Find the last nonzero coefficient. 256 | int j = w - 1; 257 | while (j > 0 && sol[j] == 0) { 258 | j--; 259 | } 260 | 261 | // Replace `v` by `-v`, if sol[j] = -1. 262 | if (j > 0 && sol[j] == -1) { 263 | for (int k = 0; k <= j; k++) { 264 | sol[k] = -sol[k]; 265 | } 266 | } 267 | 268 | // Only do an insertion of the shorter vector if sol[j] = 1. 269 | if (j > 0 && sol[j] == 1) { 270 | // Update `b_{i + j} <-- \sum_{k=0}^j sol[k] b_{i + k}`. 271 | for (int k = 0; k < j; k++) { 272 | // for all 0 <= k < j: b_{i + j} += sol[k] * b_{i + k}. 273 | alter_basis(N, R, U, i + k, i + j, sol[k]); 274 | } 275 | 276 | // Move b_{i + j} to position b_i. 277 | while (--j >= 0) { 278 | swap_basis_vectors(N, R, U, i + j); 279 | } 280 | } 281 | } 282 | 283 | /* There are three possible reasons why no update was performed: 284 | * 1. A shorter vector is not found because of pruning 285 | * 2. `b_i` is already the shortest vector in the block [i, i + w) 286 | * 3. The solution coefficients do not allow an easy insertion. 287 | * 288 | * Note 1: See pruning_params.cpp for the success probability. 289 | * Note 2: In practice, reason 3 seldomly happens, when calling progressive BKZ. The algorithm 290 | * could be modified to handle such difficult insertions. 291 | */ 292 | 293 | // [3] Algorithm 1, line 6 or 8 294 | // DeepLLL-reduce [0, i + w) such that the next enumeration runs on a DeepLLL-reduced basis. 295 | _deeplll_reduce(N, R, U, delta, 4, std::min(i + w, N)); 296 | } 297 | 298 | /* 299 | * Perform BKZ-beta reduction on the basis R, and return the transformation matrix U such that 300 | * RU is BKZ-beta-reduced. 301 | * 302 | * @param R upper-triangular matrix representing the R-factor from QR decomposing the basis. 303 | * @param U transformation matrix, that was applied to R to deep-LLL-reduce it. 304 | * @param beta blocksize used for BKZ (dimension of SVP oracle that uses enumeration). 305 | * 306 | * Complexity: poly(N) * beta^{c_BKZ beta} for a fixed delta < 1, where c_BKZ ~ 0.125 in [1]. 307 | * [1] https://doi.org/10.1007/978-3-030-56880-1_7 308 | */ 309 | void bkz_reduce(const int N, FT *R, ZZ *U, const FT delta, int beta) 310 | { 311 | ZZ sol[MAX_ENUM_N]; // coefficients of the enumeration solution for SVP in block of size beta. 312 | 313 | // First init U and run deep-LLL, before performing BKZ. 314 | deeplll_reduce(N, R, U, delta, 4); 315 | 316 | if (beta <= 2) return; 317 | 318 | if (beta > N) { 319 | // Perform one HKZ-tour. 320 | // Note: this is only done at the end of the global basis! 321 | for (int i = 0; i + 2 <= N; i++) { 322 | // Solve SVP on block [i, N). 323 | svp(N, R, U, delta, i, N - i, sol); 324 | } 325 | } else { 326 | // Perform one BKZ-tour. 327 | for (int i = 0; i + beta <= N; i++) { 328 | // Solve SVP on block [i, i + beta). 329 | svp(N, R, U, delta, i, beta, sol); 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /core/pruning_params.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "types.hpp" 5 | 6 | /** 7 | * The following pruning parameters are taken by using the Pruner from [1], 8 | * bases are simulated as having the theoretical slope of BKZ-(beta-2), in 9 | * attempt at predicting bases shape during progressive-BKZ 10 | * 11 | * Generated using the script ./find_pruning_params.py. 12 | * 13 | */ 14 | const std::vector pruning_params[MAX_ENUM_N/2 + 1] = { 15 | {}, // dummy values 16 | { 1.0000, 0.0001 }, // BKZ-2, p=1.000000 17 | { 1.0000, 0.7347, 0.4695, 0.0395 }, // BKZ-4, p=0.288170 18 | { 1.0000, 0.8297, 0.6595, 0.4725, 0.3040, 0.2369 }, // BKZ-6, p=0.394078 19 | { 1.0000, 0.9040, 0.8081, 0.6523, 0.4992, 0.3503, 0.2606, 0.2169 }, // BKZ-8, p=0.339650 20 | { 1.0000, 1.0000, 0.9966, 0.9966, 0.5197, 0.5197, 0.3264, 0.3264, 0.3190, 0.3190 }, // BKZ-10, p=0.250344 21 | { 1.0000, 1.0000, 0.9900, 0.9900, 0.6683, 0.6683, 0.4022, 0.4022, 0.3848, 0.3848, 0.3848, 0.3848 }, // BKZ-12, p=0.250184 22 | { 1.0000, 1.0000, 1.0000, 0.9452, 0.8287, 0.7184, 0.5753, 0.5091, 0.4316, 0.3494, 0.3046, 0.2968, 0.2946, 0.2930 }, // BKZ-14, p=0.248952 23 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9305, 0.9305, 0.5958, 0.5958, 0.4306, 0.4306, 0.3598, 0.3598, 0.3564, 0.3564, 0.3564, 0.3564 }, // BKZ-16, p=0.250227 24 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9279, 0.8667, 0.7565, 0.6663, 0.5543, 0.4969, 0.4252, 0.3901, 0.3255, 0.3167, 0.3133, 0.3115, 0.3096, 0.3078 }, // BKZ-18, p=0.249684 25 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9307, 0.8839, 0.8004, 0.7443, 0.6453, 0.5792, 0.4931, 0.4417, 0.3841, 0.3342, 0.3306, 0.3299, 0.3299, 0.3299, 0.3299, 0.3299 }, // BKZ-20, p=0.249506 26 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9708, 0.9357, 0.8518, 0.7911, 0.7070, 0.6378, 0.5536, 0.5053, 0.4485, 0.4075, 0.3581, 0.3247, 0.2910, 0.2910, 0.2910, 0.2910, 0.2910, 0.2910 }, // BKZ-22, p=0.249768 27 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9725, 0.9691, 0.8760, 0.8735, 0.7544, 0.7519, 0.6280, 0.6236, 0.5157, 0.5077, 0.4169, 0.4161, 0.3304, 0.3176, 0.2547, 0.2490, 0.1783, 0.1741, 0.1680, 0.1679 }, // BKZ-24, p=0.249728 28 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9769, 0.9681, 0.8912, 0.8891, 0.7798, 0.7759, 0.6649, 0.6580, 0.5590, 0.5495, 0.4639, 0.4540, 0.3804, 0.3706, 0.3068, 0.2969, 0.2422, 0.2418, 0.1881, 0.1777, 0.1571, 0.1571 }, // BKZ-26, p=0.243917 29 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9812, 0.9773, 0.9050, 0.9024, 0.8053, 0.8053, 0.6992, 0.6972, 0.5969, 0.5944, 0.5061, 0.5018, 0.4260, 0.4201, 0.3561, 0.3479, 0.2972, 0.2849, 0.2405, 0.2284, 0.1852, 0.1787, 0.1673, 0.1673 }, // BKZ-28, p=0.247274 30 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9847, 0.9806, 0.9182, 0.9156, 0.8292, 0.8269, 0.7291, 0.7272, 0.6322, 0.6307, 0.5438, 0.5421, 0.4656, 0.4625, 0.3978, 0.3919, 0.3364, 0.3295, 0.2821, 0.2729, 0.2338, 0.2219, 0.1909, 0.1767, 0.1458, 0.1449 }, // BKZ-30, p=0.244716 31 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9885, 0.9885, 0.9292, 0.9273, 0.8484, 0.8456, 0.7540, 0.7504, 0.6631, 0.6579, 0.5787, 0.5721, 0.5014, 0.4946, 0.4322, 0.4255, 0.3711, 0.3642, 0.3165, 0.3092, 0.2681, 0.2593, 0.2251, 0.2143, 0.1887, 0.1762, 0.1488, 0.1274 }, // BKZ-32, p=0.234024 32 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9523, 0.9523, 0.8683, 0.8683, 0.7915, 0.7915, 0.7073, 0.7073, 0.6227, 0.6227, 0.5357, 0.5357, 0.4749, 0.4749, 0.4107, 0.4107, 0.3527, 0.3527, 0.2998, 0.2998, 0.2515, 0.2515, 0.2078, 0.2078, 0.1729, 0.1729, 0.1705, 0.1705 }, // BKZ-34, p=0.249459 33 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9947, 0.9903, 0.9493, 0.9453, 0.8848, 0.8809, 0.8000, 0.7977, 0.7174, 0.7143, 0.6389, 0.6350, 0.5656, 0.5616, 0.4991, 0.4951, 0.4396, 0.4354, 0.3864, 0.3816, 0.3387, 0.3326, 0.2962, 0.2885, 0.2584, 0.2490, 0.2270, 0.2173, 0.1947, 0.1880, 0.1598, 0.1470 }, // BKZ-36, p=0.242894 34 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9935, 0.9918, 0.9622, 0.9563, 0.8949, 0.8921, 0.8212, 0.8186, 0.7432, 0.7409, 0.6665, 0.6648, 0.5948, 0.5934, 0.5292, 0.5277, 0.4693, 0.4675, 0.4146, 0.4121, 0.3662, 0.3631, 0.3226, 0.3188, 0.2837, 0.2787, 0.2491, 0.2424, 0.2202, 0.2119, 0.1918, 0.1902, 0.1593, 0.1591 }, // BKZ-38, p=0.241266 35 | { 1.0000, 1.0000, 1.0000, 1.0000, 0.9945, 0.9945, 0.9671, 0.9615, 0.9091, 0.9065, 0.8403, 0.8378, 0.7662, 0.7640, 0.6926, 0.6910, 0.6233, 0.6218, 0.5592, 0.5576, 0.5001, 0.4983, 0.4462, 0.4440, 0.3977, 0.3951, 0.3541, 0.3509, 0.3148, 0.3106, 0.2798, 0.2741, 0.2488, 0.2410, 0.2149, 0.2139, 0.1997, 0.1970, 0.1874, 0.1874 }, // BKZ-40, p=0.249724 36 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9999, 0.9709, 0.9673, 0.9223, 0.9187, 0.8560, 0.8536, 0.7858, 0.7829, 0.7162, 0.7126, 0.6490, 0.6453, 0.5860, 0.5823, 0.5279, 0.5242, 0.4747, 0.4710, 0.4261, 0.4223, 0.3822, 0.3780, 0.3423, 0.3375, 0.3061, 0.3002, 0.2731, 0.2662, 0.2408, 0.2387, 0.2174, 0.2088, 0.2057, 0.2056, 0.2055, 0.2054 }, // BKZ-42, p=0.249948 37 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9775, 0.9750, 0.9315, 0.9279, 0.8708, 0.8650, 0.8049, 0.7974, 0.7378, 0.7299, 0.6727, 0.6648, 0.6110, 0.6035, 0.5535, 0.5464, 0.5007, 0.4939, 0.4524, 0.4459, 0.4082, 0.4017, 0.3682, 0.3618, 0.3318, 0.3248, 0.2984, 0.2907, 0.2696, 0.2696, 0.2378, 0.2366, 0.2156, 0.2151, 0.2145, 0.2145, 0.2145, 0.2145 }, // BKZ-44, p=0.248877 38 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9818, 0.9793, 0.9356, 0.9356, 0.8842, 0.8769, 0.8216, 0.8130, 0.7573, 0.7483, 0.6941, 0.6854, 0.6337, 0.6255, 0.5772, 0.5696, 0.5250, 0.5179, 0.4771, 0.4706, 0.4329, 0.4268, 0.3929, 0.3869, 0.3561, 0.3499, 0.3224, 0.3158, 0.2917, 0.2845, 0.2652, 0.2581, 0.2407, 0.2398, 0.2254, 0.2253, 0.2253, 0.2253, 0.2253, 0.2253 }, // BKZ-46, p=0.249954 39 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9896, 0.9840, 0.9498, 0.9429, 0.8881, 0.8881, 0.8386, 0.8260, 0.7763, 0.7634, 0.7152, 0.7026, 0.6567, 0.6446, 0.6015, 0.5900, 0.5500, 0.5391, 0.5025, 0.4925, 0.4594, 0.4508, 0.4159, 0.4072, 0.3798, 0.3719, 0.3459, 0.3384, 0.3146, 0.3086, 0.2858, 0.2760, 0.2582, 0.2505, 0.2479, 0.2479, 0.2479, 0.2479, 0.2479, 0.2479, 0.2479, 0.2479 }, // BKZ-48, p=0.249836 40 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9996, 0.9683, 0.9650, 0.9234, 0.9034, 0.8593, 0.8418, 0.7963, 0.7813, 0.7358, 0.7221, 0.6781, 0.6648, 0.6227, 0.6092, 0.5692, 0.5564, 0.5212, 0.5133, 0.4835, 0.4750, 0.4391, 0.4300, 0.4044, 0.3950, 0.3679, 0.3594, 0.3351, 0.3271, 0.3017, 0.2909, 0.2743, 0.2739, 0.2602, 0.2591, 0.2587, 0.2583, 0.2579, 0.2575, 0.2571, 0.2566, 0.2562, 0.2558 }, // BKZ-50, p=0.249867 41 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9899, 0.9898, 0.9588, 0.9559, 0.9138, 0.9106, 0.8600, 0.8567, 0.8020, 0.7987, 0.7448, 0.7416, 0.6872, 0.6843, 0.6313, 0.6313, 0.5822, 0.5799, 0.5343, 0.5325, 0.4909, 0.4893, 0.4512, 0.4497, 0.4134, 0.4123, 0.3800, 0.3792, 0.3497, 0.3488, 0.3232, 0.3209, 0.2992, 0.2949, 0.2772, 0.2712, 0.2510, 0.2482, 0.2417, 0.2417, 0.2417, 0.2417, 0.2417, 0.2417, 0.2417, 0.2417 }, // BKZ-52, p=0.234962 42 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9917, 0.9892, 0.9645, 0.9619, 0.9232, 0.9202, 0.8722, 0.8692, 0.8178, 0.8145, 0.7619, 0.7587, 0.7069, 0.7038, 0.6537, 0.6509, 0.6029, 0.6004, 0.5554, 0.5554, 0.5112, 0.5096, 0.4718, 0.4704, 0.4353, 0.4340, 0.4002, 0.3991, 0.3694, 0.3682, 0.3415, 0.3394, 0.3165, 0.3130, 0.2939, 0.2888, 0.2692, 0.2667, 0.2504, 0.2501, 0.2401, 0.2401, 0.2401, 0.2401, 0.2401, 0.2401, 0.2401, 0.2401 }, // BKZ-54, p=0.237040 43 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9948, 0.9946, 0.9672, 0.9672, 0.9312, 0.9286, 0.8836, 0.8809, 0.8315, 0.8289, 0.7780, 0.7753, 0.7248, 0.7220, 0.6731, 0.6703, 0.6239, 0.6212, 0.5766, 0.5743, 0.5333, 0.5314, 0.4917, 0.4902, 0.4554, 0.4547, 0.4206, 0.4193, 0.3894, 0.3882, 0.3607, 0.3590, 0.3347, 0.3321, 0.3111, 0.3074, 0.2874, 0.2858, 0.2707, 0.2645, 0.2503, 0.2501, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500 }, // BKZ-56, p=0.241267 44 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9947, 0.9947, 0.9742, 0.9716, 0.9389, 0.9361, 0.8942, 0.8914, 0.8446, 0.8420, 0.7931, 0.7906, 0.7416, 0.7391, 0.6911, 0.6886, 0.6423, 0.6401, 0.5966, 0.5941, 0.5532, 0.5520, 0.5124, 0.5106, 0.4757, 0.4750, 0.4407, 0.4397, 0.4084, 0.4073, 0.3797, 0.3783, 0.3531, 0.3510, 0.3288, 0.3258, 0.3067, 0.3026, 0.2839, 0.2822, 0.2660, 0.2654, 0.2514, 0.2513, 0.2512, 0.2512, 0.2512, 0.2512, 0.2512, 0.2512, 0.2512, 0.2512 }, // BKZ-58, p=0.245958 45 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9962, 0.9962, 0.9779, 0.9752, 0.9458, 0.9426, 0.9039, 0.9008, 0.8569, 0.8541, 0.8072, 0.8046, 0.7573, 0.7554, 0.7082, 0.7056, 0.6603, 0.6585, 0.6151, 0.6133, 0.5717, 0.5697, 0.5314, 0.5313, 0.4956, 0.4942, 0.4589, 0.4577, 0.4285, 0.4274, 0.3995, 0.3970, 0.3709, 0.3693, 0.3459, 0.3435, 0.3225, 0.3194, 0.3010, 0.2970, 0.2803, 0.2758, 0.2681, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679, 0.2679 }, // BKZ-60, p=0.249855 46 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9993, 0.9989, 0.9810, 0.9774, 0.9519, 0.9471, 0.9129, 0.9078, 0.8678, 0.8632, 0.8198, 0.8159, 0.7710, 0.7678, 0.7229, 0.7201, 0.6761, 0.6738, 0.6314, 0.6294, 0.5891, 0.5872, 0.5491, 0.5475, 0.5118, 0.5103, 0.4773, 0.4759, 0.4450, 0.4436, 0.4152, 0.4138, 0.3875, 0.3860, 0.3619, 0.3602, 0.3383, 0.3362, 0.3168, 0.3141, 0.2974, 0.2936, 0.2854, 0.2854, 0.2652, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648, 0.2648 }, // BKZ-62, p=0.249548 47 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9996, 0.9837, 0.9794, 0.9576, 0.9510, 0.9212, 0.9139, 0.8782, 0.8714, 0.8319, 0.8261, 0.7844, 0.7797, 0.7372, 0.7335, 0.6913, 0.6884, 0.6472, 0.6449, 0.6054, 0.6035, 0.5659, 0.5644, 0.5283, 0.5266, 0.4948, 0.4936, 0.4617, 0.4601, 0.4323, 0.4307, 0.4038, 0.4022, 0.3776, 0.3758, 0.3534, 0.3513, 0.3315, 0.3291, 0.3125, 0.3109, 0.2983, 0.2895, 0.2823, 0.2823, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702, 0.2702 }, // BKZ-64, p=0.249417 48 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9998, 0.9848, 0.9800, 0.9634, 0.9545, 0.9315, 0.9206, 0.8903, 0.8795, 0.8451, 0.8355, 0.7987, 0.7903, 0.7524, 0.7452, 0.7072, 0.7010, 0.6635, 0.6583, 0.6218, 0.6174, 0.5824, 0.5785, 0.5454, 0.5419, 0.5108, 0.5077, 0.4785, 0.4756, 0.4478, 0.4446, 0.4201, 0.4169, 0.3942, 0.3910, 0.3703, 0.3667, 0.3482, 0.3442, 0.3333, 0.3333, 0.3098, 0.3049, 0.2919, 0.2911, 0.2785, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782, 0.2782 }, // BKZ-66, p=0.249798 49 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9907, 0.9902, 0.9800, 0.9800, 0.9359, 0.9308, 0.9035, 0.8904, 0.8571, 0.8467, 0.8113, 0.8026, 0.7660, 0.7586, 0.7216, 0.7154, 0.6786, 0.6734, 0.6375, 0.6330, 0.5984, 0.5945, 0.5615, 0.5582, 0.5270, 0.5240, 0.4946, 0.4919, 0.4637, 0.4608, 0.4359, 0.4329, 0.4096, 0.4067, 0.3854, 0.3822, 0.3630, 0.3593, 0.3423, 0.3380, 0.3230, 0.3182, 0.3033, 0.3019, 0.2864, 0.2854, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837, 0.2837 }, // BKZ-68, p=0.249974 50 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9995, 0.9902, 0.9889, 0.9832, 0.9800, 0.9444, 0.9399, 0.9140, 0.8990, 0.8688, 0.8559, 0.8237, 0.8124, 0.7792, 0.7693, 0.7355, 0.7270, 0.6931, 0.6858, 0.6523, 0.6461, 0.6136, 0.6082, 0.5769, 0.5722, 0.5425, 0.5384, 0.5102, 0.5067, 0.4788, 0.4749, 0.4511, 0.4471, 0.4244, 0.4209, 0.3999, 0.3960, 0.3770, 0.3729, 0.3559, 0.3514, 0.3362, 0.3314, 0.3171, 0.3148, 0.3029, 0.3023, 0.2889, 0.2886, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885, 0.2885 }, // BKZ-70, p=0.249247 51 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9913, 0.9881, 0.9805, 0.9800, 0.9800, 0.9800, 0.9268, 0.9161, 0.8853, 0.8688, 0.8364, 0.8240, 0.7914, 0.7814, 0.7484, 0.7399, 0.7068, 0.6995, 0.6666, 0.6603, 0.6282, 0.6228, 0.5917, 0.5870, 0.5573, 0.5531, 0.5249, 0.5212, 0.4947, 0.4916, 0.4651, 0.4613, 0.4392, 0.4355, 0.4142, 0.4107, 0.3912, 0.3875, 0.3698, 0.3657, 0.3500, 0.3454, 0.3316, 0.3265, 0.3119, 0.3092, 0.2958, 0.2946, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929, 0.2929 }, // BKZ-72, p=0.249934 52 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9998, 0.9920, 0.9909, 0.9890, 0.9890, 0.9800, 0.9800, 0.9538, 0.9538, 0.8956, 0.8848, 0.8509, 0.8352, 0.8041, 0.7920, 0.7612, 0.7510, 0.7201, 0.7112, 0.6805, 0.6727, 0.6424, 0.6357, 0.6061, 0.6002, 0.5718, 0.5665, 0.5394, 0.5347, 0.5089, 0.5046, 0.4805, 0.4768, 0.4528, 0.4484, 0.4285, 0.4241, 0.4049, 0.4007, 0.3833, 0.3788, 0.3632, 0.3583, 0.3444, 0.3391, 0.3246, 0.3215, 0.3086, 0.3049, 0.2978, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974, 0.2974 }, // BKZ-74, p=0.249416 53 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9936, 0.9908, 0.9830, 0.9826, 0.9800, 0.9800, 0.9800, 0.9800, 0.9188, 0.9161, 0.8697, 0.8498, 0.8161, 0.8040, 0.7725, 0.7636, 0.7322, 0.7246, 0.6934, 0.6868, 0.6556, 0.6500, 0.6193, 0.6146, 0.5849, 0.5809, 0.5524, 0.5490, 0.5218, 0.5188, 0.4934, 0.4905, 0.4652, 0.4626, 0.4415, 0.4379, 0.4176, 0.4147, 0.3961, 0.3928, 0.3760, 0.3721, 0.3570, 0.3527, 0.3393, 0.3346, 0.3219, 0.3203, 0.3067, 0.3015, 0.2984, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983, 0.2983 }, // BKZ-76, p=0.250040 54 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9934, 0.9913, 0.9877, 0.9877, 0.9854, 0.9854, 0.9800, 0.9800, 0.9484, 0.9484, 0.8803, 0.8798, 0.8334, 0.8206, 0.7854, 0.7795, 0.7445, 0.7410, 0.7062, 0.7036, 0.6683, 0.6665, 0.6315, 0.6305, 0.5964, 0.5959, 0.5633, 0.5633, 0.5324, 0.5302, 0.5037, 0.5036, 0.4758, 0.4722, 0.4512, 0.4512, 0.4287, 0.4284, 0.4075, 0.4073, 0.3886, 0.3875, 0.3702, 0.3679, 0.3521, 0.3492, 0.3350, 0.3317, 0.3179, 0.3159, 0.3048, 0.3037, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031, 0.3031 }, // BKZ-78, p=0.249555 55 | { 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 0.9937, 0.9911, 0.9869, 0.9867, 0.9853, 0.9853, 0.9800, 0.9800, 0.9623, 0.9623, 0.8995, 0.8926, 0.8496, 0.8281, 0.7976, 0.7852, 0.7558, 0.7466, 0.7176, 0.7097, 0.6809, 0.6740, 0.6455, 0.6396, 0.6117, 0.6066, 0.5795, 0.5750, 0.5492, 0.5455, 0.5207, 0.5167, 0.4933, 0.4909, 0.4669, 0.4632, 0.4438, 0.4400, 0.4210, 0.4177, 0.4001, 0.3966, 0.3806, 0.3768, 0.3624, 0.3582, 0.3454, 0.3409, 0.3297, 0.3249, 0.3153, 0.3093, 0.3064, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063, 0.3063 }, // BKZ-80, p=0.249058 56 | }; 57 | 58 | const FT* get_pruning_coefficients(const int block_size) { 59 | assert(2 <= block_size && block_size <= MAX_ENUM_N); 60 | return &pruning_params[(block_size + 1) / 2][0]; 61 | } 62 | -------------------------------------------------------------------------------- /core/types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CORE_TYPES_HPP 2 | #define CORE_TYPES_HPP 3 | 4 | 5 | constexpr int MAX_ENUM_N = 80; // See enumeration.cpp:16 6 | 7 | // floating-point type 8 | typedef double FT; 9 | 10 | // integer type 11 | typedef long long ZZ; 12 | 13 | 14 | #endif // CORE_TYPES_HPP 15 | -------------------------------------------------------------------------------- /find_pruning_params.py: -------------------------------------------------------------------------------- 1 | from math import pi, log, exp 2 | from fpylll import Pruning, util 3 | from scipy.special import loggamma 4 | 5 | 6 | proba_goal = .25 7 | overhead = 2 8 | 9 | 10 | def slope(n): 11 | if n < 40: 12 | return -log(2.0**(-0.040)) 13 | lV = log(pi)*(n/2) - loggamma(1+n/2) 14 | return -(lV + log(proba_goal/2)) * (2/n) * (1. / (n - 1)) 15 | 16 | 17 | for beta in range(2, 82, 2): 18 | sl = slope(beta - 2) 19 | profile = [exp(- sl * 2 * i) for i in range(beta)] 20 | rad = 1.11 * util.gaussian_heuristic(profile) 21 | while True: 22 | pr = Pruning.run(rad, 2.0**overhead, [profile], proba_goal, flags=Pruning.ZEALOUS) 23 | if pr.expectation > proba_goal / 1.1: 24 | break 25 | overhead += 1 26 | 27 | coeffs = ", ".join([f"{x:.4f}" for x in pr.coefficients]) 28 | print(f"{{{coeffs}}}, // BKZ-{beta}, p={pr.expectation:.6f}") 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from sys import argv, platform 3 | from setuptools import Extension, setup 4 | from Cython.Build import cythonize 5 | 6 | import numpy as np 7 | 8 | 9 | # Make sure OpenMP is used in Cython and Eigen. 10 | openmp_arg = '/openmp' if platform.startswith("win") else '-fopenmp' 11 | include_dirs = [np.get_include()] 12 | 13 | # Look for the Eigen library in `/usr/include` and `~/.local/include`. 14 | for f in [Path('eigen3'), Path('/usr/include/eigen3'), Path.home().joinpath('.local/include/eigen3')]: 15 | if f.exists() and f.is_dir(): 16 | include_dirs += [str(f)] 17 | break 18 | else: 19 | print("ERROR: Eigen3 library is required!") 20 | print("NOTE : Please run 'make eigen3'") 21 | exit(1) 22 | 23 | # Compile with extra arguments 24 | compile_args = [ 25 | '--std=c++17', 26 | '-DNPY_NO_DEPRECATED_API=NPY_1_9_API_VERSION', 27 | openmp_arg, 28 | ] 29 | 30 | # Link with extra arguments 31 | link_args = [openmp_arg] 32 | 33 | if '--cython-gdb' in argv: 34 | # Debug arguments 35 | debug_args = [ 36 | '-O1', 37 | '-fsanitize=address,undefined', 38 | '-g', 39 | '-fno-omit-frame-pointer', 40 | ] 41 | compile_args += debug_args 42 | link_args += debug_args 43 | else: 44 | # "Release" arguments 45 | compile_args += [ 46 | '-O3', 47 | '-march=native', 48 | '-DEIGEN_NO_DEBUG', 49 | ] 50 | 51 | extensions = [Extension( 52 | name="blaster_core", 53 | sources=["core/blaster.pyx"], 54 | include_dirs=include_dirs, 55 | extra_compile_args=compile_args, 56 | extra_link_args=link_args 57 | )] 58 | 59 | setup( 60 | ext_modules=cythonize(extensions, language_level="3", build_dir='build/cpp'), 61 | options={'build': {'build_lib': 'src/'}}, 62 | ) 63 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script for running BLASter lattice reduction from the command line 4 | """ 5 | 6 | import argparse 7 | from multiprocessing import cpu_count 8 | from sys import stderr 9 | from math import log2, ceil 10 | 11 | import numpy as np 12 | 13 | # Local imports 14 | from lattice_io import read_qary_lattice, write_lattice 15 | from blaster import reduce 16 | from stats import gaussian_heuristic, rhf, slope, get_profile 17 | 18 | 19 | def __main__(): 20 | parser = argparse.ArgumentParser( 21 | prog='BLASter', 22 | description='LLL-reduce a lattice using a fast, modern implementation', 23 | epilog='Input/output is formatted as is done in fpLLL') 24 | 25 | # Global settings 26 | parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') 27 | parser.add_argument( 28 | '--cores', '-j', type=int, default=cpu_count() // 2, 29 | help='number of cores to be used') 30 | 31 | # I/O arguments 32 | parser.add_argument('--input', '-i', type=str, help='Input file (default=stdin)') 33 | parser.add_argument('--output', '-o', type=str, help='Output file (default=stdout)') 34 | parser.add_argument('--logfile', '-l', type=str, default=None, help='Logging file') 35 | parser.add_argument( 36 | '--profile', '-p', action='store_true', dest='debug', 37 | help='Give information on the profile of the output basis') 38 | parser.add_argument( 39 | '--quiet', '-q', action='store_true', 40 | help='Quiet mode will not output the output basis') 41 | parser.add_argument( 42 | '--anim', '-a', type=str, 43 | help='Output a gif-file animating the basis profile during lattice reduction') 44 | 45 | # LLL parameters 46 | parser.add_argument( 47 | '--delta', type=float, default=0.99, 48 | help='delta factor for Lovasz condition') 49 | parser.add_argument( 50 | '--lll_size', '-L', type=int, default=64, 51 | help='Size of blocks on which to call LLL/deep-LLL locally & in parallel') 52 | parser.add_argument( 53 | '--no-seysen', '-s', action='store_false', dest='use_seysen', 54 | help='Use size reduction if argument is given. Otherwise use Seysen\'s reduction.') 55 | 56 | # Parameters specific to deep-LLL: 57 | parser.add_argument( 58 | '--depth', '-d', type=int, default=0, 59 | help='Maximum allowed depth for "deep insertions" in deep-LLL. 0 if not desired.') 60 | 61 | # Parameters specific to BKZ: 62 | parser.add_argument( 63 | '--beta', '-b', type=int, 64 | help='Blocksize used within BKZ. 0 if not desired.') 65 | parser.add_argument( 66 | '--bkz-tours', '-t', type=int, default=8, 67 | help='Number of BKZ-tours to perform.') 68 | parser.add_argument( 69 | '--bkz-size', '-S', type=int, default=64, 70 | help='Size of blocks on which to call BKZ locally & in parallel.') 71 | parser.add_argument( 72 | '--bkz-prog', '-P', type=int, 73 | help='Progressive blocksize increment for BKZ.') 74 | 75 | # Parse the command line arguments 76 | args = parser.parse_args() 77 | 78 | # Perform sanity checks 79 | assert 0.25 < args.delta and args.delta < 1.0, 'Invalid value for delta!' 80 | assert args.lll_size >= 2, 'LLL block size must be at least 2!' 81 | assert not args.depth or not args.beta, 'Cannot run combination of deep-LLL and BKZ!' 82 | 83 | # Read the basis from input (file) 84 | B = read_qary_lattice(args.input) 85 | n = B.shape[1] # rank of basis 86 | 87 | if args.verbose: 88 | # Experimentally, LLL gives a RHF of 1.02190 89 | # See: https://github.com/malb/lattice-estimator/blob/main/estimator/reduction.py 90 | # TODO: adjust slope to the prediction for deep-LLL and BKZ. 91 | log_slope = log2(1.02190) # Slope of graph of basis profile, log2(||b_i*||^2). 92 | log_det = sum(get_profile(B)) 93 | norm_b1 = 2.0**(log_slope * (n-1) + log_det / n) 94 | 95 | comparison = "" 96 | if np.count_nonzero(B[:, 0]) == 1: 97 | q = sum(B[:, 0]) 98 | cmp = "<" if norm_b1 < q else ">=" 99 | comparison = f'{cmp} {int(q):d} ' 100 | print(f'E[∥b₁∥] ~ {norm_b1:.2f} {comparison}' 101 | f'(GH: λ₁ ~ {gaussian_heuristic(B):.2f})', 102 | file=stderr) 103 | 104 | # Multithreading is used in two places: 105 | # 1) Matrix multiplication (controlled by Eigen) 106 | # 2) Lattice reduction in `core/blaster.pyx` (done using Cython's `prange`, which uses OPENMP) 107 | # Notes: using more cores creates more overhead, so use cores wisely! 108 | # Re 1): Starting around dimension >500, there is a performance gain using multiple threads 109 | # Re 2): The program cannot use more cores in lattice reduction 110 | # than the number of blocks, so do not spawn more than this number. 111 | args.cores = max(1, min(args.cores, ceil(n / args.lll_size), cpu_count() // 2)) 112 | 113 | # Run BLASter lattice reduction on basis B 114 | U, B_red, tprof = reduce(B, **vars(args)) 115 | 116 | # Write B_red to the output file 117 | print_mat = args.output is not None 118 | if print_mat and args.output == args.input: 119 | print_mat = input('WARNING: input & output files are same!\nContinue? (y/n) ') == 'y' 120 | if print_mat: 121 | write_lattice(args.output, B_red) 122 | elif not args.quiet: 123 | print(B_red.astype(np.int64)) 124 | 125 | # Print time consumption 126 | if args.verbose: 127 | print('\n', str(tprof), sep="", file=stderr) 128 | 129 | # Print basis profile 130 | if args.debug: 131 | prof = get_profile(B_red) 132 | print('\nProfile = [' + ' '.join([f'{x:.2f}' for x in prof]) + ']\n' 133 | f'RHF = {rhf(prof):.5f}^n, slope = {slope(prof):.6f}, ' 134 | f'∥b_1∥ = {2.0**prof[0]:.1f}', file=stderr) 135 | 136 | # Assert that applying U on the basis B indeed gives the reduced basis B_red. 137 | assert (B @ U == B_red).all() 138 | 139 | 140 | if __name__ == '__main__': 141 | np.set_printoptions(linewidth=1000, threshold=2147483647, suppress=True) 142 | __main__() 143 | -------------------------------------------------------------------------------- /src/blaster.py: -------------------------------------------------------------------------------- 1 | """ 2 | BLASter lattice reduction: LLL with QR decomposition, Seysen's reduction, and 3 | segments, in which lattice reduction is done in parallel. 4 | """ 5 | from functools import partial 6 | from sys import stderr 7 | from time import perf_counter_ns 8 | 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | from matplotlib.animation import ArtistAnimation, PillowWriter 12 | 13 | # Local imports 14 | from blaster_core import \ 15 | set_debug_flag, set_num_cores, block_lll, block_deep_lll, block_bkz, ZZ_right_matmul 16 | from size_reduction import is_lll_reduced, is_weakly_lll_reduced, size_reduce, seysen_reduce 17 | from stats import get_profile, rhf, slope, potential 18 | 19 | 20 | class TimeProfile: 21 | """ 22 | Object containing time spent on different parts when running BLASter. 23 | """ 24 | 25 | def __init__(self, use_seysen: bool = False): 26 | self._strs = [ 27 | "QR-decomp.", "LLL-red.", "BKZ-red.", 28 | "Seysen-red." if use_seysen else "Size-red. ", "Matrix-mul." 29 | ] 30 | self.num_iterations = 0 31 | self.times = [0] * 5 32 | 33 | def tick(self, *times): 34 | self.num_iterations += 1 35 | self.times = [x + y for x, y in zip(self.times, times)] 36 | 37 | def __str__(self): 38 | return ( 39 | f"Iterations: {self.num_iterations}\n" + 40 | "\n".join(f"t_{{{s:11}}}={t/10**9:10.3f}s" for s, t in zip(self._strs, self.times) if t) 41 | ) 42 | 43 | 44 | def lll_reduce(B, U, U_seysen, lll_size, delta, depth, 45 | tprof, tracers, debug, use_seysen): 46 | """ 47 | Perform BLASter's lattice reduction on basis B, and keep track of the transformation in U. 48 | If `depth` is supplied, use deep insertions up to depth `depth`. 49 | """ 50 | n, is_reduced, offset = B.shape[1], False, 0 51 | red_fn = partial(block_deep_lll, depth) if depth else block_lll 52 | 53 | # Keep running until the basis is LLL reduced. 54 | while not is_reduced: 55 | # Step 1: QR-decompose B, and only store the upper-triangular matrix R. 56 | t1 = perf_counter_ns() 57 | R = np.linalg.qr(B, mode='r') 58 | 59 | # Step 2: Call LLL concurrently on small blocks. 60 | t2 = perf_counter_ns() 61 | offset = lll_size // 2 if offset == 0 else 0 62 | red_fn(R, B, U, delta, offset, lll_size) # LLL or deep-LLL 63 | 64 | if debug: 65 | for i in range(offset, n, lll_size): 66 | j = min(n, i + lll_size) 67 | # Check whether R_[i:j) is really LLL-reduced. 68 | assert is_lll_reduced(R[i:j, i:j], delta) 69 | 70 | # Step 3: QR-decompose again because LLL "destroys" the QR decomposition. 71 | # Note: it does not destroy the bxb blocks, but everything above these: yes! 72 | t3 = perf_counter_ns() 73 | R = np.linalg.qr(B, mode='r') 74 | 75 | # Step 4: Seysen reduce or size reduce the upper-triangular matrix R. 76 | t4 = perf_counter_ns() 77 | with np.errstate(all='raise'): 78 | (seysen_reduce if use_seysen else size_reduce)(R, U_seysen) 79 | 80 | # Step 5: Update B and U with transformation from Seysen's reduction. 81 | t5 = perf_counter_ns() 82 | ZZ_right_matmul(U, U_seysen) 83 | ZZ_right_matmul(B, U_seysen) 84 | 85 | # Step 6: Check whether the basis is weakly-LLL reduced. 86 | t6 = perf_counter_ns() 87 | 88 | is_reduced = is_weakly_lll_reduced(R, delta) 89 | tprof.tick(t2 - t1 + t4 - t3, t3 - t2, 0, t5 - t4, t6 - t5) 90 | 91 | # After time measurement: 92 | prof = get_profile(R, True) # Seysen did not modify the diagonal of R 93 | note = (f"DeepLLL-{depth}" if depth else "LLL", None) 94 | for tracer in tracers.values(): 95 | tracer(tprof.num_iterations, prof, note) 96 | 97 | 98 | def bkz_reduce(B, U, U_seysen, lll_size, delta, depth, 99 | beta, bkz_tours, bkz_size, tprof, tracers, debug, use_seysen): 100 | """ 101 | Perform BLASter's BKZ reduction on basis B, and keep track of the transformation in U. 102 | If `depth` is supplied, BLASter's deep-LLL is called in between calls of the SVP oracle. 103 | Otherwise BLASter's LLL is run. 104 | """ 105 | # BKZ parameters: 106 | n, tours_done, cur_front = B.shape[1], 0, 0 107 | 108 | lll_reduce(B, U, U_seysen, lll_size, delta, depth, tprof, tracers, debug, use_seysen) 109 | 110 | while tours_done < bkz_tours: 111 | # Step 1: QR-decompose B, and only store the upper-triangular matrix R. 112 | t1 = perf_counter_ns() 113 | R = np.linalg.qr(B, mode='r') 114 | 115 | # Step 2: Call BKZ concurrently on small blocks! 116 | t2 = perf_counter_ns() 117 | # norm_before = abs(R[cur_front, cur_front]) 118 | block_bkz(beta, R, B, U, delta, cur_front % beta, bkz_size) 119 | 120 | # Step 3: QR-decompose again because BKZ "destroys" the QR decomposition. 121 | # Note: it does not destroy the bxb blocks, but everything above these: yes! 122 | t3 = perf_counter_ns() 123 | R = np.linalg.qr(B, mode='r') 124 | # assert abs(R[cur_front, cur_front]) <= norm_before 125 | 126 | # Step 4: Seysen reduce or size reduce the upper-triangular matrix R. 127 | t4 = perf_counter_ns() 128 | with np.errstate(all='raise'): 129 | (seysen_reduce if use_seysen else size_reduce)(R, U_seysen) 130 | 131 | # Step 5: Update B and U with transformation from Seysen's reduction. 132 | t5 = perf_counter_ns() 133 | ZZ_right_matmul(U, U_seysen) 134 | ZZ_right_matmul(B, U_seysen) 135 | 136 | t6 = perf_counter_ns() 137 | 138 | tprof.tick(t2 - t1 + t4 - t3, 0, t3 - t2, t5 - t4, t6 - t5) 139 | 140 | # After time measurement: 141 | prof = get_profile(R, True) # Seysen did not modify the diagonal of R 142 | note = (f"BKZ-{beta}", (beta, tours_done, bkz_tours, cur_front)) 143 | for tracer in tracers.values(): 144 | tracer(tprof.num_iterations, prof, note) 145 | 146 | # After printing: update the current location of the 'reduction front' 147 | if cur_front + beta > n: 148 | # HKZ-reduction was performed at the end, which is the end of a tour. 149 | cur_front = 0 150 | tours_done += 1 151 | else: 152 | cur_front += (bkz_size - beta + 1) 153 | 154 | # Perform a final LLL reduction at the end 155 | lll_reduce(B, U, U_seysen, lll_size, delta, depth, tprof, tracers, debug, use_seysen) 156 | 157 | 158 | def reduce( 159 | B, lll_size: int = 64, delta: float = 0.99, cores: int = 1, debug: bool = False, 160 | verbose: bool = False, logfile: str = None, anim: str = None, depth: int = 0, 161 | use_seysen: bool = False, 162 | **kwds 163 | ): 164 | """ 165 | :param B: a basis, consisting of *column vectors*. 166 | :param delta: delta factor for Lagrange reduction, 167 | :param cores: number of cores to use, and 168 | :param lll_size: the block-size for LLL, and 169 | :param debug: whether or not to debug and print more output on time consumption. 170 | :param kwds: additional arguments (for BKZ reduction). 171 | 172 | :return: tuple (U, B · U, tprof) where: 173 | U: the transformation matrix such that B · U is LLL reduced, 174 | B · U: an LLL-reduced basis, 175 | tprof: TimeProfile object. 176 | """ 177 | n, tprof = B.shape[1], TimeProfile(use_seysen) 178 | lll_size = min(max(2, lll_size), n) 179 | 180 | set_num_cores(cores) 181 | set_debug_flag(1 if debug else 0) 182 | 183 | tracers = {} 184 | if verbose: 185 | def trace_print(_, prof, note): 186 | log_str = '.' 187 | if note[0].startswith('BKZ'): 188 | beta, tour, ntours, touridx = note[1] 189 | log_str = (f"\nBKZ(β:{beta:3d},t:{tour + 1:2d}/{ntours:2d}, o:{touridx:4d}): " 190 | f"slope={slope(prof):.6f}, rhf={rhf(prof):.6f}") 191 | print(log_str, end="", file=stderr, flush=True) 192 | tracers['v'] = trace_print 193 | 194 | # Set up logfile 195 | has_logfile = logfile is not None 196 | if has_logfile: 197 | tstart = perf_counter_ns() 198 | logfile = open(logfile, "w", encoding="utf8") 199 | print('it,walltime,rhf,slope,potential,note', file=logfile, flush=True) 200 | 201 | def trace_logfile(it, prof, note): 202 | walltime = (perf_counter_ns() - tstart) * 10**-9 203 | print(f'{it:4d},{walltime:.6f},{rhf(prof):8.6f},{slope(prof):9.6f},' 204 | f'{potential(prof):9.3f},{note[0]}', file=logfile) 205 | 206 | tracers['l'] = trace_logfile 207 | 208 | # Set up animation 209 | has_animation = anim is not None 210 | if has_animation: 211 | fig, ax = plt.subplots() 212 | ax.set(xlim=[0, n]) 213 | artists = [] 214 | 215 | def trace_anim(_, prof, __): 216 | artists.append(ax.plot(range(n), prof, color="blue")) 217 | 218 | tracers['a'] = trace_anim 219 | 220 | B = B.copy() # Do not modify B in-place, but work with a copy. 221 | U = np.identity(n, dtype=np.int64) 222 | U_seysen = np.identity(n, dtype=np.int64) 223 | 224 | beta = kwds.get("beta") 225 | try: 226 | if not beta: 227 | lll_reduce(B, U, U_seysen, lll_size, delta, depth, tprof, tracers, debug, use_seysen) 228 | else: 229 | # Parse BKZ parameters: 230 | bkz_tours = kwds.get("bkz_tours") or 1 231 | bkz_size = kwds.get("bkz_size") or lll_size 232 | bkz_prog = kwds.get("bkz_prog") or beta 233 | 234 | # Progressive-BKZ: start running BKZ-beta' for some `beta' >= 40`, 235 | # then increase the blocksize beta' by `bkz_prog` and run BKZ-beta' again, 236 | # and repeat this until `beta' = beta`. 237 | betas = range(40 + ((beta - 40) % bkz_prog), beta + 1, bkz_prog) 238 | 239 | # In the literature on BKZ, it is usual to run LLL before calling the SVP oracle in BKZ. 240 | # However, it is actually better to preprocess the basis with 4-deep-LLL instead of LLL, 241 | # before calling the SVP oracle. 242 | for beta_ in betas: 243 | bkz_reduce(B, U, U_seysen, lll_size, delta, 4, beta_, 244 | bkz_tours if beta_ == beta else 1, bkz_size, 245 | tprof, tracers, debug, use_seysen) 246 | except KeyboardInterrupt: 247 | pass # When interrupted, give the partially reduced basis. 248 | 249 | # Close logfile 250 | if has_logfile: 251 | logfile.close() 252 | 253 | # Save and/or show the animation 254 | if has_animation: 255 | # Saving the animation takes a LONG time. 256 | if verbose: 257 | print('\nOutputting animation...', file=stderr) 258 | fig.tight_layout() 259 | ani = ArtistAnimation(fig=fig, artists=artists, interval=200) 260 | # Generate 1920x1080 image: 261 | plt.gcf().set_size_inches(16, 9) 262 | # plt.show() 263 | ani.save(anim, dpi=120, writer=PillowWriter(fps=5)) 264 | 265 | return U, B, tprof 266 | -------------------------------------------------------------------------------- /src/lattice_io.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read/write a matrix in fplll format, to/from numpy array. 3 | """ 4 | import numpy as np 5 | 6 | 7 | def read_qary_lattice(input_file=None): 8 | """ 9 | Read a matrix from a file, or from stdin. 10 | :param input_file: file name, or when None, read from stdin. 11 | :return: a matrix consisting of column vectors. 12 | """ 13 | data = [] 14 | if input_file is None: 15 | data.append(input()) 16 | while data[-1][-2] != ']': 17 | data.append(input()) 18 | else: 19 | with open(input_file, 'r', encoding='utf-8') as f: 20 | data.append(f.readline()[:-1]) 21 | while data[-1] != ']' and data[-1][-2] != ']': 22 | data.append(f.readline()[:-1]) 23 | 24 | # Strip away starting '[' and ending ']' 25 | assert data[0][0] == '[' and data[-1][-1] == ']' 26 | data[0] = data[0][1:] 27 | if data[-1] == ']': 28 | # Flatter and fpLLL output ']' on a separate line instead of '[]]' 29 | data.pop() 30 | else: 31 | data[-1] = data[-1][:-1] 32 | 33 | # Convert data to list of integers 34 | data = [list(map(int, line[1:-1].strip().split(' '))) for line in data] 35 | 36 | if np.count_nonzero(data[-1]) == 1: 37 | # The q-ary vectors are at the back, so reverse the basis vectors in place. 38 | data.reverse() 39 | 40 | # Use column vectors. 41 | return np.ascontiguousarray(np.array(data, dtype=np.int64).transpose()) 42 | 43 | 44 | def write_lattice(output_file, basis): 45 | """ 46 | Outputs a basis with column vectors to a file in fplll format. 47 | :param output_file: file name 48 | :param basis: the matrix to output 49 | """ 50 | basis = basis.transpose() 51 | 52 | with open(output_file, 'w', encoding='utf-8') as f: 53 | f.write('[') 54 | for (i, v) in enumerate(basis): 55 | f.write('[' + ' '.join(map(str, v)) + (']\n' if i < len(basis) - 1 else ']]\n')) 56 | -------------------------------------------------------------------------------- /src/size_reduction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for calling Babai's nearest plane algorithm, size-reducing a basis or 3 | Seysen-reducing a basis. 4 | 5 | In comments, the old recursive functions are kept for clarity. 6 | """ 7 | from functools import cache 8 | import numpy as np 9 | 10 | # Local imports 11 | from blaster_core import ZZ_left_matmul_strided, FT_matmul 12 | 13 | 14 | # Reduction properties: 15 | 16 | 17 | def is_weakly_lll_reduced(R, delta=.99): 18 | """ 19 | Return whether R is Weakly-LLL-reduced 20 | :param R: upper-triangular matrix 21 | :param delta: delta-factor used in the Lovasz condition 22 | :return: bool 23 | """ 24 | n = len(R) 25 | for pos in range(0, n - 1): 26 | # vectors are b0 = (u, 0), b1 = (v, w). 27 | u = abs(R[pos, pos]) 28 | v, w = R[pos, pos + 1], R[pos + 1, pos + 1] 29 | v_mod = ((v + u/2) % u) - u/2 30 | 31 | if v_mod**2 + w**2 <= delta * u**2: 32 | return False # ||b1||^2 <= delta ||b0||^2 33 | return True 34 | 35 | 36 | def is_size_reduced(R): 37 | """ 38 | Return whether R is size-reduced. 39 | :param R: upper-triangular matrix 40 | :return: bool 41 | """ 42 | return all(max(abs(R[i, i + 1:])) <= abs(R[i, i]) / 2 for i in range(len(R) - 1)) 43 | 44 | 45 | def is_lll_reduced(R, delta=.99): 46 | """ 47 | Return whether R is LLL-reduced (weakly-LLL & size-reduced) 48 | :param R: upper-triangular matrix 49 | :param delta: delta-factor used in the Lovasz condition 50 | :return: bool 51 | """ 52 | return is_weakly_lll_reduced(R, delta) and is_size_reduced(R) 53 | 54 | 55 | @cache 56 | def __reduction_ranges(n): 57 | """ 58 | Return list of ranges that needs to be reduced. 59 | 60 | More generally, it returns, without using recursion, the list that would be 61 | the output of the following Python program: 62 | 63 | <<>> 64 | def rec_range(n): 65 | bc, res = [], [] 66 | def F(l, r): 67 | if l == r: 68 | return 69 | if l + 1 == r: 70 | bc.append(l) 71 | else: 72 | m = (l + r) // 2 73 | F(l, m) 74 | F(m, r) 75 | res.append((l, m, r)) 76 | return F(0, n) 77 | <<>> 78 | 79 | :param n: the length of the array that requires reduction 80 | :return: pair containing `the base_cases` and `result`. 81 | `base_cases` is a list of indices `i` such that: 82 | `i + 1` needs to be reduced w.r.t. `i`. 83 | `result` is a list of triples `(i, j, k)` such that: 84 | `[j:k)` needs to be reduced w.r.t. `[i:j)`. 85 | The guarantee is that for any 0 <= i < j < n: 86 | 1) `i in base_cases && j = i + 1`, 87 | OR 88 | 2) there is a triple (u, v, w) such that `i in [u, v)` and `j in [v, w)`. 89 | """ 90 | bit_shift, parts, result, base_cases = 1, 1, [], [] 91 | while parts < n: 92 | left_bound, left_idx = 0, 0 93 | for i in range(1, parts + 1): 94 | right_bound = left_bound + 2 * n 95 | 96 | mid_idx = (left_bound + n) >> bit_shift 97 | right_idx = right_bound >> bit_shift 98 | 99 | if right_idx > left_idx + 1: 100 | # Only consider nontrivial intervals 101 | if right_idx == left_idx + 2: 102 | # Return length 2 intervals separately to unroll base case. 103 | base_cases.append(left_idx) 104 | else: 105 | # Properly sized interval: 106 | result.append((left_idx, mid_idx, right_idx)) 107 | left_bound, left_idx = right_bound, right_idx 108 | parts *= 2 109 | bit_shift += 1 110 | return base_cases, list(reversed(result)) 111 | 112 | 113 | @cache 114 | def __babai_ranges(n): 115 | # Assume all indices are base cases initially 116 | range_around = [False] * n 117 | for (i, j, k) in __reduction_ranges(n)[1]: 118 | # Mark node `j` as responsible to reduce [i, j) wrt [j, k) once Babai is at/past index j. 119 | range_around[j] = (i, k) 120 | return range_around 121 | 122 | 123 | # Reduction algorithms 124 | 125 | 126 | def nearest_plane(R, T, U): 127 | """ 128 | Perform Babai's Nearest Plane algorithm on multiple targets (all the columns of T), with 129 | respect to the upper-triangular basis R. 130 | This function updates T <- T + RU such that `T + RU` is in the fundamental Babai domain. 131 | Namely, |(T + RU)_{ij}| <= 0.5 R_ii. 132 | 133 | Complexity: O(N n^{omega-1}) if R is a `n x n` matrix, T is a `n x N` matrix, and `N >= n`. 134 | 135 | :param R: upper-triangular basis of a lattice. 136 | :param T: matrix containing many targets requiring reduction. 137 | :param U: the output transformation used to reduce T wrt R. 138 | :return: Nothing! The result is in T and U. 139 | """ 140 | n = len(R) 141 | if n > 1: 142 | range_around = __babai_ranges(n) 143 | for j in range(n-1, 0, -1): 144 | # All targets are reduced w.r.t all basis vectors that come *after* j. 145 | # Compute the reduction coefficient (U_j) w.r.t basis vector j. 146 | U[j, :] = -np.rint((1.0 / R[j, j]) * T[j, :]).astype(np.int64) 147 | # Reduce jth coordinate of T wrt b_j but only in the jth coefficient! 148 | T[j, :] += R[j, j] * U[j, :] 149 | 150 | if not range_around[j]: 151 | T[j-1, :] += R[j-1, j] * U[j, :].astype(np.float64) 152 | else: 153 | i, k = range_around[j] 154 | # Apply reduction of [j:k) on the coefficients T[i:j). 155 | # R12, T1, U2 = R[i:j, j:k], T[i:j, :], U[j:k, :] 156 | # T1 = T1 + R12 · U2 157 | T[i:j, :] += FT_matmul(R[i:j, j:k], U[j:k, :].astype(np.float64)) 158 | 159 | # 0 is a special case because it never needs to propagate reductions. 160 | U[0, :] = -np.rint((1.0 / R[0, 0]) * T[0, :]).astype(np.int64) 161 | # Reduce 0th coordinate of T wrt b_0 but only in the 0th coefficient! 162 | T[0, :] += R[0, 0] * U[0, :] 163 | 164 | 165 | def size_reduce(R, U): 166 | """ 167 | Perform size reduction on R *inplace*, and write the transformation done to R in U, such that 168 | calling this function with (R, U) will update the value R to R' = RU. 169 | 170 | Complexity: O(n^omega) for a `n x n` matrix R. 171 | 172 | :param R: upper-triangular basis of a lattice. 173 | :param U: the matrix U to store the transformation *applied* to R. 174 | U will be upper triangular with unit diagonal. 175 | :return: Nothing! R is size reduced in place. 176 | """ 177 | # Assume diag(U) = (1, 1, ..., 1). 178 | n = len(R) 179 | 180 | base_cases, ranges = __reduction_ranges(n) 181 | for i in base_cases: 182 | U[i, i + 1] = -round(R[i, i + 1] / R[i, i]) 183 | R[i, i + 1] += R[i, i] * U[i, i + 1] 184 | 185 | for (i, j, k) in ranges: 186 | # Size reduce [j, k) with respect to [i, j). 187 | # 188 | # [R11 R12] [U11 U12] [S11 S12] 189 | # R = [ 0 R22], U = [ 0 U22], S = R · U = [ 0 S22] 190 | # 191 | # The previous iteration computed U11 and U22. 192 | # Currently, R11 and R22 contain the values of 193 | # S11 = R11 · U11 and S22 = R22 · U22 respectively. 194 | 195 | # W = R12 · U22 196 | R[i:j, j:k] = FT_matmul(R[i:j, j:k], U[j:k, j:k].astype(np.float64)) 197 | 198 | # U12', S12 = NearestPlane(S11, W) 199 | nearest_plane(R[i:j, i:j], R[i:j, j:k], U[i:j, j:k]) 200 | 201 | # U12 = U11 · U12' 202 | ZZ_left_matmul_strided(U[i:j, i:j], U[i:j, j:k]) 203 | 204 | 205 | def seysen_reduce(R, U): 206 | """ 207 | Perform Seysen's reduction on a matrix R, while keeping track of the transformation matrix U. 208 | The matrix R is updated along the way. 209 | 210 | :param R: an upper-triangular matrix that will be modified 211 | :param U: an upper-triangular transformation matrix such that diag(U) = (1, 1, ..., 1). 212 | :return: Nothing! R is Seysen reduced in place. 213 | """ 214 | # Assume diag(U) = (1, 1, ..., 1). 215 | n = len(R) 216 | 217 | base_cases, ranges = __reduction_ranges(n) 218 | for i in base_cases: 219 | U[i, i + 1] = -round(R[i, i + 1] / R[i, i]) 220 | R[i, i + 1] += R[i, i] * U[i, i + 1] 221 | 222 | for (i, j, k) in ranges: 223 | # Seysen reduce [j, k) with respect to [i, j). 224 | # 225 | # [R11 R12] [U11 U12] [S11 S12] 226 | # R = [ 0 R22], U = [ 0 U22], S = R · U = [ 0 S22] 227 | # 228 | # The previous iteration has computed U11 and U22. 229 | # Currently, R11 and R22 contain the values of 230 | # S11 = R11 · U11 and S22 = R22 · U22 respectively. 231 | 232 | # S12' = R12 · U22. 233 | R[i:j, j:k] = FT_matmul(R[i:j, j:k], U[j:k, j:k].astype(np.float64)) 234 | 235 | # U12' = round(-S11^{-1} · S12'). 236 | U[i:j, j:k] = np.rint( 237 | FT_matmul(-np.linalg.inv(R[i:j, i:j]), R[i:j, j:k]) 238 | ).astype(np.int64) 239 | 240 | # S12 = S12' + S11 · U12'. 241 | R[i:j, j:k] += FT_matmul(R[i:j, i:j], U[i:j, j:k].astype(np.float64)) 242 | 243 | # U12 = U11 · U12' 244 | ZZ_left_matmul_strided(U[i:j, i:j], U[i:j, j:k]) 245 | 246 | 247 | # For didactical reasons, here are the recursive versions of: 248 | # - nearest_plane, 249 | # - size_reduce, and 250 | # - seysen_reduce. 251 | # 252 | # 253 | # def nearest_plane(R, T, U): 254 | # """ 255 | # Perform Babai's Nearest Plane algorithm on multiple targets (all the columns of T), with 256 | # respect to the upper-triangular basis R. 257 | # This function updates T <- T + RU such that `T + RU` is in the fundamental Babai domain. 258 | # Namely, |(T + RU)_{ij}| <= 0.5 R_ii. 259 | # 260 | # Complexity: O(N n^{omega-1}) if R is a `n x n` matrix, T is a `n x N` matrix, and `N >= n`. 261 | # 262 | # :param R: upper-triangular basis of a lattice. 263 | # :param T: matrix containing many targets requiring reduction. 264 | # :param U: the output transformation used to reduce T wrt R. 265 | # :return: Nothing! The result is in T and U. 266 | # """ 267 | # n, m = R.shape[0], R.shape[0] // 2 268 | # if n == 1: 269 | # U[0, :] = -np.rint((1.0 / R[0, 0]) * T).astype(np.int64) 270 | # T += R[0, 0] * U 271 | # else: 272 | # # R11, R12, R22 = R[:m, :m], R[:m, m:], R[m:, m:] 273 | # # T1, T2 = T[:m, :], T[m:, :] 274 | # # U1, U2 = U[:m, :], U[m:, :] 275 | # 276 | # # U2 = NP(R22, T2) 277 | # nearest_plane(R[m:, m:], T[m:, :], U[m:, :]) 278 | # 279 | # # T1 = T1 + R12 · U2 280 | # T[:m, :] += FT_matmul(R[:m, m:], U[m:, :].astype(np.float64)) 281 | # 282 | # # U1 = NP(R11, T1) 283 | # nearest_plane(R[:m, :m], T[:m, :], U[:m, :]) 284 | # 285 | # 286 | # def size_reduce(R, U): 287 | # """ 288 | # Perform size reduction on R *inplace*, and write the transformation done to R in U, such that 289 | # calling this function with (R, U) will update the value R to R' = RU. 290 | # 291 | # Complexity: O(n^omega) for a `n x n` matrix R. 292 | # 293 | # :param R: upper-triangular basis of a lattice. 294 | # :param U: the matrix U to store the transformation *applied* to R. 295 | # U will be upper triangular with unit diagonal. 296 | # :return: Nothing! R is size reduced in place. 297 | # """ 298 | # n, m = R.shape[0], R.shape[0] // 2 299 | # if n == 1: 300 | # return 301 | # 302 | # if n == 2: 303 | # U[0, 1] = -round(R[0, 1] / R[0, 0]) 304 | # R[0, 1] += R[0, 0] * U[0, 1] 305 | # else: 306 | # # R11, R12, R22 = R[:m, :m], R[:m, m:], R[m:, m:] 307 | # # U11, U12, U22 = U[:m, :m], U[:m, m:], U[m:, m:] 308 | # 309 | # # U11 = SizeReduce(R11) 310 | # size_reduce(R[:m, :m], U[:m, :m]) 311 | # 312 | # # U22 = SizeReduce(R22) 313 | # size_reduce(R[m:, m:], U[m:, m:]) 314 | # 315 | # # R12 = R12 · U22 316 | # R[:m, m:] = FT_matmul(R[:m, m:], U[m:, m:].astype(np.float64)) 317 | # 318 | # # U12' = NearestPlane(basis=R11', target=R12), R12 = R12 + R11' U12' 319 | # nearest_plane(R[:m, :m], R[:m, m:], U[:m, m:]) 320 | # 321 | # # Note: NP was called with the size-reduced R11' = R11 · U11. 322 | # # U12 = U11 · U12' 323 | # # U[:m, m:] = U[:m, :m] @ U[:m, m:] 324 | # ZZ_left_matmul_strided(U[:m, :m], U[:m, m:]) 325 | # 326 | # 327 | # def seysen_reduce(R, U): 328 | # """ 329 | # Seysen reduce a matrix R, recursive style, and store the result in U. 330 | # See: Algorithm 7 from [KEF21]. 331 | # [KEF21] P. Kircher, T. Espitau, P.-A. Fouque. Towards faster polynomial-time lattice reduction. 332 | # :param R: an upper-triangular matrix (having row vectors). 333 | # :param U: a unimodular transformation U such that RU is Seysen-Reduced. 334 | # :return: None! The result is stored in U. 335 | # """ 336 | # n, m = len(R), len(R) // 2 337 | # if n == 1: 338 | # # Base case 339 | # U[0, 0] = 1 340 | # elif n == 2: 341 | # # Make sure RU is size-reduced, i.e. |R00*X + R01| <= |R00|/2 342 | # U[0, 0] = U[1, 1] = 1 343 | # U[0, 1] = -round(R[0, 1] / R[0, 0]) 344 | # else: 345 | # # R11, R12, R22 = R[:m, :m], R[:m, m:], R[m:, m:] 346 | # seysen_reduce(R[:m, :m], U[:m, :m]) 347 | # seysen_reduce(R[m:, m:], U[m:, m:]) 348 | # 349 | # # S11 = R11 · U11 350 | # S11 = FT_matmul(R[:m, :m], U[:m, :m].astype(np.float64)) 351 | # 352 | # # S12' = R12 · U22 353 | # S12 = FT_matmul(R[:m, m:], U[m:, m:].astype(np.float64)) 354 | # 355 | # # U12' = round(-S11^{-1} S12'). 356 | # U[i:j, j:k] = np.rint(FT_matmul(-np.linalg.inv(S11), S12)).astype(np.int64) 357 | # 358 | # # U12 = U11 · U12' 359 | # ZZ_left_matmul_strided(U[:m, :m], U[:m, m:]) 360 | -------------------------------------------------------------------------------- /src/stats.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for computing the basis profile of a given basis, and measuring its quality. 3 | """ 4 | from math import exp, gamma, log2, pi 5 | import numpy as np 6 | 7 | 8 | def get_profile(basis, is_upper=False): 9 | """ 10 | Return the profile of a basis, i.e. log_2 ||b_i*|| for i=1, ..., n. 11 | Note: the logarithm is done base 2, similar to https://github.com/keeganryan/flatter. 12 | :param basis: basis for a lattice 13 | :param is_upper: whether `basis` is already an upper triangular matrix or not 14 | """ 15 | upper = basis if is_upper else np.linalg.qr(basis, mode='r') 16 | return [log2(abs(d_i)) for d_i in upper.diagonal()] 17 | 18 | 19 | def gh(dim): 20 | """ 21 | Return the Gaussian Heuristic at dimension n. This gives a prediction of 22 | the length of the shortest vector in a lattice of unit volume. 23 | :param n: lattice dimension 24 | :return: GH(n) 25 | """ 26 | if dim >= 100: 27 | return float(dim / (2*pi*exp(1)))**0.5 28 | return float(gamma(1.0 + 0.5 * dim)**(1.0 / dim) / pi**0.5) 29 | 30 | 31 | def gaussian_heuristic(basis): 32 | """ 33 | Return the Gaussian Heuristic for a particular basis. 34 | """ 35 | rank = basis.shape[1] 36 | return gh(rank) * 2.0**(sum(get_profile(basis)) / rank) 37 | 38 | 39 | def rhf(profile): 40 | """ 41 | Return the n-th root Hermite factor, given the profile of some basis, i.e. 42 | rhf(B) = (||b_0|| / det(B)^{1/n})^{1/n}. 43 | :param profile: profile belonging to some basis of some lattice 44 | """ 45 | n = len(profile) 46 | return 2.0**((profile[0] - sum(profile) / n) / n) 47 | 48 | 49 | def slope(profile): 50 | """ 51 | Return the current slope of a profile 52 | """ 53 | n = len(profile) 54 | i_mean = (n - 1) * 0.5 55 | v1 = sum(profile[i] * (i - i_mean) for i in range(n)) 56 | v2 = sum((i - i_mean)**2 for i in range(n)) 57 | return v1 / v2 58 | 59 | 60 | def potential(profile): 61 | """ 62 | Return the (log2 of the) potential of a basis profile. 63 | Normally in lattice reduction, this is a strictly decreasing function of time, and is used to 64 | prove that LLL runs in polynomial time. 65 | """ 66 | n = len(profile) 67 | return sum((n - i) * profile[i] for i in range(n)) 68 | --------------------------------------------------------------------------------