tempo_xtask/
generate_state_bloat.rs1use 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
26const MAGIC: &[u8; 8] = b"TEMPOSB\x00";
28
29const VERSION: u16 = 1;
31
32#[derive(Debug, clap::Args)]
34pub(crate) struct GenerateStateBloat {
35 #[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 #[arg(short, long, default_value = "1024")]
45 size: u64,
46
47 #[arg(short, long, default_values_t = vec![0u64])]
50 token: Vec<u64>,
51
52 #[arg(short, long, default_value = "state_bloat.bin")]
54 out: PathBuf,
55
56 #[arg(long, default_value = "1000000")]
58 balance: u64,
59
60 #[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; let num_tokens = tokens.len() as u64;
85
86 let header_size = 40u64;
91 let entry_size = 64u64;
92 let overhead_per_token = header_size + entry_size; 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 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 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 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 derive_address_fast(&seed, i as u64)
152 }
153 })
154 .collect();
155 pb.finish_with_message("done");
156
157 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 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 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); let balance_value = U256::from(balance);
187 let total_supply = balance_value * U256::from(total_accounts);
188
189 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 let balance_slot_bytes: Vec<[u8; 32]> = balance_slots
196 .iter()
197 .map(|s| s.to_be_bytes::<32>())
198 .collect();
199
200 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; write_header(&mut writer, *token_addr, pair_count)?;
220
221 writer.write_all(&total_supply_slot_bytes)?;
223 writer.write_all(&total_supply_bytes)?;
224
225 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
251fn 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
260fn derive_address_fast(seed: &[u8; 32], index: u64) -> Address {
264 let mut buf = [0u8; 40]; buf[..32].copy_from_slice(seed);
266 buf[32..].copy_from_slice(&index.to_be_bytes());
267 let hash = keccak256(buf);
268 Address::from_slice(&hash[12..])
270}
271
272fn 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 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
286fn compute_mapping_slot(key: Address, base_slot: U256) -> U256 {
288 let mut buf = [0u8; 64];
289 buf[12..32].copy_from_slice(key.as_slice());
291 buf[32..].copy_from_slice(&base_slot.to_be_bytes::<32>());
293 U256::from_be_bytes(keccak256(buf).0)
294}
295
296fn 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())?; 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 let addr: Address = "0x1234567890123456789012345678901234567890"
334 .parse()
335 .unwrap();
336 let slot = compute_mapping_slot(addr, tip20_slots::BALANCES);
337
338 let slot2 = compute_mapping_slot(addr, tip20_slots::BALANCES);
340 assert_eq!(slot, slot2);
341
342 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 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}