From a210db80c7126b5b6a48244a2594a8a8fae74091 Mon Sep 17 00:00:00 2001 From: ron-starkware Date: Wed, 15 Jul 2026 10:16:01 +0300 Subject: [PATCH] starknet_api: escape the Pedersen image in Blake2 contract address derivation --- crates/starknet_api/src/core.rs | 67 +++++++++++++++++++++++----- crates/starknet_api/src/core_test.rs | 41 +++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/crates/starknet_api/src/core.rs b/crates/starknet_api/src/core.rs index fc65f639028..914beca5d80 100644 --- a/crates/starknet_api/src/core.rs +++ b/crates/starknet_api/src/core.rs @@ -326,9 +326,51 @@ impl TryFrom for ContractAddress { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AddressDerivationHash { Pedersen, + /// Blake2, escaped out of the Pedersen image so the two schemes' addresses are disjoint. Blake2, } +/// The STARK curve is `y^2 = x^3 + STARK_CURVE_ALPHA * x + STARK_CURVE_BETA`. +const STARK_CURVE_ALPHA: Felt = Felt::ONE; +const STARK_CURVE_BETA: Felt = + Felt::from_hex_unchecked("0x6f21413efbe40de150e596d72f7a8c5609ad26c15c915c1f4cdfcb99cee9e89"); +/// Addresses below this bound (the field prime minus L2_ADDRESS_UPPER_BOUND) have a second lift +/// into the field: both `address` and `address + L2_ADDRESS_UPPER_BOUND` are below the prime. +const ADDRESS_SECOND_LIFT_BOUND: Felt = + Felt::from_hex_unchecked("0x11000000000000000000000000000000000000000000000101"); + +/// Returns whether `value` is the x-coordinate of a STARK curve point, i.e. whether +/// `value^3 + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA` is a square in the field. +pub fn is_stark_curve_x_coordinate(value: &Felt) -> bool { + let value = *value; + (value * value * value + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA).sqrt().is_some() +} + +/// Returns whether some Pedersen hash output reduces (mod L2_ADDRESS_UPPER_BOUND) to `address`. +/// A Pedersen output is the x-coordinate of a STARK curve point, so `address` is reachable iff +/// one of its lifts into the field is a curve x-coordinate. +pub fn is_pedersen_reachable_address(address: &Felt) -> bool { + is_stark_curve_x_coordinate(address) + || (*address < ADDRESS_SECOND_LIFT_BOUND + && is_stark_curve_x_coordinate(&(address + Felt::from(&*L2_ADDRESS_UPPER_BOUND)))) +} + +/// Increments `raw_address` (wrapping mod L2_ADDRESS_UPPER_BOUND, skipping the reserved 0x0/0x1) +/// until no Pedersen derivation can reach it, so a funded-but-undeployed Blake2 address cannot be +/// front-run through the Pedersen deploy paths. Expected ~1 increment; each step is a square-root +/// check, never a re-hash. +fn escape_pedersen_image(raw_address: Felt) -> Felt { + let address_upper_bound = Felt::from(&*L2_ADDRESS_UPPER_BOUND); + let mut address = raw_address; + while address < Felt::TWO || is_pedersen_reachable_address(&address) { + address += Felt::ONE; + if address == address_upper_bound { + address = Felt::ZERO; + } + } + address +} + pub fn calculate_contract_address( salt: ContractAddressSalt, class_hash: ClassHash, @@ -336,20 +378,23 @@ pub fn calculate_contract_address( deployer_address: ContractAddress, address_derivation_hash: AddressDerivationHash, ) -> Result { - match address_derivation_hash { + let address = match address_derivation_hash { AddressDerivationHash::Pedersen => calculate_contract_address_inner::( salt, class_hash, constructor_calldata, deployer_address, - ), - AddressDerivationHash::Blake2 => calculate_contract_address_inner::( - salt, - class_hash, - constructor_calldata, - deployer_address, - ), - } + )?, + AddressDerivationHash::Blake2 => { + escape_pedersen_image(calculate_contract_address_inner::( + salt, + class_hash, + constructor_calldata, + deployer_address, + )?) + } + }; + ContractAddress::try_from(address) } fn calculate_contract_address_inner( @@ -357,7 +402,7 @@ fn calculate_contract_address_inner( class_hash: ClassHash, constructor_calldata: &Calldata, deployer_address: ContractAddress, -) -> Result { +) -> Result { let constructor_calldata_hash = H::hash_array(&constructor_calldata.0); let contract_address_prefix = format!("0x{}", hex::encode(CONTRACT_ADDRESS_PREFIX)); let address = H::hash_array(&[ @@ -371,7 +416,7 @@ fn calculate_contract_address_inner( ]); let (_, address) = address.div_rem(&L2_ADDRESS_UPPER_BOUND); - ContractAddress::try_from(address) + Ok(address) } /// The hash of a ContractClass. diff --git a/crates/starknet_api/src/core_test.rs b/crates/starknet_api/src/core_test.rs index ae2cd1b5b85..07b81316585 100644 --- a/crates/starknet_api/src/core_test.rs +++ b/crates/starknet_api/src/core_test.rs @@ -9,6 +9,7 @@ use crate::core::{ ascii_as_felt, calculate_contract_address, felt_to_u128, + is_pedersen_reachable_address, AddressDerivationHash, ChainId, ContractAddress, @@ -85,6 +86,46 @@ fn test_calculate_contract_address() { assert_eq!(actual_address, expected_address); } +#[rstest] +#[case::block_hash_table_address(felt!("0x1"), true)] +#[case::two(felt!("0x2"), true)] +#[case::five(felt!("0x5"), false)] +#[case::large(felt!("0x1234567890abcdef"), true)] +fn test_is_pedersen_reachable_address(#[case] address: Felt, #[case] expected_reachable: bool) { + assert_eq!(is_pedersen_reachable_address(&address), expected_reachable); +} + +// Frozen vectors for the Blake2 derivation with the escape rule, cross-checked against an +// independent python implementation. Deployer = 0, class_hash = 0x4242, +// constructor_calldata = [42, 2^63, 1337]; the cases span 0 to 7 escape increments. +#[rstest] +#[case::zero_increments(777, "0x781e95f4b806dfe5b550756620c77a108d974a5b5d1198b1d45901ac1f89e9f")] +#[case::one_increment(771, "0x566c3e328f3fd5a311267250cadc3c1c4de799db54180fcf862fe90b622571d")] +#[case::two_increments(776, "0x1cd7f5c31ef1b147b816048b025a6cc345e7e023aa8ed97222a883e31dc8435")] +#[case::three_increments(775, "0x4f7ba32369d7f68c42a7619242a52a5be9e803459f5afae1772d9377b161c4c")] +#[case::seven_increments(774, "0x47d0c1ff356a1d540cd9f2efa122b60168b1007ef0603af0857bc6100e4b8e8")] +fn test_blake_contract_address_escapes_pedersen_image( + #[case] salt: u16, + #[case] expected_address: &str, +) { + let constructor_calldata = + Calldata(vec![Felt::from(42_u8), Felt::from(1_u64 << 63), Felt::from(1337_u16)].into()); + + let actual_address = calculate_contract_address( + ContractAddressSalt(Felt::from(salt)), + class_hash!("0x4242"), + &constructor_calldata, + ContractAddress::default(), + AddressDerivationHash::Blake2, + ) + .unwrap(); + + let expected_address = + ContractAddress::try_from(Felt::from_hex_unchecked(expected_address)).unwrap(); + assert_eq!(actual_address, expected_address); + assert!(!is_pedersen_reachable_address(actual_address.0.key())); +} + #[test] fn eth_address_serde() { let eth_address = EthAddress::try_from(felt!("0x001")).unwrap();