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: 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; 30] = [
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        ];
676
677        for height in &REQUIRED_HEIGHTS {
678            assert!(height_infos.get(height).is_some());
679        }
680    }
681
682    #[test]
683    fn test_mainnet_heights() {
684        heights_are_present(&mainnet::HEIGHT_INFOS);
685    }
686
687    #[test]
688    fn test_calibnet_heights() {
689        heights_are_present(&calibnet::HEIGHT_INFOS);
690    }
691
692    #[test]
693    fn test_devnet_heights() {
694        heights_are_present(&devnet::HEIGHT_INFOS);
695    }
696
697    #[test]
698    fn test_butterflynet_heights() {
699        heights_are_present(&butterflynet::HEIGHT_INFOS);
700    }
701
702    #[test]
703    fn test_get_upgrade_height_no_env_var() {
704        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_1");
705        assert_eq!(epoch, None);
706    }
707
708    #[test]
709    fn test_get_upgrade_height_valid_env_var() {
710        unsafe { std::env::set_var("FOREST_TEST_VAR_2", "10") };
711        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_2");
712        assert_eq!(epoch, Some(10));
713    }
714
715    #[test]
716    fn test_get_upgrade_height_invalid_env_var() {
717        unsafe { std::env::set_var("FOREST_TEST_VAR_3", "foo") };
718        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_3");
719        assert_eq!(epoch, None);
720    }
721
722    #[test]
723    fn test_calculate_expected_epoch() {
724        // now, genesis, block_delay
725        assert_eq!(0, calculate_expected_epoch(0, 0, 1));
726        assert_eq!(5, calculate_expected_epoch(5, 0, 1));
727
728        let mainnet_genesis = 1598306400;
729        let mainnet_block_delay = 30;
730
731        assert_eq!(
732            0,
733            calculate_expected_epoch(mainnet_genesis, mainnet_genesis, mainnet_block_delay)
734        );
735
736        assert_eq!(
737            0,
738            calculate_expected_epoch(
739                mainnet_genesis + mainnet_block_delay as u64 - 1,
740                mainnet_genesis,
741                mainnet_block_delay
742            )
743        );
744
745        assert_eq!(
746            1,
747            calculate_expected_epoch(
748                mainnet_genesis + mainnet_block_delay as u64,
749                mainnet_genesis,
750                mainnet_block_delay
751            )
752        );
753    }
754
755    #[test]
756    fn network_chain_display() {
757        assert_eq!(
758            NetworkChain::Mainnet.to_string(),
759            mainnet::NETWORK_COMMON_NAME
760        );
761        assert_eq!(
762            NetworkChain::Calibnet.to_string(),
763            calibnet::NETWORK_COMMON_NAME
764        );
765        assert_eq!(
766            NetworkChain::Butterflynet.to_string(),
767            butterflynet::NETWORK_COMMON_NAME
768        );
769        assert_eq!(
770            NetworkChain::Devnet("dummydevnet".into()).to_string(),
771            "dummydevnet"
772        );
773    }
774
775    #[test]
776    fn chain_config() {
777        ChainConfig::mainnet();
778        ChainConfig::calibnet();
779        ChainConfig::devnet();
780        ChainConfig::butterflynet();
781    }
782
783    #[test]
784    fn network_version() {
785        let cfg = ChainConfig::calibnet();
786        assert_eq!(cfg.network_version(1_013_134 - 1), NetworkVersion::V20);
787        assert_eq!(cfg.network_version(1_013_134), NetworkVersion::V20);
788        assert_eq!(cfg.network_version(1_013_134 + 1), NetworkVersion::V21);
789        assert_eq!(cfg.network_version_revision(1_013_134 + 1), 0);
790        assert_eq!(cfg.network_version(1_070_494), NetworkVersion::V21);
791        assert_eq!(cfg.network_version_revision(1_070_494), 0);
792        assert_eq!(cfg.network_version(1_070_494 + 1), NetworkVersion::V21);
793        assert_eq!(cfg.network_version_revision(1_070_494 + 1), 1);
794    }
795
796    #[test]
797    fn test_network_height_with_actor_bundle() {
798        let cfg = ChainConfig::mainnet();
799        let info = cfg.network_height_with_actor_bundle(5_348_280 + 1).unwrap();
800        assert_eq!(info.height, Height::GoldenWeek);
801        let info = cfg.network_height_with_actor_bundle(5_348_280).unwrap();
802        // No actor bundle for Tock, so it should be Teep
803        assert_eq!(info.height, Height::Teep);
804        let info = cfg.network_height_with_actor_bundle(5_348_280 - 1).unwrap();
805        assert_eq!(info.height, Height::Teep);
806        assert!(cfg.network_height_with_actor_bundle(1).is_none());
807        assert!(cfg.network_height_with_actor_bundle(0).is_none());
808    }
809}