Skip to content

Commit f2c2b3a

Browse files
authored
Merge pull request #60 from opentensor/feat/roman/python314
Fix unit tests + move workflow to gh
2 parents a04e274 + 379e7e2 commit f2c2b3a

File tree

7 files changed

+112
-102
lines changed

7 files changed

+112
-102
lines changed

.circleci/config.yml

Lines changed: 0 additions & 60 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
uses: PyO3/[email protected]
4848
with:
4949
target: ${{ matrix.platform.target }}
50-
args: --release --out dist --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
50+
args: --release --out dist --interpreter python3.10 python3.11 python3.12 python3.13 python3.14
5151
sccache: 'true'
5252
manylinux: auto
5353
before-script-linux: |
@@ -83,7 +83,7 @@ jobs:
8383
uses: PyO3/[email protected]
8484
with:
8585
target: ${{ matrix.platform.target }}
86-
args: --release --out dist --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
86+
args: --release --out dist --interpreter python3.10 python3.11 python3.12 python3.13 python3
8787
sccache: 'true'
8888

8989
- name: Upload wheels
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Test and Lint
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
ruff:
9+
name: Ruff Linting
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.9.13'
19+
20+
- name: Install Ruff
21+
run: pip install ruff
22+
23+
- name: Run Ruff
24+
run: ruff check .
25+
26+
build-and-test:
27+
name: Build and Test (Python ${{ matrix.python-version }})
28+
runs-on: ubuntu-latest
29+
strategy:
30+
matrix:
31+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
32+
fail-fast: false
33+
34+
steps:
35+
- name: Checkout code
36+
uses: actions/checkout@v4
37+
38+
- name: Set up Python
39+
uses: actions/setup-python@v5
40+
with:
41+
python-version: ${{ matrix.python-version }}
42+
43+
- name: Set up Rust
44+
uses: dtolnay/rust-toolchain@stable
45+
46+
- name: Set Up Virtual Environment
47+
run: |
48+
python -m venv .venv
49+
source .venv/bin/activate
50+
python -m pip install --upgrade pip
51+
python -m pip install '.[dev]'
52+
53+
- name: Create test results directory
54+
run: mkdir -p test-results
55+
56+
- name: Run Tests
57+
run: |
58+
source .venv/bin/activate
59+
pytest tests/
60+
continue-on-error: false
61+

bittensor_drand/__init__.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def encrypt(
9595
"""
9696
return _encrypt(data, n_blocks, block_time)
9797

98+
9899
def encrypt_at_round(data: bytes, reveal_round: int) -> tuple[bytes, int]:
99100
"""Encrypts arbitrary binary data for a specific Drand reveal round.
100101
@@ -128,18 +129,19 @@ def decrypt(encrypted_data: bytes, no_errors: bool = True) -> Optional[bytes]:
128129
"""
129130
return _decrypt(encrypted_data, no_errors)
130131

132+
131133
def decrypt_with_signature(encrypted_data: bytes, signature_hex: str) -> bytes:
132134
"""Decrypts data using a provided Drand signature.
133135
This function is useful when decrypting multiple ciphertexts for the same round,
134136
allowing you to fetch the signature once and reuse it, avoiding redundant API calls.
135-
137+
136138
Arguments:
137139
encrypted_data: The encrypted data to decrypt.
138140
signature_hex: Hex-encoded Drand BLS signature for the reveal round.
139-
141+
140142
Returns:
141143
decrypted_data (bytes): The decrypted data.
142-
144+
143145
Raises:
144146
ValueError: If decryption fails or signature is invalid.
145147
"""
@@ -150,18 +152,19 @@ def get_signature_for_round(reveal_round: int) -> str:
150152
"""Fetches the Drand signature for a specific round.
151153
This is useful for batch decryption scenarios where you want to decrypt
152154
multiple ciphertexts for the same round without making redundant API calls.
153-
155+
154156
Arguments:
155157
reveal_round: The Drand round number to fetch the signature for.
156-
158+
157159
Returns:
158160
signature_hex (str): Hex-encoded BLS signature for the round.
159-
161+
160162
Raises:
161163
ValueError: If the signature cannot be fetched or is not yet available.
162164
"""
163165
return _get_signature_for_round(reveal_round)
164166

167+
165168
def get_latest_round() -> int:
166169
"""Gets the latest revealed Drand round number.
167170

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ classifiers = [
2121
"Topic :: Software Development :: Build Tools",
2222
"License :: OSI Approved :: MIT License",
2323
"Programming Language :: Python :: 3 :: Only",
24-
"Programming Language :: Python :: 3.9",
2524
"Programming Language :: Python :: 3.10",
2625
"Programming Language :: Python :: 3.11",
2726
"Programming Language :: Python :: 3.12",
2827
"Programming Language :: Python :: 3.13",
28+
"Programming Language :: Python :: 3.14",
2929
"Topic :: Scientific/Engineering",
3030
"Topic :: Scientific/Engineering :: Mathematics",
3131
"Topic :: Scientific/Engineering :: Artificial Intelligence",
@@ -49,4 +49,4 @@ exclude = ["tests*"]
4949
dev = [
5050
"maturin==1.7.0",
5151
"pytest-asyncio==0.23.7"
52-
]
52+
]

tests/test_all_functions.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
23
import bittensor_drand as btcr
34

45

@@ -28,33 +29,35 @@ def test_encrypt_and_decrypt():
2829
assert decrypted is not None
2930
assert decrypted == data
3031

32+
3133
def test_encrypt_at_round_and_decrypt():
3234
data = b"test data for specific round"
33-
35+
3436
# Get a round that's already revealed (in the past)
3537
current_round = btcr.get_latest_round()
3638
past_round = current_round - 100 # Use a round from the past
37-
39+
3840
# Encrypt at specific round
3941
encrypted, returned_round = btcr.encrypt_at_round(data, past_round)
4042
assert isinstance(encrypted, bytes)
4143
assert returned_round == past_round
42-
44+
4345
# Should be able to decrypt immediately since the round is in the past
4446
decrypted = btcr.decrypt(encrypted)
4547
assert decrypted is not None
4648
assert decrypted == data
47-
49+
4850
# Test with future round
4951
future_round = current_round + 1000
5052
encrypted_future, returned_future_round = btcr.encrypt_at_round(data, future_round)
5153
assert isinstance(encrypted_future, bytes)
5254
assert returned_future_round == future_round
53-
55+
5456
# Attempting to decrypt future round should fail or return None
5557
decrypted_future = btcr.decrypt(encrypted_future, no_errors=True)
5658
assert decrypted_future is None # Can't decrypt yet
5759

60+
5861
def test_get_signature_for_round():
5962
# Get a past round that's already revealed
6063
current_round = btcr.get_latest_round()
@@ -65,7 +68,7 @@ def test_get_signature_for_round():
6568
assert isinstance(signature, str)
6669
assert len(signature) > 0
6770
# Drand signatures are hex-encoded, so should only contain hex characters
68-
assert all(c in '0123456789abcdef' for c in signature.lower())
71+
assert all(c in "0123456789abcdef" for c in signature.lower())
6972

7073

7174
def test_decrypt_with_signature():
@@ -104,9 +107,7 @@ def test_batch_decryption_optimization():
104107
past_round = current_round - 100
105108

106109
# Encrypt all messages at the same round
107-
encrypted_messages = [
108-
btcr.encrypt_at_round(msg, past_round)[0] for msg in messages
109-
]
110+
encrypted_messages = [btcr.encrypt_at_round(msg, past_round)[0] for msg in messages]
110111

111112
# Fetch signature once
112113
signature = btcr.get_signature_for_round(past_round)
@@ -118,8 +119,11 @@ def test_batch_decryption_optimization():
118119

119120
# Verify all messages decrypted correctly
120121
assert decrypted_messages == messages
121-
print(f"Successfully decrypted {len(messages)} messages with a single signature fetch!")
122-
122+
print(
123+
f"Successfully decrypted {len(messages)} messages with a single signature fetch!"
124+
)
125+
126+
123127
def test_get_encrypted_commitment():
124128
encrypted, round_ = btcr.get_encrypted_commitment("my_commitment", 1)
125129
assert isinstance(encrypted, bytes)
@@ -146,7 +150,7 @@ def test_get_encrypted_commit():
146150
netuid,
147151
subnet_reveal_period_epochs,
148152
block_time,
149-
hotkey
153+
hotkey,
150154
)
151155
assert isinstance(encrypted, bytes)
152156
assert isinstance(round_, int)

