Skip to main content

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