Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 2 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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<T>` form, then `py::array_t<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<double|complex<double>|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**.
2 changes: 1 addition & 1 deletion SEAL
Submodule SEAL updated 230 files
9 changes: 8 additions & 1 deletion seal.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]): ...
Expand Down Expand Up @@ -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."""
...
Expand Down Expand Up @@ -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."""
...


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
10 changes: 7 additions & 3 deletions src/wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PYBIND11_MAKE_OPAQUE(std::vector<std::int64_t>);
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<std::vector<double>>(
m, "VectorDouble", py::buffer_protocol(),
Expand Down Expand Up @@ -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_<EncryptionParameterQualifiers, std::unique_ptr<EncryptionParameterQualifiers, py::nodelete>>(
Expand Down Expand Up @@ -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_),
Expand Down Expand Up @@ -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);
Expand Down
Loading