starknet_core/
utils.rs

1use alloc::string::*;
2
3use crate::crypto::compute_hash_on_elements;
4
5use sha3::{Digest, Keccak256};
6use starknet_crypto::pedersen_hash;
7use starknet_types_core::felt::Felt;
8use starknet_types_core::felt::NonZeroFelt;
9
10const DEFAULT_ENTRY_POINT_NAME: &str = "__default__";
11const DEFAULT_L1_ENTRY_POINT_NAME: &str = "__l1_default__";
12
13// 2 ** 251 - 256
14const ADDR_BOUND: NonZeroFelt = NonZeroFelt::from_raw([
15    576459263475590224,
16    18446744073709255680,
17    160989183,
18    18446743986131443745,
19]);
20
21// Cairo string of "STARKNET_CONTRACT_ADDRESS"
22const CONTRACT_ADDRESS_PREFIX: Felt = Felt::from_raw([
23    533439743893157637,
24    8635008616843941496,
25    17289941567720117366,
26    3829237882463328880,
27]);
28
29/// The uniqueness settings for UDC deployments.
30#[derive(Debug, Clone)]
31pub enum UdcUniqueness {
32    /// Contract deployment is not unique to the deployer, as in any deployer account can deploy to
33    /// this same deployed address given the same settings.
34    NotUnique,
35    /// Contract deployment is unique to the deployer, as in the deployer address is used to form
36    /// part of the deployment salt, making it impossible to another deployer account to deploy to
37    /// the same deployed address, even using the same UDC inputs.
38    Unique(UdcUniqueSettings),
39}
40
41/// The uniqueness settings when using [`UdcUniqueness::Unique`] for contract deployment.
42#[derive(Debug, Clone)]
43pub struct UdcUniqueSettings {
44    /// Contract address of the deployer account, which is the caller of the UDC.
45    pub deployer_address: Felt,
46    /// The UDC address.
47    pub udc_contract_address: Felt,
48}
49
50mod errors {
51    use core::fmt::{Display, Formatter, Result};
52
53    /// The string provided contains non-ASCII characters.
54    #[derive(Debug)]
55    pub struct NonAsciiNameError;
56
57    /// Possible errors for encoding a Cairo short string.
58    #[derive(Debug)]
59    pub enum CairoShortStringToFeltError {
60        /// The string provided contains non-ASCII characters.
61        NonAsciiCharacter,
62        /// The string provided is longer than 31 characters.
63        StringTooLong,
64    }
65
66    /// Possible errors for decoding a Cairo short string.
67    #[derive(Debug)]
68    pub enum ParseCairoShortStringError {
69        /// The encoded [`Felt`](super::Felt) value is out of range.
70        ValueOutOfRange,
71        /// A null terminator (`0x00`) is encountered.
72        UnexpectedNullTerminator,
73    }
74
75    #[cfg(feature = "std")]
76    impl std::error::Error for NonAsciiNameError {}
77
78    impl Display for NonAsciiNameError {
79        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
80            write!(f, "the provided name contains non-ASCII characters")
81        }
82    }
83
84    #[cfg(feature = "std")]
85    impl std::error::Error for CairoShortStringToFeltError {}
86
87    impl Display for CairoShortStringToFeltError {
88        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
89            match self {
90                Self::NonAsciiCharacter => {
91                    write!(f, "Cairo string can only contain ASCII characters")
92                }
93                Self::StringTooLong => {
94                    write!(f, "short string exceeds maximum length of 31 characters")
95                }
96            }
97        }
98    }
99
100    #[cfg(feature = "std")]
101    impl std::error::Error for ParseCairoShortStringError {}
102
103    impl Display for ParseCairoShortStringError {
104        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
105            match self {
106                Self::ValueOutOfRange => write!(f, "field element value out of range"),
107                Self::UnexpectedNullTerminator => write!(f, "unexpected null terminator"),
108            }
109        }
110    }
111}
112pub use errors::{CairoShortStringToFeltError, NonAsciiNameError, ParseCairoShortStringError};
113
114/// A variant of eth-keccak that computes a value that fits in a Starknet field element. It performs
115/// a standard Keccak-256 but with the 6 most significant bits removed.
116pub fn starknet_keccak(data: &[u8]) -> Felt {
117    let mut hasher = Keccak256::new();
118    hasher.update(data);
119    let mut hash = hasher.finalize();
120
121    // Remove the first 6 bits
122    hash[0] &= 0b00000011;
123
124    // Because we know hash is always 32 bytes
125    Felt::from_bytes_be(unsafe { &*(hash[..].as_ptr() as *const [u8; 32]) })
126}
127
128/// Calculates the entrypoint selector from a human-readable function name.
129///
130/// Returns the [Starknet Keccak](fn.starknet_keccak) of the function name in most cases, except for
131/// 2 special built-in default entrypoints of `__default__` and `__l1_default__` for which `0` is
132/// returned instead.
133pub fn get_selector_from_name(func_name: &str) -> Result<Felt, NonAsciiNameError> {
134    if func_name == DEFAULT_ENTRY_POINT_NAME || func_name == DEFAULT_L1_ENTRY_POINT_NAME {
135        Ok(Felt::ZERO)
136    } else {
137        let name_bytes = func_name.as_bytes();
138        if name_bytes.is_ascii() {
139            Ok(starknet_keccak(name_bytes))
140        } else {
141            Err(NonAsciiNameError)
142        }
143    }
144}
145
146/// Calculates the standard storage slot address of storage variable in a Cairo contract.
147///
148/// The storage address is calculated by taking the [Starknet Keccak](fn.starknet_keccak) of the
149/// storage variable name, and applying [Pedersen hash](fn.pedersen_hash) for any additional keys
150/// in the case of using a `LegacyMap`.
151pub fn get_storage_var_address(var_name: &str, args: &[Felt]) -> Result<Felt, NonAsciiNameError> {
152    let var_name_bytes = var_name.as_bytes();
153    if var_name_bytes.is_ascii() {
154        let mut res = starknet_keccak(var_name_bytes);
155        for arg in args {
156            res = pedersen_hash(&res, arg);
157        }
158        Ok(normalize_address(res))
159    } else {
160        Err(NonAsciiNameError)
161    }
162}
163
164/// Converts Cairo short string to [`Felt`].
165pub fn cairo_short_string_to_felt(str: &str) -> Result<Felt, CairoShortStringToFeltError> {
166    if !str.is_ascii() {
167        return Err(CairoShortStringToFeltError::NonAsciiCharacter);
168    }
169    if str.len() > 31 {
170        return Err(CairoShortStringToFeltError::StringTooLong);
171    }
172
173    let ascii_bytes = str.as_bytes();
174
175    let mut buffer = [0u8; 32];
176    buffer[(32 - ascii_bytes.len())..].copy_from_slice(ascii_bytes);
177
178    // The conversion will never fail
179    Ok(Felt::from_bytes_be(&buffer))
180}
181
182/// Converts [`Felt`] to Cairo short string.
183pub fn parse_cairo_short_string(felt: &Felt) -> Result<String, ParseCairoShortStringError> {
184    if felt == &Felt::ZERO {
185        return Ok(String::new());
186    }
187
188    let be_bytes = felt.to_bytes_be();
189    if be_bytes[0] > 0 {
190        return Err(ParseCairoShortStringError::ValueOutOfRange);
191    }
192
193    let mut buffer = String::with_capacity(31);
194    for byte in be_bytes {
195        if byte == 0u8 {
196            if !buffer.is_empty() {
197                return Err(ParseCairoShortStringError::UnexpectedNullTerminator);
198            }
199        } else {
200            buffer.push(byte as char)
201        }
202    }
203    Ok(buffer)
204}
205
206/// Computes the target contract address of a "native" contract deployment. Use
207/// [`get_udc_deployed_address`] instead if you want to compute the target address for deployments
208/// through the Universal Deployer Contract.
209///
210/// This function can be used to calculate target addresses from `DEPLOY_ACCOUNT` transactions or
211/// invoking the `deploy` syscall. The `deployer_address` parameter should be set to `0` for
212/// `DEPLOY_ACCOUNT` transactions, and in other cases, to the address of the contract where the
213/// `deploy` syscall is invoked.
214pub fn get_contract_address(
215    salt: Felt,
216    class_hash: Felt,
217    constructor_calldata: &[Felt],
218    deployer_address: Felt,
219) -> Felt {
220    normalize_address(compute_hash_on_elements(&[
221        CONTRACT_ADDRESS_PREFIX,
222        deployer_address,
223        salt,
224        class_hash,
225        compute_hash_on_elements(constructor_calldata),
226    ]))
227}
228
229/// Computes the target contract address for deployments through the Universal Deploy Contract.
230pub fn get_udc_deployed_address(
231    salt: Felt,
232    class_hash: Felt,
233    uniqueness: &UdcUniqueness,
234    constructor_calldata: &[Felt],
235) -> Felt {
236    match uniqueness {
237        UdcUniqueness::NotUnique => {
238            get_contract_address(salt, class_hash, constructor_calldata, Felt::ZERO)
239        }
240        UdcUniqueness::Unique(settings) => {
241            let unique_salt = pedersen_hash(&settings.deployer_address, &salt);
242            get_contract_address(
243                unique_salt,
244                class_hash,
245                constructor_calldata,
246                settings.udc_contract_address,
247            )
248        }
249    }
250}
251
252/// Normalizes an address to be in the range of `[0, 2^251 - 256)`.
253pub fn normalize_address(address: Felt) -> Felt {
254    address.mod_floor(&ADDR_BOUND)
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
263    fn test_starknet_keccak() {
264        // Generated from `cairo-lang`
265        let data = b"execute";
266        let expected_hash =
267            Felt::from_hex("0240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44")
268                .unwrap();
269
270        let hash = starknet_keccak(data);
271
272        assert_eq!(hash, expected_hash);
273    }
274
275    #[test]
276    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
277    fn test_get_selector_from_name() {
278        // Generated from `cairo-lang`
279        let func_name = "execute";
280        let expected_selector =
281            Felt::from_hex("0240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44")
282                .unwrap();
283
284        let selector = get_selector_from_name(func_name).unwrap();
285
286        assert_eq!(selector, expected_selector);
287    }
288
289    #[test]
290    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
291    fn test_get_default_selector() {
292        let default_selector =
293            Felt::from_hex("0000000000000000000000000000000000000000000000000000000000000000")
294                .unwrap();
295
296        assert_eq!(
297            get_selector_from_name("__default__").unwrap(),
298            default_selector
299        );
300        assert_eq!(
301            get_selector_from_name("__l1_default__").unwrap(),
302            default_selector
303        );
304    }
305
306    #[test]
307    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
308    fn test_get_selector_from_non_ascii_name() {
309        let func_name = "🦀";
310
311        match get_selector_from_name(func_name) {
312            Err(_) => {}
313            _ => panic!("Should throw error on non-ASCII name"),
314        };
315    }
316
317    #[test]
318    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
319    fn test_get_storage_var_address() {
320        // Generated from `cairo-lang`
321        let var_name = "balance";
322        let expected_addr =
323            Felt::from_hex("0x0206f38f7e4f15e87567361213c28f235cccdaa1d7fd34c9db1dfe9489c6a091")
324                .unwrap();
325
326        let addr = get_storage_var_address(var_name, &[]).unwrap();
327
328        assert_eq!(addr, expected_addr);
329    }
330
331    #[test]
332    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
333    fn test_get_storage_var_address_with_args() {
334        // Generated from `cairo-lang`
335        let var_name = "balanceOf";
336        let expected_addr =
337            Felt::from_hex("0x07de334d65aa93d9185729b424025918b18892418c85b802775d1f0d2be30a1d")
338                .unwrap();
339
340        let addr = get_storage_var_address(var_name, &[1234u64.into()]).unwrap();
341
342        assert_eq!(addr, expected_addr);
343    }
344
345    #[test]
346    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
347    fn test_cairo_short_string_to_felt() {
348        let data = [
349            (
350                "abcdefghijklmnopqrstuvwxyz",
351                "156490583352162063278528710879425690470022892627113539022649722",
352            ),
353            (
354                "1234567890123456789012345678901",
355                "86921973946889608444641514252360676678984087116218318142845213717418291249",
356            ),
357        ];
358
359        for (str, felt_dec) in data {
360            assert_eq!(
361                cairo_short_string_to_felt(str).unwrap(),
362                Felt::from_dec_str(felt_dec).unwrap()
363            );
364        }
365    }
366
367    #[test]
368    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
369    fn test_cairo_short_string_to_felt_too_long() {
370        assert!(matches!(
371            cairo_short_string_to_felt("12345678901234567890123456789012"),
372            Err(CairoShortStringToFeltError::StringTooLong)
373        ));
374    }
375
376    #[test]
377    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
378    fn test_cairo_short_string_to_felt_non_ascii() {
379        assert!(matches!(
380            cairo_short_string_to_felt("🦀"),
381            Err(CairoShortStringToFeltError::NonAsciiCharacter)
382        ));
383    }
384
385    #[test]
386    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
387    fn test_parse_cairo_short_string() {
388        let data = [
389            (
390                "abcdefghijklmnopqrstuvwxyz",
391                "156490583352162063278528710879425690470022892627113539022649722",
392            ),
393            (
394                "1234567890123456789012345678901",
395                "86921973946889608444641514252360676678984087116218318142845213717418291249",
396            ),
397        ];
398
399        for (str, felt_dec) in data {
400            assert_eq!(
401                parse_cairo_short_string(&Felt::from_dec_str(felt_dec).unwrap()).unwrap(),
402                str
403            );
404        }
405    }
406
407    #[test]
408    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
409    fn test_parse_cairo_short_string_too_long() {
410        assert!(matches!(
411            parse_cairo_short_string(
412                &Felt::from_hex(
413                    "0x0111111111111111111111111111111111111111111111111111111111111111"
414                )
415                .unwrap()
416            ),
417            Err(ParseCairoShortStringError::ValueOutOfRange)
418        ));
419    }
420
421    #[test]
422    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
423    fn test_parse_cairo_short_string_unexpected_null() {
424        assert!(matches!(
425            parse_cairo_short_string(
426                &Felt::from_hex(
427                    "0x0011111111111111111111111111111111111111111111111111111111110011"
428                )
429                .unwrap()
430            ),
431            Err(ParseCairoShortStringError::UnexpectedNullTerminator)
432        ));
433    }
434
435    #[test]
436    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
437    fn test_get_contract_address() {
438        assert_eq!(
439            get_contract_address(
440                Felt::from_hex(
441                    "0x0018a7a329d1d85b621350f2b5fc9c64b2e57dfe708525f0aff2c90de1e5b9c8"
442                )
443                .unwrap(),
444                Felt::from_hex(
445                    "0x0750cd490a7cd1572411169eaa8be292325990d33c5d4733655fe6b926985062"
446                )
447                .unwrap(),
448                &[Felt::ONE],
449                Felt::ZERO
450            ),
451            Felt::from_hex("0x00da27ef7c3869c3a6cc6a0f7bf07a51c3e590825adba8a51cae27d815839eec")
452                .unwrap()
453        )
454    }
455
456    #[test]
457    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
458    fn test_udc_address_not_unique() {
459        let address = get_udc_deployed_address(
460            Felt::from_hex("0x06df0e9a9842d97ff3f4c6de7494d6e69d0a107a72150f9c53d59515b91ed9cb")
461                .unwrap(),
462            Felt::from_hex("0x0562fc1d911530d18a86ea3ef4be50018923898d3c573288c5abb9c2344459ed")
463                .unwrap(),
464            &UdcUniqueness::NotUnique,
465            &[Felt::from_hex("0x1234").unwrap()],
466        );
467
468        assert_eq!(
469            Felt::from_hex("0x0288e5952d2f2f0e897ea0c5401c6e9f584a89eebfb08b5b26f090a8bbf67eb6",)
470                .unwrap(),
471            address
472        );
473    }
474
475    #[test]
476    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
477    fn test_udc_address_unique() {
478        let address = get_udc_deployed_address(
479            Felt::from_hex("0x01f65976b95bf17ae1cb04afc9fc1eeee26d3e1aaa1f30aa535bf261e4322ab8")
480                .unwrap(),
481            Felt::from_hex("0x0562fc1d911530d18a86ea3ef4be50018923898d3c573288c5abb9c2344459ed")
482                .unwrap(),
483            &UdcUniqueness::Unique(UdcUniqueSettings {
484                deployer_address: Felt::from_hex(
485                    "0x00b1461de04c6a1aa3375bdf9b7723a8779c082ffe21311d683a0b15c078b5dc",
486                )
487                .unwrap(),
488                udc_contract_address: Felt::from_hex(
489                    "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf",
490                )
491                .unwrap(),
492            }),
493            &[Felt::from_hex("0x1234").unwrap()],
494        );
495
496        assert_eq!(
497            Felt::from_hex("0x02406943b25942021f213b047c8765e531dddce3b981722f7aeb2ca137e18dbf",)
498                .unwrap(),
499            address
500        );
501    }
502}