starknet_rust_core/
crypto.rs

1use starknet_types_core::felt::Felt;
2
3use starknet_rust_crypto::{
4    blake2s_hash, blake2s_hash_many, blake2s_hash_single, poseidon_hash, poseidon_hash_many,
5    poseidon_hash_single, rfc6979_generate_k, sign, verify, Blake2Hasher, PoseidonHasher,
6    SignError, VerifyError,
7};
8pub use starknet_rust_crypto::{pedersen_hash, ExtendedSignature, Signature};
9
10mod errors {
11    use core::fmt::{Display, Formatter, Result};
12
13    /// Errors when performing ECDSA [`sign`](fn.ecdsa_sign) operations.
14    #[derive(Debug)]
15    pub enum EcdsaSignError {
16        /// The message hash is not in the range of `[0, 2^251)`.
17        MessageHashOutOfRange,
18    }
19
20    #[derive(Debug)]
21    /// Errors when performing ECDSA [`verify`](fn.ecdsa_verify) operations.
22    pub enum EcdsaVerifyError {
23        /// The message hash is not in the range of `[0, 2^251)`.
24        MessageHashOutOfRange,
25        /// The public key is not a valid point on the STARK curve.
26        InvalidPublicKey,
27        /// The `r` value is not in the range of `[0, 2^251)`.
28        SignatureROutOfRange,
29        /// The `s` value is not in the range of `[0, 2^251)`.
30        SignatureSOutOfRange,
31    }
32
33    #[cfg(feature = "std")]
34    impl std::error::Error for EcdsaSignError {}
35
36    impl Display for EcdsaSignError {
37        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
38            match self {
39                Self::MessageHashOutOfRange => write!(f, "message hash out of range"),
40            }
41        }
42    }
43
44    #[cfg(feature = "std")]
45    impl std::error::Error for EcdsaVerifyError {}
46
47    impl Display for EcdsaVerifyError {
48        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
49            match self {
50                Self::MessageHashOutOfRange => write!(f, "message hash out of range"),
51                Self::InvalidPublicKey => write!(f, "invalid public key"),
52                Self::SignatureROutOfRange => write!(f, "signature r value out of range"),
53                Self::SignatureSOutOfRange => write!(f, "signature s value out of range"),
54            }
55        }
56    }
57}
58pub use errors::{EcdsaSignError, EcdsaVerifyError};
59
60/// Computes the Pedersen hash of a list of [`Felt`].
61///
62/// The hash is computed by starting with `0`, hashing it recursively against all elements in
63/// the list, and finally also hashing against the length of the list.
64///
65/// For example, calling `compute_hash_on_elements([7, 8])` would return:
66///
67/// ```markdown
68/// pedersen_hash(pedersen_hash(pedersen_hash(0, 7)), 8), 2)
69/// ```
70pub fn compute_hash_on_elements<'a, ESI, II>(data: II) -> Felt
71where
72    ESI: ExactSizeIterator<Item = &'a Felt>,
73    II: IntoIterator<IntoIter = ESI>,
74{
75    let mut current_hash = Felt::ZERO;
76    let data_iter = data.into_iter();
77    let data_len = Felt::from(data_iter.len());
78
79    for elem in data_iter {
80        current_hash = pedersen_hash(&current_hash, elem);
81    }
82
83    pedersen_hash(&current_hash, &data_len)
84}
85
86/// Signs a hash using deterministic ECDSA on the STARK curve. The signature returned can be used
87/// to recover the public key.
88pub fn ecdsa_sign(
89    private_key: &Felt,
90    message_hash: &Felt,
91) -> Result<ExtendedSignature, EcdsaSignError> {
92    // Seed-retry logic ported from `cairo-lang`
93    let mut seed = None;
94    loop {
95        let k = rfc6979_generate_k(message_hash, private_key, seed.as_ref());
96
97        match sign(private_key, message_hash, &k) {
98            Ok(sig) => {
99                return Ok(sig);
100            }
101            Err(SignError::InvalidMessageHash) => {
102                return Err(EcdsaSignError::MessageHashOutOfRange)
103            }
104            Err(SignError::InvalidK) => {
105                // Bump seed and retry
106                seed = match seed {
107                    Some(prev_seed) => Some(prev_seed + Felt::ONE),
108                    None => Some(Felt::ONE),
109                };
110            }
111        };
112    }
113}
114
115/// Verified an ECDSA signature on the STARK curve.
116pub fn ecdsa_verify(
117    public_key: &Felt,
118    message_hash: &Felt,
119    signature: &Signature,
120) -> Result<bool, EcdsaVerifyError> {
121    match verify(public_key, message_hash, &signature.r, &signature.s) {
122        Ok(result) => Ok(result),
123        Err(VerifyError::InvalidMessageHash) => Err(EcdsaVerifyError::MessageHashOutOfRange),
124        Err(VerifyError::InvalidPublicKey) => Err(EcdsaVerifyError::InvalidPublicKey),
125        Err(VerifyError::InvalidR) => Err(EcdsaVerifyError::SignatureROutOfRange),
126        Err(VerifyError::InvalidS) => Err(EcdsaVerifyError::SignatureSOutOfRange),
127    }
128}
129
130/// A hash function that is used in Starknet.
131#[derive(Clone, Debug, Copy, Eq, PartialEq)]
132pub struct HashFunction {
133    inner: HashFunctionInner,
134}
135
136#[derive(Clone, Debug, Copy, Eq, PartialEq)]
137enum HashFunctionInner {
138    Poseidon,
139    Blake2s,
140}
141
142/// A stateful hasher that can be updated with messages and finalized to produce a hash.
143#[derive(Debug, Clone)]
144pub struct StatefulHasher {
145    inner: StatefulHasherInner,
146}
147
148#[derive(Debug, Clone)]
149enum StatefulHasherInner {
150    Poseidon(PoseidonHasher),
151    Blake2s(Blake2Hasher),
152}
153
154impl HashFunction {
155    /// Creates a new stateful hasher with chosen hash function.
156    pub fn stateful(&self) -> StatefulHasher {
157        let hasher = match &self.inner {
158            HashFunctionInner::Poseidon => StatefulHasherInner::Poseidon(PoseidonHasher::new()),
159            HashFunctionInner::Blake2s => StatefulHasherInner::Blake2s(Blake2Hasher::new()),
160        };
161        StatefulHasher { inner: hasher }
162    }
163
164    /// Creates a new Poseidon hash function.
165    pub fn poseidon() -> Self {
166        Self {
167            inner: HashFunctionInner::Poseidon,
168        }
169    }
170
171    /// Creates a new Blake2s hash function.
172    pub fn blake2s() -> Self {
173        Self {
174            inner: HashFunctionInner::Blake2s,
175        }
176    }
177
178    /// Computes the Starknet Poseidon hash of x and y with chosen hash function.
179    pub fn hash(&self, x: Felt, y: Felt) -> Felt {
180        match &self.inner {
181            HashFunctionInner::Poseidon => poseidon_hash(x, y),
182            HashFunctionInner::Blake2s => blake2s_hash(x, y),
183        }
184    }
185
186    /// Computes the Starknet hash of a single [`Felt`] with chosen hash function.
187    pub fn hash_single(&self, input: Felt) -> Felt {
188        match &self.inner {
189            HashFunctionInner::Poseidon => poseidon_hash_single(input),
190            HashFunctionInner::Blake2s => blake2s_hash_single(input),
191        }
192    }
193
194    /// Computes the Starknet hash of an arbitrary number of [`Felt`]s with chosen hash function.
195    pub fn hash_many(&self, inputs: &[Felt]) -> Felt {
196        match &self.inner {
197            HashFunctionInner::Poseidon => poseidon_hash_many(inputs),
198            HashFunctionInner::Blake2s => blake2s_hash_many(inputs),
199        }
200    }
201}
202
203impl StatefulHasher {
204    /// Absorbs message into the hash.
205    pub fn update(&mut self, msg: Felt) {
206        match &mut self.inner {
207            StatefulHasherInner::Poseidon(hasher) => hasher.update(msg),
208            StatefulHasherInner::Blake2s(hasher) => hasher.update(msg),
209        }
210    }
211
212    /// Finishes and returns hash.
213    pub fn finalize(self) -> Felt {
214        match self.inner {
215            StatefulHasherInner::Poseidon(hasher) => hasher.finalize(),
216            StatefulHasherInner::Blake2s(hasher) => hasher.finalize(),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
227    fn test_compute_hash_on_elements() {
228        // Generated with `cairo-lang`
229        let hash = compute_hash_on_elements(&[
230            Felt::from_hex("0xaa").unwrap(),
231            Felt::from_hex("0xbb").unwrap(),
232            Felt::from_hex("0xcc").unwrap(),
233            Felt::from_hex("0xdd").unwrap(),
234        ]);
235        let expected_hash =
236            Felt::from_hex("025cde77210b1c223b2c6e69db6e9021aa1599177ab177474d5326cd2a62cb69")
237                .unwrap();
238
239        assert_eq!(expected_hash, hash);
240    }
241
242    #[test]
243    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
244    fn test_compute_hash_on_elements_empty_data() {
245        // Generated with `cairo-lang`
246        let hash = compute_hash_on_elements(&[]);
247        let expected_hash =
248            Felt::from_hex("049ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804")
249                .unwrap();
250
251        assert_eq!(expected_hash, hash);
252    }
253
254    #[test]
255    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
256    fn test_ecdsa_sign() {
257        // Generated with `cairo-lang`
258        let signature = ecdsa_sign(
259            &Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
260                .unwrap(),
261            &Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
262                .unwrap(),
263        )
264        .unwrap();
265        let expected_r =
266            Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
267                .unwrap();
268        let expected_s =
269            Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
270                .unwrap();
271
272        assert_eq!(signature.r, expected_r);
273        assert_eq!(signature.s, expected_s);
274    }
275
276    #[test]
277    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
278    fn test_ecdsa_sign_message_hash_out_of_range() {
279        match ecdsa_sign(
280            &Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79")
281                .unwrap(),
282            &Felt::from_hex("0800000000000000000000000000000000000000000000000000000000000000")
283                .unwrap(),
284        ) {
285            Err(EcdsaSignError::MessageHashOutOfRange) => {}
286            _ => panic!("Should throw error on out of range message hash"),
287        };
288    }
289
290    #[test]
291    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
292    fn test_ecdsa_verify_valid_signature() {
293        // Generated with `cairo-lang`
294        let public_key =
295            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
296                .unwrap();
297        let message_hash =
298            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
299                .unwrap();
300        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
301            .unwrap();
302        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a")
303            .unwrap();
304
305        assert!(ecdsa_verify(&public_key, &message_hash, &Signature { r, s }).unwrap());
306    }
307
308    #[test]
309    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
310    fn test_ecdsa_verify_invalid_signature() {
311        // Generated with `cairo-lang`
312        let public_key =
313            Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159")
314                .unwrap();
315        let message_hash =
316            Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76")
317                .unwrap();
318        let r = Felt::from_hex("061ec782f76a66f6984efc3a1b6d152a124c701c00abdd2bf76641b4135c770f")
319            .unwrap();
320        let s = Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9b")
321            .unwrap();
322
323        assert!(!ecdsa_verify(&public_key, &message_hash, &Signature { r, s }).unwrap());
324    }
325
326    #[test]
327    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
328    fn test_hash_function_hash() {
329        let x = Felt::from_hex("0x1").unwrap();
330        let y = Felt::from_hex("0x2").unwrap();
331
332        assert_eq!(HashFunction::poseidon().hash(x, y), poseidon_hash(x, y));
333        assert_eq!(HashFunction::blake2s().hash(x, y), blake2s_hash(x, y));
334    }
335
336    #[test]
337    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
338    fn test_hash_function_hash_single() {
339        let x = Felt::from_hex("0x1").unwrap();
340
341        assert_eq!(
342            HashFunction::poseidon().hash_single(x),
343            poseidon_hash_single(x)
344        );
345        assert_eq!(
346            HashFunction::blake2s().hash_single(x),
347            blake2s_hash_single(x)
348        );
349    }
350
351    #[test]
352    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
353    fn test_hash_function_hash_many() {
354        let many = vec![
355            Felt::from_hex("0x3").unwrap(),
356            Felt::from_hex("0x4").unwrap(),
357        ];
358
359        assert_eq!(
360            HashFunction::poseidon().hash_many(&many),
361            poseidon_hash_many(&many)
362        );
363
364        assert_eq!(
365            HashFunction::blake2s().hash_many(&many),
366            blake2s_hash_many(&many)
367        );
368    }
369
370    #[test]
371    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
372    fn test_hash_function_stateful() {
373        let x = Felt::from_hex("0x1").unwrap();
374        let y = Felt::from_hex("0x2").unwrap();
375
376        let mut poseidon_hasher = HashFunction::poseidon().stateful();
377        poseidon_hasher.update(x);
378        poseidon_hasher.update(y);
379        assert_eq!(poseidon_hasher.finalize(), poseidon_hash_many(&[x, y]));
380
381        let mut blake2s_hasher = HashFunction::blake2s().stateful();
382        blake2s_hasher.update(x);
383        blake2s_hasher.update(y);
384        assert_eq!(blake2s_hasher.finalize(), blake2s_hash_many(&[x, y]));
385    }
386}