From edd6b6bcaa360e0887b4930ba78d56c958700612 Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:58:04 -0400 Subject: [PATCH] feat: EIP-6492 support (#78) --- .github/workflows/ci.yaml | 4 - README.md | 9 +- blockchain_api/src/lib.rs | 2 +- relay_rpc/Cargo.toml | 28 +- relay_rpc/build.rs | 98 ++++ relay_rpc/contracts/Create2.sol | 49 ++ .../contracts}/Eip1271Mock.sol | 8 +- relay_rpc/contracts/Eip6492.sol | 111 ++++ relay_rpc/src/auth/cacao.rs | 10 +- .../signature/{eip1271/mod.rs => eip1271.rs} | 108 ++-- relay_rpc/src/auth/cacao/signature/eip191.rs | 6 +- relay_rpc/src/auth/cacao/signature/eip6492.rs | 482 ++++++++++++++++++ .../signature/{eip1271 => }/get_rpc_url.rs | 0 relay_rpc/src/auth/cacao/signature/mod.rs | 29 +- .../src/auth/cacao/signature/test_helpers.rs | 55 +- relay_rpc/src/auth/cacao/tests.rs | 4 +- 16 files changed, 917 insertions(+), 86 deletions(-) create mode 100644 relay_rpc/build.rs create mode 100644 relay_rpc/contracts/Create2.sol rename {contracts => relay_rpc/contracts}/Eip1271Mock.sol (94%) create mode 100644 relay_rpc/contracts/Eip6492.sol rename relay_rpc/src/auth/cacao/signature/{eip1271/mod.rs => eip1271.rs} (63%) create mode 100644 relay_rpc/src/auth/cacao/signature/eip6492.rs rename relay_rpc/src/auth/cacao/signature/{eip1271 => }/get_rpc_url.rs (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 312e617..0288989 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,10 +56,6 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - # pre-build contracts to avoid race condition installing solc during `forge create` in tests - - name: Build contracts - run: forge build -C contracts --cache-path=target/.forge/cache --out=target/.forge/out - uses: Swatinem/rust-cache@v2 diff --git a/README.md b/README.md index 973fd77..cdc7eda 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,14 @@ The core Relay client. Provides access to all available Relay RPC methods to bui Provides all of the Relay domain types (e.g. `ClientId`, `ProjectId` etc.) as well as auth token generation and validation functionality. -### Test dependencies +### `cacao` feature -Foundry is required to be installed to your system for testing: +To aid IDE integration you may want to add this to your local `relay_rpc/Cargo.toml` file: + +```toml +[features] +default = ["cacao"] +``` # License diff --git a/blockchain_api/src/lib.rs b/blockchain_api/src/lib.rs index 96a40ed..4c70fb0 100644 --- a/blockchain_api/src/lib.rs +++ b/blockchain_api/src/lib.rs @@ -1,6 +1,6 @@ pub use reqwest::Error; use { - relay_rpc::{auth::cacao::signature::eip1271::get_rpc_url::GetRpcUrl, domain::ProjectId}, + relay_rpc::{auth::cacao::signature::get_rpc_url::GetRpcUrl, domain::ProjectId}, serde::Deserialize, std::{collections::HashSet, convert::Infallible, sync::Arc, time::Duration}, tokio::{sync::RwLock, task::JoinHandle}, diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 3fd7ecf..c9ff0d7 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -8,7 +8,7 @@ license = "Apache-2.0" cacao = [ "dep:k256", "dep:sha3", - "dep:alloy-providers", + "dep:alloy-provider", "dep:alloy-transport", "dep:alloy-transport-http", "dep:alloy-rpc-types", @@ -16,7 +16,8 @@ cacao = [ "dep:alloy-json-abi", "dep:alloy-sol-types", "dep:alloy-primitives", - "dep:alloy-node-bindings" + "dep:alloy-node-bindings", + "dep:alloy-contract" ] [dependencies] @@ -45,19 +46,24 @@ k256 = { version = "0.13", optional = true } sha3 = { version = "0.10", optional = true } sha2 = { version = "0.10.6" } url = "2" -alloy-providers = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-transport = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-transport-http = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy.git", rev = "e6f98e1", optional = true } -alloy-json-abi = { version = "0.6.2", optional = true } -alloy-sol-types = { version = "0.6.2", optional = true } -alloy-primitives = { version = "0.6.2", 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-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 } 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 new file mode 100644 index 0000000..6d8b1a1 --- /dev/null +++ b/relay_rpc/build.rs @@ -0,0 +1,98 @@ +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 new file mode 100644 index 0000000..062722e --- /dev/null +++ b/relay_rpc/contracts/Create2.sol @@ -0,0 +1,49 @@ +// 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/contracts/Eip1271Mock.sol b/relay_rpc/contracts/Eip1271Mock.sol similarity index 94% rename from contracts/Eip1271Mock.sol rename to relay_rpc/contracts/Eip1271Mock.sol index 6a50659..35fa8b9 100644 --- a/contracts/Eip1271Mock.sol +++ b/relay_rpc/contracts/Eip1271Mock.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.25; // https://eips.ethereum.org/EIPS/eip-1271#reference-implementation contract Eip1271Mock { - address owner; + address owner_eoa; - constructor() { - owner = msg.sender; + constructor(address _owner_eoa) { + owner_eoa = _owner_eoa; } /** @@ -17,7 +17,7 @@ contract Eip1271Mock { bytes calldata _signature ) external view returns (bytes4) { // Validate signatures - if (recoverSigner(_hash, _signature) == owner) { + if (recoverSigner(_hash, _signature) == owner_eoa) { return 0x1626ba7e; } else { return 0xffffffff; diff --git a/relay_rpc/contracts/Eip6492.sol b/relay_rpc/contracts/Eip6492.sol new file mode 100644 index 0000000..41586fe --- /dev/null +++ b/relay_rpc/contracts/Eip6492.sol @@ -0,0 +1,111 @@ +// 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 e215716..179077f 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::{eip1271::get_rpc_url::GetRpcUrl, Signature}, + signature::{get_rpc_url::GetRpcUrl, Signature}, }, alloy_primitives::hex::FromHexError, core::fmt::Debug, @@ -39,6 +39,9 @@ pub enum CacaoError { #[error("EIP-1271 signatures not supported")] Eip1271NotSupported, + #[error("EIP-6492 signatures not supported")] + Eip6492NotSupported, + #[error("Unsupported signature type")] UnsupportedSignature, @@ -50,6 +53,9 @@ pub enum CacaoError { #[error("Internal EIP-1271 resolution error: {0}")] Eip1271Internal(alloy_json_rpc::RpcError>), + + #[error("Internal EIP-6492 resolution error: {0}")] + Eip6492Internal(alloy_json_rpc::RpcError>), } impl From for CacaoError { @@ -101,7 +107,7 @@ pub struct Cacao { impl Cacao { const ETHEREUM: &'static str = "Ethereum"; - pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result { + pub async fn verify(&self, provider: Option<&impl GetRpcUrl>) -> Result<(), CacaoError> { self.p.validate()?; self.h.validate()?; self.s.verify(self, provider).await diff --git a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs b/relay_rpc/src/auth/cacao/signature/eip1271.rs similarity index 63% rename from relay_rpc/src/auth/cacao/signature/eip1271/mod.rs rename to relay_rpc/src/auth/cacao/signature/eip1271.rs index 99eed65..6deddf7 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271.rs @@ -1,15 +1,12 @@ use { super::CacaoError, - alloy_primitives::{Address, FixedBytes}, - alloy_providers::provider::{Provider, TempProvider}, - alloy_rpc_types::{CallInput, CallRequest}, + alloy_primitives::Address, + alloy_provider::{network::Ethereum, Provider, ReqwestProvider}, + alloy_rpc_types::{TransactionInput, TransactionRequest}, alloy_sol_types::{sol, SolCall}, - alloy_transport_http::Http, url::Url, }; -pub mod get_rpc_url; - pub const EIP1271: &str = "eip1271"; // https://eips.ethereum.org/EIPS/eip-1271 @@ -28,38 +25,39 @@ pub async fn verify_eip1271( address: Address, hash: &[u8; 32], provider: Url, -) -> Result { - let provider = Provider::new(Http::new(provider)); +) -> Result<(), CacaoError> { + let provider = ReqwestProvider::::new_http(provider); - let call_request = CallRequest { - to: Some(address), - input: CallInput::new( + let call_request = TransactionRequest::default() + .to(address) + .input(TransactionInput::new( isValidSignatureCall { - _hash: FixedBytes::from(hash), - _signature: signature, + _hash: hash.into(), + _signature: signature.into(), } .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 + 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) } - } else { - CacaoError::Eip1271Internal(e) - } - })?; + })?; let magic = result.get(..4); if let Some(magic) = magic { if magic == MAGIC_VALUE.to_be_bytes().to_vec() { - Ok(true) + Ok(()) } else { Err(CacaoError::Verification) } @@ -75,7 +73,13 @@ mod test { crate::auth::cacao::signature::{ eip191::eip191_bytes, strip_hex_prefix, - test_helpers::{deploy_contract, message_hash, sign_message, spawn_anvil}, + test_helpers::{ + deploy_contract, + message_hash, + sign_message, + spawn_anvil, + EIP1271_MOCK_CONTRACT, + }, }, alloy_primitives::address, k256::ecdsa::SigningKey, @@ -100,30 +104,40 @@ mod test { let provider = "https://rpc.walletconnect.com/v1?chainId=eip155:1&projectId=xxx" .parse() .unwrap(); - assert!(verify_eip1271(signature, address, hash, provider) + verify_eip1271(signature, address, hash, provider) .await - .unwrap()); + .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).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); - assert!( - verify_eip1271(signature, contract_address, &message_hash(message), rpc_url) - .await - .unwrap() - ); + 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).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); @@ -138,7 +152,13 @@ mod test { #[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).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( @@ -155,7 +175,13 @@ mod test { #[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).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); @@ -172,7 +198,13 @@ mod test { #[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).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); diff --git a/relay_rpc/src/auth/cacao/signature/eip191.rs b/relay_rpc/src/auth/cacao/signature/eip191.rs index ba782df..2287ef5 100644 --- a/relay_rpc/src/auth/cacao/signature/eip191.rs +++ b/relay_rpc/src/auth/cacao/signature/eip191.rs @@ -20,7 +20,7 @@ pub fn verify_eip191( signature: &[u8], address: &Address, hash: Keccak256, -) -> Result { +) -> Result<(), CacaoError> { use k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}; let sig = Sig::try_from(signature.get(..64).ok_or(CacaoError::Verification)?) @@ -47,7 +47,7 @@ pub fn verify_eip191( if address_encoded.to_lowercase() != strip_hex_prefix(&address.to_string()).to_lowercase() { Err(CacaoError::Verification) } else { - Ok(true) + Ok(()) } } @@ -68,7 +68,7 @@ mod tests { let message = "xxx"; let signature = sign_message(message, &private_key); let address = Address::from_private_key(&private_key); - assert!(verify_eip191(&signature, &address, message_hash_internal(message)).unwrap()); + verify_eip191(&signature, &address, message_hash_internal(message)).unwrap(); } #[test] diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs new file mode 100644 index 0000000..0fc5117 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -0,0 +1,482 @@ +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/eip1271/get_rpc_url.rs b/relay_rpc/src/auth/cacao/signature/get_rpc_url.rs similarity index 100% rename from relay_rpc/src/auth/cacao/signature/eip1271/get_rpc_url.rs rename to relay_rpc/src/auth/cacao/signature/get_rpc_url.rs diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index edec7b9..4eb0ae1 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -1,7 +1,9 @@ use { self::{ - eip1271::{get_rpc_url::GetRpcUrl, verify_eip1271, EIP1271}, + eip1271::{verify_eip1271, EIP1271}, eip191::{eip191_bytes, verify_eip191, EIP191}, + eip6492::{verify_eip6492, EIP6492}, + get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, alloy_primitives::Address, @@ -12,6 +14,8 @@ use { pub mod eip1271; pub mod eip191; +pub mod eip6492; +pub mod get_rpc_url; #[cfg(test)] mod test_helpers; @@ -27,7 +31,7 @@ impl Signature { &self, cacao: &Cacao, provider: Option<&impl GetRpcUrl>, - ) -> Result { + ) -> Result<(), CacaoError> { let address = cacao.p.address()?; let signature = data_encoding::HEXLOWER_PERMISSIVE @@ -63,6 +67,27 @@ impl Signature { Err(CacaoError::Eip1271NotSupported) } } + 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, + ) + .await + } else { + Err(CacaoError::ProviderNotAvailable) + } + } else { + Err(CacaoError::Eip6492NotSupported) + } + } _ => Err(CacaoError::UnsupportedSignature), } } diff --git a/relay_rpc/src/auth/cacao/signature/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs index ac6ba36..f7f305c 100644 --- a/relay_rpc/src/auth/cacao/signature/test_helpers.rs +++ b/relay_rpc/src/auth/cacao/signature/test_helpers.rs @@ -11,8 +11,16 @@ use { 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::new().spawn(); + let anvil = Anvil::at(format_foundry_dir("bin/anvil")).spawn(); let provider = anvil.endpoint().parse().unwrap(); let private_key = anvil.keys().first().unwrap().clone(); ( @@ -22,22 +30,37 @@ pub async fn spawn_anvil() -> (AnvilInstance, Url, SigningKey) { ) } -pub async fn deploy_contract(rpc_url: &Url, private_key: &SigningKey) -> Address { +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 output = Command::new("forge") - .args([ - "create", - "--contracts=contracts", - "Eip1271Mock", - "--rpc-url", - rpc_url.as_str(), - "--private-key", - &key_encoded, - "--cache-path", - "target/.forge/cache", - "--out", - "target/.forge/out", - ]) + 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() diff --git a/relay_rpc/src/auth/cacao/tests.rs b/relay_rpc/src/auth/cacao/tests.rs index 408120f..0b5dcea 100644 --- a/relay_rpc/src/auth/cacao/tests.rs +++ b/relay_rpc/src/auth/cacao/tests.rs @@ -1,4 +1,4 @@ -use {super::signature::eip1271::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; +use {super::signature::get_rpc_url::GetRpcUrl, crate::auth::cacao::Cacao, url::Url}; struct MockGetRpcUrl; @@ -34,7 +34,6 @@ async fn cacao_verify_success() { let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); let result = cacao.verify(Some(&MockGetRpcUrl)).await; assert!(result.is_ok()); - assert!(result.map_err(|_| false).unwrap()); let identity_key = cacao.p.identity_key(); assert!(identity_key.is_ok()); @@ -71,7 +70,6 @@ async fn cacao_verify_success_identity_in_audience() { let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); let result = cacao.verify(Some(&MockGetRpcUrl)).await; assert!(result.is_ok()); - assert!(result.map_err(|_| false).unwrap()); let identity_key = cacao.p.identity_key(); assert!(identity_key.is_ok());