1use std::{
8 collections::BTreeMap,
9 pin::pin,
10 sync::{
11 Arc,
12 atomic::{AtomicU64, Ordering},
13 },
14 time::{Duration, Instant},
15};
16
17use futures::future::BoxFuture;
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use tokio::time::timeout;
21use tokio_util::sync::CancellationToken;
22use tracing::{error, info, warn};
23use vbs::version::Version;
24use versions::DRB_AND_HEADER_UPGRADE_VERSION;
25
26use crate::{
27 HotShotConfig,
28 data::EpochNumber,
29 traits::{
30 node_implementation::NodeType,
31 storage::{LoadDrbProgressFn, StoreDrbProgressFn},
32 },
33};
34
35#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
36pub struct DrbInput {
37 pub epoch: u64,
39 pub iteration: u64,
41 pub value: [u8; 32],
43 pub difficulty_level: u64,
45}
46
47pub type DrbDifficultySelectorFn =
48 Arc<dyn Fn(Version) -> BoxFuture<'static, u64> + Send + Sync + 'static>;
49
50pub fn drb_difficulty_selector<TYPES: NodeType>(
51 config: &HotShotConfig<TYPES>,
52) -> DrbDifficultySelectorFn {
53 let base_difficulty = config.drb_difficulty;
54 let upgrade_difficulty = config.drb_upgrade_difficulty;
55 Arc::new(move |version| {
56 Box::pin(async move {
57 if version >= DRB_AND_HEADER_UPGRADE_VERSION {
58 upgrade_difficulty
59 } else {
60 base_difficulty
61 }
62 })
63 })
64}
65
66pub const DIFFICULTY_LEVEL: u64 = 10;
78
79pub const DRB_CHECKPOINT_INTERVAL: u64 = 1_000_000_000;
81
82const DRB_CANCEL_BATCH: u64 = 100_000;
86
87pub const INITIAL_DRB_SEED_INPUT: [u8; 32] = [0; 32];
89pub const INITIAL_DRB_RESULT: [u8; 32] = [0; 32];
91
92pub type DrbSeedInput = [u8; 32];
94
95pub type DrbResult = [u8; 32];
97
98pub const KEEP_PREVIOUS_RESULT_COUNT: u64 = 8;
100
101#[must_use]
108pub fn difficulty_level() -> u64 {
109 unimplemented!("Use an arbitrary `DIFFICULTY_LEVEL` for now before we bench the hash time.");
110}
111
112#[derive(Clone, Debug, Default)]
120pub struct Heartbeat(Arc<AtomicU64>);
121
122impl Heartbeat {
123 pub fn beat(&self) {
125 self.0.fetch_add(1, Ordering::Relaxed);
126 }
127
128 pub fn count(&self) -> u64 {
131 self.0.load(Ordering::Relaxed)
132 }
133
134 pub async fn while_alive<T, F>(&self, deadline: Duration, f: F) -> Option<T>
136 where
137 F: Future<Output = T>,
138 {
139 let mut f = pin!(f);
140 let mut ticks = self.count();
141 loop {
142 match timeout(deadline, &mut f).await {
143 Ok(val) => return Some(val),
144 Err(_) => {
145 let observed = self.count();
146 if observed == ticks {
147 return None;
148 }
149 ticks = observed;
150 },
151 }
152 }
153 }
154}
155
156fn hash_batches(
161 mut hash: [u8; 32],
162 count: u64,
163 cancel: CancellationToken,
164 heartbeat: Heartbeat,
165) -> Option<[u8; 32]> {
166 let mut done = 0u64;
167 while done < count {
168 if cancel.is_cancelled() {
169 return None;
170 }
171 let n = DRB_CANCEL_BATCH.min(count - done);
172 for _ in 0..n {
173 hash = Sha256::digest(hash).into();
174 }
175 done += n;
176 heartbeat.beat();
177 }
178 Some(hash)
179}
180
181pub const DRB_PROGRESS_LOAD_TIMEOUT: Duration = Duration::from_secs(60);
185
186#[must_use]
200pub async fn compute_drb_result(
201 drb_input: DrbInput,
202 store_drb_progress: StoreDrbProgressFn,
203 load_drb_progress: LoadDrbProgressFn,
204 progress_load_timeout: Duration,
205 heartbeat: Heartbeat,
206 cancel: CancellationToken,
207) -> Option<DrbResult> {
208 info!(target: "announce::drb", ?drb_input, "beginning drb calculation");
209 let mut drb_input = drb_input;
210
211 match tokio::time::timeout(progress_load_timeout, load_drb_progress(drb_input.epoch)).await {
212 Ok(Ok(loaded_drb_input)) => {
213 if loaded_drb_input.difficulty_level != drb_input.difficulty_level {
214 error!(
215 ?drb_input,
216 ?loaded_drb_input,
217 "we are calculating the drb result with input that has a different difficulty \
218 level for this epoch than a previously stored one => discarding the value \
219 from storage"
220 );
221 } else if loaded_drb_input.iteration >= drb_input.iteration {
222 drb_input = loaded_drb_input;
223 }
224 },
225 Ok(Err(e)) => {
226 warn!(
227 epoch = drb_input.epoch,
228 "loading stored drb progress failed; computing from the given input: {e}"
229 );
230 },
231 Err(_) => {
232 warn!(
233 epoch = drb_input.epoch,
234 ?progress_load_timeout,
235 "loading stored drb progress timed out; computing from the given input"
236 );
237 },
238 }
239
240 heartbeat.beat();
243
244 let mut hash: [u8; 32] = drb_input.value;
245 let mut iteration = drb_input.iteration;
246 let remaining_iterations = drb_input
247 .difficulty_level
248 .checked_sub(iteration)
249 .unwrap_or_else(|| {
250 panic!(
251 "DRB difficulty level {} exceeds the iteration {} of the input we were given. \
252 This is a fatal error",
253 drb_input.difficulty_level, iteration
254 )
255 });
256
257 let final_checkpoint = remaining_iterations / DRB_CHECKPOINT_INTERVAL;
258
259 let mut last_time = Instant::now();
260 let mut last_iteration = iteration;
261
262 for _ in 0..final_checkpoint {
264 let c = cancel.clone();
265 let h = heartbeat.clone();
266 hash =
267 tokio::task::spawn_blocking(move || hash_batches(hash, DRB_CHECKPOINT_INTERVAL, c, h))
268 .await
269 .expect("completes")?;
270
271 iteration += DRB_CHECKPOINT_INTERVAL;
272
273 let updated_drb_input = DrbInput {
274 epoch: drb_input.epoch,
275 iteration,
276 value: hash,
277 difficulty_level: drb_input.difficulty_level,
278 };
279
280 let elapsed_time = last_time.elapsed().as_millis();
281
282 let store_drb_progress = store_drb_progress.clone();
283 tokio::spawn(async move {
284 info!(
285 target: "announce::drb",
286 ?updated_drb_input,
287 %last_iteration,
288 %elapsed_time,
289 "storing partial drb progress"
290 );
291 if let Err(err) = store_drb_progress(updated_drb_input).await {
292 warn!(%err, "failed to store drb progress during calculation");
293 }
294 });
295
296 last_time = Instant::now();
297 last_iteration = iteration;
298 }
299
300 let final_checkpoint_iteration = iteration;
301 let remainder = drb_input
304 .difficulty_level
305 .checked_sub(final_checkpoint_iteration)
306 .expect("sufficient iterations");
307
308 let c = cancel.clone();
310 let h = heartbeat.clone();
311 let drb_result = tokio::task::spawn_blocking(move || hash_batches(hash, remainder, c, h))
312 .await
313 .expect("DRB spawn_blocking panicked")?;
314
315 let final_drb_input = DrbInput {
316 epoch: drb_input.epoch,
317 iteration: drb_input.difficulty_level,
318 value: drb_result,
319 difficulty_level: drb_input.difficulty_level,
320 };
321
322 info!(target: "announce::drb", ?final_drb_input, "completed drb calculation");
323
324 let store_drb_progress = store_drb_progress.clone();
325 tokio::spawn(async move {
326 if let Err(err) = store_drb_progress(final_drb_input).await {
327 warn!(%err, "failed to store drb progress during calculation");
328 }
329 });
330
331 Some(drb_result)
332}
333
334#[derive(Clone, Debug)]
336pub struct DrbResults {
337 pub results: BTreeMap<EpochNumber, DrbResult>,
339}
340
341impl DrbResults {
342 #[must_use]
343 pub fn new() -> Self {
345 Self {
346 results: BTreeMap::from([
347 (EpochNumber::new(1), INITIAL_DRB_RESULT),
348 (EpochNumber::new(2), INITIAL_DRB_RESULT),
349 ]),
350 }
351 }
352
353 pub fn store_result(&mut self, epoch: EpochNumber, result: DrbResult) {
354 self.results.insert(epoch, result);
355 }
356
357 pub fn garbage_collect(&mut self, epoch: EpochNumber) {
359 if epoch.u64() < KEEP_PREVIOUS_RESULT_COUNT {
360 return;
361 }
362
363 let retain_epoch = epoch - KEEP_PREVIOUS_RESULT_COUNT;
364 self.results = self.results.split_off(&retain_epoch);
368 }
369}
370
371impl Default for DrbResults {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377pub mod election {
393 use alloy_primitives::{U256, U512};
394 use sha2::{Digest, Sha256, Sha512};
395
396 use crate::traits::signature_key::{SignatureKey, StakeTableEntryType};
397
398 fn cyclic_xor(drb: [u8; 32], public_key: Vec<u8>) -> Vec<u8> {
400 let drb: Vec<u8> = drb.to_vec();
401
402 let mut result: Vec<u8> = vec![];
403
404 for (drb_byte, public_key_byte) in public_key.iter().zip(drb.iter().cycle()) {
405 result.push(drb_byte ^ public_key_byte);
406 }
407
408 result
409 }
410
411 pub fn generate_stake_cdf<Key: SignatureKey, Entry: StakeTableEntryType<Key>>(
413 mut stake_table: Vec<Entry>,
414 drb: [u8; 32],
415 ) -> RandomizedCommittee<Entry> {
416 stake_table.sort_by(|a, b| {
418 cyclic_xor(drb, a.public_key().to_bytes())
419 .cmp(&cyclic_xor(drb, b.public_key().to_bytes()))
420 });
421
422 let mut hasher = Sha256::new();
423
424 let mut cumulative_stake = U256::from(0);
425 let mut cdf = vec![];
426
427 for entry in stake_table {
428 cumulative_stake += entry.stake();
429 hasher.update(entry.public_key().to_bytes());
430
431 cdf.push((entry, cumulative_stake));
432 }
433
434 RandomizedCommittee {
435 cdf,
436 stake_table_hash: hasher.finalize().into(),
437 drb,
438 }
439 }
440
441 pub fn select_randomized_leader<
449 SignatureKey,
450 Entry: StakeTableEntryType<SignatureKey> + Clone,
451 >(
452 randomized_committee: &RandomizedCommittee<Entry>,
453 view: u64,
454 ) -> Entry {
455 let RandomizedCommittee {
456 cdf,
457 stake_table_hash,
458 drb,
459 } = randomized_committee;
460 let mut hasher = Sha512::new();
462 hasher.update(drb);
463 hasher.update(view.to_le_bytes());
464 hasher.update(stake_table_hash);
465 let raw_breakpoint: [u8; 64] = hasher.finalize().into();
466
467 let remainder: U512 =
469 U512::from_le_bytes(raw_breakpoint) % U512::from(cdf.last().unwrap().1);
470
471 let breakpoint: U256 = U256::from_le_slice(&remainder.to_le_bytes_vec()[0..32]);
473
474 let index = cdf.partition_point(|(_, cumulative_stake)| breakpoint >= *cumulative_stake);
480
481 cdf[index].0.clone()
483 }
484
485 #[derive(Clone, Debug)]
486 pub struct RandomizedCommittee<Entry> {
487 cdf: Vec<(Entry, U256)>,
489 stake_table_hash: [u8; 32],
491 drb: [u8; 32],
493 }
494
495 impl<Entry> RandomizedCommittee<Entry> {
496 pub fn drb_result(&self) -> [u8; 32] {
497 self.drb
498 }
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use std::{collections::HashMap, time::Duration};
505
506 use alloy_primitives::U256;
507 use rand::RngCore;
508 use sha2::{Digest, Sha256};
509 use tokio_util::sync::CancellationToken;
510
511 use super::{
512 DRB_CANCEL_BATCH, DRB_CHECKPOINT_INTERVAL, DRB_PROGRESS_LOAD_TIMEOUT, DrbInput, Heartbeat,
513 compute_drb_result,
514 election::{generate_stake_cdf, select_randomized_leader},
515 hash_batches,
516 };
517 use crate::{
518 signature_key::BLSPubKey,
519 stake_table::StakeTableEntry,
520 traits::{
521 signature_key::{BuilderSignatureKey, StakeTableEntryType},
522 storage::{LoadDrbProgressFn, null_load_drb_progress_fn, null_store_drb_progress_fn},
523 },
524 };
525
526 #[test]
527 fn test_hash_batches_pre_cancelled() {
528 let cancel = CancellationToken::new();
529 cancel.cancel();
530 assert_eq!(
531 hash_batches([0u8; 32], 10, cancel, Heartbeat::default()),
532 None
533 );
534 }
535
536 #[test]
537 fn test_hash_batches_count_zero() {
538 let input = [42u8; 32];
539 let result = hash_batches(input, 0, CancellationToken::new(), Heartbeat::default());
540 assert_eq!(result, Some(input));
541 }
542
543 #[test]
544 fn test_hash_batches_small_count() {
545 let input = [1u8; 32];
546 let count = 5u64;
547 let mut expected = input;
549 for _ in 0..count {
550 expected = Sha256::digest(expected).into();
551 }
552 let result = hash_batches(input, count, CancellationToken::new(), Heartbeat::default());
553 assert_eq!(result, Some(expected));
554 }
555
556 #[test]
557 fn test_hash_batches_cancel_after_first_batch() {
558 let count = DRB_CANCEL_BATCH + 1;
560 let cancel = CancellationToken::new();
561
562 let intermediate = hash_batches(
564 [0u8; 32],
565 DRB_CANCEL_BATCH,
566 CancellationToken::new(),
567 Heartbeat::default(),
568 )
569 .expect("first batch must complete");
570 cancel.cancel();
571 let result = hash_batches(
573 intermediate,
574 count - DRB_CANCEL_BATCH,
575 cancel,
576 Heartbeat::default(),
577 );
578 assert_eq!(result, None);
579 }
580
581 #[tokio::test]
582 async fn test_compute_drb_result_cancelled() {
583 let cancel = CancellationToken::new();
586 cancel.cancel();
587 let drb_input = DrbInput {
588 epoch: 1,
589 iteration: 0,
590 value: [0u8; 32],
591 difficulty_level: DRB_CHECKPOINT_INTERVAL * 5,
592 };
593 let result = compute_drb_result(
594 drb_input,
595 null_store_drb_progress_fn(),
596 null_load_drb_progress_fn(),
597 DRB_PROGRESS_LOAD_TIMEOUT,
598 Heartbeat::default(),
599 cancel,
600 )
601 .await;
602 assert_eq!(result, None);
603 }
604
605 #[tokio::test]
610 async fn test_compute_drb_result_hung_progress_load() {
611 let hung_load: LoadDrbProgressFn =
612 std::sync::Arc::new(|_| Box::pin(std::future::pending()));
613 let drb_input = DrbInput {
614 epoch: 1,
615 iteration: 0,
616 value: [0u8; 32],
617 difficulty_level: 10,
618 };
619 let result = tokio::time::timeout(
620 Duration::from_secs(5),
621 compute_drb_result(
622 drb_input,
623 null_store_drb_progress_fn(),
624 hung_load,
625 Duration::from_millis(100),
626 Heartbeat::default(),
627 CancellationToken::new(),
628 ),
629 )
630 .await
631 .expect("the computation parked on the hung progress load instead of bounding it");
632 assert!(
633 result.is_some(),
634 "computation with a hung progress load returned no result"
635 );
636 }
637
638 #[test]
639 fn test_randomized_leader() {
640 let mut rng = rand::thread_rng();
641 let drb: [u8; 32] = Sha256::digest(b"drb").into();
643 let stake_table_entries: Vec<_> = (0..10)
645 .map(|i| StakeTableEntry {
646 stake_key: BLSPubKey::generated_from_seed_indexed([0u8; 32], i).0,
647 stake_amount: U256::from(rng.next_u64() % 100 + 1),
648 })
649 .collect();
650 let randomized_committee = generate_stake_cdf(stake_table_entries.clone(), drb);
651
652 let num_views = 100000;
654 let mut selected = HashMap::<_, u64>::new();
655 for i in 0..num_views {
657 let leader = select_randomized_leader(&randomized_committee, i);
658 *selected.entry(leader).or_insert(0) += 1;
659 }
660
661 let mut tvd = 0.;
663 let total_stakes = stake_table_entries
664 .iter()
665 .map(|e| e.stake())
666 .sum::<U256>()
667 .to::<u64>() as f64;
668 for entry in stake_table_entries {
669 let expected = entry.stake().to::<u64>() as f64 / total_stakes;
670 let actual = *selected.get(&entry).unwrap_or(&0) as f64 / num_views as f64;
671 tvd += (expected - actual).abs();
672 }
673
674 assert!(tvd >= 0.0);
676 assert!(tvd < 0.03);
678 }
679}