Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/0.1.0 #10

Merged
merged 9 commits into from
Dec 12, 2024
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: 60 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -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"]
8 changes: 8 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "bittensor-commit-reveal"
version = "0.1.0a1"
version = "0.1.0"
description = ""
readme = "README.md"
license = {file = "LICENSE"}
Expand Down
43 changes: 18 additions & 25 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,14 @@ async fn generate_commit(
block_time: u64,
) -> Result<(Vec<u8>, 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";
Expand All @@ -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)),
Expand All @@ -94,15 +88,15 @@ 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());
hasher.finalize().to_vec()
};
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::<TinyBLS381, AESGCMStreamCipherProvider, OsRng>(
pub_key,
Expand All @@ -118,7 +112,7 @@ async fn generate_commit(
)
})?;

// 6. Compress ct
// Compress ciphertext
let mut ct_bytes: Vec<u8> = Vec::new();
ct.serialize_compressed(&mut ct_bytes).map_err(|e| {
(
Expand All @@ -127,7 +121,6 @@ async fn generate_commit(
)
})?;

// 7. Return result
Ok((ct_bytes, reveal_round))
}

Expand Down
197 changes: 197 additions & 0 deletions src/tests/test_commit_reveal.py
Original file line number Diff line number Diff line change
@@ -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
Loading