1#![allow(clippy::arithmetic_side_effects)]
10
11pub mod constants;
12pub mod epoch_schedule;
13#[cfg(test)]
14mod epoch_schedule_vectors;
15
16use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
17use codec::{Decode, Encode};
18use rand_core::{OsRng, RngCore};
19use serde::Deserialize;
20use sha2::Digest;
21#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
24use std::time::{SystemTime, UNIX_EPOCH};
25use subtensor_macros::freeze_struct;
26use tle::{
27 curves::drand::TinyBLS381,
28 ibe::fullident::Identity,
29 stream_ciphers::AESGCMStreamCipherProvider,
30 tlock::{tld, tle, TLECiphertext},
31};
32use w3f_bls::EngineBLS;
33#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
34use web_time::{SystemTime, UNIX_EPOCH};
35
36use crate::error::CoreError;
37#[cfg(feature = "host")]
38use constants::{DRAND_ENDPOINTS, QUICKNET_CHAIN_HASH};
39use constants::{DRAND_PERIOD, DRAND_PUBLIC_KEY, GENESIS_TIME, SECURITY_BLOCK_OFFSET};
40
41fn tl_err(msg: impl Into<String>) -> CoreError {
42 CoreError::Crypto(msg.into())
43}
44
45fn now_unix() -> Result<std::time::Duration, CoreError> {
46 SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .map_err(|e| tl_err(format!("SystemTime error: {e:?}")))
49}
50
51#[freeze_struct("705b0f6dde3ed6e")]
52#[derive(Encode, Decode, Debug, PartialEq)]
53pub struct WeightsTlockPayload {
54 pub hotkey: Vec<u8>,
55 pub uids: Vec<u16>,
56 pub values: Vec<u16>,
57 pub version_key: u64,
58}
59
60#[freeze_struct("b96b617eb03c11a6")]
61#[derive(Encode, Decode)]
62pub struct UserData {
63 pub encrypted_data: Vec<u8>,
64 pub reveal_round: u64,
65}
66
67fn decode_user_data(encrypted_data: &[u8]) -> Result<UserData, CoreError> {
68 let mut input = encrypted_data;
69 let user_data = UserData::decode(&mut input)
70 .map_err(|e| tl_err(format!("Error deserializing data: {e:?}")))?;
71 if !input.is_empty() {
72 return Err(tl_err("Error deserializing data: trailing bytes"));
73 }
74 Ok(user_data)
75}
76
77#[derive(Deserialize)]
78pub struct DrandResponse {
79 pub round: u64,
80 pub signature: String,
81}
82
83pub fn reveal_round(encrypted_data: &[u8]) -> Result<u64, CoreError> {
88 let user_data = decode_user_data(encrypted_data)?;
89 Ok(user_data.reveal_round)
90}
91
92pub fn inner_ciphertext(encrypted_data: &[u8]) -> Result<Vec<u8>, CoreError> {
98 let user_data = decode_user_data(encrypted_data)?;
99 Ok(user_data.encrypted_data)
100}
101
102pub fn encrypt_and_compress(
105 serialized_data: &[u8],
106 reveal_round: u64,
107) -> Result<Vec<u8>, CoreError> {
108 let pub_key_bytes = hex::decode(DRAND_PUBLIC_KEY)
109 .map_err(|e| tl_err(format!("Decoding public key failed: {e:?}")))?;
110 let pub_key =
111 <TinyBLS381 as EngineBLS>::PublicKeyGroup::deserialize_compressed(&*pub_key_bytes)
112 .map_err(|e| tl_err(format!("Deserializing public key failed: {e:?}")))?;
113
114 let message = {
116 let mut hasher = sha2::Sha256::new();
117 hasher.update(reveal_round.to_be_bytes());
118 hasher.finalize().to_vec()
119 };
120 let identity = Identity::new(b"", vec![message]);
121
122 let mut esk = [0u8; 32];
124 OsRng.fill_bytes(&mut esk);
125 let ct = tle::<TinyBLS381, AESGCMStreamCipherProvider, OsRng>(
126 pub_key,
127 esk,
128 serialized_data,
129 identity,
130 OsRng,
131 )
132 .map_err(|e| tl_err(format!("Encryption failed: {e:?}")))?;
133
134 let mut ct_bytes: Vec<u8> = Vec::new();
136 ct.serialize_compressed(&mut ct_bytes)
137 .map_err(|e| tl_err(format!("Ciphertext serialization failed: {e:?}")))?;
138
139 Ok(ct_bytes)
140}
141
142pub fn decrypt_and_decompress(
144 encrypted_data: &[u8],
145 signature_bytes: &[u8],
146) -> Result<Vec<u8>, CoreError> {
147 let ciphertext = TLECiphertext::<TinyBLS381>::deserialize_compressed(encrypted_data)
148 .map_err(|e| tl_err(format!("Error deserializing ciphertext: {e:?}")))?;
149
150 let sign = <TinyBLS381 as EngineBLS>::SignatureGroup::deserialize_compressed(signature_bytes)
151 .map_err(|e| tl_err(format!("Signature deserialization error: {e:?}")))?;
152
153 tld::<TinyBLS381, AESGCMStreamCipherProvider>(ciphertext, sign)
154 .map_err(|e| tl_err(format!("Error decrypting ciphertext: {e:?}")))
155}
156
157pub fn generate_commit_v2(
161 uids: Vec<u16>,
162 values: Vec<u16>,
163 version_key: u64,
164 state: epoch_schedule::EpochScheduleState,
165 subnet_reveal_period_epochs: u64,
166 block_time: f64,
167 hotkey: Vec<u8>,
168) -> Result<(Vec<u8>, u64), CoreError> {
169 let first_reveal_blk =
170 epoch_schedule::predict_first_reveal_block(&state, subnet_reveal_period_epochs)
171 .map_err(|e| tl_err(e.to_string()))?;
172
173 let target_ingest_blk = first_reveal_blk.saturating_add(SECURITY_BLOCK_OFFSET);
174 let blocks_until_ingest = target_ingest_blk.saturating_sub(state.current_block);
175 let secs_until_ingest = blocks_until_ingest as f64 * block_time;
176
177 let now_secs = now_unix()?.as_secs_f64();
178
179 let target_secs = now_secs + secs_until_ingest;
180 let mut reveal_round =
181 ((target_secs - GENESIS_TIME as f64) / DRAND_PERIOD as f64).floor() as u64;
182 if reveal_round < 1 {
183 reveal_round = 1;
184 }
185
186 let payload = WeightsTlockPayload {
187 hotkey,
188 uids,
189 values,
190 version_key,
191 };
192
193 let ct_bytes = encrypt_and_compress(&payload.encode(), reveal_round)?;
194 Ok((ct_bytes, reveal_round))
195}
196
197pub fn encrypt_commitment(
200 data: &str,
201 blocks_until_reveal: u64,
202 block_time: f64,
203) -> Result<(Vec<u8>, u64), CoreError> {
204 let serialized_data = data.encode();
205
206 let now = now_unix()?.as_secs();
207 let reveal_round = ((now - GENESIS_TIME)
208 + (blocks_until_reveal as f64 * block_time).round() as u64)
209 / DRAND_PERIOD;
210
211 let ct_bytes = encrypt_and_compress(&serialized_data, reveal_round)?;
212 Ok((ct_bytes, reveal_round))
213}
214
215pub fn encrypt_n_blocks(
218 data: &[u8],
219 n_blocks: u64,
220 block_time: f64,
221) -> Result<(Vec<u8>, u64), CoreError> {
222 let now = now_unix()?.as_secs_f64();
223 let reveal_timestamp = (n_blocks as f64 * block_time + now).ceil() as u64 - GENESIS_TIME;
224 let reveal_round = reveal_timestamp / DRAND_PERIOD;
225 encrypt_at_round(data, reveal_round)
226}
227
228pub fn encrypt_at_round(data: &[u8], reveal_round: u64) -> Result<(Vec<u8>, u64), CoreError> {
231 let encrypted_data = encrypt_and_compress(data, reveal_round)?;
232 let encrypted_with_reveal_round = UserData {
233 encrypted_data,
234 reveal_round,
235 }
236 .encode();
237 Ok((encrypted_with_reveal_round, reveal_round))
238}
239
240#[cfg(feature = "host")]
242pub fn get_round_info(round: Option<u64>) -> Result<DrandResponse, CoreError> {
243 let mut last_error = None;
244
245 for endpoint in DRAND_ENDPOINTS.iter() {
246 let url = match round {
247 Some(r) => format!("{}/{}/public/{}", endpoint, QUICKNET_CHAIN_HASH, r),
248 None => format!("{}/{}/public/latest", endpoint, QUICKNET_CHAIN_HASH),
249 };
250
251 let response = match reqwest::blocking::get(&url) {
252 Ok(resp) => resp,
253 Err(e) => {
254 last_error = Some(format!("Connection error to {}: {}", endpoint, e));
255 continue;
256 }
257 };
258
259 match response.json::<DrandResponse>() {
260 Ok(parsed) => return Ok(parsed),
261 Err(e) => {
262 last_error = Some(format!("Parsing error from {}: {}", endpoint, e));
263 continue;
264 }
265 }
266 }
267
268 Err(tl_err(last_error.unwrap_or_else(|| {
269 "Failed to get data from all Drand endpoints".to_string()
270 })))
271}
272
273#[cfg(feature = "host")]
276pub fn get_reveal_round_signature(
277 reveal_round: Option<u64>,
278 no_errors: bool,
279) -> Result<Option<String>, CoreError> {
280 match get_round_info(reveal_round) {
281 Ok(r) => Ok(Some(r.signature)),
282 Err(e) if no_errors => {
283 let _ = e;
284 Ok(None)
285 }
286 Err(e) => Err(tl_err(format!(
287 "Failed to get Drand round {reveal_round:?}: {e}"
288 ))),
289 }
290}
291
292#[cfg(feature = "host")]
298pub fn decrypt(encrypted_data: &[u8], no_errors: bool) -> Result<Option<Vec<u8>>, CoreError> {
299 let user_data = match decode_user_data(encrypted_data) {
300 Ok(data) => data,
301 Err(_) if no_errors => return Ok(None),
302 Err(e) => return Err(e),
303 };
304
305 let Some(signature_hex) = get_reveal_round_signature(Some(user_data.reveal_round), no_errors)?
306 else {
307 return if no_errors {
308 Ok(None)
309 } else {
310 Err(tl_err("Signature not available"))
311 };
312 };
313
314 decrypt_with_signature_hex(&user_data.encrypted_data, &signature_hex).map(Some)
315}
316
317pub fn decrypt_with_signature(
320 encrypted_data: &[u8],
321 signature_hex: &str,
322) -> Result<Vec<u8>, CoreError> {
323 let user_data = decode_user_data(encrypted_data)?;
324 decrypt_with_signature_hex(&user_data.encrypted_data, signature_hex)
325}
326
327fn decrypt_with_signature_hex(encrypted: &[u8], signature_hex: &str) -> Result<Vec<u8>, CoreError> {
328 let signature_bytes = hex::decode(signature_hex)
329 .map_err(|e| tl_err(format!("Invalid hex in signature: {e:?}")))?;
330 decrypt_and_decompress(encrypted, &signature_bytes)
331}
332
333#[cfg(test)]
334mod tests {
335 #![allow(clippy::unwrap_used, clippy::expect_used)]
336
337 use super::*;
338
339 #[test]
340 fn test_encrypt_and_decrypt_static_key() {
341 let message = b"hello, bittensor!";
342 let reveal_round = 17200000;
343
344 let encrypted =
345 encrypt_and_compress(message, reveal_round).expect("Encryption should succeed");
346
347 let signature_hex = get_reveal_round_signature(Some(reveal_round), false)
348 .expect("Should get signature")
349 .expect("Signature should not be None");
350
351 let signature_bytes = hex::decode(&signature_hex).expect("Hex decoding failed");
352
353 let decrypted = decrypt_and_decompress(&encrypted, &signature_bytes)
354 .expect("Decryption should succeed");
355
356 assert_eq!(message.to_vec(), decrypted);
357 }
358
359 #[test]
360 fn test_get_round_info_and_signature() {
361 let round = 17200000;
362 let info = get_round_info(Some(round)).expect("Drand round should be available");
363
364 assert_eq!(info.round, round);
365 assert!(!info.signature.is_empty());
366
367 let sig = get_reveal_round_signature(Some(round), false).unwrap();
368 assert!(sig.is_some());
369 }
370
371 #[test]
372 fn test_encrypt_commitment_format() {
373 let data = "example string";
374 let (encrypted, round) = encrypt_commitment(data, 10, 12.0).expect("Encryption failed");
375 assert!(!encrypted.is_empty());
376 assert!(round > 0);
377 }
378
379 #[test]
380 fn inner_ciphertext_unwraps_user_data_envelope() {
381 let expected = b"chain submit bytes".to_vec();
382 let mut envelope = UserData {
383 encrypted_data: expected.clone(),
384 reveal_round: 30_000_000,
385 }
386 .encode();
387
388 assert_eq!(inner_ciphertext(&envelope).unwrap(), expected);
389
390 envelope.push(0);
391 assert!(
392 inner_ciphertext(&envelope).is_err(),
393 "trailing bytes must be rejected"
394 );
395 }
396
397 #[test]
398 fn test_generate_commit_v2_structure() {
399 let state = epoch_schedule::EpochScheduleState {
400 last_epoch_block: 100,
401 pending_epoch_at: 0,
402 subnet_epoch_index: 0,
403 tempo: 50,
404 blocks_since_last_step: 0,
405 current_block: 120,
406 };
407 let uids = vec![1, 2, 3];
408 let values = vec![100, 200, 300];
409 let hotkey = vec![1, 2, 3];
410
411 let (encrypted, reveal_round) = generate_commit_v2(
412 uids.clone(),
413 values.clone(),
414 42,
415 state,
416 1,
417 12.0,
418 hotkey.clone(),
419 )
420 .expect("Commit generation failed");
421
422 assert!(!encrypted.is_empty());
423 assert!(reveal_round > 0);
424 }
425}