Skip to content
Open
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
13 changes: 9 additions & 4 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ permissions:

jobs:
test:
name: Run Unit Tests
name: Run Unit Tests (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- '3.12'
- '3.14t'
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: ${{ matrix.python-version }}
- name: Build wheel
uses: PyO3/maturin-action@v1
with:
command: build
args: -o dist -i python3.12
args: -o dist -i python
- name: Install wheel
run: |
pip install dist/*.whl
Expand All @@ -42,7 +47,7 @@ jobs:
- name: Run tests
run: |
pip install pytest
pytest tests.py
python -m pytest tests.py
linux:
runs-on: ${{ matrix.platform.runner }}
needs: test
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ pip install py-ed25519-zebra-bindings
pip install -r requirements.txt
maturin develop
```

The extension supports free-threaded CPython. To build and test it with Python
3.14t while keeping the GIL disabled:

```
uv venv --python 3.14t
uv pip install -r requirements.txt
uv pip install .
.venv/bin/python -X gil=0 -m unittest -v tests.py
```

### Build wheels
```
pip install -r requirements.txt
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["maturin>=1.7.0,<1.10.0"]
requires = ["maturin>=1.14.0,<2.0.0"]
build-backend = "maturin"

[project]
Expand All @@ -22,7 +22,9 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13"
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Free Threading :: 3 - Stable"
]

[project.urls]
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Rust->Python build process
maturin~=1.7
maturin~=1.14
toml
# Dev-only
bumpversion
pytest
py-bip39-bindings~=0.1
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub fn ed_verify(signature: &[u8], message: &[u8], public: &[u8]) -> bool {


/// This module is a Python module implemented in Rust.
#[pymodule]
#[pymodule(gil_used = false)]
fn ed25519_zebra(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(ed_from_seed, m)?)?;
m.add_function(wrap_pyfunction!(ed_sign, m)?)?;
Expand Down
69 changes: 63 additions & 6 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import subprocess
import sys
import sysconfig
import threading
import unittest
from concurrent.futures import ThreadPoolExecutor

import bip39
import ed25519_zebra


TEST_SEED = bytes.fromhex(
"31625bbf7c317c00d063f829c483c360737fab5210cdbb2d14c328165b15d180",
)


def exercise_crypto_concurrently(barrier, seed, worker_id):
barrier.wait()
private_key, public_key = ed25519_zebra.ed_from_seed(seed)
derived_public_key = ed25519_zebra.ed_public_from_secret(private_key)

for iteration in range(100):
message = f"worker-{worker_id}-iteration-{iteration}".encode()
signature = ed25519_zebra.ed_sign(private_key, message)
if not ed25519_zebra.ed_verify(signature, message, public_key):
return False

return public_key == derived_public_key


class MyTestCase(unittest.TestCase):
def test_sign_and_verify_message(self):

message = b"test"

# Get private and public key from seed
seed = bip39.bip39_to_mini_secret('daughter song common combine misery cotton audit morning stuff weasel flee field','')
private_key, public_key = ed25519_zebra.ed_from_seed(bytes(seed))
private_key, public_key = ed25519_zebra.ed_from_seed(TEST_SEED)

# Generate signature
signature = ed25519_zebra.ed_sign(private_key, message)
Expand Down Expand Up @@ -55,9 +77,7 @@ def test_verify_invalid_public_key(self):

def test_verify_invalid_signature(self):
# Get private and public key from seed
seed = bip39.bip39_to_mini_secret(
'daughter song common combine misery cotton audit morning stuff weasel flee field', '')
private_key, public_key = ed25519_zebra.ed_from_seed(bytes(seed))
private_key, public_key = ed25519_zebra.ed_from_seed(TEST_SEED)

# Verify message with signature
self.assertFalse(ed25519_zebra.ed_verify(bytes(32), b"test", public_key))
Expand All @@ -74,6 +94,43 @@ def test_public_from_private(self):
public_key = ed25519_zebra.ed_public_from_secret(private_key)
self.assertEqual(bytes.fromhex("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"), public_key)

def test_concurrent_signing_and_verification(self):
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1:
self.assertFalse(sys._is_gil_enabled())

worker_count = 16
barrier = threading.Barrier(worker_count)
seed = bytes(range(32))

with ThreadPoolExecutor(max_workers=worker_count) as executor:
results = executor.map(
exercise_crypto_concurrently,
[barrier] * worker_count,
[seed] * worker_count,
range(worker_count),
)

self.assertTrue(all(results))

@unittest.skipUnless(
sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
"requires a free-threaded Python build",
)
def test_import_does_not_enable_gil(self):
result = subprocess.run(
[
sys.executable,
"-X",
"gil=0",
"-c",
"import ed25519_zebra, sys; assert not sys._is_gil_enabled()",
],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)


if __name__ == '__main__':
unittest.main()