Skip to main content

tempo/
init_state.rs

1//! Initialize state from a binary dump file.
2//!
3//! This command loads TIP20 storage slots from a binary file and applies them
4//! to the genesis state. The binary format is produced by `tempo-xtask generate-state-bloat`.
5
6use 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
30/// Magic bytes for the state bloat binary format (8 bytes)
31const MAGIC: &[u8; 8] = b"TEMPOSB\x00";
32
33/// Expected format version
34const VERSION: u16 = 1;
35
36/// Initialize state from a binary dump file.
37#[derive(Debug, Parser)]
38pub(crate) struct InitFromBinaryDump<C: reth_cli::chainspec::ChainSpecParser = TempoChainSpecParser>
39{
40    #[command(flatten)]
41    env: EnvironmentArgs<C>,
42
43    /// Path to the binary state dump file.
44    ///
45    /// The file should be generated by `tempo-xtask generate-state-bloat`.
46    #[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    /// Execute the init-from-binary-dump command.
54    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        // Verify we're at genesis (block 0)
66        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        // Collect storage entries per address for hashing
83        let mut storage_for_hashing: HashMap<alloy_primitives::Address, Vec<StorageEntry>> =
84            HashMap::new();
85
86        // Track addresses for account hashing (we need to create empty accounts)
87        let mut addresses_seen: HashSet<alloy_primitives::Address> = HashSet::default();
88
89        // Process blocks from binary file
90        loop {
91            // Try to read header
92            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            // Validate magic
100            ensure!(
101                &header_buf[..8] == MAGIC,
102                "invalid magic bytes in block header"
103            );
104
105            // Validate version
106            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            // Skip flags (2 bytes at offset 10)
113
114            // Read address (20 bytes at offset 12)
115            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            // Read pair count (8 bytes at offset 32)
120            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            // Get cursors for plain state tables
132            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            // Insert empty account for this address (required for storage to be included in trie)
137            // Only insert if the account doesn't already exist from genesis
138            if account_cursor.seek_exact(address)?.is_none() {
139                account_cursor.upsert(address, &Account::default())?;
140            }
141
142            // Collect storage for hashing
143            let storage_entries = storage_for_hashing.entry(address).or_default();
144
145            // Read and insert entries
146            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                // Skip zero values (they represent deletion)
158                if value.is_zero() {
159                    continue;
160                }
161
162                let entry = StorageEntry { key: slot, value };
163
164                // Insert into plain storage state
165                storage_cursor.upsert(address, &entry)?;
166
167                // Collect for hashed storage
168                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        // Write hashed account entries for addresses that have storage.
202        // We create empty accounts (zero balance, zero nonce, no code) for precompile addresses.
203        // This is required for the storage to be included in the state trie computation.
204        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        // Write hashed storage entries for trie computation
218        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        // Commit the transaction
227        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        // Note: The state root will need to be recomputed.
237        // The node will compute the actual state root on startup.
238        info!(
239            target: "tempo::cli",
240            "State loaded. The node will compute the state root on startup."
241        );
242
243        Ok(())
244    }
245}