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