Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Ethereum genesis.json initialization #2034

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

pedro-pelicioni-cw
Copy link
Contributor

@pedro-pelicioni-cw pedro-pelicioni-cw commented Mar 6, 2025

User description

This PR adds support for initializing Stratus from a standard Ethereum genesis.json file.

Features

  • Parse and load accounts from genesis.json
  • Support for EOA accounts with balance and nonce
  • Basic structure for contract accounts with code and storage
  • Automatic detection of genesis.json in the execution directory

Implementation Details

  • Added new module src/eth/genesis.rs with structures to parse genesis.json
  • Integrated with existing reset_to_genesis functionality in stratus_storage.rs
  • Added support for hex and decimal balance parsing
  • Maintained camelCase field names for compatibility with standard Ethereum genesis format

Limitations and Future Work

  • Contract code loading is implemented but not fully working yet
  • Contract storage is processed but not applied to the state
  • These limitations will be addressed in future PRs

Usage

Place a genesis.json file in the Stratus execution directory. When Stratus starts with the dev feature enabled, it will automatically detect and use this file to initialize the blockchain state.

Testing

An example genesis.json file is included in the PR. To test:

  1. Place the genesis.json in the execution directory
  2. Run just stratus to start Stratus with dev features
  3. Verify account creation with: curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "latest"],"id":1}' http://localhost:3000

PR Type

Enhancement


Description

  • Add support for Ethereum genesis.json initialization

  • Parse and load accounts from genesis.json

  • Support EOA and contract accounts

  • Integrate with existing reset_to_genesis functionality


Changes walkthrough 📝

Relevant files
Enhancement
genesis.rs
Implement genesis.json parsing and account conversion       

