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