tests/test_commit_reveal.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import pytest
21
import time
2+
33
from bittensor_drand import get_encrypted_commit
44

55
SUBTENSOR_PULSE_DELAY = 24
@@ -129,7 +129,7 @@ def test_generate_commit_various_tempos():
129129
NETUID,
130130
SUBNET_REVEAL_PERIOD_EPOCHS,
131131
BLOCK_TIME,
132-
hotkey
132+
hotkey,
133133
)
134134

135135
assert len(ct_pybytes) > 0, f"Ciphertext is empty for tempo {tempo}"
@@ -174,22 +174,24 @@ def compute_expected_reveal_round(
174174
current_epoch = block_with_offset // tempo_plus_one
175175

176176
reveal_epoch = current_epoch + subnet_reveal_period_epochs
177-
reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one
178-
179-
blocks_until_reveal = max(reveal_block_number - current_block, 0)
180-
time_until_reveal = blocks_until_reveal * block_time
181-
182-
while time_until_reveal < SUBTENSOR_PULSE_DELAY * PERIOD:
183-
# If there's at least one block until the reveal, break early and don't force more lead time
184-
if blocks_until_reveal > 0:
185-
break
186-
reveal_epoch += 1
187-
reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one
188-
blocks_until_reveal = max(reveal_block_number - current_block, 0)
189-
time_until_reveal = blocks_until_reveal * block_time
190-
191-
reveal_time = now + time_until_reveal
192-
reveal_round = (
193-
(reveal_time - GENESIS_TIME + PERIOD - 1) // PERIOD
194-
) - SUBTENSOR_PULSE_DELAY
177+
first_reveal_blk = reveal_epoch * tempo_plus_one - netuid_plus_one
178+
179+
# Rust adds SECURITY_BLOCK_OFFSET = 3
180+
SECURITY_BLOCK_OFFSET = 3
181+
target_ingest_blk = first_reveal_blk + SECURITY_BLOCK_OFFSET
182+
183+
blocks_until_ingest = max(target_ingest_blk - current_block, 0)
184+
secs_until_ingest = blocks_until_ingest * block_time
185+
186+
target_secs = now + secs_until_ingest
187+
188+
# Rust uses floor() and does NOT subtract SUBTENSOR_PULSE_DELAY
189+
reveal_round = int((target_secs - GENESIS_TIME) / PERIOD)
190+
191+
if reveal_round < 1:
192+
reveal_round = 1
193+
194+
reveal_time = target_secs
195+
time_until_reveal = secs_until_ingest
196+
195197
return reveal_round, reveal_time, time_until_reveal

0 commit comments

Comments
 (0)