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