1use std::{collections::BTreeMap, sync::Arc, time::Instant};
8
9use futures::future::BoxFuture;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use tokio_util::sync::CancellationToken;
13use tracing::{error, info, warn};
14use vbs::version::Version;
15use versions::DRB_AND_HEADER_UPGRADE_VERSION;
16
17use crate::{
18 HotShotConfig,
19 data::EpochNumber,
20 traits::{
21 node_implementation::NodeType,
22 storage::{LoadDrbProgressFn, StoreDrbProgressFn},
23 },
24};
25
26#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
27pub struct DrbInput {
28 pub epoch: u64,
30 pub iteration: u64,
32 pub value: [u8; 32],
34 pub difficulty_level: u64,
36}
37
38pub type DrbDifficultySelectorFn =
39 Arc<dyn Fn(Version) -> BoxFuture<'static, u64> + Send + Sync + 'static>;
40
41pub fn drb_difficulty_selector<TYPES: NodeType>(
42 config: &HotShotConfig<TYPES>,
43) -> DrbDifficultySelectorFn {
44 let base_difficulty = config.drb_difficulty;
45 let upgrade_difficulty = config.drb_upgrade_difficulty;
46 Arc::new(move |version| {
47 Box::pin(async move {
48 if version >= DRB_AND_HEADER_UPGRADE_VERSION {
49 upgrade_difficulty
50 } else {
51 base_difficulty
52 }
53 })
54 })
55}
56
57pub const DIFFICULTY_LEVEL: u64 = 10;
69
70pub const DRB_CHECKPOINT_INTERVAL: u64 = 1_000_000_000;
72
73const DRB_CANCEL_BATCH: u64 = 1_000_000;
76
77pub const INITIAL_DRB_SEED_INPUT: [u8; 32] = [0; 32];
79pub const INITIAL_DRB_RESULT: [u8; 32] = [0; 32];
81
82pub type DrbSeedInput = [u8; 32];
84
85pub type DrbResult = [u8; 32];
87
88pub const KEEP_PREVIOUS_RESULT_COUNT: u64 = 8;
90
91#[must_use]
98pub fn difficulty_level() -> u64 {
99 unimplemented!("Use an arbitrary `DIFFICULTY_LEVEL` for now before we bench the hash time.");
100}
101
102fn hash_batches(mut hash: [u8; 32], count: u64, cancel: CancellationToken) -> Option<[u8; 32]> {
106 let mut done = 0u64;
107 while done < count {
108 if cancel.is_cancelled() {
109 return None;
110 }
111 let n = DRB_CANCEL_BATCH.min(count - done);
112 for _ in 0..n {
113 hash = Sha256::digest(hash).into();
114 }
115 done += n;
116 }
117 Some(hash)
118}
119
120#[must_use]
129pub async fn compute_drb_result(
130 drb_input: DrbInput,
131 store_drb_progress: StoreDrbProgressFn,
132 load_drb_progress: LoadDrbProgressFn,
133 cancel: CancellationToken,
134) -> Option<DrbResult> {
135 info!(target: "announce::drb", ?drb_input, "beginning drb calculation");
136 let mut drb_input = drb_input;
137
138 if let Ok(loaded_drb_input) = load_drb_progress(drb_input.epoch).await {
139 if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
140 error!(
141 ?drb_input,
142 ?loaded_drb_input,
143 "we are calculating the drb result with input that has a different difficulty \
144 level for this epoch than a previously stored one => discarding the value from \
145 storage"
146 );
147 } else if loaded_drb_input.iteration >= drb_input.iteration {
148 drb_input = loaded_drb_input;
149 }
150 }
151
152 let mut hash: [u8; 32] = drb_input.value;
153 let mut iteration = drb_input.iteration;
154 let remaining_iterations = drb_input
155 .difficulty_level
156 .checked_sub(iteration)
157 .unwrap_or_else(|| {
158 panic!(
159 "DRB difficulty level {} exceeds the iteration {} of the input we were given. \
160 This is a fatal error",
161 drb_input.difficulty_level, iteration
162 )
163 });
164
165 let final_checkpoint = remaining_iterations / DRB_CHECKPOINT_INTERVAL;
166
167 let mut last_time = Instant::now();
168 let mut last_iteration = iteration;
169
170 for _ in 0..final_checkpoint {
172 let c = cancel.clone();
173 hash = tokio::task::spawn_blocking(move || hash_batches(hash, DRB_CHECKPOINT_INTERVAL, c))
174 .await
175 .expect("completes")?;
176
177 iteration += DRB_CHECKPOINT_INTERVAL;
178
179 let updated_drb_input = DrbInput {
180 epoch: drb_input.epoch,
181 iteration,
182 value: hash,
183 difficulty_level: drb_input.difficulty_level,
184 };
185
186 let elapsed_time = last_time.elapsed().as_millis();
187
188 let store_drb_progress = store_drb_progress.clone();
189 tokio::spawn(async move {
190 info!(
191 target: "announce::drb",
192 ?updated_drb_input,
193 %last_iteration,
194 %elapsed_time,
195 "storing partial drb progress"
196 );
197 if let Err(err) = store_drb_progress(updated_drb_input).await {
198 warn!(%err, "failed to store drb progress during calculation");
199 }
200 });
201
202 last_time = Instant::now();
203 last_iteration = iteration;
204 }
205
206 let final_checkpoint_iteration = iteration;
207 let remainder = drb_input
210 .difficulty_level
211 .checked_sub(final_checkpoint_iteration)
212 .expect("sufficient iterations");
213
214 let c = cancel.clone();
216 let drb_result = tokio::task::spawn_blocking(move || hash_batches(hash, remainder, c))
217 .await
218 .expect("DRB spawn_blocking panicked")?;
219
220 let final_drb_input = DrbInput {
221 epoch: drb_input.epoch,
222 iteration: drb_input.difficulty_level,
223 value: drb_result,
224 difficulty_level: drb_input.difficulty_level,
225 };
226
227 info!(target: "announce::drb", ?final_drb_input, "completed drb calculation");
228
229 let store_drb_progress = store_drb_progress.clone();
230 tokio::spawn(async move {
231 if let Err(err) = store_drb_progress(final_drb_input).await {
232 warn!(%err, "failed to store drb progress during calculation");
233 }
234 });
235
236 Some(drb_result)
237}
238
239#[derive(Clone, Debug)]
241pub struct DrbResults {
242 pub results: BTreeMap<EpochNumber, DrbResult>,
244}
245
246impl DrbResults {
247 #[must_use]
248 pub fn new() -> Self {
250 Self {
251 results: BTreeMap::from([
252 (EpochNumber::new(1), INITIAL_DRB_RESULT),
253 (EpochNumber::new(2), INITIAL_DRB_RESULT),
254 ]),
255 }
256 }
257
258 pub fn store_result(&mut self, epoch: EpochNumber, result: DrbResult) {
259 self.results.insert(epoch, result);
260 }
261
262 pub fn garbage_collect(&mut self, epoch: EpochNumber) {
264 if epoch.u64() < KEEP_PREVIOUS_RESULT_COUNT {
265 return;
266 }
267
268 let retain_epoch = epoch - KEEP_PREVIOUS_RESULT_COUNT;
269 self.results = self.results.split_off(&retain_epoch);
273 }
274}
275
276impl Default for DrbResults {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282pub mod election {
298 use alloy_primitives::{U256, U512};
299 use sha2::{Digest, Sha256, Sha512};
300
301 use crate::traits::signature_key::{SignatureKey, StakeTableEntryType};
302
303 fn cyclic_xor(drb: [u8; 32], public_key: Vec<u8>) -> Vec<u8> {
305 let drb: Vec<u8> = drb.to_vec();
306
307 let mut result: Vec<u8> = vec![];
308
309 for (drb_byte, public_key_byte) in public_key.iter().zip(drb.iter().cycle()) {
310 result.push(drb_byte ^ public_key_byte);
311 }
312
313 result
314 }
315
316 pub fn generate_stake_cdf<Key: SignatureKey, Entry: StakeTableEntryType<Key>>(
318 mut stake_table: Vec<Entry>,
319 drb: [u8; 32],
320 ) -> RandomizedCommittee<Entry> {
321 stake_table.sort_by(|a, b| {
323 cyclic_xor(drb, a.public_key().to_bytes())
324 .cmp(&cyclic_xor(drb, b.public_key().to_bytes()))
325 });
326
327 let mut hasher = Sha256::new();
328
329 let mut cumulative_stake = U256::from(0);
330 let mut cdf = vec![];
331
332 for entry in stake_table {
333 cumulative_stake += entry.stake();
334 hasher.update(entry.public_key().to_bytes());
335
336 cdf.push((entry, cumulative_stake));
337 }
338
339 RandomizedCommittee {
340 cdf,
341 stake_table_hash: hasher.finalize().into(),
342 drb,
343 }
344 }
345
346 pub fn select_randomized_leader<
354 SignatureKey,
355 Entry: StakeTableEntryType<SignatureKey> + Clone,
356 >(
357 randomized_committee: &RandomizedCommittee<Entry>,
358 view: u64,
359 ) -> Entry {
360 let RandomizedCommittee {
361 cdf,
362 stake_table_hash,
363 drb,
364 } = randomized_committee;
365 let mut hasher = Sha512::new();
367 hasher.update(drb);
368 hasher.update(view.to_le_bytes());
369 hasher.update(stake_table_hash);
370 let raw_breakpoint: [u8; 64] = hasher.finalize().into();
371
372 let remainder: U512 =
374 U512::from_le_bytes(raw_breakpoint) % U512::from(cdf.last().unwrap().1);
375
376 let breakpoint: U256 = U256::from_le_slice(&remainder.to_le_bytes_vec()[0..32]);
378
379 let index = cdf.partition_point(|(_, cumulative_stake)| breakpoint >= *cumulative_stake);
385
386 cdf[index].0.clone()
388 }
389
390 #[derive(Clone, Debug)]
391 pub struct RandomizedCommittee<Entry> {
392 cdf: Vec<(Entry, U256)>,
394 stake_table_hash: [u8; 32],
396 drb: [u8; 32],
398 }
399
400 impl<Entry> RandomizedCommittee<Entry> {
401 pub fn drb_result(&self) -> [u8; 32] {
402 self.drb
403 }
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use std::collections::HashMap;
410
411 use alloy_primitives::U256;
412 use rand::RngCore;
413 use sha2::{Digest, Sha256};
414 use tokio_util::sync::CancellationToken;
415
416 use super::{
417 DRB_CANCEL_BATCH, DRB_CHECKPOINT_INTERVAL, DrbInput, compute_drb_result,
418 election::{generate_stake_cdf, select_randomized_leader},
419 hash_batches,
420 };
421 use crate::{
422 signature_key::BLSPubKey,
423 stake_table::StakeTableEntry,
424 traits::{
425 signature_key::{BuilderSignatureKey, StakeTableEntryType},
426 storage::{null_load_drb_progress_fn, null_store_drb_progress_fn},
427 },
428 };
429
430 #[test]
431 fn test_hash_batches_pre_cancelled() {
432 let cancel = CancellationToken::new();
433 cancel.cancel();
434 assert_eq!(hash_batches([0u8; 32], 10, cancel), None);
435 }
436
437 #[test]
438 fn test_hash_batches_count_zero() {
439 let input = [42u8; 32];
440 let result = hash_batches(input, 0, CancellationToken::new());
441 assert_eq!(result, Some(input));
442 }
443
444 #[test]
445 fn test_hash_batches_small_count() {
446 let input = [1u8; 32];
447 let count = 5u64;
448 let mut expected = input;
450 for _ in 0..count {
451 expected = Sha256::digest(expected).into();
452 }
453 let result = hash_batches(input, count, CancellationToken::new());
454 assert_eq!(result, Some(expected));
455 }
456
457 #[test]
458 fn test_hash_batches_cancel_after_first_batch() {
459 let count = DRB_CANCEL_BATCH + 1;
461 let cancel = CancellationToken::new();
462
463 let intermediate = hash_batches([0u8; 32], DRB_CANCEL_BATCH, CancellationToken::new())
465 .expect("first batch must complete");
466 cancel.cancel();
467 let result = hash_batches(intermediate, count - DRB_CANCEL_BATCH, cancel);
469 assert_eq!(result, None);
470 }
471
472 #[tokio::test]
473 async fn test_compute_drb_result_cancelled() {
474 let cancel = CancellationToken::new();
477 cancel.cancel();
478 let drb_input = DrbInput {
479 epoch: 1,
480 iteration: 0,
481 value: [0u8; 32],
482 difficulty_level: DRB_CHECKPOINT_INTERVAL * 5,
483 };
484 let result = compute_drb_result(
485 drb_input,
486 null_store_drb_progress_fn(),
487 null_load_drb_progress_fn(),
488 cancel,
489 )
490 .await;
491 assert_eq!(result, None);
492 }
493
494 #[test]
495 fn test_randomized_leader() {
496 let mut rng = rand::thread_rng();
497 let drb: [u8; 32] = Sha256::digest(b"drb").into();
499 let stake_table_entries: Vec<_> = (0..10)
501 .map(|i| StakeTableEntry {
502 stake_key: BLSPubKey::generated_from_seed_indexed([0u8; 32], i).0,
503 stake_amount: U256::from(rng.next_u64() % 100 + 1),
504 })
505 .collect();
506 let randomized_committee = generate_stake_cdf(stake_table_entries.clone(), drb);
507
508 let num_views = 100000;
510 let mut selected = HashMap::<_, u64>::new();
511 for i in 0..num_views {
513 let leader = select_randomized_leader(&randomized_committee, i);
514 *selected.entry(leader).or_insert(0) += 1;
515 }
516
517 let mut tvd = 0.;
519 let total_stakes = stake_table_entries
520 .iter()
521 .map(|e| e.stake())
522 .sum::<U256>()
523 .to::<u64>() as f64;
524 for entry in stake_table_entries {
525 let expected = entry.stake().to::<u64>() as f64 / total_stakes;
526 let actual = *selected.get(&entry).unwrap_or(&0) as f64 / num_views as f64;
527 tvd += (expected - actual).abs();
528 }
529
530 assert!(tvd >= 0.0);
532 assert!(tvd < 0.03);
534 }
535}