From 03a2e54aff228957e786ef46750f9eabbb7fbfe6 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 19 Jun 2025 18:04:23 -0300 Subject: [PATCH 1/4] add create2-address command --- Cargo.lock | 1 + cli/Cargo.toml | 1 + cli/src/cli.rs | 122 +++++++++++++++++++++++++++++++++++++++++++--- cli/src/utils.rs | 11 +++++ sdk/src/create.rs | 73 ++++++++++++++++++++++++++- sdk/src/utils.rs | 31 ++++++++++++ 6 files changed, 231 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca97923..2276b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4955,6 +4955,7 @@ dependencies = [ "itertools 0.14.0", "keccak-hash", "log", + "rand 0.9.1", "rex-sdk", "secp256k1", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1f7ff86..981d7cb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -42,3 +42,4 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } # Serde serde = "1.0.218" serde_json = "1.0.139" +rand = "0.9.1" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 4a8370b..0f13d32 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}, @@ -9,15 +9,18 @@ use clap::{ArgAction, Parser, Subcommand}; use ethrex_common::{Address, Bytes, H256, H520}; use keccak_hash::keccak; use rex_sdk::calldata::{Value, decode_calldata}; -use rex_sdk::create::compute_create_address; +use rex_sdk::create::{ + DETERMINISTIC_DEPLOYER, brute_force_create2, 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"); @@ -110,12 +113,66 @@ 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, + }, #[clap(about = "Deploy a contract")] Deploy { #[clap(flatten)] @@ -234,13 +291,64 @@ impl Command { println!("{block_number}"); } Command::CreateAddress { - address, + deployer, nonce, rpc_url, } => { - let nonce = nonce.unwrap_or(EthClient::new(&rpc_url).get_nonce(address).await?); + let nonce = match nonce { + Some(n) => n, + None => { + let nonce = EthClient::new(&rpc_url).get_nonce(deployer).await?; + println!("Latest nonce: {nonce}"); + nonce + } + }; + + println!("Address: {:#x}", compute_create_address(deployer, nonce)) + } + Command::Create2Address { + deployer, + init_code, + salt, + init_code_hash, + begins, + ends, + contains, + case_sensitive, + } => { + 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. + print!("\nComputing Create2 Address..."); + io::stdout().flush().ok(); + let start = std::time::Instant::now(); + let (salt, contract_address) = brute_force_create2( + 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!("0x{:x}", compute_create_address(address, nonce)) + 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/src/create.rs b/sdk/src/create.rs index 2edff0f..5de84b8 100644 --- a/sdk/src/create.rs +++ b/sdk/src/create.rs @@ -1,6 +1,11 @@ use ethrex_common::Address; use ethrex_rlp::encode::RLPEncode; -use keccak_hash::keccak; +use keccak_hash::{H256, keccak}; +use rand::RngCore; + +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 +15,72 @@ 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().map_or(true, |b| addr_str.starts_with(b)); + let matches_ends = ends.as_ref().map_or(true, |e| addr_str.ends_with(e)); + let matches_contains = contains.as_ref().map_or(true, |c| addr_str.contains(c)); + + if matches_begins && matches_ends && matches_contains { + return (salt, candidate_address); + } + } +} + #[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 +} From ade6f10012c8668b6e768d8c6aa9ce11a3606099 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Thu, 19 Jun 2025 18:42:35 -0300 Subject: [PATCH 2/4] switch to rayon alternative --- Cargo.lock | 2 ++ cli/Cargo.toml | 3 ++- cli/src/cli.rs | 18 ++++++++++++++---- sdk/Cargo.toml | 1 + sdk/src/create.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2276b95..2da82f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4956,6 +4956,7 @@ dependencies = [ "keccak-hash", "log", "rand 0.9.1", + "rayon", "rex-sdk", "secp256k1", "serde", @@ -4989,6 +4990,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 981d7cb..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" @@ -42,4 +44,3 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } # Serde serde = "1.0.218" serde_json = "1.0.139" -rand = "0.9.1" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 0f13d32..5407942 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -10,7 +10,8 @@ use ethrex_common::{Address, Bytes, H256, H520}; use keccak_hash::keccak; use rex_sdk::calldata::{Value, decode_calldata}; use rex_sdk::create::{ - DETERMINISTIC_DEPLOYER, brute_force_create2, compute_create_address, compute_create2_address, + 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; @@ -172,6 +173,13 @@ pub(crate) enum Command { 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 { @@ -315,6 +323,7 @@ impl Command { ends, contains, case_sensitive, + threads, } => { let init_code_hash = init_code_hash .or_else(|| init_code.as_ref().map(keccak)) @@ -328,10 +337,11 @@ impl Command { } None => { // If salt is not provided, search for a salt that matches the criteria set by the user. - print!("\nComputing Create2 Address..."); + println!("\nComputing Create2 Address with {threads} threads..."); io::stdout().flush().ok(); + let start = std::time::Instant::now(); - let (salt, contract_address) = brute_force_create2( + let (salt, contract_address) = brute_force_create2_rayon( deployer, init_code_hash, begins, @@ -340,7 +350,7 @@ impl Command { case_sensitive, ); let duration = start.elapsed(); - println!(" Generated in: {:.2?}.", duration); + println!("Generated in: {:.2?}.", duration); (salt, contract_address) } }; 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 5de84b8..44817b3 100644 --- a/sdk/src/create.rs +++ b/sdk/src/create.rs @@ -2,6 +2,9 @@ use ethrex_common::Address; use ethrex_rlp::encode::RLPEncode; use keccak_hash::{H256, keccak}; use rand::RngCore; +use rayon::prelude::*; +use std::iter; +use std::sync::Arc; use crate::utils::to_checksum_address; @@ -81,6 +84,47 @@ pub fn brute_force_create2( } } +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().map_or(true, |b| addr_str.starts_with(b)); + let matches_ends = ends.as_deref().map_or(true, |e| addr_str.ends_with(&e)); + let matches_contains = contains.as_deref().map_or(true, |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; From a1c02e1b32f62553d6c3e5cbe902a8b582ae5d6f Mon Sep 17 00:00:00 2001 From: JereSalo Date: Mon, 23 Jun 2025 10:12:32 -0300 Subject: [PATCH 3/4] clippy lint --- sdk/src/create.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/src/create.rs b/sdk/src/create.rs index 44817b3..6105314 100644 --- a/sdk/src/create.rs +++ b/sdk/src/create.rs @@ -74,9 +74,9 @@ pub fn brute_force_create2( }; // Validate that address satisfies the requirements given by the user. - let matches_begins = begins.as_ref().map_or(true, |b| addr_str.starts_with(b)); - let matches_ends = ends.as_ref().map_or(true, |e| addr_str.ends_with(e)); - let matches_contains = contains.as_ref().map_or(true, |c| addr_str.contains(c)); + 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); @@ -112,9 +112,9 @@ pub fn brute_force_create2_rayon( to_checksum_address(&format!("{addr:x}")) }; - let matches_begins = begins.as_deref().map_or(true, |b| addr_str.starts_with(b)); - let matches_ends = ends.as_deref().map_or(true, |e| addr_str.ends_with(&e)); - let matches_contains = contains.as_deref().map_or(true, |c| addr_str.contains(&c)); + 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 }) From 6c035484f749d8a0b2a85673137c3c0d8f018d55 Mon Sep 17 00:00:00 2001 From: JereSalo Date: Wed, 16 Jul 2025 18:42:51 -0300 Subject: [PATCH 4/4] fix name in get_nonce --- cli/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index bb6f721..24c49e3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -308,7 +308,7 @@ impl Command { } => { let nonce = nonce.unwrap_or( EthClient::new(&rpc_url)? - .get_nonce(address, BlockByNumber::Latest) + .get_nonce(deployer, BlockByNumber::Latest) .await?, );