From 47c4ed2fa8d7e45c08b68adbcd738ef10990ba34 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 18:58:28 -0400 Subject: [PATCH 01/11] feat: EIP-6492 support --- blockchain_api/src/lib.rs | 2 +- relay_rpc/Cargo.toml | 24 +- relay_rpc/src/auth/cacao.rs | 4 +- .../signature/{eip1271/mod.rs => eip1271.rs} | 60 +++-- relay_rpc/src/auth/cacao/signature/eip191.rs | 6 +- relay_rpc/src/auth/cacao/signature/eip6492.rs | 218 ++++++++++++++++++ .../signature/{eip1271 => }/get_rpc_url.rs | 0 relay_rpc/src/auth/cacao/signature/mod.rs | 7 +- relay_rpc/src/auth/cacao/tests.rs | 4 +- 9 files changed, 271 insertions(+), 54 deletions(-) rename relay_rpc/src/auth/cacao/signature/{eip1271/mod.rs => eip1271.rs} (80%) 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/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 a60bcc8..9ba6a43 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" cacao = [ "dep:k256", "dep:sha3", - "dep:alloy-providers", + "dep:alloy-provider", "dep:alloy-transport", "dep:alloy-transport-http", "dep:alloy-rpc-types", @@ -15,7 +15,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] @@ -44,15 +45,16 @@ 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] diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index e215716..e60385e 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, @@ -101,7 +101,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 80% rename from relay_rpc/src/auth/cacao/signature/eip1271/mod.rs rename to relay_rpc/src/auth/cacao/signature/eip1271.rs index 99eed65..9e78a5d 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) } @@ -100,9 +98,9 @@ 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] @@ -113,11 +111,9 @@ mod test { 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] 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..c2472a5 --- /dev/null +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -0,0 +1,218 @@ +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! { + // `bytecode` copied from target/.forge/out/Eip6492.sol/ValidateSigOffchain.json#bytecode.object + // Copy example contract from the EIP temporarily to `contracts/Eip6492.sol` to generate the bytecode + #[sol(rpc, bytecode = "0x608060405234801561001057600080fd5b50604051610d8e380380610d8e83398101604081905261002f91610124565b600060405161003d906100dd565b604051809103906000f080158015610059573d6000803e3d6000fd5b5090506000816001600160a01b0316638f0684308686866040518463ffffffff1660e01b815260040161008e939291906101fb565b6020604051808303816000875af11580156100ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100d19190610244565b9050806000526001601ff35b610b208061026e83390190565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561011b578181015183820152602001610103565b50506000910152565b60008060006060848603121561013957600080fd5b83516001600160a01b038116811461015057600080fd5b6020850151604086015191945092506001600160401b038082111561017457600080fd5b818601915086601f83011261018857600080fd5b81518181111561019a5761019a6100ea565b604051601f8201601f19908116603f011681019083821181831017156101c2576101c26100ea565b816040528281528960208487010111156101db57600080fd5b6101ec836020830160208801610100565b80955050505050509250925092565b60018060a01b0384168152826020820152606060408201526000825180606084015261022e816080850160208701610100565b601f01601f191691909101608001949350505050565b60006020828403121561025657600080fd5b8151801515811461026657600080fd5b939250505056fe6080604052348015600f57600080fd5b50610b018061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806376be4cea146100465780638f0684301461006d57806398ef1ed814610080575b600080fd5b61005961005436600461070d565b610093565b604051901515815260200160405180910390f35b61005961007b366004610792565b610537565b61005961008e366004610792565b6105b6565b60006001600160a01b0387163b6060827f649264926492649264926492649264926492649264926492649264926492649288886100d16020826107ee565b6100dd928b9290610815565b6100e69161083f565b14905080156101c6576000606089828a6101016020826107ee565b9261010e93929190610815565b81019061011b9190610900565b9550909250905084158061012c5750865b156101bf57600080836001600160a01b03168360405161014c919061099a565b6000604051808303816000865af19150503d8060008114610189576040519150601f19603f3d011682016040523d82523d6000602084013e61018e565b606091505b5091509150816101bc5780604051639d0d6e2d60e01b81526004016101b391906109e2565b60405180910390fd5b50505b5050610200565b87878080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509294505050505b808061020c5750600083115b1561036f57604051630b135d3f60e11b81526001600160a01b038b1690631626ba7e9061023f908c9086906004016109fc565b602060405180830381865afa925050508015610278575060408051601f3d908101601f1916820190925261027591810190610a15565b60015b6102f4573d8080156102a6576040519150601f19603f3d011682016040523d82523d6000602084013e6102ab565b606091505b50851580156102ba5750600084115b156102d9576102ce8b8b8b8b8b6001610093565b94505050505061052d565b80604051636f2a959960e01b81526004016101b391906109e2565b6001600160e01b03198116630b135d3f60e11b14801581610313575086155b801561031f5750600085115b1561033f576103338c8c8c8c8c6001610093565b9550505050505061052d565b8415801561034a5750825b8015610354575087155b1561036357806000526001601ffd5b945061052d9350505050565b604187146103e55760405162461bcd60e51b815260206004820152603a60248201527f5369676e617475726556616c696461746f72237265636f7665725369676e657260448201527f3a20696e76616c6964207369676e6174757265206c656e67746800000000000060648201526084016101b3565b60006103f46020828a8c610815565b6103fd9161083f565b9050600061040f604060208b8d610815565b6104189161083f565b905060008a8a604081811061042f5761042f610a3f565b919091013560f81c915050601b811480159061044f57508060ff16601c14155b156104b25760405162461bcd60e51b815260206004820152602d60248201527f5369676e617475726556616c696461746f723a20696e76616c6964207369676e60448201526c617475726520762076616c756560981b60648201526084016101b3565b6040805160008152602081018083528e905260ff83169181019190915260608101849052608081018390526001600160a01b038e169060019060a0016020604051602081039080840390855afa158015610510573d6000803e3d6000fd5b505050602060405103516001600160a01b03161496505050505050505b9695505050505050565b604051633b5f267560e11b815260009030906376be4cea906105689088908890889088906001908990600401610a55565b6020604051808303816000875af1158015610587573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105ab9190610aae565b90505b949350505050565b604051633b5f267560e11b815260009030906376be4cea906105e690889088908890889088908190600401610a55565b6020604051808303816000875af1925050508015610621575060408051601f3d908101601f1916820190925261061e91810190610aae565b60015b610697573d80801561064f576040519150601f19603f3d011682016040523d82523d6000602084013e610654565b606091505b5080516001819003610693578160008151811061067357610673610a3f565b6020910101516001600160f81b031916600160f81b1492506105ae915050565b8082fd5b90506105ae565b6001600160a01b03811681146106b357600080fd5b50565b60008083601f8401126106c857600080fd5b50813567ffffffffffffffff8111156106e057600080fd5b6020830191508360208285010111156106f857600080fd5b9250929050565b80151581146106b357600080fd5b60008060008060008060a0878903121561072657600080fd5b86356107318161069e565b955060208701359450604087013567ffffffffffffffff81111561075457600080fd5b61076089828a016106b6565b9095509350506060870135610774816106ff565b91506080870135610784816106ff565b809150509295509295509295565b600080600080606085870312156107a857600080fd5b84356107b38161069e565b935060208501359250604085013567ffffffffffffffff8111156107d657600080fd5b6107e2878288016106b6565b95989497509550505050565b8181038181111561080f57634e487b7160e01b600052601160045260246000fd5b92915050565b6000808585111561082557600080fd5b8386111561083257600080fd5b5050820193919092039150565b8035602083101561080f57600019602084900360031b1b1692915050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261088457600080fd5b813567ffffffffffffffff8082111561089f5761089f61085d565b604051601f8301601f19908116603f011681019082821181831017156108c7576108c761085d565b816040528381528660208588010111156108e057600080fd5b836020870160208301376000602085830101528094505050505092915050565b60008060006060848603121561091557600080fd5b83356109208161069e565b9250602084013567ffffffffffffffff8082111561093d57600080fd5b61094987838801610873565b9350604086013591508082111561095f57600080fd5b5061096c86828701610873565b9150509250925092565b60005b83811015610991578181015183820152602001610979565b50506000910152565b600082516109ac818460208701610976565b9190910192915050565b600081518084526109ce816020860160208601610976565b601f01601f19169290920160200192915050565b6020815260006109f560208301846109b6565b9392505050565b8281526040602082015260006105ae60408301846109b6565b600060208284031215610a2757600080fd5b81516001600160e01b0319811681146109f557600080fd5b634e487b7160e01b600052603260045260246000fd5b6001600160a01b03871681526020810186905260a0604082018190528101849052838560c0830137600060c085830181019190915292151560608201529015156080820152601f909201601f1916909101019392505050565b600060208284031215610ac057600080fd5b81516109f5816106ff56fea2646970667358221220304a40774481cc601339bc29f4dc264e3cf712d479399cec7c2b1a9aeeec962964736f6c63430008190033")] + contract ValidateSigOffchain { + constructor (address _signer, bytes32 _hash, bytes memory _signature); + } +} + +pub async fn verify_eip6492( + signature: Vec, + address: Address, + hash: &[u8; 32], + provider: Url, +) -> Result<(), CacaoError> { + let provider = ReqwestProvider::::new_http(provider); + + let call_request = TransactionRequest::default().input(TransactionInput::new( + [ + ValidateSigOffchain::BYTECODE.clone(), + ValidateSigOffchain::constructorCall { + _signer: address, + _hash: hash.into(), + _signature: signature.into(), + } + .abi_encode() + .into(), + ] + .concat() + .into(), + )); + + let result = provider + .call(&call_request, Default::default()) + .await + .map_err(CacaoError::Eip1271Internal)?; + 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::test_helpers::{ + deploy_contract, + message_hash, + sign_message, + spawn_anvil, + }, + k256::ecdsa::SigningKey, + }; + + #[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).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).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).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).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).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) + )); + } +} 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..5eefbdb 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -1,7 +1,8 @@ use { self::{ - eip1271::{get_rpc_url::GetRpcUrl, verify_eip1271, EIP1271}, + eip1271::{verify_eip1271, EIP1271}, eip191::{eip191_bytes, verify_eip191, EIP191}, + get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, alloy_primitives::Address, @@ -12,6 +13,8 @@ use { pub mod eip1271; pub mod eip191; +pub mod eip6492; +pub mod get_rpc_url; #[cfg(test)] mod test_helpers; @@ -27,7 +30,7 @@ impl Signature { &self, cacao: &Cacao, provider: Option<&impl GetRpcUrl>, - ) -> Result { + ) -> Result<(), CacaoError> { let address = cacao.p.address()?; let signature = data_encoding::HEXLOWER_PERMISSIVE 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()); From be2c19550920c165457296a9f9452ff4c7b4349f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 19:03:33 -0400 Subject: [PATCH 02/11] chore: separate error --- relay_rpc/src/auth/cacao.rs | 3 +++ relay_rpc/src/auth/cacao/signature/eip6492.rs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index e60385e..49f5d02 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -50,6 +50,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 { diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index c2472a5..b54040c 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -46,7 +46,8 @@ pub async fn verify_eip6492( let result = provider .call(&call_request, Default::default()) .await - .map_err(CacaoError::Eip1271Internal)?; + .map_err(CacaoError::Eip6492Internal)?; + let magic = result.first(); if let Some(magic) = magic { if magic == &MAGIC_VALUE { From aec8d23aeec54128c774390895f24de789abc0cb Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 19:25:39 -0400 Subject: [PATCH 03/11] chore: don't hardcode bytecode --- .github/workflows/ci.yaml | 4 - Cargo.toml | 4 + build.rs | 54 +++++++++ contracts/Eip6492.sol | 111 ++++++++++++++++++ relay_rpc/src/auth/cacao/signature/eip6492.rs | 33 +++--- 5 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 build.rs create mode 100644 contracts/Eip6492.sol 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/Cargo.toml b/Cargo.toml index 7857ebc..de49c05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,10 @@ rand = "0.8.5" futures-util = "0.3" once_cell = "1.19" +[build-dependencies] +serde_json = "1.0" +hex = "0.4.3" + [[example]] name = "websocket_client" required-features = ["client", "rpc"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b52deb3 --- /dev/null +++ b/build.rs @@ -0,0 +1,54 @@ +use { + serde_json::Value, + std::process::{Command, Stdio}, +}; + +fn main() { + println!("cargo::rerun-if-changed=contracts/"); + compile_contracts(); + + extract_eip6492_bytecode(); +} + +fn compile_contracts() { + let output = Command::new("forge") + .args([ + "build", + "--contracts=contracts", + "--cache-path", + "target/.forge/cache", + "--out", + "target/.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()); +} + +fn extract_eip6492_bytecode() { + const EIP6492_FILE: &str = "target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; + const EIP6492_BYTECODE_FILE: &str = + "target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; + + let contents = serde_json::from_slice::(&std::fs::read(EIP6492_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(EIP6492_BYTECODE_FILE, bytecode).unwrap(); +} diff --git a/contracts/Eip6492.sol b/contracts/Eip6492.sol new file mode 100644 index 0000000..41586fe --- /dev/null +++ b/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/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index b54040c..0782cc6 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -12,13 +12,12 @@ pub const EIP6492: &str = "eip6492"; // https://eips.ethereum.org/EIPS/eip-6492 const MAGIC_VALUE: u8 = 0x01; sol! { - // `bytecode` copied from target/.forge/out/Eip6492.sol/ValidateSigOffchain.json#bytecode.object - // Copy example contract from the EIP temporarily to `contracts/Eip6492.sol` to generate the bytecode - #[sol(rpc, bytecode = "0x608060405234801561001057600080fd5b50604051610d8e380380610d8e83398101604081905261002f91610124565b600060405161003d906100dd565b604051809103906000f080158015610059573d6000803e3d6000fd5b5090506000816001600160a01b0316638f0684308686866040518463ffffffff1660e01b815260040161008e939291906101fb565b6020604051808303816000875af11580156100ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100d19190610244565b9050806000526001601ff35b610b208061026e83390190565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561011b578181015183820152602001610103565b50506000910152565b60008060006060848603121561013957600080fd5b83516001600160a01b038116811461015057600080fd5b6020850151604086015191945092506001600160401b038082111561017457600080fd5b818601915086601f83011261018857600080fd5b81518181111561019a5761019a6100ea565b604051601f8201601f19908116603f011681019083821181831017156101c2576101c26100ea565b816040528281528960208487010111156101db57600080fd5b6101ec836020830160208801610100565b80955050505050509250925092565b60018060a01b0384168152826020820152606060408201526000825180606084015261022e816080850160208701610100565b601f01601f191691909101608001949350505050565b60006020828403121561025657600080fd5b8151801515811461026657600080fd5b939250505056fe6080604052348015600f57600080fd5b50610b018061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806376be4cea146100465780638f0684301461006d57806398ef1ed814610080575b600080fd5b61005961005436600461070d565b610093565b604051901515815260200160405180910390f35b61005961007b366004610792565b610537565b61005961008e366004610792565b6105b6565b60006001600160a01b0387163b6060827f649264926492649264926492649264926492649264926492649264926492649288886100d16020826107ee565b6100dd928b9290610815565b6100e69161083f565b14905080156101c6576000606089828a6101016020826107ee565b9261010e93929190610815565b81019061011b9190610900565b9550909250905084158061012c5750865b156101bf57600080836001600160a01b03168360405161014c919061099a565b6000604051808303816000865af19150503d8060008114610189576040519150601f19603f3d011682016040523d82523d6000602084013e61018e565b606091505b5091509150816101bc5780604051639d0d6e2d60e01b81526004016101b391906109e2565b60405180910390fd5b50505b5050610200565b87878080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509294505050505b808061020c5750600083115b1561036f57604051630b135d3f60e11b81526001600160a01b038b1690631626ba7e9061023f908c9086906004016109fc565b602060405180830381865afa925050508015610278575060408051601f3d908101601f1916820190925261027591810190610a15565b60015b6102f4573d8080156102a6576040519150601f19603f3d011682016040523d82523d6000602084013e6102ab565b606091505b50851580156102ba5750600084115b156102d9576102ce8b8b8b8b8b6001610093565b94505050505061052d565b80604051636f2a959960e01b81526004016101b391906109e2565b6001600160e01b03198116630b135d3f60e11b14801581610313575086155b801561031f5750600085115b1561033f576103338c8c8c8c8c6001610093565b9550505050505061052d565b8415801561034a5750825b8015610354575087155b1561036357806000526001601ffd5b945061052d9350505050565b604187146103e55760405162461bcd60e51b815260206004820152603a60248201527f5369676e617475726556616c696461746f72237265636f7665725369676e657260448201527f3a20696e76616c6964207369676e6174757265206c656e67746800000000000060648201526084016101b3565b60006103f46020828a8c610815565b6103fd9161083f565b9050600061040f604060208b8d610815565b6104189161083f565b905060008a8a604081811061042f5761042f610a3f565b919091013560f81c915050601b811480159061044f57508060ff16601c14155b156104b25760405162461bcd60e51b815260206004820152602d60248201527f5369676e617475726556616c696461746f723a20696e76616c6964207369676e60448201526c617475726520762076616c756560981b60648201526084016101b3565b6040805160008152602081018083528e905260ff83169181019190915260608101849052608081018390526001600160a01b038e169060019060a0016020604051602081039080840390855afa158015610510573d6000803e3d6000fd5b505050602060405103516001600160a01b03161496505050505050505b9695505050505050565b604051633b5f267560e11b815260009030906376be4cea906105689088908890889088906001908990600401610a55565b6020604051808303816000875af1158015610587573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105ab9190610aae565b90505b949350505050565b604051633b5f267560e11b815260009030906376be4cea906105e690889088908890889088908190600401610a55565b6020604051808303816000875af1925050508015610621575060408051601f3d908101601f1916820190925261061e91810190610aae565b60015b610697573d80801561064f576040519150601f19603f3d011682016040523d82523d6000602084013e610654565b606091505b5080516001819003610693578160008151811061067357610673610a3f565b6020910101516001600160f81b031916600160f81b1492506105ae915050565b8082fd5b90506105ae565b6001600160a01b03811681146106b357600080fd5b50565b60008083601f8401126106c857600080fd5b50813567ffffffffffffffff8111156106e057600080fd5b6020830191508360208285010111156106f857600080fd5b9250929050565b80151581146106b357600080fd5b60008060008060008060a0878903121561072657600080fd5b86356107318161069e565b955060208701359450604087013567ffffffffffffffff81111561075457600080fd5b61076089828a016106b6565b9095509350506060870135610774816106ff565b91506080870135610784816106ff565b809150509295509295509295565b600080600080606085870312156107a857600080fd5b84356107b38161069e565b935060208501359250604085013567ffffffffffffffff8111156107d657600080fd5b6107e2878288016106b6565b95989497509550505050565b8181038181111561080f57634e487b7160e01b600052601160045260246000fd5b92915050565b6000808585111561082557600080fd5b8386111561083257600080fd5b5050820193919092039150565b8035602083101561080f57600019602084900360031b1b1692915050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261088457600080fd5b813567ffffffffffffffff8082111561089f5761089f61085d565b604051601f8301601f19908116603f011681019082821181831017156108c7576108c761085d565b816040528381528660208588010111156108e057600080fd5b836020870160208301376000602085830101528094505050505092915050565b60008060006060848603121561091557600080fd5b83356109208161069e565b9250602084013567ffffffffffffffff8082111561093d57600080fd5b61094987838801610873565b9350604086013591508082111561095f57600080fd5b5061096c86828701610873565b9150509250925092565b60005b83811015610991578181015183820152602001610979565b50506000910152565b600082516109ac818460208701610976565b9190910192915050565b600081518084526109ce816020860160208601610976565b601f01601f19169290920160200192915050565b6020815260006109f560208301846109b6565b9392505050565b8281526040602082015260006105ae60408301846109b6565b600060208284031215610a2757600080fd5b81516001600160e01b0319811681146109f557600080fd5b634e487b7160e01b600052603260045260246000fd5b6001600160a01b03871681526020810186905260a0604082018190528101849052838560c0830137600060c085830181019190915292151560608201529015156080820152601f909201601f1916909101019392505050565b600060208284031215610ac057600080fd5b81516109f5816106ff56fea2646970667358221220304a40774481cc601339bc29f4dc264e3cf712d479399cec7c2b1a9aeeec962964736f6c63430008190033")] contract ValidateSigOffchain { constructor (address _signer, bytes32 _hash, bytes memory _signature); } } +const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = + include_bytes!("../../../../../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"); pub async fn verify_eip6492( signature: Vec, @@ -28,23 +27,21 @@ pub async fn verify_eip6492( ) -> Result<(), CacaoError> { let provider = ReqwestProvider::::new_http(provider); - let call_request = TransactionRequest::default().input(TransactionInput::new( - [ - ValidateSigOffchain::BYTECODE.clone(), - ValidateSigOffchain::constructorCall { - _signer: address, - _hash: hash.into(), - _signature: signature.into(), - } - .abi_encode() - .into(), - ] - .concat() - .into(), - )); + 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(&call_request, Default::default()) + .call(&transaction_request, Default::default()) .await .map_err(CacaoError::Eip6492Internal)?; From 53373ae6582e78ad6ba7082b5f3229b4fa75b1f0 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 19:40:38 -0400 Subject: [PATCH 04/11] chore: move Forge code into relay_rpc --- Cargo.toml | 4 ---- README.md | 9 +++++++++ relay_rpc/Cargo.toml | 4 ++++ build.rs => relay_rpc/build.rs | 10 ++++++---- {contracts => relay_rpc/contracts}/Eip1271Mock.sol | 0 {contracts => relay_rpc/contracts}/Eip6492.sol | 0 relay_rpc/src/auth/cacao/signature/test_helpers.rs | 2 +- 7 files changed, 20 insertions(+), 9 deletions(-) rename build.rs => relay_rpc/build.rs (79%) rename {contracts => relay_rpc/contracts}/Eip1271Mock.sol (100%) rename {contracts => relay_rpc/contracts}/Eip6492.sol (100%) diff --git a/Cargo.toml b/Cargo.toml index de49c05..7857ebc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,6 @@ rand = "0.8.5" futures-util = "0.3" once_cell = "1.19" -[build-dependencies] -serde_json = "1.0" -hex = "0.4.3" - [[example]] name = "websocket_client" required-features = ["client", "rpc"] diff --git a/README.md b/README.md index 973fd77..e29e2a0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ 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. +### `cacao` feature + +To aid IDE integration you may want to add this to your local `relay_rpc/Cargo.toml` file: + +```toml +[features] +default = ["cacao"] +``` + ### Test dependencies Foundry is required to be installed to your system for testing: diff --git a/relay_rpc/Cargo.toml b/relay_rpc/Cargo.toml index 9ba6a43..63dd658 100644 --- a/relay_rpc/Cargo.toml +++ b/relay_rpc/Cargo.toml @@ -60,5 +60,9 @@ 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/build.rs b/relay_rpc/build.rs similarity index 79% rename from build.rs rename to relay_rpc/build.rs index b52deb3..6b2b241 100644 --- a/build.rs +++ b/relay_rpc/build.rs @@ -4,9 +4,11 @@ use { }; fn main() { - println!("cargo::rerun-if-changed=contracts/"); + println!("cargo::rerun-if-changed=relay_rpc/contracts/"); + #[cfg(feature = "cacao")] compile_contracts(); + #[cfg(feature = "cacao")] extract_eip6492_bytecode(); } @@ -14,7 +16,7 @@ fn compile_contracts() { let output = Command::new("forge") .args([ "build", - "--contracts=contracts", + "--contracts=relay_rpc/contracts", "--cache-path", "target/.forge/cache", "--out", @@ -35,9 +37,9 @@ fn compile_contracts() { } fn extract_eip6492_bytecode() { - const EIP6492_FILE: &str = "target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; + const EIP6492_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; const EIP6492_BYTECODE_FILE: &str = - "target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; + "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; let contents = serde_json::from_slice::(&std::fs::read(EIP6492_FILE).unwrap()).unwrap(); let bytecode = contents diff --git a/contracts/Eip1271Mock.sol b/relay_rpc/contracts/Eip1271Mock.sol similarity index 100% rename from contracts/Eip1271Mock.sol rename to relay_rpc/contracts/Eip1271Mock.sol diff --git a/contracts/Eip6492.sol b/relay_rpc/contracts/Eip6492.sol similarity index 100% rename from contracts/Eip6492.sol rename to relay_rpc/contracts/Eip6492.sol diff --git a/relay_rpc/src/auth/cacao/signature/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs index ac6ba36..fe12b9a 100644 --- a/relay_rpc/src/auth/cacao/signature/test_helpers.rs +++ b/relay_rpc/src/auth/cacao/signature/test_helpers.rs @@ -27,7 +27,7 @@ pub async fn deploy_contract(rpc_url: &Url, private_key: &SigningKey) -> Address let output = Command::new("forge") .args([ "create", - "--contracts=contracts", + "--contracts=relay_rpc/contracts", "Eip1271Mock", "--rpc-url", rpc_url.as_str(), From 6216d9ffa3fb5a9386b43e68f28f0864036ac1bf Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 21:51:38 -0400 Subject: [PATCH 05/11] fix: test EIP-6492 signatures --- relay_rpc/build.rs | 20 +- relay_rpc/contracts/Create2.sol | 49 ++++ relay_rpc/contracts/Eip1271Mock.sol | 8 +- relay_rpc/src/auth/cacao/signature/eip1271.rs | 48 +++- relay_rpc/src/auth/cacao/signature/eip6492.rs | 239 +++++++++++++++++- .../src/auth/cacao/signature/test_helpers.rs | 41 ++- 6 files changed, 369 insertions(+), 36 deletions(-) create mode 100644 relay_rpc/contracts/Create2.sol diff --git a/relay_rpc/build.rs b/relay_rpc/build.rs index 6b2b241..0e0c746 100644 --- a/relay_rpc/build.rs +++ b/relay_rpc/build.rs @@ -9,7 +9,7 @@ fn main() { compile_contracts(); #[cfg(feature = "cacao")] - extract_eip6492_bytecode(); + extract_bytecodes(); } fn compile_contracts() { @@ -36,12 +36,18 @@ fn compile_contracts() { assert!(output.status.success()); } -fn extract_eip6492_bytecode() { - const EIP6492_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; - const EIP6492_BYTECODE_FILE: &str = - "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; +const EIP6492_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; +const EIP6492_BYTECODE_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; +const EIP1271_MOCK_FILE: &str = "../target/.forge/out/Eip1271Mock.sol/Eip1271Mock.json"; +const EIP1271_MOCK_BYTECODE_FILE: &str = + "../target/.forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode"; +fn extract_bytecodes() { + extract_bytecode(EIP6492_FILE, EIP6492_BYTECODE_FILE); + extract_bytecode(EIP1271_MOCK_FILE, EIP1271_MOCK_BYTECODE_FILE); +} - let contents = serde_json::from_slice::(&std::fs::read(EIP6492_FILE).unwrap()).unwrap(); +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() @@ -52,5 +58,5 @@ fn extract_eip6492_bytecode() { .strip_prefix("0x") .unwrap(); let bytecode = hex::decode(bytecode).unwrap(); - std::fs::write(EIP6492_BYTECODE_FILE, 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/relay_rpc/contracts/Eip1271Mock.sol b/relay_rpc/contracts/Eip1271Mock.sol index 6a50659..35fa8b9 100644 --- a/relay_rpc/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/src/auth/cacao/signature/eip1271.rs b/relay_rpc/src/auth/cacao/signature/eip1271.rs index 9e78a5d..6deddf7 100644 --- a/relay_rpc/src/auth/cacao/signature/eip1271.rs +++ b/relay_rpc/src/auth/cacao/signature/eip1271.rs @@ -73,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, @@ -106,7 +112,13 @@ mod test { #[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); @@ -119,7 +131,13 @@ mod test { #[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); @@ -134,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( @@ -151,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); @@ -168,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/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index 0782cc6..8e5edc1 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -66,7 +66,11 @@ mod test { message_hash, sign_message, spawn_anvil, + CREATE2_CONTRACT, + EIP1271_MOCK_CONTRACT, }, + alloy_primitives::{b256, Uint}, + alloy_sol_types::{SolCall, SolValue}, k256::ecdsa::SigningKey, }; @@ -134,7 +138,13 @@ mod test { #[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); @@ -147,7 +157,13 @@ mod test { #[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); @@ -162,7 +178,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( @@ -179,7 +201,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); @@ -196,7 +224,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); @@ -213,4 +247,199 @@ mod test { Err(CacaoError::Verification) )); } + + const EIP1271_MOCK_BYTECODE: &[u8] = + include_bytes!("../../../../../target/.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] + #[ignore] + 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/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs index fe12b9a..0828cef 100644 --- a/relay_rpc/src/auth/cacao/signature/test_helpers.rs +++ b/relay_rpc/src/auth/cacao/signature/test_helpers.rs @@ -22,22 +22,35 @@ 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 mut args = vec![ + "create", + "--contracts=relay_rpc/contracts", + contract_name, + "--rpc-url", + rpc_url.as_str(), + "--private-key", + &key_encoded, + "--cache-path", + "target/.forge/cache", + "--out", + "target/.forge/out", + ]; + if let Some(arg) = constructor_arg { + args.push("--constructor-args"); + args.push(arg); + } let output = Command::new("forge") - .args([ - "create", - "--contracts=relay_rpc/contracts", - "Eip1271Mock", - "--rpc-url", - rpc_url.as_str(), - "--private-key", - &key_encoded, - "--cache-path", - "target/.forge/cache", - "--out", - "target/.forge/out", - ]) + .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() From d3ee328fe55440dd97ac0c406cca3f4a307b5178 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 22:12:45 -0400 Subject: [PATCH 06/11] fix: restore test_eip6492_fail_wrong_contract_address --- relay_rpc/src/auth/cacao/signature/eip6492.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index 8e5edc1..572e51b 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -43,7 +43,17 @@ pub async fn verify_eip6492( let result = provider .call(&transaction_request, Default::default()) .await - .map_err(CacaoError::Eip6492Internal)?; + .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 { @@ -387,7 +397,6 @@ mod test { } #[tokio::test] - #[ignore] async fn test_eip6492_fail_wrong_contract_address() { let (_anvil, rpc_url, private_key) = spawn_anvil().await; let create2_factory_address = @@ -409,7 +418,7 @@ mod test { signature, predeploy_address, &message_hash(message), - rpc_url + rpc_url, ) .await, Err(CacaoError::Verification) From d0b3400e8116a9eb6f8901abd527ce6fcf6da8ea Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 23 Apr 2024 22:20:42 -0400 Subject: [PATCH 07/11] fix: support EIP-6492 CACAO verification --- relay_rpc/src/auth/cacao.rs | 3 +++ relay_rpc/src/auth/cacao/signature/mod.rs | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/relay_rpc/src/auth/cacao.rs b/relay_rpc/src/auth/cacao.rs index 49f5d02..179077f 100644 --- a/relay_rpc/src/auth/cacao.rs +++ b/relay_rpc/src/auth/cacao.rs @@ -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, diff --git a/relay_rpc/src/auth/cacao/signature/mod.rs b/relay_rpc/src/auth/cacao/signature/mod.rs index 5eefbdb..4eb0ae1 100644 --- a/relay_rpc/src/auth/cacao/signature/mod.rs +++ b/relay_rpc/src/auth/cacao/signature/mod.rs @@ -2,6 +2,7 @@ use { self::{ eip1271::{verify_eip1271, EIP1271}, eip191::{eip191_bytes, verify_eip191, EIP191}, + eip6492::{verify_eip6492, EIP6492}, get_rpc_url::GetRpcUrl, }, super::{Cacao, CacaoError}, @@ -66,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), } } From 18c31910c9ea95a3552990ede247e522085ccfe9 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 24 Apr 2024 17:28:02 -0400 Subject: [PATCH 08/11] fix: install foundry in build.rs --- README.md | 4 -- relay_rpc/build.rs | 62 +++++++++++++++---- relay_rpc/src/auth/cacao/signature/eip6492.rs | 10 +-- .../src/auth/cacao/signature/test_helpers.rs | 18 ++++-- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e29e2a0..cdc7eda 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,6 @@ To aid IDE integration you may want to add this to your local `relay_rpc/Cargo.t default = ["cacao"] ``` -### Test dependencies - -Foundry is required to be installed to your system for testing: - # License [Apache License (Version 2.0)](LICENSE) diff --git a/relay_rpc/build.rs b/relay_rpc/build.rs index 0e0c746..1c2e9a0 100644 --- a/relay_rpc/build.rs +++ b/relay_rpc/build.rs @@ -4,23 +4,54 @@ use { }; fn main() { - println!("cargo::rerun-if-changed=relay_rpc/contracts/"); #[cfg(feature = "cacao")] - compile_contracts(); + build_contracts(); +} - #[cfg(feature = "cacao")] +fn build_contracts() { + println!("cargo::rerun-if-changed=contracts"); + install_foundary(); + compile_contracts(); extract_bytecodes(); } +fn format_foundry_dir(path: &str) -> String { + format!( + "{}/../../../../.foundry/{}", + std::env::var("OUT_DIR").unwrap(), + path + ) +} + +fn install_foundary() { + 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("forge") + let output = Command::new(format_foundry_dir("bin/forge")) .args([ "build", "--contracts=relay_rpc/contracts", "--cache-path", - "target/.forge/cache", + &format_foundry_dir("forge/cache"), "--out", - "target/.forge/out", + &format_foundry_dir("forge/out"), ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -36,14 +67,19 @@ fn compile_contracts() { assert!(output.status.success()); } -const EIP6492_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.json"; -const EIP6492_BYTECODE_FILE: &str = "../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"; -const EIP1271_MOCK_FILE: &str = "../target/.forge/out/Eip1271Mock.sol/Eip1271Mock.json"; -const EIP1271_MOCK_BYTECODE_FILE: &str = - "../target/.forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode"; +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(EIP6492_FILE, EIP6492_BYTECODE_FILE); - extract_bytecode(EIP1271_MOCK_FILE, EIP1271_MOCK_BYTECODE_FILE); + 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) { diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index 572e51b..642e75a 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -16,8 +16,9 @@ sol! { constructor (address _signer, bytes32 _hash, bytes memory _signature); } } -const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = - include_bytes!("../../../../../target/.forge/out/Eip6492.sol/ValidateSigOffchain.bytecode"); +const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = include_bytes!( + "../../../../../target/.foundry/forge/out/Eip6492.sol/ValidateSigOffchain.bytecode" +); pub async fn verify_eip6492( signature: Vec, @@ -258,8 +259,9 @@ mod test { )); } - const EIP1271_MOCK_BYTECODE: &[u8] = - include_bytes!("../../../../../target/.forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode"); + const EIP1271_MOCK_BYTECODE: &[u8] = include_bytes!( + "../../../../../target/.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, diff --git a/relay_rpc/src/auth/cacao/signature/test_helpers.rs b/relay_rpc/src/auth/cacao/signature/test_helpers.rs index 0828cef..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(); ( @@ -32,6 +40,8 @@ pub async fn deploy_contract( 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", @@ -41,15 +51,15 @@ pub async fn deploy_contract( "--private-key", &key_encoded, "--cache-path", - "target/.forge/cache", + &cache_folder, "--out", - "target/.forge/out", + &out_folder, ]; if let Some(arg) = constructor_arg { args.push("--constructor-args"); args.push(arg); } - let output = Command::new("forge") + let output = Command::new(format_foundry_dir("bin/forge")) .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) From 01cc5538d22a8acc0e0396affa8c1e8429afc655 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 24 Apr 2024 17:33:06 -0400 Subject: [PATCH 09/11] fix: relative include path --- relay_rpc/src/auth/cacao/signature/eip6492.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index 642e75a..825c003 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -16,9 +16,10 @@ sol! { constructor (address _signer, bytes32 _hash, bytes memory _signature); } } -const VALIDATE_SIG_OFFCHAIN_BYTECODE: &[u8] = include_bytes!( - "../../../../../target/.foundry/forge/out/Eip6492.sol/ValidateSigOffchain.bytecode" -); +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, @@ -259,9 +260,10 @@ mod test { )); } - const EIP1271_MOCK_BYTECODE: &[u8] = include_bytes!( - "../../../../../target/.foundry/forge/out/Eip1271Mock.sol/Eip1271Mock.bytecode" - ); + 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, From aafed3ae8db3cc2c3395456e6e08ea8a3e8627b6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 24 Apr 2024 17:34:57 -0400 Subject: [PATCH 10/11] fix: spelling --- relay_rpc/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relay_rpc/build.rs b/relay_rpc/build.rs index 1c2e9a0..6d8b1a1 100644 --- a/relay_rpc/build.rs +++ b/relay_rpc/build.rs @@ -10,7 +10,7 @@ fn main() { fn build_contracts() { println!("cargo::rerun-if-changed=contracts"); - install_foundary(); + install_foundry(); compile_contracts(); extract_bytecodes(); } @@ -23,7 +23,7 @@ fn format_foundry_dir(path: &str) -> String { ) } -fn install_foundary() { +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(); From 77f0a863acc14d397d59cb4f57bd004faae422d3 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 25 Apr 2024 11:22:11 -0400 Subject: [PATCH 11/11] chore: add manual test case --- relay_rpc/src/auth/cacao/signature/eip6492.rs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/relay_rpc/src/auth/cacao/signature/eip6492.rs b/relay_rpc/src/auth/cacao/signature/eip6492.rs index 825c003..0fc5117 100644 --- a/relay_rpc/src/auth/cacao/signature/eip6492.rs +++ b/relay_rpc/src/auth/cacao/signature/eip6492.rs @@ -73,19 +73,43 @@ pub async fn verify_eip6492( mod test { use { super::*, - crate::auth::cacao::signature::test_helpers::{ - deploy_contract, - message_hash, - sign_message, - spawn_anvil, - CREATE2_CONTRACT, - EIP1271_MOCK_CONTRACT, + crate::auth::cacao::signature::{ + strip_hex_prefix, + test_helpers::{ + deploy_contract, + message_hash, + sign_message, + spawn_anvil, + CREATE2_CONTRACT, + EIP1271_MOCK_CONTRACT, + }, }, - alloy_primitives::{b256, Uint}, + 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;