diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml
index 05baa0b23d3..9cbe882ea83 100644
--- a/crates/consensus/Cargo.toml
+++ b/crates/consensus/Cargo.toml
@@ -91,6 +91,7 @@ std = [
]
k256 = ["dep:k256", "alloy-primitives/k256", "alloy-eips/k256"]
secp256k1 = ["dep:secp256k1"]
+crypto-backend = []
kzg = ["dep:c-kzg", "alloy-eips/kzg", "std"]
arbitrary = [
"std",
diff --git a/crates/consensus/src/crypto.rs b/crates/consensus/src/crypto.rs
index c43398f71b4..95351e4623f 100644
--- a/crates/consensus/src/crypto.rs
+++ b/crates/consensus/src/crypto.rs
@@ -6,6 +6,9 @@ use alloy_primitives::U256;
#[cfg(any(feature = "secp256k1", feature = "k256"))]
use alloy_primitives::Signature;
+#[cfg(feature = "crypto-backend")]
+pub use backend::{install_default_provider, CryptoProvider, CryptoProviderAlreadySetError};
+
/// Error for signature S.
#[derive(Debug, thiserror::Error)]
#[error("signature S value is greater than `secp256k1n / 2`")]
@@ -49,6 +52,148 @@ pub const SECP256K1N_HALF: U256 = U256::from_be_bytes([
0x5D, 0x57, 0x6E, 0x73, 0x57, 0xA4, 0x50, 0x1D, 0xDF, 0xE9, 0x2F, 0x46, 0x68, 0x1B, 0x20, 0xA0,
]);
+/// Crypto backend module for pluggable cryptographic implementations.
+#[cfg(feature = "crypto-backend")]
+pub mod backend {
+ use super::*;
+ use alloc::sync::Arc;
+ use alloy_primitives::Address;
+
+ #[cfg(feature = "std")]
+ use std::sync::OnceLock;
+
+ #[cfg(not(feature = "std"))]
+ use once_cell::race::OnceBox;
+
+ /// Trait for cryptographic providers that can perform signature recovery.
+ ///
+ /// This trait allows pluggable cryptographic backends for Ethereum signature recovery.
+ /// By default, alloy uses compile-time selected implementations (secp256k1 or k256),
+ /// but applications can install a custom provider to override this behavior.
+ ///
+ /// # Why is this needed?
+ ///
+ /// The primary reason is performance - when targeting special execution environments
+ /// that require custom cryptographic logic. For example, zkVMs (zero-knowledge virtual
+ /// machines) may have special accelerators that would allow them to perform signature
+ /// recovery faster.
+ ///
+ /// # Usage
+ ///
+ /// 1. Enable the `crypto-backend` feature in your `Cargo.toml`
+ /// 2. Implement the `CryptoProvider` trait for your custom backend
+ /// 3. Install it globally using [`install_default_provider`]
+ /// 4. All subsequent signature recovery operations will use your provider
+ ///
+ /// Note: This trait currently only provides signature recovery functionality,
+ /// not signature creation. For signature creation, use the compile-time selected
+ /// implementations in the [`secp256k1`] module.
+ ///
+ /// ```rust,ignore
+ /// use alloy_consensus::crypto::backend::{CryptoProvider, install_default_provider};
+ /// use alloy_primitives::Address;
+ /// use alloc::sync::Arc;
+ ///
+ /// struct MyCustomProvider;
+ ///
+ /// impl CryptoProvider for MyCustomProvider {
+ /// fn recover_signer_unchecked(
+ /// &self,
+ /// sig: &[u8; 65],
+ /// msg: &[u8; 32],
+ /// ) -> Result
{
+ /// // Your custom implementation here
+ /// todo!()
+ /// }
+ /// }
+ ///
+ /// // Install your provider globally
+ /// install_default_provider(Arc::new(MyCustomProvider)).unwrap();
+ /// ```
+ pub trait CryptoProvider: Send + Sync + 'static {
+ /// Recover signer from signature and message hash, without ensuring low S values.
+ fn recover_signer_unchecked(
+ &self,
+ sig: &[u8; 65],
+ msg: &[u8; 32],
+ ) -> Result;
+ }
+
+ /// Global default crypto provider.
+ #[cfg(feature = "std")]
+ static DEFAULT_PROVIDER: OnceLock> = OnceLock::new();
+
+ #[cfg(not(feature = "std"))]
+ static DEFAULT_PROVIDER: OnceBox> = OnceBox::new();
+
+ /// Error returned when attempting to install a provider when one is already installed.
+ /// Contains the provider that was attempted to be installed.
+ pub struct CryptoProviderAlreadySetError {
+ /// The provider that was attempted to be installed.
+ pub provider: Arc,
+ }
+
+ impl core::fmt::Debug for CryptoProviderAlreadySetError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ f.debug_struct("CryptoProviderAlreadySetError")
+ .field("provider", &"")
+ .finish()
+ }
+ }
+
+ impl core::fmt::Display for CryptoProviderAlreadySetError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "crypto provider already installed")
+ }
+ }
+
+ impl core::error::Error for CryptoProviderAlreadySetError {}
+
+ /// Install the default crypto provider.
+ ///
+ /// This sets the global default provider used by the high-level crypto functions.
+ /// Returns an error containing the provider that was attempted to be installed if one is
+ /// already set.
+ pub fn install_default_provider(
+ provider: Arc,
+ ) -> Result<(), CryptoProviderAlreadySetError> {
+ #[cfg(feature = "std")]
+ {
+ DEFAULT_PROVIDER.set(provider).map_err(|provider| {
+ // Return the provider we tried to install in the error
+ CryptoProviderAlreadySetError { provider }
+ })
+ }
+ #[cfg(not(feature = "std"))]
+ {
+ DEFAULT_PROVIDER.set(Box::new(provider)).map_err(|provider| {
+ // Return the provider we tried to install in the error
+ CryptoProviderAlreadySetError { provider: *provider }
+ })
+ }
+ }
+
+ /// Get the currently installed default provider, panicking if none is installed.
+ pub fn get_default_provider() -> &'static dyn CryptoProvider {
+ try_get_provider().map_or_else(
+ || panic!("No crypto backend installed. Call install_default_provider() first."),
+ |provider| provider,
+ )
+ }
+
+ /// Try to get the currently installed default provider, returning None if none is installed.
+ pub(super) fn try_get_provider() -> Option<&'static dyn CryptoProvider> {
+ #[cfg(feature = "std")]
+ {
+ DEFAULT_PROVIDER.get().map(|arc| arc.as_ref())
+ }
+ #[cfg(not(feature = "std"))]
+ {
+ DEFAULT_PROVIDER.get().map(|arc| arc.as_ref())
+ }
+ }
+}
+
/// Secp256k1 cryptographic functions.
#[cfg(any(feature = "secp256k1", feature = "k256"))]
pub mod secp256k1 {
@@ -78,6 +223,13 @@ pub mod secp256k1 {
sig[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
sig[64] = signature.v() as u8;
+ // Try dynamic backend first when crypto-backend feature is enabled
+ #[cfg(feature = "crypto-backend")]
+ if let Some(provider) = super::backend::try_get_provider() {
+ return provider.recover_signer_unchecked(&sig, &hash.0);
+ }
+
+ // Fallback to compile-time selected implementation
// NOTE: we are removing error from underlying crypto library as it will restrain primitive
// errors and we care only if recovery is passing or not.
imp::recover_signer_unchecked(&sig, &hash.0).map_err(|_| RecoveryError::new())
@@ -291,4 +443,92 @@ mod tests {
assert_eq!(secp256k1_recovered, k256_recovered);
}
+
+ #[cfg(feature = "crypto-backend")]
+ mod backend_tests {
+ use crate::crypto::{backend::CryptoProvider, RecoveryError};
+ use alloc::sync::Arc;
+ use alloy_primitives::{Address, Signature, B256};
+
+ /// Mock crypto provider for testing
+ struct MockCryptoProvider {
+ should_fail: bool,
+ return_address: Address,
+ }
+
+ impl CryptoProvider for MockCryptoProvider {
+ fn recover_signer_unchecked(
+ &self,
+ _sig: &[u8; 65],
+ _msg: &[u8; 32],
+ ) -> Result {
+ if self.should_fail {
+ Err(RecoveryError::new())
+ } else {
+ Ok(self.return_address)
+ }
+ }
+ }
+
+ #[test]
+ fn test_crypto_backend_basic_functionality() {
+ // Test that when a provider is installed, it's actually used
+ let custom_address = Address::from([0x99; 20]); // Unique test address
+ let provider =
+ Arc::new(MockCryptoProvider { should_fail: false, return_address: custom_address });
+
+ // Try to install the provider (may fail if already set from other tests)
+ let install_result = crate::crypto::backend::install_default_provider(provider);
+
+ // Create test signature and hash
+ let signature = Signature::new(
+ alloy_primitives::U256::from(123u64),
+ alloy_primitives::U256::from(456u64),
+ false,
+ );
+ let hash = B256::from([0xAB; 32]);
+
+ // Call the high-level function
+ let result = crate::crypto::secp256k1::recover_signer_unchecked(&signature, hash);
+
+ // If our provider was successfully installed, we should get our custom address
+ if install_result.is_ok() {
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), custom_address);
+ }
+ // If provider was already set, we still should get a valid result
+ else {
+ assert!(result.is_ok()); // Should work with any provider
+ }
+ }
+
+ #[test]
+ fn test_provider_already_set_error() {
+ // First installation might work or fail if already set from another test
+ // Since tests are ran in parallel.
+ let provider1 = Arc::new(MockCryptoProvider {
+ should_fail: false,
+ return_address: Address::from([0x11; 20]),
+ });
+ let _result1 = crate::crypto::backend::install_default_provider(provider1);
+
+ // Second installation should always fail since OnceLock can only be set once
+ let provider2 = Arc::new(MockCryptoProvider {
+ should_fail: true,
+ return_address: Address::from([0x22; 20]),
+ });
+ let result2 = crate::crypto::backend::install_default_provider(provider2);
+
+ // The second attempt should fail with CryptoProviderAlreadySetError
+ assert!(result2.is_err());
+
+ // The error should contain the provider we tried to install (provider2)
+ if let Err(err) = result2 {
+ // We can't easily compare Arc pointers due to type erasure,
+ // but we can verify the error contains a valid provider
+ // (just by accessing it without panicking)
+ let _provider_ref = err.provider.as_ref();
+ }
+ }
+ }
}