diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 3915538a..e436b4ca 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -41,3 +41,29 @@ jobs: python -m pip install tox - run: | python -m tox run -r + + windows: + runs-on: windows-latest + strategy: + matrix: + python-version: ['3.11'] # Using a stable Python version for Windows testing + toxenv: [core, wheel] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Test with tox + shell: bash + run: | + if [[ "${{ matrix.toxenv }}" == "wheel" ]]; then + python -m tox run -e windows-wheel + else + python -m tox run -e py311-${{ matrix.toxenv }} + fi diff --git a/libp2p/crypto/ecc.py b/libp2p/crypto/ecc.py index 2afec9a4..ec31bc3e 100644 --- a/libp2p/crypto/ecc.py +++ b/libp2p/crypto/ecc.py @@ -1,11 +1,4 @@ -from fastecdsa import ( - keys, - point, -) -from fastecdsa import curve as curve_types -from fastecdsa.encoding.sec1 import ( - SEC1Encoder, -) +import sys from libp2p.crypto.keys import ( KeyPair, @@ -14,67 +7,126 @@ from libp2p.crypto.keys import ( PublicKey, ) +if sys.platform != "win32": + from fastecdsa import ( + keys, + point, + ) + from fastecdsa import curve as curve_types + from fastecdsa.encoding.sec1 import ( + SEC1Encoder, + ) +else: + from coincurve import PrivateKey as CPrivateKey + from coincurve import PublicKey as CPublicKey -def infer_local_type(curve: str) -> curve_types.Curve: + +def infer_local_type(curve: str) -> object: """ - Convert a ``str`` representation of some elliptic curve to a + Convert a str representation of some elliptic curve to a representation understood by the backend of this module. """ - if curve == "P-256": + if curve != "P-256": + raise NotImplementedError("Only P-256 curve is supported") + + if sys.platform != "win32": return curve_types.P256 - else: - raise NotImplementedError() + return "P-256" # coincurve only supports P-256 -class ECCPublicKey(PublicKey): - def __init__(self, impl: point.Point, curve: curve_types.Curve) -> None: - self.impl = impl - self.curve = curve +if sys.platform != "win32": - def to_bytes(self) -> bytes: - return SEC1Encoder.encode_public_key(self.impl, compressed=False) + class ECCPublicKey(PublicKey): + def __init__(self, impl: point.Point, curve: curve_types.Curve) -> None: + self.impl = impl + self.curve = curve - @classmethod - def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey": - curve_type = infer_local_type(curve) - public_key_impl = SEC1Encoder.decode_public_key(data, curve_type) - return cls(public_key_impl, curve_type) + def to_bytes(self) -> bytes: + return SEC1Encoder.encode_public_key(self.impl, compressed=False) - def get_type(self) -> KeyType: - return KeyType.ECC_P256 + @classmethod + def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey": + curve_type = infer_local_type(curve) + public_key_impl = SEC1Encoder.decode_public_key(data, curve_type) + return cls(public_key_impl, curve_type) - def verify(self, data: bytes, signature: bytes) -> bool: - raise NotImplementedError() + def get_type(self) -> KeyType: + return KeyType.ECC_P256 + def verify(self, data: bytes, signature: bytes) -> bool: + raise NotImplementedError() -class ECCPrivateKey(PrivateKey): - def __init__(self, impl: int, curve: curve_types.Curve) -> None: - self.impl = impl - self.curve = curve + class ECCPrivateKey(PrivateKey): + def __init__(self, impl: int, curve: curve_types.Curve) -> None: + self.impl = impl + self.curve = curve - @classmethod - def new(cls, curve: str) -> "ECCPrivateKey": - curve_type = infer_local_type(curve) - private_key_impl = keys.gen_private_key(curve_type) - return cls(private_key_impl, curve_type) + @classmethod + def new(cls, curve: str) -> "ECCPrivateKey": + curve_type = infer_local_type(curve) + private_key_impl = keys.gen_private_key(curve_type) + return cls(private_key_impl, curve_type) - def to_bytes(self) -> bytes: - return keys.export_key(self.impl, self.curve) + def to_bytes(self) -> bytes: + return keys.export_key(self.impl, self.curve) - def get_type(self) -> KeyType: - return KeyType.ECC_P256 + def get_type(self) -> KeyType: + return KeyType.ECC_P256 - def sign(self, data: bytes) -> bytes: - raise NotImplementedError() + def sign(self, data: bytes) -> bytes: + raise NotImplementedError() - def get_public_key(self) -> PublicKey: - public_key_impl = keys.get_public_key(self.impl, self.curve) - return ECCPublicKey(public_key_impl, self.curve) + def get_public_key(self) -> PublicKey: + public_key_impl = keys.get_public_key(self.impl, self.curve) + return ECCPublicKey(public_key_impl, self.curve) + +else: + + class ECCPublicKey(PublicKey): + def __init__(self, impl: CPublicKey, curve: str) -> None: + self.impl = impl + self.curve = curve + + def to_bytes(self) -> bytes: + return self.impl.format(compressed=False) + + @classmethod + def from_bytes(cls, data: bytes, curve: str) -> "ECCPublicKey": + curve_type = infer_local_type(curve) + return cls(CPublicKey(data), curve_type) # type: ignore[arg-type] + + def get_type(self) -> KeyType: + return KeyType.ECC_P256 + + def verify(self, data: bytes, signature: bytes) -> bool: + raise NotImplementedError() + + class ECCPrivateKey(PrivateKey): + def __init__(self, impl: CPrivateKey, curve: str) -> None: + self.impl = impl + self.curve = curve + + @classmethod + def new(cls, curve: str) -> "ECCPrivateKey": + curve_type = infer_local_type(curve) + return cls(CPrivateKey(), curve_type) # type: ignore[arg-type] + + def to_bytes(self) -> bytes: + return self.impl.secret + + def get_type(self) -> KeyType: + return KeyType.ECC_P256 + + def sign(self, data: bytes) -> bytes: + raise NotImplementedError() + + def get_public_key(self) -> PublicKey: + return ECCPublicKey(self.impl.public_key, self.curve) def create_new_key_pair(curve: str) -> KeyPair: """ - Return a new ECC keypair with the requested ``curve`` type, e.g. + Return a new ECC keypair with the requested curve type, e.g. "P-256". """ private_key = ECCPrivateKey.new(curve) diff --git a/libp2p/crypto/key_exchange.py b/libp2p/crypto/key_exchange.py index 4fe7373f..5a713fd3 100644 --- a/libp2p/crypto/key_exchange.py +++ b/libp2p/crypto/key_exchange.py @@ -1,11 +1,26 @@ +import sys from typing import ( Callable, cast, ) -from fastecdsa.encoding import ( - util, -) +if sys.platform != "win32": + from fastecdsa.encoding import ( + util, + ) + + int_bytelen = util.int_bytelen +else: + from math import ( + ceil, + log2, + ) + + def int_bytelen(n: int) -> int: + if n == 0: + return 1 + return ceil(log2(abs(n) + 1) / 8) + from libp2p.crypto.ecc import ( ECCPrivateKey, @@ -18,8 +33,6 @@ from libp2p.crypto.keys import ( SharedKeyGenerator = Callable[[bytes], bytes] -int_bytelen = util.int_bytelen - def create_ephemeral_key_pair(curve_type: str) -> tuple[PublicKey, SharedKeyGenerator]: """Facilitates ECDH key exchange.""" @@ -32,9 +45,15 @@ def create_ephemeral_key_pair(curve_type: str) -> tuple[PublicKey, SharedKeyGene private_key = cast(ECCPrivateKey, key_pair.private_key) remote_point = ECCPublicKey.from_bytes(serialized_remote_public_key, curve_type) - secret_point = remote_point.impl * private_key.impl - secret_x_coordinate = secret_point.x - byte_size = int_bytelen(secret_x_coordinate) - return secret_x_coordinate.to_bytes(byte_size, byteorder="big") + + if sys.platform != "win32": + secret_point = remote_point.impl * private_key.impl + secret_x_coordinate = secret_point.x + byte_size = int_bytelen(secret_x_coordinate) + return secret_x_coordinate.to_bytes(byte_size, byteorder="big") + else: + # Windows implementation using coincurve + shared_key = private_key.impl.ecdh(remote_point.impl.public_key) + return shared_key return key_pair.public_key, _key_exchange diff --git a/newsfragments/498.docs.rst b/newsfragments/498.docs.rst new file mode 100644 index 00000000..472adea8 --- /dev/null +++ b/newsfragments/498.docs.rst @@ -0,0 +1 @@ +Updates ``Feature Breakdown`` in ``README`` to more closely match the list of standard modules. diff --git a/newsfragments/507.bugfix.rst b/newsfragments/507.bugfix.rst new file mode 100644 index 00000000..eb12ea83 --- /dev/null +++ b/newsfragments/507.bugfix.rst @@ -0,0 +1 @@ +Added Windows compatibility by using coincurve instead of fastecdsa on Windows platforms diff --git a/newsfragments/513.bugfix.rst b/newsfragments/513.bugfix.rst new file mode 100644 index 00000000..404eee93 --- /dev/null +++ b/newsfragments/513.bugfix.rst @@ -0,0 +1 @@ +Fixed import path in the examples to use updated `net_stream` module path, resolving ModuleNotFoundError when running the examples. diff --git a/newsfragments/522.internal.rst b/newsfragments/522.internal.rst new file mode 100644 index 00000000..3ec35f80 --- /dev/null +++ b/newsfragments/522.internal.rst @@ -0,0 +1 @@ +Fixes broken CI lint run, bumps ``pre-commit-hooks`` version to ``5.0.0`` and ``mdformat`` to ``0.7.22``. diff --git a/setup.py b/setup.py index 6b3a71e8..67b2c31d 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,19 @@ #!/usr/bin/env python -import os +import sys from setuptools import ( find_packages, setup, ) +description = "libp2p: The Python implementation of the libp2p networking stack" + +# Platform-specific dependencies +if sys.platform == "win32": + crypto_requires = [] # We'll use coincurve instead of fastecdsa on Windows +else: + crypto_requires = ["fastecdsa==1.7.5"] + extras_require = { "dev": [ "build>=0.9.0", @@ -35,23 +43,11 @@ extras_require["dev"] = ( extras_require["dev"] + extras_require["docs"] + extras_require["test"] ) -fastecdsa = [ - # No official fastecdsa==1.7.4,1.7.5 wheels for Windows, using a pypi package that includes - # the original library, but also windows-built wheels (32+64-bit) on those versions. - # Fixme: Remove section when fastecdsa has released a windows-compatible wheel - # (specifically: both win32 and win_amd64 targets) - # See the following issues for more information; - # https://github.com/libp2p/py-libp2p/issues/363 - # https://github.com/AntonKueltz/fastecdsa/issues/11 - "fastecdsa-any==1.7.5;sys_platform=='win32'", - # Wheels are provided for these platforms, or compiling one is minimally frustrating in a - # default python installation. - "fastecdsa==1.7.5;sys_platform!='win32'", -] - -with open("./README.md") as readme: - long_description = readme.read() - +try: + with open("./README.md", encoding="utf-8") as readme: + long_description = readme.read() +except FileNotFoundError: + long_description = description install_requires = [ "base58>=1.0.3", @@ -70,19 +66,14 @@ install_requires = [ "trio>=0.26.0", ] - -# NOTE: Some dependencies break RTD builds. We can not install system dependencies on the -# RTD system so we have to exclude these dependencies when we are in an RTD environment. -readthedocs_is_building = os.environ.get("READTHEDOCS", False) -if not readthedocs_is_building: - install_requires.extend(fastecdsa) - +# Add platform-specific dependencies +install_requires.extend(crypto_requires) setup( name="libp2p", # *IMPORTANT*: Don't manually change the version here. See Contributing docs for the release process. version="0.2.3", - description="""libp2p: The Python implementation of the libp2p networking stack""", + description=description, long_description=long_description, long_description_content_type="text/markdown", author="The Ethereum Foundation", @@ -111,7 +102,7 @@ setup( "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ], - platforms=["unix", "linux", "osx"], + platforms=["unix", "linux", "osx", "win32"], entry_points={ "console_scripts": [ "chat-demo=examples.chat.chat:main",