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::V27;
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    FireHorse,
180}
181
182impl From<Height> for NetworkVersion {
183    fn from(height: Height) -> NetworkVersion {
184        match height {
185            Height::Breeze => NetworkVersion::V1,
186            Height::Smoke => NetworkVersion::V2,
187            Height::Ignition => NetworkVersion::V3,
188            Height::Refuel => NetworkVersion::V3,
189            Height::Assembly => NetworkVersion::V4,
190            Height::Tape => NetworkVersion::V5,
191            Height::Liftoff => NetworkVersion::V5,
192            Height::Kumquat => NetworkVersion::V6,
193            Height::Calico => NetworkVersion::V7,
194            Height::Persian => NetworkVersion::V8,
195            Height::Orange => NetworkVersion::V9,
196            Height::Claus => NetworkVersion::V9,
197            Height::Trust => NetworkVersion::V10,
198            Height::Norwegian => NetworkVersion::V11,
199            Height::Turbo => NetworkVersion::V12,
200            Height::Hyperdrive => NetworkVersion::V13,
201            Height::Chocolate => NetworkVersion::V14,
202            Height::OhSnap => NetworkVersion::V15,
203            Height::Skyr => NetworkVersion::V16,
204            Height::Shark => NetworkVersion::V17,
205            Height::Hygge => NetworkVersion::V18,
206            Height::Lightning => NetworkVersion::V19,
207            Height::Thunder => NetworkVersion::V20,
208            Height::Watermelon => NetworkVersion::V21,
209            Height::WatermelonFix => NetworkVersion::V21,
210            Height::WatermelonFix2 => NetworkVersion::V21,
211            Height::Dragon => NetworkVersion::V22,
212            Height::DragonFix => NetworkVersion::V22,
213            Height::Phoenix => NetworkVersion::V22,
214            Height::Waffle => NetworkVersion::V23,
215            Height::TukTuk => NetworkVersion::V24,
216            Height::Teep => NetworkVersion::V25,
217            Height::Tock => NetworkVersion::V26,
218            Height::TockFix => NetworkVersion::V26,
219            Height::GoldenWeek => NetworkVersion::V27,
220            Height::FireHorse => NetworkVersion::V28,
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 default_max_fee: TokenAmount,
285}
286
287impl ChainConfig {
288    pub fn mainnet() -> Self {
289        use mainnet::*;
290        Self {
291            network: NetworkChain::Mainnet,
292            genesis_cid: Some(GENESIS_CID.to_string()),
293            bootstrap_peers: DEFAULT_BOOTSTRAP.clone(),
294            block_delay_secs: env_or_default(
295                ENV_FOREST_BLOCK_DELAY_SECS,
296                EPOCH_DURATION_SECONDS as u32,
297            ),
298            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 10),
299            genesis_network: GENESIS_NETWORK_VERSION,
300            height_infos: HEIGHT_INFOS.clone(),
301            policy: make_mainnet_policy!(v13).into(),
302            eth_chain_id: ETH_CHAIN_ID,
303            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
304            // 1 year on mainnet
305            fip0081_ramp_duration_epochs: 365 * EPOCHS_IN_DAY as u64,
306            upgrade_teep_initial_fil_reserved: None,
307            f3_enabled: true,
308            f3_consensus: true,
309            // April 29 at 10:00 UTC
310            f3_bootstrap_epoch: 4920480,
311            f3_initial_power_table: Some(
312                "bafy2bzacecklgxd2eksmodvhgurqvorkg3wamgqkrunir3al2gchv2cikgmbu"
313                    .parse()
314                    .expect("invalid f3_initial_power_table"),
315            ),
316            enable_indexer: false,
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            default_max_fee: TokenAmount::zero(),
354        }
355    }
356
357    pub fn devnet() -> Self {
358        use devnet::*;
359        Self {
360            network: NetworkChain::Devnet("devnet".to_string()),
361            genesis_cid: None,
362            bootstrap_peers: Vec::new(),
363            block_delay_secs: env_or_default(ENV_FOREST_BLOCK_DELAY_SECS, 4),
364            propagation_delay_secs: env_or_default(ENV_FOREST_PROPAGATION_DELAY_SECS, 1),
365            genesis_network: *GENESIS_NETWORK_VERSION,
366            height_infos: HEIGHT_INFOS.clone(),
367            policy: make_devnet_policy!(v13).into(),
368            eth_chain_id: ETH_CHAIN_ID,
369            breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION,
370            // Devnet ramp is 200 epochs in Lotus (subject to change).
371            fip0081_ramp_duration_epochs: env_or_default(ENV_PLEDGE_RULE_RAMP, 200),
372            // FIP-0100: 300M -> 1.4B FIL
373            upgrade_teep_initial_fil_reserved: Some(TokenAmount::from_whole(1_400_000_000)),
374            f3_enabled: false,
375            f3_consensus: false,
376            f3_bootstrap_epoch: -1,
377            f3_initial_power_table: None,
378            enable_indexer: false,
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            default_max_fee: TokenAmount::zero(),
412        }
413    }
414
415    pub fn from_chain(network_chain: &NetworkChain) -> Self {
416        match network_chain {
417            NetworkChain::Mainnet => Self::mainnet(),
418            NetworkChain::Calibnet => Self::calibnet(),
419            NetworkChain::Butterflynet => Self::butterflynet(),
420            NetworkChain::Devnet(name) => Self {
421                network: NetworkChain::Devnet(name.clone()),
422                ..Self::devnet()
423            },
424        }
425    }
426
427    fn network_height(&self, epoch: ChainEpoch) -> Option<Height> {
428        self.height_infos
429            .iter()
430            .sorted_by_key(|(_, info)| info.epoch)
431            .rev()
432            .find(|(_, info)| epoch > info.epoch)
433            .map(|(height, _)| *height)
434    }
435
436    /// Gets the latest network height prior to the given epoch that upgrades the actor bundle
437    pub fn network_height_with_actor_bundle<'a>(
438        &'a self,
439        epoch: ChainEpoch,
440    ) -> Option<HeightInfoWithActorManifest<'a>> {
441        if let Some((height, info, manifest_cid)) = self
442            .height_infos
443            .iter()
444            .sorted_by_key(|(_, info)| info.epoch)
445            .rev()
446            .filter_map(|(height, info)| info.bundle.map(|bundle| (*height, info, bundle)))
447            .find(|(_, info, _)| epoch > info.epoch)
448        {
449            Some(HeightInfoWithActorManifest {
450                height,
451                info,
452                manifest_cid,
453            })
454        } else {
455            None
456        }
457    }
458
459    /// Returns the network version at the given epoch.
460    /// If the epoch is before the first upgrade, the genesis network version is returned.
461    pub fn network_version(&self, epoch: ChainEpoch) -> NetworkVersion {
462        self.network_height(epoch)
463            .map(NetworkVersion::from)
464            .unwrap_or(self.genesis_network_version())
465            .max(self.genesis_network)
466    }
467
468    /// Returns the network version revision at the given epoch for distinguishing network upgrades
469    /// that do not bump the network version.
470    pub fn network_version_revision(&self, epoch: ChainEpoch) -> i64 {
471        if let Some(height) = self.network_height(epoch) {
472            let nv = NetworkVersion::from(height);
473            if let Some(rev0_height) = Height::iter().find(|h| NetworkVersion::from(*h) == nv) {
474                return (height as i64) - (rev0_height as i64);
475            }
476        }
477        0
478    }
479
480    pub fn get_beacon_schedule(&self, genesis_ts: u64) -> BeaconSchedule {
481        let ds_iter = match self.network {
482            NetworkChain::Mainnet => mainnet::DRAND_SCHEDULE.iter(),
483            NetworkChain::Calibnet => calibnet::DRAND_SCHEDULE.iter(),
484            NetworkChain::Butterflynet => butterflynet::DRAND_SCHEDULE.iter(),
485            NetworkChain::Devnet(_) => devnet::DRAND_SCHEDULE.iter(),
486        };
487
488        BeaconSchedule(
489            ds_iter
490                .map(|dc| {
491                    BeaconPoint::new(
492                        dc.height,
493                        DrandBeacon::new(genesis_ts, u64::from(self.block_delay_secs), dc.config),
494                    )
495                })
496                .collect(),
497        )
498    }
499
500    pub fn epoch(&self, height: Height) -> ChainEpoch {
501        self.height_infos
502            .iter()
503            .sorted_by_key(|(_, info)| info.epoch)
504            .rev()
505            .find_map(|(infos_height, info)| {
506                if *infos_height == height {
507                    Some(info.epoch)
508                } else {
509                    None
510                }
511            })
512            .unwrap_or(0)
513    }
514
515    /// Returns true if executing between `parent` and `height` (exclusive of `height`) would
516    /// cross an expensive state migration, as registered in
517    /// [`crate::state_migration::get_migrations`].
518    pub fn has_expensive_fork_between(&self, parent: ChainEpoch, height: ChainEpoch) -> bool {
519        if parent >= height {
520            return false;
521        }
522        crate::state_migration::get_migrations::<crate::db::DbImpl>(&self.network)
523            .iter()
524            .any(|(h, _)| {
525                self.height_infos
526                    .get(h)
527                    .is_some_and(|info| info.epoch >= parent && info.epoch < height)
528            })
529    }
530
531    pub async fn genesis_bytes<DB: SettingsStore>(
532        &self,
533        db: &DB,
534    ) -> anyhow::Result<Option<Vec<u8>>> {
535        Ok(match self.network {
536            NetworkChain::Mainnet => Some(mainnet::DEFAULT_GENESIS.to_vec()),
537            NetworkChain::Calibnet => Some(calibnet::DEFAULT_GENESIS.to_vec()),
538            // Butterflynet genesis is not hardcoded in the binary, for size reasons.
539            NetworkChain::Butterflynet => Some(butterflynet::fetch_genesis(db).await?),
540            NetworkChain::Devnet(_) => None,
541        })
542    }
543
544    pub fn is_testnet(&self) -> bool {
545        self.network.is_testnet()
546    }
547
548    pub fn is_devnet(&self) -> bool {
549        self.network.is_devnet()
550    }
551
552    pub fn genesis_network_version(&self) -> NetworkVersion {
553        self.genesis_network
554    }
555
556    pub fn initial_fil_reserved(&self, network_version: NetworkVersion) -> &TokenAmount {
557        match &self.upgrade_teep_initial_fil_reserved {
558            Some(fil) if network_version >= NetworkVersion::V25 => fil,
559            _ => &INITIAL_FIL_RESERVED,
560        }
561    }
562
563    pub fn initial_fil_reserved_at_height(&self, height: i64) -> &TokenAmount {
564        let network_version = self.network_version(height);
565        self.initial_fil_reserved(network_version)
566    }
567}
568
569impl Default for ChainConfig {
570    fn default() -> Self {
571        ChainConfig::mainnet()
572    }
573}
574
575pub(crate) fn parse_bootstrap_peers(bootstrap_peer_list: &str) -> Vec<Multiaddr> {
576    bootstrap_peer_list
577        .split('\n')
578        .filter(|s| !s.is_empty())
579        .map(|s| {
580            Multiaddr::from_str(s).unwrap_or_else(|e| panic!("invalid bootstrap peer {s}: {e}"))
581        })
582        .collect()
583}
584
585#[allow(dead_code)]
586fn get_upgrade_epoch_by_height<'a>(
587    mut height_infos: impl Iterator<Item = &'a (Height, HeightInfo)>,
588    height: Height,
589) -> Option<ChainEpoch> {
590    height_infos.find_map(|(infos_height, info)| {
591        if *infos_height == height {
592            Some(info.epoch)
593        } else {
594            None
595        }
596    })
597}
598
599fn get_upgrade_height_from_env(env_var_key: &str) -> Option<ChainEpoch> {
600    if let Ok(value) = std::env::var(env_var_key) {
601        if let Ok(epoch) = value.parse() {
602            return Some(epoch);
603        } else {
604            warn!("Failed to parse {env_var_key}={value}, value should be an integer");
605        }
606    }
607    None
608}
609
610#[macro_export]
611macro_rules! make_height {
612    ($id:ident,$epoch:expr) => {
613        (
614            Height::$id,
615            HeightInfo {
616                epoch: $epoch,
617                bundle: None,
618            },
619        )
620    };
621    ($id:ident,$epoch:expr,$bundle:expr) => {
622        (
623            Height::$id,
624            HeightInfo {
625                epoch: $epoch,
626                bundle: Some(Cid::try_from($bundle).unwrap()),
627            },
628        )
629    };
630}
631
632// The formula matches lotus
633// ```go
634// sinceGenesis := build.Clock.Now().Sub(genesisTime)
635// expectedHeight := int64(sinceGenesis.Seconds()) / int64(build.BlockDelaySecs)
636// ```
637// See <https://github.com/filecoin-project/lotus/blob/b27c861485695d3f5bb92bcb281abc95f4d90fb6/chain/sync.go#L180>
638pub fn calculate_expected_epoch(
639    now_timestamp: u64,
640    genesis_timestamp: u64,
641    block_delay_secs: u32,
642) -> i64 {
643    (now_timestamp.saturating_sub(genesis_timestamp) / u64::from(block_delay_secs)) as i64
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    fn heights_are_present(height_infos: &HashMap<Height, HeightInfo>) {
651        /// These are required heights that need to be defined for all networks, for, e.g., conformance
652        /// with `Filecoin.StateGetNetworkParams` RPC method.
653        const REQUIRED_HEIGHTS: [Height; 31] = [
654            Height::Breeze,
655            Height::Smoke,
656            Height::Ignition,
657            Height::Refuel,
658            Height::Assembly,
659            Height::Tape,
660            Height::Liftoff,
661            Height::Kumquat,
662            Height::Calico,
663            Height::Persian,
664            Height::Orange,
665            Height::Claus,
666            Height::Trust,
667            Height::Norwegian,
668            Height::Turbo,
669            Height::Hyperdrive,
670            Height::Chocolate,
671            Height::OhSnap,
672            Height::Skyr,
673            Height::Shark,
674            Height::Hygge,
675            Height::Lightning,
676            Height::Thunder,
677            Height::Watermelon,
678            Height::Dragon,
679            Height::Phoenix,
680            Height::Waffle,
681            Height::TukTuk,
682            Height::Teep,
683            Height::GoldenWeek,
684            Height::FireHorse,
685        ];
686
687        for height in &REQUIRED_HEIGHTS {
688            assert!(height_infos.get(height).is_some());
689        }
690    }
691
692    #[test]
693    fn test_mainnet_heights() {
694        heights_are_present(&mainnet::HEIGHT_INFOS);
695    }
696
697    #[test]
698    fn has_expensive_fork_between_matches_upgrade_epochs() {
699        let cfg = ChainConfig::mainnet();
700        let shark = cfg.epoch(Height::Shark);
701        assert!(cfg.has_expensive_fork_between(shark - 1, shark + 1));
702        assert!(!cfg.has_expensive_fork_between(shark - 1, shark));
703    }
704
705    #[test]
706    fn test_calibnet_heights() {
707        heights_are_present(&calibnet::HEIGHT_INFOS);
708    }
709
710    #[test]
711    fn test_devnet_heights() {
712        heights_are_present(&devnet::HEIGHT_INFOS);
713    }
714
715    #[test]
716    fn test_butterflynet_heights() {
717        heights_are_present(&butterflynet::HEIGHT_INFOS);
718    }
719
720    #[test]
721    fn test_get_upgrade_height_no_env_var() {
722        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_1");
723        assert_eq!(epoch, None);
724    }
725
726    #[test]
727    fn test_get_upgrade_height_valid_env_var() {
728        unsafe { std::env::set_var("FOREST_TEST_VAR_2", "10") };
729        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_2");
730        assert_eq!(epoch, Some(10));
731    }
732
733    #[test]
734    fn test_get_upgrade_height_invalid_env_var() {
735        unsafe { std::env::set_var("FOREST_TEST_VAR_3", "foo") };
736        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_3");
737        assert_eq!(epoch, None);
738    }
739
740    #[test]
741    fn test_calculate_expected_epoch() {
742        // now, genesis, block_delay
743        assert_eq!(0, calculate_expected_epoch(0, 0, 1));
744        assert_eq!(5, calculate_expected_epoch(5, 0, 1));
745
746        let mainnet_genesis = 1598306400;
747        let mainnet_block_delay = 30;
748
749        assert_eq!(
750            0,
751            calculate_expected_epoch(mainnet_genesis, mainnet_genesis, mainnet_block_delay)
752        );
753
754        assert_eq!(
755            0,
756            calculate_expected_epoch(
757                mainnet_genesis + u64::from(mainnet_block_delay) - 1,
758                mainnet_genesis,
759                mainnet_block_delay
760            )
761        );
762
763        assert_eq!(
764            1,
765            calculate_expected_epoch(
766                mainnet_genesis + u64::from(mainnet_block_delay),
767                mainnet_genesis,
768                mainnet_block_delay
769            )
770        );
771    }
772
773    #[test]
774    fn network_chain_display() {
775        assert_eq!(
776            NetworkChain::Mainnet.to_string(),
777            mainnet::NETWORK_COMMON_NAME
778        );
779        assert_eq!(
780            NetworkChain::Calibnet.to_string(),
781            calibnet::NETWORK_COMMON_NAME
782        );
783        assert_eq!(
784            NetworkChain::Butterflynet.to_string(),
785            butterflynet::NETWORK_COMMON_NAME
786        );
787        assert_eq!(
788            NetworkChain::Devnet("dummydevnet".into()).to_string(),
789            "dummydevnet"
790        );
791    }
792
793    #[test]
794    fn chain_config() {
795        ChainConfig::mainnet();
796        ChainConfig::calibnet();
797        ChainConfig::devnet();
798        ChainConfig::butterflynet();
799    }
800
801    #[test]
802    fn network_version() {
803        let cfg = ChainConfig::calibnet();
804        assert_eq!(cfg.network_version(1_013_134 - 1), NetworkVersion::V20);
805        assert_eq!(cfg.network_version(1_013_134), NetworkVersion::V20);
806        assert_eq!(cfg.network_version(1_013_134 + 1), NetworkVersion::V21);
807        assert_eq!(cfg.network_version_revision(1_013_134 + 1), 0);
808        assert_eq!(cfg.network_version(1_070_494), NetworkVersion::V21);
809        assert_eq!(cfg.network_version_revision(1_070_494), 0);
810        assert_eq!(cfg.network_version(1_070_494 + 1), NetworkVersion::V21);
811        assert_eq!(cfg.network_version_revision(1_070_494 + 1), 1);
812    }
813
814    #[test]
815    fn test_network_height_with_actor_bundle() {
816        let cfg = ChainConfig::mainnet();
817        let info = cfg.network_height_with_actor_bundle(5_348_280 + 1).unwrap();
818        assert_eq!(info.height, Height::GoldenWeek);
819        let info = cfg.network_height_with_actor_bundle(5_348_280).unwrap();
820        // No actor bundle for Tock, so it should be Teep
821        assert_eq!(info.height, Height::Teep);
822        let info = cfg.network_height_with_actor_bundle(5_348_280 - 1).unwrap();
823        assert_eq!(info.height, Height::Teep);
824        assert!(cfg.network_height_with_actor_bundle(1).is_none());
825        assert!(cfg.network_height_with_actor_bundle(0).is_none());
826    }
827}