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    pub async fn genesis_bytes<DB: SettingsStore>(
516        &self,
517        db: &DB,
518    ) -> anyhow::Result<Option<Vec<u8>>> {
519        Ok(match self.network {
520            NetworkChain::Mainnet => Some(mainnet::DEFAULT_GENESIS.to_vec()),
521            NetworkChain::Calibnet => Some(calibnet::DEFAULT_GENESIS.to_vec()),
522            // Butterflynet genesis is not hardcoded in the binary, for size reasons.
523            NetworkChain::Butterflynet => Some(butterflynet::fetch_genesis(db).await?),
524            NetworkChain::Devnet(_) => None,
525        })
526    }
527
528    pub fn is_testnet(&self) -> bool {
529        self.network.is_testnet()
530    }
531
532    pub fn is_devnet(&self) -> bool {
533        self.network.is_devnet()
534    }
535
536    pub fn genesis_network_version(&self) -> NetworkVersion {
537        self.genesis_network
538    }
539
540    pub fn initial_fil_reserved(&self, network_version: NetworkVersion) -> &TokenAmount {
541        match &self.upgrade_teep_initial_fil_reserved {
542            Some(fil) if network_version >= NetworkVersion::V25 => fil,
543            _ => &INITIAL_FIL_RESERVED,
544        }
545    }
546
547    pub fn initial_fil_reserved_at_height(&self, height: i64) -> &TokenAmount {
548        let network_version = self.network_version(height);
549        self.initial_fil_reserved(network_version)
550    }
551}
552
553impl Default for ChainConfig {
554    fn default() -> Self {
555        ChainConfig::mainnet()
556    }
557}
558
559pub(crate) fn parse_bootstrap_peers(bootstrap_peer_list: &str) -> Vec<Multiaddr> {
560    bootstrap_peer_list
561        .split('\n')
562        .filter(|s| !s.is_empty())
563        .map(|s| {
564            Multiaddr::from_str(s).unwrap_or_else(|e| panic!("invalid bootstrap peer {s}: {e}"))
565        })
566        .collect()
567}
568
569#[allow(dead_code)]
570fn get_upgrade_epoch_by_height<'a>(
571    mut height_infos: impl Iterator<Item = &'a (Height, HeightInfo)>,
572    height: Height,
573) -> Option<ChainEpoch> {
574    height_infos.find_map(|(infos_height, info)| {
575        if *infos_height == height {
576            Some(info.epoch)
577        } else {
578            None
579        }
580    })
581}
582
583fn get_upgrade_height_from_env(env_var_key: &str) -> Option<ChainEpoch> {
584    if let Ok(value) = std::env::var(env_var_key) {
585        if let Ok(epoch) = value.parse() {
586            return Some(epoch);
587        } else {
588            warn!("Failed to parse {env_var_key}={value}, value should be an integer");
589        }
590    }
591    None
592}
593
594#[macro_export]
595macro_rules! make_height {
596    ($id:ident,$epoch:expr) => {
597        (
598            Height::$id,
599            HeightInfo {
600                epoch: $epoch,
601                bundle: None,
602            },
603        )
604    };
605    ($id:ident,$epoch:expr,$bundle:expr) => {
606        (
607            Height::$id,
608            HeightInfo {
609                epoch: $epoch,
610                bundle: Some(Cid::try_from($bundle).unwrap()),
611            },
612        )
613    };
614}
615
616// The formula matches lotus
617// ```go
618// sinceGenesis := build.Clock.Now().Sub(genesisTime)
619// expectedHeight := int64(sinceGenesis.Seconds()) / int64(build.BlockDelaySecs)
620// ```
621// See <https://github.com/filecoin-project/lotus/blob/b27c861485695d3f5bb92bcb281abc95f4d90fb6/chain/sync.go#L180>
622pub fn calculate_expected_epoch(
623    now_timestamp: u64,
624    genesis_timestamp: u64,
625    block_delay_secs: u32,
626) -> i64 {
627    (now_timestamp.saturating_sub(genesis_timestamp) / u64::from(block_delay_secs)) as i64
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    fn heights_are_present(height_infos: &HashMap<Height, HeightInfo>) {
635        /// These are required heights that need to be defined for all networks, for, e.g., conformance
636        /// with `Filecoin.StateGetNetworkParams` RPC method.
637        const REQUIRED_HEIGHTS: [Height; 31] = [
638            Height::Breeze,
639            Height::Smoke,
640            Height::Ignition,
641            Height::Refuel,
642            Height::Assembly,
643            Height::Tape,
644            Height::Liftoff,
645            Height::Kumquat,
646            Height::Calico,
647            Height::Persian,
648            Height::Orange,
649            Height::Claus,
650            Height::Trust,
651            Height::Norwegian,
652            Height::Turbo,
653            Height::Hyperdrive,
654            Height::Chocolate,
655            Height::OhSnap,
656            Height::Skyr,
657            Height::Shark,
658            Height::Hygge,
659            Height::Lightning,
660            Height::Thunder,
661            Height::Watermelon,
662            Height::Dragon,
663            Height::Phoenix,
664            Height::Waffle,
665            Height::TukTuk,
666            Height::Teep,
667            Height::GoldenWeek,
668            Height::FireHorse,
669        ];
670
671        for height in &REQUIRED_HEIGHTS {
672            assert!(height_infos.get(height).is_some());
673        }
674    }
675
676    #[test]
677    fn test_mainnet_heights() {
678        heights_are_present(&mainnet::HEIGHT_INFOS);
679    }
680
681    #[test]
682    fn test_calibnet_heights() {
683        heights_are_present(&calibnet::HEIGHT_INFOS);
684    }
685
686    #[test]
687    fn test_devnet_heights() {
688        heights_are_present(&devnet::HEIGHT_INFOS);
689    }
690
691    #[test]
692    fn test_butterflynet_heights() {
693        heights_are_present(&butterflynet::HEIGHT_INFOS);
694    }
695
696    #[test]
697    fn test_get_upgrade_height_no_env_var() {
698        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_1");
699        assert_eq!(epoch, None);
700    }
701
702    #[test]
703    fn test_get_upgrade_height_valid_env_var() {
704        unsafe { std::env::set_var("FOREST_TEST_VAR_2", "10") };
705        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_2");
706        assert_eq!(epoch, Some(10));
707    }
708
709    #[test]
710    fn test_get_upgrade_height_invalid_env_var() {
711        unsafe { std::env::set_var("FOREST_TEST_VAR_3", "foo") };
712        let epoch = get_upgrade_height_from_env("FOREST_TEST_VAR_3");
713        assert_eq!(epoch, None);
714    }
715
716    #[test]
717    fn test_calculate_expected_epoch() {
718        // now, genesis, block_delay
719        assert_eq!(0, calculate_expected_epoch(0, 0, 1));
720        assert_eq!(5, calculate_expected_epoch(5, 0, 1));
721
722        let mainnet_genesis = 1598306400;
723        let mainnet_block_delay = 30;
724
725        assert_eq!(
726            0,
727            calculate_expected_epoch(mainnet_genesis, mainnet_genesis, mainnet_block_delay)
728        );
729
730        assert_eq!(
731            0,
732            calculate_expected_epoch(
733                mainnet_genesis + u64::from(mainnet_block_delay) - 1,
734                mainnet_genesis,
735                mainnet_block_delay
736            )
737        );
738
739        assert_eq!(
740            1,
741            calculate_expected_epoch(
742                mainnet_genesis + u64::from(mainnet_block_delay),
743                mainnet_genesis,
744                mainnet_block_delay
745            )
746        );
747    }
748
749    #[test]
750    fn network_chain_display() {
751        assert_eq!(
752            NetworkChain::Mainnet.to_string(),
753            mainnet::NETWORK_COMMON_NAME
754        );
755        assert_eq!(
756            NetworkChain::Calibnet.to_string(),
757            calibnet::NETWORK_COMMON_NAME
758        );
759        assert_eq!(
760            NetworkChain::Butterflynet.to_string(),
761            butterflynet::NETWORK_COMMON_NAME
762        );
763        assert_eq!(
764            NetworkChain::Devnet("dummydevnet".into()).to_string(),
765            "dummydevnet"
766        );
767    }
768
769    #[test]
770    fn chain_config() {
771        ChainConfig::mainnet();
772        ChainConfig::calibnet();
773        ChainConfig::devnet();
774        ChainConfig::butterflynet();
775    }
776
777    #[test]
778    fn network_version() {
779        let cfg = ChainConfig::calibnet();
780        assert_eq!(cfg.network_version(1_013_134 - 1), NetworkVersion::V20);
781        assert_eq!(cfg.network_version(1_013_134), NetworkVersion::V20);
782        assert_eq!(cfg.network_version(1_013_134 + 1), NetworkVersion::V21);
783        assert_eq!(cfg.network_version_revision(1_013_134 + 1), 0);
784        assert_eq!(cfg.network_version(1_070_494), NetworkVersion::V21);
785        assert_eq!(cfg.network_version_revision(1_070_494), 0);
786        assert_eq!(cfg.network_version(1_070_494 + 1), NetworkVersion::V21);
787        assert_eq!(cfg.network_version_revision(1_070_494 + 1), 1);
788    }
789
790    #[test]
791    fn test_network_height_with_actor_bundle() {
792        let cfg = ChainConfig::mainnet();
793        let info = cfg.network_height_with_actor_bundle(5_348_280 + 1).unwrap();
794        assert_eq!(info.height, Height::GoldenWeek);
795        let info = cfg.network_height_with_actor_bundle(5_348_280).unwrap();
796        // No actor bundle for Tock, so it should be Teep
797        assert_eq!(info.height, Height::Teep);
798        let info = cfg.network_height_with_actor_bundle(5_348_280 - 1).unwrap();
799        assert_eq!(info.height, Height::Teep);
800        assert!(cfg.network_height_with_actor_bundle(1).is_none());
801        assert!(cfg.network_height_with_actor_bundle(0).is_none());
802    }
803}