From 6937f2339daf82f2fdf6ea653075ef58d2c10f22 Mon Sep 17 00:00:00 2001 From: Dakota Brink <779390+codabrink@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:50:35 -0400 Subject: [PATCH] Full stack SCW tests with external Anvil instance (#1141) * add image * connect mls service to foundry * dockerfile * localhost * test passes * cleanup * cleanup * unnecesasry arg * foundry -> anvil * test utils feature * Make test E2E * cleanup * cleanup * cleanup --------- Co-authored-by: Nicholas Molnar <65710+neekolas@users.noreply.github.com> --- dev/build_validation_service_local | 6 +- dev/docker/anvil.Dockerfile | 6 + dev/docker/docker-compose.yml | 9 + dev/docker/up | 2 +- dev/validation_service/Dockerfile | 4 +- mls_validation_service/Cargo.toml | 3 + xmtp_id/src/associations/serialization.rs | 5 +- xmtp_id/src/associations/test_utils.rs | 1 + .../src/associations/verified_signature.rs | 4 + .../src/scw_verifier/chain_rpc_verifier.rs | 66 +++++++- xmtp_id/src/scw_verifier/mod.rs | 11 ++ .../scw_verifier/remote_signature_verifier.rs | 6 +- xmtp_mls/src/builder.rs | 159 +++++++++--------- xmtp_mls/src/storage/encrypted_store/group.rs | 6 +- 14 files changed, 194 insertions(+), 94 deletions(-) create mode 100644 dev/docker/anvil.Dockerfile diff --git a/dev/build_validation_service_local b/dev/build_validation_service_local index 95c18f581..979099a47 100755 --- a/dev/build_validation_service_local +++ b/dev/build_validation_service_local @@ -5,12 +5,12 @@ set -eu if [ ! -x "$(command -v x86_64-linux-gnu-gcc)" ] && [ "$(uname)" = "Darwin" ]; then echo "Installing cross compile toolchain" brew tap messense/macos-cross-toolchains - brew install x86_64-unknown-linux-gnu + brew install x86_64-unknown-linux-gnu fi rustup target add x86_64-unknown-linux-gnu export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc -cargo build --release --package mls_validation_service --target x86_64-unknown-linux-gnu +cargo build --release --package mls_validation_service --features test-utils --target x86_64-unknown-linux-gnu mkdir -p .cache cp -f ./target/x86_64-unknown-linux-gnu/release/mls-validation-service ./.cache/mls-validation-service -docker build --platform=linux/amd64 -t xmtp/mls-validation-service:latest -f ./dev/validation_service/local.Dockerfile . \ No newline at end of file +docker build --platform=linux/amd64 -t xmtp/mls-validation-service:latest -f ./dev/validation_service/local.Dockerfile . diff --git a/dev/docker/anvil.Dockerfile b/dev/docker/anvil.Dockerfile new file mode 100644 index 000000000..dc36b1929 --- /dev/null +++ b/dev/docker/anvil.Dockerfile @@ -0,0 +1,6 @@ +# syntax=docker/dockerfile:1.4 +FROM ghcr.io/foundry-rs/foundry + +WORKDIR /anvil + +ENTRYPOINT anvil --host 0.0.0.0 --base-fee 100 diff --git a/dev/docker/docker-compose.yml b/dev/docker/docker-compose.yml index 01dda9772..902586445 100644 --- a/dev/docker/docker-compose.yml +++ b/dev/docker/docker-compose.yml @@ -26,6 +26,15 @@ services: build: context: ../.. dockerfile: ./dev/validation_service/local.Dockerfile + environment: + ANVIL_URL: "http://anvil:8545" + + anvil: + build: + dockerfile: ./anvil.Dockerfile + platform: linux/amd64 + ports: + - 8545:8545 db: image: postgres:13 diff --git a/dev/docker/up b/dev/docker/up index 633f0db7b..e317f1f10 100755 --- a/dev/docker/up +++ b/dev/docker/up @@ -3,4 +3,4 @@ set -eou pipefail script_dir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" "${script_dir}"/compose pull -"${script_dir}"/compose up -d --build \ No newline at end of file +"${script_dir}"/compose up -d --build diff --git a/dev/validation_service/Dockerfile b/dev/validation_service/Dockerfile index 6549ab50c..665df30d7 100644 --- a/dev/validation_service/Dockerfile +++ b/dev/validation_service/Dockerfile @@ -1,10 +1,10 @@ FROM rust:1-bullseye as builder WORKDIR /code COPY . . -RUN cargo build --release --package mls_validation_service +RUN cargo build --release --features test-utils --package mls_validation_service FROM debian:bullseye-slim RUN apt-get update && apt-get install -y sqlite3 curl COPY --from=builder /code/target/release/mls-validation-service /usr/local/bin/mls-validation-service ENV RUST_LOG=info -CMD ["mls-validation-service"] \ No newline at end of file +CMD ["mls-validation-service"] diff --git a/mls_validation_service/Cargo.toml b/mls_validation_service/Cargo.toml index bae6eefbb..3d8cf55c9 100644 --- a/mls_validation_service/Cargo.toml +++ b/mls_validation_service/Cargo.toml @@ -37,3 +37,6 @@ rand = { workspace = true } sha2.workspace = true xmtp_id = { workspace = true, features = ["test-utils"] } xmtp_mls = { workspace = true, features = ["test-utils"] } + +[features] +test-utils = ["xmtp_id/test-utils"] diff --git a/xmtp_id/src/associations/serialization.rs b/xmtp_id/src/associations/serialization.rs index e907df6a6..8957bdd86 100644 --- a/xmtp_id/src/associations/serialization.rs +++ b/xmtp_id/src/associations/serialization.rs @@ -309,11 +309,12 @@ impl From for Vec { } } -impl From<&SmartContractWalletValidationResponseProto> for ValidationResponse { - fn from(value: &SmartContractWalletValidationResponseProto) -> Self { +impl From for ValidationResponse { + fn from(value: SmartContractWalletValidationResponseProto) -> Self { Self { is_valid: value.is_valid, block_number: value.block_number, + error: value.error, } } } diff --git a/xmtp_id/src/associations/test_utils.rs b/xmtp_id/src/associations/test_utils.rs index f675fb30b..e1ac1d4bb 100644 --- a/xmtp_id/src/associations/test_utils.rs +++ b/xmtp_id/src/associations/test_utils.rs @@ -60,6 +60,7 @@ impl SmartContractSignatureVerifier for MockSmartContractSignatureVerifier { Ok(ValidationResponse { is_valid: self.is_valid_signature, block_number: Some(1), + error: None, }) } } diff --git a/xmtp_id/src/associations/verified_signature.rs b/xmtp_id/src/associations/verified_signature.rs index 023055d86..7f564b02c 100644 --- a/xmtp_id/src/associations/verified_signature.rs +++ b/xmtp_id/src/associations/verified_signature.rs @@ -153,6 +153,10 @@ impl VerifiedSignature { signature_bytes.to_vec(), )) } else { + tracing::error!( + "Smart contract wallet signature is invalid {:?}", + response.error + ); Err(SignatureError::Invalid) } } diff --git a/xmtp_id/src/scw_verifier/chain_rpc_verifier.rs b/xmtp_id/src/scw_verifier/chain_rpc_verifier.rs index 4b6a05714..ec32edb70 100644 --- a/xmtp_id/src/scw_verifier/chain_rpc_verifier.rs +++ b/xmtp_id/src/scw_verifier/chain_rpc_verifier.rs @@ -105,6 +105,7 @@ impl SmartContractSignatureVerifier for RpcSmartContractWalletVerifier { Ok(ValidationResponse { is_valid, block_number: block_number.as_number().map(|n| n.0[0]), + error: None, }) } } @@ -117,7 +118,10 @@ pub mod tests { use super::*; use ethers::{ abi::{self, Token}, - core::utils::Anvil, + core::{ + k256::{elliptic_curve::SecretKey, Secp256k1}, + utils::Anvil, + }, middleware::{MiddlewareBuilder, SignerMiddleware}, signers::{LocalWallet, Signer as _}, types::{H256, U256}, @@ -159,6 +163,66 @@ pub mod tests { } } + pub struct AnvilMeta { + pub keys: Vec>, + pub endpoint: String, + pub chain_id: u64, + } + + /// Test harness that loads a local docker anvil node with deployed smart contracts. + pub async fn with_docker_smart_contracts(fun: Func) + where + Func: FnOnce( + AnvilMeta, + Provider, + SignerMiddleware, LocalWallet>, + SmartContracts, + ) -> Fut, + Fut: futures::Future, + { + // Spawn an anvil instance to get the keys and chain_id + let anvil = Anvil::new().port(8546u16).spawn(); + + let anvil_meta = AnvilMeta { + keys: anvil.keys().to_vec(), + chain_id: anvil.chain_id(), + endpoint: "http://localhost:8545".to_string(), + }; + + let keys = anvil.keys().to_vec(); + let contract_deployer: LocalWallet = keys[9].clone().into(); + let provider = Provider::::try_from(&anvil_meta.endpoint).unwrap(); + let client = SignerMiddleware::new( + provider.clone(), + contract_deployer.clone().with_chain_id(anvil_meta.chain_id), + ); + // 1. coinbase smart wallet + // deploy implementation for factory + let implementation = CoinbaseSmartWallet::deploy(Arc::new(client.clone()), ()) + .unwrap() + .gas_price(100) + .send() + .await + .unwrap(); + // deploy factory + let factory = + CoinbaseSmartWalletFactory::deploy(Arc::new(client.clone()), implementation.address()) + .unwrap() + .gas_price(100) + .send() + .await + .unwrap(); + + let smart_contracts = SmartContracts::new(factory); + fun( + anvil_meta, + provider.clone(), + client.clone(), + smart_contracts, + ) + .await + } + /// Test harness that loads a local anvil node with deployed smart contracts. pub async fn with_smart_contracts(fun: Func) where diff --git a/xmtp_id/src/scw_verifier/mod.rs b/xmtp_id/src/scw_verifier/mod.rs index 9cddbb307..863705b35 100644 --- a/xmtp_id/src/scw_verifier/mod.rs +++ b/xmtp_id/src/scw_verifier/mod.rs @@ -54,6 +54,7 @@ pub trait SmartContractSignatureVerifier: Send + Sync + DynClone + 'static { pub struct ValidationResponse { pub is_valid: bool, pub block_number: Option, + pub error: Option, } dyn_clone::clone_trait_object!(SmartContractSignatureVerifier); @@ -128,6 +129,16 @@ impl MultiSmartContractSignatureVerifier { info!("No upgraded chain url for chain {id}, using default."); }; }); + + #[cfg(feature = "test-utils")] + if let Ok(url) = env::var("ANVIL_URL") { + info!("Adding anvil to the verifiers: {url}"); + self.verifiers.insert( + "eip155:31337".to_string(), + Box::new(RpcSmartContractWalletVerifier::new(url)), + ); + } + self } diff --git a/xmtp_id/src/scw_verifier/remote_signature_verifier.rs b/xmtp_id/src/scw_verifier/remote_signature_verifier.rs index d1902222c..d65fa6137 100644 --- a/xmtp_id/src/scw_verifier/remote_signature_verifier.rs +++ b/xmtp_id/src/scw_verifier/remote_signature_verifier.rs @@ -52,6 +52,10 @@ impl SmartContractSignatureVerifier for RemoteSignatureVerifier { let VerifySmartContractWalletSignaturesResponse { responses } = result.into_inner(); - Ok((&responses[0]).into()) + Ok(responses + .into_iter() + .next() + .expect("Api given one request will return one response") + .into()) } } diff --git a/xmtp_mls/src/builder.rs b/xmtp_mls/src/builder.rs index cf69b91bf..0f2596732 100644 --- a/xmtp_mls/src/builder.rs +++ b/xmtp_mls/src/builder.rs @@ -692,92 +692,89 @@ mod tests { abi::Token, signers::{LocalWallet, Signer as _}, types::{Bytes, H256, U256}, + utils::hash_message, }; use std::sync::Arc; - use xmtp_id::associations::AccountId; - use xmtp_id::is_smart_contract; - use xmtp_id::scw_verifier::tests::{with_smart_contracts, CoinbaseSmartWallet}; - use xmtp_id::scw_verifier::{ - MultiSmartContractSignatureVerifier, SmartContractSignatureVerifier, + use xmtp_id::associations::{ + unverified::NewUnverifiedSmartContractWalletSignature, AccountId, }; - - with_smart_contracts(|anvil, _provider, client, smart_contracts| async move { - let key = anvil.keys()[0].clone(); - let wallet: LocalWallet = key.clone().into(); - - let owners = vec![Bytes::from(H256::from(wallet.address()).0.to_vec())]; - - let scw_factory = smart_contracts.coinbase_smart_wallet_factory(); - let nonce = U256::from(0); - - let scw_addr = scw_factory - .get_address(owners.clone(), nonce) - .await - .unwrap(); - - let contract_call = scw_factory.create_account(owners.clone(), nonce); - - contract_call.send().await.unwrap().await.unwrap(); - - assert!(is_smart_contract(scw_addr, anvil.endpoint(), None) - .await - .unwrap()); - - let identity_strategy = IdentityStrategy::CreateIfNotFound( - generate_inbox_id(&wallet.address().to_string(), &0), - wallet.address().to_string(), - 0, - None, - ); - let store = EncryptedMessageStore::new( - StorageOption::Persistent(tmp_path()), - EncryptedMessageStore::generate_enc_key(), - ) - .unwrap(); - let api_client: Client = ClientBuilder::new(identity_strategy) - .store(store) - .local_client() - .await - .build() - .await - .unwrap(); - - let hash = H256::random().into(); - let smart_wallet = CoinbaseSmartWallet::new( - scw_addr, - Arc::new(client.with_signer(wallet.clone().with_chain_id(anvil.chain_id()))), - ); - let replay_safe_hash = smart_wallet.replay_safe_hash(hash).call().await.unwrap(); - let account_id = AccountId::new_evm(anvil.chain_id(), format!("{scw_addr:?}")); - - let signature: Bytes = ethers::abi::encode(&[Token::Tuple(vec![ - Token::Uint(U256::from(0)), - Token::Bytes(wallet.sign_hash(replay_safe_hash.into()).unwrap().to_vec()), - ])]) - .into(); - - let valid_response = api_client - .smart_contract_signature_verifier() - .is_valid_signature(account_id.clone(), hash, signature.clone(), None) - .await - .unwrap(); - - // The mls validation service can't connect to our anvil instance, so it'll return false - // This is to make sure the communication at least works. - assert!(!valid_response.is_valid); - assert_eq!(valid_response.block_number, None); - - // So let's immitate more or less what the mls validation is doing locally, and validate there. - let mut multi_verifier = MultiSmartContractSignatureVerifier::default(); - multi_verifier.add_verifier(account_id.get_chain_id().to_string(), anvil.endpoint()); - let response = multi_verifier - .is_valid_signature(account_id, hash, signature, None) - .await + use xmtp_id::scw_verifier::tests::{with_docker_smart_contracts, CoinbaseSmartWallet}; + + with_docker_smart_contracts( + |anvil_meta, _provider, client, smart_contracts| async move { + let wallet: LocalWallet = anvil_meta.keys[0].clone().into(); + + let owners = vec![Bytes::from(H256::from(wallet.address()).0.to_vec())]; + + let scw_factory = smart_contracts.coinbase_smart_wallet_factory(); + let nonce = U256::from(0); + + let scw_addr = scw_factory + .get_address(owners.clone(), nonce) + .await + .unwrap(); + + let contract_call = scw_factory.create_account(owners.clone(), nonce); + + contract_call.send().await.unwrap().await.unwrap(); + let account_id = AccountId::new_evm(anvil_meta.chain_id, format!("{scw_addr:?}")); + let account_id_string: String = account_id.clone().into(); + + let identity_strategy = IdentityStrategy::CreateIfNotFound( + generate_inbox_id(&account_id_string, &0), + account_id_string, + 0, + None, + ); + let store = EncryptedMessageStore::new( + StorageOption::Persistent(tmp_path()), + EncryptedMessageStore::generate_enc_key(), + ) .unwrap(); + let xmtp_client: Client = ClientBuilder::new(identity_strategy) + .store(store) + .local_client() + .await + .build() + .await + .unwrap(); + + let smart_wallet = CoinbaseSmartWallet::new( + scw_addr, + Arc::new(client.with_signer(wallet.clone().with_chain_id(anvil_meta.chain_id))), + ); + let mut signature_request = xmtp_client.context.signature_request().unwrap(); + let signature_text = signature_request.signature_text(); + let hash_to_sign = hash_message(signature_text); + let replay_safe_hash = smart_wallet + .replay_safe_hash(hash_to_sign.into()) + .call() + .await + .unwrap(); + let signature_bytes: Bytes = ethers::abi::encode(&[Token::Tuple(vec![ + Token::Uint(U256::from(0)), + Token::Bytes(wallet.sign_hash(replay_safe_hash.into()).unwrap().to_vec()), + ])]) + .into(); + + signature_request + .add_new_unverified_smart_contract_signature( + NewUnverifiedSmartContractWalletSignature::new( + signature_bytes.to_vec(), + account_id.clone(), + None, + ), + xmtp_client.context.scw_verifier.as_ref(), + ) + .await + .unwrap(); - assert!(response.is_valid); - assert!(response.block_number.is_some()); - }) + xmtp_client + .register_identity(signature_request) + .await + .unwrap(); + }, + ) .await; } } diff --git a/xmtp_mls/src/storage/encrypted_store/group.rs b/xmtp_mls/src/storage/encrypted_store/group.rs index 4f9bc7335..70083ea3e 100644 --- a/xmtp_mls/src/storage/encrypted_store/group.rs +++ b/xmtp_mls/src/storage/encrypted_store/group.rs @@ -199,17 +199,17 @@ impl DbConnection { pub fn find_dm_group( &self, - dm_target_inbox_id: &str, + target_inbox_id: &str, ) -> Result, StorageError> { let query = dsl::groups .order(dsl::created_at_ns.asc()) - .filter(dsl::dm_inbox_id.eq(Some(&dm_target_inbox_id))); + .filter(dsl::dm_inbox_id.eq(Some(target_inbox_id))); let groups: Vec = self.raw_query(|conn| query.load(conn))?; if groups.len() > 1 { tracing::info!( "More than one group found for dm_inbox_id {}", - dm_target_inbox_id + target_inbox_id ); }