From 628b3e37422a48d79c342110c8713e76b94aab8c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 27 Apr 2024 20:05:04 -0400 Subject: [PATCH 1/8] fix: use erc6492 lib and remove dedicated ERC-1271 verification logic --- relay_rpc/Cargo.toml | 25 +- relay_rpc/build.rs | 98 ---- relay_rpc/contracts/Create2.sol | 49 -- relay_rpc/contracts/Eip1271Mock.sol | 81 --- relay_rpc/contracts/Eip6492.sol | 111 ---- relay_rpc/src/auth/cacao.rs | 23 +- relay_rpc/src/auth/cacao/signature/eip1271.rs | 224 -------- relay_rpc/src/auth/cacao/signature/eip191.rs | 51 +- relay_rpc/src/auth/cacao/signature/eip6492.rs | 482 ------------------ relay_rpc/src/auth/cacao/signature/mod.rs | 90 ++-- .../src/auth/cacao/signature/test_helpers.rs | 102 ---- 11 files changed, 61 insertions(+), 1275 deletions(-) delete mode 100644 relay_rpc/build.rs delete mode 100644 relay_rpc/contracts/Create2.sol delete mode 100644 relay_rpc/contracts/Eip1271Mock.sol delete mode 100644 relay_rpc/contracts/Eip6492.sol delete mode 100644 relay_rpc/src/auth/cacao/signature/eip1271.rs delete mode 100644 relay_rpc/src/auth/cacao/signature/eip6492.rs delete mode 100644 relay_rpc/src/auth/cacao/signature/test_helpers.rs diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index c9ff0d7..677a92a 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -5,19 +5,12 @@ edition = "2021" license = "Apache-2.0" [features] +default = ["cacao"] cacao = [ "dep:k256", - "dep:sha3", "dep:alloy-provider", - "dep:alloy-transport", - "dep:alloy-transport-http", - "dep:alloy-rpc-types", - "dep:alloy-json-rpc", - "dep:alloy-json-abi", - "dep:alloy-sol-types", "dep:alloy-primitives", - "dep:alloy-node-bindings", - "dep:alloy-contract" + "dep:erc6492", ] [dependencies] @@ -43,27 +36,15 @@ regex = "1.7" once_cell = "1.16" jsonwebtoken = "8.1" k256 = { version = "0.13", optional = true } -sha3 = { version = "0.10", optional = true } sha2 = { version = "0.10.6" } url = "2" alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-contract = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } -alloy-json-abi = { version = "0.7.0", optional = true } -alloy-sol-types = { version = "0.7.0", optional = true } alloy-primitives = { version = "0.7.0", optional = true } +erc6492 = { git = "https://github.com/WalletConnect/erc6492.git", optional = true } strum = { version = "0.26", features = ["strum_macros", "derive"] } [dev-dependencies] tokio = { version = "1.35.1", features = ["test-util", "macros"] } -[build-dependencies] -serde_json = "1.0" -hex = "0.4.3" - [lints.clippy] indexing_slicing = "deny" diff --git a/relay_rpc/build.rs b/relay_rpc/build.rs deleted file mode 100644 index 6d8b1a1..0000000 --- a/relay_rpc/build.rs +++ /dev/null @@ -1,98 +0,0 @@ -use { - serde_json::Value, - std::process::{Command, Stdio}, -}; - -fn main() { - #[cfg(feature = "cacao")] - build_contracts(); -} - -fn build_contracts() { - println!("cargo::rerun-if-changed=contracts"); - install_foundry(); - compile_contracts(); - extract_bytecodes(); -} - -fn format_foundry_dir(path: &str) -> String { - format!( - "{}/../../../../.foundry/{}", - std::env::var("OUT_DIR").unwrap(), - path - ) -} - -fn install_foundry() { - let bin_folder = format_foundry_dir("bin"); - std::fs::remove_dir_all(&bin_folder).ok(); - std::fs::create_dir_all(&bin_folder).unwrap(); - let output = Command::new("bash") - .args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .unwrap(); - println!("foundryup status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("foundryup stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("foundryup stderr: {stderr:?}"); - assert!(output.status.success()); -} - -fn compile_contracts() { - let output = Command::new(format_foundry_dir("bin/forge")) - .args([ - "build", - "--contracts=relay_rpc/contracts", - "--cache-path", - &format_foundry_dir("forge/cache"), - "--out", - &format_foundry_dir("forge/out"), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .unwrap(); - println!("forge status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("forge stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("forge stderr: {stderr:?}"); - assert!(output.status.success()); -} - -const EIP6492_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.json"; -const EIP6492_BYTECODE_FILE: &str = "forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; -const EIP1271_MOCK_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.json"; -const EIP1271_MOCK_BYTECODE_FILE: &str = "forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode"; -fn extract_bytecodes() { - extract_bytecode( - &format_foundry_dir(EIP6492_FILE), - &format_foundry_dir(EIP6492_BYTECODE_FILE), - ); - extract_bytecode( - &format_foundry_dir(EIP1271_MOCK_FILE), - &format_foundry_dir(EIP1271_MOCK_BYTECODE_FILE), - ); -} - -fn extract_bytecode(input_file: &str, output_file: &str) { - let contents = serde_json::from_slice::(&std::fs::read(input_file).unwrap()).unwrap(); - let bytecode = contents - .get("bytecode") - .unwrap() - .get("object") - .unwrap() - .as_str() - .unwrap() - .strip_prefix("0x") - .unwrap(); - let bytecode = hex::decode(bytecode).unwrap(); - std::fs::write(output_file, bytecode).unwrap(); -} diff --git a/relay_rpc/contracts/Create2.sol b/relay_rpc/contracts/Create2.sol deleted file mode 100644 index 062722e..0000000 --- a/relay_rpc/contracts/Create2.sol +++ /dev/null @@ -1,49 +0,0 @@ -// https://github.com/Genesis3800/CREATE2Factory/blob/b202029eadc0299e6e5923dd90db4200c2f7955a/src/Create2.sol - -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract Create2 { - - error Create2InsufficientBalance(uint256 received, uint256 minimumNeeded); - - error Create2EmptyBytecode(); - - error Create2FailedDeployment(); - - function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr) { - - if (msg.value < amount) { - revert Create2InsufficientBalance(msg.value, amount); - } - - if (bytecode.length == 0) { - revert Create2EmptyBytecode(); - } - - assembly { - addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) - } - - if (addr == address(0)) { - revert Create2FailedDeployment(); - } - } - - function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address addr) { - - address contractAddress = address(this); - - assembly { - let ptr := mload(0x40) - - mstore(add(ptr, 0x40), bytecodeHash) - mstore(add(ptr, 0x20), salt) - mstore(ptr, contractAddress) - let start := add(ptr, 0x0b) - mstore8(start, 0xff) - addr := keccak256(start, 85) - } - } - -} diff --git a/relay_rpc/contracts/Eip1271Mock.sol b/relay_rpc/contracts/Eip1271Mock.sol deleted file mode 100644 index 35fa8b9..0000000 --- a/relay_rpc/contracts/Eip1271Mock.sol +++ /dev/null @@ -1,81 +0,0 @@ -pragma solidity ^0.8.25; - -// https://eips.ethereum.org/EIPS/eip-1271#reference-implementation - -contract Eip1271Mock { - address owner_eoa; - - constructor(address _owner_eoa) { - owner_eoa = _owner_eoa; - } - - /** - * @notice Verifies that the signer is the owner of the signing contract. - */ - function isValidSignature( - bytes32 _hash, - bytes calldata _signature - ) external view returns (bytes4) { - // Validate signatures - if (recoverSigner(_hash, _signature) == owner_eoa) { - return 0x1626ba7e; - } else { - return 0xffffffff; - } - } - - /** - * @notice Recover the signer of hash, assuming it's an EOA account - * @dev Only for EthSign signatures - * @param _hash Hash of message that was signed - * @param _signature Signature encoded as (bytes32 r, bytes32 s, uint8 v) - */ - function recoverSigner( - bytes32 _hash, - bytes memory _signature - ) internal pure returns (address signer) { - require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length"); - - // Variables are not scoped in Solidity. - uint8 v = uint8(_signature[64]); - bytes32 r; - bytes32 s; - assembly { - // Slice the signature into r and s components - r := mload(add(_signature, 32)) - s := mload(add(_signature, 64)) - } - - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - // - // Source OpenZeppelin - // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol - - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - revert("SignatureValidator#recoverSigner: invalid signature 's' value"); - } - - if (v != 27 && v != 28) { - revert("SignatureValidator#recoverSigner: invalid signature 'v' value"); - } - - // Recover ECDSA signer - signer = ecrecover(_hash, v, r, s); - - // Prevent signer from being 0x0 - require( - signer != address(0x0), - "SignatureValidator#recoverSigner: INVALID_SIGNER" - ); - - return signer; - } -} diff --git a/relay_rpc/contracts/Eip6492.sol b/relay_rpc/contracts/Eip6492.sol deleted file mode 100644 index 41586fe..0000000 --- a/relay_rpc/contracts/Eip6492.sol +++ /dev/null @@ -1,111 +0,0 @@ -// As per ERC-1271 -interface IERC1271Wallet { - function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); -} - -error ERC1271Revert(bytes error); -error ERC6492DeployFailed(bytes error); - -contract UniversalSigValidator { - bytes32 private constant ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492; - bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e; - - function isValidSigImpl( - address _signer, - bytes32 _hash, - bytes calldata _signature, - bool allowSideEffects, - bool tryPrepare - ) public returns (bool) { - uint contractCodeLen = address(_signer).code.length; - bytes memory sigToValidate; - // The order here is strictly defined in https://eips.ethereum.org/EIPS/eip-6492 - // - ERC-6492 suffix check and verification first, while being permissive in case the contract is already deployed; if the contract is deployed we will check the sig against the deployed version, this allows 6492 signatures to still be validated while taking into account potential key rotation - // - ERC-1271 verification if there's contract code - // - finally, ecrecover - bool isCounterfactual = bytes32(_signature[_signature.length-32:_signature.length]) == ERC6492_DETECTION_SUFFIX; - if (isCounterfactual) { - address create2Factory; - bytes memory factoryCalldata; - (create2Factory, factoryCalldata, sigToValidate) = abi.decode(_signature[0:_signature.length-32], (address, bytes, bytes)); - - if (contractCodeLen == 0 || tryPrepare) { - (bool success, bytes memory err) = create2Factory.call(factoryCalldata); - if (!success) revert ERC6492DeployFailed(err); - } - } else { - sigToValidate = _signature; - } - - // Try ERC-1271 verification - if (isCounterfactual || contractCodeLen > 0) { - try IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate) returns (bytes4 magicValue) { - bool isValid = magicValue == ERC1271_SUCCESS; - - // retry, but this time assume the prefix is a prepare call - if (!isValid && !tryPrepare && contractCodeLen > 0) { - return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true); - } - - if (contractCodeLen == 0 && isCounterfactual && !allowSideEffects) { - // if the call had side effects we need to return the - // result using a `revert` (to undo the state changes) - assembly { - mstore(0, isValid) - revert(31, 1) - } - } - - return isValid; - } catch (bytes memory err) { - // retry, but this time assume the prefix is a prepare call - if (!tryPrepare && contractCodeLen > 0) { - return isValidSigImpl(_signer, _hash, _signature, allowSideEffects, true); - } - - revert ERC1271Revert(err); - } - } - - // ecrecover verification - require(_signature.length == 65, 'SignatureValidator#recoverSigner: invalid signature length'); - bytes32 r = bytes32(_signature[0:32]); - bytes32 s = bytes32(_signature[32:64]); - uint8 v = uint8(_signature[64]); - if (v != 27 && v != 28) { - revert('SignatureValidator: invalid signature v value'); - } - return ecrecover(_hash, v, r, s) == _signer; - } - - function isValidSigWithSideEffects(address _signer, bytes32 _hash, bytes calldata _signature) - external returns (bool) - { - return this.isValidSigImpl(_signer, _hash, _signature, true, false); - } - - function isValidSig(address _signer, bytes32 _hash, bytes calldata _signature) - external returns (bool) - { - try this.isValidSigImpl(_signer, _hash, _signature, false, false) returns (bool isValid) { return isValid; } - catch (bytes memory error) { - // in order to avoid side effects from the contract getting deployed, the entire call will revert with a single byte result - uint len = error.length; - if (len == 1) return error[0] == 0x01; - // all other errors are simply forwarded, but in custom formats so that nothing else can revert with a single byte in the call - else assembly { revert(error, len) } - } - } -} - -// this is a helper so we can perform validation in a single eth_call without pre-deploying a singleton -contract ValidateSigOffchain { - constructor (address _signer, bytes32 _hash, bytes memory _signature) { - UniversalSigValidator validator = new UniversalSigValidator(); - bool isValidSig = validator.isValidSigWithSideEffects(_signer, _hash, _signature); - assembly { - mstore(0, isValidSig) - return(31, 1) - } - } -} diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 179077f..9f888bc 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -4,10 +4,10 @@ use { payload::Payload, signature::{get_rpc_url::GetRpcUrl, Signature}, }, - alloy_primitives::hex::FromHexError, + alloy_primitives::AddressError, core::fmt::Debug, + erc6492::RpcError, serde::{Deserialize, Serialize}, - serde_json::value::RawValue, std::{ fmt::{Display, Write}, sync::Arc, @@ -31,16 +31,10 @@ pub enum CacaoError { PayloadResources, #[error("Invalid address")] - AddressInvalid, + AddressInvalid(AddressError), - #[error("Address not EIP-191")] - AddressNotEip191(FromHexError), - - #[error("EIP-1271 signatures not supported")] - Eip1271NotSupported, - - #[error("EIP-6492 signatures not supported")] - Eip6492NotSupported, + #[error("Provider address verification not supported")] + ProviderAddressVerificationNotSupported, #[error("Unsupported signature type")] UnsupportedSignature, @@ -51,11 +45,8 @@ pub enum CacaoError { #[error("Unable to verify")] Verification, - #[error("Internal EIP-1271 resolution error: {0}")] - Eip1271Internal(alloy_json_rpc::RpcError>), - - #[error("Internal EIP-6492 resolution error: {0}")] - Eip6492Internal(alloy_json_rpc::RpcError>), + #[error("Internal verification error: {0}")] + Rpc(RpcError), } impl From for CacaoError { diff --git a/relay_rpc/src/auth/cacao/signature/eip1271.rs b/relay_rpc/src/auth/cacao/signature/eip1271.rs deleted file mode 100644 index 6deddf7..0000000 --- a/relay_rpc/src/auth/cacao/signature/eip1271.rs +++ /dev/null @@ -1,224 +0,0 @@ -use { - super::CacaoError, - alloy_primitives::Address, - alloy_provider::{network::Ethereum, Provider, ReqwestProvider}, - alloy_rpc_types::{TransactionInput, TransactionRequest}, - alloy_sol_types::{sol, SolCall}, - url::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<(), CacaoError> { - let provider = ReqwestProvider::::new_http(provider); - - let call_request = TransactionRequest::default() - .to(address) - .input(TransactionInput::new( - isValidSignatureCall { - _hash: hash.into(), - _signature: signature.into(), - } - .abi_encode() - .into(), - )); - - let result = provider - .call(&call_request, Default::default()) - .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) - } - })?; - - let magic = result.get(..4); - if let Some(magic) = magic { - if magic == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(()) - } else { - Err(CacaoError::Verification) - } - } else { - Err(CacaoError::Verification) - } -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::auth::cacao::signature::{ - eip191::eip191_bytes, - strip_hex_prefix, - test_helpers::{ - deploy_contract, - message_hash, - sign_message, - spawn_anvil, - EIP1271_MOCK_CONTRACT, - }, - }, - alloy_primitives::address, - k256::ecdsa::SigningKey, - sha3::{Digest, Keccak256}, - }; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip1271_manual() { - 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(); - verify_eip1271(signature, address, hash, provider) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let mut contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - *contract_address.0.first_mut().unwrap() = - contract_address.0.first().unwrap().wrapping_add(1); - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - assert!(matches!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - let message2 = "yyy"; - assert!(matches!( - verify_eip1271( - signature, - contract_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs index 2287ef5..33ad441 100644 --- a/relay_rpc/src/auth/cacao/signature/eip191.rs +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -1,8 +1,6 @@ use { super::CacaoError, - crate::auth::cacao::signature::strip_hex_prefix, - alloy_primitives::Address, - sha3::{Digest, Keccak256}, + alloy_primitives::{Address, FixedBytes}, }; pub const EIP191: &str = "eip191"; @@ -19,7 +17,7 @@ pub fn eip191_bytes(message: &str) -> Vec { pub fn verify_eip191( signature: &[u8], address: &Address, - hash: Keccak256, + hash: FixedBytes<32>, ) -> Result<(), CacaoError> { use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; @@ -28,47 +26,42 @@ pub fn verify_eip191( let recovery_id = RecoveryId::try_from(signature.get(64).ok_or(CacaoError::Verification)? % 27) .map_err(|_| CacaoError::Verification)?; - let recovered_key = VerifyingKey::recover_from_digest(hash, &sig, recovery_id) + let recovered_key = VerifyingKey::recover_from_prehash(hash.as_slice(), &sig, recovery_id) .map_err(|_| CacaoError::Verification)?; - let hash = Keccak256::default() - .chain_update( - recovered_key - .to_encoded_point(false) - .as_bytes() - .get(1..) - .ok_or(CacaoError::Verification)?, - ) - .finalize(); - let add = hash.get(12..).ok_or(CacaoError::Verification)?; + let add = Address::from_public_key(&recovered_key); - let address_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(add); - - if address_encoded.to_lowercase() != strip_hex_prefix(&address.to_string()).to_lowercase() { - Err(CacaoError::Verification) - } else { + if &add == address { Ok(()) + } else { + Err(CacaoError::Verification) } } #[cfg(test)] mod tests { use { - crate::auth::cacao::signature::{ - eip191::verify_eip191, - test_helpers::{message_hash_internal, sign_message}, - }, - alloy_primitives::Address, + crate::auth::cacao::signature::eip191::verify_eip191, + alloy_primitives::{eip191_hash_message, Address}, k256::ecdsa::SigningKey, }; + pub fn sign_message(message: &str, private_key: &SigningKey) -> Vec { + let (signature, recovery): (k256::ecdsa::Signature, _) = private_key + .sign_prehash_recoverable(eip191_hash_message(message).as_slice()) + .unwrap(); + let signature = signature.to_bytes(); + // need for +27 is mentioned in EIP-1271 reference implementation + [&signature[..], &[recovery.to_byte() + 27]].concat() + } + #[test] fn test_eip191() { let private_key = SigningKey::random(&mut rand::thread_rng()); let message = "xxx"; let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); - verify_eip191(&signature, &address, message_hash_internal(message)).unwrap(); + verify_eip191(&signature, &address, eip191_hash_message(message)).unwrap(); } #[test] @@ -78,7 +71,7 @@ mod tests { let mut signature = sign_message(message, &private_key); *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); let address = Address::from_private_key(&private_key); - assert!(verify_eip191(&signature, &address, message_hash_internal(message)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); } #[test] @@ -88,7 +81,7 @@ mod tests { let signature = sign_message(message, &private_key); let mut address = Address::from_private_key(&private_key); *address.0.first_mut().unwrap() = address.0.first().unwrap().wrapping_add(1); - assert!(verify_eip191(&signature, &address, message_hash_internal(message)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); } #[test] @@ -98,6 +91,6 @@ mod tests { let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); let message2 = "yyy"; - assert!(verify_eip191(&signature, &address, message_hash_internal(message2)).is_err()); + assert!(verify_eip191(&signature, &address, eip191_hash_message(message2)).is_err()); } } diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs deleted file mode 100644 index 0fc5117..0000000 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ /dev/null @@ -1,482 +0,0 @@ -use { - crate::auth::cacao::CacaoError, - alloy_primitives::Address, - alloy_provider::{network::Ethereum, Provider, ReqwestProvider}, - alloy_rpc_types::{TransactionInput, TransactionRequest}, - alloy_sol_types::{sol, SolConstructor}, - url::Url, -}; - -pub const EIP6492: &str = "eip6492"; - -// https://eips.ethereum.org/EIPS/eip-6492 -const MAGIC_VALUE: u8 = 0x01; -sol! { - contract ValidateSigOffchain { - constructor (address _signer, bytes32 _hash, bytes memory _signature); - } -} -const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), - "/../../../../.foundry/forge/out/Eip6492.sol/ValidateSigOffchain.bytecode" -)); - -pub async fn verify_eip6492( - signature: Vec, - address: Address, - hash: &[u8; 32], - provider: Url, -) -> Result<(), CacaoError> { - let provider = ReqwestProvider::::new_http(provider); - - let call = ValidateSigOffchain::constructorCall { - _signer: address, - _hash: hash.into(), - _signature: signature.into(), - }; - let bytes = VALIDATE_SIG_OFFCHAIN_BYTECODE - .iter() - .cloned() - .chain(call.abi_encode()) - .collect::>(); - let transaction_request = - TransactionRequest::default().input(TransactionInput::new(bytes.into())); - - let result = provider - .call(&transaction_request, Default::default()) - .await - .map_err(|e| { - if let Some(error_response) = e.as_error_resp() { - if error_response.message == "execution reverted" { - CacaoError::Verification - } else { - CacaoError::Eip6492Internal(e) - } - } else { - CacaoError::Eip6492Internal(e) - } - })?; - - let magic = result.first(); - if let Some(magic) = magic { - if magic == &MAGIC_VALUE { - Ok(()) - } else { - Err(CacaoError::Verification) - } - } else { - Err(CacaoError::Verification) - } -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::auth::cacao::signature::{ - strip_hex_prefix, - test_helpers::{ - deploy_contract, - message_hash, - sign_message, - spawn_anvil, - CREATE2_CONTRACT, - EIP1271_MOCK_CONTRACT, - }, - }, - alloy_primitives::{address, b256, Uint}, - alloy_sol_types::{SolCall, SolValue}, - k256::ecdsa::SigningKey, - }; - - // Manual test. Paste address, signature, message, and project ID to verify - // function - #[tokio::test] - #[ignore] - async fn test_eip6492_manual() { - let address = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - let message = "xxx"; - let signature = "xxx"; - - let signature = data_encoding::HEXLOWER_PERMISSIVE - .decode(strip_hex_prefix(signature).as_bytes()) - .map_err(|_| CacaoError::Verification) - .unwrap(); - let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" - .parse() - .unwrap(); - verify_eip6492(signature, address, &message_hash(message), provider) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip191_pass() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let address = Address::from_private_key(&private_key); - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip191_wrong_signature() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - let address = Address::from_private_key(&private_key); - assert!( - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip191_wrong_address() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let mut address = Address::from_private_key(&private_key); - *address.0.first_mut().unwrap() = address.0.first().unwrap().wrapping_add(1); - assert!( - verify_eip6492(signature, address, &message_hash(message), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip191_wrong_message() { - let (_anvil, rpc_url, _private_key) = spawn_anvil().await; - - let private_key = SigningKey::random(&mut rand::thread_rng()); - let message = "xxx"; - let signature = sign_message(message, &private_key); - let address = Address::from_private_key(&private_key); - let message2 = "yyy"; - assert!( - verify_eip6492(signature, address, &message_hash(message2), rpc_url) - .await - .is_err() - ); - } - - #[tokio::test] - async fn test_eip1271_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip1271_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let mut contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - *contract_address.0.first_mut().unwrap() = - contract_address.0.first().unwrap().wrapping_add(1); - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - assert!(matches!( - verify_eip6492(signature, contract_address, &message_hash(message), rpc_url).await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip1271_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let contract_address = deploy_contract( - &rpc_url, - &private_key, - EIP1271_MOCK_CONTRACT, - Some(&Address::from_private_key(&private_key).to_string()), - ) - .await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - - let message2 = "yyy"; - assert!(matches!( - verify_eip6492( - signature, - contract_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - const EIP1271_MOCK_BYTECODE: &[u8] = include_bytes!(concat!( - env!("OUT_DIR"), - "/../../../../.foundry/forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode" - )); - const EIP6492_MAGIC_BYTES: [u16; 16] = [ - 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, - 0x6492, 0x6492, 0x6492, 0x6492, 0x6492, - ]; - sol! { - contract Eip1271Mock { - address owner_eoa; - - constructor(address owner_eoa) { - owner_eoa = owner_eoa; - } - } - } - - sol! { - contract Create2 { - function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) external payable returns (address addr); - } - } - - fn predeploy_signature( - owner_eoa: Address, - create2_factory_address: Address, - signature: Vec, - ) -> (Address, Vec) { - let salt = b256!("7c5ea36004851c764c44143b1dcb59679b11c9a68e5f41497f6cf3d480715331"); - let contract_bytecode = EIP1271_MOCK_BYTECODE; - let contract_constructor = Eip1271Mock::constructorCall { owner_eoa }; - - let bytecode = contract_bytecode - .iter() - .cloned() - .chain(contract_constructor.abi_encode()) - .collect::>(); - let predeploy_address = create2_factory_address.create2_from_code(salt, bytecode.clone()); - let signature = ( - create2_factory_address, - Create2::deployCall { - amount: Uint::ZERO, - salt, - bytecode: bytecode.into(), - } - .abi_encode(), - signature, - ) - .abi_encode_sequence() - .into_iter() - .chain( - EIP6492_MAGIC_BYTES - .iter() - .flat_map(|&x| x.to_be_bytes().into_iter()), - ) - .collect::>(); - (predeploy_address, signature) - } - - #[tokio::test] - async fn test_eip6492_pass() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url, - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_eip6492_wrong_signature() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let mut signature = sign_message(message, &private_key); - *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_fail_wrong_signer() { - let (anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message( - message, - &SigningKey::from_bytes(&anvil.keys().get(1).unwrap().to_bytes()).unwrap(), - ); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_fail_wrong_contract_address() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (mut predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - *predeploy_address.0.first_mut().unwrap() = - predeploy_address.0.first().unwrap().wrapping_add(1); - - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message), - rpc_url, - ) - .await, - Err(CacaoError::Verification) - )); - } - - #[tokio::test] - async fn test_eip6492_wrong_message() { - let (_anvil, rpc_url, private_key) = spawn_anvil().await; - let create2_factory_address = - deploy_contract(&rpc_url, &private_key, CREATE2_CONTRACT, None).await; - - let message = "xxx"; - let signature = sign_message(message, &private_key); - let (predeploy_address, signature) = predeploy_signature( - Address::from_private_key(&private_key), - create2_factory_address, - signature, - ); - - let message2 = "yyy"; - assert!(matches!( - verify_eip6492( - signature, - predeploy_address, - &message_hash(message2), - rpc_url - ) - .await, - Err(CacaoError::Verification) - )); - } -} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index 4eb0ae1..9b133b6 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -1,99 +1,67 @@ use { self::{ - eip1271::{verify_eip1271, EIP1271}, - eip191::{eip191_bytes, verify_eip191, EIP191}, - eip6492::{verify_eip6492, EIP6492}, + eip191::{verify_eip191, EIP191}, get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, - alloy_primitives::Address, + alloy_primitives::{eip191_hash_message, hex::FromHex, Address, Bytes}, + alloy_provider::{network::Ethereum, ReqwestProvider}, + erc6492::verify_signature, serde::{Deserialize, Serialize}, - sha3::{Digest, Keccak256}, - std::str::FromStr, }; -pub mod eip1271; pub mod eip191; -pub mod eip6492; pub mod get_rpc_url; -#[cfg(test)] -mod test_helpers; - #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct Signature { pub t: String, pub s: String, } +pub const EIP1271: &str = "eip1271"; +pub const EIP6492: &str = "eip6492"; + impl Signature { pub async fn verify( &self, cacao: &Cacao, provider: Option<&impl GetRpcUrl>, ) -> Result<(), CacaoError> { + let chain_id = cacao.p.chain_id_reference()?; 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()?)); + let address = + Address::parse_checksummed(address, None).map_err(CacaoError::AddressInvalid)?; + let signature = Bytes::from_hex(&cacao.s.s).map_err(|_| CacaoError::Verification)?; + let message = cacao.siwe_message()?; match self.t.as_str() { - EIP191 => verify_eip191( - &signature, - &address.parse().map_err(CacaoError::AddressNotEip191)?, - hash, - ), - EIP1271 => { - if let Some(provider) = provider { - let chain_id = cacao.p.chain_id_reference()?; - let provider = provider.get_rpc_url(chain_id).await; - 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) - } - } else { - Err(CacaoError::Eip1271NotSupported) - } + EIP191 => { + // Technically we can use EIP-6492 to verify EIP-191 signatures as well, + // but since we know the signature type we can avoid an RPC request. + verify_eip191(&signature, &address, eip191_hash_message(message)) } - EIP6492 => { + EIP1271 | EIP6492 => { if let Some(provider) = provider { - let chain_id = cacao.p.chain_id_reference()?; - let provider = provider.get_rpc_url(chain_id).await; - if let Some(provider) = provider { - verify_eip6492( - signature, - Address::from_str(&address).map_err(|_| CacaoError::AddressInvalid)?, - &hash.finalize()[..] - .try_into() - .expect("hash length is 32 bytes"), - provider, - ) + let provider = ReqwestProvider::::new_http( + provider + .get_rpc_url(chain_id) + .await + .ok_or(CacaoError::ProviderNotAvailable)?, + ); + let result = verify_signature(signature, address, message, provider) .await + .map_err(CacaoError::Rpc)?; + if result.is_valid() { + Ok(()) } else { - Err(CacaoError::ProviderNotAvailable) + Err(CacaoError::Verification) } } else { - Err(CacaoError::Eip6492NotSupported) + 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/signature/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs deleted file mode 100644 index f7f305c..0000000 --- a/relay_rpc/src/auth/cacao/signature/test_helpers.rs +++ /dev/null @@ -1,102 +0,0 @@ -use { - super::eip191::eip191_bytes, - alloy_node_bindings::{Anvil, AnvilInstance}, - alloy_primitives::Address, - k256::ecdsa::SigningKey, - regex::Regex, - sha2::Digest, - sha3::Keccak256, - std::process::Stdio, - tokio::process::Command, - url::Url, -}; - -fn format_foundry_dir(path: &str) -> String { - format!( - "{}/../../../../.foundry/{}", - std::env::var("OUT_DIR").unwrap(), - path - ) -} - -pub async fn spawn_anvil() -> (AnvilInstance, Url, SigningKey) { - let anvil = Anvil::at(format_foundry_dir("bin/anvil")).spawn(); - let provider = anvil.endpoint().parse().unwrap(); - let private_key = anvil.keys().first().unwrap().clone(); - ( - anvil, - provider, - SigningKey::from_bytes(&private_key.to_bytes()).unwrap(), - ) -} - -pub const EIP1271_MOCK_CONTRACT: &str = "Eip1271Mock"; -pub const CREATE2_CONTRACT: &str = "Create2"; - -pub async fn deploy_contract( - rpc_url: &Url, - private_key: &SigningKey, - contract_name: &str, - constructor_arg: Option<&str>, -) -> Address { - let key_encoded = data_encoding::HEXLOWER_PERMISSIVE.encode(&private_key.to_bytes()); - let cache_folder = format_foundry_dir("forge/cache"); - let out_folder = format_foundry_dir("forge/out"); - let mut args = vec![ - "create", - "--contracts=relay_rpc/contracts", - contract_name, - "--rpc-url", - rpc_url.as_str(), - "--private-key", - &key_encoded, - "--cache-path", - &cache_folder, - "--out", - &out_folder, - ]; - if let Some(arg) = constructor_arg { - args.push("--constructor-args"); - args.push(arg); - } - let output = Command::new(format_foundry_dir("bin/forge")) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap() - .wait_with_output() - .await - .unwrap(); - println!("forge status: {:?}", output.status); - let stdout = String::from_utf8(output.stdout).unwrap(); - println!("forge stdout: {stdout:?}"); - let stderr = String::from_utf8(output.stderr).unwrap(); - println!("forge stderr: {stderr:?}"); - assert!(output.status.success()); - let (_, [contract_address]) = Regex::new("Deployed to: (0x[0-9a-fA-F]+)") - .unwrap() - .captures(&stdout) - .unwrap() - .extract(); - contract_address.parse().unwrap() -} - -pub fn sign_message(message: &str, private_key: &SigningKey) -> Vec { - let (signature, recovery): (k256::ecdsa::Signature, _) = private_key - .sign_digest_recoverable(message_hash_internal(message)) - .unwrap(); - let signature = signature.to_bytes(); - // need for +27 is mentioned in EIP-1271 reference implementation - [&signature[..], &[recovery.to_byte() + 27]].concat() -} - -pub fn message_hash_internal(message: &str) -> Keccak256 { - Keccak256::new_with_prefix(eip191_bytes(message)) -} - -pub fn message_hash(message: &str) -> [u8; 32] { - message_hash_internal(message).finalize()[..] - .try_into() - .unwrap() -} From 989f68bd38052c15d8cb1485d80eb1b764cbecae Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 27 Apr 2024 20:26:07 -0400 Subject: [PATCH 2/8] fix: use Alloy address recovery --- relay_rpc/Cargo.toml | 3 +- relay_rpc/src/auth/cacao/signature/eip191.rs | 34 ++++++-------------- relay_rpc/src/auth/cacao/signature/mod.rs | 4 +-- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 677a92a..0473b69 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -7,7 +7,6 @@ license = "Apache-2.0" [features] default = ["cacao"] cacao = [ - "dep:k256", "dep:alloy-provider", "dep:alloy-primitives", "dep:erc6492", @@ -35,7 +34,6 @@ chrono = { version = "0.4", default-features = false, features = [ regex = "1.7" once_cell = "1.16" jsonwebtoken = "8.1" -k256 = { version = "0.13", optional = true } sha2 = { version = "0.10.6" } url = "2" alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } @@ -44,6 +42,7 @@ erc6492 = { git = "https://github.com/WalletConnect/erc6492.git", optional = tru strum = { version = "0.26", features = ["strum_macros", "derive"] } [dev-dependencies] +k256 = "0.13" tokio = { version = "1.35.1", features = ["test-util", "macros"] } [lints.clippy] diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs index 33ad441..7fec4fa 100644 --- a/relay_rpc/src/auth/cacao/signature/eip191.rs +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -1,36 +1,20 @@ use { super::CacaoError, - alloy_primitives::{Address, FixedBytes}, + alloy_primitives::{Address, Signature}, }; 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: &Address, - hash: FixedBytes<32>, + message: &[u8], ) -> Result<(), CacaoError> { - use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; - - let sig = Sig::try_from(signature.get(..64).ok_or(CacaoError::Verification)?) - .map_err(|_| CacaoError::Verification)?; - let recovery_id = RecoveryId::try_from(signature.get(64).ok_or(CacaoError::Verification)? % 27) + let signature = Signature::try_from(signature).map_err(|_| CacaoError::Verification)?; + let add = signature + .recover_address_from_msg(message) .map_err(|_| CacaoError::Verification)?; - let recovered_key = VerifyingKey::recover_from_prehash(hash.as_slice(), &sig, recovery_id) - .map_err(|_| CacaoError::Verification)?; - - let add = Address::from_public_key(&recovered_key); - if &add == address { Ok(()) } else { @@ -61,7 +45,7 @@ mod tests { let message = "xxx"; let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); - verify_eip191(&signature, &address, eip191_hash_message(message)).unwrap(); + verify_eip191(&signature, &address, message.as_bytes()).unwrap(); } #[test] @@ -71,7 +55,7 @@ mod tests { let mut signature = sign_message(message, &private_key); *signature.first_mut().unwrap() = signature.first().unwrap().wrapping_add(1); let address = Address::from_private_key(&private_key); - assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); + assert!(verify_eip191(&signature, &address, message.as_bytes()).is_err()); } #[test] @@ -81,7 +65,7 @@ mod tests { let signature = sign_message(message, &private_key); let mut address = Address::from_private_key(&private_key); *address.0.first_mut().unwrap() = address.0.first().unwrap().wrapping_add(1); - assert!(verify_eip191(&signature, &address, eip191_hash_message(message)).is_err()); + assert!(verify_eip191(&signature, &address, message.as_bytes()).is_err()); } #[test] @@ -91,6 +75,6 @@ mod tests { let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); let message2 = "yyy"; - assert!(verify_eip191(&signature, &address, eip191_hash_message(message2)).is_err()); + assert!(verify_eip191(&signature, &address, message2.as_bytes()).is_err()); } } diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index 9b133b6..c04b077 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -4,7 +4,7 @@ use { get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, - alloy_primitives::{eip191_hash_message, hex::FromHex, Address, Bytes}, + alloy_primitives::{hex::FromHex, Address, Bytes}, alloy_provider::{network::Ethereum, ReqwestProvider}, erc6492::verify_signature, serde::{Deserialize, Serialize}, @@ -39,7 +39,7 @@ impl Signature { EIP191 => { // Technically we can use EIP-6492 to verify EIP-191 signatures as well, // but since we know the signature type we can avoid an RPC request. - verify_eip191(&signature, &address, eip191_hash_message(message)) + verify_eip191(&signature, &address, message.as_bytes()) } EIP1271 | EIP6492 => { if let Some(provider) = provider { From 1ff884f84c7ec7bc2e22968085e512334c77ea48 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sat, 27 Apr 2024 20:37:28 -0400 Subject: [PATCH 3/8] chore: remove Foundry --- .github/workflows/ci.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0288989..6440a9b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,9 +54,6 @@ jobs: profile: default override: true - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v1 From 9ed235e2dc388d729bf0bffa28301234e16a5bb5 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 5 May 2024 14:23:36 +0100 Subject: [PATCH 4/8] fix: use Provider instead of URL --- blockchain_api/Cargo.toml | 2 ++ blockchain_api/src/lib.rs | 15 ++++++++++----- relay_client/src/websocket/stream.rs | 4 ++-- relay_rpc/Cargo.toml | 6 +++++- relay_rpc/src/auth/cacao.rs | 4 ++-- .../src/auth/cacao/signature/get_provider.rs | 9 +++++++++ .../src/auth/cacao/signature/get_rpc_url.rs | 6 ------ relay_rpc/src/auth/cacao/signature/mod.rs | 17 +++++++---------- relay_rpc/src/auth/cacao/tests.rs | 14 +++++++++++--- 9 files changed, 48 insertions(+), 29 deletions(-) create mode 100644 relay_rpc/src/auth/cacao/signature/get_provider.rs delete mode 100644 relay_rpc/src/auth/cacao/signature/get_rpc_url.rs diff --git a/blockchain_api/Cargo.toml b/blockchain_api/Cargo.toml index 13eedc1..dfc7879 100644 --- a/blockchain_api/Cargo.toml +++ b/blockchain_api/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" license = "Apache-2.0" [dependencies] +alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } relay_rpc = { path = "../relay_rpc", features = ["cacao"] } reqwest = { version = "0.12.2", features = ["json"] } serde = "1.0" diff --git a/blockchain_api/src/lib.rs b/blockchain_api/src/lib.rs index 4c70fb0..feaaddf 100644 --- a/blockchain_api/src/lib.rs +++ b/blockchain_api/src/lib.rs @@ -1,6 +1,7 @@ pub use reqwest::Error; use { - relay_rpc::{auth::cacao::signature::get_rpc_url::GetRpcUrl, domain::ProjectId}, + alloy_provider::ReqwestProvider, + relay_rpc::{auth::cacao::signature::get_provider::GetProvider, domain::ProjectId}, serde::Deserialize, std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration}, tokio::{sync::RwLock, task::JoinHandle}, @@ -98,18 +99,22 @@ fn build_rpc_url(blockchain_api_rpc_endpoint: Url, chain_id: &str, project_id: & url } -impl GetRpcUrl for BlockchainApiProvider { - async fn get_rpc_url(&self, chain_id: String) -> Option { +impl GetProvider for BlockchainApiProvider { + type Provider = ReqwestProvider; + type Transport = alloy_transport_http::Http; + + async fn get_provider(&self, chain_id: String) -> Option { self.supported_chains .read() .await .contains(&chain_id) .then(|| { - build_rpc_url( + let url = build_rpc_url( self.blockchain_api_rpc_endpoint.clone(), &chain_id, self.project_id.as_ref(), - ) + ); + ReqwestProvider::new_http(url) }) } } diff --git a/relay_client/src/websocket/stream.rs b/relay_client/src/websocket/stream.rs index 28a5e1e..d2b8cab 100644 --- a/relay_client/src/websocket/stream.rs +++ b/relay_client/src/websocket/stream.rs @@ -141,7 +141,7 @@ impl ClientStream { /// Closes the connection. pub async fn close(&mut self, frame: Option>) -> Result<(), ClientError> { - self.close_frame = frame.clone(); + self.close_frame.clone_from(&frame); self.socket .close(frame) .await @@ -220,7 +220,7 @@ impl ClientStream { } Message::Close(frame) => { - self.close_frame = frame.clone(); + self.close_frame.clone_from(frame); Some(StreamEvent::ConnectionClosed(frame.clone())) } diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 0473b69..96e1256 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -7,9 +7,10 @@ license = "Apache-2.0" [features] default = ["cacao"] cacao = [ + "dep:erc6492", + "dep:alloy-transport", "dep:alloy-provider", "dep:alloy-primitives", - "dep:erc6492", ] [dependencies] @@ -36,6 +37,7 @@ once_cell = "1.16" jsonwebtoken = "8.1" sha2 = { version = "0.10.6" } url = "2" +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } alloy-primitives = { version = "0.7.0", optional = true } erc6492 = { git = "https://github.com/WalletConnect/erc6492.git", optional = true } @@ -44,6 +46,8 @@ strum = { version = "0.26", features = ["strum_macros", "derive"] } [dev-dependencies] k256 = "0.13" tokio = { version = "1.35.1", features = ["test-util", "macros"] } +alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } +reqwest = "0.12.4" [lints.clippy] indexing_slicing = "deny" diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 9f888bc..ddbf461 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::{get_rpc_url::GetRpcUrl, Signature}, + signature::{get_provider::GetProvider, Signature}, }, alloy_primitives::AddressError, core::fmt::Debug, @@ -98,7 +98,7 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result<(), CacaoError> { + pub async fn verify(&self, provider: Option<&impl GetProvider>) -> Result<(), CacaoError> { self.p.validate()?; self.h.validate()?; self.s.verify(self, provider).await diff --git a/relay_rpc/src/auth/cacao/signature/get_provider.rs b/relay_rpc/src/auth/cacao/signature/get_provider.rs new file mode 100644 index 0000000..903f950 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/get_provider.rs @@ -0,0 +1,9 @@ +use {alloy_provider::Provider, alloy_transport::Transport}; + +pub trait GetProvider { + type Transport: Transport + Clone; + type Provider: Provider; + + #[allow(async_fn_in_trait)] + async fn get_provider(&self, chain_id: String) -> Option; +} diff --git a/relay_rpc/src/auth/cacao/signature/get_rpc_url.rs b/relay_rpc/src/auth/cacao/signature/get_rpc_url.rs deleted file mode 100644 index fb712b9..0000000 --- a/relay_rpc/src/auth/cacao/signature/get_rpc_url.rs +++ /dev/null @@ -1,6 +0,0 @@ -use url::Url; - -pub trait GetRpcUrl { - #[allow(async_fn_in_trait)] - async fn get_rpc_url(&self, chain_id: String) -> Option; -} diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index c04b077..c5bfd21 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -1,17 +1,16 @@ use { self::{ eip191::{verify_eip191, EIP191}, - get_rpc_url::GetRpcUrl, + get_provider::GetProvider, }, super::{Cacao, CacaoError}, alloy_primitives::{hex::FromHex, Address, Bytes}, - alloy_provider::{network::Ethereum, ReqwestProvider}, erc6492::verify_signature, serde::{Deserialize, Serialize}, }; pub mod eip191; -pub mod get_rpc_url; +pub mod get_provider; #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct Signature { @@ -26,7 +25,7 @@ impl Signature { pub async fn verify( &self, cacao: &Cacao, - provider: Option<&impl GetRpcUrl>, + provider: Option<&impl GetProvider>, ) -> Result<(), CacaoError> { let chain_id = cacao.p.chain_id_reference()?; let address = cacao.p.address()?; @@ -43,12 +42,10 @@ impl Signature { } EIP1271 | EIP6492 => { if let Some(provider) = provider { - let provider = ReqwestProvider::::new_http( - provider - .get_rpc_url(chain_id) - .await - .ok_or(CacaoError::ProviderNotAvailable)?, - ); + let provider = provider + .get_provider(chain_id) + .await + .ok_or(CacaoError::ProviderNotAvailable)?; let result = verify_signature(signature, address, message, provider) .await .map_err(CacaoError::Rpc)?; diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 0b5dcea..0ccdd00 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,9 +1,17 @@ -use {super::signature::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; +use { + super::signature::get_provider::GetProvider, + crate::auth::cacao::Cacao, + alloy_provider::ReqwestProvider, +}; struct MockGetRpcUrl; -impl GetRpcUrl for MockGetRpcUrl { - async fn get_rpc_url(&self, _: String) -> Option { +// https://github.com/alloy-rs/alloy/issues/568 +impl GetProvider for MockGetRpcUrl { + type Provider = ReqwestProvider; + type Transport = alloy_transport_http::Http; + + async fn get_provider(&self, _: String) -> Option { None } } From c4bb4b263d57e465412297ddc6a41e8cdf99b301 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 5 May 2024 14:24:53 +0100 Subject: [PATCH 5/8] chore: revert enabling cacao feature by default --- relay_rpc/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 96e1256..bc5f4c4 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" license = "Apache-2.0" [features] -default = ["cacao"] cacao = [ "dep:erc6492", "dep:alloy-transport", From eb8fbc3b6145a298a0ad4ccf881ca7d5b02d91bf Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 5 May 2024 14:26:17 +0100 Subject: [PATCH 6/8] chore: fix order --- relay_rpc/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index bc5f4c4..993dcb4 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -7,8 +7,8 @@ license = "Apache-2.0" [features] cacao = [ "dep:erc6492", - "dep:alloy-transport", "dep:alloy-provider", + "dep:alloy-transport", "dep:alloy-primitives", ] @@ -36,8 +36,8 @@ once_cell = "1.16" jsonwebtoken = "8.1" sha2 = { version = "0.10.6" } url = "2" -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } +alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7", optional = true } alloy-primitives = { version = "0.7.0", optional = true } erc6492 = { git = "https://github.com/WalletConnect/erc6492.git", optional = true } strum = { version = "0.26", features = ["strum_macros", "derive"] } From b0dc76b73c0184903e44cecdb4ad60fe8d000842 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 5 May 2024 14:51:13 +0100 Subject: [PATCH 7/8] fix: reuse reqwest client --- blockchain_api/Cargo.toml | 1 + blockchain_api/src/lib.rs | 50 +++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/blockchain_api/Cargo.toml b/blockchain_api/Cargo.toml index dfc7879..bb32462 100644 --- a/blockchain_api/Cargo.toml +++ b/blockchain_api/Cargo.toml @@ -7,6 +7,7 @@ license = "Apache-2.0" [dependencies] alloy-provider = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } +alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy.git", rev = "d68a6b7" } relay_rpc = { path = "../relay_rpc", features = ["cacao"] } reqwest = { version = "0.12.2", features = ["json"] } serde = "1.0" diff --git a/blockchain_api/src/lib.rs b/blockchain_api/src/lib.rs index feaaddf..a3898a1 100644 --- a/blockchain_api/src/lib.rs +++ b/blockchain_api/src/lib.rs @@ -1,9 +1,16 @@ pub use reqwest::Error; use { alloy_provider::ReqwestProvider, + alloy_rpc_client::RpcClient, + alloy_transport_http::Http, relay_rpc::{auth::cacao::signature::get_provider::GetProvider, domain::ProjectId}, serde::Deserialize, - std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration}, + std::{ + collections::{HashMap, HashSet}, + convert::Infallible, + sync::Arc, + time::Duration, + }, tokio::{sync::RwLock, task::JoinHandle}, tracing::error, url::Url, @@ -27,6 +34,8 @@ pub struct BlockchainApiProvider { blockchain_api_rpc_endpoint: Url, supported_chains: Arc>>, refresh_job: Arc>, + http_client: reqwest::Client, + provider_cache: Arc>>, } impl Drop for BlockchainApiProvider { @@ -87,6 +96,8 @@ impl BlockchainApiProvider { blockchain_api_rpc_endpoint, supported_chains, refresh_job: Arc::new(refresh_job), + http_client: reqwest::Client::new(), + provider_cache: Arc::new(RwLock::new(HashMap::new())), }) } } @@ -104,18 +115,31 @@ impl GetProvider for BlockchainApiProvider { type Transport = alloy_transport_http::Http; async fn get_provider(&self, chain_id: String) -> Option { - self.supported_chains - .read() - .await - .contains(&chain_id) - .then(|| { - let url = build_rpc_url( - self.blockchain_api_rpc_endpoint.clone(), - &chain_id, - self.project_id.as_ref(), - ); - ReqwestProvider::new_http(url) - }) + if self.supported_chains.read().await.contains(&chain_id) { + Some( + if let Some(provider) = self.provider_cache.read().await.get(&chain_id) { + provider.clone() + } else { + let url = build_rpc_url( + self.blockchain_api_rpc_endpoint.clone(), + &chain_id, + self.project_id.as_ref(), + ); + let provider = ReqwestProvider::new({ + let http = Http::with_client(self.http_client.clone(), url); + let is_local = http.guess_local(); + RpcClient::new(http, is_local) + }); + self.provider_cache + .write() + .await + .insert(chain_id.clone(), provider.clone()); + provider + }, + ) + } else { + None + } } } From e079ec659bfcdf2769e5f90d854a4220f3616f6f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 5 May 2024 15:00:38 +0100 Subject: [PATCH 8/8] chore: disable cache --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6440a9b..ecd1f20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,7 +54,7 @@ jobs: profile: default override: true - - uses: Swatinem/rust-cache@v2 + # - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v1 with: