forest/cli/subcommands/
chain_cmd.rs

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