Skip to content

Commit 538de5a

Browse files
authored
Merge branch 'staging' into fix/zyzniewski/async_raise_error
2 parents 55899a6 + 2703462 commit 538de5a

File tree

4 files changed

+214
-2
lines changed

4 files changed

+214
-2
lines changed

bittensor/core/timelock.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import struct
2+
import time
3+
from typing import Optional, Union
4+
5+
from bittensor_commit_reveal import (
6+
encrypt as _btr_encrypt,
7+
decrypt as _btr_decrypt,
8+
get_latest_round,
9+
)
10+
11+
TLE_ENCRYPTED_DATA_SUFFIX = b"AES_GCM_"
12+
13+
14+
def encrypt(
15+
data: Union[bytes, str], n_blocks: int, block_time: Union[int, float] = 12.0
16+
) -> tuple[bytes, int]:
17+
"""Encrypts data using TimeLock Encryption
18+
19+
Arguments:
20+
data: Any bytes data to be encrypted.
21+
n_blocks: Number of blocks to encrypt.
22+
block_time: Time in seconds for each block. Default is `12.0` seconds.
23+
24+
Returns:
25+
tuple: A tuple containing the encrypted data and reveal TimeLock reveal round.
26+
27+
Raises:
28+
PyValueError: If failed to encrypt data.
29+
30+
Usage:
31+
data = "From Cortex to Bittensor"
32+
33+
# default usage
34+
encrypted_data, reveal_round = encrypt(data, 10)
35+
36+
# passing block_time for fast-blocks node
37+
encrypted_data, reveal_round = encrypt(data, 15, block_time=0.25)
38+
39+
encrypted_data, reveal_round = encrypt(data, 5)
40+
41+
42+
Note:
43+
For using this function with fast-blocks node you need to set block_time to 0.25 seconds.
44+
data, round = encrypt(data, n_blocks, block_time=0.25)
45+
"""
46+
if isinstance(data, str):
47+
data = data.encode()
48+
return _btr_encrypt(data, n_blocks, block_time)
49+
50+
51+
def decrypt(
52+
encrypted_data: bytes, no_errors: bool = True, return_str: bool = False
53+
) -> Optional[Union[bytes, str]]:
54+
"""Decrypts encrypted data using TimeLock Decryption
55+
56+
Arguments:
57+
encrypted_data: Encrypted data to be decrypted.
58+
no_errors: If True, no errors will be raised during decryption.
59+
return_str: convert decrypted data to string if `True`. Default is `False`.
60+
61+
Returns:
62+
decrypted_data: Decrypted data, when reveled round is reached.
63+
64+
Usage:
65+
# default usage
66+
decrypted_data = decrypt(encrypted_data)
67+
68+
# passing no_errors=False for raising errors during decryption
69+
decrypted_data = decrypt(encrypted_data, no_errors=False)
70+
71+
# passing return_str=True for returning decrypted data as string
72+
decrypted_data = decrypt(encrypted_data, return_str=True)
73+
"""
74+
result = _btr_decrypt(encrypted_data, no_errors)
75+
if result is None:
76+
return None
77+
if return_str:
78+
return result.decode()
79+
return result
80+
81+
82+
def wait_reveal_and_decrypt(
83+
encrypted_data: bytes,
84+
reveal_round: Optional[int] = None,
85+
no_errors: bool = True,
86+
return_str: bool = False,
87+
) -> bytes:
88+
"""
89+
Waits for reveal round and decrypts data using TimeLock Decryption.
90+
91+
Arguments:
92+
encrypted_data: Encrypted data to be decrypted.
93+
reveal_round: Reveal round to wait for. If None, will be parsed from encrypted data.
94+
no_errors: If True, no errors will be raised during decryption.
95+
return_str: convert decrypted data to string if `True`. Default is `False`.
96+
97+
Raises:
98+
struct.error: If failed to parse reveal round from encrypted data.
99+
TypeError: If reveal_round is None or wrong type.
100+
IndexError: If provided encrypted_data does not contain reveal round.
101+
102+
Returns:
103+
bytes: Decrypted data.
104+
105+
Usage:
106+
import bittensor as bt
107+
encrypted, reveal_round = bt.timelock.encrypt("Cortex is power", 3)
108+
"""
109+
if reveal_round is None:
110+
try:
111+
reveal_round = struct.unpack(
112+
"<Q", encrypted_data.split(TLE_ENCRYPTED_DATA_SUFFIX)[-1]
113+
)[0]
114+
except (struct.error, TypeError, IndexError):
115+
raise ValueError("Failed to parse reveal round from encrypted data.")
116+
117+
while get_latest_round() <= reveal_round:
118+
# sleep Drand QuickNet period time (3 sec)
119+
time.sleep(3)
120+
121+
return decrypt(encrypted_data, no_errors, return_str)
122+
123+
124+
__all__ = [
125+
"decrypt",
126+
"encrypt",
127+
"get_latest_round",
128+
"wait_reveal_and_decrypt",
129+
]

