1use std::{
7 collections::HashMap,
8 fs::File,
9 io::{BufReader, Read},
10 path::PathBuf,
11};
12
13use alloy_primitives::{B256, U256, map::HashSet};
14use clap::Parser;
15use eyre::{Context as _, ensure};
16use reth_chainspec::EthereumHardforks;
17use reth_cli_commands::common::{AccessRights, CliNodeTypes, EnvironmentArgs};
18use reth_db_api::{
19 cursor::{DbCursorRO, DbCursorRW},
20 tables,
21 transaction::DbTxMut,
22};
23use reth_ethereum::{chainspec::EthChainSpec, tasks::Runtime};
24use reth_primitives_traits::{Account, StorageEntry};
25use reth_provider::{BlockNumReader, DatabaseProviderFactory, HashingWriter};
26use reth_storage_api::DBProvider;
27use tempo_chainspec::spec::TempoChainSpecParser;
28use tracing::info;
29
30const MAGIC: &[u8; 8] = b"TEMPOSB\x00";
32
33const VERSION: u16 = 1;
35
36#[derive(Debug, Parser)]
38pub(crate) struct InitFromBinaryDump<C: reth_cli::chainspec::ChainSpecParser = TempoChainSpecParser>
39{
40 #[command(flatten)]
41 env: EnvironmentArgs<C>,
42
43 #[arg(value_name = "BINARY_DUMP_FILE")]
47 state: PathBuf,
48}
49
50impl<C: reth_cli::chainspec::ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>>
51 InitFromBinaryDump<C>
52{
53 pub(crate) async fn execute<N>(self, runtime: Runtime) -> eyre::Result<()>
55 where
56 N: CliNodeTypes<ChainSpec = C::ChainSpec>,
57 {
58 info!(target: "tempo::cli", "Tempo init-from-binary-dump starting");
59
60 let environment = self.env.init::<N>(AccessRights::RW, runtime)?;
61 let provider_factory = environment.provider_factory;
62
63 let provider_rw = provider_factory.database_provider_rw()?;
64
65 let last_block = provider_rw.last_block_number()?;
67 ensure!(
68 last_block == 0,
69 "init-from-binary-dump must be run on a freshly initialized database at block 0, \
70 but found block {last_block}"
71 );
72
73 info!(target: "tempo::cli", path = %self.state.display(), "Loading binary state dump");
74
75 let file = File::open(&self.state)
76 .wrap_err_with(|| format!("failed to open {}", self.state.display()))?;
77 let mut reader = BufReader::with_capacity(64 * 1024 * 1024, file);
78
79 let mut total_entries = 0u64;
80 let mut total_tokens = 0u64;
81
82 let mut storage_for_hashing: HashMap<alloy_primitives::Address, Vec<StorageEntry>> =
84 HashMap::new();
85
86 let mut addresses_seen: HashSet<alloy_primitives::Address> = HashSet::default();
88
89 loop {
91 let mut header_buf = [0u8; 40];
93 match reader.read_exact(&mut header_buf) {
94 Ok(()) => {}
95 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
96 Err(e) => return Err(e).wrap_err("failed to read block header"),
97 }
98
99 ensure!(
101 &header_buf[..8] == MAGIC,
102 "invalid magic bytes in block header"
103 );
104
105 let version = u16::from_be_bytes([header_buf[8], header_buf[9]]);
107 ensure!(
108 version == VERSION,
109 "unsupported binary format version {version}, expected {VERSION}"
110 );
111
112 let mut address_bytes = [0u8; 20];
116 address_bytes.copy_from_slice(&header_buf[12..32]);
117 let address = alloy_primitives::Address::from(address_bytes);
118
119 let pair_count = u64::from_be_bytes(header_buf[32..40].try_into().unwrap());
121
122 info!(
123 target: "tempo::cli",
124 %address,
125 pair_count,
126 "Processing token storage"
127 );
128
129 addresses_seen.insert(address);
130
131 let tx = provider_rw.tx_ref();
133 let mut storage_cursor = tx.cursor_dup_write::<tables::PlainStorageState>()?;
134 let mut account_cursor = tx.cursor_write::<tables::PlainAccountState>()?;
135
136 if account_cursor.seek_exact(address)?.is_none() {
139 account_cursor.upsert(address, &Account::default())?;
140 }
141
142 let storage_entries = storage_for_hashing.entry(address).or_default();
144
145 let mut entry_buf = [0u8; 64];
147 let start = std::time::Instant::now();
148 let mut last_log = start;
149 for i in 0..pair_count {
150 reader
151 .read_exact(&mut entry_buf)
152 .wrap_err("failed to read storage entry")?;
153
154 let slot = B256::from_slice(&entry_buf[..32]);
155 let value = U256::from_be_bytes::<32>(entry_buf[32..64].try_into().unwrap());
156
157 if value.is_zero() {
159 continue;
160 }
161
162 let entry = StorageEntry { key: slot, value };
163
164 storage_cursor.upsert(address, &entry)?;
166
167 storage_entries.push(entry);
169
170 total_entries += 1;
171
172 let now = std::time::Instant::now();
173 if now.duration_since(last_log) >= std::time::Duration::from_secs(5)
174 || i + 1 == pair_count
175 {
176 let pct = ((i + 1) as f64 / pair_count as f64) * 100.0;
177 let elapsed = start.elapsed();
178 let pairs_per_sec = (i + 1) as f64 / elapsed.as_secs_f64();
179 info!(
180 target: "tempo::cli",
181 %address,
182 progress = format_args!("{}/{} ({pct:.0}%)", i + 1, pair_count),
183 elapsed = ?elapsed,
184 pairs_per_sec = pairs_per_sec as u64,
185 "Inserting storage"
186 );
187 last_log = now;
188 }
189 }
190
191 total_tokens += 1;
192 }
193
194 info!(
195 target: "tempo::cli",
196 total_tokens,
197 total_entries,
198 "Plain storage state written, now writing hashed state..."
199 );
200
201 let empty_account = Account::default();
205 provider_rw.insert_account_for_hashing(
206 addresses_seen
207 .iter()
208 .map(|addr| (*addr, Some(empty_account))),
209 )?;
210
211 info!(
212 target: "tempo::cli",
213 addresses = addresses_seen.len(),
214 "Hashed accounts written"
215 );
216
217 provider_rw.insert_storage_for_hashing(
219 storage_for_hashing
220 .into_iter()
221 .map(|(addr, entries)| (addr, entries.into_iter())),
222 )?;
223
224 info!(target: "tempo::cli", "Hashed storage written");
225
226 provider_rw.commit()?;
228
229 info!(
230 target: "tempo::cli",
231 total_tokens,
232 total_entries,
233 "Binary state dump loaded successfully"
234 );
235
236 info!(
239 target: "tempo::cli",
240 "State loaded. The node will compute the state root on startup."
241 );
242
243 Ok(())
244 }
245}