From 19eca03857ff95b2f967701c8665fa7a1c0eba15 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 31 Jan 2024 16:52:48 -0500 Subject: [PATCH] feat: add blockchain API provider --- relay_rpc/src/auth/cacao.rs | 2 +- relay_rpc/src/auth/cacao/signature.rs | 190 ------------------ .../cacao/signature/eip1271/blockchain_api.rs | 58 ++++++ .../cacao/signature/eip1271/get_rpc_url.rs | 5 + .../src/auth/cacao/signature/eip1271/mod.rs | 98 +++++++++ relay_rpc/src/auth/cacao/signature/eip191.rs | 39 ++++ relay_rpc/src/auth/cacao/signature/mod.rs | 63 ++++++ relay_rpc/src/auth/cacao/tests.rs | 2 +- 8 files changed, 265 insertions(+), 192 deletions(-) delete mode 100644 relay_rpc/src/auth/cacao/signature.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip1271/mod.rs create mode 100644 relay_rpc/src/auth/cacao/signature/eip191.rs create mode 100644 relay_rpc/src/auth/cacao/signature/mod.rs diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index a8bb5b7..52bd759 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -2,7 +2,7 @@ use { self::{ header::Header, payload::Payload, - signature::{GetRpcUrl, Signature}, + signature::{eip1271::get_rpc_url::GetRpcUrl, Signature}, }, core::fmt::Debug, serde::{Deserialize, Serialize}, diff --git a/relay_rpc/src/auth/cacao/signature.rs b/relay_rpc/src/auth/cacao/signature.rs deleted file mode 100644 index 892aaca..0000000 --- a/relay_rpc/src/auth/cacao/signature.rs +++ /dev/null @@ -1,190 +0,0 @@ -use { - super::{Cacao, CacaoError}, - alloy_primitives::{Address, FixedBytes}, - alloy_providers::provider::{Provider, TempProvider}, - alloy_rpc_types::{CallInput, CallRequest}, - alloy_sol_types::{sol, SolCall}, - alloy_transport_http::Http, - serde::{Deserialize, Serialize}, - sha3::{Digest, Keccak256}, - std::str::FromStr, - url::Url, -}; - -pub const EIP191: &str = "eip191"; -pub const EIP1271: &str = "eip1271"; - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] -pub struct Signature { - pub t: String, - pub s: String, -} - -pub trait GetRpcUrl { - fn get_rpc_url(&self, chain_id: String) -> Option; -} - -impl Signature { - pub async fn verify( - &self, - cacao: &Cacao, - get_provider: &impl GetRpcUrl, - ) -> Result { - let address = cacao.p.address()?; - - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) - .map_err(|_| CacaoError::Verification)?; - - let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); - - match self.t.as_str() { - EIP191 => Eip191.verify(&signature, &address, hash), - EIP1271 => { - let chain_id = cacao.p.chain_id_reference()?; - let provider = get_provider.get_rpc_url(chain_id); - if let Some(provider) = provider { - Eip1271 - .verify( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider, - ) - .await - } else { - Err(CacaoError::ProviderNotAvailable) - } - } - _ => Err(CacaoError::UnsupportedSignature), - } - } -} - -pub fn eip191_bytes(message: &str) -> Vec { - format!( - "\u{0019}Ethereum Signed Message:\n{}{}", - message.as_bytes().len(), - message - ) - .into() -} - -pub struct Eip191; - -impl Eip191 { - fn verify(&self, signature: &[u8], address: &str, hash: Keccak256) -> Result { - use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; - - let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; - let recovery_id = - RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; - - let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) - .map_err(|_| CacaoError::Verification)?; - - let add = &Keccak256::default() - .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) - .finalize()[12..]; - - let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); - - if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { - Err(CacaoError::Verification) - } else { - Ok(true) - } - } -} - -/// Remove the "0x" prefix from a hex string. -fn strip_hex_prefix(s: &str) -> &str { - s.strip_prefix("0x").unwrap_or(s) -} - -pub struct Eip1271; - -// https://eips.ethereum.org/EIPS/eip-1271 -const MAGIC_VALUE: u32 = 0x1626ba7e; -sol! { - function isValidSignature( - bytes32 _hash, - bytes memory _signature) - public - view - returns (bytes4 magicValue); -} - -impl Eip1271 { - async fn verify( - &self, - signature: Vec, - address: Address, - hash: &[u8; 32], - provider: Url, - ) -> Result { - let provider = Provider::new(Http::new(provider)); - - let call_request = CallRequest { - to: Some(address), - input: CallInput::new( - isValidSignatureCall { - _hash: FixedBytes::from(hash), - _signature: signature, - } - .abi_encode() - .into(), - ), - ..Default::default() - }; - - let result = provider.call(call_request, None).await.map_err(|e| { - if let Some(error_response) = e.as_error_resp() { - if error_response.message.starts_with("execution reverted:") { - CacaoError::Verification - } else { - CacaoError::Eip1271Internal(e) - } - } else { - CacaoError::Eip1271Internal(e) - } - })?; - - if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(true) - } else { - Err(CacaoError::Verification) - } - } -} - -#[cfg(test)] -mod test { - use {super::*, alloy_primitives::address}; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip1271() { - let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - let signature = "xxx"; - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification) - .unwrap(); - let message = "xxx"; - let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] - .try_into() - .unwrap(); - let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" - .parse() - .unwrap(); - assert!(Eip1271 - .verify(signature, address, hash, provider) - .await - .unwrap()); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs new file mode 100644 index 0000000..233759c --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/blockchain_api.rs @@ -0,0 +1,58 @@ +use {super::get_rpc_url::GetRpcUrl, crate::domain::ProjectId, url::Url}; + +// https://github.com/WalletConnect/blockchain-api/blob/master/SUPPORTED_CHAINS.md +const SUPPORTED_CHAINS: [&str; 26] = [ + "eip155:1", + "eip155:5", + "eip155:11155111", + "eip155:10", + "eip155:420", + "eip155:42161", + "eip155:421613", + "eip155:137", + "eip155:80001", + "eip155:1101", + "eip155:42220", + "eip155:1313161554", + "eip155:1313161555", + "eip155:56", + "eip155:56", + "eip155:43114", + "eip155:43113", + "eip155:324", + "eip155:280", + "near", + "eip155:100", + "solana:4sgjmw1sunhzsxgspuhpqldx6wiyjntz", + "eip155:8453", + "eip155:84531", + "eip155:7777777", + "eip155:999", +]; + +pub struct BlockchainApiProvider { + project_id: ProjectId, +} + +impl BlockchainApiProvider { + pub fn new(project_id: ProjectId) -> Self { + Self { project_id } + } +} + +impl GetRpcUrl for BlockchainApiProvider { + fn get_rpc_url(&self, chain_id: String) -> Option { + if SUPPORTED_CHAINS.contains(&chain_id.as_str()) { + Some( + format!( + "https://rpc.walletconnect.com/v1?chainId={chain_id}&projectId={}", + self.project_id + ) + .parse() + .expect("Provider URL should be valid"), + ) + } else { + None + } + } +} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs new file mode 100644 index 0000000..9639c17 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs @@ -0,0 +1,5 @@ +use url::Url; + +pub trait GetRpcUrl { + fn get_rpc_url(&self, chain_id: String) -> Option; +} diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs new file mode 100644 index 0000000..5c6184b --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs @@ -0,0 +1,98 @@ +use { + super::CacaoError, + alloy_primitives::{Address, FixedBytes}, + alloy_providers::provider::{Provider, TempProvider}, + alloy_rpc_types::{CallInput, CallRequest}, + alloy_sol_types::{sol, SolCall}, + alloy_transport_http::Http, + url::Url, +}; + +pub mod blockchain_api; +pub mod get_rpc_url; + +pub const EIP1271: &str = "eip1271"; + +// https://eips.ethereum.org/EIPS/eip-1271 +const MAGIC_VALUE: u32 = 0x1626ba7e; +sol! { + function isValidSignature( + bytes32 _hash, + bytes memory _signature) + public + view + returns (bytes4 magicValue); +} + +pub async fn verify_eip1271( + signature: Vec, + address: Address, + hash: &[u8; 32], + provider: Url, +) -> Result { + let provider = Provider::new(Http::new(provider)); + + let call_request = CallRequest { + to: Some(address), + input: CallInput::new( + isValidSignatureCall { + _hash: FixedBytes::from(hash), + _signature: signature, + } + .abi_encode() + .into(), + ), + ..Default::default() + }; + + let result = provider.call(call_request, None).await.map_err(|e| { + if let Some(error_response) = e.as_error_resp() { + if error_response.message.starts_with("execution reverted:") { + CacaoError::Verification + } else { + CacaoError::Eip1271Internal(e) + } + } else { + CacaoError::Eip1271Internal(e) + } + })?; + + if result[..4] == MAGIC_VALUE.to_be_bytes().to_vec() { + Ok(true) + } else { + Err(CacaoError::Verification) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::auth::cacao::signature::{eip191::eip191_bytes, strip_hex_prefix}, + alloy_primitives::address, + sha3::{Digest, Keccak256}, + }; + + // Manual test. Paste address, signature, message, and project ID to verify + // function + #[tokio::test] + #[ignore] + async fn test_eip1271() { + let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let signature = "xxx"; + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(signature).as_bytes()) + .map_err(|_| CacaoError::Verification) + .unwrap(); + let message = "xxx"; + let hash = &Keccak256::new_with_prefix(eip191_bytes(message)).finalize()[..] + .try_into() + .unwrap(); + let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" + .parse() + .unwrap(); + assert!(verify_eip1271(signature, address, hash, provider) + .await + .unwrap()); + } +} diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs new file mode 100644 index 0000000..4cb180d --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -0,0 +1,39 @@ +use { + super::CacaoError, + crate::auth::cacao::signature::strip_hex_prefix, + sha3::{Digest, Keccak256}, +}; + +pub const EIP191: &str = "eip191"; + +pub fn eip191_bytes(message: &str) -> Vec { + format!( + "\u{0019}Ethereum Signed Message:\n{}{}", + message.as_bytes().len(), + message + ) + .into() +} + +pub fn verify_eip191(signature: &[u8], address: &str, hash: Keccak256) -> Result { + use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; + + let sig = Sig::try_from(&signature[..64]).map_err(|_| CacaoError::Verification)?; + let recovery_id = + RecoveryId::try_from(&signature[64] % 27).map_err(|_| CacaoError::Verification)?; + + let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) + .map_err(|_| CacaoError::Verification)?; + + let add = &Keccak256::default() + .chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) + .finalize()[12..]; + + let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); + + if address_encoded.to_lowercase() != strip_hex_prefix(address).to_lowercase() { + Err(CacaoError::Verification) + } else { + Ok(true) + } +} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs new file mode 100644 index 0000000..bd2c4b2 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -0,0 +1,63 @@ +use { + self::{ + eip1271::{get_rpc_url::GetRpcUrl, verify_eip1271, EIP1271}, + eip191::{eip191_bytes, verify_eip191, EIP191}, + }, + super::{Cacao, CacaoError}, + alloy_primitives::Address, + serde::{Deserialize, Serialize}, + sha3::{Digest, Keccak256}, + std::str::FromStr, +}; + +pub mod eip1271; +pub mod eip191; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] +pub struct Signature { + pub t: String, + pub s: String, +} + +impl Signature { + pub async fn verify( + &self, + cacao: &Cacao, + get_provider: &impl GetRpcUrl, + ) -> Result { + let address = cacao.p.address()?; + + let signature = data_encoding::HEXLOWER_PERMISSIVE + .decode(strip_hex_prefix(&cacao.s.s).as_bytes()) + .map_err(|_| CacaoError::Verification)?; + + let hash = Keccak256::new_with_prefix(eip191_bytes(&cacao.siwe_message()?)); + + match self.t.as_str() { + EIP191 => verify_eip191(&signature, &address, hash), + EIP1271 => { + let chain_id = cacao.p.chain_id_reference()?; + let provider = get_provider.get_rpc_url(chain_id); + if let Some(provider) = provider { + verify_eip1271( + signature, + Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, + &hash.finalize()[..] + .try_into() + .expect("hash length is 32 bytes"), + provider, + ) + .await + } else { + Err(CacaoError::ProviderNotAvailable) + } + } + _ => Err(CacaoError::UnsupportedSignature), + } + } +} + +/// Remove the "0x" prefix from a hex string. +fn strip_hex_prefix(s: &str) -> &str { + s.strip_prefix("0x").unwrap_or(s) +} diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 644deee..74c01d4 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,4 +1,4 @@ -use {super::signature::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; +use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; struct MockGetRpcUrl;