forest/tool/subcommands/
shed_cmd.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod migration;
5use migration::*;
6
7use crate::{
8    libp2p::keypair::get_keypair,
9    rpc::{
10        self, ApiPaths, RpcMethodExt as _,
11        chain::{ChainGetTipSetByHeight, ChainHead},
12        types::ApiTipsetKey,
13    },
14};
15use anyhow::Context as _;
16use base64::{Engine, prelude::BASE64_STANDARD};
17use clap::Subcommand;
18use clap::ValueEnum;
19use futures::{StreamExt as _, TryFutureExt as _, TryStreamExt as _};
20use openrpc_types::ReferenceOr;
21use std::path::PathBuf;
22
23#[derive(Subcommand)]
24pub enum ShedCommands {
25    /// Enumerate the tipset CIDs for a span of epochs starting at `height` and working backwards.
26    ///
27    /// Useful for getting blocks to live test an RPC endpoint.
28    SummarizeTipsets {
29        /// If omitted, defaults to the HEAD of the node.
30        #[arg(long)]
31        height: Option<u32>,
32        #[arg(long)]
33        ancestors: u32,
34    },
35    /// Generate a `PeerId` from the given key-pair file.
36    PeerIdFromKeyPair {
37        /// Path to the key-pair file.
38        keypair: PathBuf,
39    },
40    /// Generate a base64-encoded private key from the given key-pair file.
41    /// This effectively transforms Forest's key-pair file into a Lotus-compatible private key.
42    PrivateKeyFromKeyPair {
43        /// Path to the key-pair file.
44        keypair: PathBuf,
45    },
46    /// Generate a key-pair file from the given base64-encoded private key.
47    /// This effectively transforms Lotus's private key into a Forest-compatible key-pair file.
48    /// If `output` is not provided, the key-pair is printed to stdout as a base64-encoded string.
49    KeyPairFromPrivateKey {
50        /// Base64-encoded private key.
51        private_key: String,
52        /// Path to save the key-pair file.
53        #[arg(short, long)]
54        output: Option<PathBuf>,
55    },
56    /// Dump the OpenRPC definition for the node.
57    Openrpc {
58        include: Vec<String>,
59        /// Which API path to dump.
60        #[arg(long)]
61        path: ApiPaths,
62        /// A comma-separated list of fields to omit from the output (e.g., "summary,description").
63        #[arg(long, value_delimiter = ',')]
64        omit: Option<Vec<OmitField>>,
65    },
66    /// Run a network upgrade migration
67    MigrateState(MigrateStateCommand),
68}
69
70#[derive(Debug, Clone, ValueEnum, PartialEq)]
71pub enum OmitField {
72    Summary,
73    Description,
74}
75
76impl ShedCommands {
77    pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
78        match self {
79            ShedCommands::SummarizeTipsets { height, ancestors } => {
80                let head = ChainHead::call(&client, ()).await?;
81                let end_height = match height {
82                    Some(it) => it,
83                    None => head
84                        .epoch()
85                        .try_into()
86                        .context("HEAD epoch out-of-bounds")?,
87                };
88                let start_height = end_height
89                    .checked_sub(ancestors)
90                    .context("couldn't set start height")?;
91
92                let mut epoch2cids =
93                    futures::stream::iter((start_height..=end_height).map(|epoch| {
94                        ChainGetTipSetByHeight::call(
95                            &client,
96                            (i64::from(epoch), ApiTipsetKey(Some(head.key().clone()))),
97                        )
98                        .map_ok(|tipset| {
99                            let cids = tipset.block_headers().iter().map(|it| *it.cid());
100                            (tipset.epoch(), cids.collect::<Vec<_>>())
101                        })
102                    }))
103                    .buffered(12);
104
105                while let Some((epoch, cids)) = epoch2cids.try_next().await? {
106                    println!("{epoch}:");
107                    for cid in cids {
108                        println!("- {cid}");
109                    }
110                }
111            }
112            ShedCommands::PeerIdFromKeyPair { keypair } => {
113                let keypair = get_keypair(&keypair)
114                    .with_context(|| format!("couldn't get keypair from {}", keypair.display()))?;
115                println!("{}", keypair.public().to_peer_id());
116            }
117            ShedCommands::PrivateKeyFromKeyPair { keypair } => {
118                let keypair = get_keypair(&keypair)
119                    .with_context(|| format!("couldn't get keypair from {}", keypair.display()))?;
120                let encoded = BASE64_STANDARD.encode(keypair.to_protobuf_encoding()?);
121                println!("{encoded}");
122            }
123            ShedCommands::KeyPairFromPrivateKey {
124                private_key,
125                output,
126            } => {
127                let private_key = BASE64_STANDARD.decode(private_key)?;
128                let keypair_data = libp2p::identity::Keypair::from_protobuf_encoding(&private_key)?
129                    // While a keypair can be any type, Forest only supports Ed25519.
130                    .try_into_ed25519()?
131                    .to_bytes();
132                if let Some(output) = output {
133                    std::fs::write(output, keypair_data)?;
134                } else {
135                    println!("{}", BASE64_STANDARD.encode(keypair_data));
136                }
137            }
138            ShedCommands::Openrpc {
139                include,
140                path,
141                omit,
142            } => {
143                let include = include.iter().map(String::as_str).collect::<Vec<_>>();
144
145                let mut openrpc_doc = crate::rpc::openrpc(
146                    path,
147                    match include.is_empty() {
148                        true => None,
149                        false => Some(&include),
150                    },
151                );
152                if let Some(omit_fields) = omit {
153                    for method in &mut openrpc_doc.methods {
154                        if let ReferenceOr::Item(m) = method {
155                            if omit_fields.contains(&OmitField::Summary) {
156                                m.summary = None;
157                            }
158                            if omit_fields.contains(&OmitField::Description) {
159                                m.description = None;
160                            }
161                        }
162                    }
163                }
164                openrpc_doc.methods.sort_by(|a, b| match (a, b) {
165                    (ReferenceOr::Item(a), ReferenceOr::Item(b)) => a.name.cmp(&b.name),
166                    _ => std::cmp::Ordering::Equal,
167                });
168
169                println!("{}", serde_json::to_string_pretty(&openrpc_doc)?);
170            }
171            ShedCommands::MigrateState(cmd) => cmd.run(client).await?,
172        }
173        Ok(())
174    }
175}