diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4bdb4eb --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,60 @@ +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/CHANGELOG.MD b/CHANGELOG.MD index 9d0f8c5..3d87327 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,13 @@ # Changelog +## v0.1.0 /2024-12-12 + +## What's Changed +* fix reveal_round calculation edge cases by @JohnReedV in https://github.com/opentensor/bittensor-commit-reveal/pull/8 +* Adds circleci test config by @ibraheem-opentensor in https://github.com/opentensor/bittensor-commit-reveal/pull/9 + +**Full Changelog**: https://github.com/opentensor/bittensor-commit-reveal/compare/v0.1.0a1...v0.1.0 + ## v0.1.0a1 /2024-12-03 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index a779fb4..b9303df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bittensor-commit-reveal" -version = "0.1.0a1" +version = "0.1.0" description = "" readme = "README.md" license = {file = "LICENSE"} diff --git a/src/lib.rs b/src/lib.rs index 0532057..0fe5eee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,18 +33,14 @@ async fn generate_commit( block_time: u64, ) -> Result<(Vec, u64), (std::io::Error, String)> { // Steps comes from here https://github.com/opentensor/subtensor/pull/982/files#diff-7261bf1c7f19fc66a74c1c644ec2b4b277a341609710132fb9cd5f622350a6f5R120-R131 - // 1 Instantiate payload + // Instantiate payload let payload = WeightsTlockPayload { uids, values, version_key, }; - - // 2 Serialize payload let serialized_payload = payload.encode(); - // Calculate reveal_round - // all of 3 variables are constants for drand quicknet let period = 3; let genesis_time = 1692803367; let public_key = "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"; @@ -54,31 +50,29 @@ async fn generate_commit( .unwrap() .as_secs(); - // Compute the current epoch index let tempo_plus_one = tempo + 1; let netuid_plus_one = (netuid as u64) + 1; let block_with_offset = current_block + netuid_plus_one; let current_epoch = block_with_offset / tempo_plus_one; - // Compute the reveal epoch - let reveal_epoch = current_epoch + subnet_reveal_period_epochs; - - // Compute the block number when the reveal epoch starts - let reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one; - - // Compute the number of blocks until the reveal epoch - let blocks_until_reveal = reveal_block_number.saturating_sub(current_block); - - // Compute the time until the reveal in seconds - let time_until_reveal = blocks_until_reveal * block_time; + // Calculate reveal epoch and ensure enough time for SUBTENSOR_PULSE_DELAY pulses + let mut reveal_epoch = current_epoch + subnet_reveal_period_epochs; + let mut reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one; + let mut blocks_until_reveal = reveal_block_number.saturating_sub(current_block); + let mut time_until_reveal = blocks_until_reveal * block_time; + + // Ensure at least SUBTENSOR_PULSE_DELAY * period seconds lead time + while time_until_reveal < SUBTENSOR_PULSE_DELAY * period { + reveal_epoch += 1; + reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one; + blocks_until_reveal = reveal_block_number.saturating_sub(current_block); + time_until_reveal = blocks_until_reveal * block_time; + } - // Compute the reveal time in seconds since UNIX_EPOCH let reveal_time = now + time_until_reveal; - - // Compute the reveal round, ensuring we round up let reveal_round = ((reveal_time - genesis_time + period - 1) / period) - SUBTENSOR_PULSE_DELAY; - // 3. Deserialize the public key + // Deserialize public key let pub_key_bytes = hex::decode(public_key).map_err(|e| { ( std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}", e)), @@ -94,7 +88,7 @@ async fn generate_commit( ) })?; - // 4 Create identity + // Create identity from reveal_round let message = { let mut hasher = sha2::Sha256::new(); hasher.update(reveal_round.to_be_bytes()); @@ -102,7 +96,7 @@ async fn generate_commit( }; let identity = Identity::new(b"", vec![message]); - // 5. Encryption via tle with t-lock under the hood + // Encrypt payload let esk = [2; 32]; let ct = tle::( pub_key, @@ -118,7 +112,7 @@ async fn generate_commit( ) })?; - // 6. Compress ct + // Compress ciphertext let mut ct_bytes: Vec = Vec::new(); ct.serialize_compressed(&mut ct_bytes).map_err(|e| { ( @@ -127,7 +121,6 @@ async fn generate_commit( ) })?; - // 7. Return result Ok((ct_bytes, reveal_round)) } diff --git a/src/tests/test_commit_reveal.py b/src/tests/test_commit_reveal.py new file mode 100644 index 0000000..fd07524 --- /dev/null +++ b/src/tests/test_commit_reveal.py @@ -0,0 +1,197 @@ +import pytest +import time +from bittensor_commit_reveal import get_encrypted_commit + +SUBTENSOR_PULSE_DELAY = 24 +PERIOD = 3 # Drand period in seconds +GENESIS_TIME = 1692803367 + + +def test_get_encrypted_commits(): + uids = [1, 2] + weights = [11, 22] + version_key = 50 + tempo = 100 + current_block = 1000 + netuid = 1 + reveal_period = 2 + block_time = 12 + + start_time = int(time.time()) + ct_pybytes, reveal_round = get_encrypted_commit( + uids, + weights, + version_key, + tempo, + current_block, + netuid, + reveal_period, + block_time, + ) + + # Basic checks + assert ( + ct_pybytes is not None and len(ct_pybytes) > 0 + ), "Ciphertext should not be empty" + assert reveal_round > 0, "Reveal round should be positive" + + expected_reveal_round, _, _ = compute_expected_reveal_round( + start_time, tempo, current_block, netuid, reveal_period, block_time + ) + + # The reveal_round should be close to what we predict + assert ( + abs(reveal_round - expected_reveal_round) <= 1 + ), f"Reveal round {reveal_round} not close to expected {expected_reveal_round}" + + +def test_generate_commit_success(): + uids = [1, 2, 3] + values = [10, 20, 30] + version_key = 42 + tempo = 50 + current_block = 500 + netuid = 100 + subnet_reveal_period_epochs = 2 + block_time = 12 + + start_time = int(time.time()) + ct_pybytes, reveal_round = get_encrypted_commit( + uids, + values, + version_key, + tempo, + current_block, + netuid, + subnet_reveal_period_epochs, + block_time, + ) + + assert ( + ct_pybytes is not None and len(ct_pybytes) > 0 + ), "Ciphertext should not be empty" + assert reveal_round > 0, "Reveal round should be positive" + + expected_reveal_round, expected_reveal_time, time_until_reveal = ( + compute_expected_reveal_round( + start_time, + tempo, + current_block, + netuid, + subnet_reveal_period_epochs, + block_time, + ) + ) + + assert ( + abs(reveal_round - expected_reveal_round) <= 1 + ), f"Reveal round {reveal_round} differs from expected {expected_reveal_round}" + + required_lead_time = SUBTENSOR_PULSE_DELAY * PERIOD + computed_reveal_time = ( + GENESIS_TIME + (reveal_round + SUBTENSOR_PULSE_DELAY) * PERIOD + ) + assert computed_reveal_time - start_time >= required_lead_time, ( + "Not enough lead time before reveal. " + f"computed_reveal_time={computed_reveal_time}, start_time={start_time}, required={required_lead_time}" + ) + + assert ( + time_until_reveal >= SUBTENSOR_PULSE_DELAY * PERIOD + ), f"time_until_reveal {time_until_reveal} is less than required {SUBTENSOR_PULSE_DELAY * PERIOD}" + + +@pytest.mark.asyncio +async def test_generate_commit_various_tempos(): + NETUID = 1 + CURRENT_BLOCK = 100_000 + SUBNET_REVEAL_PERIOD_EPOCHS = 1 + BLOCK_TIME = 6 + TEMPOS = [10, 50, 100, 250, 360, 500, 750, 1000] + + uids = [0] + values = [100] + version_key = 1 + + for tempo in TEMPOS: + start_time = int(time.time()) + + ct_pybytes, reveal_round = get_encrypted_commit( + uids, + values, + version_key, + tempo, + CURRENT_BLOCK, + NETUID, + SUBNET_REVEAL_PERIOD_EPOCHS, + BLOCK_TIME, + ) + + assert len(ct_pybytes) > 0, f"Ciphertext is empty for tempo {tempo}" + assert reveal_round > 0, f"Reveal round is zero or negative for tempo {tempo}" + + expected_reveal_round, _, time_until_reveal = compute_expected_reveal_round( + start_time, + tempo, + CURRENT_BLOCK, + NETUID, + SUBNET_REVEAL_PERIOD_EPOCHS, + BLOCK_TIME, + ) + + assert ( + abs(reveal_round - expected_reveal_round) <= 1 + ), f"Tempo {tempo}: reveal_round {reveal_round} not close to expected {expected_reveal_round}" + + computed_reveal_time = ( + GENESIS_TIME + (reveal_round + SUBTENSOR_PULSE_DELAY) * PERIOD + ) + required_lead_time = SUBTENSOR_PULSE_DELAY * PERIOD + + assert computed_reveal_time - start_time >= required_lead_time, ( + f"Tempo {tempo}: Not enough lead time: reveal_time={computed_reveal_time}, " + f"start_time={start_time}, required={required_lead_time}" + ) + + assert ( + time_until_reveal >= SUBTENSOR_PULSE_DELAY * PERIOD + ), f"Tempo {tempo}: time_until_reveal {time_until_reveal} is less than required {SUBTENSOR_PULSE_DELAY * PERIOD}" + + +def compute_expected_reveal_round( + now: int, + tempo: int, + current_block: int, + netuid: int, + subnet_reveal_period_epochs: int, + block_time: int, +): + tempo_plus_one = tempo + 1 + netuid_plus_one = netuid + 1 + block_with_offset = current_block + netuid_plus_one + current_epoch = block_with_offset // tempo_plus_one + + # Initial guess for reveal_epoch + reveal_epoch = current_epoch + subnet_reveal_period_epochs + reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one + + # Compute blocks_until_reveal, ensure non-negative + blocks_until_reveal = reveal_block_number - current_block + if blocks_until_reveal < 0: + blocks_until_reveal = 0 + time_until_reveal = blocks_until_reveal * block_time + + # Adjust until we have enough lead time (at least SUBTENSOR_PULSE_DELAY pulses * period seconds) + while time_until_reveal < SUBTENSOR_PULSE_DELAY * PERIOD: + reveal_epoch += 1 + reveal_block_number = reveal_epoch * tempo_plus_one - netuid_plus_one + blocks_until_reveal = reveal_block_number - current_block + if blocks_until_reveal < 0: + blocks_until_reveal = 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 + return reveal_round, reveal_time, time_until_reveal