diff --git a/crates/hive-utils/src/hive/genesis.rs b/crates/hive-utils/src/hive/genesis.rs index 75fc1490d..573b38483 100644 --- a/crates/hive-utils/src/hive/genesis.rs +++ b/crates/hive-utils/src/hive/genesis.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::fs; +use std::fs::File; use std::io::Error as IoError; use std::path::Path; @@ -51,7 +51,8 @@ pub struct HiveGenesisConfig { impl HiveGenesisConfig { pub fn from_file(path: &str) -> Result { - Ok(serde_json::from_str(&fs::read_to_string(path)?)?) + let hive_genesis_file = File::open(path).unwrap(); + Ok(serde_json::from_reader(hive_genesis_file).unwrap()) } } @@ -73,7 +74,7 @@ lazy_static! { pub async fn serialize_hive_to_madara_genesis_config( hive_genesis: HiveGenesisConfig, mut madara_loader: GenesisLoader, - combined_genesis: &Path, + combined_genesis_path: &Path, compiled_path: &Path, ) -> Result<(), IoError> { // Compute the class hash of Kakarot contracts @@ -224,10 +225,10 @@ pub async fn serialize_hive_to_madara_genesis_config( madara_loader.storage.push(is_initialized); }); - // Serialize the loader to a string - let madara_genesis_str = serde_json::to_string_pretty(&madara_loader)?; - // Write the string to a file - fs::write(combined_genesis, madara_genesis_str)?; + let combined_genesis_file = + File::options().create_new(true).write(true).append(true).open(combined_genesis_path).unwrap(); + // Serialize the loader to a string and then write to a file + serde_json::to_writer_pretty(combined_genesis_file, &madara_loader)?; Ok(()) } @@ -252,6 +253,7 @@ pub struct AccountInfo { #[cfg(test)] mod tests { + use std::fs::File; use std::str::FromStr; use reth_primitives::U256; @@ -303,21 +305,21 @@ mod tests { let hive_genesis = HiveGenesisConfig::from_file("./src/test_data/hive_genesis.json").unwrap(); let madara_loader = serde_json::from_str::(std::include_str!("../test_data/madara_genesis.json")).unwrap(); - let combined_genesis = Path::new("./src/test_data/combined_genesis.json"); + let combined_genesis_path = Path::new("./src/test_data/combined_genesis.json"); let compiled_path = Path::new("./cairo-contracts/build"); // When - serialize_hive_to_madara_genesis_config(hive_genesis, madara_loader, combined_genesis, compiled_path) + serialize_hive_to_madara_genesis_config(hive_genesis, madara_loader, combined_genesis_path, compiled_path) .await .unwrap(); + let combined_genesis_file = File::open(combined_genesis_path).unwrap(); + // Then - let combined_genesis = fs::read_to_string("./src/test_data/combined_genesis.json").unwrap(); - let loader: GenesisLoader = - serde_json::from_str(&combined_genesis).expect("Failed to read combined_genesis.json"); + let loader: GenesisLoader = serde_json::from_reader(combined_genesis_file).unwrap(); assert_eq!(9 + 3 + 7, loader.contracts.len()); // 9 original + 3 Kakarot contracts + 7 hive // After - fs::remove_file("./src/test_data/combined_genesis.json").unwrap(); + std::fs::remove_file(combined_genesis_path).unwrap(); } } diff --git a/crates/test-utils/src/bin/dump-katana.rs b/crates/test-utils/src/bin/dump-katana.rs index 914aff4b4..5469d7a99 100644 --- a/crates/test-utils/src/bin/dump-katana.rs +++ b/crates/test-utils/src/bin/dump-katana.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::fs::File; use ethers::abi::Token; use git2::{Repository, SubmoduleIgnore}; @@ -35,11 +36,11 @@ async fn main() { .dump_state() .expect("Failed to call dump_state on Katana state"); - let state = serde_json::to_string(&dump_state).expect("Failed to serialize state"); - // Dump the state std::fs::create_dir_all(".katana/").expect("Failed to create Kakata dump dir"); - std::fs::write(".katana/dump.json", state).expect("Failed to write dump to .katana/dump.json"); + let katana_dump_file = + File::options().create_new(true).read(true).write(true).append(true).open(".katana/dump.json").unwrap(); + serde_json::to_writer(katana_dump_file, &dump_state); let deployer_account = DeployerAccount { address: test_context.client().deployer_account().address(), diff --git a/crates/test-utils/src/deploy_helpers.rs b/crates/test-utils/src/deploy_helpers.rs index f78e84493..f0ab8d49c 100644 --- a/crates/test-utils/src/deploy_helpers.rs +++ b/crates/test-utils/src/deploy_helpers.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::fs::{self}; +use std::fs::{self, File}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -103,12 +103,10 @@ pub fn get_contract(filename: &str) -> CompactContractBytecode { let compiled_solidity_path = std::path::Path::new(&foundry_default_out).join(dot_sol).join(dot_json); let compiled_solidity_path_from_root = root_project_path!(&compiled_solidity_path); - // Read the content of the file - let contents = fs::read_to_string(compiled_solidity_path_from_root).unwrap_or_else(|_| { + let compiled_solidity_file = File::open(compiled_solidity_path_from_root).unwrap_or_else(|_| { panic!("Could not read file: {}. please run `make setup` to ensure solidity files are compiled", filename) }); - - serde_json::from_str(&contents).unwrap() + serde_json::from_reader(compiled_solidity_file).unwrap() } /// Encodes a contract's bytecode and constructor arguments into deployable bytecode. @@ -773,11 +771,12 @@ impl KakarotTestEnvironmentContext { let sequencer = construct_kakarot_test_sequencer().await; // Get root path - let mut dir = root_project_path!(".katana/dump.json"); + let mut katana_dump_path = root_project_path!(".katana/dump.json"); // Load the dumped state into the sequencer - let state = std::fs::read_to_string(dir.clone()).expect("Failed to read Katana dump"); - let state: SerializableState = serde_json::from_str(&state).expect("Failed to deserialize Katana dump"); + let katana_dump_file = File::open(&katana_dump_path).unwrap(); + let state: SerializableState = + serde_json::from_reader(katana_dump_file).expect("Failed to deserialize Katana dump"); let mut sequencer_state = sequencer.sequencer.backend.state.write().await; sequencer_state.load_state(state).expect("Failed to load dumped state into sequencer"); @@ -787,9 +786,9 @@ impl KakarotTestEnvironmentContext { sequencer.sequencer.backend.mine_empty_block().await; // Load the dumped contracts - dir.pop(); - dir.push("contracts.json"); - let contracts = std::fs::read(dir).expect("Failed to read contracts"); + katana_dump_path.pop(); + katana_dump_path.push("contracts.json"); + let contracts = std::fs::read(katana_dump_path).expect("Failed to read contracts"); let contracts: HashMap<&str, serde_json::Value> = serde_json::from_slice(&contracts).expect("Failed to deserialize contracts"); diff --git a/crates/test-utils/src/hive_utils/hive/genesis.rs b/crates/test-utils/src/hive_utils/hive/genesis.rs new file mode 100644 index 000000000..b58fa94b8 --- /dev/null +++ b/crates/test-utils/src/hive_utils/hive/genesis.rs @@ -0,0 +1,323 @@ +use std::collections::HashMap; +use std::fs; +use std::io::Error as IoError; +use std::path::Path; + +use eyre::Result; +use kakarot_rpc_core::client::constants::STARKNET_NATIVE_TOKEN; +use kakarot_rpc_core::models::felt::Felt252Wrapper; +use lazy_static::lazy_static; +use reth_primitives::{Address, Bytes, H256, U256, U64}; +use serde::{Deserialize, Serialize}; +use starknet::core::types::FieldElement; + +use crate::deploy_helpers::compute_kakarot_contracts_class_hash; +use crate::hive_utils::kakarot::compute_starknet_address; +use crate::hive_utils::madara::utils::{ + genesis_approve_kakarot, genesis_fund_starknet_address, genesis_set_bytecode, + genesis_set_storage_kakarot_contract_account, genesis_set_storage_starknet_contract, +}; +use crate::hive_utils::types::{ClassHash, ContractAddress, ContractStorageKey, Felt, StorageValue}; + +#[derive(Deserialize, Serialize)] +pub struct GenesisLoader { + pub madara_path: Option, + pub contract_classes: Vec<(ClassHash, ContractClassPath)>, + pub contracts: Vec<(ContractAddress, ClassHash)>, + pub storage: Vec<(ContractStorageKey, StorageValue)>, + pub fee_token_address: ContractAddress, + pub seq_addr_updated: bool, +} + +#[derive(Deserialize, Serialize)] +pub struct ContractClassPath { + pub path: String, + pub version: u8, +} + +/// Types from https://github.com/ethereum/go-ethereum/blob/master/core/genesis.go#L49C1-L58 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HiveGenesisConfig { + pub config: Config, + pub coinbase: Address, + pub difficulty: U64, + pub extra_data: Bytes, + pub gas_limit: U64, + pub nonce: U64, + pub timestamp: U64, + pub alloc: HashMap, +} + +impl HiveGenesisConfig { + pub fn from_file(path: &str) -> Result { + let hive_genesis_file = File::open(path)?; + Ok(serde_json::from_reader(&hive_genesis_file)?) + } +} + +// Define constant addresses for Kakarot contracts +lazy_static! { + pub static ref KAKAROT_ADDRESS: FieldElement = FieldElement::from_hex_be("0x9001").unwrap(); // Safe unwrap, 0x9001 + pub static ref BLOCKHASH_REGISTRY_ADDRESS: FieldElement = FieldElement::from_hex_be("0x9002").unwrap(); // Safe unwrap, 0x9002 + pub static ref DEPLOYER_ACCOUNT_ADDRESS: FieldElement = FieldElement::from_hex_be("0x9003").unwrap(); // Safe unwrap, 0x9003 +} + +/// Convert Hive Genesis Config to Madara Genesis Config +/// +/// This function will: +/// 1. Load the Madara genesis file +/// 2. Compute the class hash of Kakarot contracts +/// 3. Add Kakarot contracts to Loader +/// 4. Add Hive accounts to Loader (fund, storage, bytecode, proxy implementation) +/// 5. Serialize Loader to Madara genesis file +pub async fn serialize_hive_to_madara_genesis_config( + hive_genesis: HiveGenesisConfig, + mut madara_loader: GenesisLoader, + combined_genesis: &Path, + compiled_path: &Path, +) -> Result<(), IoError> { + // Compute the class hash of Kakarot contracts + let class_hashes = compute_kakarot_contracts_class_hash(); + + // { contract : class_hash } + let mut kakarot_contracts = HashMap::::new(); + + // Add Kakarot contracts Contract Classes to loader + // Vec so no need to sort + class_hashes.iter().for_each(|(filename, class_hash)| { + madara_loader.contract_classes.push(( + Felt(*class_hash), + ContractClassPath { + // Add the compiled path to the Kakarot contract filename + path: compiled_path.join(filename).with_extension("json").into_os_string().into_string().unwrap(), /* safe unwrap, + * valid path */ + version: 0, + }, + )); + + // Add Kakarot contracts {contract : class_hash} to Kakarot Contracts HashMap + // Remove .json from filename to get contract name + kakarot_contracts.insert(filename.to_string(), *class_hash); + }); + + // Set the Kakarot contracts address and proxy class hash + let account_proxy_class_hash = *kakarot_contracts.get("proxy").expect("Failed to get proxy class hash"); + let contract_account_class_hash = + *kakarot_contracts.get("contract_account").expect("Failed to get contract_account class hash"); + let eoa_class_hash = *kakarot_contracts.get("externally_owned_account").expect("Failed to get eoa class hash"); + + // Add Kakarot contracts to Loader + madara_loader.contracts.push(( + Felt(*KAKAROT_ADDRESS), + Felt(*kakarot_contracts.get("kakarot").expect("Failed to get kakarot class hash")), + )); + madara_loader.contracts.push(( + Felt(*BLOCKHASH_REGISTRY_ADDRESS), + Felt(*kakarot_contracts.get("blockhash_registry").expect("Failed to get blockhash_registry class hash")), + )); + madara_loader.contracts.push(( + Felt(*DEPLOYER_ACCOUNT_ADDRESS), + Felt(*kakarot_contracts.get("OpenzeppelinAccount").expect("Failed to get deployer account class hash")), + )); + + // Set storage keys of Kakarot contract + // https://github.com/kkrt-labs/kakarot/blob/main/src/kakarot/constants.cairo + let storage_keys = [ + ("native_token_address", FieldElement::from_hex_be(STARKNET_NATIVE_TOKEN).unwrap()), + ("contract_account_class_hash", contract_account_class_hash), + ("externally_owned_account", eoa_class_hash), + ("account_proxy_class_hash", account_proxy_class_hash), + ("blockhash_registry_address", *BLOCKHASH_REGISTRY_ADDRESS), + // TODO: Use DEPLOY_FEE constant https://github.com/kkrt-labs/kakarot-rpc/pull/431/files#diff-88f745498d0aaf0b185085d99a74f0feaf253f047babc85770847931e7f726c3R125 + ("deploy_fee", FieldElement::from(100000_u64)), + ]; + + storage_keys.into_iter().for_each(|(key, value)| { + let storage = genesis_set_storage_starknet_contract(*KAKAROT_ADDRESS, key, &[], value, 0); + madara_loader.storage.push(storage); + }); + + // Add Hive accounts to loader + // Convert the EVM accounts to Starknet accounts using compute_starknet_address + // Sort by key to ensure deterministic order + let mut hive_accounts: Vec<(reth_primitives::H160, AccountInfo)> = hive_genesis.alloc.into_iter().collect(); + hive_accounts.sort_by_key(|(address, _)| *address); + hive_accounts.into_iter().for_each(|(evm_address, account_info)| { + // Use the given Kakarot contract address and declared proxy class hash for compute_starknet_address + let starknet_address = compute_starknet_address( + *KAKAROT_ADDRESS, + account_proxy_class_hash, + FieldElement::from_byte_slice_be(evm_address.as_bytes()).unwrap(), /* safe unwrap since evm_address + * is 20 bytes */ + ); + // Push to contracts + madara_loader.contracts.push((Felt(starknet_address), Felt(account_proxy_class_hash))); + + // Set the balance of the account and approve Kakarot for infinite allowance + // Call genesis_fund_starknet_address util to get the storage tuples + let balances = genesis_fund_starknet_address(starknet_address, account_info.balance); + let allowance = genesis_approve_kakarot(starknet_address, *KAKAROT_ADDRESS, U256::MAX); + balances.into_iter().zip(allowance.into_iter()).for_each(|(balance, allowance)| { + madara_loader.storage.push(balance); + madara_loader.storage.push(allowance); + }); + + // Set the storage of the account, if any + if let Some(storage) = account_info.storage { + let mut storage: Vec<(U256, U256)> = storage.into_iter().collect(); + storage.sort_by_key(|(key, _)| *key); + storage.into_iter().for_each(|(key, value)| { + // Call genesis_set_storage_kakarot_contract_account util to get the storage tuples + let storages = genesis_set_storage_kakarot_contract_account(starknet_address, key, value); + storages.into_iter().for_each(|storage| { + madara_loader.storage.push(storage); + }); + }); + } + + // Determine the proxy implementation class hash based on whether bytecode is present + // Set the bytecode to the storage of the account, if any + let proxy_implementation_class_hash = if let Some(bytecode) = account_info.code { + let bytecode_len = + genesis_set_storage_starknet_contract(starknet_address, "bytecode_len_", &[], bytecode.len().into(), 0); + let bytecode = genesis_set_bytecode(&bytecode, starknet_address); + + // Set the bytecode of the account + madara_loader.storage.extend(bytecode); + // Set the bytecode length of the account + madara_loader.storage.push(bytecode_len); + + // Set the Owner + let owner = + genesis_set_storage_starknet_contract(starknet_address, "Ownable_owner", &[], *KAKAROT_ADDRESS, 0); + madara_loader.storage.push(owner); + + // Since it has bytecode, it's a contract account + contract_account_class_hash + } else { + // Set kakarot address + let kakarot_address = + genesis_set_storage_starknet_contract(starknet_address, "kakarot_address", &[], *KAKAROT_ADDRESS, 0); + madara_loader.storage.push(kakarot_address); + + // Since it has no bytecode, it's an externally owned account + eoa_class_hash + }; + + // Set the proxy implementation of the account to the determined class hash + let proxy_implementation_storage = genesis_set_storage_starknet_contract( + starknet_address, + "_implementation", + &[], + proxy_implementation_class_hash, + 0, // 0 since it's storage value is felt + ); + madara_loader.storage.push(proxy_implementation_storage); + + // Set the evm address of the account and the "is_initialized" flag + let evm_address: Felt252Wrapper = evm_address.into(); + let evm_address = + genesis_set_storage_starknet_contract(starknet_address, "evm_address", &[], evm_address.into(), 0); + let is_initialized = + genesis_set_storage_starknet_contract(starknet_address, "is_initialized_", &[], FieldElement::ONE, 0); + madara_loader.storage.push(evm_address); + madara_loader.storage.push(is_initialized); + }); + + // Serilaize the loader to a string and then to a file + let combined_genesis_file= File::new(combined_genesis); + fs::write(combined_genesis, serde_json::to_string_pretty(&madara_loader)?)?; + + Ok(()) +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Config { + pub chain_id: i128, + pub homestead_block: i128, + pub eip150_block: i128, + pub eip150_hash: H256, + pub eip155_block: i128, + pub eip158_block: i128, +} + +#[derive(Serialize, Deserialize)] +pub struct AccountInfo { + pub balance: U256, + pub code: Option, + pub storage: Option>, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use reth_primitives::U256; + + use super::*; + + #[test] + fn test_read_hive_genesis() { + // Read the hive genesis file + let genesis = HiveGenesisConfig::from_file("./src/hive_utils/test_data/hive_genesis.json").unwrap(); + + // Verify the genesis file has the expected number of accounts + assert_eq!(genesis.alloc.len(), 7); + + // Verify balance of each account is not empty + assert!(genesis.alloc.values().all(|account_info| account_info.balance >= U256::from(0))); + + // Verify the storage field for each account + // Since there is only one account with non-empty storage, we can hardcode the expected values + assert!(genesis.alloc.values().all(|account_info| { + account_info.storage.as_ref().map_or(true, |storage| { + storage.len() == 2 + && *storage + .get( + &U256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(), + ) + .unwrap() + == U256::from_str("0x1234").unwrap() + && *storage + .get( + &U256::from_str("0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9") + .unwrap(), + ) + .unwrap() + == U256::from_str("0x01").unwrap() + }) + })); + + // Verify the code field for each account, if exists, is not empty + assert!( + genesis.alloc.values().all(|account_info| account_info.code.as_ref().map_or(true, |code| !code.is_empty())) + ); + } + + #[tokio::test] + async fn test_madara_genesis() { + // Given + let hive_genesis = HiveGenesisConfig::from_file("./src/hive_utils/test_data/hive_genesis.json").unwrap(); + let madara_loader = + serde_json::from_str::(std::include_str!("../test_data/madara_genesis.json")).unwrap(); + let combined_genesis_path = Path::new("./src/hive_utils/test_data/combined_genesis.json"); + let compiled_path = Path::new("./cairo-contracts/build"); + + // When + serialize_hive_to_madara_genesis_config(hive_genesis, madara_loader, combined_genesis_path, compiled_path) + .await + .unwrap(); + + // Then + let combined_genesis_file = File::open(combined_genesis_path).unwrap(); + let loader: GenesisLoader = + serde_json::from_reader(&combined_genesis_file).expect("Failed to read combined_genesis.json"); + assert_eq!(9 + 3 + 7, loader.contracts.len()); // 9 original + 3 Kakarot contracts + 7 hive + + // After + fs::remove_file(combined_genesis_path).unwrap(); + } +}