forest/dev/subcommands/
export_state_tree_cmd.rs1use crate::{
5 chain::{ChainStore, index::ResolveNullTipset},
6 cid_collections::FileBackedCidHashSet,
7 cli_shared::{chain_path, read_config},
8 daemon::db_util::load_all_forest_cars,
9 db::{
10 CAR_DB_DIR_NAME,
11 car::{ManyCar, forest::FOREST_CAR_FILE_EXTENSION},
12 db_engine::{db_root, open_db},
13 },
14 genesis::read_genesis_header,
15 ipld::IpldStream,
16 networks::{ChainConfig, NetworkChain},
17 shim::{clock::ChainEpoch, executor::Receipt},
18};
19use anyhow::Context as _;
20use clap::Args;
21use itertools::Itertools;
22use std::{
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26use tokio::io::AsyncWriteExt as _;
27
28#[derive(Debug, Args)]
30pub struct ExportStateTreeCommand {
31 #[arg(long, required = true)]
33 chain: NetworkChain,
34 #[arg(long)]
36 db: Option<PathBuf>,
37 #[arg(long)]
39 from: ChainEpoch,
40 #[arg(long)]
42 to: ChainEpoch,
43 #[arg(short, long)]
45 output: Option<PathBuf>,
46}
47
48impl ExportStateTreeCommand {
49 pub async fn run(self) -> anyhow::Result<()> {
50 let Self {
51 chain,
52 db,
53 from,
54 to,
55 output,
56 } = self;
57 let output = output.unwrap_or_else(|| {
58 Path::new(&format!(
59 "statetree_{chain}_{to}_{from}{FOREST_CAR_FILE_EXTENSION}"
60 ))
61 .to_owned()
62 });
63 let db_root_path = if let Some(db) = db {
64 db
65 } else {
66 let (_, config) = read_config(None, Some(chain.clone()))?;
67 db_root(&chain_path(&config))?
68 };
69 let forest_car_db_dir = db_root_path.join(CAR_DB_DIR_NAME);
70 let db: Arc<ManyCar<crate::db::parity_db::ParityDb>> =
71 Arc::new(ManyCar::new(open_db(db_root_path, &Default::default())?));
72 load_all_forest_cars(&db, &forest_car_db_dir)?;
73
74 let chain_config = Arc::new(ChainConfig::from_chain(&chain));
75 let genesis_header =
76 read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db)
77 .await?;
78 let chain_store = ChainStore::new(db.clone(), chain_config, genesis_header)?;
79
80 let start_ts = chain_store
81 .chain_index()
82 .load_required_tipset_by_height(
83 from,
84 chain_store.heaviest_tipset(),
85 ResolveNullTipset::TakeNewer,
86 )
87 .await?;
88
89 let mut ipld_roots = vec![];
90 for (child, ts) in start_ts
91 .chain(&db)
92 .tuple_windows()
93 .take_while(|(_, parent)| parent.epoch() >= to)
94 {
95 ipld_roots.extend([*child.parent_state(), *child.parent_message_receipts()]);
96 ipld_roots.extend(ts.block_headers().iter().map(|h| h.messages));
97 let receipts = Receipt::get_receipts(&db, *child.parent_message_receipts())
98 .with_context(|| {
99 format!(
100 "failed to get receipts, root: {}, epoch: {}, tipset key: {}",
101 *child.parent_message_receipts(),
102 ts.epoch(),
103 ts.key(),
104 )
105 })?;
106 ipld_roots.extend(receipts.into_iter().filter_map(|r| r.events_root()));
107 }
108 let roots = nunny::vec![ipld_roots.first().cloned().context("no ipld roots found")?];
109 let stream = IpldStream::new(
110 db,
111 ipld_roots.clone(),
112 FileBackedCidHashSet::new_in_temp_dir()?,
113 );
114 let frames = crate::db::car::forest::Encoder::compress_stream_default(stream);
115 let tmp =
116 tempfile::NamedTempFile::new_in(output.parent().unwrap_or_else(|| Path::new(".")))?
117 .into_temp_path();
118 let mut writer = tokio::io::BufWriter::new(tokio::fs::File::create(&tmp).await?);
119 crate::db::car::forest::Encoder::write(&mut writer, roots, frames).await?;
120 writer.flush().await?;
121 tmp.persist(output)?;
122
123 Ok(())
124 }
125}