diff --git a/.github/workflows/maturin.yml b/.github/workflows/maturin.yml deleted file mode 100644 index cc6338a9..00000000 --- a/.github/workflows/maturin.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Maturin - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -env: - python-versions: [ "3.7", "3.8", "3.9", "3.10" ] - -jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [ x86_64 ] - # TODO: Enable these upon stable release - # x86, - # aarch64, - # armv7, - # s390x, - # ppc64le - python-version: ${{ env.python-versions }} - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ferveo-python/Cargo.toml - manylinux: auto - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - windows: - runs-on: windows-latest - strategy: - matrix: - target: [ x64, x86 ] - python-version: ${{ env.python-versions }} - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.target }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ferveo-python/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [ x86_64, aarch64 ] - python-version: ${{ env.python-versions }} - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter --manifest-path ferveo-python/Cargo.toml - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - release: - name: Release - runs-on: ubuntu-latest - # Only runs on tagged versions - if: "startsWith(github.ref, 'refs/tags/')" - needs: [ linux, windows, macos ] - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels - - name: Publish to PyPI - uses: PyO3/maturin-action@v1 - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - with: - command: upload - args: --skip-existing * diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 04be31b8..e14f8e2a 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -175,13 +175,17 @@ jobs: run: python examples/server_api_simple.py working-directory: ferveo-python - - name: Install pytest - run: pip install pytest + - name: Install pip dependencies + run: pip install pytest mypy - name: Run pytest run: pytest working-directory: ferveo-python + - name: Run mypy.stubtest + run: python -m mypy.stubtest ferveo + working-directory: ferveo-python + codecov: runs-on: ubuntu-latest needs: [ test ] diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..c398b0d5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/ferveo-python/Cargo.toml b/ferveo-python/Cargo.toml index 7fc78583..44a81853 100644 --- a/ferveo-python/Cargo.toml +++ b/ferveo-python/Cargo.toml @@ -8,7 +8,6 @@ publish = false [lib] crate-type = ["cdylib"] -name = "ferveo_py" [features] extension-module = ["pyo3/extension-module"] @@ -18,12 +17,5 @@ default = ["extension-module"] ferveo = { package = "ferveo-pre-release", path = "../ferveo", features = ["bindings-python"] } pyo3 = "0.18.2" -# We avoid declaring "pyo3/extension-module" in `dependencies` since it causes compile-time issues: -# https://github.com/PyO3/pyo3/issues/340 -# Instead, we expose it in certain cases: -# https://github.com/PyO3/maturin/issues/325 -[tool.maturin] -features = ["pyo3/extension-module"] - [build-dependencies] pyo3-build-config = "*" diff --git a/ferveo-python/Pipfile b/ferveo-python/Pipfile new file mode 100644 index 00000000..c398b0d5 --- /dev/null +++ b/ferveo-python/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/ferveo-python/README.md b/ferveo-python/README.md index 0e61c107..3d067b6b 100644 --- a/ferveo-python/README.md +++ b/ferveo-python/README.md @@ -3,10 +3,3 @@ ## Build You will need to have `setuptools-rust` installed. Then, for development, you can just do `pip install -e .` as usual. - -## Publish - -```bash -maturin build --release -maturin publish -``` diff --git a/ferveo-python/examples/exception.py b/ferveo-python/examples/exception.py index a653b43a..4e16aea9 100644 --- a/ferveo-python/examples/exception.py +++ b/ferveo-python/examples/exception.py @@ -1,4 +1,4 @@ -from ferveo_py import ( +from ferveo import ( Keypair, Validator, Dkg, diff --git a/ferveo-python/examples/server_api_precomputed.py b/ferveo-python/examples/server_api_precomputed.py index c0973b66..0916738d 100644 --- a/ferveo-python/examples/server_api_precomputed.py +++ b/ferveo-python/examples/server_api_precomputed.py @@ -1,4 +1,4 @@ -from ferveo_py import ( +from ferveo import ( encrypt, combine_decryption_shares_precomputed, decrypt_with_shared_secret, diff --git a/ferveo-python/examples/server_api_simple.py b/ferveo-python/examples/server_api_simple.py index de26ba38..e41c9f24 100644 --- a/ferveo-python/examples/server_api_simple.py +++ b/ferveo-python/examples/server_api_simple.py @@ -1,4 +1,4 @@ -from ferveo_py import ( +from ferveo import ( encrypt, combine_decryption_shares_simple, decrypt_with_shared_secret, diff --git a/ferveo-python/ferveo/__init__.py b/ferveo-python/ferveo/__init__.py index 43b4bbd6..f89aa778 100644 --- a/ferveo-python/ferveo/__init__.py +++ b/ferveo-python/ferveo/__init__.py @@ -1,4 +1,4 @@ -from .ferveo_py import ( +from ._ferveo import ( encrypt, combine_decryption_shares_simple, combine_decryption_shares_precomputed, diff --git a/ferveo-python/ferveo/__init__.pyi b/ferveo-python/ferveo/__init__.pyi index e16189d1..1dfab2f0 100644 --- a/ferveo-python/ferveo/__init__.pyi +++ b/ferveo-python/ferveo/__init__.pyi @@ -1,17 +1,18 @@ -from typing import Sequence +from typing import Sequence, final +@final class Keypair: @staticmethod def random() -> Keypair: ... @staticmethod - def from_secure_randomness(data: bytes) -> Keypair: + def from_secure_randomness(secure_randomness: bytes) -> Keypair: ... @staticmethod - def secure_randomness_size(data: bytes) -> int: + def secure_randomness_size() -> int: ... @staticmethod @@ -25,6 +26,7 @@ class Keypair: ... +@final class FerveoPublicKey: @staticmethod def from_bytes(data: bytes) -> FerveoPublicKey: @@ -36,10 +38,15 @@ class FerveoPublicKey: def __hash__(self) -> int: ... - def __richcmp__(self, other: FerveoPublicKey, op: int) -> bool: + @staticmethod + def serialized_size() -> int: + ... + + def __eq__(self, other: object) -> bool: ... +@final class Validator: def __init__(self, address: str, public_key: FerveoPublicKey): @@ -50,6 +57,7 @@ class Validator: public_key: FerveoPublicKey +@final class Transcript: @staticmethod def from_bytes(data: bytes) -> Transcript: @@ -59,6 +67,7 @@ class Transcript: ... +@final class DkgPublicKey: @staticmethod def from_bytes(data: bytes) -> DkgPublicKey: @@ -67,7 +76,12 @@ class DkgPublicKey: def __bytes__(self) -> bytes: ... + @staticmethod + def serialized_size() -> int: + ... + +@final class ValidatorMessage: def __init__( @@ -81,6 +95,7 @@ class ValidatorMessage: transcript: Transcript +@final class Dkg: def __init__( @@ -102,6 +117,7 @@ class Dkg: ... +@final class Ciphertext: @staticmethod def from_bytes(data: bytes) -> Ciphertext: @@ -111,6 +127,7 @@ class Ciphertext: ... +@final class DecryptionShareSimple: @staticmethod def from_bytes(data: bytes) -> DecryptionShareSimple: @@ -120,6 +137,7 @@ class DecryptionShareSimple: ... +@final class DecryptionSharePrecomputed: @staticmethod def from_bytes(data: bytes) -> DecryptionSharePrecomputed: @@ -129,6 +147,7 @@ class DecryptionSharePrecomputed: ... +@final class AggregatedTranscript: def __init__(self, messages: Sequence[ValidatorMessage]): @@ -163,6 +182,7 @@ class AggregatedTranscript: ... +@final class SharedSecret: @staticmethod @@ -173,15 +193,19 @@ class SharedSecret: ... +@final class FerveoVariant: - @staticmethod - def simple() -> str: ... + Simple: FerveoVariant + Precomputed: FerveoVariant - @staticmethod - def precomputed() -> str: ... + def __eq__(self, other: object) -> bool: + ... + + def __hash__(self) -> int: + ... -def encrypt(message: bytes, add: bytes, dkg_public_key: DkgPublicKey) -> Ciphertext: +def encrypt(message: bytes, aad: bytes, dkg_public_key: DkgPublicKey) -> Ciphertext: ... @@ -267,7 +291,3 @@ class ValidatorPublicKeyMismatch(Exception): class SerializationError(Exception): pass - - -class InvalidVariant(Exception): - pass diff --git a/ferveo-python/pyproject.toml b/ferveo-python/pyproject.toml index a0010648..f92911b2 100644 --- a/ferveo-python/pyproject.toml +++ b/ferveo-python/pyproject.toml @@ -1,14 +1,4 @@ [build-system] -requires = ["maturin>=0.14,<0.15"] -build-backend = "maturin" - -[project] -name = "ferveo" -requires-python = ">=3.7" -classifiers = [ - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] +requires = ["setuptools", "wheel", "setuptools-rust"] diff --git a/ferveo-python/src/lib.rs b/ferveo-python/src/lib.rs index fff55e96..71e29c9f 100644 --- a/ferveo-python/src/lib.rs +++ b/ferveo-python/src/lib.rs @@ -2,6 +2,6 @@ use ferveo::bindings_python::*; use pyo3::prelude::*; #[pymodule] -fn ferveo_py(py: Python, m: &PyModule) -> PyResult<()> { +fn _ferveo(py: Python, m: &PyModule) -> PyResult<()> { make_ferveo_py_module(py, m) } diff --git a/ferveo-python/test/test_ferveo.py b/ferveo-python/test/test_ferveo.py index b92f1c01..7d93c637 100644 --- a/ferveo-python/test/test_ferveo.py +++ b/ferveo-python/test/test_ferveo.py @@ -1,6 +1,6 @@ import pytest -from ferveo_py import ( +from ferveo import ( encrypt, combine_decryption_shares_simple, combine_decryption_shares_precomputed, @@ -11,7 +11,8 @@ Dkg, AggregatedTranscript, DkgPublicKey, - ThresholdEncryptionError + ThresholdEncryptionError, + FerveoVariant ) @@ -19,26 +20,26 @@ def gen_eth_addr(i: int) -> str: return f"0x{i:040x}" -def decryption_share_for_variant(variant, agg_transcript): - if variant == "simple": +def decryption_share_for_variant(v: FerveoVariant, agg_transcript): + if v == FerveoVariant.Simple: return agg_transcript.create_decryption_share_simple - elif variant == "precomputed": + elif v == FerveoVariant.Precomputed: return agg_transcript.create_decryption_share_precomputed else: raise ValueError("Unknown variant") -def combine_shares_for_variant(variant, decryption_shares): - if variant == "simple": +def combine_shares_for_variant(v: FerveoVariant, decryption_shares): + if v == FerveoVariant.Simple: return combine_decryption_shares_simple(decryption_shares) - elif variant == "precomputed": + elif v == FerveoVariant.Precomputed: return combine_decryption_shares_precomputed(decryption_shares) else: raise ValueError("Unknown variant") -def scenario_for_variant(variant, shares_num, threshold, shares_to_use): - if variant not in ["simple", "precomputed"]: +def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_to_use): + if variant not in [FerveoVariant.Simple, FerveoVariant.Precomputed]: raise ValueError("Unknown variant: " + variant) tau = 1 @@ -98,12 +99,12 @@ def scenario_for_variant(variant, shares_num, threshold, shares_to_use): shared_secret = combine_shares_for_variant(variant, decryption_shares) - if variant == "simple" and len(decryption_shares) < threshold: + if variant == FerveoVariant.Simple and len(decryption_shares) < threshold: with pytest.raises(ThresholdEncryptionError): decrypt_with_shared_secret(ciphertext, aad, shared_secret) return - if variant == "precomputed" and len(decryption_shares) < shares_num: + if variant == FerveoVariant.Precomputed and len(decryption_shares) < shares_num: with pytest.raises(ThresholdEncryptionError): decrypt_with_shared_secret(ciphertext, aad, shared_secret) return @@ -113,31 +114,32 @@ def scenario_for_variant(variant, shares_num, threshold, shares_to_use): def test_simple_tdec_has_enough_messages(): - scenario_for_variant("simple", shares_num=4, threshold=3, shares_to_use=3) + scenario_for_variant(FerveoVariant.Simple, shares_num=4, threshold=3, shares_to_use=3) def test_simple_tdec_doesnt_have_enough_messages(): - scenario_for_variant("simple", shares_num=4, threshold=3, shares_to_use=2) + scenario_for_variant(FerveoVariant.Simple, shares_num=4, threshold=3, shares_to_use=2) def test_precomputed_tdec_has_enough_messages(): - scenario_for_variant("precomputed", shares_num=4, threshold=4, shares_to_use=4) + scenario_for_variant(FerveoVariant.Precomputed, shares_num=4, threshold=4, shares_to_use=4) def test_precomputed_tdec_doesnt_have_enough_messages(): - scenario_for_variant("precomputed", shares_num=4, threshold=4, shares_to_use=3) + scenario_for_variant(FerveoVariant.Precomputed, shares_num=4, threshold=4, shares_to_use=3) PARAMS = [ - (1, 'simple'), - (4, 'simple'), - (8, 'simple'), - (32, 'simple'), - (1, 'precomputed'), - (4, 'precomputed'), - (8, 'precomputed'), - (32, 'precomputed'), - + (1, FerveoVariant.Simple), + (3, FerveoVariant.Simple), + (4, FerveoVariant.Simple), + (7, FerveoVariant.Simple), + (8, FerveoVariant.Simple), + (1, FerveoVariant.Precomputed), + (3, FerveoVariant.Precomputed), + (4, FerveoVariant.Precomputed), + (7, FerveoVariant.Precomputed), + (8, FerveoVariant.Precomputed), ] TEST_CASES_WITH_THRESHOLD_RANGE = [] diff --git a/ferveo-python/test/test_serialization.py b/ferveo-python/test/test_serialization.py index 6b564be2..8533d437 100644 --- a/ferveo-python/test/test_serialization.py +++ b/ferveo-python/test/test_serialization.py @@ -1,4 +1,4 @@ -from ferveo_py import ( +from ferveo import ( Keypair, Validator, Dkg, @@ -81,5 +81,8 @@ def test_public_key_serialization(): def test_ferveo_variant_serialization(): - assert FerveoVariant.precomputed() == "FerveoVariant::Precomputed" - assert FerveoVariant.simple() == "FerveoVariant::Simple" + assert str(FerveoVariant.Precomputed) == "FerveoVariant::Precomputed" + assert str(FerveoVariant.Simple) == "FerveoVariant::Simple" + assert FerveoVariant.Precomputed == FerveoVariant.Precomputed + assert FerveoVariant.Simple == FerveoVariant.Simple + assert FerveoVariant.Precomputed != FerveoVariant.Simple diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index 10b283a7..ccf9ff5f 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -21,6 +21,10 @@ pub type Validator = crate::Validator; pub type Transcript = PubliclyVerifiableSS; pub type ValidatorMessage = (Validator, Transcript); +#[cfg(feature = "bindings-python")] +use crate::bindings_python; +#[cfg(feature = "bindings-wasm")] +use crate::bindings_wasm; pub use crate::EthereumAddress; use crate::{ do_verify_aggregation, Error, PVSSMap, PubliclyVerifiableParams, @@ -70,7 +74,9 @@ pub fn decrypt_with_shared_secret( } /// The ferveo variant to use for the decryption share derivation. -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Copy, Clone)] +#[derive( + PartialEq, Eq, Debug, Serialize, Deserialize, Copy, Clone, PartialOrd, +)] pub enum FerveoVariant { /// The simple variant requires m of n shares to decrypt Simple, @@ -101,6 +107,20 @@ impl FerveoVariant { } } +#[cfg(feature = "bindings-python")] +impl From for FerveoVariant { + fn from(variant: bindings_python::FerveoVariant) -> Self { + variant.0 + } +} + +#[cfg(feature = "bindings-wasm")] +impl From for FerveoVariant { + fn from(variant: bindings_wasm::FerveoVariant) -> Self { + variant.0 + } +} + #[serde_as] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DkgPublicKey( diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index cf19ebc9..7d1cb93b 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -1,4 +1,7 @@ -use std::fmt::{Debug, Formatter}; +use std::{ + fmt, + fmt::{Debug, Formatter}, +}; use ferveo_common::serialization::{FromBytes, ToBytes}; use pyo3::{ @@ -177,8 +180,9 @@ macro_rules! generate_bytes_serialization { #[pymethods] impl $struct_name { #[staticmethod] - pub fn from_bytes(bytes: &[u8]) -> PyResult { - from_py_bytes(bytes).map(Self) + #[pyo3(signature = (data))] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_py_bytes(data).map(Self) } fn __bytes__(&self) -> PyResult { @@ -193,12 +197,11 @@ macro_rules! generate_boxed_bytes_serialization { #[pymethods] impl $struct_name { #[staticmethod] - pub fn from_bytes(bytes: &[u8]) -> PyResult { - Ok($struct_name( - $inner_struct_name::from_bytes(bytes).map_err(|err| { - FerveoPythonError::Other(err.to_string()) - })?, - )) + #[pyo3(signature = (data))] + pub fn from_bytes(data: &[u8]) -> PyResult { + Ok($struct_name($inner_struct_name::from_bytes(data).map_err( + |err| FerveoPythonError::Other(err.to_string()), + )?)) } fn __bytes__(&self) -> PyResult { @@ -235,9 +238,9 @@ pub fn encrypt( #[pyfunction] pub fn combine_decryption_shares_simple( - shares: Vec, + decryption_shares: Vec, ) -> SharedSecret { - let shares = shares + let shares = decryption_shares .iter() .map(|share| share.0.clone()) .collect::>(); @@ -247,9 +250,9 @@ pub fn combine_decryption_shares_simple( #[pyfunction] pub fn combine_decryption_shares_precomputed( - shares: Vec, + decryption_shares: Vec, ) -> SharedSecret { - let shares = shares + let shares = decryption_shares .iter() .map(|share| share.0.clone()) .collect::>(); @@ -268,18 +271,45 @@ pub fn decrypt_with_shared_secret( } #[pyclass(module = "ferveo")] -struct FerveoVariant {} +#[derive( + Clone, PartialEq, PartialOrd, Eq, derive_more::From, derive_more::AsRef, +)] +pub struct FerveoVariant(pub(crate) api::FerveoVariant); #[pymethods] impl FerveoVariant { - #[staticmethod] - fn precomputed() -> &'static str { - api::FerveoVariant::Precomputed.as_str() + #[classattr] + #[pyo3(name = "Precomputed")] + fn precomputed() -> FerveoVariant { + api::FerveoVariant::Precomputed.into() } - #[staticmethod] - fn simple() -> &'static str { - api::FerveoVariant::Simple.as_str() + #[classattr] + #[pyo3(name = "Simple")] + fn simple() -> FerveoVariant { + api::FerveoVariant::Simple.into() + } + + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { + richcmp(self, other, op) + } + + fn __hash__(&self) -> PyResult { + let bytes = self + .0 + .to_bytes() + .map_err(|err| FerveoPythonError::Other(err.to_string()))?; + hash("FerveoVariant", &bytes) + } +} + +impl fmt::Display for FerveoVariant { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) } } @@ -303,9 +333,10 @@ impl Keypair { } #[staticmethod] - pub fn from_secure_randomness(bytes: &[u8]) -> PyResult { - let keypair = api::Keypair::from_secure_randomness(bytes) - .map_err(|err| FerveoPythonError::Other(err.to_string()))?; + pub fn from_secure_randomness(secure_randomness: &[u8]) -> PyResult { + let keypair = + api::Keypair::from_secure_randomness(secure_randomness) + .map_err(|err| FerveoPythonError::Other(err.to_string()))?; Ok(Self(keypair)) } diff --git a/ferveo/src/bindings_wasm.rs b/ferveo/src/bindings_wasm.rs index ab610160..7b9ae484 100644 --- a/ferveo/src/bindings_wasm.rs +++ b/ferveo/src/bindings_wasm.rs @@ -162,18 +162,33 @@ macro_rules! generate_common_methods { } #[wasm_bindgen] -pub struct FerveoVariant {} +#[derive(Clone, Debug, derive_more::AsRef, derive_more::From)] +pub struct FerveoVariant(pub(crate) api::FerveoVariant); + +impl fmt::Display for FerveoVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +generate_common_methods!(FerveoVariant); #[wasm_bindgen] impl FerveoVariant { #[wasm_bindgen(js_name = "precomputed", getter)] - pub fn precomputed() -> String { - api::FerveoVariant::Precomputed.as_str().to_string() + pub fn precomputed() -> FerveoVariant { + FerveoVariant(api::FerveoVariant::Precomputed) } #[wasm_bindgen(js_name = "simple", getter)] - pub fn simple() -> String { - api::FerveoVariant::Simple.as_str().to_string() + pub fn simple() -> FerveoVariant { + FerveoVariant(api::FerveoVariant::Simple) + } + + #[allow(clippy::inherent_to_string_shadow_display)] + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> String { + self.0.to_string() } }