├── .coveragerc ├── .editorconfig ├── .flake8 ├── .gitignore ├── .mypy.ini ├── .pylintrc ├── .pytest.ini ├── .tool-versions ├── .vscode ├── launch.json └── settings.json ├── Pipfile ├── Pipfile.lock ├── README.md ├── main.ipynb ├── setup.py ├── tests ├── e2e_half_adder_test.py ├── e2e_minimum_test.py ├── e2e_mux_test.py └── e2e_nand_test.py └── tfhe ├── __init__.py ├── boot_gates.py ├── keys.py ├── lwe.py ├── lwe_bootstrapping.py ├── numeric_functions.py ├── polynomials.py ├── tgsw.py ├── tlwe.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,ipynb}] 12 | indent_size = 4 13 | quote_type = double 14 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | exclude = .git,.venv,__pycache__,build,dist 4 | extend-ignore = 5 | # Whitespace before ':' (E203) 6 | E203 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,vim,emacs,sublimetext,visualstudiocode,intellij+all,python,jupyternotebooks 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,vim,emacs,sublimetext,visualstudiocode,intellij+all,python,jupyternotebooks 3 | 4 | ### Emacs ### 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | 19 | # flymake-mode 20 | *_flymake.* 21 | 22 | # eshell files 23 | /eshell/history 24 | /eshell/lastdir 25 | 26 | # elpa packages 27 | /elpa/ 28 | 29 | # reftex files 30 | *.rel 31 | 32 | # AUCTeX auto folder 33 | /auto/ 34 | 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | 39 | # Flycheck 40 | flycheck_*.el 41 | 42 | # server auth directory 43 | /server/ 44 | 45 | # projectiles files 46 | .projectile 47 | 48 | # directory configuration 49 | .dir-locals.el 50 | 51 | # network security 52 | /network-security.data 53 | 54 | 55 | ### Intellij+all ### 56 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 57 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 58 | 59 | # User-specific stuff 60 | .idea/**/workspace.xml 61 | .idea/**/tasks.xml 62 | .idea/**/usage.statistics.xml 63 | .idea/**/dictionaries 64 | .idea/**/shelf 65 | 66 | # AWS User-specific 67 | .idea/**/aws.xml 68 | 69 | # Generated files 70 | .idea/**/contentModel.xml 71 | 72 | # Sensitive or high-churn files 73 | .idea/**/dataSources/ 74 | .idea/**/dataSources.ids 75 | .idea/**/dataSources.local.xml 76 | .idea/**/sqlDataSources.xml 77 | .idea/**/dynamic.xml 78 | .idea/**/uiDesigner.xml 79 | .idea/**/dbnavigator.xml 80 | 81 | # Gradle 82 | .idea/**/gradle.xml 83 | .idea/**/libraries 84 | 85 | # Gradle and Maven with auto-import 86 | # When using Gradle or Maven with auto-import, you should exclude module files, 87 | # since they will be recreated, and may cause churn. Uncomment if using 88 | # auto-import. 89 | # .idea/artifacts 90 | # .idea/compiler.xml 91 | # .idea/jarRepositories.xml 92 | # .idea/modules.xml 93 | # .idea/*.iml 94 | # .idea/modules 95 | # *.iml 96 | # *.ipr 97 | 98 | # CMake 99 | cmake-build-*/ 100 | 101 | # Mongo Explorer plugin 102 | .idea/**/mongoSettings.xml 103 | 104 | # File-based project format 105 | *.iws 106 | 107 | # IntelliJ 108 | out/ 109 | 110 | # mpeltonen/sbt-idea plugin 111 | .idea_modules/ 112 | 113 | # JIRA plugin 114 | atlassian-ide-plugin.xml 115 | 116 | # Cursive Clojure plugin 117 | .idea/replstate.xml 118 | 119 | # SonarLint plugin 120 | .idea/sonarlint/ 121 | 122 | # Crashlytics plugin (for Android Studio and IntelliJ) 123 | com_crashlytics_export_strings.xml 124 | crashlytics.properties 125 | crashlytics-build.properties 126 | fabric.properties 127 | 128 | # Editor-based Rest Client 129 | .idea/httpRequests 130 | 131 | # Android studio 3.1+ serialized cache file 132 | .idea/caches/build_file_checksums.ser 133 | 134 | ### Intellij+all Patch ### 135 | # Ignore everything but code style settings and run configurations 136 | # that are supposed to be shared within teams. 137 | 138 | .idea/* 139 | 140 | !.idea/codeStyles 141 | !.idea/runConfigurations 142 | 143 | ### JupyterNotebooks ### 144 | # gitignore template for Jupyter Notebooks 145 | # website: http://jupyter.org/ 146 | 147 | .ipynb_checkpoints 148 | */.ipynb_checkpoints/* 149 | 150 | # IPython 151 | profile_default/ 152 | ipython_config.py 153 | 154 | # Remove previous ipynb_checkpoints 155 | # git rm -r .ipynb_checkpoints/ 156 | 157 | ### Linux ### 158 | 159 | # temporary files which can be created if a process still has a handle open of a deleted file 160 | .fuse_hidden* 161 | 162 | # KDE directory preferences 163 | .directory 164 | 165 | # Linux trash folder which might appear on any partition or disk 166 | .Trash-* 167 | 168 | # .nfs files are created when an open file is removed but is still being accessed 169 | .nfs* 170 | 171 | ### macOS ### 172 | # General 173 | .DS_Store 174 | .AppleDouble 175 | .LSOverride 176 | 177 | # Icon must end with two \r 178 | Icon 179 | 180 | 181 | # Thumbnails 182 | ._* 183 | 184 | # Files that might appear in the root of a volume 185 | .DocumentRevisions-V100 186 | .fseventsd 187 | .Spotlight-V100 188 | .TemporaryItems 189 | .Trashes 190 | .VolumeIcon.icns 191 | .com.apple.timemachine.donotpresent 192 | 193 | # Directories potentially created on remote AFP share 194 | .AppleDB 195 | .AppleDesktop 196 | Network Trash Folder 197 | Temporary Items 198 | .apdisk 199 | 200 | ### macOS Patch ### 201 | # iCloud generated files 202 | *.icloud 203 | 204 | ### Python ### 205 | # Byte-compiled / optimized / DLL files 206 | __pycache__/ 207 | *.py[cod] 208 | *$py.class 209 | 210 | # C extensions 211 | *.so 212 | 213 | # Distribution / packaging 214 | .Python 215 | build/ 216 | develop-eggs/ 217 | downloads/ 218 | eggs/ 219 | .eggs/ 220 | lib/ 221 | lib64/ 222 | parts/ 223 | sdist/ 224 | var/ 225 | wheels/ 226 | share/python-wheels/ 227 | *.egg-info/ 228 | .installed.cfg 229 | *.egg 230 | MANIFEST 231 | 232 | # PyInstaller 233 | # Usually these files are written by a python script from a template 234 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 235 | *.manifest 236 | *.spec 237 | 238 | # Installer logs 239 | pip-log.txt 240 | pip-delete-this-directory.txt 241 | 242 | # Unit test / coverage reports 243 | htmlcov/ 244 | .tox/ 245 | .nox/ 246 | .coverage 247 | .coverage.* 248 | .cache 249 | nosetests.xml 250 | coverage.xml 251 | *.cover 252 | *.py,cover 253 | .hypothesis/ 254 | .pytest_cache/ 255 | cover/ 256 | 257 | # Translations 258 | *.mo 259 | *.pot 260 | 261 | # Django stuff: 262 | *.log 263 | local_settings.py 264 | db.sqlite3 265 | db.sqlite3-journal 266 | 267 | # Flask stuff: 268 | instance/ 269 | .webassets-cache 270 | 271 | # Scrapy stuff: 272 | .scrapy 273 | 274 | # Sphinx documentation 275 | docs/_build/ 276 | 277 | # PyBuilder 278 | .pybuilder/ 279 | target/ 280 | 281 | # Jupyter Notebook 282 | 283 | # IPython 284 | 285 | # pyenv 286 | # For a library or package, you might want to ignore these files since the code is 287 | # intended to run in multiple environments; otherwise, check them in: 288 | # .python-version 289 | 290 | # pipenv 291 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 292 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 293 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 294 | # install all needed dependencies. 295 | #Pipfile.lock 296 | 297 | # poetry 298 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 299 | # This is especially recommended for binary packages to ensure reproducibility, and is more 300 | # commonly ignored for libraries. 301 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 302 | #poetry.lock 303 | 304 | # pdm 305 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 306 | #pdm.lock 307 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 308 | # in version control. 309 | # https://pdm.fming.dev/#use-with-ide 310 | .pdm.toml 311 | 312 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 313 | __pypackages__/ 314 | 315 | # Celery stuff 316 | celerybeat-schedule 317 | celerybeat.pid 318 | 319 | # SageMath parsed files 320 | *.sage.py 321 | 322 | # Environments 323 | .env 324 | .venv 325 | env/ 326 | venv/ 327 | ENV/ 328 | env.bak/ 329 | venv.bak/ 330 | 331 | # Spyder project settings 332 | .spyderproject 333 | .spyproject 334 | 335 | # Rope project settings 336 | .ropeproject 337 | 338 | # mkdocs documentation 339 | /site 340 | 341 | # mypy 342 | .mypy_cache/ 343 | .dmypy.json 344 | dmypy.json 345 | 346 | # Pyre type checker 347 | .pyre/ 348 | 349 | # pytype static type analyzer 350 | .pytype/ 351 | 352 | # Cython debug symbols 353 | cython_debug/ 354 | 355 | # PyCharm 356 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 357 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 358 | # and can be added to the global gitignore or merged into this file. For a more nuclear 359 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 360 | #.idea/ 361 | 362 | ### Python Patch ### 363 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 364 | poetry.toml 365 | 366 | # ruff 367 | .ruff_cache/ 368 | 369 | # LSP config files 370 | pyrightconfig.json 371 | 372 | ### SublimeText ### 373 | # Cache files for Sublime Text 374 | *.tmlanguage.cache 375 | *.tmPreferences.cache 376 | *.stTheme.cache 377 | 378 | # Workspace files are user-specific 379 | *.sublime-workspace 380 | 381 | # Project files should be checked into the repository, unless a significant 382 | # proportion of contributors will probably not be using Sublime Text 383 | # *.sublime-project 384 | 385 | # SFTP configuration file 386 | sftp-config.json 387 | sftp-config-alt*.json 388 | 389 | # Package control specific files 390 | Package Control.last-run 391 | Package Control.ca-list 392 | Package Control.ca-bundle 393 | Package Control.system-ca-bundle 394 | Package Control.cache/ 395 | Package Control.ca-certs/ 396 | Package Control.merged-ca-bundle 397 | Package Control.user-ca-bundle 398 | oscrypto-ca-bundle.crt 399 | bh_unicode_properties.cache 400 | 401 | # Sublime-github package stores a github token in this file 402 | # https://packagecontrol.io/packages/sublime-github 403 | GitHub.sublime-settings 404 | 405 | ### Vim ### 406 | # Swap 407 | [._]*.s[a-v][a-z] 408 | !*.svg # comment out if you don't need vector files 409 | [._]*.sw[a-p] 410 | [._]s[a-rt-v][a-z] 411 | [._]ss[a-gi-z] 412 | [._]sw[a-p] 413 | 414 | # Session 415 | Session.vim 416 | Sessionx.vim 417 | 418 | # Temporary 419 | .netrwhist 420 | # Auto-generated tag files 421 | tags 422 | # Persistent undo 423 | [._]*.un~ 424 | 425 | ### VisualStudioCode ### 426 | .vscode/* 427 | !.vscode/settings.json 428 | !.vscode/tasks.json 429 | !.vscode/launch.json 430 | !.vscode/extensions.json 431 | !.vscode/*.code-snippets 432 | 433 | # Local History for Visual Studio Code 434 | .history/ 435 | 436 | # Built Visual Studio Code Extensions 437 | *.vsix 438 | 439 | ### VisualStudioCode Patch ### 440 | # Ignore all local history of files 441 | .history 442 | .ionide 443 | 444 | ### Windows ### 445 | # Windows thumbnail cache files 446 | Thumbs.db 447 | Thumbs.db:encryptable 448 | ehthumbs.db 449 | ehthumbs_vista.db 450 | 451 | # Dump file 452 | *.stackdump 453 | 454 | # Folder config file 455 | [Dd]esktop.ini 456 | 457 | # Recycle Bin used on file shares 458 | $RECYCLE.BIN/ 459 | 460 | # Windows Installer files 461 | *.cab 462 | *.msi 463 | *.msix 464 | *.msm 465 | *.msp 466 | 467 | # Windows shortcuts 468 | *.lnk 469 | 470 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,vim,emacs,sublimetext,visualstudiocode,intellij+all,python,jupyternotebooks 471 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | strict = True 3 | pretty = False 4 | follow_imports = silent 5 | warn_unreachable = True 6 | show_column_numbers = True 7 | ignore_missing_imports = True 8 | exclude = .git|.venv|__pycache__|build|dist 9 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore = .git,.venv,__pycache__,build,dist 3 | disable = 4 | C0114, # missing-module-docstring 5 | C0115, # missing-class-docstring 6 | C0116, # missing-function-docstring 7 | C0103, # invalid-name 8 | R0902, # too-many-instance-attributes 9 | R0903, # too-few-public-methods 10 | R0913, # too-many-arguments 11 | R0914, # too-many-locals 12 | 13 | [FORMAT] 14 | max-line-length = 88 15 | 16 | [TYPECHECK] 17 | generated-members=numpy 18 | -------------------------------------------------------------------------------- /.pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | error 4 | # FIXME: Ignores overflow detected by Numpy in 5 | # `lwe_key_switch_translate_from_array` method. 6 | ignore:overflow encountered in scalar subtract 7 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.11.2 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Python: File", 5 | "type": "python", 6 | "request": "launch", 7 | "program": "${file}", 8 | "justMyCode": true, 9 | "cwd": "${workspaceFolder}", 10 | "python": "${workspaceFolder}/.venv/bin/python" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnSaveMode": "file", 5 | "editor.defaultFormatter": "ms-python.python", 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": true 8 | } 9 | }, 10 | "editor.formatOnSave": true, 11 | "isort.args": ["--profile", "black"], 12 | "python.formatting.provider": "black", 13 | "python.analysis.typeCheckingMode": "basic", 14 | "python.linting.pylintEnabled": true, 15 | "python.linting.flake8Enabled": true, 16 | "python.linting.mypyEnabled": true, 17 | "python.linting.mypyArgs": [ 18 | "--follow-imports=silent", 19 | "--ignore-missing-imports", 20 | "--show-column-numbers", 21 | "--no-pretty", 22 | "--warn-unreachable", 23 | "--strict" 24 | ], 25 | // TODO: Update 26 | "python.linting.mypyPath": "", 27 | "python.linting.flake8Path": "", 28 | "python.linting.pylintPath": "", 29 | "python.formatting.blackPath": "", 30 | "python.defaultInterpreterPath": "" 31 | } 32 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | tfhe-py = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | mypy = "~=1.3.0" 11 | black = "~=23.3.0" 12 | pylint = "~=2.17.4" 13 | flake8 = "~=6.0.0" 14 | pytest = "~=7.3.1" 15 | isort = "~=5.12.0" 16 | py-spy = "~=0.3.14" 17 | coverage = "~=7.2.7" 18 | ipykernel = "~=6.23.1" 19 | 20 | [requires] 21 | python_version = "3.11" 22 | 23 | [scripts] 24 | test = "coverage run -m pytest . -s -v" 25 | coverage = "coverage report -m" 26 | mypy = "mypy ." 27 | flake8 = "flake8 ." 28 | pylint = "pylint . --recursive true" 29 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "39f0e3283f51aaa8edd0fd451c4e4c8b82f69fc6842b8bce69a317ca0b525fca" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.11" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "numpy": { 20 | "hashes": [ 21 | "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187", 22 | "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812", 23 | "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7", 24 | "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4", 25 | "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6", 26 | "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0", 27 | "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4", 28 | "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570", 29 | "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4", 30 | "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f", 31 | "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80", 32 | "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289", 33 | "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385", 34 | "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078", 35 | "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c", 36 | "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463", 37 | "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3", 38 | "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950", 39 | "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155", 40 | "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7", 41 | "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c", 42 | "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096", 43 | "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17", 44 | "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf", 45 | "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4", 46 | "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02", 47 | "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c", 48 | "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b" 49 | ], 50 | "markers": "python_version >= '3.8'", 51 | "version": "==1.24.3" 52 | }, 53 | "tfhe-py": { 54 | "editable": true, 55 | "path": "." 56 | } 57 | }, 58 | "develop": { 59 | "appnope": { 60 | "hashes": [ 61 | "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", 62 | "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" 63 | ], 64 | "markers": "platform_system == 'Darwin'", 65 | "version": "==0.1.3" 66 | }, 67 | "astroid": { 68 | "hashes": [ 69 | "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324", 70 | "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f" 71 | ], 72 | "markers": "python_full_version >= '3.7.2'", 73 | "version": "==2.15.5" 74 | }, 75 | "asttokens": { 76 | "hashes": [ 77 | "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3", 78 | "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c" 79 | ], 80 | "version": "==2.2.1" 81 | }, 82 | "backcall": { 83 | "hashes": [ 84 | "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", 85 | "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" 86 | ], 87 | "version": "==0.2.0" 88 | }, 89 | "black": { 90 | "hashes": [ 91 | "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", 92 | "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", 93 | "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", 94 | "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", 95 | "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", 96 | "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", 97 | "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", 98 | "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", 99 | "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", 100 | "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", 101 | "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", 102 | "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", 103 | "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", 104 | "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", 105 | "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", 106 | "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", 107 | "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", 108 | "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", 109 | "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", 110 | "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", 111 | "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", 112 | "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", 113 | "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", 114 | "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", 115 | "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" 116 | ], 117 | "index": "pypi", 118 | "version": "==23.3.0" 119 | }, 120 | "click": { 121 | "hashes": [ 122 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 123 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 124 | ], 125 | "markers": "python_version >= '3.7'", 126 | "version": "==8.1.3" 127 | }, 128 | "comm": { 129 | "hashes": [ 130 | "sha256:16613c6211e20223f215fc6d3b266a247b6e2641bf4e0a3ad34cb1aff2aa3f37", 131 | "sha256:a61efa9daffcfbe66fd643ba966f846a624e4e6d6767eda9cf6e993aadaab93e" 132 | ], 133 | "markers": "python_version >= '3.6'", 134 | "version": "==0.1.3" 135 | }, 136 | "coverage": { 137 | "hashes": [ 138 | "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", 139 | "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", 140 | "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", 141 | "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", 142 | "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", 143 | "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", 144 | "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", 145 | "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", 146 | "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", 147 | "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", 148 | "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", 149 | "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", 150 | "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", 151 | "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", 152 | "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", 153 | "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", 154 | "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", 155 | "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", 156 | "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", 157 | "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", 158 | "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", 159 | "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", 160 | "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", 161 | "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", 162 | "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", 163 | "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", 164 | "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", 165 | "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", 166 | "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", 167 | "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", 168 | "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", 169 | "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", 170 | "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", 171 | "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", 172 | "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", 173 | "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", 174 | "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", 175 | "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", 176 | "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", 177 | "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", 178 | "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", 179 | "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", 180 | "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", 181 | "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", 182 | "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", 183 | "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", 184 | "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", 185 | "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", 186 | "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", 187 | "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", 188 | "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", 189 | "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", 190 | "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", 191 | "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", 192 | "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", 193 | "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", 194 | "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", 195 | "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", 196 | "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", 197 | "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" 198 | ], 199 | "index": "pypi", 200 | "version": "==7.2.7" 201 | }, 202 | "debugpy": { 203 | "hashes": [ 204 | "sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c", 205 | "sha256:279d64c408c60431c8ee832dfd9ace7c396984fd7341fa3116aee414e7dcd88d", 206 | "sha256:33edb4afa85c098c24cc361d72ba7c21bb92f501104514d4ffec1fb36e09c01a", 207 | "sha256:38ed626353e7c63f4b11efad659be04c23de2b0d15efff77b60e4740ea685d07", 208 | "sha256:5224eabbbeddcf1943d4e2821876f3e5d7d383f27390b82da5d9558fd4eb30a9", 209 | "sha256:53f7a456bc50706a0eaabecf2d3ce44c4d5010e46dfc65b6b81a518b42866267", 210 | "sha256:9cd10cf338e0907fdcf9eac9087faa30f150ef5445af5a545d307055141dd7a4", 211 | "sha256:aaf6da50377ff4056c8ed470da24632b42e4087bc826845daad7af211e00faad", 212 | "sha256:b3e7ac809b991006ad7f857f016fa92014445085711ef111fdc3f74f66144096", 213 | "sha256:bae1123dff5bfe548ba1683eb972329ba6d646c3a80e6b4c06cd1b1dd0205e9b", 214 | "sha256:c0ff93ae90a03b06d85b2c529eca51ab15457868a377c4cc40a23ab0e4e552a3", 215 | "sha256:c4c2f0810fa25323abfdfa36cbbbb24e5c3b1a42cb762782de64439c575d67f2", 216 | "sha256:d71b31117779d9a90b745720c0eab54ae1da76d5b38c8026c654f4a066b0130a", 217 | "sha256:dbe04e7568aa69361a5b4c47b4493d5680bfa3a911d1e105fbea1b1f23f3eb45", 218 | "sha256:de86029696e1b3b4d0d49076b9eba606c226e33ae312a57a46dca14ff370894d", 219 | "sha256:e3876611d114a18aafef6383695dfc3f1217c98a9168c1aaf1a02b01ec7d8d1e", 220 | "sha256:ed6d5413474e209ba50b1a75b2d9eecf64d41e6e4501977991cdc755dc83ab0f", 221 | "sha256:f90a2d4ad9a035cee7331c06a4cf2245e38bd7c89554fe3b616d90ab8aab89cc" 222 | ], 223 | "markers": "python_version >= '3.7'", 224 | "version": "==1.6.7" 225 | }, 226 | "decorator": { 227 | "hashes": [ 228 | "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", 229 | "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" 230 | ], 231 | "markers": "python_version >= '3.5'", 232 | "version": "==5.1.1" 233 | }, 234 | "dill": { 235 | "hashes": [ 236 | "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", 237 | "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" 238 | ], 239 | "markers": "python_version >= '3.11'", 240 | "version": "==0.3.6" 241 | }, 242 | "executing": { 243 | "hashes": [ 244 | "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc", 245 | "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107" 246 | ], 247 | "version": "==1.2.0" 248 | }, 249 | "flake8": { 250 | "hashes": [ 251 | "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", 252 | "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" 253 | ], 254 | "index": "pypi", 255 | "version": "==6.0.0" 256 | }, 257 | "iniconfig": { 258 | "hashes": [ 259 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 260 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 261 | ], 262 | "markers": "python_version >= '3.7'", 263 | "version": "==2.0.0" 264 | }, 265 | "ipykernel": { 266 | "hashes": [ 267 | "sha256:1aba0ae8453e15e9bc6b24e497ef6840114afcdb832ae597f32137fa19d42a6f", 268 | "sha256:77aeffab056c21d16f1edccdc9e5ccbf7d96eb401bd6703610a21be8b068aadc" 269 | ], 270 | "index": "pypi", 271 | "version": "==6.23.1" 272 | }, 273 | "ipython": { 274 | "hashes": [ 275 | "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1", 276 | "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf" 277 | ], 278 | "markers": "python_version >= '3.9'", 279 | "version": "==8.14.0" 280 | }, 281 | "isort": { 282 | "hashes": [ 283 | "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", 284 | "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" 285 | ], 286 | "index": "pypi", 287 | "version": "==5.12.0" 288 | }, 289 | "jedi": { 290 | "hashes": [ 291 | "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e", 292 | "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612" 293 | ], 294 | "markers": "python_version >= '3.6'", 295 | "version": "==0.18.2" 296 | }, 297 | "jupyter-client": { 298 | "hashes": [ 299 | "sha256:9fe233834edd0e6c0aa5f05ca2ab4bdea1842bfd2d8a932878212fc5301ddaf0", 300 | "sha256:b18219aa695d39e2ad570533e0d71fb7881d35a873051054a84ee2a17c4b7389" 301 | ], 302 | "markers": "python_version >= '3.8'", 303 | "version": "==8.2.0" 304 | }, 305 | "jupyter-core": { 306 | "hashes": [ 307 | "sha256:6db75be0c83edbf1b7c9f91ec266a9a24ef945da630f3120e1a0046dc13713fc", 308 | "sha256:d4201af84559bc8c70cead287e1ab94aeef3c512848dde077b7684b54d67730d" 309 | ], 310 | "markers": "python_version >= '3.8'", 311 | "version": "==5.3.0" 312 | }, 313 | "lazy-object-proxy": { 314 | "hashes": [ 315 | "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", 316 | "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", 317 | "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", 318 | "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", 319 | "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", 320 | "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", 321 | "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", 322 | "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", 323 | "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", 324 | "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", 325 | "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", 326 | "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", 327 | "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", 328 | "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", 329 | "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", 330 | "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", 331 | "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", 332 | "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", 333 | "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", 334 | "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", 335 | "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", 336 | "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", 337 | "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", 338 | "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", 339 | "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", 340 | "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", 341 | "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", 342 | "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", 343 | "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", 344 | "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", 345 | "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", 346 | "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", 347 | "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", 348 | "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", 349 | "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", 350 | "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" 351 | ], 352 | "markers": "python_version >= '3.7'", 353 | "version": "==1.9.0" 354 | }, 355 | "matplotlib-inline": { 356 | "hashes": [ 357 | "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", 358 | "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" 359 | ], 360 | "markers": "python_version >= '3.5'", 361 | "version": "==0.1.6" 362 | }, 363 | "mccabe": { 364 | "hashes": [ 365 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 366 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 367 | ], 368 | "markers": "python_version >= '3.6'", 369 | "version": "==0.7.0" 370 | }, 371 | "mypy": { 372 | "hashes": [ 373 | "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703", 374 | "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf", 375 | "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4", 376 | "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85", 377 | "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd", 378 | "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae", 379 | "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd", 380 | "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca", 381 | "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305", 382 | "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409", 383 | "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c", 384 | "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb", 385 | "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee", 386 | "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a", 387 | "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228", 388 | "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897", 389 | "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d", 390 | "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f", 391 | "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152", 392 | "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf", 393 | "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8", 394 | "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11", 395 | "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017", 396 | "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929", 397 | "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e", 398 | "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a" 399 | ], 400 | "index": "pypi", 401 | "version": "==1.3.0" 402 | }, 403 | "mypy-extensions": { 404 | "hashes": [ 405 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 406 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 407 | ], 408 | "markers": "python_version >= '3.5'", 409 | "version": "==1.0.0" 410 | }, 411 | "nest-asyncio": { 412 | "hashes": [ 413 | "sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8", 414 | "sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290" 415 | ], 416 | "markers": "python_version >= '3.5'", 417 | "version": "==1.5.6" 418 | }, 419 | "packaging": { 420 | "hashes": [ 421 | "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", 422 | "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" 423 | ], 424 | "markers": "python_version >= '3.7'", 425 | "version": "==23.1" 426 | }, 427 | "parso": { 428 | "hashes": [ 429 | "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", 430 | "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" 431 | ], 432 | "markers": "python_version >= '3.6'", 433 | "version": "==0.8.3" 434 | }, 435 | "pathspec": { 436 | "hashes": [ 437 | "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", 438 | "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" 439 | ], 440 | "markers": "python_version >= '3.7'", 441 | "version": "==0.11.1" 442 | }, 443 | "pexpect": { 444 | "hashes": [ 445 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 446 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 447 | ], 448 | "markers": "sys_platform != 'win32'", 449 | "version": "==4.8.0" 450 | }, 451 | "pickleshare": { 452 | "hashes": [ 453 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 454 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 455 | ], 456 | "version": "==0.7.5" 457 | }, 458 | "platformdirs": { 459 | "hashes": [ 460 | "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", 461 | "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5" 462 | ], 463 | "markers": "python_version >= '3.7'", 464 | "version": "==3.5.1" 465 | }, 466 | "pluggy": { 467 | "hashes": [ 468 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 469 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 470 | ], 471 | "markers": "python_version >= '3.6'", 472 | "version": "==1.0.0" 473 | }, 474 | "prompt-toolkit": { 475 | "hashes": [ 476 | "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b", 477 | "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f" 478 | ], 479 | "markers": "python_full_version >= '3.7.0'", 480 | "version": "==3.0.38" 481 | }, 482 | "psutil": { 483 | "hashes": [ 484 | "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d", 485 | "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217", 486 | "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4", 487 | "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c", 488 | "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f", 489 | "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da", 490 | "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4", 491 | "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42", 492 | "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5", 493 | "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4", 494 | "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9", 495 | "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f", 496 | "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30", 497 | "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48" 498 | ], 499 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 500 | "version": "==5.9.5" 501 | }, 502 | "ptyprocess": { 503 | "hashes": [ 504 | "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", 505 | "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" 506 | ], 507 | "version": "==0.7.0" 508 | }, 509 | "pure-eval": { 510 | "hashes": [ 511 | "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", 512 | "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3" 513 | ], 514 | "version": "==0.2.2" 515 | }, 516 | "py-spy": { 517 | "hashes": [ 518 | "sha256:3e8e48032e71c94c3dd51694c39e762e4bbfec250df5bf514adcdd64e79371e0", 519 | "sha256:590905447241d789d9de36cff9f52067b6f18d8b5e9fb399242041568d414461", 520 | "sha256:5b342cc5feb8d160d57a7ff308de153f6be68dcf506ad02b4d67065f2bae7f45", 521 | "sha256:8f5b311d09f3a8e33dbd0d44fc6e37b715e8e0c7efefafcda8bfd63b31ab5a31", 522 | "sha256:f59b0b52e56ba9566305236375e6fc68888261d0d36b5addbe3cf85affbefc0e", 523 | "sha256:fd6211fe7f587b3532ba9d300784326d9a6f2b890af7bf6fff21a029ebbc812b", 524 | "sha256:fe7efe6c91f723442259d428bf1f9ddb9c1679828866b353d539345ca40d9dd2" 525 | ], 526 | "index": "pypi", 527 | "version": "==0.3.14" 528 | }, 529 | "pycodestyle": { 530 | "hashes": [ 531 | "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", 532 | "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" 533 | ], 534 | "markers": "python_version >= '3.6'", 535 | "version": "==2.10.0" 536 | }, 537 | "pyflakes": { 538 | "hashes": [ 539 | "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", 540 | "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" 541 | ], 542 | "markers": "python_version >= '3.6'", 543 | "version": "==3.0.1" 544 | }, 545 | "pygments": { 546 | "hashes": [ 547 | "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", 548 | "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" 549 | ], 550 | "markers": "python_version >= '3.7'", 551 | "version": "==2.15.1" 552 | }, 553 | "pylint": { 554 | "hashes": [ 555 | "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1", 556 | "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c" 557 | ], 558 | "index": "pypi", 559 | "version": "==2.17.4" 560 | }, 561 | "pytest": { 562 | "hashes": [ 563 | "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", 564 | "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" 565 | ], 566 | "index": "pypi", 567 | "version": "==7.3.1" 568 | }, 569 | "python-dateutil": { 570 | "hashes": [ 571 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 572 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 573 | ], 574 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 575 | "version": "==2.8.2" 576 | }, 577 | "pyzmq": { 578 | "hashes": [ 579 | "sha256:01f06f33e12497dca86353c354461f75275a5ad9eaea181ac0dc1662da8074fa", 580 | "sha256:0b6b42f7055bbc562f63f3df3b63e3dd1ebe9727ff0f124c3aa7bcea7b3a00f9", 581 | "sha256:0c4fc2741e0513b5d5a12fe200d6785bbcc621f6f2278893a9ca7bed7f2efb7d", 582 | "sha256:108c96ebbd573d929740d66e4c3d1bdf31d5cde003b8dc7811a3c8c5b0fc173b", 583 | "sha256:13bbe36da3f8aaf2b7ec12696253c0bf6ffe05f4507985a8844a1081db6ec22d", 584 | "sha256:154bddda2a351161474b36dba03bf1463377ec226a13458725183e508840df89", 585 | "sha256:19d0383b1f18411d137d891cab567de9afa609b214de68b86e20173dc624c101", 586 | "sha256:1a6169e69034eaa06823da6a93a7739ff38716142b3596c180363dee729d713d", 587 | "sha256:1fc56a0221bdf67cfa94ef2d6ce5513a3d209c3dfd21fed4d4e87eca1822e3a3", 588 | "sha256:2a21fec5c3cea45421a19ccbe6250c82f97af4175bc09de4d6dd78fb0cb4c200", 589 | "sha256:2b15247c49d8cbea695b321ae5478d47cffd496a2ec5ef47131a9e79ddd7e46c", 590 | "sha256:2f5efcc29056dfe95e9c9db0dfbb12b62db9c4ad302f812931b6d21dd04a9119", 591 | "sha256:2f666ae327a6899ff560d741681fdcdf4506f990595201ed39b44278c471ad98", 592 | "sha256:332616f95eb400492103ab9d542b69d5f0ff628b23129a4bc0a2fd48da6e4e0b", 593 | "sha256:33d5c8391a34d56224bccf74f458d82fc6e24b3213fc68165c98b708c7a69325", 594 | "sha256:3575699d7fd7c9b2108bc1c6128641a9a825a58577775ada26c02eb29e09c517", 595 | "sha256:3830be8826639d801de9053cf86350ed6742c4321ba4236e4b5568528d7bfed7", 596 | "sha256:3a522510e3434e12aff80187144c6df556bb06fe6b9d01b2ecfbd2b5bfa5c60c", 597 | "sha256:3bed53f7218490c68f0e82a29c92335daa9606216e51c64f37b48eb78f1281f4", 598 | "sha256:414b8beec76521358b49170db7b9967d6974bdfc3297f47f7d23edec37329b00", 599 | "sha256:442d3efc77ca4d35bee3547a8e08e8d4bb88dadb54a8377014938ba98d2e074a", 600 | "sha256:47b915ba666c51391836d7ed9a745926b22c434efa76c119f77bcffa64d2c50c", 601 | "sha256:48e5e59e77c1a83162ab3c163fc01cd2eebc5b34560341a67421b09be0891287", 602 | "sha256:4a82faae00d1eed4809c2f18b37f15ce39a10a1c58fe48b60ad02875d6e13d80", 603 | "sha256:4a983c8694667fd76d793ada77fd36c8317e76aa66eec75be2653cef2ea72883", 604 | "sha256:4c2fc7aad520a97d64ffc98190fce6b64152bde57a10c704b337082679e74f67", 605 | "sha256:4cb27ef9d3bdc0c195b2dc54fcb8720e18b741624686a81942e14c8b67cc61a6", 606 | "sha256:4d67609b37204acad3d566bb7391e0ecc25ef8bae22ff72ebe2ad7ffb7847158", 607 | "sha256:5482f08d2c3c42b920e8771ae8932fbaa0a67dff925fc476996ddd8155a170f3", 608 | "sha256:5489738a692bc7ee9a0a7765979c8a572520d616d12d949eaffc6e061b82b4d1", 609 | "sha256:5693dcc4f163481cf79e98cf2d7995c60e43809e325b77a7748d8024b1b7bcba", 610 | "sha256:58416db767787aedbfd57116714aad6c9ce57215ffa1c3758a52403f7c68cff5", 611 | "sha256:5873d6a60b778848ce23b6c0ac26c39e48969823882f607516b91fb323ce80e5", 612 | "sha256:5af31493663cf76dd36b00dafbc839e83bbca8a0662931e11816d75f36155897", 613 | "sha256:5e7fbcafa3ea16d1de1f213c226005fea21ee16ed56134b75b2dede5a2129e62", 614 | "sha256:65346f507a815a731092421d0d7d60ed551a80d9b75e8b684307d435a5597425", 615 | "sha256:6581e886aec3135964a302a0f5eb68f964869b9efd1dbafdebceaaf2934f8a68", 616 | "sha256:69511d604368f3dc58d4be1b0bad99b61ee92b44afe1cd9b7bd8c5e34ea8248a", 617 | "sha256:7018289b402ebf2b2c06992813523de61d4ce17bd514c4339d8f27a6f6809492", 618 | "sha256:71c7b5896e40720d30cd77a81e62b433b981005bbff0cb2f739e0f8d059b5d99", 619 | "sha256:75217e83faea9edbc29516fc90c817bc40c6b21a5771ecb53e868e45594826b0", 620 | "sha256:7e23a8c3b6c06de40bdb9e06288180d630b562db8ac199e8cc535af81f90e64b", 621 | "sha256:80c41023465d36280e801564a69cbfce8ae85ff79b080e1913f6e90481fb8957", 622 | "sha256:831ba20b660b39e39e5ac8603e8193f8fce1ee03a42c84ade89c36a251449d80", 623 | "sha256:851fb2fe14036cfc1960d806628b80276af5424db09fe5c91c726890c8e6d943", 624 | "sha256:8751f9c1442624da391bbd92bd4b072def6d7702a9390e4479f45c182392ff78", 625 | "sha256:8b45d722046fea5a5694cba5d86f21f78f0052b40a4bbbbf60128ac55bfcc7b6", 626 | "sha256:8b697774ea8273e3c0460cf0bba16cd85ca6c46dfe8b303211816d68c492e132", 627 | "sha256:90146ab578931e0e2826ee39d0c948d0ea72734378f1898939d18bc9c823fcf9", 628 | "sha256:9301cf1d7fc1ddf668d0abbe3e227fc9ab15bc036a31c247276012abb921b5ff", 629 | "sha256:95bd3a998d8c68b76679f6b18f520904af5204f089beebb7b0301d97704634dd", 630 | "sha256:968b0c737797c1809ec602e082cb63e9824ff2329275336bb88bd71591e94a90", 631 | "sha256:97d984b1b2f574bc1bb58296d3c0b64b10e95e7026f8716ed6c0b86d4679843f", 632 | "sha256:9e68ae9864d260b18f311b68d29134d8776d82e7f5d75ce898b40a88df9db30f", 633 | "sha256:adecf6d02b1beab8d7c04bc36f22bb0e4c65a35eb0b4750b91693631d4081c70", 634 | "sha256:af56229ea6527a849ac9fb154a059d7e32e77a8cba27e3e62a1e38d8808cb1a5", 635 | "sha256:b324fa769577fc2c8f5efcd429cef5acbc17d63fe15ed16d6dcbac2c5eb00849", 636 | "sha256:b5a07c4f29bf7cb0164664ef87e4aa25435dcc1f818d29842118b0ac1eb8e2b5", 637 | "sha256:bad172aba822444b32eae54c2d5ab18cd7dee9814fd5c7ed026603b8cae2d05f", 638 | "sha256:bdca18b94c404af6ae5533cd1bc310c4931f7ac97c148bbfd2cd4bdd62b96253", 639 | "sha256:be24a5867b8e3b9dd5c241de359a9a5217698ff616ac2daa47713ba2ebe30ad1", 640 | "sha256:be86a26415a8b6af02cd8d782e3a9ae3872140a057f1cadf0133de685185c02b", 641 | "sha256:c66b7ff2527e18554030319b1376d81560ca0742c6e0b17ff1ee96624a5f1afd", 642 | "sha256:c8398a1b1951aaa330269c35335ae69744be166e67e0ebd9869bdc09426f3871", 643 | "sha256:cad9545f5801a125f162d09ec9b724b7ad9b6440151b89645241d0120e119dcc", 644 | "sha256:cb6d161ae94fb35bb518b74bb06b7293299c15ba3bc099dccd6a5b7ae589aee3", 645 | "sha256:d40682ac60b2a613d36d8d3a0cd14fbdf8e7e0618fbb40aa9fa7b796c9081584", 646 | "sha256:d6128d431b8dfa888bf51c22a04d48bcb3d64431caf02b3cb943269f17fd2994", 647 | "sha256:dbc466744a2db4b7ca05589f21ae1a35066afada2f803f92369f5877c100ef62", 648 | "sha256:ddbef8b53cd16467fdbfa92a712eae46dd066aa19780681a2ce266e88fbc7165", 649 | "sha256:e21cc00e4debe8f54c3ed7b9fcca540f46eee12762a9fa56feb8512fd9057161", 650 | "sha256:eb52e826d16c09ef87132c6e360e1879c984f19a4f62d8a935345deac43f3c12", 651 | "sha256:f0d9e7ba6a815a12c8575ba7887da4b72483e4cfc57179af10c9b937f3f9308f", 652 | "sha256:f1e931d9a92f628858a50f5bdffdfcf839aebe388b82f9d2ccd5d22a38a789dc", 653 | "sha256:f45808eda8b1d71308c5416ef3abe958f033fdbb356984fabbfc7887bed76b3f", 654 | "sha256:f6d39e42a0aa888122d1beb8ec0d4ddfb6c6b45aecb5ba4013c27e2f28657765", 655 | "sha256:fc34fdd458ff77a2a00e3c86f899911f6f269d393ca5675842a6e92eea565bae" 656 | ], 657 | "markers": "python_version >= '3.6'", 658 | "version": "==25.1.0" 659 | }, 660 | "six": { 661 | "hashes": [ 662 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 663 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 664 | ], 665 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 666 | "version": "==1.16.0" 667 | }, 668 | "stack-data": { 669 | "hashes": [ 670 | "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815", 671 | "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8" 672 | ], 673 | "version": "==0.6.2" 674 | }, 675 | "tomlkit": { 676 | "hashes": [ 677 | "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171", 678 | "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3" 679 | ], 680 | "markers": "python_version >= '3.7'", 681 | "version": "==0.11.8" 682 | }, 683 | "tornado": { 684 | "hashes": [ 685 | "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4", 686 | "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf", 687 | "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d", 688 | "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba", 689 | "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe", 690 | "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411", 691 | "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2", 692 | "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0", 693 | "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c", 694 | "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f", 695 | "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829" 696 | ], 697 | "markers": "python_version >= '3.8'", 698 | "version": "==6.3.2" 699 | }, 700 | "traitlets": { 701 | "hashes": [ 702 | "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", 703 | "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9" 704 | ], 705 | "markers": "python_version >= '3.7'", 706 | "version": "==5.9.0" 707 | }, 708 | "typing-extensions": { 709 | "hashes": [ 710 | "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", 711 | "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" 712 | ], 713 | "markers": "python_version >= '3.7'", 714 | "version": "==4.6.3" 715 | }, 716 | "wcwidth": { 717 | "hashes": [ 718 | "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", 719 | "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" 720 | ], 721 | "version": "==0.2.6" 722 | }, 723 | "wrapt": { 724 | "hashes": [ 725 | "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", 726 | "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", 727 | "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", 728 | "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", 729 | "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", 730 | "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", 731 | "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", 732 | "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", 733 | "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", 734 | "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", 735 | "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", 736 | "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", 737 | "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", 738 | "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", 739 | "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", 740 | "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", 741 | "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", 742 | "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", 743 | "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", 744 | "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", 745 | "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", 746 | "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", 747 | "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", 748 | "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", 749 | "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", 750 | "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", 751 | "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", 752 | "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", 753 | "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", 754 | "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", 755 | "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", 756 | "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", 757 | "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", 758 | "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", 759 | "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", 760 | "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", 761 | "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", 762 | "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", 763 | "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", 764 | "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", 765 | "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", 766 | "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", 767 | "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", 768 | "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", 769 | "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", 770 | "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", 771 | "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", 772 | "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", 773 | "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", 774 | "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", 775 | "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", 776 | "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", 777 | "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", 778 | "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", 779 | "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", 780 | "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", 781 | "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", 782 | "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", 783 | "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", 784 | "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", 785 | "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", 786 | "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", 787 | "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", 788 | "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", 789 | "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", 790 | "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", 791 | "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", 792 | "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", 793 | "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", 794 | "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", 795 | "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", 796 | "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", 797 | "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", 798 | "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", 799 | "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" 800 | ], 801 | "markers": "python_version >= '3.11'", 802 | "version": "==1.15.0" 803 | } 804 | } 805 | } 806 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TFHE-py 2 | 3 | Python implementation of the [Fully Homomorphic Encryption (FHE)](https://en.wikipedia.org/wiki/Homomorphic_encryption#Fully_homomorphic_encryption) scheme [TFHE: Fast Fully Homomorphic Encryption over the Torus](https://eprint.iacr.org/2018/421.pdf). 4 | 5 | You can see example usages in the [Jupyter Notebook](./main.ipynb) and the [tests](./tests/) directory. 6 | 7 | The starting point of this implementation was the code written by [NuCypher](https://www.nucypher.com) for their [NuFHE](https://github.com/nucypher/nufhe) library. More specifically the code in commit [17f3b62](https://github.com/nucypher/nufhe/commit/17f3b6200425a42b84ff844928550e9add684280) was used which itself seems to be a port of their Julia version [nucypher/TFHE.jl](https://github.com/nucypher/TFHE.jl) which looks like a port (see [this commit](https://github.com/nucypher/TFHE.jl/commit/bf33742310a369c6da133593cbbefd75374bbefb)) of the original [tfhe/tfhe](https://github.com/tfhe/tfhe) codebase that's written in C / C++. 8 | 9 | ## Setup 10 | 11 | 1. `git clone ` 12 | 2. `asdf install` 13 | 3. `pipenv install -e .` 14 | 4. `pipenv shell` 15 | 5. `python tests/e2e_half_adder_test.py` 16 | 6. `pipenv run test` 17 | 7. `pipenv run coverage` 18 | 8. `pipenv scripts` 19 | 20 | _Optional_: Update the properties marked with `TODO:` in the [`.vscode/settings.json`](./.vscode/settings.json) file. To get the correct paths run `which ` within a Pipenv shell. 21 | 22 | ## Useful Commands 23 | 24 | ```sh 25 | asdf install 26 | 27 | pipenv install -e . 28 | pipenv install [-d] [~=] 29 | pipenv shell 30 | pipenv scripts 31 | pipenv run 32 | 33 | python 34 | 35 | flake8 36 | 37 | pylint --recursive true 38 | 39 | mypy 40 | 41 | pytest [-s] [-v] [-k ] [] 42 | 43 | coverage html 44 | coverage report -m 45 | 46 | py-spy record -o profile.svg --pid 47 | py-spy record -o profile.svg -- python 48 | py-spy top -- python 49 | ``` 50 | 51 | ## Useful Resources 52 | 53 | ### (T)FHE 54 | 55 | - [tfhe/tfhe](https://github.com/tfhe/tfhe) 56 | - [nucypher/nufhe](https://github.com/nucypher/nufhe) 57 | - [zama-ai/tfhe-rs](https://github.com/zama-ai/tfhe-rs) 58 | - [nucypher/TFHE.jl](https://github.com/nucypher/TFHE.jl) 59 | - [thedonutfactory/go-tfhe](https://github.com/thedonutfactory/go-tfhe) 60 | - [thedonutfactory/rs_tfhe](https://github.com/thedonutfactory/rs_tfhe) 61 | - [virtualsecureplatform/pyFHE](https://github.com/virtualsecureplatform/pyFHE) 62 | - [openfheorg/openfhe-development](https://github.com/openfheorg/openfhe-development) 63 | - [TFHE: Fast Fully Homomorphic Encryption over the Torus](https://eprint.iacr.org/2018/421) 64 | - [Guide to Fully Homomorphic Encryption over the [Discretized] Torus](https://eprint.iacr.org/2021/1402) 65 | - [SoK: Fully Homomorphic Encryption over the [Discretized] Torus](https://tches.iacr.org/index.php/TCHES/article/view/9836) 66 | - [TFHE Deep Dive - Part I - Ciphertext types](https://www.zama.ai/post/tfhe-deep-dive-part-1) 67 | - [TFHE Deep Dive - Part II - Encodings and linear leveled operations](https://www.zama.ai/post/tfhe-deep-dive-part-2) 68 | - [TFHE Deep Dive - Part III - Key switching and leveled multiplications](https://www.zama.ai/post/tfhe-deep-dive-part-3) 69 | - [TFHE Deep Dive - Part IV - Programmable Bootstrapping](https://www.zama.ai/post/tfhe-deep-dive-part-4) 70 | - [Introduction to practical FHE and the TFHE scheme - Ilaria Chillotti, Simons Institute 2020](https://www.youtube.com/watch?v=FFox2S4uqEo) 71 | - [TFHE Deep Dive - Ilaria Chillotti, FHE.org](https://www.youtube.com/watch?v=LZuEr4jpyUw) 72 | - [003 TFHE Deep Dive (by Ilaria Chillotti)](https://www.youtube.com/watch?v=npoHSR6-oRw) 73 | - [Part 1 Introduction to FHE and the TFHE scheme - Ilaria Chillotti, ICMS](https://www.youtube.com/watch?v=e_76kZ9j2-M) 74 | - [Part 2 Introduction to FHE and the TFHE Scheme - Ilaria Chillotti, ICMS](https://www.youtube.com/watch?v=o7_WNbVuZqQ) 75 | - [Introduction to FHE (Fully Homomorphic Encryption) - Pascal Paillier, FHE.org Meetup](https://www.youtube.com/watch?v=aruz58RarVA) 76 | 77 | ### Python 78 | 79 | - [Real Python](https://realpython.com) 80 | - [Python Cheatsheet](https://www.pythoncheatsheet.org) 81 | - [Learn X in Y minutes](https://learnxinyminutes.com/docs/python) 82 | - [TheAlgorithms/Python](https://github.com/TheAlgorithms/Python) 83 | - [gto76/python-cheatsheet](https://github.com/gto76/python-cheatsheet) 84 | - [Writing Python like it's Rust](https://kobzol.github.io/rust/python/2023/05/20/writing-python-like-its-rust.html) 85 | -------------------------------------------------------------------------------- /main.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# TFHE-py\n", 9 | "\n", 10 | "Python implementation of the [Fully Homomorphic Encryption (FHE)](https://en.wikipedia.org/wiki/Homomorphic_encryption#Fully_homomorphic_encryption) scheme [TFHE: Fast Fully Homomorphic Encryption over the Torus](https://eprint.iacr.org/2018/421.pdf)." 11 | ] 12 | }, 13 | { 14 | "attachments": {}, 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## NAND\n", 19 | "\n", 20 | "The following demo shows an evaluation of a [NAND gate](https://en.wikipedia.org/wiki/NAND_gate) using two encrypted bit arrays as input." 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import random\n", 30 | "import warnings\n", 31 | "\n", 32 | "import numpy\n", 33 | "\n", 34 | "from tfhe.boot_gates import NAND\n", 35 | "from tfhe.keys import tfhe_key_pair, tfhe_parameters, tfhe_encrypt, tfhe_decrypt, empty_ciphertext" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "# Ignores overflow detected by Numpy in `lwe_key_switch_translate_from_array` method.\n", 45 | "warnings.filterwarnings(\"ignore\", \"overflow encountered in scalar subtract\")" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "# Seed the random number generator.\n", 55 | "rng = numpy.random.RandomState(123)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 4, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "name": "stdout", 65 | "output_type": "stream", 66 | "text": [ 67 | "Expected Bits: [False False False True False True True True]\n" 68 | ] 69 | } 70 | ], 71 | "source": [ 72 | "size = 8\n", 73 | "\n", 74 | "bits1 = numpy.array([random.choice([False, True]) for _ in range(size)])\n", 75 | "bits2 = numpy.array([random.choice([False, True]) for _ in range(size)])\n", 76 | "expected_bits = numpy.array([not (b1 and b2) for b1, b2 in zip(bits1, bits2)])\n", 77 | "\n", 78 | "print(f\"Expected Bits: {expected_bits}\")" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 5, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "Secret Key: [0 1 0 0 0 0 0 1 1 0 1 1 0 1 0 1 0 1 1 0 0 0 1 1 1 0 1 0 0 0 0 1 1 1 0 0 1\n", 91 | " 0 0 1 0 1 0 1 1 1 0 0 0 0 1 1 0 0 1 0 1 0 0 1 0 1 0 1 0 0 0 0 1 0 0 1 1 1\n", 92 | " 1 0 0 0 0 1 1 0 0 0 0 1 0 1 1 0 1 1 1 0 1 1 1 0 0 0 1 0 0 1 1 1 0 1 1 0 0\n", 93 | " 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 1 0 0 1 1 0 0 0 1 0 1 1 0 0 1 0 1 1\n", 94 | " 1 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 1 1 1 0 0 1 1 0 0 1 0 0 0 1 0 1 1 0 0 0 1\n", 95 | " 0 0 1 0 0 1 0 0 1 0 0 0 0 1 0 1 1 0 0 0 1 0 0 0 1 0 0 0 1 0 1 1 1 1 0 1 1\n", 96 | " 1 1 0 0 1 1 1 1 1 0 0 1 1 1 0 1 0 0 0 1 1 1 0 1 0 0 1 1 1 0 1 1 0 0 0 1 0\n", 97 | " 1 1 0 0 1 1 1 1 1 0 1 1 1 0 1 1 0 0 0 1 1 0 0 1 0 0 0 1 0 1 1 1 0 1 1 1 0\n", 98 | " 0 0 0 1 0 1 1 1 1 0 1 1 1 1 1 1 0 0 1 0 0 1 0 1 1 1 1 1 1 0 1 1 1 0 0 0 1\n", 99 | " 1 0 1 0 0 1 0 1 1 0 0 0 0 1 1 1 1 1 0 1 0 1 1 1 0 0 1 0 0 1 0 0 0 0 0 1 1\n", 100 | " 0 1 0 0 0 1 1 0 1 0 1 0 1 1 0 0 0 1 0 1 1 0 0 1 0 0 1 1 0 0 1 1 0 1 0 1 1\n", 101 | " 1 0 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 0 0 1 0 0 0 0 1 0 0\n", 102 | " 0 0 1 0 1 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 1 0 1 0 0 0 0 1 1 1 0 0 0 1 1 1 0\n", 103 | " 0 0 0 1 0 1 1 0 1 0 0 1 0 0 0 1 0 0 0]\n" 104 | ] 105 | } 106 | ], 107 | "source": [ 108 | "secret_key, cloud_key = tfhe_key_pair(rng)\n", 109 | "\n", 110 | "print(f\"Secret Key: {secret_key.lwe_key.key}\")" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 6, 116 | "metadata": {}, 117 | "outputs": [ 118 | { 119 | "name": "stdout", 120 | "output_type": "stream", 121 | "text": [ 122 | "Ciphertext #1 - A:\n", 123 | "[[ -840855051 -1136742550 -809565389 ... -534280838 943465747\n", 124 | " -140371691]\n", 125 | " [ -721127977 1889330684 1500427088 ... -1580204609 1445239680\n", 126 | " -1596179244]\n", 127 | " [ 653050139 283965477 2093932157 ... 106297957 -761437438\n", 128 | " 666920104]\n", 129 | " ...\n", 130 | " [ 1953068548 -2114867344 -125975373 ... 92647407 2062394814\n", 131 | " -456800007]\n", 132 | " [ 1168962072 270010001 -1479199671 ... -1809719243 1566491449\n", 133 | " 63757602]\n", 134 | " [ -494803323 -49907707 -1700657286 ... -354153220 1082467922\n", 135 | " -1106648905]]\n", 136 | "\n", 137 | "Ciphertext #1 - B:\n", 138 | "[ -804448352 1601156578 -1166297905 1190138558 -1794158149 510443285\n", 139 | " -1276372954 2069967464]\n" 140 | ] 141 | } 142 | ], 143 | "source": [ 144 | "ciphertext1 = tfhe_encrypt(rng, secret_key, bits1)\n", 145 | "ciphertext2 = tfhe_encrypt(rng, secret_key, bits2)\n", 146 | "\n", 147 | "print(\"Ciphertext #1 - A:\")\n", 148 | "print(ciphertext1.a)\n", 149 | "print()\n", 150 | "print(\"Ciphertext #1 - B:\")\n", 151 | "print(ciphertext1.b)" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 7, 157 | "metadata": {}, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "Result - A:\n", 164 | "[[ 0 0 211156755 ... -1558901600 20326465\n", 165 | " -500811347]\n", 166 | " [ -249946643 1922639180 514040244 ... 0 402703712\n", 167 | " 1]\n", 168 | " [ 1130 0 0 ... 19 1161\n", 169 | " 1]\n", 170 | " ...\n", 171 | " [ 402709720 1 24 ... 1 80232992\n", 172 | " 1]\n", 173 | " [ 223328944 1 72253104 ... 7 0\n", 174 | " 5]\n", 175 | " [ 166 0 0 ... 1 122540848\n", 176 | " 1]]\n", 177 | "\n", 178 | "Result - B:\n", 179 | "[ 536870912 536870912 536870912 -536870912 536870912 -536870912\n", 180 | " -536870912 -536870912]\n" 181 | ] 182 | } 183 | ], 184 | "source": [ 185 | "params = tfhe_parameters(cloud_key)\n", 186 | "\n", 187 | "result = empty_ciphertext(params, ciphertext1.shape)\n", 188 | "\n", 189 | "print(\"Result - A:\")\n", 190 | "print(result.a)\n", 191 | "print()\n", 192 | "print(\"Result - B:\")\n", 193 | "print(result.b)" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 8, 199 | "metadata": {}, 200 | "outputs": [ 201 | { 202 | "name": "stdout", 203 | "output_type": "stream", 204 | "text": [ 205 | "Answer Bits: [False False False True False True True True]\n" 206 | ] 207 | } 208 | ], 209 | "source": [ 210 | "NAND(cloud_key, result, ciphertext1, ciphertext2)\n", 211 | "\n", 212 | "answer_bits = tfhe_decrypt(secret_key, result)\n", 213 | "\n", 214 | "print(f\"Answer Bits: {answer_bits}\")" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 9, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "assert (answer_bits == expected_bits).all()" 224 | ] 225 | }, 226 | { 227 | "attachments": {}, 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Check out the project's [README.md](./README.md) file for more information." 232 | ] 233 | } 234 | ], 235 | "metadata": { 236 | "kernelspec": { 237 | "display_name": ".venv", 238 | "language": "python", 239 | "name": "python3" 240 | }, 241 | "language_info": { 242 | "codemirror_mode": { 243 | "name": "ipython", 244 | "version": 3 245 | }, 246 | "file_extension": ".py", 247 | "mimetype": "text/x-python", 248 | "name": "python", 249 | "nbconvert_exporter": "python", 250 | "pygments_lexer": "ipython3", 251 | "version": "3.11.3" 252 | }, 253 | "orig_nbformat": 4 254 | }, 255 | "nbformat": 4, 256 | "nbformat_minor": 2 257 | } 258 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="tfhe-py", 5 | version="0.1.0", 6 | description='A Python of "Fully Homomorphic Encryption over the Torus"', 7 | url="http://github.com/pmuens/tfhe-py", 8 | author="Philipp Muens, Bogdan Opanchuk", 9 | author_email="philipp@muens.io, bogdan@nucypher.com", 10 | license="MIT", 11 | packages=["tfhe"], 12 | install_requires=["numpy>=1.24.3"], 13 | zip_safe=True, 14 | ) 15 | -------------------------------------------------------------------------------- /tests/e2e_half_adder_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | 3 | import time 4 | import warnings 5 | from typing import Tuple 6 | 7 | import numpy 8 | 9 | from tfhe.boot_gates import AND, XOR 10 | from tfhe.keys import ( 11 | empty_ciphertext, 12 | tfhe_decrypt, 13 | tfhe_encrypt, 14 | tfhe_key_pair, 15 | tfhe_parameters, 16 | ) 17 | 18 | rng = numpy.random.RandomState(123) 19 | 20 | # See: https://en.wikipedia.org/wiki/Adder_(electronics)#Half_adder 21 | EXPECTED = [ 22 | { 23 | "bits": [False, False], # A = 0 B = 0 24 | "result": [False, False], # Carry = 0 Sum = 0 25 | }, 26 | { 27 | "bits": [False, True], # A = 0 B = 1 28 | "result": [False, True], # Carry = 0 Sum = 1 29 | }, 30 | { 31 | "bits": [True, False], # A = 1 B = 0 32 | "result": [False, True], # Carry = 0 Sum = 1 33 | }, 34 | { 35 | "bits": [True, True], # A = 1 B = 1 36 | "result": [True, False], # Carry = 1 Sum = 0 37 | }, 38 | ] 39 | 40 | 41 | def test() -> None: 42 | for _, item in enumerate(EXPECTED): 43 | [bit1, bit2] = item["bits"] 44 | [expected_carry, expected_sum] = item["result"] 45 | 46 | [computed_carry, computed_sum] = run(bit1, bit2) 47 | 48 | assert computed_carry == expected_carry 49 | assert computed_sum == expected_sum 50 | 51 | 52 | def run(bit1: bool, bit2: bool) -> Tuple[bool, bool]: 53 | secret_key, cloud_key = tfhe_key_pair(rng) 54 | 55 | ciphertext1 = tfhe_encrypt(rng, secret_key, numpy.array([bit1])) 56 | ciphertext2 = tfhe_encrypt(rng, secret_key, numpy.array([bit2])) 57 | 58 | params = tfhe_parameters(cloud_key) 59 | 60 | shape = ciphertext1.shape 61 | result_carry = empty_ciphertext(params, shape) 62 | result_sum = empty_ciphertext(params, shape) 63 | 64 | AND(cloud_key, result_carry, ciphertext1, ciphertext2) 65 | XOR(cloud_key, result_sum, ciphertext1, ciphertext2) 66 | 67 | answer_bits_carry = tfhe_decrypt(secret_key, result_carry) 68 | answer_bits_sum = tfhe_decrypt(secret_key, result_sum) 69 | 70 | return answer_bits_carry[0], answer_bits_sum[0] 71 | 72 | 73 | if __name__ == "__main__": 74 | # FIXME: Ignores overflow detected by Numpy in # pylint: disable=fixme 75 | # `lwe_key_switch_translate_from_array` method. 76 | warnings.filterwarnings("ignore", "overflow encountered in scalar subtract") 77 | 78 | total = 0.0 79 | for _, item in enumerate(EXPECTED): 80 | [bit1, bit2] = item["bits"] 81 | [expected_carry, expected_sum] = item["result"] 82 | print(f"Expected:\t Carry -> {expected_carry} Sum -> {expected_sum}") 83 | 84 | t = time.time() 85 | [computed_carry, computed_sum] = run(bit1, bit2) 86 | print(f"Result:\t\t Carry -> {computed_carry} Sum -> {computed_sum}") 87 | elapsed = time.time() - t 88 | print(f"Time:\t\t {elapsed} seconds") 89 | total += elapsed 90 | print() 91 | 92 | print(f"Avg. Time:\t {total / len(EXPECTED)} seconds") 93 | -------------------------------------------------------------------------------- /tests/e2e_minimum_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import warnings 3 | 4 | import numpy 5 | 6 | from tfhe.boot_gates import CONSTANT, MUX, XNOR 7 | from tfhe.keys import ( 8 | TFHECloudKey, 9 | empty_ciphertext, 10 | tfhe_decrypt, 11 | tfhe_encrypt, 12 | tfhe_key_pair, 13 | tfhe_parameters, 14 | ) 15 | from tfhe.lwe import LWESampleArray 16 | from tfhe.utils import bitarray_to_int, int_to_bitarray 17 | 18 | rng = numpy.random.RandomState(123) 19 | 20 | EXPECTED = 42 21 | 22 | 23 | def test() -> None: 24 | assert run() == EXPECTED 25 | 26 | 27 | def run() -> int: 28 | secret_key, cloud_key = tfhe_key_pair(rng) 29 | 30 | bits42 = int_to_bitarray(42) 31 | bits4711 = int_to_bitarray(4711) 32 | 33 | ciphertext42 = tfhe_encrypt(rng, secret_key, bits42) 34 | ciphertext4711 = tfhe_encrypt(rng, secret_key, bits4711) 35 | 36 | result = encrypted_minimum(cloud_key, ciphertext42, ciphertext4711) 37 | 38 | answer_bits = tfhe_decrypt(secret_key, result) 39 | answer_int = bitarray_to_int(answer_bits) 40 | 41 | return answer_int 42 | 43 | 44 | def encrypted_minimum( 45 | cloud_key: TFHECloudKey, 46 | a: LWESampleArray, 47 | b: LWESampleArray, 48 | ) -> LWESampleArray: 49 | params = tfhe_parameters(cloud_key) 50 | 51 | shape = a.shape 52 | result = empty_ciphertext(params, shape) 53 | 54 | nb_bits = result.shape[0] 55 | 56 | params = tfhe_parameters(cloud_key) 57 | 58 | tmp1 = empty_ciphertext(params, (1,)) 59 | tmp2 = empty_ciphertext(params, (1,)) 60 | 61 | # Initialize the carry to 0. 62 | CONSTANT(tmp1, False) 63 | 64 | # Run the elementary comparator gate `n` times. 65 | for i in range(nb_bits): 66 | tmp_a = a[i : i + 1] # type: ignore 67 | tmp_b = b[i : i + 1] # type: ignore 68 | XNOR(cloud_key, tmp2, tmp_a, tmp_b) 69 | MUX(cloud_key, tmp1, tmp2, tmp1, tmp_a) 70 | 71 | # `tmp1` is the result of the comparison: 72 | # - 0 if `a` is larger 73 | # - 1 if `b` is larger 74 | # Select the max and copy it to the `result`. 75 | MUX(cloud_key, result, tmp1, b, a) 76 | 77 | return result 78 | 79 | 80 | if __name__ == "__main__": 81 | # FIXME: Ignores overflow detected by Numpy in # pylint: disable=fixme 82 | # `lwe_key_switch_translate_from_array` method. 83 | warnings.filterwarnings("ignore", "overflow encountered in scalar subtract") 84 | 85 | print(f"Expected:\t {EXPECTED}") 86 | t = time.time() 87 | res = run() 88 | print(f"Result:\t\t {res}") 89 | print(f"Time:\t\t {time.time() - t} seconds") 90 | -------------------------------------------------------------------------------- /tests/e2e_mux_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import warnings 3 | 4 | import numpy 5 | 6 | from tfhe.boot_gates import MUX 7 | from tfhe.keys import ( 8 | empty_ciphertext, 9 | tfhe_decrypt, 10 | tfhe_encrypt, 11 | tfhe_key_pair, 12 | tfhe_parameters, 13 | ) 14 | from tfhe.utils import bitarray_to_int, int_to_bitarray 15 | 16 | rng = numpy.random.RandomState(123) 17 | 18 | EXPECTED = 12345 19 | 20 | 21 | def test() -> None: 22 | assert run() == EXPECTED 23 | 24 | 25 | def run() -> int: 26 | secret_key, cloud_key = tfhe_key_pair(rng) 27 | 28 | bits2020 = int_to_bitarray(2020) 29 | bits42 = int_to_bitarray(42) 30 | bits12345 = int_to_bitarray(12345) 31 | 32 | ciphertext2020 = tfhe_encrypt(rng, secret_key, bits2020) 33 | ciphertext42 = tfhe_encrypt(rng, secret_key, bits42) 34 | ciphertext12345 = tfhe_encrypt(rng, secret_key, bits12345) 35 | 36 | params = tfhe_parameters(cloud_key) 37 | result = empty_ciphertext(params, ciphertext2020.shape) 38 | 39 | MUX(cloud_key, result, ciphertext2020, ciphertext42, ciphertext12345) 40 | 41 | answer_bits = tfhe_decrypt(secret_key, result) 42 | answer_int = bitarray_to_int(answer_bits) 43 | 44 | return answer_int 45 | 46 | 47 | if __name__ == "__main__": 48 | # FIXME: Ignores overflow detected by Numpy in # pylint: disable=fixme 49 | # `lwe_key_switch_translate_from_array` method. 50 | warnings.filterwarnings("ignore", "overflow encountered in scalar subtract") 51 | 52 | print(f"Expected:\t {EXPECTED}") 53 | t = time.time() 54 | res = run() 55 | print(f"Result:\t\t {res}") 56 | print(f"Time:\t\t {time.time() - t} seconds") 57 | -------------------------------------------------------------------------------- /tests/e2e_nand_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import warnings 4 | 5 | import numpy 6 | from numpy.typing import NDArray 7 | 8 | from tfhe.boot_gates import NAND 9 | from tfhe.keys import ( 10 | empty_ciphertext, 11 | tfhe_decrypt, 12 | tfhe_encrypt, 13 | tfhe_key_pair, 14 | tfhe_parameters, 15 | ) 16 | 17 | rng = numpy.random.RandomState(123) 18 | 19 | size = 8 20 | bits1 = numpy.array([random.choice([False, True]) for i in range(size)]) 21 | bits2 = numpy.array([random.choice([False, True]) for i in range(size)]) 22 | EXPECTED = numpy.array([not (b1 and b2) for b1, b2 in zip(bits1, bits2)]) 23 | 24 | 25 | def test() -> None: 26 | assert (run() == EXPECTED).all() 27 | 28 | 29 | def run() -> NDArray[numpy.bool_]: 30 | secret_key, cloud_key = tfhe_key_pair(rng) 31 | 32 | ciphertext1 = tfhe_encrypt(rng, secret_key, bits1) 33 | ciphertext2 = tfhe_encrypt(rng, secret_key, bits2) 34 | 35 | params = tfhe_parameters(cloud_key) 36 | 37 | result = empty_ciphertext(params, ciphertext1.shape) 38 | 39 | NAND(cloud_key, result, ciphertext1, ciphertext2) 40 | 41 | answer_bits = tfhe_decrypt(secret_key, result) 42 | 43 | return answer_bits 44 | 45 | 46 | if __name__ == "__main__": 47 | # FIXME: Ignores overflow detected by Numpy in # pylint: disable=fixme 48 | # `lwe_key_switch_translate_from_array` method. 49 | warnings.filterwarnings("ignore", "overflow encountered in scalar subtract") 50 | 51 | print(f"Expected:\t {EXPECTED}") 52 | t = time.time() 53 | res = run() 54 | print(f"Result:\t\t {res}") 55 | print(f"Time:\t\t {time.time() - t} seconds") 56 | -------------------------------------------------------------------------------- /tfhe/__init__.py: -------------------------------------------------------------------------------- 1 | from .boot_gates import CONSTANT # noqa: F401 2 | from .boot_gates import MUX # noqa: F401 3 | from .boot_gates import XNOR # noqa: F401 4 | from .keys import empty_ciphertext # noqa: F401 5 | from .keys import tfhe_decrypt # noqa: F401 6 | from .keys import tfhe_encrypt # noqa: F401 7 | from .keys import tfhe_key_pair # noqa: F401 8 | from .keys import tfhe_parameters # noqa: F401 9 | -------------------------------------------------------------------------------- /tfhe/boot_gates.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .keys import TFHECloudKey 7 | from .lwe import ( 8 | LWESampleArray, 9 | lwe_add_mul_to, 10 | lwe_add_to, 11 | lwe_copy, 12 | lwe_key_switch, 13 | lwe_negate, 14 | lwe_noiseless_trivial, 15 | lwe_sub_mul_to, 16 | lwe_sub_to, 17 | ) 18 | from .lwe_bootstrapping import tfhe_bootstrap_fft, tfhe_bootstrap_wo_ks_fft 19 | from .numeric_functions import Torus32, mod_switch_to_torus32 20 | 21 | 22 | def NAND( 23 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 24 | ) -> None: 25 | """ 26 | * Homomorphic bootstrapped NAND gate 27 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 28 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 29 | """ 30 | 31 | mu = mod_switch_to_torus32(1, 8) 32 | in_out_params = bk.params.in_out_params 33 | 34 | temp_result = LWESampleArray(in_out_params, result.shape) 35 | 36 | # compute: (0,1/8) - ca - cb 37 | nand_const = mod_switch_to_torus32(1, 8) 38 | lwe_noiseless_trivial(temp_result, nand_const) 39 | lwe_sub_to(temp_result, ca) 40 | lwe_sub_to(temp_result, cb) 41 | 42 | # if the phase is positive, the result is 1/8 43 | # if the phase is positive, else the result is -1/8 44 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 45 | 46 | 47 | def OR( 48 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 49 | ) -> None: 50 | """ 51 | * Homomorphic bootstrapped OR gate 52 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 53 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 54 | """ 55 | 56 | mu = mod_switch_to_torus32(1, 8) 57 | in_out_params = bk.params.in_out_params 58 | 59 | temp_result = LWESampleArray(in_out_params, result.shape) 60 | 61 | # compute: (0,1/8) + ca + cb 62 | or_const = mod_switch_to_torus32(1, 8) 63 | lwe_noiseless_trivial(temp_result, or_const) 64 | lwe_add_to(temp_result, ca) 65 | lwe_add_to(temp_result, cb) 66 | 67 | # if the phase is positive, the result is 1/8 68 | # if the phase is positive, else the result is -1/8 69 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 70 | 71 | 72 | def AND( 73 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 74 | ) -> None: 75 | """ 76 | * Homomorphic bootstrapped AND gate 77 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 78 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 79 | """ 80 | 81 | mu = mod_switch_to_torus32(1, 8) 82 | in_out_params = bk.params.in_out_params 83 | 84 | temp_result = LWESampleArray(in_out_params, result.shape) 85 | 86 | # compute: (0,-1/8) + ca + cb 87 | and_const = mod_switch_to_torus32(-1, 8) 88 | lwe_noiseless_trivial(temp_result, and_const) 89 | lwe_add_to(temp_result, ca) 90 | lwe_add_to(temp_result, cb) 91 | 92 | # if the phase is positive, the result is 1/8 93 | # if the phase is positive, else the result is -1/8 94 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 95 | 96 | 97 | def XOR( 98 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 99 | ) -> None: 100 | """ 101 | * Homomorphic bootstrapped XOR gate 102 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 103 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 104 | """ 105 | 106 | mu = mod_switch_to_torus32(1, 8) 107 | in_out_params = bk.params.in_out_params 108 | 109 | temp_result = LWESampleArray(in_out_params, result.shape) 110 | 111 | # compute: (0,1/4) + 2*(ca + cb) 112 | xor_const = mod_switch_to_torus32(1, 4) 113 | lwe_noiseless_trivial(temp_result, xor_const) 114 | lwe_add_mul_to(temp_result, numpy.int32(2), ca) 115 | lwe_add_mul_to(temp_result, numpy.int32(2), cb) 116 | 117 | # if the phase is positive, the result is 1/8 118 | # if the phase is positive, else the result is -1/8 119 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 120 | 121 | 122 | def XNOR( 123 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 124 | ) -> None: 125 | """ 126 | * Homomorphic bootstrapped XNOR gate 127 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 128 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 129 | """ 130 | 131 | mu = mod_switch_to_torus32(1, 8) 132 | in_out_params = bk.params.in_out_params 133 | 134 | temp_result = LWESampleArray(in_out_params, result.shape) 135 | 136 | # compute: (0,-1/4) + 2*(-ca-cb) 137 | xnor_const = mod_switch_to_torus32(-1, 4) 138 | lwe_noiseless_trivial(temp_result, xnor_const) 139 | lwe_sub_mul_to(temp_result, numpy.int32(2), ca) 140 | lwe_sub_mul_to(temp_result, numpy.int32(2), cb) 141 | 142 | # if the phase is positive, the result is 1/8 143 | # if the phase is positive, else the result is -1/8 144 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 145 | 146 | 147 | def NOT(result: LWESampleArray, ca: LWESampleArray) -> None: 148 | """ 149 | * Homomorphic bootstrapped NOT gate (doesn't need to be bootstrapped) 150 | * Takes in input 1 LWE samples (with message space [-1/8,1/8], noise<1/16) 151 | * Outputs a LWE sample (with message space [-1/8,1/8], noise<1/16) 152 | """ 153 | lwe_negate(result, ca) 154 | 155 | 156 | def COPY(result: LWESampleArray, ca: LWESampleArray) -> None: 157 | """ 158 | * Homomorphic bootstrapped COPY gate (doesn't need to be bootstrapped) 159 | * Takes in input 1 LWE samples (with message space [-1/8,1/8], noise<1/16) 160 | * Outputs a LWE sample (with message space [-1/8,1/8], noise<1/16) 161 | """ 162 | lwe_copy(result, ca) 163 | 164 | 165 | def CONSTANT(result: LWESampleArray, vals: bool | NDArray[numpy.int32]) -> None: 166 | """ 167 | * Homomorphic Trivial Constant gate (doesn't need to be bootstrapped) 168 | * Takes a boolean value) 169 | * Outputs a LWE sample (with message space [-1/8,1/8], noise<1/16) 170 | """ 171 | 172 | mu = mod_switch_to_torus32(1, 8) 173 | if isinstance(vals, numpy.ndarray): 174 | mus = cast(NDArray[Torus32], numpy.array([mu if x else -mu for x in vals])) 175 | else: 176 | mus = cast( 177 | NDArray[Torus32], 178 | numpy.ones(result.shape, numpy.int32) * (mu if vals else -mu), 179 | ) 180 | lwe_noiseless_trivial(result, mus) 181 | 182 | 183 | def NOR( 184 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 185 | ) -> None: 186 | """ 187 | * Homomorphic bootstrapped NOR gate 188 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 189 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 190 | """ 191 | 192 | mu = mod_switch_to_torus32(1, 8) 193 | in_out_params = bk.params.in_out_params 194 | 195 | temp_result = LWESampleArray(in_out_params, result.shape) 196 | 197 | # compute: (0,-1/8) - ca - cb 198 | nor_const = mod_switch_to_torus32(-1, 8) 199 | lwe_noiseless_trivial(temp_result, nor_const) 200 | lwe_sub_to(temp_result, ca) 201 | lwe_sub_to(temp_result, cb) 202 | 203 | # if the phase is positive, the result is 1/8 204 | # if the phase is positive, else the result is -1/8 205 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 206 | 207 | 208 | def ANDNY( 209 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 210 | ) -> None: 211 | """ 212 | * Homomorphic bootstrapped AndNY Gate: not(a) and b 213 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 214 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 215 | """ 216 | 217 | mu = mod_switch_to_torus32(1, 8) 218 | in_out_params = bk.params.in_out_params 219 | 220 | temp_result = LWESampleArray(in_out_params, result.shape) 221 | 222 | # compute: (0,-1/8) - ca + cb 223 | and_n_y_const = mod_switch_to_torus32(-1, 8) 224 | lwe_noiseless_trivial(temp_result, and_n_y_const) 225 | lwe_sub_to(temp_result, ca) 226 | lwe_add_to(temp_result, cb) 227 | 228 | # if the phase is positive, the result is 1/8 229 | # if the phase is positive, else the result is -1/8 230 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 231 | 232 | 233 | def ANDYN( 234 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 235 | ) -> None: 236 | """ 237 | * Homomorphic bootstrapped AndYN Gate: a and not(b) 238 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 239 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 240 | """ 241 | 242 | mu = mod_switch_to_torus32(1, 8) 243 | in_out_params = bk.params.in_out_params 244 | 245 | temp_result = LWESampleArray(in_out_params, result.shape) 246 | 247 | # compute: (0,-1/8) + ca - cb 248 | and_y_n_const = mod_switch_to_torus32(-1, 8) 249 | lwe_noiseless_trivial(temp_result, and_y_n_const) 250 | lwe_add_to(temp_result, ca) 251 | lwe_sub_to(temp_result, cb) 252 | 253 | # if the phase is positive, the result is 1/8 254 | # if the phase is positive, else the result is -1/8 255 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 256 | 257 | 258 | def ORNY( 259 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 260 | ) -> None: 261 | """ 262 | * Homomorphic bootstrapped OrNY Gate: not(a) or b 263 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 264 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 265 | """ 266 | 267 | mu = mod_switch_to_torus32(1, 8) 268 | in_out_params = bk.params.in_out_params 269 | 270 | temp_result = LWESampleArray(in_out_params, result.shape) 271 | 272 | # compute: (0,1/8) - ca + cb 273 | or_n_y_const = mod_switch_to_torus32(1, 8) 274 | lwe_noiseless_trivial(temp_result, or_n_y_const) 275 | lwe_sub_to(temp_result, ca) 276 | lwe_add_to(temp_result, cb) 277 | 278 | # if the phase is positive, the result is 1/8 279 | # if the phase is positive, else the result is -1/8 280 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 281 | 282 | 283 | def ORYN( 284 | bk: TFHECloudKey, result: LWESampleArray, ca: LWESampleArray, cb: LWESampleArray 285 | ) -> None: 286 | """ 287 | * Homomorphic bootstrapped OrYN Gate: a or not(b) 288 | * Takes in input 2 LWE samples (with message space [-1/8,1/8], noise<1/16) 289 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 290 | """ 291 | 292 | mu = mod_switch_to_torus32(1, 8) 293 | in_out_params = bk.params.in_out_params 294 | 295 | temp_result = LWESampleArray(in_out_params, result.shape) 296 | 297 | # compute: (0,1/8) + ca - cb 298 | or_y_n_const = mod_switch_to_torus32(1, 8) 299 | lwe_noiseless_trivial(temp_result, or_y_n_const) 300 | lwe_add_to(temp_result, ca) 301 | lwe_sub_to(temp_result, cb) 302 | 303 | # if the phase is positive, the result is 1/8 304 | # if the phase is positive, else the result is -1/8 305 | tfhe_bootstrap_fft(result, bk.bk_fft, mu, temp_result) 306 | 307 | 308 | def MUX( 309 | bk: TFHECloudKey, 310 | result: LWESampleArray, 311 | a: LWESampleArray, 312 | b: LWESampleArray, 313 | c: LWESampleArray, 314 | ) -> None: 315 | """ 316 | * Homomorphic bootstrapped Mux(a,b,c) = a?b:c = a*b + not(a)*c 317 | * Takes in input 3 LWE samples (with message space [-1/8,1/8], noise<1/16) 318 | * Outputs a LWE bootstrapped sample (with message space [-1/8,1/8], noise<1/16) 319 | """ 320 | 321 | mu = mod_switch_to_torus32(1, 8) 322 | in_out_params = bk.params.in_out_params 323 | extracted_params = bk.params.tgsw_params.tlwe_params.extracted_lwe_params 324 | 325 | temp_result = LWESampleArray(in_out_params, result.shape) 326 | temp_result1 = LWESampleArray(extracted_params, result.shape) 327 | u1 = LWESampleArray(extracted_params, result.shape) 328 | u2 = LWESampleArray(extracted_params, result.shape) 329 | 330 | # compute "AND(a,b)": (0,-1/8) + a + b 331 | and_const = mod_switch_to_torus32(-1, 8) 332 | lwe_noiseless_trivial(temp_result, and_const) 333 | lwe_add_to(temp_result, a) 334 | lwe_add_to(temp_result, b) 335 | # Bootstrap without KeySwitch 336 | tfhe_bootstrap_wo_ks_fft(u1, bk.bk_fft, mu, temp_result) 337 | 338 | # compute "AND(not(a),c)": (0,-1/8) - a + c 339 | lwe_noiseless_trivial(temp_result, and_const) 340 | lwe_sub_to(temp_result, a) 341 | lwe_add_to(temp_result, c) 342 | # Bootstrap without KeySwitch 343 | tfhe_bootstrap_wo_ks_fft(u2, bk.bk_fft, mu, temp_result) 344 | 345 | # Add u1=u1+u2 346 | mux_const = mod_switch_to_torus32(1, 8) 347 | lwe_noiseless_trivial(temp_result1, mux_const) 348 | lwe_add_to(temp_result1, u1) 349 | lwe_add_to(temp_result1, u2) 350 | 351 | # Key switching 352 | lwe_key_switch(result, bk.bk_fft.ks, temp_result1) 353 | -------------------------------------------------------------------------------- /tfhe/keys.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .lwe import LWEKey, LWEParams, LWESampleArray, lwe_phase, lwe_sym_encrypt 7 | from .lwe_bootstrapping import LWEBootstrappingKeyFFT 8 | from .numeric_functions import mod_switch_to_torus32 9 | from .tgsw import TGSWKey, TGSWParams 10 | from .tlwe import TLWEParams 11 | 12 | 13 | class TFHEParameters: 14 | def __init__(self) -> None: 15 | # In the reference implementation there was a parameter `minimum_lambda` here, 16 | # which was unused. 17 | 18 | # the parameters are only implemented for about 128bit of security! 19 | 20 | def mul_by_sqrt_two_over_pi(x: float) -> float: 21 | return cast(float, x * (2 / numpy.pi) ** 0.5) 22 | 23 | N = 1024 24 | k = 1 25 | n = 500 26 | bk_l = 2 27 | bk_bg_bit = 10 28 | ks_base_bit = 2 29 | ks_length = 8 30 | ks_stdev = mul_by_sqrt_two_over_pi(1 / 2**15) # standard deviation 31 | bk_stdev = mul_by_sqrt_two_over_pi(9e-9) # standard deviation 32 | max_stdev = mul_by_sqrt_two_over_pi( 33 | 1 / 2**4 / 4 34 | ) # max standard deviation for a 1/4 msg space 35 | 36 | params_in = LWEParams(n, ks_stdev, max_stdev) 37 | params_accum = TLWEParams(N, k, bk_stdev, max_stdev) 38 | params_bk = TGSWParams(bk_l, bk_bg_bit, params_accum) 39 | 40 | self.ks_t = ks_length 41 | self.ks_base_bit = ks_base_bit 42 | self.in_out_params = params_in 43 | self.tgsw_params = params_bk 44 | 45 | 46 | class TFHESecretKey: 47 | def __init__( 48 | self, params: TFHEParameters, lwe_key: LWEKey, tgsw_key: TGSWKey 49 | ) -> None: 50 | self.params = params 51 | self.lwe_key = lwe_key 52 | self.tgsw_key = tgsw_key 53 | 54 | 55 | class TFHECloudKey: 56 | def __init__(self, params: TFHEParameters, bk_fft: LWEBootstrappingKeyFFT) -> None: 57 | self.params = params 58 | self.bk_fft = bk_fft 59 | 60 | 61 | def tfhe_parameters( 62 | key: TFHECloudKey, 63 | ) -> TFHEParameters: # union(TFHESecretKey, TFHECloudKey) 64 | return key.params 65 | 66 | 67 | def tfhe_key_pair(rng: numpy.random.RandomState) -> Tuple[TFHESecretKey, TFHECloudKey]: 68 | params = TFHEParameters() 69 | 70 | lwe_key = LWEKey.from_rng(rng, params.in_out_params) 71 | tgsw_key = TGSWKey(rng, params.tgsw_params) 72 | secret_key = TFHESecretKey(params, lwe_key, tgsw_key) 73 | 74 | bk_fft = LWEBootstrappingKeyFFT( 75 | rng, params.ks_t, params.ks_base_bit, lwe_key, tgsw_key 76 | ) 77 | cloud_key = TFHECloudKey(params, bk_fft) 78 | 79 | return secret_key, cloud_key 80 | 81 | 82 | def tfhe_encrypt( 83 | rng: numpy.random.RandomState, 84 | key: TFHESecretKey, 85 | message: NDArray[numpy.bool_], 86 | ) -> LWESampleArray: 87 | result = empty_ciphertext(key.params, message.shape) 88 | _1s8 = mod_switch_to_torus32(1, 8) 89 | mus = numpy.array([_1s8 if bit else -_1s8 for bit in message]) 90 | alpha = ( 91 | key.params.in_out_params.alpha_min 92 | ) # TODO: specify noise # pylint: disable=fixme 93 | lwe_sym_encrypt(rng, result, mus, alpha, key.lwe_key) 94 | return result 95 | 96 | 97 | def tfhe_decrypt( 98 | key: TFHESecretKey, ciphertext: LWESampleArray 99 | ) -> NDArray[numpy.bool_]: 100 | mus = lwe_phase(ciphertext, key.lwe_key) 101 | return numpy.array([(mu > 0) for mu in mus]) 102 | 103 | 104 | def empty_ciphertext(params: TFHEParameters, shape: Tuple[int, ...]) -> LWESampleArray: 105 | return LWESampleArray(params.in_out_params, shape) 106 | -------------------------------------------------------------------------------- /tfhe/lwe.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .numeric_functions import ( 7 | Torus32, 8 | double_to_torus32, 9 | rand_gaussian_float, 10 | rand_gaussian_torus32, 11 | rand_uniform_int32, 12 | rand_uniform_torus32, 13 | ) 14 | 15 | 16 | class LWEParams: 17 | def __init__(self, n: int, alpha_min: float, alpha_max: float) -> None: 18 | self.n = n 19 | self.alpha_min = alpha_min 20 | self.alpha_max = alpha_max 21 | 22 | 23 | class LWEKey: 24 | def __init__(self, params: LWEParams, key: NDArray[numpy.int32]) -> None: 25 | self.params = params 26 | self.key = key # 1D array of Int32 27 | 28 | @classmethod 29 | def from_rng(cls, rng: numpy.random.RandomState, params: LWEParams) -> "LWEKey": 30 | return cls(params, rand_uniform_int32(rng, (params.n,))) 31 | 32 | # extractions Ring Lwe . Lwe 33 | @classmethod 34 | def from_key( 35 | cls, params: LWEParams, tlwe_key: Any 36 | ) -> "LWEKey": # sans doute un param supplémentaire 37 | # TYPING: tlwe_key: TLWEKey 38 | N = tlwe_key.params.N 39 | k = tlwe_key.params.k 40 | assert params.n == k * N 41 | 42 | # GPU: array operation 43 | key = ( 44 | tlwe_key.key.coefs.flatten() 45 | ) # TODO: use an approprtiate method # pylint: disable=fixme 46 | 47 | return cls(params, key) 48 | 49 | 50 | class LWESampleArray: 51 | def __init__(self, params: LWEParams, shape: Tuple[int, ...]) -> None: 52 | self.a = numpy.empty(shape + (params.n,), cast(Torus32, numpy.int32)) 53 | self.b = numpy.empty(shape, cast(Torus32, numpy.int32)) 54 | self.current_variances = numpy.empty(shape, numpy.float64) 55 | self.shape = shape 56 | self.params = params 57 | 58 | def __getitem__(self, *args: Torus32) -> "LWESampleArray": 59 | sub_a = self.a[args] 60 | sub_b = self.b[args] 61 | sub_cv = self.current_variances[args] 62 | res = LWESampleArray(self.params, sub_b.shape) 63 | 64 | res.a = sub_a 65 | res.b = sub_b 66 | res.current_variances = sub_cv 67 | 68 | return res 69 | 70 | 71 | def vec_mul_mat( 72 | b: NDArray[numpy.int32], a: NDArray[numpy.int32] 73 | ) -> NDArray[numpy.int32]: 74 | return cast(NDArray[numpy.int32], (a * b).sum(-1, dtype=numpy.int32)) 75 | 76 | 77 | # * This function encrypts message by using key, with stdev alpha 78 | # * The Lwe sample for the result must be allocated and initialized 79 | # * (this means that the parameters are already in the result) 80 | def lwe_sym_encrypt( 81 | rng: numpy.random.RandomState, 82 | result: LWESampleArray, 83 | messages: NDArray[Torus32], 84 | alpha: float, 85 | key: LWEKey, 86 | ) -> None: 87 | # TYPING: messages: Array{Torus32} 88 | 89 | assert result.shape == messages.shape 90 | 91 | n = key.params.n 92 | 93 | result.b = cast( 94 | NDArray[Torus32], 95 | rand_gaussian_torus32(rng, cast(Torus32, 0), alpha, messages.shape) + messages, 96 | ) 97 | result.a = cast(NDArray[Torus32], rand_uniform_torus32(rng, messages.shape + (n,))) 98 | result.b = cast(NDArray[Torus32], result.b + vec_mul_mat(key.key, result.a)) 99 | result.current_variances.fill(alpha**2) 100 | 101 | 102 | # This function computes the phase of sample by using key : phi = b - a.s 103 | def lwe_phase(sample: LWESampleArray, key: LWEKey) -> NDArray[Torus32]: 104 | return cast(NDArray[Torus32], sample.b - vec_mul_mat(key.key, sample.a)) 105 | 106 | 107 | # Arithmetic operations on Lwe samples 108 | 109 | 110 | # result = sample 111 | def lwe_copy(result: LWESampleArray, sample: LWESampleArray) -> None: 112 | result.a = sample.a.copy() 113 | result.b = sample.b.copy() 114 | result.current_variances = sample.current_variances.copy() 115 | 116 | 117 | # result = -sample 118 | def lwe_negate(result: LWESampleArray, sample: LWESampleArray) -> None: 119 | result.a = -sample.a 120 | result.b = -sample.b 121 | result.current_variances = sample.current_variances.copy() 122 | 123 | 124 | # result = (0,mu) 125 | def lwe_noiseless_trivial( 126 | result: LWESampleArray, mus: Torus32 | NDArray[Torus32] 127 | ) -> None: 128 | # TYPING: mus: Union{Array{Torus32}, Torus32} 129 | # GPU: array operations 130 | result.a.fill(0) 131 | numpy.copyto(result.b, mus) 132 | result.current_variances.fill(0) 133 | 134 | 135 | # result = result + sample 136 | def lwe_add_to(result: LWESampleArray, sample: LWESampleArray) -> None: 137 | # GPU: array operations or a custom kernel 138 | result.a = cast(NDArray[Torus32], result.a + sample.a) 139 | result.b = cast(NDArray[Torus32], result.b + sample.b) 140 | result.current_variances += sample.current_variances 141 | 142 | 143 | # result = result - sample 144 | def lwe_sub_to(result: LWESampleArray, sample: LWESampleArray) -> None: 145 | result.a = cast(NDArray[Torus32], result.a - sample.a) 146 | result.b = cast(NDArray[Torus32], result.b - sample.b) 147 | result.current_variances += sample.current_variances 148 | 149 | 150 | # result = result + p.sample 151 | def lwe_add_mul_to( 152 | result: LWESampleArray, p: numpy.int32, sample: LWESampleArray 153 | ) -> None: 154 | result.a = cast(NDArray[Torus32], result.a + p * sample.a) 155 | result.b = cast(NDArray[Torus32], result.b + p * sample.b) 156 | result.current_variances += p**2 * sample.current_variances 157 | 158 | 159 | # result = result - p.sample 160 | def lwe_sub_mul_to( 161 | result: LWESampleArray, p: numpy.int32, sample: LWESampleArray 162 | ) -> None: 163 | result.a = cast(NDArray[Torus32], result.a - p * sample.a) 164 | result.b = cast(NDArray[Torus32], result.b - p * sample.b) 165 | result.current_variances += p**2 * sample.current_variances 166 | 167 | 168 | # This function encrypts a message by using key and a given noise value 169 | def lwe_sym_encrypt_with_external_noise( 170 | rng: numpy.random.RandomState, 171 | result: LWESampleArray, 172 | messages: NDArray[Torus32], 173 | noises: NDArray[numpy.float64], 174 | alpha: float, 175 | key: LWEKey, 176 | ) -> None: 177 | # TYPING: messages: Array{Torus32} 178 | # TYPING: noises: Array{Float64} 179 | 180 | # @assert size(result) == size(messages) 181 | # @assert size(result) == size(noises) 182 | 183 | # GPU: will be made into a kernel 184 | 185 | # term h=0 as trivial encryption of 0 (it will not be used in the KeySwitching) 186 | result.a[:, :, 0, :] = 0 187 | result.b[:, :, 0] = 0 188 | result.current_variances[:, :, 0] = 0 189 | 190 | n = key.params.n 191 | 192 | result.b[:, :, 1:] = messages + double_to_torus32(noises) 193 | result.a[:, :, 1:, :] = rand_uniform_torus32(rng, messages.shape + (n,)) 194 | result.b[:, :, 1:] = result.b[:, :, 1:] + vec_mul_mat( 195 | key.key, result.a[:, :, 1:, :] 196 | ) 197 | result.current_variances[:, :, 1:] = alpha**2 198 | 199 | 200 | class LWEKeySwitchKey: 201 | """ 202 | Create the key switching key: 203 | * normalize the error in the beginning 204 | * chose a random vector of gaussian noises (same size as ks) 205 | * recenter the noises 206 | * generate the ks by creating noiseless encryprions and then add the noise 207 | """ 208 | 209 | def __init__( 210 | self, 211 | rng: numpy.random.RandomState, 212 | n: int, 213 | t: int, 214 | base_bit: int, 215 | in_key: LWEKey, 216 | out_key: LWEKey, 217 | ) -> None: 218 | # GPU: will be possibly made into a kernel including 219 | # lwe_sym_encrypt_with_external_noise() 220 | 221 | out_params = out_key.params 222 | 223 | base = 1 << base_bit 224 | ks = LWESampleArray(out_params, (n, t, base)) 225 | 226 | alpha = out_key.params.alpha_min 227 | 228 | # chose a random vector of gaussian noises 229 | noises = rand_gaussian_float(rng, alpha, (n, t, base - 1)) 230 | 231 | # recenter the noises 232 | noises -= noises.mean() 233 | 234 | # generate the ks 235 | 236 | # mess::Torus32 = (in_key.key[i] * Int32(h - 1)) * Int32(1 << (32 - j * base_bit)) # pylint: disable=line-too-long # noqa: E501 237 | hs = numpy.arange(2, base + 1) 238 | js = numpy.arange(1, t + 1) 239 | 240 | r_key = in_key.key.reshape(n, 1, 1) 241 | r_hs = hs.reshape(1, 1, base - 1) 242 | r_js = js.reshape(1, t, 1) 243 | 244 | messages = r_key * (r_hs - 1) * (1 << (32 - r_js * base_bit)) 245 | messages = messages.astype(cast(Torus32, numpy.int32)) 246 | 247 | lwe_sym_encrypt_with_external_noise(rng, ks, messages, noises, alpha, out_key) 248 | 249 | self.n = n # length of the input key: s' 250 | self.t = t # decomposition length 251 | self.base_bit = base_bit # log_2(base) 252 | self.base = base # decomposition base: a power of 2 253 | self.out_params = out_params # params of the output key s 254 | self.ks = ks # the keyswitch elements: a n.l.base matrix 255 | # de taille n pointe vers ks1 un tableau dont les cases sont espaceés 256 | # de ell positions 257 | 258 | 259 | def lwe_key_switch_translate_from_array( 260 | result: LWESampleArray, 261 | ks: LWESampleArray, 262 | ai: NDArray[Torus32], 263 | n: int, 264 | t: int, 265 | base_bit: int, 266 | ) -> None: 267 | """ 268 | * translates the message of the result sample by -sum(a[i].s[i]) where s 269 | * is the secret 270 | * embedded in ks. 271 | * @param result the LWE sample to translate by -sum(ai.si). 272 | * @param ks The (n x t x base) key switching key 273 | * ks[i][j][k] encodes k.s[i]/base^(j+1) 274 | * @param params The common LWE parameters of ks and result 275 | * @param ai The input torus array 276 | * @param n The size of the input key 277 | * @param t The precision of the keyswitch (technically, 1/2.base^t) 278 | * @param base_bit Log_2 of base 279 | """ 280 | # TYPING: ai: Array{Torus32, 2} 281 | # GPU: array operations or (most probably) a custom kernel 282 | 283 | base = 1 << base_bit # base=2 in [CGGI16] 284 | prec_offset = 1 << (32 - (1 + base_bit * t)) # precision 285 | mask = base - 1 286 | 287 | js = numpy.arange(1, t + 1).reshape(1, 1, t) 288 | ai = ai.reshape(ai.shape + (1,)) 289 | aijs = (((ai + prec_offset) >> (32 - js * base_bit)) & mask) + 1 290 | 291 | for i in range(result.shape[0]): 292 | for l in range(n): # noqa: E741 293 | for j in range(t): 294 | x = aijs[i, l, j] - 1 295 | if x != 0: 296 | result.a[i, :] = result.a[i, :] - ks.a[l, j, x, :] 297 | # FIXME: numpy detects overflow # pylint: disable=fixme 298 | # there, and gives a warning, 299 | # but it's normal finite size 300 | # integer arithmetic, and works 301 | # as intended 302 | result.b[i] -= ks.b[l, j, x] 303 | result.current_variances[i] += ks.current_variances[l, j, x] 304 | 305 | 306 | # sample=(a',b') 307 | def lwe_key_switch( 308 | result: LWESampleArray, ks: LWEKeySwitchKey, sample: LWESampleArray 309 | ) -> None: 310 | n = ks.n 311 | base_bit = ks.base_bit 312 | t = ks.t 313 | 314 | lwe_noiseless_trivial(result, sample.b) 315 | lwe_key_switch_translate_from_array(result, ks.ks, sample.a, n, t, base_bit) 316 | -------------------------------------------------------------------------------- /tfhe/lwe_bootstrapping.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .lwe import LWEKey, LWEKeySwitchKey, LWESampleArray, lwe_key_switch 7 | from .numeric_functions import Torus32, mod_switch_from_torus32 8 | from .polynomials import ( 9 | IntPolynomialArray, 10 | LagrangeHalfCPolynomialArray, 11 | TorusPolynomialArray, 12 | torus_polynomial_mul_by_xai, 13 | ) 14 | from .tgsw import ( 15 | TGSWKey, 16 | TGSWParams, 17 | TGSWSampleArray, 18 | TGSWSampleFFTArray, 19 | tgsw_fft_extern_mul_to_tlwe, 20 | tgsw_sym_encrypt_int, 21 | tgsw_to_fft_convert, 22 | ) 23 | from .tlwe import ( 24 | TLWESampleArray, 25 | TLWESampleFFTArray, 26 | tlwe_add_to, 27 | tlwe_copy, 28 | tlwe_extract_lwe_sample, 29 | tlwe_mul_by_xai_minus_one, 30 | tlwe_noiseless_trivial, 31 | ) 32 | 33 | 34 | def lwe_bootstrapping_key( 35 | rng: numpy.random.RandomState, 36 | ks_t: int, 37 | ks_base_bit: int, 38 | key_in: LWEKey, 39 | rgsw_key: TGSWKey, 40 | ) -> Tuple[TGSWSampleArray, LWEKeySwitchKey]: 41 | bk_params = rgsw_key.params 42 | in_out_params = key_in.params 43 | accum_params = bk_params.tlwe_params 44 | extract_params = accum_params.extracted_lwe_params 45 | 46 | n = in_out_params.n 47 | N = extract_params.n 48 | 49 | accum_key = rgsw_key.tlwe_key 50 | extracted_key = LWEKey.from_key(extract_params, accum_key) 51 | 52 | ks = LWEKeySwitchKey(rng, N, ks_t, ks_base_bit, extracted_key, key_in) 53 | 54 | bk = TGSWSampleArray(bk_params, (n,)) 55 | kin = key_in.key 56 | alpha = accum_params.alpha_min 57 | 58 | tgsw_sym_encrypt_int(rng, bk, kin, alpha, rgsw_key) 59 | 60 | return bk, ks 61 | 62 | 63 | class LWEBootstrappingKeyFFT: 64 | def __init__( 65 | self, 66 | rng: numpy.random.RandomState, 67 | ks_t: int, 68 | ks_base_bit: int, 69 | lwe_key: LWEKey, 70 | tgsw_key: TGSWKey, 71 | ) -> None: 72 | in_out_params = lwe_key.params 73 | bk_params = tgsw_key.params 74 | accum_params = bk_params.tlwe_params 75 | extract_params = accum_params.extracted_lwe_params 76 | 77 | bk, ks = lwe_bootstrapping_key(rng, ks_t, ks_base_bit, lwe_key, tgsw_key) 78 | 79 | n = in_out_params.n 80 | 81 | # Bootstrapping Key FFT 82 | bk_fft = TGSWSampleFFTArray(bk_params, (n,)) 83 | tgsw_to_fft_convert(bk_fft, bk) 84 | 85 | self.in_out_params = ( 86 | in_out_params # paramètre de l'input et de l'output. key: s 87 | ) 88 | self.bk_params = bk_params # params of the Gsw elems in bk. key: s" 89 | self.accum_params = accum_params # params of the accum variable key: s" 90 | self.extract_params = extract_params # params after extraction: key: s' 91 | self.bk_fft = bk_fft # the bootstrapping key (s->s") 92 | self.ks = ks # the keyswitch key (s'->s) 93 | 94 | 95 | def tfhe_mux_rotate_fft( 96 | result: TLWESampleArray, 97 | accum: TLWESampleArray, 98 | bki: TGSWSampleFFTArray, 99 | bk_idx: int, 100 | barai: NDArray[numpy.int32], 101 | bk_params: TGSWParams, 102 | tmpa: TLWESampleFFTArray, 103 | deca: IntPolynomialArray, 104 | deca_fft: LagrangeHalfCPolynomialArray, 105 | ) -> None: 106 | # TYPING: barai::Array{Int32} 107 | # ACC = BKi*[(X^barai-1)*ACC]+ACC 108 | # temp = (X^barai-1)*ACC 109 | tlwe_mul_by_xai_minus_one(result, barai, accum) 110 | 111 | # temp *= BKi 112 | tgsw_fft_extern_mul_to_tlwe(result, bki, bk_idx, bk_params, tmpa, deca, deca_fft) 113 | 114 | # ACC += temp 115 | tlwe_add_to(result, accum) 116 | 117 | 118 | def tfhe_blind_rotate_fft( 119 | accum: TLWESampleArray, 120 | bk_fft: TGSWSampleFFTArray, 121 | bara: NDArray[numpy.int32], 122 | n: int, 123 | bk_params: TGSWParams, 124 | ) -> None: 125 | """ 126 | * multiply the accumulator by X^sum(bara_i.s_i) 127 | * @param accum the TLWE sample to multiply 128 | * @param bk An array of n TGSW FFT samples where bk_i encodes s_i 129 | * @param bara An array of n coefficients between 0 and 2N-1 130 | * @param bk_params The parameters of bk 131 | """ 132 | # TYPING: bara::Array{Int32} 133 | 134 | temp = TLWESampleArray(bk_params.tlwe_params, accum.shape) 135 | temp2 = temp 136 | temp3 = accum 137 | 138 | accum_in_temp3 = True 139 | 140 | # For use in tgsw_fft_extern_mul_to_tlwe(), so that we don't have to 141 | # allocate them `n` times 142 | tmpa = TLWESampleFFTArray(bk_params.tlwe_params, accum.shape) 143 | deca = IntPolynomialArray(bk_params.tlwe_params.N, accum.a.shape + (bk_params.l,)) 144 | deca_fft = LagrangeHalfCPolynomialArray( 145 | bk_params.tlwe_params.N, 146 | accum.shape + (bk_params.tlwe_params.k + 1, bk_params.l), 147 | ) 148 | 149 | for i in range(n): 150 | # GPU: will have to be passed as a pair `bara`, `i` 151 | barai = bara[:, i] # !!! assuming the ciphertext is 1D 152 | 153 | # FIXME: We could pass the view bk_fft[i] here, but on the current # pylint: disable=fixme # noqa: E501 154 | # Julia it's too slow 155 | tfhe_mux_rotate_fft( 156 | temp2, temp3, bk_fft, i, barai, bk_params, tmpa, deca, deca_fft 157 | ) 158 | 159 | temp2, temp3 = temp3, temp2 160 | accum_in_temp3 = not accum_in_temp3 161 | 162 | if not accum_in_temp3: # temp3 != accum 163 | tlwe_copy(accum, temp3) 164 | 165 | 166 | def tfhe_blind_rotate_and_extract_fft( 167 | result: LWESampleArray, 168 | v: TorusPolynomialArray, 169 | bk: TGSWSampleFFTArray, 170 | barb: NDArray[numpy.int32], 171 | bara: NDArray[numpy.int32], 172 | n: int, 173 | bk_params: TGSWParams, 174 | ) -> None: 175 | """ 176 | * result = LWE(v_p) where p=barb-sum(bara_i.s_i) mod 2N 177 | * @param result the output LWE sample 178 | * @param v a 2N-elt anticyclic function (represented by a TorusPolynomial) 179 | * @param bk An array of n TGSW FFT samples where bk_i encodes s_i 180 | * @param barb A coefficients between 0 and 2N-1 181 | * @param bara An array of n coefficients between 0 and 2N-1 182 | * @param bk_params The parameters of bk 183 | """ 184 | # TYPING: barb::Array{Int32}, 185 | # TYPING: bara::Array{Int32} 186 | 187 | accum_params = bk_params.tlwe_params 188 | extract_params = accum_params.extracted_lwe_params 189 | N = accum_params.N 190 | 191 | # Test polynomial 192 | test_vect_bis = TorusPolynomialArray(N, result.shape) 193 | # Accumulator 194 | acc = TLWESampleArray(accum_params, result.shape) 195 | 196 | # testvector = X^{2N-barb}*v 197 | # GPU: array operations or a custom kernel 198 | torus_polynomial_mul_by_xai(test_vect_bis, 2 * N - barb, v) 199 | 200 | tlwe_noiseless_trivial(acc, test_vect_bis) 201 | 202 | # Blind rotation 203 | tfhe_blind_rotate_fft(acc, bk, bara, n, bk_params) 204 | 205 | # Extraction 206 | tlwe_extract_lwe_sample(result, acc, extract_params, accum_params) 207 | 208 | 209 | def tfhe_bootstrap_wo_ks_fft( 210 | result: LWESampleArray, bk: LWEBootstrappingKeyFFT, mu: Torus32, x: LWESampleArray 211 | ) -> None: 212 | """ 213 | * result = LWE(mu) iff phase(x)>0, LWE(-mu) iff phase(x)<0 214 | * @param result The resulting LWESample 215 | * @param bk The bootstrapping + keyswitch key 216 | * @param mu The output message (if phase(x)>0) 217 | * @param x The input sample 218 | """ 219 | 220 | bk_params = bk.bk_params 221 | accum_params = bk.accum_params 222 | in_params = bk.in_out_params 223 | N = accum_params.N 224 | n = in_params.n 225 | 226 | test_vec = TorusPolynomialArray(N, result.shape) 227 | 228 | # Modulus switching 229 | # GPU: array operations or a custom kernel 230 | barb = mod_switch_from_torus32(cast(Torus32, x.b), 2 * N) 231 | bara = mod_switch_from_torus32(cast(Torus32, x.a), 2 * N) 232 | 233 | # the initial test_vec = [mu,mu,mu,...,mu] 234 | # TODO: use an appropriate method # pylint: disable=fixme 235 | # GPU: array operations or a custom kernel 236 | test_vec.coefs_t.fill(mu) 237 | 238 | # Bootstrapping rotation and extraction 239 | tfhe_blind_rotate_and_extract_fft( 240 | result, test_vec, bk.bk_fft, barb, bara, n, bk_params 241 | ) 242 | 243 | 244 | def tfhe_bootstrap_fft( 245 | result: LWESampleArray, bk: LWEBootstrappingKeyFFT, mu: Torus32, x: LWESampleArray 246 | ) -> None: 247 | """ 248 | * result = LWE(mu) iff phase(x)>0, LWE(-mu) iff phase(x)<0 249 | * @param result The resulting LweSample 250 | * @param bk The bootstrapping + keyswitch key 251 | * @param mu The output message (if phase(x)>0) 252 | * @param x The input sample 253 | """ 254 | 255 | u = LWESampleArray(bk.accum_params.extracted_lwe_params, result.shape) 256 | 257 | tfhe_bootstrap_wo_ks_fft(u, bk, mu, x) 258 | 259 | # Key switching 260 | lwe_key_switch(result, bk.ks, u) 261 | -------------------------------------------------------------------------------- /tfhe/numeric_functions.py: -------------------------------------------------------------------------------- 1 | from typing import NewType, Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | Torus32 = NewType("Torus32", numpy.int32) 7 | 8 | 9 | def rand_uniform_int32( 10 | rng: numpy.random.RandomState, shape: Tuple[int, ...] 11 | ) -> NDArray[numpy.int32]: 12 | return rng.randint(0, 2, size=shape, dtype=numpy.int32) 13 | 14 | 15 | def rand_uniform_torus32( 16 | rng: numpy.random.RandomState, shape: Tuple[int, ...] 17 | ) -> Torus32 | NDArray[Torus32]: 18 | # TODO: if dims == () (it happens), the return value # pylint: disable=fixme 19 | # is not an array -> type instability also, there's probably 20 | # instability for arrays of different dims too. Naturally, 21 | # it applies for all other rand_ functions. 22 | return cast( 23 | NDArray[Torus32], 24 | rng.randint(-(2**31), 2**31, size=shape, dtype=numpy.int32), 25 | ) 26 | 27 | 28 | def rand_gaussian_float( 29 | rng: numpy.random.RandomState, sigma: float, shape: Tuple[int, ...] 30 | ) -> NDArray[numpy.float64]: 31 | return rng.normal(size=shape, scale=sigma) 32 | 33 | 34 | # Gaussian sample centered in message, with standard deviation sigma 35 | def rand_gaussian_torus32( 36 | rng: numpy.random.RandomState, 37 | message: Torus32, 38 | sigma: float, 39 | shape: Tuple[int, ...], 40 | ) -> Torus32: 41 | # Attention: all the implementation will use the stdev instead of the 42 | # gaussian fourier param 43 | return cast( 44 | Torus32, message + double_to_torus32(rng.normal(size=shape, scale=sigma)) 45 | ) 46 | 47 | 48 | # Used to approximate the phase to the nearest message possible in the message space 49 | # The constant m_size will indicate on which message space we are working 50 | # (how many messages possible) 51 | # 52 | # "work on 63 bits instead of 64, because in our practical cases, it's more precise" 53 | def mod_switch_from_torus32(phase: Torus32, m_size: int) -> NDArray[numpy.int32]: 54 | # TODO: check if it can be simplified (wrt type conversions) # pylint: disable=fixme 55 | interval = (1 << 63) // m_size * 2 # width of each intervall 56 | half_interval = interval // 2 # begin of the first intervall 57 | phase64 = (phase.astype(numpy.uint32).astype(numpy.uint64) << 32) + half_interval 58 | # floor to the nearest multiples of interval 59 | return cast( 60 | NDArray[numpy.int32], 61 | (phase64 // interval).astype(numpy.int64).astype(numpy.int32), 62 | ) 63 | 64 | 65 | # Used to approximate the phase to the nearest message possible in the message space 66 | # The constant m_size will indicate on which message space we are working 67 | # (how many messages possible) 68 | # 69 | # "work on 63 bits instead of 64, because in our practical cases, it's more precise" 70 | def mod_switch_to_torus32(mu: int, m_size: int) -> Torus32: 71 | interval = ((1 << 63) // m_size) * 2 # width of each intervall 72 | phase64 = mu * interval 73 | # floor to the nearest multiples of interval 74 | return cast(Torus32, numpy.int32(phase64 >> 32)) 75 | 76 | 77 | # from double to Torus32 78 | def double_to_torus32(d: NDArray[numpy.float64]) -> Torus32: 79 | return cast(Torus32, ((d - numpy.trunc(d)) * 2**32).astype(numpy.int32)) 80 | 81 | 82 | def int64_to_int32(x: numpy.int64) -> numpy.int32: 83 | return x.astype(numpy.int32) 84 | 85 | 86 | def float_to_int32(x: NDArray[numpy.float64]) -> numpy.int32: 87 | return cast(numpy.int32, numpy.round(x).astype(numpy.int64).astype(numpy.int32)) 88 | -------------------------------------------------------------------------------- /tfhe/polynomials.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .numeric_functions import Torus32, float_to_int32 7 | 8 | 9 | # This structure represents an integer polynomial modulo X^N+1 10 | class IntPolynomialArray: 11 | def __init__(self, N: int, shape: Tuple[int, ...]) -> None: 12 | self.coefs = numpy.empty(shape + (N,), numpy.int32) 13 | self.polynomial_size = N 14 | self.shape = shape 15 | 16 | 17 | # This structure represents an torus polynomial modulo X^N+1 18 | class TorusPolynomialArray: 19 | def __init__(self, N: int, shape: Tuple[int, ...]) -> None: 20 | self.coefs_t = numpy.empty(shape + (N,), cast(Torus32, numpy.int32)) 21 | self.polynomial_size = N 22 | self.shape = shape 23 | 24 | @classmethod 25 | def from_arr(cls, arr: NDArray[Torus32]) -> "TorusPolynomialArray": 26 | obj = cls(arr.shape[-1], arr.shape[:-1]) 27 | obj.coefs_t = arr 28 | return obj 29 | 30 | 31 | # This structure is used for FFT operations, and is a representation 32 | # over C of a polynomial in R[X]/X^N+1 33 | class LagrangeHalfCPolynomialArray: 34 | def __init__(self, N: int, shape: Tuple[int, ...]) -> None: 35 | assert N % 2 == 0 36 | self.coefs_c = numpy.empty(shape + (N // 2,), numpy.complex128) 37 | self.polynomial_size = N 38 | self.shape = shape 39 | 40 | 41 | def coefs( 42 | p: TorusPolynomialArray | IntPolynomialArray | LagrangeHalfCPolynomialArray, 43 | ) -> NDArray[Torus32 | numpy.int32 | numpy.complex128]: 44 | # TODO: different field names help with debugging, remove later # pylint: disable=fixme # noqa: E501 45 | if isinstance(p, IntPolynomialArray): 46 | return p.coefs 47 | if isinstance(p, TorusPolynomialArray): 48 | return p.coefs_t 49 | return p.coefs_c 50 | 51 | 52 | def flat_coefs( 53 | p: TorusPolynomialArray | IntPolynomialArray | LagrangeHalfCPolynomialArray, 54 | ) -> NDArray[Torus32 | numpy.int32 | numpy.complex128]: 55 | cp = coefs(p) 56 | return cp.reshape(numpy.prod(p.shape), cp.shape[-1]) 57 | 58 | 59 | def polynomial_size( 60 | p: TorusPolynomialArray | IntPolynomialArray | LagrangeHalfCPolynomialArray, 61 | ) -> int: 62 | return p.polynomial_size 63 | 64 | 65 | def prepare_ifft_input( 66 | rev_in: NDArray[numpy.float64], 67 | a: NDArray[Torus32 | numpy.int32 | numpy.complex128], 68 | coeff: float, 69 | N: int, 70 | ) -> None: 71 | rev_in[:, :N] = a * coeff 72 | rev_in[:, N:] = -rev_in[:, :N] 73 | 74 | 75 | def prepare_ifft_output( 76 | res: NDArray[Torus32 | numpy.int32 | numpy.complex128], 77 | rev_out: NDArray[numpy.complex128], 78 | N: int, 79 | ) -> None: 80 | # FIXME: when Julia is smart enough, can be replaced by: # pylint: disable=fixme 81 | res[:, : N // 2] = rev_out[:, 1 : N + 1 : 2] 82 | 83 | 84 | def int_polynomial_ifft( 85 | result: LagrangeHalfCPolynomialArray, p: IntPolynomialArray 86 | ) -> None: 87 | res = flat_coefs(result) 88 | a = flat_coefs(p) 89 | N = polynomial_size(p) 90 | 91 | in_arr = numpy.empty((res.shape[0], 2 * N), numpy.float64) 92 | prepare_ifft_input(in_arr, a, 1 / 2, N) 93 | out_arr = numpy.fft.rfft(in_arr) 94 | prepare_ifft_output(res, out_arr, N) 95 | 96 | 97 | def torus_polynomial_ifft( 98 | result: LagrangeHalfCPolynomialArray, p: TorusPolynomialArray 99 | ) -> None: 100 | res = flat_coefs(result) 101 | a = flat_coefs(p) 102 | N = polynomial_size(p) 103 | 104 | in_arr = numpy.empty((res.shape[0], 2 * N), numpy.float64) 105 | prepare_ifft_input(in_arr, a, 1 / 2**33, N) 106 | out_arr = numpy.fft.rfft(in_arr) 107 | prepare_ifft_output(res, out_arr, N) 108 | 109 | 110 | def prepare_fft_input( 111 | fw_in: NDArray[numpy.complex128], 112 | a: NDArray[Torus32 | numpy.int32 | numpy.complex128], 113 | N: int, 114 | ) -> None: 115 | fw_in[:, 0 : N + 1 : 2] = 0 116 | fw_in[:, 1 : N + 1 : 2] = a 117 | 118 | 119 | def prepare_fft_output( 120 | res: NDArray[Torus32 | numpy.int32 | numpy.complex128], 121 | fw_out: NDArray[numpy.float64], 122 | coef: float, 123 | N: int, 124 | ) -> None: 125 | res[:, :] = float_to_int32(fw_out[:, :N] * coef) 126 | 127 | 128 | def torus_polynomial_fft( 129 | result: TorusPolynomialArray, p: LagrangeHalfCPolynomialArray 130 | ) -> None: 131 | res = flat_coefs(result) 132 | a = flat_coefs(p) 133 | N = polynomial_size(p) 134 | 135 | in_arr = numpy.empty((res.shape[0], N + 1), numpy.complex128) 136 | prepare_fft_input(in_arr, a, N) 137 | out_arr = numpy.fft.irfft(in_arr) 138 | 139 | # the first part is from the original libtfhe; 140 | # the second part is from a different FFT scaling in Julia 141 | coeff: float = (2**32 / N) * (2 * N) 142 | prepare_fft_output(res, out_arr, coeff, N) 143 | 144 | 145 | def torus_polynomial_add_mul( 146 | result: TorusPolynomialArray, poly1: IntPolynomialArray, poly2: TorusPolynomialArray 147 | ) -> None: 148 | N = polynomial_size(result) 149 | tmp1 = LagrangeHalfCPolynomialArray(N, poly1.shape) 150 | tmp2 = LagrangeHalfCPolynomialArray(N, poly2.shape) 151 | tmp3 = LagrangeHalfCPolynomialArray(N, result.shape) 152 | tmpr = TorusPolynomialArray(N, result.shape) 153 | int_polynomial_ifft(tmp1, poly1) 154 | torus_polynomial_ifft(tmp2, poly2) 155 | lagrange_polynomial_mul(tmp3, tmp1, tmp2) 156 | torus_polynomial_fft(tmpr, tmp3) 157 | torus_polynomial_add_to(result, tmpr) 158 | 159 | 160 | # sets to zero 161 | def lagrange_polynomial_clear(reps: LagrangeHalfCPolynomialArray) -> None: 162 | reps.coefs_c.fill(0) 163 | 164 | 165 | # termwise multiplication in Lagrange space */ 166 | def lagrange_polynomial_mul( 167 | result: LagrangeHalfCPolynomialArray, 168 | a: LagrangeHalfCPolynomialArray, 169 | b: LagrangeHalfCPolynomialArray, 170 | ) -> None: 171 | numpy.copyto(result.coefs_c, a.coefs_c * b.coefs_c) 172 | 173 | 174 | # TorusPolynomial = 0 175 | def torus_polynomial_clear(result: TorusPolynomialArray) -> None: 176 | result.coefs_t.fill(0) 177 | 178 | 179 | # TorusPolynomial += TorusPolynomial 180 | def torus_polynomial_add_to( 181 | result: TorusPolynomialArray, poly2: TorusPolynomialArray 182 | ) -> None: 183 | result.coefs_t = cast(NDArray[Torus32], result.coefs_t + poly2.coefs_t) 184 | 185 | 186 | # result = (X^ai-1) * source 187 | def torus_polynomial_mul_by_xai_minus_one( 188 | out: TorusPolynomialArray, ais: NDArray[numpy.int32], in_: TorusPolynomialArray 189 | ) -> None: 190 | out_c = out.coefs_t 191 | in_c = in_.coefs_t 192 | 193 | N = out_c.shape[-1] 194 | for i in range(out.shape[0]): 195 | ai = ais[i] 196 | if ai < N: 197 | out_c[i, :, :ai] = ( 198 | -in_c[i, :, (N - ai) : N] - in_c[i, :, :ai] 199 | ) # sur que i-a<0 200 | out_c[i, :, ai:N] = ( 201 | in_c[i, :, : (N - ai)] - in_c[i, :, ai:N] 202 | ) # sur que N>i-a>=0 203 | else: 204 | aa = ai - N 205 | out_c[i, :, :aa] = ( 206 | in_c[i, :, (N - aa) : N] - in_c[i, :, :aa] 207 | ) # sur que i-a<0 208 | out_c[i, :, aa:N] = ( 209 | -in_c[i, :, : (N - aa)] - in_c[i, :, aa:N] 210 | ) # sur que N>i-a>=0 211 | 212 | 213 | # result= X^{a}*source 214 | def torus_polynomial_mul_by_xai( 215 | out: TorusPolynomialArray, ais: NDArray[numpy.int32], in_: TorusPolynomialArray 216 | ) -> None: 217 | out_c = out.coefs_t 218 | in_c = in_.coefs_t 219 | 220 | N = out_c.shape[-1] 221 | for i in range(out.shape[0]): 222 | ai = ais[i] 223 | if ai < N: 224 | out_c[i, :ai] = -in_c[i, (N - ai) : N] # sur que i-a<0 225 | out_c[i, ai:N] = in_c[i, : (N - ai)] # sur que N>i-a>=0 226 | else: 227 | aa = ai - N 228 | out_c[i, :aa] = in_c[i, (N - aa) : N] # sur que i-a<0 229 | out_c[i, aa:N] = -in_c[i, : (N - aa)] # sur que N>i-a>=0 230 | -------------------------------------------------------------------------------- /tfhe/tgsw.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .numeric_functions import int64_to_int32 7 | from .polynomials import ( 8 | IntPolynomialArray, 9 | LagrangeHalfCPolynomialArray, 10 | TorusPolynomialArray, 11 | int_polynomial_ifft, 12 | ) 13 | from .tlwe import ( 14 | TLWEKey, 15 | TLWEParams, 16 | TLWESampleArray, 17 | TLWESampleFFTArray, 18 | tlwe_fft_clear, 19 | tlwe_from_fft_convert, 20 | tlwe_sym_encrypt_zero, 21 | tlwe_to_fft_convert, 22 | ) 23 | 24 | 25 | class TGSWParams: 26 | def __init__( 27 | self, l: int, bg_bit: int, tlwe_params: TLWEParams # noqa: E741 28 | ) -> None: 29 | bg = 1 << bg_bit 30 | half_bg = bg // 2 31 | 32 | h = numpy.int32(1) << ( 33 | 32 - numpy.arange(1, l + 1) * bg_bit 34 | ) # 1/(bg^(i+1)) as a Torus32 35 | 36 | # offset = bg/2 * (2^(32-bg_bit) + 2^(32-2*bg_bit) + ... + 2^(32-l*bg_bit)) 37 | offset = int64_to_int32( 38 | cast( 39 | numpy.int64, sum(1 << (32 - numpy.arange(1, l + 1) * bg_bit)) * half_bg 40 | ) 41 | ) 42 | 43 | self.l = l # decomp length # noqa: E741 44 | self.bg_bit = bg_bit # log_2(bg) 45 | self.bg = bg # decomposition base (must be a power of 2) 46 | self.half_bg = half_bg # bg/2 47 | self.mask_mod = bg - 1 # bg-1 48 | self.tlwe_params = tlwe_params # Params of each row 49 | self.kpl = (tlwe_params.k + 1) * l # number of rows = (k+1)*l 50 | self.h = h # powers of bg_bit 51 | self.offset = offset # offset = bg/2 * (2^(32-bg_bit) + 2^(32-2*bg_bit) + ... + 2^(32-l*bg_bit)) # pylint: disable=line-too-long # noqa: E501 52 | 53 | 54 | class TGSWKey: 55 | def __init__(self, rng: numpy.random.RandomState, params: TGSWParams) -> None: 56 | tlwe_key = TLWEKey(rng, params.tlwe_params) 57 | self.params = params # the parameters 58 | self.tlwe_params = params.tlwe_params # the tlwe params of each rows 59 | self.tlwe_key = tlwe_key 60 | 61 | 62 | class TGSWSampleArray: 63 | def __init__(self, params: TGSWParams, shape: Tuple[int, ...]) -> None: 64 | self.k = params.tlwe_params.k 65 | self.l = params.l # noqa: E741 66 | self.samples = TLWESampleArray(params.tlwe_params, shape + (self.k + 1, self.l)) 67 | 68 | 69 | class TGSWSampleFFTArray: 70 | def __init__(self, params: TGSWParams, shape: Tuple[int, ...]) -> None: 71 | self.k = params.tlwe_params.k 72 | self.l = params.l # noqa: E741 73 | self.samples = TLWESampleFFTArray( 74 | params.tlwe_params, shape + (self.k + 1, self.l) 75 | ) 76 | 77 | 78 | # Result += mu*H, mu integer 79 | def tgsw_add_mu_int_h( 80 | result: TGSWSampleArray, messages: NDArray[numpy.int32], params: TGSWParams 81 | ) -> None: 82 | # TYPING: messages::Array{Int32, 1} 83 | 84 | k = params.tlwe_params.k 85 | l = params.l # noqa: E741 86 | h = params.h 87 | 88 | # compute result += H 89 | 90 | # returns an underlying coefs_t of TorusPolynomialArray, with the total size 91 | # (N, k + 1 [from TLweSample], l, k + 1 [from TGswSample], n) 92 | # messages: (n,) 93 | # h: (l,) 94 | # TODO: use an appropriate method # pylint: disable=fixme 95 | # TODO: not sure if it's possible to fully vectorize it # pylint: disable=fixme 96 | for bloc in range(k + 1): 97 | result.samples.a.coefs_t[:, bloc, :, bloc, 0] = result.samples.a.coefs_t[ 98 | :, bloc, :, bloc, 0 99 | ] + messages.reshape(messages.size, 1) * h.reshape(1, l) 100 | 101 | 102 | # Result = tGsw(0) 103 | def tgsw_encrypt_zero( 104 | rng: numpy.random.RandomState, result: TGSWSampleArray, alpha: float, key: TGSWKey 105 | ) -> None: 106 | rlkey = key.tlwe_key 107 | tlwe_sym_encrypt_zero(rng, result.samples, alpha, rlkey) 108 | 109 | 110 | # encrypts a constant message 111 | def tgsw_sym_encrypt_int( 112 | rng: numpy.random.RandomState, 113 | result: TGSWSampleArray, 114 | messages: NDArray[numpy.int32], 115 | alpha: float, 116 | key: TGSWKey, 117 | ) -> None: 118 | # TYPING: messages::Array{Int32, 1} 119 | tgsw_encrypt_zero(rng, result, alpha, key) 120 | tgsw_add_mu_int_h(result, messages, key.params) 121 | 122 | 123 | def tgsw_torus32_polynomial_decomp_h( 124 | result: IntPolynomialArray, sample: TorusPolynomialArray, params: TGSWParams 125 | ) -> None: 126 | # GPU: array operations or (more probably) a custom kernel 127 | 128 | N = params.tlwe_params.N 129 | l = params.l # noqa: E741 130 | bg_bit = params.bg_bit 131 | 132 | mask_mod = params.mask_mod 133 | half_bg = params.half_bg 134 | offset = params.offset 135 | 136 | def decal(p: NDArray[numpy.int32]) -> NDArray[numpy.int32]: 137 | return 32 - p * bg_bit 138 | 139 | ps = numpy.arange(1, l + 1).reshape(1, 1, l, 1) 140 | sample_coefs = sample.coefs_t.reshape(sample.shape + (1, N)) 141 | 142 | # do the decomposition 143 | result.coefs[:, :, :, :] = ( 144 | ((sample_coefs + offset) >> decal(ps)) & mask_mod 145 | ) - half_bg 146 | 147 | 148 | # For all the kpl TLWE samples composing the TGSW sample 149 | # It computes the inverse FFT of the coefficients of the TLWE sample 150 | def tgsw_to_fft_convert(result: TGSWSampleFFTArray, source: TGSWSampleArray) -> None: 151 | tlwe_to_fft_convert(result.samples, source.samples) 152 | 153 | 154 | def tLwe_fft_add_mul_t_to( 155 | res: NDArray[numpy.complex128], 156 | a: NDArray[numpy.complex128], 157 | b: NDArray[numpy.complex128], 158 | bk_idx: int, 159 | ) -> None: 160 | # GPU: array operations or (more probably) a custom kernel 161 | 162 | ml, k_plus1, n_div2 = res.shape 163 | l = a.shape[-2] # noqa: E741 164 | 165 | d = a.reshape(ml, k_plus1, l, 1, n_div2) 166 | for i in range(k_plus1): 167 | for j in range(l): 168 | res += d[:, i, j, :, :] * b[bk_idx, i, j, :, :] 169 | 170 | 171 | # External product (*): accum = gsw (*) accum 172 | def tgsw_fft_extern_mul_to_tlwe( 173 | accum: TLWESampleArray, 174 | gsw: TGSWSampleFFTArray, 175 | bk_idx: int, 176 | params: TGSWParams, 177 | tmp_a: TLWESampleFFTArray, 178 | deca: IntPolynomialArray, 179 | deca_fft: LagrangeHalfCPolynomialArray, 180 | ) -> None: 181 | tgsw_torus32_polynomial_decomp_h(deca, accum.a, params) 182 | 183 | int_polynomial_ifft(deca_fft, deca) 184 | 185 | tlwe_fft_clear(tmp_a) 186 | 187 | res = tmp_a.a.coefs_c 188 | a = deca_fft.coefs_c 189 | b = gsw.samples.a.coefs_c 190 | 191 | tLwe_fft_add_mul_t_to(res, a, b, bk_idx) 192 | 193 | tlwe_from_fft_convert(accum, tmp_a) 194 | -------------------------------------------------------------------------------- /tfhe/tlwe.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, cast 2 | 3 | import numpy 4 | from numpy.typing import NDArray 5 | 6 | from .lwe import LWEParams, LWESampleArray 7 | from .numeric_functions import ( 8 | Torus32, 9 | rand_gaussian_torus32, 10 | rand_uniform_int32, 11 | rand_uniform_torus32, 12 | ) 13 | from .polynomials import ( 14 | IntPolynomialArray, 15 | LagrangeHalfCPolynomialArray, 16 | TorusPolynomialArray, 17 | int_polynomial_ifft, 18 | lagrange_polynomial_clear, 19 | lagrange_polynomial_mul, 20 | torus_polynomial_add_to, 21 | torus_polynomial_clear, 22 | torus_polynomial_fft, 23 | torus_polynomial_ifft, 24 | torus_polynomial_mul_by_xai_minus_one, 25 | ) 26 | 27 | 28 | class TLWEParams: 29 | def __init__(self, N: int, k: int, alpha_min: float, alpha_max: float) -> None: 30 | self.N = N # a power of 2: degree of the polynomials 31 | self.k = k # number of polynomials in the mask 32 | self.alpha_min = alpha_min # minimal noise s.t. the sample is secure 33 | self.alpha_max = alpha_max # maximal noise s.t. we can decrypt 34 | self.extracted_lwe_params = LWEParams( 35 | N * k, alpha_min, alpha_max 36 | ) # lwe params if one extracts 37 | 38 | 39 | class TLWEKey: 40 | def __init__(self, rng: numpy.random.RandomState, params: TLWEParams) -> None: 41 | N = params.N 42 | k = params.k 43 | # GPU: array operation or RNG on device 44 | key = IntPolynomialArray(N, (k,)) 45 | key.coefs[:, :] = rand_uniform_int32(rng, (k, N)) 46 | 47 | self.params = params # the parameters of the key 48 | self.key = key # the key (i.e k binary polynomials) 49 | 50 | 51 | class TLWESampleArray: 52 | def __init__(self, params: TLWEParams, shape: Tuple[int, ...]) -> None: 53 | self.k = params.k 54 | 55 | # array of length k+1: mask + right term 56 | self.a = TorusPolynomialArray(params.N, shape + (self.k + 1,)) 57 | 58 | # avg variance of the sample 59 | self.current_variances = numpy.zeros(shape, numpy.float64) 60 | 61 | self.shape = shape 62 | 63 | 64 | class TLWESampleFFTArray: 65 | def __init__(self, params: TLWEParams, shape: Tuple[int, ...]) -> None: 66 | self.k = params.k 67 | 68 | # array of length k+1: mask + right term 69 | self.a = LagrangeHalfCPolynomialArray(params.N, shape + (self.k + 1,)) 70 | 71 | # avg variance of the sample 72 | self.current_variances = numpy.zeros(shape, numpy.float64) 73 | 74 | self.shape = shape 75 | 76 | 77 | def tlwe_extract_lwe_sample_index( 78 | result: LWESampleArray, 79 | x: TLWESampleArray, 80 | index: int, 81 | params: LWEParams, 82 | r_params: TLWEParams, 83 | ) -> None: 84 | N = r_params.N 85 | k = r_params.k 86 | assert params.n == k * N 87 | 88 | # TODO: use an appropriate method to get coefs_t # pylint: disable=fixme 89 | a_view = result.a.reshape(result.shape + (k, N)) 90 | a_view[:, :, : (index + 1)] = x.a.coefs_t[:, :k, index::-1] 91 | a_view[:, :, (index + 1) :] = -x.a.coefs_t[:, :k, :index:-1] 92 | 93 | numpy.copyto(result.b, x.a.coefs_t[:, k, index]) 94 | 95 | 96 | def tlwe_extract_lwe_sample( 97 | result: LWESampleArray, x: TLWESampleArray, params: LWEParams, r_params: TLWEParams 98 | ) -> None: 99 | tlwe_extract_lwe_sample_index(result, x, 0, params, r_params) 100 | 101 | 102 | # create an homogeneous tlwe sample 103 | def tlwe_sym_encrypt_zero( 104 | rng: numpy.random.RandomState, result: TLWESampleArray, alpha: float, key: TLWEKey 105 | ) -> None: 106 | N = key.params.N 107 | k = key.params.k 108 | 109 | # TODO: use an appropriate method # pylint: disable=fixme 110 | 111 | result.a.coefs_t[:, :, :, k, :] = rand_gaussian_torus32( 112 | rng, cast(Torus32, 0), alpha, result.shape + (N,) 113 | ) 114 | 115 | result.a.coefs_t[:, :, :, :k, :] = rand_uniform_torus32(rng, result.shape + (k, N)) 116 | 117 | tmp1 = LagrangeHalfCPolynomialArray(N, key.key.shape) 118 | tmp2 = LagrangeHalfCPolynomialArray(N, result.shape + (k,)) 119 | tmp3 = LagrangeHalfCPolynomialArray(N, result.shape + (k,)) 120 | tmp_r = TorusPolynomialArray(N, result.shape + (k,)) 121 | 122 | int_polynomial_ifft(tmp1, key.key) 123 | torus_polynomial_ifft( 124 | tmp2, TorusPolynomialArray.from_arr(result.a.coefs_t[:, :, :, :k, :]) 125 | ) 126 | lagrange_polynomial_mul(tmp3, tmp1, tmp2) 127 | torus_polynomial_fft(tmp_r, tmp3) 128 | 129 | for i in range(k): 130 | result.a.coefs_t[:, :, :, k, :] = ( 131 | result.a.coefs_t[:, :, :, k, :] + tmp_r.coefs_t[:, :, :, i, :] 132 | ) 133 | 134 | result.current_variances.fill(alpha**2) 135 | 136 | 137 | # Arithmetic operations on TLwe samples 138 | 139 | 140 | # result = sample 141 | def tlwe_copy(result: TLWESampleArray, sample: TLWESampleArray) -> None: 142 | # GPU: array operations or a custom kernel 143 | numpy.copyto( 144 | result.a.coefs_t, sample.a.coefs_t 145 | ) # TODO: use an appropriate method? # pylint: disable=fixme 146 | numpy.copyto(result.current_variances, sample.current_variances) 147 | 148 | 149 | # result = (0,mu) 150 | def tlwe_noiseless_trivial(result: TLWESampleArray, mu: TorusPolynomialArray) -> None: 151 | # GPU: array operations or a custom kernel 152 | torus_polynomial_clear(result.a) 153 | result.a.coefs_t[ 154 | :, result.k, : 155 | ] = mu.coefs_t # TODO: wrap in a function? # pylint: disable=fixme 156 | result.current_variances.fill(0.0) 157 | 158 | 159 | # result = result + sample 160 | def tlwe_add_to(result: TLWESampleArray, sample: TLWESampleArray) -> None: 161 | # GPU: array operations or a custom kernel 162 | torus_polynomial_add_to(result.a, sample.a) 163 | result.current_variances += sample.current_variances 164 | 165 | 166 | # mult externe de X^ai-1 par bki 167 | def tlwe_mul_by_xai_minus_one( 168 | result: TLWESampleArray, ai: NDArray[numpy.int32], bk: TLWESampleArray 169 | ) -> None: 170 | # TYPING: ai::Array{Int32} 171 | torus_polynomial_mul_by_xai_minus_one(result.a, ai, bk.a) 172 | 173 | 174 | # Computes the inverse FFT of the coefficients of the TLWE sample 175 | def tlwe_to_fft_convert(result: TLWESampleFFTArray, source: TLWESampleArray) -> None: 176 | torus_polynomial_ifft(result.a, source.a) 177 | numpy.copyto(result.current_variances, source.current_variances) 178 | 179 | 180 | # Computes the FFT of the coefficients of the TLWEfft sample 181 | def tlwe_from_fft_convert(result: TLWESampleArray, source: TLWESampleFFTArray) -> None: 182 | torus_polynomial_fft(result.a, source.a) 183 | numpy.copyto(result.current_variances, source.current_variances) 184 | 185 | 186 | # Arithmetic operations on TLwe samples 187 | 188 | 189 | # result = (0,0) 190 | def tlwe_fft_clear(result: TLWESampleFFTArray) -> None: 191 | lagrange_polynomial_clear(result.a) 192 | result.current_variances.fill(0.0) 193 | -------------------------------------------------------------------------------- /tfhe/utils.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from numpy.typing import NDArray 3 | 4 | 5 | def int_to_bitarray(x: int) -> NDArray[numpy.bool_]: 6 | return numpy.array([((x >> i) & 1 != 0) for i in range(16)]) 7 | 8 | 9 | def bitarray_to_int(x: NDArray[numpy.bool_]) -> int: 10 | int_answer = 0 11 | for i in range(16): 12 | int_answer = int_answer | (x[i] << i) 13 | return int_answer 14 | --------------------------------------------------------------------------------