diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 4bdb4eb..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,60 +0,0 @@ -version: 2.1 - -orbs: - python: circleci/python@2.1.1 - -jobs: - ruff: - resource_class: small - parameters: - python-version: - type: string - docker: - - image: cimg/python:<< parameters.python-version >> - steps: - - checkout - - run: - name: Install Ruff - command: pip install ruff - - run: - name: Run Ruff - command: ruff check . - - build-and-test: - resource_class: medium - parallelism: 2 - parameters: - python-version: - type: string - docker: - - image: cimg/python:<< parameters.python-version >> - steps: - - checkout - - run: - name: Set Up Virtual Environment - command: | - curl https://sh.rustup.rs -sSf | sh -s -- -y - . "$HOME/.cargo/env" - python -m venv .venv - . .venv/bin/activate - python -m pip install --upgrade pip - python -m pip install '.[dev]' - - run: - name: Run Tests - command: | - . .venv/bin/activate - pytest tests/ - - store_test_results: - path: test-results - - store_artifacts: - path: test-results - -workflows: - test-and-lint: - jobs: - - ruff: - python-version: "3.9.13" - - build-and-test: - matrix: - parameters: - python-version: ["3.9", "3.10", "3.11", "3.12"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0bbbdc..35134ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: uses: PyO3/maturin-action@v1.44.0 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: | @@ -83,7 +83,7 @@ jobs: uses: PyO3/maturin-action@v1.44.0 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 diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml new file mode 100644 index 0000000..7ce23c8 --- /dev/null +++ b/.github/workflows/test-and-lint.yml @@ -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 + diff --git a/bittensor_drand/__init__.py b/bittensor_drand/__init__.py index 90af0a7..954a465 100644 --- a/bittensor_drand/__init__.py +++ b/bittensor_drand/__init__.py @@ -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. @@ -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. """ @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 57e069f..c7c27d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -49,4 +49,4 @@ exclude = ["tests*"] dev = [ "maturin==1.7.0", "pytest-asyncio==0.23.7" -] \ No newline at end of file +] diff --git a/tests/test_all_functions.py b/tests/test_all_functions.py index 6e5dc3b..fe70c53 100644 --- a/tests/test_all_functions.py +++ b/tests/test_all_functions.py @@ -1,4 +1,5 @@ import time + import bittensor_drand as btcr @@ -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() @@ -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(): @@ -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) @@ -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) @@ -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) diff --git a/tests/test_commit_reveal.py b/tests/test_commit_reveal.py index f6a9506..1d3f9f1 100644 --- a/tests/test_commit_reveal.py +++ b/tests/test_commit_reveal.py @@ -1,5 +1,5 @@ -import pytest import time + from bittensor_drand import get_encrypted_commit SUBTENSOR_PULSE_DELAY = 24 @@ -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}" @@ -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