From 52757e3770aa9fad60289a8e76661ea65564b06f Mon Sep 17 00:00:00 2001 From: max Date: Thu, 30 Jul 2026 23:56:58 -0700 Subject: [PATCH 1/3] Update CLAUDE.md and AGENTS.md --- AGENTS.md | 32 ++------------------------------ CLAUDE.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 778ab57..494e990 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,33 +1,5 @@ # Repository Guidelines -## Project Structure & Module Organization -- `src/wrapper.cpp`: pybind11 bindings that expose Microsoft SEAL C++ APIs as the Python module `seal`. -- `SEAL/`: Microsoft SEAL submodule source and CMake build output (`SEAL/build/...`). -- `pybind11/`: pybind11 submodule headers used by the extension build. -- `examples/`: runnable Python usage samples (for example `4_bgv_basics.py`, `7_serialization.py`). -- Root build files: `setup.py`, `pyproject.toml`, `Dockerfile`, and `README.md`. +Read the following document as the project specification: -## Build, Test, and Development Commands -- `git submodule update --init --recursive`: fetch SEAL and pybind11 submodules. -- `cmake -S SEAL -B SEAL/build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF && cmake --build SEAL/build`: build static SEAL libraries used by the Python extension. -- `python3 setup.py build_ext -i`: build `seal` extension in-place for local development. -- `python3 setup.py install`: install the module into the active environment. -- `cp seal.*.so examples && python3 examples/4_bgv_basics.py`: smoke-test a Linux/macOS build with an example. -- `docker build -t seal-python -f Dockerfile .`: build reproducible container environment. - -## Coding Style & Naming Conventions -- Python: follow PEP 8, 4-space indentation, `snake_case` for functions/variables. -- C++ bindings: keep existing style in `wrapper.cpp` (4-space indentation, grouped bindings by SEAL header/domain). -- Exposed Python symbols should match upstream SEAL naming where practical (for API familiarity). -- Prefer adding small, focused binding blocks rather than large mixed edits. - -## Testing Guidelines -- No formal `tests/` suite is currently checked in; use example scripts as regression checks. -- For binding changes, run at least one arithmetic flow (`examples/4_bgv_basics.py`) and one serialization flow (`examples/7_serialization.py`). -- If adding new behavior, include a minimal runnable example in `examples/` named after the feature. - -## Commit & Pull Request Guidelines -- Recent history favors short imperative subjects (for example: `Update deps`, `Update README.md`, `Update SEAL`). -- Keep commit titles under ~72 characters and focused on one change. -- PRs should include: purpose, build/test commands run, platform used, and any API surface changes. -- Link related issues and include sample output when behavior changes are user-visible. +- ./CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f042fda --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A pybind11 binding that exposes the Microsoft SEAL homomorphic-encryption C++ library as a single Python extension module named `seal`. Published to PyPI as `seal-python`. + +Everything is bound from one translation unit: `src/wrapper.cpp` (~1200 lines, single `PYBIND11_MODULE(seal, m)`). There is no Python-level package — no `seal/` source directory exists; the importable `seal` is the compiled `.so`/`.pyd` sitting in the working directory or site-packages. + +## Build + +The SEAL submodule must be built into static libs **before** `setup.py` will run — `setup.py` globs `SEAL/build/lib/*.a` (`*.lib` on Windows) into `extra_objects` and calls `sys.exit(1)` if none are found. + +```bash +git submodule update --init --recursive # SEAL only; pybind11 comes from pip +pip install numpy pybind11 +cmake -S SEAL -B SEAL/build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF +cmake --build SEAL/build +python3 setup.py build_ext -i # produces seal.*.so in repo root +``` + +The three `-DSEAL_USE_*=OFF` flags are load-bearing: they are used identically in the Dockerfile, in `RELEASE.md`, and in CI, and the `compr_mode_type.zlib`/`.zstd` enum values in `wrapper.cpp` are `#ifdef`-gated on them, so the default build ships compression-mode `none` only. + +Windows requires the **x64 Native Tools Command Prompt for VS** (x64 only) and `-G Ninja`. On macOS, export `MACOSX_DEPLOYMENT_TARGET` before both the CMake and the `setup.py` step so the static lib and the extension agree (see README). + +Docker: `docker build -t huelse/seal -f Dockerfile .` does the whole sequence on Ubuntu 22.04. + +## Test + +There is no test suite. `examples/` doubles as the regression suite; the scripts import `seal` from the current directory, so copy the built module in first: + +```bash +cp seal.*.so examples && cd examples +python3 4_bgv_basics.py # run a single "test" +``` + +After any binding change, run at least one arithmetic flow (`4_bgv_basics.py`) and one serialization flow (`7_serialization.py`). `examples/seal_helper.py` provides the shared `print_parameters` / `print_vector` helpers the numbered examples use. CI's only smoke test is `python -c "import seal; print(seal.__version__)"`. + +## Working on the bindings + +- **`seal.pyi` is hand-maintained and must be edited alongside `src/wrapper.cpp`.** The project ships PEP 561 typing (`seal.pyi` + `py.typed`); `setup.py`'s `build_ext_with_typing` subclass copies both next to the compiled extension on every build. A new or changed binding that isn't mirrored in the stub silently degrades to `Any` for users. +- **The version string lives in two places that must stay in sync**: `__version__` in `setup.py` and `m.attr("__version__")` in `src/wrapper.cpp:21`. +- Bindings are grouped by upstream SEAL header with a `// encryptionparams.h`-style comment marking each block; keep new bindings in the matching block and mirror upstream SEAL names so the API stays familiar. +- Every binding carries a `SEAL_DOC("...")` docstring (the macro is a passthrough) and named `py::arg(...)`. Follow that convention. +- **pybind11 overload order matters.** Encoders register the same Python name several times — `std::vector` form, then `py::array_t` (NumPy) form, then `py::iterable` form — and pybind11 tries them in registration order. Insert new overloads with that resolution order in mind; the NumPy paths raise `"E101: Number of dimensions must be one"` for non-1-D input. +- C++ overloads that Python cannot disambiguate get distinct names: complex CKKS encoding is exposed as `encode_complex`, not an `encode` overload. +- Serialization is bound as lambdas taking a **file path string** (`save(path)` / `load(context, path)`), plus a `load_bytes(context, py::bytes)` variant — not raw C++ streams. Supported on `EncryptionParameters, Ciphertext, Plaintext, SecretKey, PublicKey, RelinKeys, GaloisKeys`. +- `std::vector|uint64_t|int64_t>` are `PYBIND11_MAKE_OPAQUE`'d and bound as `VectorDouble` / `VectorComplex` / `VectorUInt` / `VectorInt` with the buffer protocol, so they do not implicitly convert to/from Python lists. + +## Release + +`RELEASE.md` has the full checklist. `.github/workflows/wheels.yml` builds cibuildwheel wheels (cp38–cp314, x86_64 Linux / AMD64 Windows / auto64 macOS, no musllinux or PyPy) plus an sdist on every push and PR, and publishes to PyPI via trusted publishing only on a **published GitHub release**. From 7c8d30a20f594f21cc443833521cd8e200ac7f0d Mon Sep 17 00:00:00 2001 From: max Date: Thu, 30 Jul 2026 23:57:57 -0700 Subject: [PATCH 2/3] Update SEAL to 4.4.0 --- SEAL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SEAL b/SEAL index 7a931d5..04d53b9 160000 --- a/SEAL +++ b/SEAL @@ -1 +1 @@ -Subproject commit 7a931d55ba84a40b85938f6ca3ac206f18654093 +Subproject commit 04d53b99ce745efc26bb4965be609b9894755227 From 69ff0af9af2d4b4b04351c1b2cf0aa52306ee965 Mon Sep 17 00:00:00 2001 From: max Date: Sun, 2 Aug 2026 20:22:59 -0700 Subject: [PATCH 3/3] Sync to 4.4.0, add contains_seed function, rich invariant_noise_budget doc --- seal.pyi | 9 ++++++++- setup.py | 2 +- src/wrapper.cpp | 10 +++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/seal.pyi b/seal.pyi index b44b85d..4fbcbd7 100644 --- a/seal.pyi +++ b/seal.pyi @@ -51,6 +51,7 @@ class error_type(IntEnum): invalid_plain_modulus_too_large: int invalid_plain_modulus_nonzero: int failed_creating_rns_tool: int + invalid_coeff_modulus_non_prime: int class VectorDouble(list[float]): ... @@ -542,6 +543,10 @@ class Ciphertext: """Return whether the ciphertext is transparent.""" ... + def contains_seed(self) -> bool: + """Return whether the ciphertext holds a seeded, serialization-only representation that is not valid for homomorphic computation.""" + ... + def is_ntt_form(self) -> bool: """Return whether the ciphertext is stored in NTT form.""" ... @@ -1158,7 +1163,9 @@ class Decryptor: ... def invariant_noise_budget(self, encrypted: Ciphertext) -> int: - """Return the invariant noise budget of a ciphertext in bits.""" + """Return the invariant noise budget of a ciphertext in bits. Warning: the result depends on the + secret key, so calling this on ciphertexts of unverified provenance can leak the secret key; only + use it as a diagnostic on ciphertexts produced by the caller's own computation.""" ... diff --git a/setup.py b/setup.py index 6d4e657..91ecfd8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ import pybind11 from pybind11.setup_helpers import Pybind11Extension, build_ext -__version__ = "4.1.2.1" +__version__ = "4.4.0" BASE_DIR = Path(__file__).resolve().parent include_dirs = [get_python_inc(), pybind11.get_include(), 'SEAL/native/src', 'SEAL/build/native/src'] diff --git a/src/wrapper.cpp b/src/wrapper.cpp index 42fdf47..20fed25 100644 --- a/src/wrapper.cpp +++ b/src/wrapper.cpp @@ -18,7 +18,7 @@ PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MODULE(seal, m) { m.doc() = "Microsoft SEAL for Python, from https://github.com/Huelse/SEAL-Python"; - m.attr("__version__") = "4.1.2.1"; + m.attr("__version__") = "4.4.0"; py::bind_vector>( m, "VectorDouble", py::buffer_protocol(), @@ -173,7 +173,8 @@ PYBIND11_MODULE(seal, m) .value("invalid_plain_modulus_coprimality", EncryptionParameterQualifiers::error_type::invalid_plain_modulus_coprimality) .value("invalid_plain_modulus_too_large", EncryptionParameterQualifiers::error_type::invalid_plain_modulus_too_large) .value("invalid_plain_modulus_nonzero", EncryptionParameterQualifiers::error_type::invalid_plain_modulus_nonzero) - .value("failed_creating_rns_tool", EncryptionParameterQualifiers::error_type::failed_creating_rns_tool); + .value("failed_creating_rns_tool", EncryptionParameterQualifiers::error_type::failed_creating_rns_tool) + .value("invalid_coeff_modulus_non_prime", EncryptionParameterQualifiers::error_type::invalid_coeff_modulus_non_prime); // context.h py::class_>( @@ -403,6 +404,7 @@ PYBIND11_MODULE(seal, m) .def("size", &Ciphertext::size, SEAL_DOC("Return the number of polynomials in the ciphertext.")) .def("size_capacity", &Ciphertext::size_capacity, SEAL_DOC("Return the allocated ciphertext capacity measured in polynomials.")) .def("is_transparent", &Ciphertext::is_transparent, SEAL_DOC("Return True if the ciphertext is transparent, which is generally insecure.")) + .def("contains_seed", &Ciphertext::contains_seed, SEAL_DOC("Return True if the ciphertext holds a seeded, serialization-only representation that is not valid for homomorphic computation.")) .def("is_ntt_form", py::overload_cast<>(&Ciphertext::is_ntt_form, py::const_), SEAL_DOC("Return True if the ciphertext is stored in NTT form.")) .def("parms_id", py::overload_cast<>(&Ciphertext::parms_id, py::const_), @@ -1087,7 +1089,9 @@ PYBIND11_MODULE(seal, m) .def("decrypt", &Decryptor::decrypt, py::arg("encrypted"), py::arg("destination"), SEAL_DOC("Decrypt a ciphertext into destination.")) .def("invariant_noise_budget", &Decryptor::invariant_noise_budget, py::arg("encrypted"), - SEAL_DOC("Return the invariant noise budget of a ciphertext in bits.")) + SEAL_DOC("Return the invariant noise budget of a ciphertext in bits. Warning: the result depends on the " + "secret key, so calling this on ciphertexts of unverified provenance can leak the secret key; only " + "use it as a diagnostic on ciphertexts produced by the caller's own computation.")) .def("decrypt", [](Decryptor &decryptor, const Ciphertext &encrypted){ Plaintext pt; decryptor.decrypt(encrypted, pt);