Skip to main content

forest/cli/subcommands/
chain_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod list;
5use list::ChainListCommand;
6
7mod prune;
8use prune::ChainPruneCommands;
9
10use super::print_pretty_lotus_json;
11use crate::blocks::{Tipset, TipsetKey};
12use crate::lotus_json::HasLotusJson;
13use crate::message::ChainMessage;
14use crate::rpc::{self, prelude::*};
15use anyhow::{bail, ensure};
16use cid::Cid;
17use clap::Subcommand;
18use nunny::Vec as NonEmpty;
19
20#[derive(Debug, Clone, clap::ValueEnum)]
21pub enum Format {
22    Json,
23    Text,
24}
25
26#[derive(Debug, Subcommand)]
27pub enum ChainCommands {
28    /// Retrieves and prints out the block specified by the given CID
29    Block {
30        #[arg(short)]
31        cid: Cid,
32    },
33
34    /// Prints out the genesis tipset
35    Genesis,
36
37    /// Prints out the canonical head of the chain
38    Head {
39        /// Print the first `n` tipsets from the head (inclusive).
40        /// Tipsets are categorized by epoch in descending order.
41        #[arg(short = 'n', long, default_value = "1")]
42        tipsets: u64,
43        /// Format of the output. `json` or `text`.
44        #[arg(long, default_value = "text")]
45        format: Format,
46    },
47
48    /// Reads and prints out a message referenced by the specified CID from the
49    /// chain block store
50    Message {
51        #[arg(short)]
52        cid: Cid,
53    },
54
55    /// Reads and prints out IPLD nodes referenced by the specified CID from
56    /// chain block store and returns raw bytes
57    ReadObj {
58        #[arg(short)]
59        cid: Cid,
60    },
61
62    /// Manually set the head to the given tipset. This invalidates blocks
63    /// between the desired head and the new head
64    SetHead {
65        /// Construct the new head tipset from these CIDs
66        #[arg(num_args = 1.., required = true)]
67        cids: Vec<Cid>,
68        /// Use the tipset from this epoch as the new head.
69        /// Negative numbers specify decrements from the current head.
70        #[arg(long, conflicts_with = "cids", allow_hyphen_values = true)]
71        epoch: Option<i64>,
72        /// Skip confirmation dialogue.
73        #[arg(short, long, aliases = ["yes", "no-confirm"], short_alias = 'y')]
74        force: bool,
75    },
76    #[command(subcommand)]
77    Prune(ChainPruneCommands),
78    List(ChainListCommand),
79}
80
81impl ChainCommands {
82    pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
83        match self {
84            Self::Block { cid } => {
85                print_pretty_lotus_json(ChainGetBlock::call(&client, (cid,)).await?)
86            }
87            Self::Genesis => print_pretty_lotus_json(ChainGetGenesis::call(&client, ()).await?),
88            Self::Head { tipsets, format } => print_chain_head(&client, tipsets, format).await,
89            Self::Message { cid } => {
90                let bytes = ChainReadObj::call(&client, (cid,)).await?;
91                match fvm_ipld_encoding::from_slice::<ChainMessage>(&bytes)? {
92                    ChainMessage::Unsigned(m) => print_pretty_lotus_json(m),
93                    ChainMessage::Signed(m) => {
94                        let cid = m.cid();
95                        println!(
96                            "{}",
97                            serde_json::to_string_pretty(&m.into_lotus_json().with_cid(cid))?
98                        );
99                        Ok(())
100                    }
101                }
102            }
103            Self::ReadObj { cid } => {
104                let bytes = ChainReadObj::call(&client, (cid,)).await?;
105                println!("{}", hex::encode(bytes));
106                Ok(())
107            }
108            Self::SetHead {
109                cids,
110                epoch: Some(epoch),
111                force: no_confirm,
112            } => {
113                maybe_confirm(no_confirm, SET_HEAD_CONFIRMATION_MESSAGE)?;
114                assert!(cids.is_empty(), "should be disallowed by clap");
115                let tipset = tipset_by_epoch_or_offset(&client, epoch).await?;
116                ChainSetHead::call(&client, (tipset.key().clone(),)).await?;
117                Ok(())
118            }
119            Self::SetHead {
120                cids,
121                epoch: None,
122                force: no_confirm,
123            } => {
124                maybe_confirm(no_confirm, SET_HEAD_CONFIRMATION_MESSAGE)?;
125                ChainSetHead::call(
126                    &client,
127                    (TipsetKey::from(
128                        NonEmpty::new(cids).expect("empty vec disallowed by clap"),
129                    ),),
130                )
131                .await?;
132                Ok(())
133            }
134            Self::Prune(cmd) => cmd.run(client).await,
135            Self::List(cmd) => cmd.run(client).await,
136        }
137    }
138}
139
140/// If `epoch_or_offset` is negative, get the tipset that many blocks before the
141/// current head. Else treat `epoch_or_offset` as an epoch, and get that tipset.
142async fn tipset_by_epoch_or_offset(
143    client: &rpc::Client,
144    epoch_or_offset: i64,
145) -> Result<Tipset, jsonrpsee::core::ClientError> {
146    let current_head = ChainHead::call(client, ()).await?;
147
148    let target_epoch = match epoch_or_offset.is_negative() {
149        true => current_head.epoch() + epoch_or_offset, // adding negative number
150        false => epoch_or_offset,
151    };
152    ChainGetTipSetByHeight::call(client, (target_epoch, current_head.key().clone().into())).await
153}
154
155const SET_HEAD_CONFIRMATION_MESSAGE: &str =
156    "Manually setting head is an unsafe operation that could brick the node! Continue?";
157
158fn maybe_confirm(no_confirm: bool, prompt: impl Into<String>) -> anyhow::Result<()> {
159    if no_confirm {
160        return Ok(());
161    }
162    let should_continue = dialoguer::Confirm::new()
163        .default(false)
164        .with_prompt(prompt)
165        .wait_for_newline(true)
166        .interact()?;
167    match should_continue {
168        true => Ok(()),
169        false => bail!("Operation cancelled by user"),
170    }
171}
172
173#[derive(Debug, serde::Serialize)]
174struct TipsetInfo {
175    epoch: u64,
176    cids: Vec<String>,
177}
178
179/// Collects `n` tipsets from the head (inclusive) and returns them as a list of
180/// [`TipsetInfo`] objects.
181async fn collect_n_tipsets(client: &rpc::Client, n: u64) -> anyhow::Result<Vec<TipsetInfo>> {
182    ensure!(n > 0, "number of tipsets must be positive");
183    let current_epoch = ChainHead::call(client, ()).await?.epoch() as u64;
184    let mut tipsets = Vec::with_capacity(n as usize);
185    for epoch in (current_epoch.saturating_sub(n - 1)..=current_epoch).rev() {
186        let tipset = tipset_by_epoch_or_offset(client, epoch.try_into()?).await?;
187        tipsets.push(TipsetInfo {
188            epoch,
189            cids: tipset.cids().iter().map(|cid| cid.to_string()).collect(),
190        });
191    }
192    Ok(tipsets)
193}
194
195/// Print the first `n` tipsets from the head (inclusive).
196async fn print_chain_head(client: &rpc::Client, n: u64, format: Format) -> anyhow::Result<()> {
197    let tipsets = collect_n_tipsets(client, n).await?;
198    match format {
199        Format::Json => {
200            println!("{}", serde_json::to_string_pretty(&tipsets)?);
201        }
202        Format::Text => {
203            tipsets.iter().for_each(|epoch_info| {
204                println!("[{}]", epoch_info.epoch);
205                epoch_info.cids.iter().for_each(|cid| {
206                    println!("{cid}");
207                });
208            });
209        }
210    }
211    Ok(())
212}