diff --git a/Cargo.lock b/Cargo.lock index cf83eee..1b7a377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5150,6 +5150,8 @@ dependencies = [ "itertools 0.14.0", "keccak-hash", "log", + "rand 0.9.1", + "rayon", "rex-sdk", "secp256k1", "serde", @@ -5183,6 +5185,7 @@ dependencies = [ "keccak-hash", "log", "rand 0.8.5", + "rayon", "reqwest 0.12.20", "secp256k1", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1f7ff86..8d5caf7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,6 +33,8 @@ hex.workspace = true itertools = "0.14.0" toml = "0.8.19" dirs = "6.0.0" +rand = "0.9.1" +rayon = "1.10.0" # Logging log = "0.4" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 4112116..24c49e3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,5 +1,5 @@ use crate::commands::l2; -use crate::utils::{parse_contract_creation, parse_func_call, parse_hex}; +use crate::utils::{parse_contract_creation, parse_func_call, parse_hex, parse_hex_string}; use crate::{ commands::autocomplete, common::{CallArgs, DeployArgs, SendArgs, TransferArgs}, @@ -10,15 +10,19 @@ use ethrex_common::{Address, Bytes, H256, H520}; use keccak_hash::keccak; use rex_sdk::calldata::{Value, decode_calldata}; use rex_sdk::client::eth::BlockByNumber; -use rex_sdk::create::compute_create_address; +use rex_sdk::create::{ + DETERMINISTIC_DEPLOYER, brute_force_create2_rayon, compute_create_address, + compute_create2_address, +}; use rex_sdk::sign::{get_address_from_message_and_signature, sign_hash}; +use rex_sdk::utils::to_checksum_address; use rex_sdk::{ balance_in_eth, client::{EthClient, Overrides, eth::get_address_from_secret_key}, transfer, wait_for_transaction_receipt, }; - use secp256k1::SecretKey; +use std::io::{self, Write}; pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); @@ -111,12 +115,73 @@ pub(crate) enum Command { #[clap(about = "Compute contract address given the deployer address and nonce.")] CreateAddress { #[arg(help = "Deployer address.")] - address: Address, + deployer: Address, #[arg(short = 'n', long, help = "Deployer Nonce. Latest by default.")] nonce: Option, #[arg(long, default_value = "http://localhost:8545", env = "RPC_URL")] rpc_url: String, }, + Create2Address { + #[arg( + short = 'd', + long, + help = "Deployer address. Default is Mainnet Deterministic Deployer", + default_value = DETERMINISTIC_DEPLOYER + )] + deployer: Address, + #[arg( + short = 'i', + long, + help = "Initcode of the contract to deploy.", + required_unless_present_any = ["init_code_hash"], + conflicts_with_all = ["init_code_hash"] + )] + init_code: Option, + #[arg( + long, + help = "Hash of the initcode (keccak256).", + required_unless_present_any = ["init_code"], + conflicts_with_all = ["init_code"] + )] + init_code_hash: Option, + #[arg(short = 's', long, help = "Salt for CREATE2 opcode")] + salt: Option, + #[arg( + long, + required_unless_present_any = ["salt", "ends", "contains"], + help = "Address must begin with this hex prefix.", + value_parser = parse_hex_string, + )] + begins: Option, + #[arg( + long, + required_unless_present_any = ["salt", "begins", "contains"], + help = "Address must end with this hex suffix.", + value_parser = parse_hex_string, + )] + ends: Option, + #[arg( + long, + required_unless_present_any = ["salt", "begins", "ends"], + help = "Address must contain this hex substring.", + value_parser = parse_hex_string, + )] + contains: Option, + #[arg( + long, + help = "Make the address search case sensitive when using begins, ends, or contains.", + default_value_t = false, + conflicts_with_all = ["salt"], + )] + case_sensitive: bool, + #[arg( + long, + help = "Number of threads to use for brute-forcing. Defaults to the number of logical CPUs.", + default_value_t = rayon::current_num_threads(), + conflicts_with_all = ["salt"], + )] + threads: usize, + }, #[clap(about = "Deploy a contract")] Deploy { #[clap(flatten)] @@ -237,17 +302,63 @@ impl Command { println!("{block_number}"); } Command::CreateAddress { - address, + deployer, nonce, rpc_url, } => { let nonce = nonce.unwrap_or( EthClient::new(&rpc_url)? - .get_nonce(address, BlockByNumber::Latest) + .get_nonce(deployer, BlockByNumber::Latest) .await?, ); - println!("0x{:x}", compute_create_address(address, nonce)) + println!("Address: {:#x}", compute_create_address(deployer, nonce)) + } + Command::Create2Address { + deployer, + init_code, + salt, + init_code_hash, + begins, + ends, + contains, + case_sensitive, + threads, + } => { + let init_code_hash = init_code_hash + .or_else(|| init_code.as_ref().map(keccak)) + .ok_or_else(|| eyre::eyre!("init_code_hash and init_code are both None"))?; + + let (salt, contract_address) = match salt { + Some(salt) => { + let contract_address = + compute_create2_address(deployer, init_code_hash, salt); + (salt, contract_address) + } + None => { + // If salt is not provided, search for a salt that matches the criteria set by the user. + println!("\nComputing Create2 Address with {threads} threads..."); + io::stdout().flush().ok(); + + let start = std::time::Instant::now(); + let (salt, contract_address) = brute_force_create2_rayon( + deployer, + init_code_hash, + begins, + ends, + contains, + case_sensitive, + ); + let duration = start.elapsed(); + println!("Generated in: {:.2?}.", duration); + (salt, contract_address) + } + }; + + let contract_address = to_checksum_address(&format!("{contract_address:x}")); + + println!("\nSalt: {salt:#x}"); + println!("\nAddress: 0x{contract_address}"); } Command::Transaction { tx_hash, rpc_url } => { let eth_client = EthClient::new(&rpc_url)?; diff --git a/cli/src/utils.rs b/cli/src/utils.rs index f617d1b..0eb3794 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -24,6 +24,17 @@ pub fn parse_hex(s: &str) -> eyre::Result { } } +/// Parses a hex string, stripping the "0x" prefix if present. +/// Unlike `parse_hex`, the string doesn't need to be of even length. +pub fn parse_hex_string(s: &str) -> eyre::Result { + let s = s.strip_prefix("0x").unwrap_or(s); + if s.chars().all(|c| c.is_ascii_hexdigit()) { + Ok(s.to_string()) + } else { + Err(eyre::eyre!("Invalid hex string")) + } +} + fn parse_call_args(args: Vec) -> eyre::Result)>> { let mut args_iter = args.iter(); let Some(signature) = args_iter.next() else { diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 54bfb7f..3fbcd3d 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -30,6 +30,7 @@ toml = "0.8.19" dirs = "6.0.0" envy = "0.4.2" thiserror.workspace = true +rayon = "1.10.0" # Logging log = "0.4" diff --git a/sdk/src/create.rs b/sdk/src/create.rs index 2edff0f..6105314 100644 --- a/sdk/src/create.rs +++ b/sdk/src/create.rs @@ -1,6 +1,14 @@ use ethrex_common::Address; use ethrex_rlp::encode::RLPEncode; -use keccak_hash::keccak; +use keccak_hash::{H256, keccak}; +use rand::RngCore; +use rayon::prelude::*; +use std::iter; +use std::sync::Arc; + +use crate::utils::to_checksum_address; + +pub const DETERMINISTIC_DEPLOYER: &str = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; /// address = keccak256(rlp([sender_address,sender_nonce]))[12:] pub fn compute_create_address(sender_address: Address, sender_nonce: u64) -> Address { @@ -10,6 +18,113 @@ pub fn compute_create_address(sender_address: Address, sender_nonce: u64) -> Add Address::from_slice(&keccak_bytes[12..]) } +/// address = keccak256(0xff || deployer_address || salt || keccak256(initialization_code))[12:] +pub fn compute_create2_address( + deployer_address: Address, + init_code_hash: H256, + salt: H256, +) -> Address { + Address::from_slice( + &keccak( + [ + &[0xff], + deployer_address.as_bytes(), + &salt.0, + init_code_hash.as_bytes(), + ] + .concat(), + ) + .as_bytes()[12..], + ) +} + +/// Brute-force Create2 address generation +/// This function generates random salts until it finds one that matches the specified criteria. +/// `begins`, `ends`, and `contains` are optional filters for the generated address. +/// If they are not provided, the function will not filter based on that criterion. +/// Returns the salt and the generated address. +pub fn brute_force_create2( + deployer: Address, + init_code_hash: H256, + mut begins: Option, + mut ends: Option, + mut contains: Option, + case_sensitive: bool, +) -> (H256, Address) { + // If we don't care about case convert everything to lowercase. + if !case_sensitive { + begins = begins.map(|b| b.to_lowercase()); + ends = ends.map(|e| e.to_lowercase()); + contains = contains.map(|c| c.to_lowercase()); + } + loop { + // Generate random salt + let mut salt_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut salt_bytes); + let salt = H256::from(salt_bytes); + + // Compute Create2 Address + let candidate_address = compute_create2_address(deployer, init_code_hash, salt); + + // Address as string without 0x prefix + let addr_str = if !case_sensitive { + format!("{candidate_address:x}") + } else { + to_checksum_address(&format!("{candidate_address:x}")) + }; + + // Validate that address satisfies the requirements given by the user. + let matches_begins = begins.as_ref().is_none_or(|b| addr_str.starts_with(b)); + let matches_ends = ends.as_ref().is_none_or(|e| addr_str.ends_with(e)); + let matches_contains = contains.as_ref().is_none_or(|c| addr_str.contains(c)); + + if matches_begins && matches_ends && matches_contains { + return (salt, candidate_address); + } + } +} + +pub fn brute_force_create2_rayon( + deployer: Address, + init_code_hash: H256, + begins: Option, + ends: Option, + contains: Option, + case_sensitive: bool, +) -> (H256, Address) { + let begins = Arc::new(begins.map(|s| if case_sensitive { s } else { s.to_lowercase() })); + let ends = Arc::new(ends.map(|s| if case_sensitive { s } else { s.to_lowercase() })); + let contains = Arc::new(contains.map(|s| if case_sensitive { s } else { s.to_lowercase() })); + + iter::repeat_with(|| { + let mut salt_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut salt_bytes); + H256::from(salt_bytes) + }) + .par_bridge() // Convert into a parallel iterator + .find_any(|salt| { + // Find a salt that satisfies the criteria set by the user. + let addr = compute_create2_address(deployer, init_code_hash, *salt); + + let addr_str = if !case_sensitive { + format!("{addr:x}") + } else { + to_checksum_address(&format!("{addr:x}")) + }; + + let matches_begins = begins.as_deref().is_none_or(|b| addr_str.starts_with(b)); + let matches_ends = ends.as_deref().is_none_or(|e| addr_str.ends_with(&e)); + let matches_contains = contains.as_deref().is_none_or(|c| addr_str.contains(c)); + + matches_begins && matches_ends && matches_contains + }) + .map(|salt| { + let addr = compute_create2_address(deployer, init_code_hash, salt); + (salt, addr) + }) + .expect("should eventually find a match") +} + #[test] fn compute_address() { use std::str::FromStr; diff --git a/sdk/src/utils.rs b/sdk/src/utils.rs index 93f0bef..08bf912 100644 --- a/sdk/src/utils.rs +++ b/sdk/src/utils.rs @@ -1,4 +1,5 @@ use ethrex_common::H256; +use keccak_hash::keccak; use secp256k1::SecretKey; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -17,3 +18,33 @@ where let hex = H256::from_slice(&secret_key.secret_bytes()); hex.serialize(serializer) } + +/// EIP-55 Checksum Address. +/// This is how addresses are actually displayed on ethereum apps +/// Returns address as string without "0x" prefix +pub fn to_checksum_address(address: &str) -> String { + // Trim if necessary + let addr = address.trim_start_matches("0x").to_lowercase(); + + // Hash the raw address using Keccak-256 + let hash = keccak(&addr); + + // Convert hash to hex string + let hash_hex = hex::encode(hash); + + // Apply checksum by walking each nibble + let mut checksummed = String::with_capacity(40); + + for (i, c) in addr.chars().enumerate() { + let hash_char = hash_hex.chars().nth(i).unwrap(); + let hash_value = hash_char.to_digit(16).unwrap(); + + if c.is_ascii_alphabetic() && hash_value >= 8 { + checksummed.push(c.to_ascii_uppercase()); + } else { + checksummed.push(c); + } + } + + checksummed +}