bittensor/utils/easy_imports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828
from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401
2929

30-
from bittensor.core import settings
30+
from bittensor.core import settings, timelock # noqa: F401
3131
from bittensor.core.async_subtensor import AsyncSubtensor
3232
from bittensor.core.axon import Axon
3333
from bittensor.core.chain_data import ( # noqa: F401

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ dependencies = [
3434
"pydantic>=2.3, <3",
3535
"scalecodec==1.2.11",
3636
"uvicorn",
37-
"bittensor-commit-reveal>=0.3.1",
37+
"bittensor-commit-reveal>=0.4.0",
3838
"bittensor-wallet>=3.0.8",
3939
"async-substrate-interface>=1.1.0"
4040
]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import struct
2+
import time
3+
4+
import pytest
5+
6+
from bittensor.core import timelock
7+
8+
9+
def test_encrypt_returns_valid_tuple():
10+
"""Test that encrypt() returns a (bytes, int) tuple."""
11+
encrypted, reveal_round = timelock.encrypt("Bittensor", n_blocks=1)
12+
assert isinstance(encrypted, bytes)
13+
assert isinstance(reveal_round, int)
14+
assert reveal_round > 0
15+
16+
17+
def test_encrypt_with_fast_block_time():
18+
"""Test encrypt() with fast-blocks mode (block_time = 0.25s)."""
19+
encrypted, reveal_round = timelock.encrypt("Fast mode", 5, block_time=0.25)
20+
assert isinstance(encrypted, bytes)
21+
assert isinstance(reveal_round, int)
22+
23+
24+
def test_decrypt_returns_bytes_or_none():
25+
"""Test that decrypt() returns bytes after reveal round, or None before."""
26+
data = b"Decode me"
27+
encrypted, reveal_round = timelock.encrypt(data, 1)
28+
29+
current_round = timelock.get_latest_round()
30+
if current_round < reveal_round:
31+
decrypted = timelock.decrypt(encrypted)
32+
assert decrypted is None
33+
else:
34+
decrypted = timelock.decrypt(encrypted)
35+
assert decrypted == data
36+
37+
38+
def test_decrypt_raises_if_no_errors_false_and_invalid_data():
39+
"""Test that decrypt() raises an error on invalid data when no_errors=False."""
40+
with pytest.raises(Exception):
41+
timelock.decrypt(b"corrupt data", no_errors=False)
42+
43+
44+
def test_decrypt_with_return_str():
45+
"""Test decrypt() with return_str=True returns a string."""
46+
plaintext = "Stringified!"
47+
encrypted, _ = timelock.encrypt(plaintext, 1, block_time=0.25)
48+
result = timelock.decrypt(encrypted, no_errors=True, return_str=True)
49+
if result is not None:
50+
assert isinstance(result, str)
51+
52+
53+
def test_get_latest_round_is_monotonic():
54+
"""Test that get_latest_round() is monotonic over time."""
55+
r1 = timelock.get_latest_round()
56+
time.sleep(3)
57+
r2 = timelock.get_latest_round()
58+
assert r2 >= r1
59+
60+
61+
def test_wait_reveal_and_decrypt_auto_round():
62+
"""Test wait_reveal_and_decrypt() without explicit reveal_round."""
63+
msg = "Reveal and decrypt test"
64+
encrypted, _ = timelock.encrypt(msg, 1)
65+
result = timelock.wait_reveal_and_decrypt(encrypted, return_str=True)
66+
assert result == msg
67+
68+
69+
def test_wait_reveal_and_decrypt_manual_round():
70+
"""Test wait_reveal_and_decrypt() with explicit reveal_round."""
71+
msg = "Manual round decryption"
72+
encrypted, reveal_round = timelock.encrypt(msg, 1)
73+
result = timelock.wait_reveal_and_decrypt(encrypted, reveal_round, return_str=True)
74+
assert result == msg
75+
76+
77+
def test_unpack_reveal_round_struct():
78+
"""Test that reveal_round can be extracted from encrypted data."""
79+
encrypted, reveal_round = timelock.encrypt("parse test", 1)
80+
parsed = struct.unpack(
81+
"<Q", encrypted.split(timelock.TLE_ENCRYPTED_DATA_SUFFIX)[-1]
82+
)[0]
83+
assert parsed == reveal_round

0 commit comments

Comments
 (0)