stratus/eth/
genesis.rs

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
25/// Type alias for a collection of storage slots in the format (address, slot)
26pub type GenesisSlots = Vec<(Address, Slot)>;
27
28/// Represents the configuration of an Ethereum genesis.json file
29#[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/// Chain configuration in genesis.json
52#[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/// Account in the genesis.json file
85#[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    /// Loads a genesis configuration from a file.
98    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    /// Converts the genesis configuration to Stratus accounts and storage slots.
106    ///
107    /// Returns a tuple containing:
108    /// - A vector of accounts with their balance, nonce, and bytecode
109    /// - A vector of storage slots in the format (address, slot_index, value)
110    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            // Remove 0x prefix if present
116            let addr_str = addr_str.trim_start_matches("0x");
117
118            // Use FromStr to convert the address
119            let address = Address::from_str(addr_str)?;
120
121            // Create the account
122            let mut account = Account::new_empty(address);
123
124            // Convert balance
125            let balance = if genesis_account.balance.starts_with("0x") {
126                // For hex strings
127                Wei::from_hex_str(&genesis_account.balance)?
128            } else {
129                // For decimal strings
130                let value = genesis_account.balance.parse::<u64>().unwrap_or(0);
131                Wei::from(U256::from(value))
132            };
133
134            account.balance = balance;
135
136            // Add nonce if it exists
137            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            // Add code if it exists
148            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            // Process storage slots if they exist
158            if let Some(storage) = &genesis_account.storage {
159                for (slot_key, slot_value) in storage {
160                    // Parse slot key and value in just two lines
161                    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                    // Add slot to the list
165                    slots.push((address, Slot::new(slot_index, slot_value)));
166                }
167            }
168
169            accounts.push(account);
170        }
171        Ok((accounts, slots))
172    }
173
174    /// Converts the genesis configuration to Stratus accounts.
175    ///
176    /// This is a convenience wrapper around to_stratus_accounts_and_slots that only returns the accounts.
177    pub fn to_stratus_accounts(&self) -> Result<Vec<Account>> {
178        let (accounts, _) = self.to_stratus_accounts_and_slots()?;
179        Ok(accounts)
180    }
181
182    /// Creates a genesis block from the genesis configuration.
183    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        // Parse timestamp
196        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        // Parse gas limit
204        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        // Parse gas used (if present)
212        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        // Parse nonce
224        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        // Parse coinbase/miner address
236        let miner = Address::from_str(&self.coinbase).unwrap_or_default();
237
238        // Parse extra data
239        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        // Parse parent hash (if present)
247        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        // Parse mix hash
263        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        // Parse difficulty
275        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        // Create a basic header
282        let mut header = BlockHeader::new(BlockNumber::ZERO, UnixTime::from(timestamp));
283
284        // Update with genesis.json values
285        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        // For nonce, we need to convert to B64 first
294        let nonce_b64 = B64::from_slice(&nonce_bytes);
295        header.nonce = nonce_b64.into();
296
297        // Create the block
298        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        // Default implementation for development environment
311        let mut alloc = BTreeMap::new();
312
313        // Add test accounts
314        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            // Add code if not empty
327            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        // Minimal default implementation for non-development environments
371        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        // Create a temporary file without using tempfile
414        let file_path = "test_genesis.json";
415
416        // Create a minimal genesis.json file
417        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        // Write the JSON to the file
438        let mut file = File::create(file_path).unwrap();
439        file.write_all(json.as_bytes()).unwrap();
440
441        // Load the genesis config from the file
442        let genesis = GenesisConfig::load_from_file(file_path).unwrap();
443
444        // Test address conversion
445        let addr_str = "f39fd6e51aad88f6f4ce6ab8827279cfffb92266";
446        let addr = Address::from_str(addr_str).unwrap();
447
448        // Convert accounts
449        let accounts = genesis.to_stratus_accounts().unwrap();
450
451        // Verify the account
452        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))); // 0x1000 in decimal
457
458        // Clean up
459        std::fs::remove_file(file_path).unwrap();
460    }
461
462    #[test]
463    fn test_default_genesis_config() {
464        // Test the default implementation
465        let default_genesis = GenesisConfig::default();
466
467        // Verify chain ID is set to 2008
468        assert_eq!(default_genesis.config.chainId, 2008);
469
470        #[cfg(feature = "dev")]
471        {
472            // In dev mode, we should have test accounts
473            let test_accounts = crate::eth::primitives::test_accounts();
474            let stratus_accounts = default_genesis.to_stratus_accounts().unwrap();
475
476            // Verify the number of accounts matches
477            assert_eq!(stratus_accounts.len(), test_accounts.len());
478
479            // Verify each account exists in the genesis config
480            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            // In non-dev mode, we should have an empty alloc
489            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        // Path to the local genesis file
502        let file_path = "config/genesis.local.json";
503
504        // Test 1: Direct file loading
505        let genesis = GenesisConfig::load_from_file(file_path).expect("Failed to load genesis.local.json");
506
507        // Verify basic properties
508        assert_eq!(genesis.config.chainId, 2008);
509
510        // Verify accounts
511        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        // Verify the first account
515        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        // Verify account balance
522        if let Some(account) = accounts.iter().find(|account| account.address == first_addr) {
523            assert_eq!(account.balance, Wei::TEST_BALANCE);
524        }
525
526        // Test 2: Clap integration - command line arguments
527        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        // Load the file using the path obtained via clap
532        let genesis_from_clap = GenesisConfig::load_from_file(config.genesis_path.unwrap()).expect("Failed to load genesis.local.json via clap");
533
534        // Verify that the file loaded via clap has the same chainId
535        assert_eq!(genesis_from_clap.config.chainId, 2008);
536
537        // Test 3: Clap integration - environment variables
538        unsafe {
539            env::set_var("GENESIS_JSON_PATH", file_path);
540        }
541        let args = vec!["program"]; // No command line arguments
542        let config = GenesisFileConfig::parse_from(args);
543        assert_eq!(config.genesis_path, Some(file_path.to_string()));
544
545        // Load the file using the path obtained via environment variable
546        let genesis_from_env = GenesisConfig::load_from_file(config.genesis_path.unwrap()).expect("Failed to load genesis.local.json via env var");
547
548        // Verify that the file loaded via environment variable has the same chainId
549        assert_eq!(genesis_from_env.config.chainId, 2008);
550
551        // Clean up
552        unsafe {
553            env::remove_var("GENESIS_JSON_PATH");
554        }
555    }
556}