src/eth/genesis.rs

  • Implement GenesisConfig struct for parsing genesis.json
  • Add methods to load and find genesis file
  • Convert genesis accounts to Stratus internal format
  • Implement hex string conversion for internal types
  • +320/-0 
    mod.rs
    Add genesis module to eth namespace                                           

    src/eth/mod.rs

    • Add 'genesis' module to eth namespace
    +1/-0     
    stratus_storage.rs
    Integrate genesis.json with reset_to_genesis function       

    src/eth/storage/stratus_storage.rs

  • Modify reset_to_genesis to use genesis.json if available
  • Fall back to test accounts if genesis.json is not found
  • Add error handling for genesis file loading and parsing
  • +31/-5   
    Configuration changes
    genesis.json
    Add example genesis.json file                                                       

    genesis.json

  • Add example genesis.json file with chain configuration
  • Include pre-funded accounts and a contract account
  • Set initial parameters like gasLimit, difficulty, etc.
  • +44/-0   

    Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • This PR adds support for initializing Stratus from a standard Ethereum genesis.json file.
    
    ## Features
    
    - Parse and load accounts from genesis.json
    - Support for EOA accounts with balance and nonce
    - Basic structure for contract accounts with code and storage
    - Automatic detection of genesis.json in the execution directory
    
    ## Implementation Details
    
    - Added new module `src/eth/genesis.rs` with structures to parse genesis.json
    - Integrated with existing reset_to_genesis functionality in stratus_storage.rs
    - Added support for hex and decimal balance parsing
    - Maintained camelCase field names for compatibility with standard Ethereum genesis format
    
    ## Limitations and Future Work
    
    - Contract code loading is implemented but not fully working yet
    - Contract storage is processed but not applied to the state
    - These limitations will be addressed in future PRs
    
    ## Usage
    
    Place a genesis.json file in the Stratus execution directory. When Stratus starts
    with the `dev` feature enabled, it will automatically detect and use this file
    to initialize the blockchain state.
    
    ## Testing
    
    An example genesis.json file is included in the PR. To test:
    1. Place the genesis.json in the execution directory
    2. Run `just stratus` to start Stratus with dev features
    3. Verify account creation with:
       curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "latest"],"id":1}' http://localhost:3000
    Copy link

    github-actions bot commented Mar 6, 2025

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The error handling in the reset_to_genesis function could be improved. Currently, if there's an error loading or parsing the genesis.json file, it falls back to using test accounts without proper error propagation.

    // Try to load genesis.json
    let genesis_accounts = if let Some(genesis_path) = GenesisConfig::find_genesis_file(None) {
        tracing::info!("Found genesis.json file at {:?}, using it for initialization", genesis_path);
        match GenesisConfig::load_from_file(genesis_path) {
            Ok(genesis_config) => match genesis_config.to_stratus_accounts() {
                Ok(accounts) => {
                    tracing::info!("Loaded {} accounts from genesis.json", accounts.len());
                    accounts
                }
                Err(e) => {
                    tracing::error!("Failed to convert genesis accounts: {:?}", e);
                    test_accounts()
                }
            },
            Err(e) => {
                tracing::error!("Failed to load genesis.json: {:?}", e);
                test_accounts()
            }
        }
    } else {
        tracing::info!("No genesis.json file found, using default test accounts");
        test_accounts()
    };
    Tracing Improvement

    The tracing events in the to_stratus_accounts function could be enhanced to use structured logging with fields instead of formatted strings.

    pub fn to_stratus_accounts(&self) -> Result<Vec<Account>> {
        let mut accounts = Vec::new();
        let mut slots_to_add = Vec::new();
    
        for (addr_str, genesis_account) in &self.alloc {
            tracing::debug!("Processing account: {}", addr_str);
    
            let address = Address::from_str_hex(addr_str.trim_start_matches("0x"))?;
    
            // Convert balance from hex string to Wei
            let balance_str = genesis_account.balance.trim_start_matches("0x");
            let balance = if genesis_account.balance.starts_with("0x") {
                // For hex strings, use our custom implementation
                let bytes = hex::decode(balance_str)?;
                let mut array = [0u8; 32];
                let start = array.len() - bytes.len();
                array[start..].copy_from_slice(&bytes);
                let ethereum_u256 = EthereumU256::from_big_endian(&array);
                Wei::from(ethereum_u256)
            } else {
                // For decimal strings, use from_dec_str
                Wei::from(EthereumU256::from_dec_str(balance_str).map_err(|e| anyhow!("Failed to parse balance: {}", e))?)
            };
            tracing::debug!("Account balance: {}", balance.0);
    
            // Create the account
            let mut account = Account::new_empty(address);
            account.balance = balance;
    
            // Add code if it exists
            if let Some(code) = &genesis_account.code {
                let code_bytes = hex::decode(code.trim_start_matches("0x"))?;
                tracing::debug!("Added bytecode of length: {}", code_bytes.len());
                account.bytecode = Some(Bytecode::new_raw(code_bytes.into()));
            }
    
            // Add nonce if it exists
            if let Some(nonce) = &genesis_account.nonce {
                let nonce_str = nonce.trim_start_matches("0x");
                let nonce_value = if nonce_str.starts_with("0x") {
                    // For hex strings, use from_str_radix with base 16
                    let u256 = EthereumU256::from_str_radix(nonce_str.trim_start_matches("0x"), 16).map_err(|e| anyhow!("Failed to parse nonce: {}", e))?;
                    u256.as_u64()
                } else {
                    nonce_str.parse::<u64>()?
                };
                account.nonce = Nonce::from(nonce_value);
                tracing::debug!("Account nonce: {}", nonce_value);
            }
    
            // Add storage if it exists
            if let Some(storage) = &genesis_account.storage {
                tracing::debug!("Account has {} storage entries", storage.len());
                for (key_str, value_str) in storage {
                    // Convert key and value from hex string to U256
                    let key_str = key_str.trim_start_matches("0x");
                    let value_str = value_str.trim_start_matches("0x");
    
                    // Use AlloyU256 for initial parsing
                    let key_alloy = AlloyU256::from_str_radix(key_str, 16).map_err(|e| anyhow!("Failed to parse storage key: {}", e))?;
                    let value_alloy = AlloyU256::from_str_radix(value_str, 16).map_err(|e| anyhow!("Failed to parse storage value: {}", e))?;
    
                    // Convert from alloy_primitives::U256 to ethereum_types::U256
                    let key_bytes = key_alloy.to_be_bytes::<32>();
                    let key_ethereum = EthereumU256::from_big_endian(&key_bytes);
    
                    let value_bytes = value_alloy.to_be_bytes::<32>();
                    let value_ethereum = EthereumU256::from_big_endian(&value_bytes);
    
                    // Create a SlotIndex from the key
                    let slot_index = SlotIndex(key_ethereum);
    
                    // Create a Slot with the value
                    let slot_value = SlotValue(value_ethereum);
                    let slot = Slot::new(slot_index, slot_value);
    
                    // Add the slot to the account
                    tracing::debug!("Added storage slot: key={:?}, value={:?}", key_alloy, value_alloy);
    
                    // For now, just store the slots in a temporary vector
                    // TODO: Implement proper storage handling in a future PR
                    slots_to_add.push((address, slot));
                }
            }
    
            accounts.push(account);
        }
    
        Ok(accounts)
    }
    Performance Concern

    The conversion of hex strings to U256 values in the to_stratus_accounts function involves multiple conversions and could be optimized for better performance.

    // Convert balance from hex string to Wei
    let balance_str = genesis_account.balance.trim_start_matches("0x");
    let balance = if genesis_account.balance.starts_with("0x") {
        // For hex strings, use our custom implementation
        let bytes = hex::decode(balance_str)?;
        let mut array = [0u8; 32];
        let start = array.len() - bytes.len();
        array[start..].copy_from_slice(&bytes);
        let ethereum_u256 = EthereumU256::from_big_endian(&array);
        Wei::from(ethereum_u256)
    } else {
        // For decimal strings, use from_dec_str
        Wei::from(EthereumU256::from_dec_str(balance_str).map_err(|e| anyhow!("Failed to parse balance: {}", e))?)
    };

    Copy link

    github-actions bot commented Mar 6, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix nonce parsing bug

    The nonce parsing logic contains a bug. The condition nonce_str.starts_with("0x")
    should be nonce.starts_with("0x") to correctly identify hexadecimal inputs. Also,
    consider refactoring this logic into a separate function for better maintainability.

    src/eth/genesis.rs [157-168]

    +fn parse_nonce(nonce: &str) -> Result<u64> {
    +    let nonce_str = nonce.trim_start_matches("0x");
    +    if nonce.starts_with("0x") {
    +        EthereumU256::from_str_radix(nonce_str, 16)
    +            .map_err(|e| anyhow!("Failed to parse nonce: {}", e))
    +            .map(|u256| u256.as_u64())
    +    } else {
    +        nonce_str.parse::<u64>().map_err(|e| anyhow!("Failed to parse nonce: {}", e))
    +    }
    +}
    +
    +// Usage
     if let Some(nonce) = &genesis_account.nonce {
    -    let nonce_str = nonce.trim_start_matches("0x");
    -    let nonce_value = if nonce_str.starts_with("0x") {
    -        // For hex strings, use from_str_radix with base 16
    -        let u256 = EthereumU256::from_str_radix(nonce_str.trim_start_matches("0x"), 16).map_err(|e| anyhow!("Failed to parse nonce: {}", e))?;
    -        u256.as_u64()
    -    } else {
    -        nonce_str.parse::<u64>()?
    -    };
    +    let nonce_value = parse_nonce(nonce)?;
         account.nonce = Nonce::from(nonce_value);
         tracing::debug!("Account nonce: {}", nonce_value);
     }
    Suggestion importance[1-10]: 9

    __

    Why: This suggestion fixes a critical bug in the nonce parsing logic and improves the code structure. The bug could lead to incorrect nonce values, which is a significant issue. The refactoring also enhances code maintainability and readability.

    High
    General
    Refactor balance parsing for clarity

    Refactor the balance parsing logic into a separate function to improve code
    readability and reusability. This function should handle both hexadecimal and
    decimal string inputs, reducing duplication and potential inconsistencies.

    src/eth/genesis.rs [130-142]

    -let balance = if genesis_account.balance.starts_with("0x") {
    -    // For hex strings, use our custom implementation
    -    let bytes = hex::decode(balance_str)?;
    -    let mut array = [0u8; 32];
    -    let start = array.len() - bytes.len();
    -    array[start..].copy_from_slice(&bytes);
    -    let ethereum_u256 = EthereumU256::from_big_endian(&array);
    -    Wei::from(ethereum_u256)
    -} else {
    -    // For decimal strings, use from_dec_str
    -    Wei::from(EthereumU256::from_dec_str(balance_str).map_err(|e| anyhow!("Failed to parse balance: {}", e))?)
    -};
    +fn parse_balance(balance: &str) -> Result<Wei> {
    +    let balance_str = balance.trim_start_matches("0x");
    +    if balance.starts_with("0x") {
    +        let bytes = hex::decode(balance_str)?;
    +        let mut array = [0u8; 32];
    +        let start = array.len() - bytes.len();
    +        array[start..].copy_from_slice(&bytes);
    +        Ok(Wei::from(EthereumU256::from_big_endian(&array)))
    +    } else {
    +        Ok(Wei::from(EthereumU256::from_dec_str(balance_str).map_err(|e| anyhow!("Failed to parse balance: {}", e))?))
    +    }
    +}
     
    +// Usage
    +let balance = parse_balance(&genesis_account.balance)?;
    +
    Suggestion importance[1-10]: 7

    __

    Why: This suggestion improves code readability and maintainability by extracting the balance parsing logic into a separate function. It reduces code duplication and centralizes the parsing logic, making it easier to maintain and update in the future.

    Medium

    - Enhance genesis file loading with environment-specific and local config support
    - Improve error handling and hex string parsing for addresses and balances
    - Simplify account conversion logic with more robust parsing
    - Add test case for minimal genesis file loading
    - Remove unused imports and clean up code structure
    - Update storage reset to use improved genesis loading mechanism
    Comment on lines 457 to 458
    /// If a genesis.json file is available, it will be used.
    /// Otherwise, it will use the hardcoded genesis block and test accounts.
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I think we should remove the hardcoded stuff and leave all this for the genesis.json. This would help validate the PR against all E2E tests as well

    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    I think this is fine, although with the introduction of genesis.json, this stuff is not really "hardcoded" as much as it is a default value. What I do think you should do is implement Default for GenesisConfig and use that (the implementations should be different depending on weather the dev feature flag is set).

    Comment on lines 98 to 128
    pub fn find_genesis_file(custom_path: Option<&str>) -> Option<PathBuf> {
    if let Some(path) = custom_path {
    let path_buf = PathBuf::from(path);
    if path_buf.exists() {
    return Some(path_buf);
    }
    }

    // Check for environment-specific genesis file
    if let Ok(env) = std::env::var("ENV") {
    let env_path = format!("config/genesis.{}.json", env.to_lowercase());
    let env_path_buf = PathBuf::from(&env_path);
    if env_path_buf.exists() {
    return Some(env_path_buf);
    }
    }

    // Check for default local genesis file
    let local_path = PathBuf::from("config/genesis.local.json");
    if local_path.exists() {
    return Some(local_path);
    }

    // Fallback to current directory
    let default_path = PathBuf::from("genesis.json");
    if default_path.exists() {
    return Some(default_path);
    }

    None
    }
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Maybe we should accept an ENV VAR for this genesis.json path. Something like GENESIS_JSON_PATH. And not use ENV related names

    Copy link
    Contributor

    @carneiro-cw carneiro-cw Mar 7, 2025

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Define this type of configuration in src/config.rs (using clap)

    fn from_str_hex(hex: &str) -> Result<Self>;
    }

    impl FromStrHex for Address {
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Implement traits for primitive types in its declaration file (stratus/src/eth/primitives/address.rs). Also, this conversion already exists, (although I guess we should have a docstring explaining that the FromStr implementation for Address converts from a hex string).

    @carneiro-cw carneiro-cw changed the title Add support for Ethereum genesis.json initialization feat: add support for Ethereum genesis.json initialization Mar 7, 2025
    … default setup
    
    - Add GenesisFileConfig to support genesis.json path configuration via CLI and environment variable
    - Improve genesis file discovery in GenesisConfig with environment variable support
    - Implement default genesis configuration for dev and non-dev environments
    - Update Address parsing to use more robust hex decoding
    - Refactor genesis loading in StratusStorage to use default genesis config when no file is found
    - Add comprehensive tests for genesis configuration and path resolution
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    None yet
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    3 participants