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