diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecc6d11bc8a6..4e8a894670c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,7 +30,10 @@
### Added
- [#6057](https://github.com/ChainSafe/forest/issues/6057) Added `--no-progress-timeout` to `forest-cli f3 ready` subcommand to exit when F3 is stuck for the given timeout.
-- [#6000](https://github.com/ChainSafe/forest/pull/6000) Add support for the `Filecoin.StateDecodeParams` API methods to enable decoding actors method params.
+
+- [#6000](https://github.com/ChainSafe/forest/pull/6000) Added support for the `Filecoin.StateDecodeParams` API methods to enable decoding actors method params.
+
+- [#6079](https://github.com/ChainSafe/forest/pull/6079) Added prometheus metrics `network_version`, `network_version_revision` and `actor_version`.
- [#6068](https://github.com/ChainSafe/forest/issues/6068) Added `--index-backfill-epochs` to `forest-tool api serve`.
diff --git a/docs/docs/users/reference/metrics.md b/docs/docs/users/reference/metrics.md
index 292e1f7cb114..9ad3cb27afb1 100644
--- a/docs/docs/users/reference/metrics.md
+++ b/docs/docs/users/reference/metrics.md
@@ -17,6 +17,9 @@ title: Metrics
| `full_peers` | Gauge | Count | Number of healthy peers recognized by the node |
| `bad_peers` | Gauge | Count | Number of bad peers recognized by the node |
| `expected_network_height` | Gauge | Count | The expected network height based on the current time and the genesis block time |
+| `network_version` | Gauge | Count | Network version of the current chain head |
+| `network_version_revision` | Gauge | Count | Network version revision of the current chain head |
+| `actor_version` | Gauge | Count | Actor version of the current chain head |
| `forest_db_size` | Gauge | Bytes | Size of Forest database in bytes |
| `bitswap_message_count` | Counter | Count | Number of `bitswap` messages. Indexed by `type` |
| `bitswap_container_capacities` | Gauge | Count | Capacity for each `bitswap` container. Indexed by `type` |
@@ -288,6 +291,33 @@ expected_network_height 2519530
```
+
+ Example `network_version` output
+```
+# HELP network_version Network version of the current chain head
+# TYPE network_version gauge
+network_version 27
+```
+
+
+
+ Example `network_version_revision` output
+```
+# HELP network_version_revision Network version revision of the current chain head
+# TYPE network_version_revision gauge
+network_version_revision 0
+```
+
+
+
+ Example `actor_version` output
+```
+# HELP actor_version Actor version of the current chain head
+# TYPE actor_version gauge
+actor_version 17
+```
+
+
Example `build_info` output
```
diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs
index e28b7a8df907..14c718c1b7b4 100644
--- a/src/chain/store/chain_store.rs
+++ b/src/chain/store/chain_store.rs
@@ -6,6 +6,7 @@ use super::{
index::{ChainIndex, ResolveNullTipset},
tipset_tracker::TipsetTracker,
};
+use crate::db::{EthMappingsStore, EthMappingsStoreExt, IndicesStore, IndicesStoreExt};
use crate::interpreter::{BlockMessages, VMTrace};
use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite};
use crate::message::{ChainMessage, Message as MessageTrait, SignedMessage};
@@ -22,10 +23,6 @@ use crate::{
blocks::{CachingBlockHeader, Tipset, TipsetKey, TxMeta},
db::HeaviestTipsetKeyProvider,
};
-use crate::{
- chain_sync::metrics,
- db::{EthMappingsStore, EthMappingsStoreExt, IndicesStore, IndicesStoreExt},
-};
use crate::{fil_cns, utils::cache::SizeTrackingLruCache};
use ahash::{HashMap, HashMapExt, HashSet};
use anyhow::Context as _;
@@ -146,7 +143,6 @@ where
/// Sets heaviest tipset
pub fn set_heaviest_tipset(&self, ts: Arc) -> Result<(), Error> {
- metrics::HEAD_EPOCH.set(ts.epoch());
self.heaviest_tipset_key_provider
.set_heaviest_tipset_key(ts.key())?;
if self.publisher.send(HeadChange::Apply(ts)).is_err() {
diff --git a/src/chain_sync/metrics.rs b/src/chain_sync/metrics.rs
index 95efe617cd74..e620a92d2185 100644
--- a/src/chain_sync/metrics.rs
+++ b/src/chain_sync/metrics.rs
@@ -3,7 +3,7 @@
use prometheus_client::{
encoding::{EncodeLabelKey, EncodeLabelSet, EncodeLabelValue, LabelSetEncoder},
- metrics::{counter::Counter, family::Family, gauge::Gauge, histogram::Histogram},
+ metrics::{counter::Counter, family::Family, histogram::Histogram},
};
use std::sync::LazyLock;
@@ -44,15 +44,6 @@ pub static INVALID_TIPSET_TOTAL: LazyLock = LazyLock::new(|| {
);
metric
});
-pub static HEAD_EPOCH: LazyLock = LazyLock::new(|| {
- let metric = Gauge::default();
- crate::metrics::default_registry().register(
- "head_epoch",
- "Latest epoch synchronized to the node",
- metric.clone(),
- );
- metric
-});
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Libp2pMessageKindLabel(&'static str);
diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs
index a9f740bffd68..a0d611ce47b5 100644
--- a/src/daemon/mod.rs
+++ b/src/daemon/mod.rs
@@ -15,8 +15,10 @@ use crate::cli_shared::{
chain_path,
cli::{CliOpts, Config},
};
-use crate::daemon::context::{AppContext, DbType};
-use crate::daemon::db_util::import_chain_as_forest_car;
+use crate::daemon::{
+ context::{AppContext, DbType},
+ db_util::import_chain_as_forest_car,
+};
use crate::db::gc::SnapshotGarbageCollector;
use crate::db::ttl::EthMappingCollector;
use crate::libp2p::{Libp2pService, PeerManager};
@@ -26,6 +28,7 @@ use crate::rpc::RPCState;
use crate::rpc::eth::filter::EthEventHandler;
use crate::rpc::start_rpc;
use crate::shim::clock::ChainEpoch;
+use crate::shim::state_tree::StateTree;
use crate::shim::version::NetworkVersion;
use crate::utils;
use crate::utils::{proofs_api::ensure_proof_params_downloaded, version::FOREST_VERSION_STRING};
@@ -202,10 +205,47 @@ async fn maybe_start_metrics_service(
);
let db_directory = crate::db::db_engine::db_root(&chain_path(config))?;
let db = ctx.db.writer().clone();
- services.spawn(async {
- crate::metrics::init_prometheus(prometheus_listener, db_directory, db)
+
+ let get_chain_head_height = Arc::new({
+ // Use `Weak` to not dead lock GC.
+ let chain_store = Arc::downgrade(ctx.state_manager.chain_store());
+ move || {
+ chain_store
+ .upgrade()
+ .map(|cs| cs.heaviest_tipset().epoch())
+ .unwrap_or_default()
+ }
+ });
+ let get_chain_head_actor_version = Arc::new({
+ // Use `Weak` to not dead lock GC.
+ let chain_store = Arc::downgrade(ctx.state_manager.chain_store());
+ move || {
+ if let Some(cs) = chain_store.upgrade()
+ && let Ok(state) =
+ StateTree::new_from_root(cs.db.clone(), cs.heaviest_tipset().parent_state())
+ && let Ok(bundle_meta) = state.get_actor_bundle_metadata()
+ && let Ok(actor_version) = bundle_meta.actor_major_version()
+ {
+ return actor_version;
+ }
+ 0
+ }
+ });
+ services.spawn({
+ let chain_config = ctx.chain_config().clone();
+ let get_chain_head_height = get_chain_head_height.clone();
+ async {
+ crate::metrics::init_prometheus(
+ prometheus_listener,
+ db_directory,
+ db,
+ chain_config,
+ get_chain_head_height,
+ get_chain_head_actor_version,
+ )
.await
.context("Failed to initiate prometheus server")
+ }
});
crate::metrics::register_collector(Box::new(
@@ -215,6 +255,7 @@ async fn maybe_start_metrics_service(
.chain_store()
.genesis_block_header()
.timestamp,
+ get_chain_head_height,
),
));
}
diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs
index f2862265e7ad..f9be6aae249f 100644
--- a/src/metrics/mod.rs
+++ b/src/metrics/mod.rs
@@ -3,7 +3,7 @@
pub mod db;
-use crate::db::DBStatistics;
+use crate::{db::DBStatistics, networks::ChainConfig, shim::clock::ChainEpoch};
use axum::{Router, http::StatusCode, response::IntoResponse, routing::get};
use parking_lot::{RwLock, RwLockWriteGuard};
use prometheus_client::{
@@ -86,6 +86,9 @@ pub async fn init_prometheus(
prometheus_listener: TcpListener,
db_directory: PathBuf,
db: Arc,
+ chain_config: Arc,
+ get_chain_head_height: Arc ChainEpoch + Send + Sync + 'static>,
+ get_chain_head_actor_version: Arc u64 + Send + Sync + 'static>,
) -> anyhow::Result<()>
where
DB: DBStatistics + Send + Sync + 'static,
@@ -101,6 +104,13 @@ where
crate::utils::version::ForestVersionCollector::new(),
));
register_collector(Box::new(crate::metrics::db::DBCollector::new(db_directory)));
+ register_collector(Box::new(
+ crate::networks::metrics::NetworkVersionCollector::new(
+ chain_config,
+ get_chain_head_height,
+ get_chain_head_actor_version,
+ ),
+ ));
// Create an configure HTTP server
let app = Router::new()
diff --git a/src/networks/metrics.rs b/src/networks/metrics.rs
index 058d899ac83b..3eb62693e382 100644
--- a/src/networks/metrics.rs
+++ b/src/networks/metrics.rs
@@ -1,47 +1,162 @@
// Copyright 2019-2025 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT
-use prometheus_client::{collector::Collector, encoding::EncodeMetric, metrics::gauge::Gauge};
+use std::sync::Arc;
+
+use educe::Educe;
+use prometheus_client::{
+ collector::Collector,
+ encoding::{DescriptorEncoder, EncodeMetric},
+ metrics::gauge::Gauge,
+};
use super::calculate_expected_epoch;
+use crate::{networks::ChainConfig, shim::clock::ChainEpoch};
-#[derive(Debug)]
-pub struct NetworkHeightCollector {
+#[derive(Educe)]
+#[educe(Debug)]
+pub struct NetworkHeightCollector
+where
+ F: Fn() -> ChainEpoch,
+{
block_delay_secs: u32,
genesis_timestamp: u64,
- network_height: Gauge,
+ #[educe(Debug(ignore))]
+ get_chain_head_height: Arc,
}
-impl NetworkHeightCollector {
- pub fn new(block_delay_secs: u32, genesis_timestamp: u64) -> Self {
+impl NetworkHeightCollector
+where
+ F: Fn() -> ChainEpoch,
+{
+ pub fn new(
+ block_delay_secs: u32,
+ genesis_timestamp: u64,
+ get_chain_head_height: Arc,
+ ) -> Self {
Self {
block_delay_secs,
genesis_timestamp,
- network_height: Gauge::default(),
+ get_chain_head_height,
}
}
}
-impl Collector for NetworkHeightCollector {
+impl Collector for NetworkHeightCollector
+where
+ F: Fn() -> ChainEpoch + Send + Sync + 'static,
+{
fn encode(
&self,
mut encoder: prometheus_client::encoding::DescriptorEncoder,
) -> Result<(), std::fmt::Error> {
- let metric_encoder = encoder.encode_descriptor(
- "expected_network_height",
- "The expected network height based on the current time and the genesis block time",
- None,
- self.network_height.metric_type(),
- )?;
-
- let expected_epoch = calculate_expected_epoch(
- chrono::Utc::now().timestamp() as u64,
- self.genesis_timestamp,
- self.block_delay_secs,
- );
- self.network_height.set(expected_epoch);
- self.network_height.encode(metric_encoder)?;
+ {
+ let network_height: Gauge = Default::default();
+ let epoch = (self.get_chain_head_height)();
+ network_height.set(epoch);
+ let metric_encoder = encoder.encode_descriptor(
+ "head_epoch",
+ "Latest epoch synchronized to the node",
+ None,
+ network_height.metric_type(),
+ )?;
+ network_height.encode(metric_encoder)?;
+ }
+ {
+ let expected_network_height: Gauge = Default::default();
+ let expected_epoch = calculate_expected_epoch(
+ chrono::Utc::now().timestamp() as u64,
+ self.genesis_timestamp,
+ self.block_delay_secs,
+ );
+ expected_network_height.set(expected_epoch);
+ let metric_encoder = encoder.encode_descriptor(
+ "expected_network_height",
+ "The expected network height based on the current time and the genesis block time",
+ None,
+ expected_network_height.metric_type(),
+ )?;
+ expected_network_height.encode(metric_encoder)?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Educe)]
+#[educe(Debug)]
+pub struct NetworkVersionCollector
+where
+ F1: Fn() -> ChainEpoch,
+ F2: Fn() -> u64,
+{
+ chain_config: Arc,
+ #[educe(Debug(ignore))]
+ get_chain_head_height: Arc,
+ #[educe(Debug(ignore))]
+ get_chain_head_actor_version: Arc,
+}
+impl NetworkVersionCollector
+where
+ F1: Fn() -> ChainEpoch,
+ F2: Fn() -> u64,
+{
+ pub fn new(
+ chain_config: Arc,
+ get_chain_head_height: Arc,
+ get_chain_head_actor_version: Arc,
+ ) -> Self {
+ Self {
+ chain_config,
+ get_chain_head_height,
+ get_chain_head_actor_version,
+ }
+ }
+}
+
+impl Collector for NetworkVersionCollector
+where
+ F1: Fn() -> ChainEpoch + Send + Sync + 'static,
+ F2: Fn() -> u64 + Send + Sync + 'static,
+{
+ fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
+ let epoch = (self.get_chain_head_height)();
+ {
+ let network_version = self.chain_config.network_version(epoch);
+ let nv_gauge: Gauge = Default::default();
+ nv_gauge.set(u32::from(network_version) as _);
+ let metric_encoder = encoder.encode_descriptor(
+ "network_version",
+ "Network version of the current chain head",
+ None,
+ nv_gauge.metric_type(),
+ )?;
+ nv_gauge.encode(metric_encoder)?;
+ }
+ {
+ let network_version_revision = self.chain_config.network_version_revision(epoch);
+ let nv_gauge: Gauge = Default::default();
+ nv_gauge.set(network_version_revision);
+ let metric_encoder = encoder.encode_descriptor(
+ "network_version_revision",
+ "Network version revision of the current chain head",
+ None,
+ nv_gauge.metric_type(),
+ )?;
+ nv_gauge.encode(metric_encoder)?;
+ }
+ {
+ let actor_version = (self.get_chain_head_actor_version)();
+ let av_gauge: Gauge = Default::default();
+ av_gauge.set(actor_version as _);
+ let metric_encoder = encoder.encode_descriptor(
+ "actor_version",
+ "Actor version of the current chain head",
+ None,
+ av_gauge.metric_type(),
+ )?;
+ av_gauge.encode(metric_encoder)?;
+ }
Ok(())
}
}
diff --git a/src/networks/mod.rs b/src/networks/mod.rs
index d067c8157372..fc3723107de1 100644
--- a/src/networks/mod.rs
+++ b/src/networks/mod.rs
@@ -10,7 +10,9 @@ use fil_actors_shared::v13::runtime::Policy;
use itertools::Itertools;
use libp2p::Multiaddr;
use serde::{Deserialize, Serialize};
+use strum::IntoEnumIterator;
use strum_macros::Display;
+use strum_macros::EnumIter;
use tracing::warn;
use crate::beacon::{BeaconPoint, BeaconSchedule, DrandBeacon, DrandConfig};
@@ -27,8 +29,8 @@ pub use network_name::{GenesisNetworkName, StateNetworkName};
mod actors_bundle;
pub use actors_bundle::{
- ACTOR_BUNDLES, ACTOR_BUNDLES_METADATA, ActorBundleInfo, generate_actor_bundle,
- get_actor_bundles_metadata,
+ ACTOR_BUNDLES, ACTOR_BUNDLES_METADATA, ActorBundleInfo, ActorBundleMetadata,
+ generate_actor_bundle, get_actor_bundles_metadata,
};
mod drand;
@@ -134,7 +136,7 @@ impl NetworkChain {
}
/// Defines the meaningful heights of the protocol.
-#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
+#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
pub enum Height {
Breeze,
@@ -411,19 +413,36 @@ impl ChainConfig {
}
}
- /// Returns the network version at the given epoch.
- /// If the epoch is before the first upgrade, the genesis network version is returned.
- pub fn network_version(&self, epoch: ChainEpoch) -> NetworkVersion {
+ fn network_height(&self, epoch: ChainEpoch) -> Option {
self.height_infos
.iter()
.sorted_by_key(|(_, info)| info.epoch)
.rev()
.find(|(_, info)| epoch > info.epoch)
- .map(|(height, _)| NetworkVersion::from(*height))
+ .map(|(height, _)| *height)
+ }
+
+ /// Returns the network version at the given epoch.
+ /// If the epoch is before the first upgrade, the genesis network version is returned.
+ pub fn network_version(&self, epoch: ChainEpoch) -> NetworkVersion {
+ self.network_height(epoch)
+ .map(NetworkVersion::from)
.unwrap_or(self.genesis_network_version())
.max(self.genesis_network)
}
+ /// Returns the network version revision at the given epoch for distinguishing network upgrades
+ /// that do not bump the network version.
+ pub fn network_version_revision(&self, epoch: ChainEpoch) -> i64 {
+ if let Some(height) = self.network_height(epoch) {
+ let nv = NetworkVersion::from(height);
+ if let Some(rev0_height) = Height::iter().find(|h| NetworkVersion::from(*h) == nv) {
+ return (height as i64) - (rev0_height as i64);
+ }
+ }
+ 0
+ }
+
pub fn get_beacon_schedule(&self, genesis_ts: u64) -> BeaconSchedule {
let ds_iter = match self.network {
NetworkChain::Mainnet => mainnet::DRAND_SCHEDULE.iter(),
@@ -721,4 +740,17 @@ mod tests {
ChainConfig::devnet();
ChainConfig::butterflynet();
}
+
+ #[test]
+ fn network_version() {
+ let cfg = ChainConfig::calibnet();
+ assert_eq!(cfg.network_version(1_013_134 - 1), NetworkVersion::V20);
+ assert_eq!(cfg.network_version(1_013_134), NetworkVersion::V20);
+ assert_eq!(cfg.network_version(1_013_134 + 1), NetworkVersion::V21);
+ assert_eq!(cfg.network_version_revision(1_013_134 + 1), 0);
+ assert_eq!(cfg.network_version(1_070_494), NetworkVersion::V21);
+ assert_eq!(cfg.network_version_revision(1_070_494), 0);
+ assert_eq!(cfg.network_version(1_070_494 + 1), NetworkVersion::V21);
+ assert_eq!(cfg.network_version_revision(1_070_494 + 1), 1);
+ }
}
diff --git a/src/shim/state_tree.rs b/src/shim/state_tree.rs
index a86b3fcb96c8..da5910cde9f7 100644
--- a/src/shim/state_tree.rs
+++ b/src/shim/state_tree.rs
@@ -5,7 +5,10 @@ use std::{
sync::Arc,
};
-use crate::shim::actors::account;
+use crate::{
+ networks::{ACTOR_BUNDLES_METADATA, ActorBundleMetadata},
+ shim::actors::account,
+};
use anyhow::{Context as _, anyhow, bail};
use cid::Cid;
use fvm_ipld_blockstore::Blockstore;
@@ -194,6 +197,15 @@ where
.with_context(|| format!("Actor not found: addr={addr}"))
}
+ /// Get the actor bundle metadata
+ pub fn get_actor_bundle_metadata(&self) -> anyhow::Result<&ActorBundleMetadata> {
+ let system_actor_code = self.get_required_actor(&Address::SYSTEM_ACTOR)?.code;
+ ACTOR_BUNDLES_METADATA
+ .values()
+ .find(|v| v.manifest.get_system() == system_actor_code)
+ .with_context(|| format!("actor bundle not found for system actor {system_actor_code}"))
+ }
+
/// Get actor state from an address. Will be resolved to ID address.
pub fn get_actor(&self, addr: &Address) -> anyhow::Result