#![allow(dead_code)]
use std::sync::LazyLock;
use std::time::Duration;
use std::{borrow::Cow, num::NonZeroUsize};
use super::{
beacon_entries::BeaconEntry,
signatures::{
PublicKeyOnG1, PublicKeyOnG2, SignatureOnG1, SignatureOnG2, verify_messages_chained,
},
};
use crate::shim::clock::ChainEpoch;
use crate::shim::version::NetworkVersion;
use crate::utils::cache::SizeTrackingLruCache;
use crate::utils::misc::env::is_env_truthy;
use crate::utils::net::global_http_client;
use ambassador::{Delegate, delegatable_trait};
use anyhow::Context as _;
use backon::{ExponentialBuilder, Retryable};
use bls_signatures::Serialize as _;
use itertools::Itertools as _;
use nonzero_ext::nonzero;
use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
use tracing::debug;
use url::Url;
pub const IGNORE_DRAND_VAR: &str = "FOREST_IGNORE_DRAND";
pub static IGNORE_DRAND: LazyLock<bool> = LazyLock::new(|| is_env_truthy(IGNORE_DRAND_VAR));
#[derive(PartialEq, Eq, Copy, Clone, Debug, SerdeSerialize, SerdeDeserialize)]
pub enum DrandNetwork {
Mainnet,
Quicknet,
Incentinet,
}
impl DrandNetwork {
pub fn is_unchained(&self) -> bool {
matches!(self, Self::Quicknet)
}
pub fn is_chained(&self) -> bool {
!self.is_unchained()
}
}
#[derive(Debug, Clone, SerdeSerialize, SerdeDeserialize, Eq, PartialEq)]
pub struct DrandConfig<'a> {
pub servers: Vec<Url>,
pub chain_info: ChainInfo<'a>,
pub network_type: DrandNetwork,
}
pub struct BeaconSchedule(pub Vec<BeaconPoint>);
impl BeaconSchedule {
pub async fn beacon_entries_for_block(
&self,
network_version: NetworkVersion,
epoch: ChainEpoch,
parent_epoch: ChainEpoch,
prev: &BeaconEntry,
) -> anyhow::Result<Vec<BeaconEntry>> {
let (cb_epoch, curr_beacon) = self.beacon_for_epoch(epoch)?;
if curr_beacon.network().is_chained() {
let (pb_epoch, _) = self.beacon_for_epoch(parent_epoch)?;
if cb_epoch != pb_epoch {
let round = curr_beacon.max_beacon_round_for_epoch(network_version, epoch);
let mut entries = Vec::with_capacity(2);
entries.push(curr_beacon.entry(round - 1).await?);
entries.push(curr_beacon.entry(round).await?);
return Ok(entries);
}
}
let max_round = curr_beacon.max_beacon_round_for_epoch(network_version, epoch);
if max_round == prev.round() {
tracing::warn!(
"Unexpected `max_round == prev.round()` condition, network_version: {network_version:?}, max_round: {max_round}, prev_round: {}",
prev.round()
);
return Ok(vec![]);
}
let prev_round = if prev.round() == 0 {
max_round - 1
} else {
prev.round()
};
if curr_beacon.network().is_unchained() {
let entry = curr_beacon.entry(max_round).await?;
Ok(vec![entry])
} else {
let mut cur = max_round;
let mut out = Vec::new();
while cur > prev_round {
let entry = curr_beacon.entry(cur).await?;
cur = entry.round() - 1;
out.push(entry);
}
out.reverse();
Ok(out)
}
}
pub fn beacon_for_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<(ChainEpoch, &BeaconImpl)> {
self.0
.iter()
.rev()
.find(|upgrade| epoch >= upgrade.height)
.map(|upgrade| (upgrade.height, &upgrade.beacon))
.context("Invalid beacon schedule, no valid beacon")
}
}
#[derive(Delegate, derive_more::From)]
#[delegate(Beacon)]
pub enum BeaconImpl {
Drand(DrandBeacon),
#[cfg(test)]
Mock(crate::beacon::mock_beacon::MockBeacon),
}
pub struct BeaconPoint {
height: ChainEpoch,
beacon: BeaconImpl,
}
impl BeaconPoint {
pub fn new(height: ChainEpoch, beacon: impl Into<BeaconImpl>) -> Self {
let beacon = beacon.into();
Self { height, beacon }
}
}
#[delegatable_trait]
pub trait Beacon {
fn network(&self) -> DrandNetwork;
fn verify_entries(&self, entries: &[BeaconEntry], prev: &BeaconEntry) -> anyhow::Result<bool>;
async fn entry(&self, round: u64) -> anyhow::Result<BeaconEntry>;
fn max_beacon_round_for_epoch(
&self,
network_version: NetworkVersion,
fil_epoch: ChainEpoch,
) -> u64;
}
#[derive(SerdeDeserialize, SerdeSerialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct ChainInfo<'a> {
pub public_key: Cow<'a, str>,
pub period: i32,
pub genesis_time: i32,
pub hash: Cow<'a, str>,
#[serde(rename = "groupHash")]
pub group_hash: Cow<'a, str>,
}
#[derive(SerdeDeserialize, SerdeSerialize, Debug, Clone)]
pub struct BeaconEntryJson {
round: u64,
randomness: String,
signature: String,
previous_signature: Option<String>,
}
pub struct DrandBeacon {
servers: Vec<Url>,
hash: String,
network: DrandNetwork,
public_key: Vec<u8>,
interval: u64,
drand_gen_time: u64,
fil_gen_time: u64,
fil_round_time: u64,
verified_beacons: SizeTrackingLruCache<u64, BeaconEntry>,
}
impl DrandBeacon {
pub fn new(genesis_ts: u64, interval: u64, config: &DrandConfig<'_>) -> Self {
assert_ne!(genesis_ts, 0, "Genesis timestamp cannot be 0");
const CACHE_SIZE: NonZeroUsize = nonzero!(1000usize);
Self {
servers: config.servers.clone(),
hash: config.chain_info.hash.to_string(),
network: config.network_type,
public_key: hex::decode(config.chain_info.public_key.as_ref())
.expect("invalid static encoding of drand hex public key"),
interval: config.chain_info.period as u64,
drand_gen_time: config.chain_info.genesis_time as u64,
fil_round_time: interval,
fil_gen_time: genesis_ts,
verified_beacons: SizeTrackingLruCache::new_with_metrics(
"verified_beacons".into(),
CACHE_SIZE,
),
}
}
fn is_verified(&self, entry: &BeaconEntry) -> bool {
let cache = self.verified_beacons.cache().read();
cache.peek(&entry.round()) == Some(entry)
}
}
impl Beacon for DrandBeacon {
fn network(&self) -> DrandNetwork {
self.network
}
fn verify_entries<'a>(
&self,
entries: &'a [BeaconEntry],
prev: &'a BeaconEntry,
) -> anyhow::Result<bool> {
let mut validated = vec![];
let is_valid = if self.network.is_unchained() {
let mut messages = vec![];
let mut signatures = vec![];
let pk = PublicKeyOnG2::from_bytes(&self.public_key)?;
{
for entry in entries.iter().unique_by(|e| e.round()) {
if self.is_verified(entry) {
continue;
}
messages.push(BeaconEntry::message_unchained(entry.round()));
signatures.push(SignatureOnG1::from_bytes(entry.signature())?);
validated.push(entry);
}
}
pk.verify_batch(
messages.iter().map(AsRef::as_ref).collect_vec().as_slice(),
signatures.iter().collect_vec().as_slice(),
)
} else {
let mut messages = vec![];
let mut signatures = vec![];
let pk = PublicKeyOnG1::from_bytes(&self.public_key)?;
{
let prev_curr_pairs = std::iter::once(prev)
.chain(entries.iter())
.unique_by(|e| e.round())
.tuple_windows::<(_, _)>();
for (prev, curr) in prev_curr_pairs {
if prev.round() > 0 && !self.is_verified(curr) {
messages.push(BeaconEntry::message_chained(curr.round(), prev.signature()));
signatures.push(SignatureOnG2::from_bytes(curr.signature())?);
validated.push(curr);
}
}
}
verify_messages_chained(
&pk,
messages.iter().map(AsRef::as_ref).collect_vec().as_slice(),
&signatures,
)
};
if is_valid && !validated.is_empty() {
let cap = self.verified_beacons.cap();
if cap < validated.len() {
tracing::warn!(%cap, validated_len=%validated.len(), "verified_beacons.cap() is too small");
}
for entry in validated {
self.verified_beacons.push(entry.round(), entry.clone());
}
}
Ok(is_valid)
}
async fn entry(&self, round: u64) -> anyhow::Result<BeaconEntry> {
let cached: Option<BeaconEntry> = self.verified_beacons.peek_cloned(&round);
match cached {
Some(cached_entry) => Ok(cached_entry),
None => {
async fn fetch_entry_from_url(
url: impl reqwest::IntoUrl,
) -> anyhow::Result<BeaconEntry> {
let resp: BeaconEntryJson = global_http_client()
.get(url)
.timeout(Duration::from_secs(15))
.send()
.await?
.error_for_status()?
.json()
.await?;
anyhow::Ok(BeaconEntry::new(resp.round, hex::decode(resp.signature)?))
}
async fn fetch_entry(
urls: impl Iterator<Item = impl reqwest::IntoUrl>,
) -> anyhow::Result<BeaconEntry> {
let mut errors = vec![];
for url in urls {
match fetch_entry_from_url(url).await {
Ok(e) => return Ok(e),
Err(e) => errors.push(e),
}
}
anyhow::bail!(
"Aggregated errors:\n{}",
errors.into_iter().map(|e| e.to_string()).join("\n\n")
);
}
let urls: Vec<_> = self
.servers
.iter()
.map(|server| {
anyhow::Ok(server.join(&format!("{}/public/{round}", self.hash))?)
})
.try_collect()?;
Ok((|| fetch_entry(urls.iter().cloned()))
.retry(ExponentialBuilder::default())
.notify(|err, dur| {
debug!(
"retrying fetch_entry after {}: {err:#}",
humantime::format_duration(dur)
);
})
.await?)
}
}
}
fn max_beacon_round_for_epoch(
&self,
network_version: NetworkVersion,
fil_epoch: ChainEpoch,
) -> u64 {
let latest_ts =
((fil_epoch as u64 * self.fil_round_time) + self.fil_gen_time) - self.fil_round_time;
if network_version <= NetworkVersion::V15 {
(latest_ts - self.drand_gen_time) / self.interval
} else {
if latest_ts < self.drand_gen_time {
return 1;
}
let from_genesis = latest_ts - self.drand_gen_time;
from_genesis / self.interval + 1
}
}
}