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
60 changes: 0 additions & 60 deletions .circleci/config.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
uses: PyO3/[email protected]
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
args: --release --out dist --interpreter python3.10 python3.11 python3.12 python3.13 python3.14
sccache: 'true'
manylinux: auto
before-script-linux: |
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
uses: PyO3/[email protected]
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --interpreter python3.9 python3.10 python3.11 python3.12 python3.13
args: --release --out dist --interpreter python3.10 python3.11 python3.12 python3.13 python3
sccache: 'true'

- name: Upload wheels
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Test and Lint

on:
push:
pull_request:

jobs:
ruff:
name: Ruff Linting
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9.13'

- name: Install Ruff
run: pip install ruff

- name: Run Ruff
run: ruff check .

build-and-test:
name: Build and Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
fail-fast: false

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Set Up Virtual Environment
run: |
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install '.[dev]'

- name: Create test results directory
run: mkdir -p test-results

- name: Run Tests
run: |
source .venv/bin/activate
pytest tests/
continue-on-error: false

15 changes: 9 additions & 6 deletions bittensor_drand/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def encrypt(
"""
return _encrypt(data, n_blocks, block_time)


def encrypt_at_round(data: bytes, reveal_round: int) -> tuple[bytes, int]:
"""Encrypts arbitrary binary data for a specific Drand reveal round.

Expand Down Expand Up @@ -128,18 +129,19 @@ def decrypt(encrypted_data: bytes, no_errors: bool = True) -> Optional[bytes]:
"""
return _decrypt(encrypted_data, no_errors)


def decrypt_with_signature(encrypted_data: bytes, signature_hex: str) -> bytes:
"""Decrypts data using a provided Drand signature.
This function is useful when decrypting multiple ciphertexts for the same round,
allowing you to fetch the signature once and reuse it, avoiding redundant API calls.

Arguments:
encrypted_data: The encrypted data to decrypt.
signature_hex: Hex-encoded Drand BLS signature for the reveal round.

Returns:
decrypted_data (bytes): The decrypted data.

Raises:
ValueError: If decryption fails or signature is invalid.
"""
Expand All @@ -150,18 +152,19 @@ def get_signature_for_round(reveal_round: int) -> str:
"""Fetches the Drand signature for a specific round.
This is useful for batch decryption scenarios where you want to decrypt
multiple ciphertexts for the same round without making redundant API calls.

Arguments:
reveal_round: The Drand round number to fetch the signature for.

Returns:
signature_hex (str): Hex-encoded BLS signature for the round.

Raises:
ValueError: If the signature cannot be fetched or is not yet available.
"""
return _get_signature_for_round(reveal_round)


def get_latest_round() -> int:
"""Gets the latest revealed Drand round number.

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ classifiers = [
"Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Mathematics",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
Expand All @@ -49,4 +49,4 @@ exclude = ["tests*"]
dev = [
"maturin==1.7.0",
"pytest-asyncio==0.23.7"
]
]
28 changes: 16 additions & 12 deletions tests/test_all_functions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import time

import bittensor_drand as btcr


Expand Down Expand Up @@ -28,33 +29,35 @@ def test_encrypt_and_decrypt():
assert decrypted is not None
assert decrypted == data


def test_encrypt_at_round_and_decrypt():
data = b"test data for specific round"

# Get a round that's already revealed (in the past)
current_round = btcr.get_latest_round()
past_round = current_round - 100 # Use a round from the past

# Encrypt at specific round
encrypted, returned_round = btcr.encrypt_at_round(data, past_round)
assert isinstance(encrypted, bytes)
assert returned_round == past_round

# Should be able to decrypt immediately since the round is in the past
decrypted = btcr.decrypt(encrypted)
assert decrypted is not None
assert decrypted == data

# Test with future round
future_round = current_round + 1000
encrypted_future, returned_future_round = btcr.encrypt_at_round(data, future_round)
assert isinstance(encrypted_future, bytes)
assert returned_future_round == future_round

# Attempting to decrypt future round should fail or return None
decrypted_future = btcr.decrypt(encrypted_future, no_errors=True)
assert decrypted_future is None # Can't decrypt yet


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


def test_decrypt_with_signature():
Expand Down Expand Up @@ -104,9 +107,7 @@ def test_batch_decryption_optimization():
past_round = current_round - 100

# Encrypt all messages at the same round
encrypted_messages = [
btcr.encrypt_at_round(msg, past_round)[0] for msg in messages
]
encrypted_messages = [btcr.encrypt_at_round(msg, past_round)[0] for msg in messages]

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

# Verify all messages decrypted correctly
assert decrypted_messages == messages
print(f"Successfully decrypted {len(messages)} messages with a single signature fetch!")

print(
f"Successfully decrypted {len(messages)} messages with a single signature fetch!"
)


def test_get_encrypted_commitment():
encrypted, round_ = btcr.get_encrypted_commitment("my_commitment", 1)
assert isinstance(encrypted, bytes)
Expand All @@ -146,7 +150,7 @@ def test_get_encrypted_commit():
netuid,
subnet_reveal_period_epochs,
block_time,
hotkey
hotkey,
)
assert isinstance(encrypted, bytes)
assert isinstance(round_, int)
42 changes: 22 additions & 20 deletions tests/test_commit_reveal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
import time

from bittensor_drand import get_encrypted_commit

SUBTENSOR_PULSE_DELAY = 24
Expand Down Expand Up @@ -129,7 +129,7 @@ def test_generate_commit_various_tempos():
NETUID,
SUBNET_REVEAL_PERIOD_EPOCHS,
BLOCK_TIME,
hotkey
hotkey,
)

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

reveal_epoch = current_epoch + subnet_reveal_period_epochs
reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one

blocks_until_reveal = max(reveal_block_number - current_block, 0)
time_until_reveal = blocks_until_reveal * block_time

while time_until_reveal < SUBTENSOR_PULSE_DELAY * PERIOD:
# If there's at least one block until the reveal, break early and don't force more lead time
if blocks_until_reveal > 0:
break
reveal_epoch += 1
reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one
blocks_until_reveal = max(reveal_block_number - current_block, 0)
time_until_reveal = blocks_until_reveal * block_time

reveal_time = now + time_until_reveal
reveal_round = (
(reveal_time - GENESIS_TIME + PERIOD - 1) // PERIOD
) - SUBTENSOR_PULSE_DELAY
first_reveal_blk = reveal_epoch * tempo_plus_one - netuid_plus_one

# Rust adds SECURITY_BLOCK_OFFSET = 3
SECURITY_BLOCK_OFFSET = 3
target_ingest_blk = first_reveal_blk + SECURITY_BLOCK_OFFSET

blocks_until_ingest = max(target_ingest_blk - current_block, 0)
secs_until_ingest = blocks_until_ingest * block_time

target_secs = now + secs_until_ingest

# Rust uses floor() and does NOT subtract SUBTENSOR_PULSE_DELAY
reveal_round = int((target_secs - GENESIS_TIME) / PERIOD)

if reveal_round < 1:
reveal_round = 1

reveal_time = target_secs
time_until_reveal = secs_until_ingest

return reveal_round, reveal_time, time_until_reveal
Loading