use std::io::Write;
use crate::beacon::{Beacon as _, BeaconEntry, BeaconSchedule};
use crate::blocks::Tipset;
use crate::chain::index::{ChainIndex, ResolveNullTipset};
use crate::networks::ChainConfig;
use crate::prelude::*;
use crate::shim::clock::ChainEpoch;
use crate::shim::externs::Rand;
use crate::shim::version::NetworkVersion;
use crate::utils::encoding::blake2b_256;
use anyhow::bail;
use blake2b_simd::Params;
use byteorder::{BigEndian, WriteBytesExt};
#[derive(derive_more::Constructor)]
pub struct ChainRand {
chain_config: Arc<ChainConfig>,
tipset: Tipset,
chain_index: ChainIndex,
beacon: Arc<BeaconSchedule>,
}
impl ShallowClone for ChainRand {
fn shallow_clone(&self) -> Self {
ChainRand {
chain_config: self.chain_config.shallow_clone(),
tipset: self.tipset.shallow_clone(),
chain_index: self.chain_index.shallow_clone(),
beacon: self.beacon.shallow_clone(),
}
}
}
impl ChainRand {
pub async fn get_chain_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
let this = self.shallow_clone();
tokio::task::spawn_blocking(move || this.get_chain_randomness_blocking(round)).await?
}
pub fn get_chain_randomness_blocking(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
let resolve = Self::null_round_resolution(self.chain_config.network_version(round));
self.chain_randomness_blocking(round, resolve)
}
fn chain_randomness_blocking(
&self,
round: ChainEpoch,
resolve: ResolveNullTipset,
) -> anyhow::Result<[u8; 32]> {
let ts = self.tipset.clone();
if round > ts.epoch() {
bail!("cannot draw randomness from the future");
}
let search_height = if round < 0 { 0 } else { round };
let rand_ts =
self.chain_index
.load_required_tipset_by_height_blocking(search_height, ts, resolve)?;
Ok(digest(
rand_ts
.min_ticket()
.context("No ticket exists for block")?
.vrfproof
.as_bytes(),
))
}
fn null_round_resolution(network_version: NetworkVersion) -> ResolveNullTipset {
if network_version < NetworkVersion::V13 {
ResolveNullTipset::TakeOlder
} else {
ResolveNullTipset::TakeNewer
}
}
pub fn get_beacon_randomness_blocking(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
let beacon_entry = self.beacon_entry_for_epoch(round)?;
Ok(digest(beacon_entry.signature()))
}
pub async fn get_beacon_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
let this = self.shallow_clone();
tokio::task::spawn_blocking(move || this.get_beacon_randomness_blocking(round)).await?
}
pub fn beacon_entry_for_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<BeaconEntry> {
let network_version = self.chain_config.network_version(epoch);
if network_version >= NetworkVersion::V14 {
return self.beacon_entry_for_epoch_post_nv14(epoch, network_version);
}
self.latest_beacon_entry_for_epoch(epoch, Self::null_round_resolution(network_version))
}
fn latest_beacon_entry_for_epoch(
&self,
epoch: ChainEpoch,
resolve: ResolveNullTipset,
) -> anyhow::Result<BeaconEntry> {
let rand_ts = self.get_beacon_randomness_tipset_blocking(epoch, resolve)?;
Ok(self.chain_index.latest_beacon_entry(rand_ts)?)
}
fn beacon_entry_for_epoch_post_nv14(
&self,
epoch: ChainEpoch,
network_version: NetworkVersion,
) -> anyhow::Result<BeaconEntry> {
if epoch < 0 {
return self.latest_beacon_entry_for_epoch(epoch, ResolveNullTipset::TakeNewer);
}
let mut rand_ts: Tipset =
self.get_beacon_randomness_tipset_blocking(epoch, ResolveNullTipset::TakeNewer)?;
let (_, beacon) = self.beacon.beacon_for_epoch(epoch)?;
let round = beacon.max_beacon_round_for_epoch(network_version, epoch)?;
for _ in 0..20 {
let cbe = &rand_ts.block_headers().first().beacon_entries;
for v in cbe {
if v.round() == round {
return Ok(v.clone());
}
}
rand_ts = self.chain_index.load_required_tipset(rand_ts.parents())?;
}
bail!("didn't find beacon for round {round:?} (epoch {epoch:?})")
}
pub fn get_beacon_randomness_tipset_blocking(
&self,
round: ChainEpoch,
resolve: ResolveNullTipset,
) -> anyhow::Result<Tipset> {
let ts = self.tipset.clone();
if round > ts.epoch() {
bail!("cannot draw randomness from the future");
}
let search_height = if round < 0 { 0 } else { round };
self.chain_index
.load_required_tipset_by_height_blocking(search_height, ts, resolve)
.map_err(|e| e.into())
}
}
impl Rand for ChainRand {
fn get_chain_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
self.get_chain_randomness_blocking(round).inspect_err(|e| {
tracing::warn!(
"get_chain_randomness failed, round: {round}, ts@{}: {}, error: {e:#}",
self.tipset.epoch(),
self.tipset.key()
);
})
}
fn get_beacon_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
self.get_beacon_randomness_blocking(round).inspect_err(|e| {
tracing::warn!(
"get_beacon_randomness failed, round: {round}, ts@{}: {}, error: {e:#}",
self.tipset.epoch(),
self.tipset.key()
);
})
}
}
pub fn draw_randomness(
rbase: &[u8],
pers: i64,
round: ChainEpoch,
entropy: &[u8],
) -> anyhow::Result<[u8; 32]> {
let mut state = Params::new().hash_length(32).to_state();
state.write_i64::<BigEndian>(pers)?;
let vrf_digest = digest(rbase);
state.write_all(&vrf_digest)?;
state.write_i64::<BigEndian>(round)?;
state.write_all(entropy)?;
let mut ret = [0u8; 32];
ret.clone_from_slice(state.finalize().as_bytes());
Ok(ret)
}
pub fn draw_randomness_from_digest(
digest: &[u8; 32],
pers: i64,
round: ChainEpoch,
entropy: &[u8],
) -> anyhow::Result<[u8; 32]> {
let mut state = Params::new().hash_length(32).to_state();
state.write_i64::<BigEndian>(pers)?;
state.write_all(digest)?;
state.write_i64::<BigEndian>(round)?;
state.write_all(entropy)?;
let mut ret = [0u8; 32];
ret.clone_from_slice(state.finalize().as_bytes());
Ok(ret)
}
pub fn digest(rbase: &[u8]) -> [u8; 32] {
blake2b_256(rbase)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beacon::{BeaconPoint, BeaconSchedule, mock_beacon::MockBeacon};
use crate::blocks::{CachingBlockHeader, RawBlockHeader};
use crate::chain::store::index::tests::persist_tipset;
use crate::db::MemoryDB;
use crate::make_height;
use crate::networks::{Height, HeightInfo};
use crate::shim::version::NetworkVersion;
use crate::test_utils::dummy_ticket;
use rstest::rstest;
const MAINNET_GENESIS_TIMESTAMP: u64 = 1598306400;
fn tipset_at(parent: Option<&Tipset>, epoch: ChainEpoch, rounds: &[u64]) -> Tipset {
Tipset::from(CachingBlockHeader::new(RawBlockHeader {
parents: parent.map(|p| p.key().clone()).unwrap_or_default(),
epoch,
timestamp: epoch as u64,
ticket: dummy_ticket(epoch as u8),
beacon_entries: rounds
.iter()
.map(|r| BeaconEntry::new(*r, vec![*r as u8; 32]))
.collect(),
..Default::default()
}))
}
fn chain_rand(
chain_config: ChainConfig,
beacon: BeaconSchedule,
chain: &[Tipset],
) -> ChainRand {
let db: Arc<MemoryDB> = Arc::new(MemoryDB::default());
for tipset in chain {
persist_tipset(tipset, &db);
}
let genesis = chain
.first()
.expect("chain starts at genesis")
.shallow_clone();
let head = chain.last().expect("chain has a head").shallow_clone();
let chain_index = ChainIndex::new(db, genesis);
ChainRand::new(Arc::new(chain_config), head, chain_index, Arc::new(beacon))
}
#[test]
fn beacon_entry_at_genesis_should_return_latest_tipset_entry() {
let chain_config = ChainConfig::mainnet();
let beacon: BeaconSchedule = chain_config.get_beacon_schedule(MAINNET_GENESIS_TIMESTAMP);
let genesis = tipset_at(None, 0, &[0]);
let entry = chain_rand(chain_config, beacon, &[genesis])
.beacon_entry_for_epoch(0)
.expect("epoch 0 resolves to the genesis beacon entry");
assert_eq!(entry.round(), 0);
assert_eq!(entry.signature(), &vec![0; 32]);
}
const BEFORE_NULLS: ChainEpoch = 10;
const AFTER_NULLS: ChainEpoch = 16;
fn chain_with_null_rounds() -> Vec<Tipset> {
let mut chain = vec![tipset_at(None, 0, &[0])];
for epoch in 1..=BEFORE_NULLS {
let parent = chain.last().expect("non-empty").shallow_clone();
chain.push(tipset_at(Some(&parent), epoch, &[epoch as u64]));
}
let parent = chain.last().expect("non-empty").shallow_clone();
let covered: Vec<u64> = ((BEFORE_NULLS + 1) as u64..=AFTER_NULLS as u64).collect();
chain.push(tipset_at(Some(&parent), AFTER_NULLS, &covered));
chain
}
fn chain_config_with_upgrade(upgrade: Option<(Height, HeightInfo)>) -> ChainConfig {
ChainConfig {
genesis_network: NetworkVersion::V12,
height_infos: upgrade.into_iter().collect(),
..ChainConfig::devnet()
}
}
const NULL_EPOCH: ChainEpoch = 14;
#[rstest]
#[case::before_nv13(None, BEFORE_NULLS as u64)]
#[case::at_nv13(Some(make_height!(Hyperdrive, 0)), AFTER_NULLS as u64)]
#[case::from_nv14(Some(make_height!(Chocolate, 0)), NULL_EPOCH as u64)]
fn null_round_beacon_entry(
#[case] upgrade: Option<(Height, HeightInfo)>,
#[case] expected_round: u64,
) {
let chain_config = chain_config_with_upgrade(upgrade);
let beacon = BeaconSchedule(vec![BeaconPoint::new(0, MockBeacon::default())]);
let entry = chain_rand(chain_config, beacon, &chain_with_null_rounds())
.beacon_entry_for_epoch(NULL_EPOCH)
.expect("resolves through the null run");
assert_eq!(entry.round(), expected_round);
}
#[rstest]
#[case::before_nv13(None, BEFORE_NULLS)]
#[case::from_nv13(Some(make_height!(Hyperdrive, 0)), AFTER_NULLS)]
fn null_round_chain_randomness(
#[case] upgrade: Option<(Height, HeightInfo)>,
#[case] expected_epoch: ChainEpoch,
) {
let chain = chain_with_null_rounds();
let expected = digest(
chain
.iter()
.find(|ts| ts.epoch() == expected_epoch)
.expect("tipset in chain")
.min_ticket()
.expect("ticket")
.vrfproof
.as_bytes(),
);
let beacon = BeaconSchedule(vec![BeaconPoint::new(0, MockBeacon::default())]);
let got = chain_rand(chain_config_with_upgrade(upgrade), beacon, &chain)
.get_chain_randomness_blocking(NULL_EPOCH)
.expect("resolves through the null run");
assert_eq!(got, expected);
}
}