alloy_consensus/
crypto.rs

1//! Cryptographic algorithms
2
3use alloc::boxed::Box;
4use alloy_primitives::U256;
5
6#[cfg(any(feature = "secp256k1", feature = "k256"))]
7use alloy_primitives::Signature;
8
9#[cfg(feature = "crypto-backend")]
10pub use backend::{install_default_provider, CryptoProvider, CryptoProviderAlreadySetError};
11
12/// Error for signature S.
13#[derive(Debug, thiserror::Error)]
14#[error("signature S value is greater than `secp256k1n / 2`")]
15pub struct InvalidSignatureS;
16
17/// Opaque error type for sender recovery.
18#[derive(Debug, Default, thiserror::Error)]
19#[error("Failed to recover the signer")]
20pub struct RecoveryError {
21    #[source]
22    source: Option<Box<dyn core::error::Error + Send + Sync + 'static>>,
23}
24
25impl RecoveryError {
26    /// Create a new error with no associated source
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Create a new error with an associated source.
32    ///
33    /// **NOTE:** The "source" should **NOT** be used to propagate cryptographic
34    /// errors e.g. signature parsing or verification errors.
35    pub fn from_source<E: core::error::Error + Send + Sync + 'static>(err: E) -> Self {
36        Self { source: Some(Box::new(err)) }
37    }
38}
39
40impl From<alloy_primitives::SignatureError> for RecoveryError {
41    fn from(err: alloy_primitives::SignatureError) -> Self {
42        Self::from_source(err)
43    }
44}
45
46/// The order of the secp256k1 curve, divided by two. Signatures that should be checked according
47/// to EIP-2 should have an S value less than or equal to this.
48///
49/// `57896044618658097711785492504343953926418782139537452191302581570759080747168`
50pub const SECP256K1N_HALF: U256 = U256::from_be_bytes([
51    0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
52    0x5D, 0x57, 0x6E, 0x73, 0x57, 0xA4, 0x50, 0x1D, 0xDF, 0xE9, 0x2F, 0x46, 0x68, 0x1B, 0x20, 0xA0,
53]);
54
55/// Crypto backend module for pluggable cryptographic implementations.
56#[cfg(feature = "crypto-backend")]
57pub mod backend {
58    use super::*;
59    use alloc::sync::Arc;
60    use alloy_primitives::Address;
61
62    #[cfg(feature = "std")]
63    use std::sync::OnceLock;
64
65    #[cfg(not(feature = "std"))]
66    use once_cell::race::OnceBox;
67
68    /// Trait for cryptographic providers that can perform signature recovery.
69    ///
70    /// This trait allows pluggable cryptographic backends for Ethereum signature recovery.
71    /// By default, alloy uses compile-time selected implementations (secp256k1 or k256),
72    /// but applications can install a custom provider to override this behavior.
73    ///
74    /// # Why is this needed?
75    ///
76    /// The primary reason is performance - when targeting special execution environments
77    /// that require custom cryptographic logic. For example, zkVMs (zero-knowledge virtual
78    /// machines) may have special accelerators that would allow them to perform signature
79    /// recovery faster.
80    ///
81    /// # Usage
82    ///
83    /// 1. Enable the `crypto-backend` feature in your `Cargo.toml`
84    /// 2. Implement the `CryptoProvider` trait for your custom backend
85    /// 3. Install it globally using [`install_default_provider`]
86    /// 4. All subsequent signature recovery operations will use your provider
87    ///
88    /// Note: This trait currently only provides signature recovery functionality,
89    /// not signature creation. For signature creation, use the compile-time selected
90    /// implementations in the [`secp256k1`] module.
91    ///
92    /// ```rust,ignore
93    /// use alloy_consensus::crypto::backend::{CryptoProvider, install_default_provider};
94    /// use alloy_primitives::Address;
95    /// use alloc::sync::Arc;
96    ///
97    /// struct MyCustomProvider;
98    ///
99    /// impl CryptoProvider for MyCustomProvider {
100    ///     fn recover_signer_unchecked(
101    ///         &self,
102    ///         sig: &[u8; 65],
103    ///         msg: &[u8; 32],
104    ///     ) -> Result<Address, RecoveryError> {
105    ///         // Your custom implementation here
106    ///         todo!()
107    ///     }
108    /// }
109    ///
110    /// // Install your provider globally
111    /// install_default_provider(Arc::new(MyCustomProvider)).unwrap();
112    /// ```
113    pub trait CryptoProvider: Send + Sync + 'static {
114        /// Recover signer from signature and message hash, without ensuring low S values.
115        fn recover_signer_unchecked(
116            &self,
117            sig: &[u8; 65],
118            msg: &[u8; 32],
119        ) -> Result<Address, RecoveryError>;
120    }
121
122    /// Global default crypto provider.
123    #[cfg(feature = "std")]
124    static DEFAULT_PROVIDER: OnceLock<Arc<dyn CryptoProvider>> = OnceLock::new();
125
126    #[cfg(not(feature = "std"))]
127    static DEFAULT_PROVIDER: OnceBox<Arc<dyn CryptoProvider>> = OnceBox::new();
128
129    /// Error returned when attempting to install a provider when one is already installed.
130    /// Contains the provider that was attempted to be installed.
131    pub struct CryptoProviderAlreadySetError {
132        /// The provider that was attempted to be installed.
133        pub provider: Arc<dyn CryptoProvider>,
134    }
135
136    impl core::fmt::Debug for CryptoProviderAlreadySetError {
137        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138            f.debug_struct("CryptoProviderAlreadySetError")
139                .field("provider", &"<crypto provider>")
140                .finish()
141        }
142    }
143
144    impl core::fmt::Display for CryptoProviderAlreadySetError {
145        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
146            write!(f, "crypto provider already installed")
147        }
148    }
149
150    impl core::error::Error for CryptoProviderAlreadySetError {}
151
152    /// Install the default crypto provider.
153    ///
154    /// This sets the global default provider used by the high-level crypto functions.
155    /// Returns an error containing the provider that was attempted to be installed if one is
156    /// already set.
157    pub fn install_default_provider(
158        provider: Arc<dyn CryptoProvider>,
159    ) -> Result<(), CryptoProviderAlreadySetError> {
160        #[cfg(feature = "std")]
161        {
162            DEFAULT_PROVIDER.set(provider).map_err(|provider| {
163                // Return the provider we tried to install in the error
164                CryptoProviderAlreadySetError { provider }
165            })
166        }
167        #[cfg(not(feature = "std"))]
168        {
169            DEFAULT_PROVIDER.set(Box::new(provider)).map_err(|provider| {
170                // Return the provider we tried to install in the error
171                CryptoProviderAlreadySetError { provider: *provider }
172            })
173        }
174    }
175
176    /// Get the currently installed default provider, panicking if none is installed.
177    pub fn get_default_provider() -> &'static dyn CryptoProvider {
178        try_get_provider().map_or_else(
179            || panic!("No crypto backend installed. Call install_default_provider() first."),
180            |provider| provider,
181        )
182    }
183
184    /// Try to get the currently installed default provider, returning None if none is installed.
185    pub(super) fn try_get_provider() -> Option<&'static dyn CryptoProvider> {
186        DEFAULT_PROVIDER.get().map(|arc| arc.as_ref())
187    }
188}
189
190/// Secp256k1 cryptographic functions.
191#[cfg(any(feature = "secp256k1", feature = "k256"))]
192pub mod secp256k1 {
193    pub use imp::{public_key_to_address, sign_message};
194
195    use super::*;
196    use alloy_primitives::{Address, B256};
197
198    #[cfg(not(feature = "secp256k1"))]
199    use super::impl_k256 as imp;
200    #[cfg(feature = "secp256k1")]
201    use super::impl_secp256k1 as imp;
202
203    /// Recover signer from message hash, _without ensuring that the signature has a low `s`
204    /// value_.
205    ///
206    /// Using this for signature validation will succeed, even if the signature is malleable or not
207    /// compliant with EIP-2. This is provided for compatibility with old signatures which have
208    /// large `s` values.
209    pub fn recover_signer_unchecked(
210        signature: &Signature,
211        hash: B256,
212    ) -> Result<Address, RecoveryError> {
213        let mut sig: [u8; 65] = [0; 65];
214
215        sig[0..32].copy_from_slice(&signature.r().to_be_bytes::<32>());
216        sig[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
217        sig[64] = signature.v() as u8;
218
219        // Try dynamic backend first when crypto-backend feature is enabled
220        #[cfg(feature = "crypto-backend")]
221        if let Some(provider) = super::backend::try_get_provider() {
222            return provider.recover_signer_unchecked(&sig, &hash.0);
223        }
224
225        // Fallback to compile-time selected implementation
226        // NOTE: we are removing error from underlying crypto library as it will restrain primitive
227        // errors and we care only if recovery is passing or not.
228        imp::recover_signer_unchecked(&sig, &hash.0).map_err(|_| RecoveryError::new())
229    }
230
231    /// Recover signer address from message hash. This ensures that the signature S value is
232    /// lower than `secp256k1n / 2`, as specified in
233    /// [EIP-2](https://eips.ethereum.org/EIPS/eip-2).
234    ///
235    /// If the S value is too large, then this will return a `RecoveryError`
236    pub fn recover_signer(signature: &Signature, hash: B256) -> Result<Address, RecoveryError> {
237        if signature.s() > SECP256K1N_HALF {
238            return Err(RecoveryError::from_source(InvalidSignatureS));
239        }
240        recover_signer_unchecked(signature, hash)
241    }
242}
243
244#[cfg(feature = "secp256k1")]
245mod impl_secp256k1 {
246    pub(crate) use ::secp256k1::Error;
247    use ::secp256k1::{
248        ecdsa::{RecoverableSignature, RecoveryId},
249        Message, PublicKey, SecretKey, SECP256K1,
250    };
251    use alloy_primitives::{keccak256, Address, Signature, B256, U256};
252
253    /// Recovers the address of the sender using secp256k1 pubkey recovery.
254    ///
255    /// Converts the public key into an ethereum address by hashing the public key with keccak256.
256    ///
257    /// This does not ensure that the `s` value in the signature is low, and _just_ wraps the
258    /// underlying secp256k1 library.
259    pub(crate) fn recover_signer_unchecked(
260        sig: &[u8; 65],
261        msg: &[u8; 32],
262    ) -> Result<Address, Error> {
263        let sig =
264            RecoverableSignature::from_compact(&sig[0..64], RecoveryId::try_from(sig[64] as i32)?)?;
265
266        let public = SECP256K1.recover_ecdsa(&Message::from_digest(*msg), &sig)?;
267        Ok(public_key_to_address(public))
268    }
269
270    /// Signs message with the given secret key.
271    /// Returns the corresponding signature.
272    pub fn sign_message(secret: B256, message: B256) -> Result<Signature, Error> {
273        let sec = SecretKey::from_slice(secret.as_ref())?;
274        let s = SECP256K1.sign_ecdsa_recoverable(&Message::from_digest(message.0), &sec);
275        let (rec_id, data) = s.serialize_compact();
276
277        let signature = Signature::new(
278            U256::try_from_be_slice(&data[..32]).expect("The slice has at most 32 bytes"),
279            U256::try_from_be_slice(&data[32..64]).expect("The slice has at most 32 bytes"),
280            i32::from(rec_id) != 0,
281        );
282        Ok(signature)
283    }
284
285    /// Converts a public key into an ethereum address by hashing the encoded public key with
286    /// keccak256.
287    pub fn public_key_to_address(public: PublicKey) -> Address {
288        // strip out the first byte because that should be the SECP256K1_TAG_PUBKEY_UNCOMPRESSED
289        // tag returned by libsecp's uncompressed pubkey serialization
290        let hash = keccak256(&public.serialize_uncompressed()[1..]);
291        Address::from_slice(&hash[12..])
292    }
293}
294
295#[cfg(feature = "k256")]
296#[cfg_attr(feature = "secp256k1", allow(unused, unreachable_pub))]
297mod impl_k256 {
298    pub(crate) use k256::ecdsa::Error;
299
300    use super::*;
301    use alloy_primitives::{keccak256, Address, B256};
302    use k256::ecdsa::{RecoveryId, SigningKey, VerifyingKey};
303
304    /// Recovers the address of the sender using secp256k1 pubkey recovery.
305    ///
306    /// Converts the public key into an ethereum address by hashing the public key with keccak256.
307    ///
308    /// This does not ensure that the `s` value in the signature is low, and _just_ wraps the
309    /// underlying secp256k1 library.
310    pub(crate) fn recover_signer_unchecked(
311        sig: &[u8; 65],
312        msg: &[u8; 32],
313    ) -> Result<Address, Error> {
314        let mut signature = k256::ecdsa::Signature::from_slice(&sig[0..64])?;
315        let mut recid = sig[64];
316
317        // normalize signature and flip recovery id if needed.
318        if let Some(sig_normalized) = signature.normalize_s() {
319            signature = sig_normalized;
320            recid ^= 1;
321        }
322        let recid = RecoveryId::from_byte(recid).expect("recovery ID is valid");
323
324        // recover key
325        let recovered_key = VerifyingKey::recover_from_prehash(&msg[..], &signature, recid)?;
326        Ok(public_key_to_address(recovered_key))
327    }
328
329    /// Signs message with the given secret key.
330    /// Returns the corresponding signature.
331    pub fn sign_message(secret: B256, message: B256) -> Result<Signature, Error> {
332        let sec = SigningKey::from_slice(secret.as_ref())?;
333        sec.sign_prehash_recoverable(&message.0).map(Into::into)
334    }
335
336    /// Converts a public key into an ethereum address by hashing the encoded public key with
337    /// keccak256.
338    pub fn public_key_to_address(public: VerifyingKey) -> Address {
339        let hash = keccak256(&public.to_encoded_point(/* compress = */ false).as_bytes()[1..]);
340        Address::from_slice(&hash[12..])
341    }
342}
343
344#[cfg(test)]
345mod tests {
346
347    #[cfg(feature = "secp256k1")]
348    #[test]
349    fn sanity_ecrecover_call_secp256k1() {
350        use super::impl_secp256k1::*;
351        use alloy_primitives::B256;
352
353        let (secret, public) = secp256k1::generate_keypair(&mut rand::thread_rng());
354        let signer = public_key_to_address(public);
355
356        let message = b"hello world";
357        let hash = alloy_primitives::keccak256(message);
358        let signature =
359            sign_message(B256::from_slice(&secret.secret_bytes()[..]), hash).expect("sign message");
360
361        let mut sig: [u8; 65] = [0; 65];
362        sig[0..32].copy_from_slice(&signature.r().to_be_bytes::<32>());
363        sig[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
364        sig[64] = signature.v() as u8;
365
366        assert_eq!(recover_signer_unchecked(&sig, &hash), Ok(signer));
367    }
368
369    #[cfg(feature = "k256")]
370    #[test]
371    fn sanity_ecrecover_call_k256() {
372        use super::impl_k256::*;
373        use alloy_primitives::B256;
374
375        let secret = k256::ecdsa::SigningKey::random(&mut rand::thread_rng());
376        let public = *secret.verifying_key();
377        let signer = public_key_to_address(public);
378
379        let message = b"hello world";
380        let hash = alloy_primitives::keccak256(message);
381        let signature =
382            sign_message(B256::from_slice(&secret.to_bytes()[..]), hash).expect("sign message");
383
384        let mut sig: [u8; 65] = [0; 65];
385        sig[0..32].copy_from_slice(&signature.r().to_be_bytes::<32>());
386        sig[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
387        sig[64] = signature.v() as u8;
388
389        assert_eq!(recover_signer_unchecked(&sig, &hash).ok(), Some(signer));
390    }
391
392    #[test]
393    #[cfg(all(feature = "secp256k1", feature = "k256"))]
394    fn sanity_secp256k1_k256_compat() {
395        use super::{impl_k256, impl_secp256k1};
396        use alloy_primitives::B256;
397
398        let (secp256k1_secret, secp256k1_public) =
399            secp256k1::generate_keypair(&mut rand::thread_rng());
400        let k256_secret = k256::ecdsa::SigningKey::from_slice(&secp256k1_secret.secret_bytes())
401            .expect("k256 secret");
402        let k256_public = *k256_secret.verifying_key();
403
404        let secp256k1_signer = impl_secp256k1::public_key_to_address(secp256k1_public);
405        let k256_signer = impl_k256::public_key_to_address(k256_public);
406        assert_eq!(secp256k1_signer, k256_signer);
407
408        let message = b"hello world";
409        let hash = alloy_primitives::keccak256(message);
410
411        let secp256k1_signature = impl_secp256k1::sign_message(
412            B256::from_slice(&secp256k1_secret.secret_bytes()[..]),
413            hash,
414        )
415        .expect("secp256k1 sign");
416        let k256_signature =
417            impl_k256::sign_message(B256::from_slice(&k256_secret.to_bytes()[..]), hash)
418                .expect("k256 sign");
419        assert_eq!(secp256k1_signature, k256_signature);
420
421        let mut sig: [u8; 65] = [0; 65];
422
423        sig[0..32].copy_from_slice(&secp256k1_signature.r().to_be_bytes::<32>());
424        sig[32..64].copy_from_slice(&secp256k1_signature.s().to_be_bytes::<32>());
425        sig[64] = secp256k1_signature.v() as u8;
426        let secp256k1_recovered =
427            impl_secp256k1::recover_signer_unchecked(&sig, &hash).expect("secp256k1 recover");
428        assert_eq!(secp256k1_recovered, secp256k1_signer);
429
430        sig[0..32].copy_from_slice(&k256_signature.r().to_be_bytes::<32>());
431        sig[32..64].copy_from_slice(&k256_signature.s().to_be_bytes::<32>());
432        sig[64] = k256_signature.v() as u8;
433        let k256_recovered =
434            impl_k256::recover_signer_unchecked(&sig, &hash).expect("k256 recover");
435        assert_eq!(k256_recovered, k256_signer);
436
437        assert_eq!(secp256k1_recovered, k256_recovered);
438    }
439
440    #[cfg(feature = "crypto-backend")]
441    mod backend_tests {
442        use crate::crypto::{backend::CryptoProvider, RecoveryError};
443        use alloc::sync::Arc;
444        use alloy_primitives::{Address, Signature, B256};
445
446        /// Mock crypto provider for testing
447        struct MockCryptoProvider {
448            should_fail: bool,
449            return_address: Address,
450        }
451
452        impl CryptoProvider for MockCryptoProvider {
453            fn recover_signer_unchecked(
454                &self,
455                _sig: &[u8; 65],
456                _msg: &[u8; 32],
457            ) -> Result<Address, RecoveryError> {
458                if self.should_fail {
459                    Err(RecoveryError::new())
460                } else {
461                    Ok(self.return_address)
462                }
463            }
464        }
465
466        #[test]
467        fn test_crypto_backend_basic_functionality() {
468            // Test that when a provider is installed, it's actually used
469            let custom_address = Address::from([0x99; 20]); // Unique test address
470            let provider =
471                Arc::new(MockCryptoProvider { should_fail: false, return_address: custom_address });
472
473            // Try to install the provider (may fail if already set from other tests)
474            let install_result = crate::crypto::backend::install_default_provider(provider);
475
476            // Create test signature and hash
477            let signature = Signature::new(
478                alloy_primitives::U256::from(123u64),
479                alloy_primitives::U256::from(456u64),
480                false,
481            );
482            let hash = B256::from([0xAB; 32]);
483
484            // Call the high-level function
485            let result = crate::crypto::secp256k1::recover_signer_unchecked(&signature, hash);
486
487            // If our provider was successfully installed, we should get our custom address
488            if install_result.is_ok() {
489                assert!(result.is_ok());
490                assert_eq!(result.unwrap(), custom_address);
491            }
492            // If provider was already set, we still should get a valid result
493            else {
494                assert!(result.is_ok()); // Should work with any provider
495            }
496        }
497
498        #[test]
499        fn test_provider_already_set_error() {
500            // First installation might work or fail if already set from another test
501            // Since tests are ran in parallel.
502            let provider1 = Arc::new(MockCryptoProvider {
503                should_fail: false,
504                return_address: Address::from([0x11; 20]),
505            });
506            let _result1 = crate::crypto::backend::install_default_provider(provider1);
507
508            // Second installation should always fail since OnceLock can only be set once
509            let provider2 = Arc::new(MockCryptoProvider {
510                should_fail: true,
511                return_address: Address::from([0x22; 20]),
512            });
513            let result2 = crate::crypto::backend::install_default_provider(provider2);
514
515            // The second attempt should fail with CryptoProviderAlreadySetError
516            assert!(result2.is_err());
517
518            // The error should contain the provider we tried to install (provider2)
519            if let Err(err) = result2 {
520                // We can't easily compare Arc pointers due to type erasure,
521                // but we can verify the error contains a valid provider
522                // (just by accessing it without panicking)
523                let _provider_ref = err.provider.as_ref();
524            }
525        }
526    }
527}