1use std::collections::BTreeMap;
2use std::fs::File;
3use std::io::BufReader;
4use std::path::Path;
5use std::str::FromStr;
6
7use alloy_primitives::B64;
8use alloy_primitives::FixedBytes;
9use alloy_primitives::U256;
10use alloy_primitives::hex;
11use anyhow::Result;
12use const_hex::FromHex;
13use serde::Deserialize;
14use serde::Serialize;
15
16use crate::alias::RevmBytecode;
17use crate::eth::primitives::Account;
18use crate::eth::primitives::Address;
19use crate::eth::primitives::Nonce;
20use crate::eth::primitives::Slot;
21use crate::eth::primitives::SlotIndex;
22use crate::eth::primitives::SlotValue;
23use crate::eth::primitives::Wei;
24
25pub type GenesisSlots = Vec<(Address, Slot)>;
27
28#[allow(non_snake_case)]
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct GenesisConfig {
32 pub config: ChainConfig,
33 pub nonce: String,
34 pub timestamp: String,
35 pub extraData: String,
36 pub gasLimit: String,
37 pub difficulty: String,
38 pub mixHash: String,
39 pub coinbase: String,
40 pub alloc: BTreeMap<String, GenesisAccount>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub number: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub gasUsed: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub parentHash: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub baseFeePerGas: Option<String>,
49}
50
51#[allow(non_snake_case)]
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ChainConfig {
55 pub chainId: u64,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub homesteadBlock: Option<u64>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub eip150Block: Option<u64>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub eip155Block: Option<u64>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub eip158Block: Option<u64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub byzantiumBlock: Option<u64>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub constantinopleBlock: Option<u64>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub petersburgBlock: Option<u64>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub istanbulBlock: Option<u64>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub berlinBlock: Option<u64>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub londonBlock: Option<u64>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub shanghaiTime: Option<u64>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub cancunTime: Option<u64>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub pragueTime: Option<u64>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct GenesisAccount {
87 pub balance: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub code: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub nonce: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub storage: Option<BTreeMap<String, String>>,
94}
95
96impl GenesisConfig {
97 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
99 let file = File::open(path)?;
100 let reader = BufReader::new(file);
101 let genesis: GenesisConfig = serde_json::from_reader(reader)?;
102 Ok(genesis)
103 }
104
105 pub fn to_stratus_accounts_and_slots(&self) -> Result<(Vec<Account>, GenesisSlots)> {
111 let mut accounts = Vec::new();
112 let mut slots = Vec::new();
113
114 for (addr_str, genesis_account) in &self.alloc {
115 let addr_str = addr_str.trim_start_matches("0x");
117
118 let address = Address::from_str(addr_str)?;
120
121 let mut account = Account::new_empty(address);
123
124 let balance = if genesis_account.balance.starts_with("0x") {
126 Wei::from_hex_str(&genesis_account.balance)?
128 } else {
129 let value = genesis_account.balance.parse::<u64>().unwrap_or(0);
131 Wei::from(U256::from(value))
132 };
133
134 account.balance = balance;
135
136 if let Some(nonce) = &genesis_account.nonce {
138 let nonce_str = nonce.trim_start_matches("0x");
139 let nonce_value = if nonce.starts_with("0x") {
140 u64::from_str_radix(nonce_str, 16).unwrap_or(0)
141 } else {
142 nonce_str.parse::<u64>().unwrap_or(0)
143 };
144 account.nonce = Nonce::from(nonce_value);
145 }
146
147 if let Some(code) = &genesis_account.code {
149 let code_str = code.trim_start_matches("0x");
150 if let Ok(code_bytes) = hex::decode(code_str)
151 && !code_bytes.is_empty()
152 {
153 account.bytecode = Some(RevmBytecode::new_raw(code_bytes.into()));
154 }
155 }
156
157 if let Some(storage) = &genesis_account.storage {
159 for (slot_key, slot_value) in storage {
160 let slot_index: SlotIndex = FixedBytes::<32>::from_hex(slot_key)?.into();
162 let slot_value: SlotValue = FixedBytes::<32>::from_hex(slot_value)?.into();
163
164 slots.push((address, Slot::new(slot_index, slot_value)));
166 }
167 }
168
169 accounts.push(account);
170 }
171 Ok((accounts, slots))
172 }
173
174 pub fn to_stratus_accounts(&self) -> Result<Vec<Account>> {
178 let (accounts, _) = self.to_stratus_accounts_and_slots()?;
179 Ok(accounts)
180 }
181
182 pub fn to_genesis_block(&self) -> Result<crate::eth::primitives::Block> {
184 use std::str::FromStr;
185
186 use alloy_primitives::hex;
187
188 use crate::eth::primitives::Block;
189 use crate::eth::primitives::BlockHeader;
190 use crate::eth::primitives::BlockNumber;
191 use crate::eth::primitives::Bytes;
192 use crate::eth::primitives::Gas;
193 use crate::eth::primitives::Hash;
194 use crate::eth::primitives::UnixTime;
195 let timestamp_str = self.timestamp.trim_start_matches("0x");
197 let timestamp = if self.timestamp.starts_with("0x") {
198 u64::from_str_radix(timestamp_str, 16).unwrap_or(0)
199 } else {
200 timestamp_str.parse::<u64>().unwrap_or(0)
201 };
202
203 let gas_limit_str = self.gasLimit.trim_start_matches("0x");
205 let gas_limit = if self.gasLimit.starts_with("0x") {
206 u64::from_str_radix(gas_limit_str, 16).unwrap_or(0)
207 } else {
208 gas_limit_str.parse::<u64>().unwrap_or(0)
209 };
210
211 let gas_used = if let Some(gas_used) = &self.gasUsed {
213 let gas_used_str = gas_used.trim_start_matches("0x");
214 if gas_used.starts_with("0x") {
215 u64::from_str_radix(gas_used_str, 16).unwrap_or(0)
216 } else {
217 gas_used_str.parse::<u64>().unwrap_or(0)
218 }
219 } else {
220 0
221 };
222
223 let nonce_str = self.nonce.trim_start_matches("0x");
225 let nonce_bytes = if let Ok(bytes) = hex::decode(nonce_str) {
226 let mut padded = [0u8; 8];
227 let start = padded.len().saturating_sub(bytes.len());
228 let copy_len = bytes.len().min(padded.len() - start);
229 padded[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
230 padded
231 } else {
232 [0u8; 8]
233 };
234
235 let miner = Address::from_str(&self.coinbase).unwrap_or_default();
237
238 let extra_data_str = self.extraData.trim_start_matches("0x");
240 let extra_data = if let Ok(bytes) = hex::decode(extra_data_str) {
241 Bytes(bytes)
242 } else {
243 Bytes(vec![])
244 };
245
246 let parent_hash = if let Some(parent_hash) = &self.parentHash {
248 let parent_hash_str = parent_hash.trim_start_matches("0x");
249 if let Ok(bytes) = hex::decode(parent_hash_str) {
250 let mut hash_bytes = [0u8; 32];
251 let start = hash_bytes.len().saturating_sub(bytes.len());
252 let copy_len = bytes.len().min(hash_bytes.len() - start);
253 hash_bytes[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
254 Hash::new(hash_bytes)
255 } else {
256 Hash::default()
257 }
258 } else {
259 Hash::default()
260 };
261
262 let mix_hash_str = self.mixHash.trim_start_matches("0x");
264 let _mix_hash = if let Ok(bytes) = hex::decode(mix_hash_str) {
265 let mut hash_bytes = [0u8; 32];
266 let start = hash_bytes.len().saturating_sub(bytes.len());
267 let copy_len = bytes.len().min(hash_bytes.len() - start);
268 hash_bytes[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
269 Hash::new(hash_bytes)
270 } else {
271 Hash::default()
272 };
273
274 let difficulty = if self.difficulty.starts_with("0x") {
276 u64::from_str_radix(self.difficulty.trim_start_matches("0x"), 16).unwrap_or(0)
277 } else {
278 self.difficulty.parse::<u64>().unwrap_or(0)
279 };
280
281 let mut header = BlockHeader::new(BlockNumber::ZERO, UnixTime::from(timestamp));
283
284 header.gas_limit = Gas::from(gas_limit);
286 header.gas_used = Gas::from(gas_used);
287 header.miner = miner;
288 header.author = miner;
289 header.extra_data = extra_data;
290 header.parent_hash = parent_hash;
291 header.difficulty = difficulty.into();
292
293 let nonce_b64 = B64::from_slice(&nonce_bytes);
295 header.nonce = nonce_b64.into();
296
297 let block = Block {
299 header,
300 transactions: Vec::new(),
301 };
302
303 Ok(block)
304 }
305}
306
307impl Default for GenesisConfig {
308 #[cfg(feature = "dev")]
309 fn default() -> Self {
310 let mut alloc = BTreeMap::new();
312
313 let test_accounts = crate::eth::primitives::test_accounts();
315 for account in test_accounts {
316 let address_str = format!("0x{}", hex::encode(account.address.as_slice()));
317 let balance_hex = format!("0x{:x}", account.balance.0);
318
319 let mut genesis_account = GenesisAccount {
320 balance: balance_hex,
321 code: None,
322 nonce: Some(format!("0x{:x}", u64::from(account.nonce))),
323 storage: None,
324 };
325
326 if let Some(ref bytecode) = account.bytecode
328 && !bytecode.is_empty()
329 {
330 genesis_account.code = Some(format!("0x{}", hex::encode(bytecode.bytes())));
331 }
332
333 alloc.insert(address_str, genesis_account);
334 }
335
336 Self {
337 config: ChainConfig {
338 chainId: 2008,
339 homesteadBlock: Some(0),
340 eip150Block: Some(0),
341 eip155Block: Some(0),
342 eip158Block: Some(0),
343 byzantiumBlock: Some(0),
344 constantinopleBlock: Some(0),
345 petersburgBlock: Some(0),
346 istanbulBlock: Some(0),
347 berlinBlock: Some(0),
348 londonBlock: Some(0),
349 shanghaiTime: Some(0),
350 cancunTime: Some(0),
351 pragueTime: None,
352 },
353 nonce: "0x0000000000000042".to_string(),
354 timestamp: "0x0".to_string(),
355 extraData: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
356 gasLimit: "0xffffffff".to_string(),
357 difficulty: "0x1".to_string(),
358 mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
359 coinbase: "0x0000000000000000000000000000000000000000".to_string(),
360 alloc,
361 number: Some("0x0".to_string()),
362 gasUsed: Some("0x0".to_string()),
363 parentHash: Some("0x0000000000000000000000000000000000000000000000000000000000000000".to_string()),
364 baseFeePerGas: Some("0x0".to_string()),
365 }
366 }
367
368 #[cfg(not(feature = "dev"))]
369 fn default() -> Self {
370 Self {
372 config: ChainConfig {
373 chainId: 2008,
374 homesteadBlock: None,
375 eip150Block: None,
376 eip155Block: None,
377 eip158Block: None,
378 byzantiumBlock: None,
379 constantinopleBlock: None,
380 petersburgBlock: None,
381 istanbulBlock: None,
382 berlinBlock: None,
383 londonBlock: None,
384 shanghaiTime: None,
385 cancunTime: None,
386 pragueTime: None,
387 },
388 nonce: "0x0".to_string(),
389 timestamp: "0x0".to_string(),
390 extraData: "0x0".to_string(),
391 gasLimit: "0x0".to_string(),
392 difficulty: "0x0".to_string(),
393 mixHash: "0x0".to_string(),
394 coinbase: "0x0000000000000000000000000000000000000000".to_string(),
395 alloc: BTreeMap::new(),
396 number: None,
397 gasUsed: None,
398 parentHash: None,
399 baseFeePerGas: None,
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_load_genesis_and_convert_accounts() {
410 use std::fs::File;
411 use std::io::Write;
412
413 let file_path = "test_genesis.json";
415
416 let json = r#"
418 {
419 "config": {
420 "chainId": 2008
421 },
422 "nonce": "0x0",
423 "timestamp": "0x0",
424 "extraData": "0x0",
425 "gasLimit": "0x0",
426 "difficulty": "0x0",
427 "mixHash": "0x0",
428 "coinbase": "0x0000000000000000000000000000000000000000",
429 "alloc": {
430 "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": {
431 "balance": "0x1000",
432 "nonce": "0x1"
433 }
434 }
435 }"#;
436
437 let mut file = File::create(file_path).unwrap();
439 file.write_all(json.as_bytes()).unwrap();
440
441 let genesis = GenesisConfig::load_from_file(file_path).unwrap();
443
444 let addr_str = "f39fd6e51aad88f6f4ce6ab8827279cfffb92266";
446 let addr = Address::from_str(addr_str).unwrap();
447
448 let accounts = genesis.to_stratus_accounts().unwrap();
450
451 assert_eq!(accounts.len(), 1);
453 let account = &accounts[0];
454 assert_eq!(account.address, addr);
455 assert_eq!(account.nonce, Nonce::from(1u64));
456 assert_eq!(account.balance, Wei::from(U256::from(4096))); std::fs::remove_file(file_path).unwrap();
460 }
461
462 #[test]
463 fn test_default_genesis_config() {
464 let default_genesis = GenesisConfig::default();
466
467 assert_eq!(default_genesis.config.chainId, 2008);
469
470 #[cfg(feature = "dev")]
471 {
472 let test_accounts = crate::eth::primitives::test_accounts();
474 let stratus_accounts = default_genesis.to_stratus_accounts().unwrap();
475
476 assert_eq!(stratus_accounts.len(), test_accounts.len());
478
479 for test_account in test_accounts {
481 let found = stratus_accounts.iter().any(|account| account.address == test_account.address);
482 assert!(found, "Test account not found in default genesis config");
483 }
484 }
485
486 #[cfg(not(feature = "dev"))]
487 {
488 assert!(default_genesis.alloc.is_empty());
490 }
491 }
492
493 #[test]
494 fn test_genesis_file_with_clap_integration() {
495 use std::env;
496
497 use clap::Parser;
498
499 use crate::config::GenesisFileConfig;
500
501 let file_path = "config/genesis.local.json";
503
504 let genesis = GenesisConfig::load_from_file(file_path).expect("Failed to load genesis.local.json");
506
507 assert_eq!(genesis.config.chainId, 2008);
509
510 let accounts = genesis.to_stratus_accounts().expect("Failed to convert accounts");
512 assert!(!accounts.is_empty(), "No accounts found in genesis.local.json");
513
514 let first_account_addr = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266";
516 let first_addr = Address::from_str(first_account_addr).unwrap();
517
518 let found = accounts.iter().any(|account| account.address == first_addr);
519 assert!(found, "First account not found in genesis.local.json");
520
521 if let Some(account) = accounts.iter().find(|account| account.address == first_addr) {
523 assert_eq!(account.balance, Wei::TEST_BALANCE);
524 }
525
526 let args = vec!["program", "--genesis-path", file_path];
528 let config = GenesisFileConfig::parse_from(args);
529 assert_eq!(config.genesis_path, Some(file_path.to_string()));
530
531 let genesis_from_clap = GenesisConfig::load_from_file(config.genesis_path.unwrap()).expect("Failed to load genesis.local.json via clap");
533
534 assert_eq!(genesis_from_clap.config.chainId, 2008);
536
537 unsafe {
539 env::set_var("GENESIS_JSON_PATH", file_path);
540 }
541 let args = vec!["program"]; let config = GenesisFileConfig::parse_from(args);
543 assert_eq!(config.genesis_path, Some(file_path.to_string()));
544
545 let genesis_from_env = GenesisConfig::load_from_file(config.genesis_path.unwrap()).expect("Failed to load genesis.local.json via env var");
547
548 assert_eq!(genesis_from_env.config.chainId, 2008);
550
551 unsafe {
553 env::remove_var("GENESIS_JSON_PATH");
554 }
555 }
556}