-
Notifications
You must be signed in to change notification settings - Fork 609
feat: Add dynamic crypto backend for ecrecover #2634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9aed6f6
6eb586d
fca2136
ccfd65c
2a0fa74
4c49d44
7e53da5
7069a4c
e455847
d106417
e5114be
0c6763e
0247aac
2fbccb1
3c5bb1b
afe0f1c
e062a71
ebf53ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Address, RecoveryError> { | ||
| /// // 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<Address, RecoveryError>; | ||
| } | ||
|
|
||
| /// Global default crypto provider. | ||
| #[cfg(feature = "std")] | ||
| static DEFAULT_PROVIDER: OnceLock<Arc<dyn CryptoProvider>> = OnceLock::new(); | ||
|
|
||
| #[cfg(not(feature = "std"))] | ||
| static DEFAULT_PROVIDER: OnceBox<Arc<dyn CryptoProvider>> = 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<dyn CryptoProvider>, | ||
| } | ||
|
|
||
| impl core::fmt::Debug for CryptoProviderAlreadySetError { | ||
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| f.debug_struct("CryptoProviderAlreadySetError") | ||
| .field("provider", &"<crypto 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<dyn CryptoProvider>, | ||
| ) -> 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); | ||
| } | ||
|
Comment on lines
+227
to
+230
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If crypto-baackend feature-flag is set, then this will call whatever backend is set here. To install your own backend, you can call
Comment on lines
+226
to
+230
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems okay
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A different strategy would be to make it such that the crypto-backend was exactly like the other two backends. It would need to have a The downside of this approach is that the fallback would need to be defined in the crypto-backend module or we could just return an error, if no backend has been installed
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
this I'd like to avoid because this will be very unpleasant for users that just want to do some simple rpc stuff |
||
|
|
||
| // 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<Address, RecoveryError> { | ||
| 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(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.