entropa_core/beacon.rs
1//! Entropy beacon — the external randomness that seeds each block.
2//!
3//! The live beacon ([`sample_live`]) is [drand](https://drand.love)'s public
4//! `quicknet` randomness network — a threshold-BLS beacon run by independent
5//! operators (the League of Entropy), publishing a fresh, unbiasable, publicly
6//! verifiable random value every 3 seconds. No single party (including us) can
7//! predict or influence it. [`sample`] is the deterministic offline fallback: used
8//! in tests, and live if drand is unreachable, so the chain keeps running instead of
9//! stalling.
10
11use serde::Deserialize;
12
13/// drand's `quicknet` chain — 3s rounds, matches Entropa's own block cadence.
14const DRAND_QUICKNET_URL: &str =
15 "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest";
16
17#[derive(Deserialize)]
18struct DrandRound {
19 round: u64,
20 randomness: String,
21}
22
23/// Fetch the current round from drand's public `quicknet` beacon. Returns `None` on
24/// any failure (network, bad status, malformed JSON) — the caller falls back to
25/// [`sample`].
26pub async fn sample_live() -> Option<String> {
27 let resp = reqwest::Client::new()
28 .get(DRAND_QUICKNET_URL)
29 .timeout(std::time::Duration::from_secs(5))
30 .send()
31 .await
32 .ok()?;
33 if !resp.status().is_success() {
34 return None;
35 }
36 let round: DrandRound = resp.json().await.ok()?;
37 Some(format!("DRAND-{}-{}", round.round, &round.randomness[..16]))
38}
39
40/// Deterministic offline fallback — derives a value from the round number. Used in
41/// tests (no network dependency) and if [`sample_live`] can't reach drand. Prefixed
42/// differently from [`sample_live`]'s output so it's visible on-chain which mode
43/// produced a given block's beacon.
44pub fn sample(round: u64) -> String {
45 let digest = blake3::hash(&round.to_be_bytes());
46 format!("BEACON-{}", &digest.to_hex()[..16])
47}