forest/state_manager/
chain_rand.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{io::Write, sync::Arc};
5
6use crate::beacon::{BeaconEntry, BeaconSchedule};
7use crate::blocks::Tipset;
8use crate::chain::index::{ChainIndex, ResolveNullTipset};
9use crate::networks::ChainConfig;
10use crate::shim::clock::ChainEpoch;
11use crate::shim::externs::Rand;
12use crate::utils::encoding::blake2b_256;
13use anyhow::{Context as _, bail};
14use blake2b_simd::Params;
15use byteorder::{BigEndian, WriteBytesExt};
16use fvm_ipld_blockstore::Blockstore;
17
18/// Allows for deriving the randomness from a particular tipset.
19pub struct ChainRand<DB> {
20    chain_config: Arc<ChainConfig>,
21    tipset: Arc<Tipset>,
22    chain_index: Arc<ChainIndex<Arc<DB>>>,
23    beacon: Arc<BeaconSchedule>,
24}
25
26impl<DB> Clone for ChainRand<DB> {
27    fn clone(&self) -> Self {
28        ChainRand {
29            chain_config: self.chain_config.clone(),
30            tipset: Arc::clone(&self.tipset),
31            chain_index: self.chain_index.clone(),
32            beacon: self.beacon.clone(),
33        }
34    }
35}
36
37impl<DB> ChainRand<DB>
38where
39    DB: Blockstore,
40{
41    pub fn new(
42        chain_config: Arc<ChainConfig>,
43        tipset: Arc<Tipset>,
44        chain_index: Arc<ChainIndex<Arc<DB>>>,
45        beacon: Arc<BeaconSchedule>,
46    ) -> Self {
47        Self {
48            chain_config,
49            tipset,
50            chain_index,
51            beacon,
52        }
53    }
54
55    /// Gets 32 bytes of randomness for `ChainRand` parameterized by the
56    /// `DomainSeparationTag`, `ChainEpoch`, Entropy from the ticket chain.
57    pub fn get_chain_randomness(
58        &self,
59        round: ChainEpoch,
60        lookback: bool,
61    ) -> anyhow::Result<[u8; 32]> {
62        let ts = Arc::clone(&self.tipset);
63
64        if round > ts.epoch() {
65            bail!("cannot draw randomness from the future");
66        }
67
68        let search_height = if round < 0 { 0 } else { round };
69
70        let resolve = if lookback {
71            ResolveNullTipset::TakeOlder
72        } else {
73            ResolveNullTipset::TakeNewer
74        };
75        let rand_ts = self
76            .chain_index
77            .tipset_by_height(search_height, ts, resolve)?;
78
79        Ok(digest(
80            rand_ts
81                .min_ticket()
82                .context("No ticket exists for block")?
83                .vrfproof
84                .as_bytes(),
85        ))
86    }
87
88    /// network version 13 onward
89    pub fn get_chain_randomness_v2(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
90        self.get_chain_randomness(round, false)
91    }
92
93    /// network version 13; without look-back
94    pub fn get_beacon_randomness_v2(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
95        self.get_beacon_randomness(round, false)
96    }
97
98    /// network version 14 onward
99    pub fn get_beacon_randomness_v3(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
100        if round < 0 {
101            return self.get_beacon_randomness_v2(round);
102        }
103
104        let beacon_entry = self.extract_beacon_entry_for_epoch(round)?;
105        Ok(digest(beacon_entry.signature()))
106    }
107
108    /// Gets 32 bytes of randomness for `ChainRand` parameterized by the
109    /// `DomainSeparationTag`, `ChainEpoch`, Entropy from the latest beacon
110    /// entry.
111    pub fn get_beacon_randomness(
112        &self,
113        round: ChainEpoch,
114        lookback: bool,
115    ) -> anyhow::Result<[u8; 32]> {
116        let rand_ts: Arc<Tipset> = self.get_beacon_randomness_tipset(round, lookback)?;
117        let be = self.chain_index.latest_beacon_entry(rand_ts)?;
118        Ok(digest(be.signature()))
119    }
120
121    pub fn extract_beacon_entry_for_epoch(&self, epoch: ChainEpoch) -> anyhow::Result<BeaconEntry> {
122        let mut rand_ts: Arc<Tipset> = self.get_beacon_randomness_tipset(epoch, false)?;
123        let (_, beacon) = self.beacon.beacon_for_epoch(epoch)?;
124        let round =
125            beacon.max_beacon_round_for_epoch(self.chain_config.network_version(epoch), epoch);
126
127        for _ in 0..20 {
128            let cbe = &rand_ts.block_headers().first().beacon_entries;
129            for v in cbe {
130                if v.round() == round {
131                    return Ok(v.clone());
132                }
133            }
134
135            rand_ts = self.chain_index.load_required_tipset(rand_ts.parents())?;
136        }
137
138        bail!(
139            "didn't find beacon for round {:?} (epoch {:?})",
140            round,
141            epoch
142        )
143    }
144
145    pub fn get_beacon_randomness_tipset(
146        &self,
147        round: ChainEpoch,
148        lookback: bool,
149    ) -> anyhow::Result<Arc<Tipset>> {
150        let ts = Arc::clone(&self.tipset);
151
152        if round > ts.epoch() {
153            bail!("cannot draw randomness from the future");
154        }
155
156        let search_height = if round < 0 { 0 } else { round };
157
158        let resolve = if lookback {
159            ResolveNullTipset::TakeOlder
160        } else {
161            ResolveNullTipset::TakeNewer
162        };
163
164        self.chain_index
165            .tipset_by_height(search_height, ts, resolve)
166            .map_err(|e| e.into())
167    }
168}
169
170impl<DB> Rand for ChainRand<DB>
171where
172    DB: Blockstore,
173{
174    fn get_chain_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
175        self.get_chain_randomness_v2(round)
176    }
177
178    fn get_beacon_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
179        self.get_beacon_randomness_v3(round)
180    }
181}
182
183/// Computes a pseudo random 32 byte `Vec`.
184pub fn draw_randomness(
185    rbase: &[u8],
186    pers: i64,
187    round: ChainEpoch,
188    entropy: &[u8],
189) -> anyhow::Result<[u8; 32]> {
190    let mut state = Params::new().hash_length(32).to_state();
191    state.write_i64::<BigEndian>(pers)?;
192    let vrf_digest = digest(rbase);
193    state.write_all(&vrf_digest)?;
194    state.write_i64::<BigEndian>(round)?;
195    state.write_all(entropy)?;
196    let mut ret = [0u8; 32];
197    ret.clone_from_slice(state.finalize().as_bytes());
198    Ok(ret)
199}
200
201/// Computes a pseudo random 32 byte `Vec` from digest
202pub fn draw_randomness_from_digest(
203    digest: &[u8; 32],
204    pers: i64,
205    round: ChainEpoch,
206    entropy: &[u8],
207) -> anyhow::Result<[u8; 32]> {
208    let mut state = Params::new().hash_length(32).to_state();
209    state.write_i64::<BigEndian>(pers)?;
210    state.write_all(digest)?;
211    state.write_i64::<BigEndian>(round)?;
212    state.write_all(entropy)?;
213    let mut ret = [0u8; 32];
214    ret.clone_from_slice(state.finalize().as_bytes());
215    Ok(ret)
216}
217
218/// Computes a 256-bit digest.
219/// See <https://github.com/filecoin-project/ref-fvm/blob/master/fvm/CHANGELOG.md#360-2023-08-18>
220pub fn digest(rbase: &[u8]) -> [u8; 32] {
221    blake2b_256(rbase)
222}