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