Skip to main content

forest/networks/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::str::FromStr;
5use std::sync::LazyLock;
6
7use ahash::HashMap;
8use cid::Cid;
9use fvm_ipld_blockstore::Blockstore;
10use itertools::Itertools;
11use libp2p::Multiaddr;
12use num_traits::Zero;
13use serde::{Deserialize, Serialize};
14use strum::IntoEnumIterator;
15use strum_macros::Display;
16use strum_macros::EnumIter;
17use tracing::warn;
18
19use crate::beacon::{BeaconPoint, BeaconSchedule, DrandBeacon, DrandConfig};
20use crate::db::SettingsStore;
21use crate::eth::EthChainId;
22use crate::shim::{
23    clock::{ChainEpoch, EPOCH_DURATION_SECONDS, EPOCHS_IN_DAY},
24    econ::TokenAmount,
25    machine::BuiltinActorManifest,
26    runtime::Policy,
27    sector::{RegisteredPoStProofV3, RegisteredSealProofV3},
28    version::NetworkVersion,
29};
30use crate::utils::misc::env::env_or_default;
31use crate::{make_butterfly_policy, make_calibnet_policy, make_devnet_policy, make_mainnet_policy};
32
33pub use network_name::{GenesisNetworkName, StateNetworkName};
34
35mod actors_bundle;
36pub use actors_bundle::{
37    ACTOR_BUNDLES, ACTOR_BUNDLES_METADATA, ActorBundleInfo, ActorBundleMetadata,
38    generate_actor_bundle, get_actor_bundles_metadata,
39};
40
41mod drand;
42
43pub mod network_name;
44
45pub mod butterflynet;
46pub mod calibnet;
47pub mod devnet;
48pub mod mainnet;
49
50pub mod metrics;
51
52/// Newest network version for all networks
53pub const NEWEST_NETWORK_VERSION: NetworkVersion = NetworkVersion::V25;
54
55const ENV_FOREST_BLOCK_DELAY_SECS: &str = "FOREST_BLOCK_DELAY_SECS";
56const ENV_FOREST_PROPAGATION_DELAY_SECS: &str = "FOREST_PROPAGATION_DELAY_SECS";
57const ENV_PLEDGE_RULE_RAMP: &str = "FOREST_PLEDGE_RULE_RAMP";
58
59static INITIAL_FIL_RESERVED: LazyLock<TokenAmount> =
60    LazyLock::new(|| TokenAmount::from_whole(300_000_000));
61
62/// Forest builtin `filecoin` network chains. In general only `mainnet` and its
63/// chain information should be considered stable.
64#[derive(
65    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Hash, derive_more::Display,
66)]
67#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
68#[serde(tag = "type", content = "name", rename_all = "lowercase")]
69#[display(rename_all = "lowercase")]
70pub enum NetworkChain {
71    #[default]
72    Mainnet,
73    Calibnet,
74    Butterflynet,
75    Devnet(String),
76}
77
78impl FromStr for NetworkChain {
79    type Err = anyhow::Error;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        match s {
83            mainnet::NETWORK_COMMON_NAME | mainnet::NETWORK_GENESIS_NAME => {
84                Ok(NetworkChain::Mainnet)
85            }
86            calibnet::NETWORK_COMMON_NAME | calibnet::NETWORK_GENESIS_NAME => {
87                Ok(NetworkChain::Calibnet)
88            }
89            butterflynet::NETWORK_COMMON_NAME => Ok(NetworkChain::Butterflynet),
90            name => Ok(NetworkChain::Devnet(name.to_owned())),
91        }
92    }
93}
94
95impl NetworkChain {
96    /// Returns the `NetworkChain`s internal name as set in the genesis block, which is not the
97    /// same as the recent state network name.
98    ///
99    /// As a rule of thumb, the internal name should be used when interacting with
100    /// protocol internals and P2P.
101    pub fn genesis_name(&self) -> GenesisNetworkName {
102        match self {
103            NetworkChain::Mainnet => mainnet::NETWORK_GENESIS_NAME.into(),
104            NetworkChain::Calibnet => calibnet::NETWORK_GENESIS_NAME.into(),
105            _ => self.to_string().into(),
106        }
107    }
108    /// Returns [`NetworkChain::Calibnet`] or [`NetworkChain::Mainnet`] if `cid`
109    /// is the hard-coded genesis CID for either of those networks.
110    pub fn from_genesis(cid: &Cid) -> Option<Self> {
111        if cid == &*mainnet::GENESIS_CID {
112            Some(Self::Mainnet)
113        } else if cid == &*calibnet::GENESIS_CID {
114            Some(Self::Calibnet)
115        } else if cid == &*butterflynet::GENESIS_CID {
116            Some(Self::Butterflynet)
117        } else {
118            None
119        }
120    }
121
122    /// Returns [`NetworkChain::Calibnet`] or [`NetworkChain::Mainnet`] if `cid`
123    /// is the hard-coded genesis CID for either of those networks.
124    ///
125    /// Else returns a [`NetworkChain::Devnet`] with a placeholder name.
126    pub fn from_genesis_or_devnet_placeholder(cid: &Cid) -> Self {
127        Self::from_genesis(cid).unwrap_or(Self::Devnet(String::from("devnet")))
128    }
129
130    pub fn is_testnet(&self) -> bool {
131        !matches!(self, NetworkChain::Mainnet)
132    }
133
134    pub fn is_devnet(&self) -> bool {
135        matches!(self, NetworkChain::Devnet(..))
136    }
137}
138
139/// Defines the meaningful heights of the protocol.
140#[derive(
141    Debug, Default, Display, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter,
142)]
143#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
144pub enum Height {
145    #[default]
146    Breeze,
147    Smoke,
148    Ignition,
149    Refuel,
150    Assembly,
151    Tape,
152    Liftoff,
153    Kumquat,
154    Calico,
155    Persian,
156    Orange,
157    Claus,
158    Trust,
159    Norwegian,
160    Turbo,
161    Hyperdrive,
162    Chocolate,
163    OhSnap,
164    Skyr,
165    Shark,
166    Hygge,
167    Lightning,
168    Thunder,
169    Watermelon,
170    WatermelonFix,
171    WatermelonFix2,
172    Dragon,
173    DragonFix,
174    Phoenix,
175    Waffle,
176    TukTuk,
177    Teep,
178    Tock,
179    TockFix,
180    GoldenWeek,
181}
182
183impl From<Height> for NetworkVersion {
184    fn from(height: Height) -> NetworkVersion {
185        match height {
186            Height::Breeze => NetworkVersion::V1,
187            Height::Smoke => NetworkVersion::V2,
188            Height::Ignition => NetworkVersion::V3,
189            Height::Refuel => NetworkVersion::V3,
190            Height::Assembly => NetworkVersion::V4,
191            Height::Tape => NetworkVersion::V5,
192            Height::Liftoff => NetworkVersion::V5,
193            Height::Kumquat => NetworkVersion::V6,
194            Height::Calico => NetworkVersion::V7,
195            Height::Persian => NetworkVersion::V8,
196            Height::Orange => NetworkVersion::V9,
197            Height::Claus => NetworkVersion::V9,
198            Height::Trust => NetworkVersion::V10,
199            Height::Norwegian => NetworkVersion::V11,
200            Height::Turbo => NetworkVersion::V12,
201            Height::Hyperdrive => NetworkVersion::V13,
202            Height::Chocolate => NetworkVersion::V14,
203            Height::OhSnap => NetworkVersion::V15,
204            Height::Skyr => NetworkVersion::V16,
205            Height::Shark => NetworkVersion::V17,
206            Height::Hygge => NetworkVersion::V18,
207            Height::Lightning => NetworkVersion::V19,
208            Height::Thunder => NetworkVersion::V20,
209            Height::Watermelon => NetworkVersion::V21,
210            Height::WatermelonFix => NetworkVersion::V21,
211            Height::WatermelonFix2 => NetworkVersion::V21,
212            Height::Dragon => NetworkVersion::V22,
213            Height::DragonFix => NetworkVersion::V22,
214            Height::Phoenix => NetworkVersion::V22,
215            Height::Waffle => NetworkVersion::V23,
216            Height::TukTuk => NetworkVersion::V24,
217            Height::Teep => NetworkVersion::V25,
218            Height::Tock => NetworkVersion::V26,
219            Height::TockFix => NetworkVersion::V26,
220            Height::GoldenWeek => NetworkVersion::V27,
221        }
222    }
223}
224
225#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
226#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
227pub struct HeightInfo {
228    pub epoch: ChainEpoch,
229    pub bundle: Option<Cid>,
230}
231
232pub struct HeightInfoWithActorManifest<'a> {
233    #[allow(dead_code)]
234    pub height: Height,
235    pub info: &'a HeightInfo,
236    pub manifest_cid: Cid,
237}
238
239impl<'a> HeightInfoWithActorManifest<'a> {
240    pub fn manifest(&self, store: &impl Blockstore) -> anyhow::Result<BuiltinActorManifest> {
241        BuiltinActorManifest::load_manifest(store, &self.manifest_cid)
242    }
243}
244
245#[derive(Clone)]
246struct DrandPoint<'a> {
247    pub height: ChainEpoch,
248    pub config: &'a LazyLock<DrandConfig<'a>>,
249}
250
251/// Defines all network configuration parameters.
252#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
253#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
254#[serde(default)]
255pub struct ChainConfig {
256    pub network: NetworkChain,
257    pub genesis_cid: Option<String>,
258    #[cfg_attr(test, arbitrary(gen(
259        |g: &mut quickcheck::Gen| {
260            let addr = std::net::Ipv4Addr::arbitrary(&mut *g);
261            let n = u8::arbitrary(g) as usize;
262            vec![addr.into(); n]
263        }
264    )))]
265    pub bootstrap_peers: Vec<Multiaddr>,
266    pub block_delay_secs: u32,
267    pub propagation_delay_secs: u32,
268    pub genesis_network: NetworkVersion,
269    pub height_infos: HashMap<Height, HeightInfo>,
270    #[cfg_attr(test, arbitrary(gen(|_g| Policy::default())))]
271    pub policy: Policy,
272    pub eth_chain_id: EthChainId,
273    pub breeze_gas_tamping_duration: i64,
274    // FIP0081 gradually comes into effect over this many epochs.
275    pub fip0081_ramp_duration_epochs: u64,
276    // See FIP-0100 and https://github.com/filecoin-project/lotus/pull/12938 for why this exists
277    pub upgrade_teep_initial_fil_reserved: Option<TokenAmount>,
278    pub f3_enabled: bool,
279    // F3Consensus set whether F3 should checkpoint tipsets finalized by F3. This flag has no effect if F3 is not enabled.
280    pub f3_consensus: bool,
281    pub f3_bootstrap_epoch: i64,
282    pub f3_initial_power_table: Option<Cid>,
283    pub enable_indexer: bool,
284    pub enable_receipt_event_caching: bool,
285    pub default_max_fee: TokenAmount,
286}
287
288impl ChainConfig {
289    pub fn mainnet() -> Self {
290        use mainnet::*;
291        Self {
292            network: NetworkChain::Mainnet,
293            genesis_cid: Some(GENESIS_CID.to_string()),
294            bootstrap_peers: DEFAULT_BOOTSTRAP.clone(),
295            block_delay_secs: env_or_default(
296                ENV_FOREST_BLOCK_DELAY_SECS,
297                EPOCH_DURATION_SECONDS as u32,
298            ),
299            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 10),
300            genesis_network: GENESIS_NETWORK_VERSION,
301            height_infos: HEIGHT_INFOS.clone(),
302            policy: make_mainnet_policy!(v13).into(),
303            eth_chain_id: ETH_CHAIN_ID,
304            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
305            // 1 year on mainnet
306            fip0081_ramp_duration_epochs: 365 * EPOCHS_IN_DAY as u64,
307            upgrade_teep_initial_fil_reserved: None,
308            f3_enabled: true,
309            f3_consensus: true,
310            // April 29 at 10:00 UTC
311            f3_bootstrap_epoch: 4920480,
312            f3_initial_power_table: Some(
313                "bafy2bzacecklgxd2eksmodvhgurqvorkg3wamgqkrunir3al2gchv2cikgmbu"
314                    .parse()
315                    .expect("invalid f3_initial_power_table"),
316            ),
317            enable_indexer: false,
318            enable_receipt_event_caching: true,
319            default_max_fee: TokenAmount::zero(),
320        }
321    }
322
323    pub fn calibnet() -> Self {
324        use calibnet::*;
325        Self {
326            network: NetworkChain::Calibnet,
327            genesis_cid: Some(GENESIS_CID.to_string()),
328            bootstrap_peers: DEFAULT_BOOTSTRAP.clone(),
329            block_delay_secs: env_or_default(
330                ENV_FOREST_BLOCK_DELAY_SECS,
331                EPOCH_DURATION_SECONDS as u32,
332            ),
333            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 10),
334            genesis_network: GENESIS_NETWORK_VERSION,
335            height_infos: HEIGHT_INFOS.clone(),
336            policy: make_calibnet_policy!(v13).into(),
337            eth_chain_id: ETH_CHAIN_ID,
338            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
339            // 3 days on calibnet
340            fip0081_ramp_duration_epochs: 3 * EPOCHS_IN_DAY as u64,
341            // FIP-0100: 300M -> 1.2B FIL
342            upgrade_teep_initial_fil_reserved: Some(TokenAmount::from_whole(1_200_000_000)),
343            // Enable after `f3_initial_power_table` is determined and set to avoid GC hell
344            // (state tree of epoch 3_451_774 - 900 has to be present in the database if `f3_initial_power_table` is not set)
345            f3_enabled: true,
346            f3_consensus: true,
347            // 2026-02-12T07:00:00Z
348            f3_bootstrap_epoch: 3_451_774,
349            f3_initial_power_table: None,
350            enable_indexer: false,
351            enable_receipt_event_caching: true,
352            default_max_fee: TokenAmount::zero(),
353        }
354    }
355
356    pub fn devnet() -> Self {
357        use devnet::*;
358        Self {
359            network: NetworkChain::Devnet("devnet".to_string()),
360            genesis_cid: None,
361            bootstrap_peers: Vec::new(),
362            block_delay_secs: env_or_default(ENV_FOREST_BLOCK_DELAY_SECS, 4),
363            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 1),
364            genesis_network: *GENESIS_NETWORK_VERSION,
365            height_infos: HEIGHT_INFOS.clone(),
366            policy: make_devnet_policy!(v13).into(),
367            eth_chain_id: ETH_CHAIN_ID,
368            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
369            // Devnet ramp is 200 epochs in Lotus (subject to change).
370            fip0081_ramp_duration_epochs: env_or_default(ENV_PLEDGE_RULE_RAMP, 200),
371            // FIP-0100: 300M -> 1.4B FIL
372            upgrade_teep_initial_fil_reserved: Some(TokenAmount::from_whole(1_400_000_000)),
373            f3_enabled: false,
374            f3_consensus: false,
375            f3_bootstrap_epoch: -1,
376            f3_initial_power_table: None,
377            enable_indexer: false,
378            enable_receipt_event_caching: true,
379            default_max_fee: TokenAmount::zero(),
380        }
381    }
382
383    pub fn butterflynet() -> Self {
384        use butterflynet::*;
385        Self {
386            network: NetworkChain::Butterflynet,
387            genesis_cid: Some(GENESIS_CID.to_string()),
388            bootstrap_peers: DEFAULT_BOOTSTRAP.clone(),
389            block_delay_secs: env_or_default(
390                ENV_FOREST_BLOCK_DELAY_SECS,
391                EPOCH_DURATION_SECONDS as u32,
392            ),
393            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 6),
394            genesis_network: GENESIS_NETWORK_VERSION,
395            height_infos: HEIGHT_INFOS.clone(),
396            policy: make_butterfly_policy!(v13).into(),
397            eth_chain_id: ETH_CHAIN_ID,
398            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
399            // Butterflynet ramp is current set to 365 days in Lotus but this may change.
400            fip0081_ramp_duration_epochs: env_or_default(
401                ENV_PLEDGE_RULE_RAMP,
402                365 * EPOCHS_IN_DAY as u64,
403            ),
404            // FIP-0100: 300M -> 1.6B FIL
405            upgrade_teep_initial_fil_reserved: Some(TokenAmount::from_whole(1_600_000_000)),
406            f3_enabled: true,
407            f3_consensus: true,
408            f3_bootstrap_epoch: 1000,
409            f3_initial_power_table: None,
410            enable_indexer: false,
411            enable_receipt_event_caching: true,
412            default_max_fee: TokenAmount::zero(),
413        }
414    }
415
416    pub fn from_chain(network_chain: &NetworkChain) -> Self {
417        match network_chain {
418            NetworkChain::Mainnet => Self::mainnet(),
419            NetworkChain::Calibnet => Self::calibnet(),
420            NetworkChain::Butterflynet => Self::butterflynet(),
421            NetworkChain::Devnet(name) => Self {
422                network: NetworkChain::Devnet(name.clone()),
423                ..Self::devnet()
424            },
425        }
426    }
427
428    fn network_height(&self, epoch: ChainEpoch) -> Option<Height> {
429        self.height_infos
430            .iter()
431            .sorted_by_key(|(_, info)| info.epoch)
432            .rev()
433            .find(|(_, info)| epoch > info.epoch)
434            .map(|(height, _)| *height)
435    }
436
437    /// Gets the latest network height prior to the given epoch that upgrades the actor bundle
438    pub fn network_height_with_actor_bundle<'a>(
439        &'a self,
440        epoch: ChainEpoch,
441    ) -> Option<HeightInfoWithActorManifest<'a>> {
442        if let Some((height, info, manifest_cid)) = self
443            .height_infos
444            .iter()
445            .sorted_by_key(|(_, info)| info.epoch)
446            .rev()
447            .filter_map(|(height, info)| info.bundle.map(|bundle| (*height, info, bundle)))
448            .find(|(_, info, _)| epoch > info.epoch)
449        {
450            Some(HeightInfoWithActorManifest {
451                height,
452                info,
453                manifest_cid,
454            })
455        } else {
456            None
457        }
458    }
459
460    /// Returns the network version at the given epoch.
461    /// If the epoch is before the first upgrade, the genesis network version is returned.
462    pub fn network_version(&self, epoch: ChainEpoch) -> NetworkVersion {
463        self.network_height(epoch)
464            .map(NetworkVersion::from)
465            .unwrap_or(self.genesis_network_version())
466            .max(self.genesis_network)
467    }
468
469    /// Returns the network version revision at the given epoch for distinguishing network upgrades
470    /// that do not bump the network version.
471    pub fn network_version_revision(&self, epoch: ChainEpoch) -> i64 {
472        if let Some(height) = self.network_height(epoch) {
473            let nv = NetworkVersion::from(height);
474            if let Some(rev0_height) = Height::iter().find(|h| NetworkVersion::from(*h) == nv) {
475                return (height as i64) - (rev0_height as i64);
476            }
477        }
478        0
479    }
480
481    pub fn get_beacon_schedule(&self, genesis_ts: u64) -> BeaconSchedule {
482        let ds_iter = match self.network {
483            NetworkChain::Mainnet => mainnet::DRAND_SCHEDULE.iter(),
484            NetworkChain::Calibnet => calibnet::DRAND_SCHEDULE.iter(),
485            NetworkChain::Butterflynet => butterflynet::DRAND_SCHEDULE.iter(),
486            NetworkChain::Devnet(_) => devnet::DRAND_SCHEDULE.iter(),
487        };
488
489        BeaconSchedule(
490            ds_iter
491                .map(|dc| BeaconPoint {
492                    height: dc.height,
493                    beacon: Box::new(DrandBeacon::new(
494                        genesis_ts,
495                        self.block_delay_secs as u64,
496                        dc.config,
497                    )),
498                })
499                .collect(),
500        )
501    }
502
503    pub fn epoch(&self, height: Height) -> ChainEpoch {
504        self.height_infos
505            .iter()
506            .sorted_by_key(|(_, info)| info.epoch)
507            .rev()
508            .find_map(|(infos_height, info)| {
509                if *infos_height == height {
510                    Some(info.epoch)
511                } else {
512                    None
513                }
514            })
515            .unwrap_or(0)
516    }
517
518    pub async fn genesis_bytes<DB: SettingsStore>(
519        &self,
520        db: &DB,
521    ) -> anyhow::Result<Option<Vec<u8>>> {
522        Ok(match self.network {
523            NetworkChain::Mainnet => Some(mainnet::DEFAULT_GENESIS.to_vec()),
524            NetworkChain::Calibnet => Some(calibnet::DEFAULT_GENESIS.to_vec()),
525            // Butterflynet genesis is not hardcoded in the binary, for size reasons.
526            NetworkChain::Butterflynet => Some(butterflynet::fetch_genesis(db).await?),
527            NetworkChain::Devnet(_) => None,
528        })
529    }
530
531    pub fn is_testnet(&self) -> bool {
532        self.network.is_testnet()
533    }
534
535    pub fn is_devnet(&self) -> bool {
536        self.network.is_devnet()
537    }
538
539    pub fn genesis_network_version(&self) -> NetworkVersion {
540        self.genesis_network
541    }
542
543    pub fn initial_fil_reserved(&self, network_version: NetworkVersion) -> &TokenAmount {
544        match &self.upgrade_teep_initial_fil_reserved {
545            Some(fil) if network_version >= NetworkVersion::V25 => fil,
546            _ => &INITIAL_FIL_RESERVED,
547        }
548    }
549
550    pub fn initial_fil_reserved_at_height(&self, height: i64) -> &TokenAmount {
551        let network_version = self.network_version(height);
552        self.initial_fil_reserved(network_version)
553    }
554}
555
556impl Default for ChainConfig {
557    fn default() -> Self {
558        ChainConfig::mainnet()
559    }
560}
561
562pub(crate) fn parse_bootstrap_peers(bootstrap_peer_list: &str) -> Vec<Multiaddr> {
563    bootstrap_peer_list
564        .split('\n')
565        .filter(|s| !s.is_empty())
566        .map(|s| {
567            Multiaddr::from_str(s).unwrap_or_else(|e| panic!("invalid bootstrap peer {s}: {e}"))
568        })
569        .collect()
570}
571
572#[allow(dead_code)]
573fn get_upgrade_epoch_by_height<'a>(
574    mut height_infos: impl Iterator<Item = &'a (Height, HeightInfo)>,
575    height: Height,
576) -> Option<ChainEpoch> {
577    height_infos.find_map(|(infos_height, info)| {
578        if *infos_height == height {
579            Some(info.epoch)
580        } else {
581            None
582        }
583    })
584}
585
586fn get_upgrade_height_from_env(env_var_key: &str) -> Option<ChainEpoch> {
587    if let Ok(value) = std::env::var(env_var_key) {
588        if let Ok(epoch) = value.parse() {
589            return Some(epoch);
590        } else {
591            warn!("Failed to parse {env_var_key}={value}, value should be an integer");
592        }
593    }
594    None
595}
596
597#[macro_export]
598macro_rules! make_height {
599    ($id:ident,$epoch:expr) => {
600        (
601            Height::$id,
602            HeightInfo {
603                epoch: $epoch,
604                bundle: None,
605            },
606        )
607    };
608    ($id:ident,$epoch:expr,$bundle:expr) => {
609        (
610            Height::$id,
611            HeightInfo {
612                epoch: $epoch,
613                bundle: Some(Cid::try_from($bundle).unwrap()),
614            },
615        )
616    };
617}
618
619// The formula matches lotus
620// ```go
621// sinceGenesis := build.Clock.Now().Sub(genesisTime)
622// expectedHeight := int64(sinceGenesis.Seconds()) / int64(build.BlockDelaySecs)
623// ```
624// See <https://github.com/filecoin-project/lotus/blob/b27c861485695d3f5bb92bcb281abc95f4d90fb6/chain/sync.go#L180>
625pub fn calculate_expected_epoch(
626    now_timestamp: u64,
627    genesis_timestamp: u64,
628    block_delay_secs: u32,
629) -> i64 {
630    (now_timestamp.saturating_sub(genesis_timestamp) / block_delay_secs as u64) as i64
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    fn heights_are_present(height_infos: &HashMap<Height, HeightInfo>) {
638        /// These are required heights that need to be defined for all networks, for, e.g., conformance
639        /// with `Filecoin.StateGetNetworkParams` RPC method.
640        const REQUIRED_HEIGHTS: [Height; 30] = [
641            Height::Breeze,
642            Height::Smoke,
643            Height::Ignition,
644            Height::Refuel,
645            Height::Assembly,
646            Height::Tape,
647            Height::Liftoff,
648            Height::Kumquat,
649            Height::Calico,
650            Height::Persian,
651            Height::Orange,
652            Height::Claus,
653            Height::Trust,
654            Height::Norwegian,
655            Height::Turbo,
656            Height::Hyperdrive,
657            Height::Chocolate,
658            Height::OhSnap,
659            Height::Skyr,
660            Height::Shark,
661            Height::Hygge,
662            Height::Lightning,
663            Height::Thunder,
664            Height::Watermelon,
665            Height::Dragon,
666            Height::Phoenix,
667            Height::Waffle,
668            Height::TukTuk,
669            Height::Teep,
670            Height::GoldenWeek,
671        ];
672
673        for height in &REQUIRED_HEIGHTS {
674            assert!(height_infos.get(height).is_some());
675        }
676    }
677
678    #[test]
679    fn test_mainnet_heights() {
680        heights_are_present(&mainnet::HEIGHT_INFOS);
681    }
682
683    #[test]
684    fn test_calibnet_heights() {
685        heights_are_present(&calibnet::HEIGHT_INFOS);
686    }
687
688    #[test]
689    fn test_devnet_heights() {
690        heights_are_present(&devnet::HEIGHT_INFOS);
691    }
692
693    #[test]
694    fn test_butterflynet_heights() {
695        heights_are_present(&butterflynet::HEIGHT_INFOS);
696    }
697
698    #[test]
699    fn test_get_upgrade_height_no_env_var() {
700        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_1");
701        assert_eq!(epoch, None);
702    }
703
704    #[test]
705    fn test_get_upgrade_height_valid_env_var() {
706        unsafe { std::env::set_var("FOREST_TEST_VAR_2", "10") };
707        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_2");
708        assert_eq!(epoch, Some(10));
709    }
710
711    #[test]
712    fn test_get_upgrade_height_invalid_env_var() {
713        unsafe { std::env::set_var("FOREST_TEST_VAR_3", "foo") };
714        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_3");
715        assert_eq!(epoch, None);
716    }
717
718    #[test]
719    fn test_calculate_expected_epoch() {
720        // now, genesis, block_delay
721        assert_eq!(0, calculate_expected_epoch(0, 0, 1));
722        assert_eq!(5, calculate_expected_epoch(5, 0, 1));
723
724        let mainnet_genesis = 1598306400;
725        let mainnet_block_delay = 30;
726
727        assert_eq!(
728            0,
729            calculate_expected_epoch(mainnet_genesis, mainnet_genesis, mainnet_block_delay)
730        );
731
732        assert_eq!(
733            0,
734            calculate_expected_epoch(
735                mainnet_genesis + mainnet_block_delay as u64 - 1,
736                mainnet_genesis,
737                mainnet_block_delay
738            )
739        );
740
741        assert_eq!(
742            1,
743            calculate_expected_epoch(
744                mainnet_genesis + mainnet_block_delay as u64,
745                mainnet_genesis,
746                mainnet_block_delay
747            )
748        );
749    }
750
751    #[test]
752    fn network_chain_display() {
753        assert_eq!(
754            NetworkChain::Mainnet.to_string(),
755            mainnet::NETWORK_COMMON_NAME
756        );
757        assert_eq!(
758            NetworkChain::Calibnet.to_string(),
759            calibnet::NETWORK_COMMON_NAME
760        );
761        assert_eq!(
762            NetworkChain::Butterflynet.to_string(),
763            butterflynet::NETWORK_COMMON_NAME
764        );
765        assert_eq!(
766            NetworkChain::Devnet("dummydevnet".into()).to_string(),
767            "dummydevnet"
768        );
769    }
770
771    #[test]
772    fn chain_config() {
773        ChainConfig::mainnet();
774        ChainConfig::calibnet();
775        ChainConfig::devnet();
776        ChainConfig::butterflynet();
777    }
778
779    #[test]
780    fn network_version() {
781        let cfg = ChainConfig::calibnet();
782        assert_eq!(cfg.network_version(1_013_134 - 1), NetworkVersion::V20);
783        assert_eq!(cfg.network_version(1_013_134), NetworkVersion::V20);
784        assert_eq!(cfg.network_version(1_013_134 + 1), NetworkVersion::V21);
785        assert_eq!(cfg.network_version_revision(1_013_134 + 1), 0);
786        assert_eq!(cfg.network_version(1_070_494), NetworkVersion::V21);
787        assert_eq!(cfg.network_version_revision(1_070_494), 0);
788        assert_eq!(cfg.network_version(1_070_494 + 1), NetworkVersion::V21);
789        assert_eq!(cfg.network_version_revision(1_070_494 + 1), 1);
790    }
791
792    #[test]
793    fn test_network_height_with_actor_bundle() {
794        let cfg = ChainConfig::mainnet();
795        let info = cfg.network_height_with_actor_bundle(5_348_280 + 1).unwrap();
796        assert_eq!(info.height, Height::GoldenWeek);
797        let info = cfg.network_height_with_actor_bundle(5_348_280).unwrap();
798        // No actor bundle for Tock, so it should be Teep
799        assert_eq!(info.height, Height::Teep);
800        let info = cfg.network_height_with_actor_bundle(5_348_280 - 1).unwrap();
801        assert_eq!(info.height, Height::Teep);
802        assert!(cfg.network_height_with_actor_bundle(1).is_none());
803        assert!(cfg.network_height_with_actor_bundle(0).is_none());
804    }
805}