Skip to main content

tempo_xtask/
generate_state_bloat.rs

1//! State bloat generation tool for generating large TIP20 storage state files.
2//!
3//! Generates a binary file containing TIP20 storage slots (total_supply + balances)
4//! that can be loaded during genesis initialization to create a bloated state.
5
6use alloy::{
7    primitives::{Address, U256, keccak256},
8    signers::{
9        local::coins_bip39::{English, Mnemonic},
10        utils::secret_key_to_address,
11    },
12};
13use coins_bip32::prelude::*;
14use eyre::{Context as _, ensure};
15use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle};
16use rayon::prelude::*;
17use std::{
18    fs::File,
19    io::{BufWriter, Write},
20    path::PathBuf,
21    sync::Arc,
22};
23use tempo_precompiles::tip20::tip20_slots;
24use tempo_primitives::transaction::TIP20_PAYMENT_PREFIX;
25
26/// Magic bytes for the state bloat binary format (8 bytes)
27const MAGIC: &[u8; 8] = b"TEMPOSB\x00";
28
29/// Format version
30const VERSION: u16 = 1;
31
32/// Generate state bloat file
33#[derive(Debug, clap::Args)]
34pub(crate) struct GenerateStateBloat {
35    /// Mnemonic to use for account generation
36    #[arg(
37        short,
38        long,
39        default_value = "test test test test test test test test test test test junk"
40    )]
41    mnemonic: String,
42
43    /// Target file size in MiB
44    #[arg(short, long, default_value = "1024")]
45    size: u64,
46
47    /// Token IDs to generate storage for (can be specified multiple times)
48    /// Uses reserved TIP20 addresses: 0x20C0...{token_id}
49    #[arg(short, long, default_values_t = vec![0u64])]
50    token: Vec<u64>,
51
52    /// Output file path
53    #[arg(short, long, default_value = "state_bloat.bin")]
54    out: PathBuf,
55
56    /// Balance value to assign to each account (in smallest units)
57    #[arg(long, default_value = "1000000")]
58    balance: u64,
59
60    /// Number of addresses to derive using proper BIP32 (signable).
61    /// Remaining addresses use fast keccak-based derivation (not signable).
62    #[arg(long, default_value = "10000")]
63    signable_count: usize,
64}
65
66impl GenerateStateBloat {
67    pub(crate) async fn run(self) -> eyre::Result<()> {
68        let Self {
69            mnemonic,
70            size,
71            token: tokens,
72            out,
73            balance,
74            signable_count,
75        } = self;
76
77        ensure!(
78            !tokens.is_empty(),
79            "at least one token ID must be specified"
80        );
81        ensure!(size > 0, "size must be greater than 0");
82
83        let target_bytes = size * 1024 * 1024; // MiB to bytes
84        let num_tokens = tokens.len() as u64;
85
86        // Calculate number of accounts needed
87        // Per token: 1 header (40 bytes) + 1 total_supply (64 bytes) + N balances (64 bytes each)
88        // Total bytes ≈ T * (40 + 64 + N * 64)
89        // Solving for N: N = (target_bytes / T - 104) / 64
90        let header_size = 40u64;
91        let entry_size = 64u64;
92        let overhead_per_token = header_size + entry_size; // header + total_supply
93        let available_for_balances = target_bytes.saturating_sub(num_tokens * overhead_per_token);
94        let total_balance_entries = available_for_balances / entry_size;
95        let accounts_per_token = total_balance_entries / num_tokens;
96
97        ensure!(
98            accounts_per_token > 0,
99            "target size too small for the number of tokens"
100        );
101
102        let total_accounts = accounts_per_token as usize;
103
104        let estimated_size_mib =
105            (num_tokens * (overhead_per_token + accounts_per_token * entry_size)) as f64
106                / (1024.0 * 1024.0);
107        let out_display = out.display();
108        println!("State bloat generation:");
109        println!("  Target size: {size} MiB");
110        println!("  Tokens: {num_tokens}");
111        println!("  Accounts per token: {accounts_per_token}");
112        println!("  Estimated file size: {estimated_size_mib:.2} MiB");
113        println!("  Output: {out_display}");
114
115        // Step 1: Derive user addresses (hybrid approach)
116        // - First `signable_count` addresses use proper BIP32 derivation (slow but signable)
117        // - Remaining addresses use fast keccak-based derivation (not signable, just for bloat)
118        let actual_signable = signable_count.min(total_accounts);
119        let fast_count = total_accounts - actual_signable;
120
121        println!(
122            "\nDeriving {total_accounts} user addresses ({actual_signable} signable, {fast_count} fast)..."
123        );
124
125        // Parse mnemonic and derive parent key once (this is the slow PBKDF2 step)
126        let parent_key = derive_parent_key(&mnemonic)?;
127        let parent_key = Arc::new(parent_key);
128        let seed = keccak256(mnemonic.as_bytes());
129
130        let pb = ProgressBar::new(total_accounts as u64);
131        pb.set_style(
132            ProgressStyle::default_bar()
133                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} ({per_sec}) ({eta})")
134                .expect("valid template"),
135        );
136
137        let user_addresses: Vec<Address> = (0..total_accounts)
138            .into_par_iter()
139            .progress_with(pb.clone())
140            .map(|i| {
141                if i < actual_signable {
142                    // Proper BIP32 derivation (signable)
143                    let child = parent_key
144                        .derive_child(i as u32)
145                        .expect("child derivation should not fail");
146                    let key: &coins_bip32::prelude::SigningKey = child.as_ref();
147                    let credential = k256::ecdsa::SigningKey::from_bytes(&key.to_bytes()).unwrap();
148                    secret_key_to_address(&credential)
149                } else {
150                    // Fast keccak-based derivation (not signable)
151                    derive_address_fast(&seed, i as u64)
152                }
153            })
154            .collect();
155        pb.finish_with_message("done");
156
157        // Step 2: Precompute balance slots (cached - same for all tokens, parallel)
158        println!("\nPrecomputing {total_accounts} balance slots (keccak256)...");
159        let pb = ProgressBar::new(total_accounts as u64);
160        pb.set_style(
161            ProgressStyle::default_bar()
162                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} ({per_sec}) ({eta})")
163                .expect("valid template"),
164        );
165
166        let balance_slots: Vec<U256> = user_addresses
167            .par_iter()
168            .progress_with(pb.clone())
169            .map(|addr| compute_mapping_slot(*addr, tip20_slots::BALANCES))
170            .collect();
171        pb.finish_with_message("done");
172
173        // Step 3: Generate token addresses
174        let token_addresses: Vec<Address> = tokens.iter().map(|&id| token_address(id)).collect();
175
176        println!("\nToken addresses:");
177        for (id, addr) in tokens.iter().zip(&token_addresses) {
178            println!("  Token {id}: {addr}");
179        }
180
181        // Step 4: Stream-write the binary file
182        println!("\nWriting state bloat file...");
183        let file = File::create(&out).wrap_err("failed to create output file")?;
184        let mut writer = BufWriter::with_capacity(64 * 1024 * 1024, file); // 64MB buffer
185
186        let balance_value = U256::from(balance);
187        let total_supply = balance_value * U256::from(total_accounts);
188
189        // Precompute constant byte representations
190        let balance_bytes = balance_value.to_be_bytes::<32>();
191        let total_supply_bytes = total_supply.to_be_bytes::<32>();
192        let total_supply_slot_bytes = tip20_slots::TOTAL_SUPPLY.to_be_bytes::<32>();
193
194        // Precompute balance slot bytes to avoid to_be_bytes in inner loop
195        let balance_slot_bytes: Vec<[u8; 32]> = balance_slots
196            .iter()
197            .map(|s| s.to_be_bytes::<32>())
198            .collect();
199
200        // Chunk size: 256k entries = 16 MiB per chunk
201        const CHUNK_ENTRIES: usize = 256 * 1024;
202        let chunks_per_token =
203            (balance_slot_bytes.len() + CHUNK_ENTRIES - 1) / CHUNK_ENTRIES.max(1);
204        let total_chunks = num_tokens as usize * chunks_per_token;
205
206        let pb = ProgressBar::new(total_chunks as u64);
207        pb.set_style(
208            ProgressStyle::default_bar()
209                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} chunks ({eta})")
210                .expect("valid template"),
211        );
212
213        let mut chunk_buf = Vec::with_capacity(CHUNK_ENTRIES * 64);
214
215        for token_addr in &token_addresses {
216            let pair_count = 1 + accounts_per_token; // total_supply + balances
217
218            // Write header: [magic:8][version:2][flags:2][address:20][pair_count:8] = 40 bytes
219            write_header(&mut writer, *token_addr, pair_count)?;
220
221            // Write total_supply entry
222            writer.write_all(&total_supply_slot_bytes)?;
223            writer.write_all(&total_supply_bytes)?;
224
225            // Write balance entries in chunks
226            for slots_chunk in balance_slot_bytes.chunks(CHUNK_ENTRIES) {
227                chunk_buf.clear();
228                for slot_bytes in slots_chunk {
229                    chunk_buf.extend_from_slice(slot_bytes);
230                    chunk_buf.extend_from_slice(&balance_bytes);
231                }
232                writer.write_all(&chunk_buf)?;
233                pb.inc(1);
234            }
235        }
236
237        writer.flush()?;
238        pb.finish_with_message("done");
239
240        let file_size = std::fs::metadata(&out)?.len();
241        println!(
242            "\nGenerated {} ({:.2} MiB)",
243            out.display(),
244            file_size as f64 / (1024.0 * 1024.0)
245        );
246
247        Ok(())
248    }
249}
250
251/// Compute a reserved TIP20 token address from a token ID.
252/// Reserved addresses use the TIP20 prefix with the token ID in the last 8 bytes.
253fn token_address(token_id: u64) -> Address {
254    let mut bytes = [0u8; 20];
255    bytes[..12].copy_from_slice(&TIP20_PAYMENT_PREFIX);
256    bytes[12..].copy_from_slice(&token_id.to_be_bytes());
257    Address::from(bytes)
258}
259
260/// Fast address derivation using keccak256(seed || index).
261/// This is much faster than BIP32 but the resulting addresses are NOT signable.
262/// Used for generating bloat addresses beyond the signable count.
263fn derive_address_fast(seed: &[u8; 32], index: u64) -> Address {
264    let mut buf = [0u8; 40]; // 32 bytes seed + 8 bytes index
265    buf[..32].copy_from_slice(seed);
266    buf[32..].copy_from_slice(&index.to_be_bytes());
267    let hash = keccak256(buf);
268    // Take last 20 bytes of hash as address
269    Address::from_slice(&hash[12..])
270}
271
272/// Derive the parent key for BIP44 Ethereum path: m/44'/60'/0'/0
273/// This performs PBKDF2 once, then subsequent child derivations are fast.
274fn derive_parent_key(mnemonic_phrase: &str) -> eyre::Result<XPriv> {
275    let mnemonic = Mnemonic::<English>::new_from_phrase(mnemonic_phrase)
276        .map_err(|e| eyre::eyre!("invalid mnemonic: {e}"))?;
277
278    // Derive seed from mnemonic (this is the slow PBKDF2 step)
279    let master: XPriv = mnemonic
280        .derive_key("m/44'/60'/0'/0", None)
281        .map_err(|e| eyre::eyre!("key derivation failed: {e}"))?;
282
283    Ok(master)
284}
285
286/// Compute a Solidity mapping slot: keccak256(pad32(key) || pad32(base_slot))
287fn compute_mapping_slot(key: Address, base_slot: U256) -> U256 {
288    let mut buf = [0u8; 64];
289    // Left-pad address to 32 bytes
290    buf[12..32].copy_from_slice(key.as_slice());
291    // Base slot as big-endian 32 bytes
292    buf[32..].copy_from_slice(&base_slot.to_be_bytes::<32>());
293    U256::from_be_bytes(keccak256(buf).0)
294}
295
296/// Write a block header to the output.
297/// Format: `[magic:8][version:2][flags:2][address:20][pair_count:8] = 40 bytes`
298fn write_header(writer: &mut impl Write, address: Address, pair_count: u64) -> eyre::Result<()> {
299    writer.write_all(MAGIC)?;
300    writer.write_all(&VERSION.to_be_bytes())?;
301    writer.write_all(&0u16.to_be_bytes())?; // flags (reserved)
302    writer.write_all(address.as_slice())?;
303    writer.write_all(&pair_count.to_be_bytes())?;
304    Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_token_address() {
313        let addr = token_address(0);
314        assert_eq!(
315            addr,
316            "0x20C0000000000000000000000000000000000000"
317                .parse::<Address>()
318                .unwrap()
319        );
320
321        let addr = token_address(1);
322        assert_eq!(
323            addr,
324            "0x20C0000000000000000000000000000000000001"
325                .parse::<Address>()
326                .unwrap()
327        );
328    }
329
330    #[test]
331    fn test_compute_mapping_slot() {
332        // Verify the slot computation matches Solidity's keccak256(abi.encode(key, slot))
333        let addr: Address = "0x1234567890123456789012345678901234567890"
334            .parse()
335            .unwrap();
336        let slot = compute_mapping_slot(addr, tip20_slots::BALANCES);
337
338        // The slot should be deterministic
339        let slot2 = compute_mapping_slot(addr, tip20_slots::BALANCES);
340        assert_eq!(slot, slot2);
341
342        // Different addresses should produce different slots
343        let other_addr: Address = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
344            .parse()
345            .unwrap();
346        let other_slot = compute_mapping_slot(other_addr, tip20_slots::BALANCES);
347        assert_ne!(slot, other_slot);
348    }
349
350    #[test]
351    fn test_header_size() {
352        let mut buf = Vec::new();
353        write_header(&mut buf, Address::ZERO, 100).unwrap();
354        assert_eq!(buf.len(), 40);
355    }
356
357    #[test]
358    fn test_derive_parent_key_matches_mnemonic_builder() {
359        use alloy::signers::local::MnemonicBuilder;
360
361        let mnemonic = "test test test test test test test test test test test junk";
362        let parent_key = derive_parent_key(mnemonic).unwrap();
363
364        // Verify first 10 addresses match MnemonicBuilder::from_phrase_nth
365        for i in 0..10u32 {
366            let expected = MnemonicBuilder::from_phrase_nth(mnemonic, i);
367
368            let child = parent_key.derive_child(i).unwrap();
369            let key: &coins_bip32::prelude::SigningKey = child.as_ref();
370            let credential = k256::ecdsa::SigningKey::from_bytes(&key.to_bytes()).unwrap();
371            let actual = secret_key_to_address(&credential);
372
373            assert_eq!(actual, expected.address(), "address mismatch at index {i}");
374        }
375    }
376
377    #[test]
378    fn test_entry_size() {
379        let slot = U256::ZERO.to_be_bytes::<32>();
380        let value = U256::from(1).to_be_bytes::<32>();
381        assert_eq!(slot.len() + value.len(), 64);
382    }
383}