Skip to main content

forest/cli/subcommands/chain_cmd/
list.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::num::NonZeroUsize;
5
6use anyhow::Context as _;
7use itertools::Itertools;
8use nonzero_ext::nonzero;
9use num::{BigInt, Zero as _};
10
11use crate::{
12    rpc::{
13        self, RpcMethodExt as _,
14        chain::{
15            ChainGetBlockMessages, ChainGetParentMessages, ChainGetParentReceipts, ChainGetTipSet,
16            ChainGetTipSetByHeight, ChainHead,
17        },
18    },
19    shim::econ::{BLOCK_GAS_LIMIT, TokenAmount},
20};
21
22/// View a segment of the chain
23#[derive(Debug, clap::Args)]
24pub struct ChainListCommand {
25    /// Start epoch (default: current head)
26    #[arg(long)]
27    epoch: Option<u64>,
28    /// Number of tipsets
29    #[arg(long, default_value_t = nonzero!(30usize))]
30    count: NonZeroUsize,
31    #[arg(long)]
32    /// View gas statistics for the chain
33    gas_stats: bool,
34}
35
36impl ChainListCommand {
37    pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
38        let count = self.count.into();
39        let mut ts = if let Some(epoch) = self.epoch {
40            ChainGetTipSetByHeight::call(&client, (epoch as _, None.into())).await?
41        } else {
42            ChainHead::call(&client, ()).await?
43        };
44        let mut tipsets = Vec::with_capacity(count);
45        loop {
46            tipsets.push(ts.clone());
47            if ts.epoch() == 0 || tipsets.len() >= count {
48                break;
49            }
50            ts = ChainGetTipSet::call(&client, (ts.parents().into(),)).await?;
51        }
52        tipsets.reverse();
53
54        for (i, ts) in tipsets.iter().enumerate() {
55            if self.gas_stats {
56                let base_fee = &ts.block_headers().first().parent_base_fee;
57                let max_fee = base_fee * BLOCK_GAS_LIMIT;
58                println!(
59                    "{height}: {n} blocks (baseFee: {base_fee} -> maxFee: {max_fee} FIL)",
60                    height = ts.epoch(),
61                    n = ts.len()
62                );
63                for b in ts.block_headers() {
64                    let msgs = ChainGetBlockMessages::call(&client, (*b.cid(),)).await?;
65                    let len = msgs.bls_msg.len() + msgs.secp_msg.len();
66                    let mut limit_sum = 0;
67                    let mut premium_sum = TokenAmount::zero();
68                    let mut premium_avg = BigInt::zero();
69                    for m in &msgs.bls_msg {
70                        limit_sum += m.gas_limit;
71                        premium_sum += m.gas_premium.clone();
72                    }
73                    for m in &msgs.secp_msg {
74                        limit_sum += m.message().gas_limit;
75                        premium_sum += m.message().gas_premium.clone();
76                    }
77
78                    if len > 0 {
79                        premium_avg = premium_sum.atto() / BigInt::from(len);
80                    }
81
82                    println!(
83                        "\t{miner}: \t{len} msgs, gasLimit: {limit_sum} / {BLOCK_GAS_LIMIT} ({ratio:.2}), avgPremium: {premium_avg}",
84                        miner = b.miner_address,
85                        ratio = (limit_sum as f64) / (BLOCK_GAS_LIMIT as f64) * 100.0
86                    );
87                }
88                if let Some(child_ts) = tipsets.get(i + 1) {
89                    let msgs = ChainGetParentMessages::call(
90                        &client,
91                        (*child_ts.block_headers().first().cid(),),
92                    )
93                    .await?;
94                    let limit_sum: u64 = msgs.iter().map(|m| m.message.gas_limit).sum();
95                    let gas_used: u64 = {
96                        let receipts = ChainGetParentReceipts::call(
97                            &client,
98                            (*child_ts.block_headers().first().cid(),),
99                        )
100                        .await?;
101                        receipts.iter().map(|r| r.gas_used).sum()
102                    };
103                    let gas_efficiency = 100. * (gas_used as f64) / (limit_sum as f64);
104                    let gas_capacity = 100. * (limit_sum as f64) / (BLOCK_GAS_LIMIT as f64);
105
106                    println!(
107                        "\ttipset: \t{n} msgs, {gas_used} ({gas_efficiency:.2}%) / {limit_sum} ({gas_capacity:.2}%)",
108                        n = msgs.len()
109                    );
110                }
111            } else {
112                let epoch = ts.epoch();
113                let time = chrono::DateTime::from_timestamp(ts.min_timestamp() as _, 0)
114                    .context("invalid timestamp")?
115                    .format("%b %e %X");
116                let tsk = ts
117                    .block_headers()
118                    .iter()
119                    .map(|h| format!("{}: {},", h.cid(), h.miner_address))
120                    .join("");
121                println!("{epoch}: ({time}) [ {tsk} ]");
122            }
123        }
124
125        Ok(())
126    }
127}