Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full stack SCW tests with external Anvil instance #1141

Merged
merged 15 commits into from
Oct 16, 2024
9 changes: 9 additions & 0 deletions dev/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ services:
build:
context: ../..
dockerfile: ./dev/validation_service/local.Dockerfile
environment:
FOUNDRY_URL: "http://foundry:8545"

foundry:
build:
dockerfile: ./foundry.Dockerfile
platform: linux/amd64
ports:
- 8545:8545

db:
image: postgres:13
Expand Down
6 changes: 6 additions & 0 deletions dev/docker/foundry.Dockerfile
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion dev/docker/up
Original file line number Diff line number Diff line change
Expand 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
"${script_dir}"/compose up -d --build
5 changes: 3 additions & 2 deletions xmtp_id/src/associations/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,12 @@ impl From<UnverifiedIdentityUpdate> for Vec<u8> {
}
}

impl From<&SmartContractWalletValidationResponseProto> for ValidationResponse {
fn from(value: &SmartContractWalletValidationResponseProto) -> Self {
impl From<SmartContractWalletValidationResponseProto> for ValidationResponse {
fn from(value: SmartContractWalletValidationResponseProto) -> Self {
Self {
is_valid: value.is_valid,
block_number: value.block_number,
error: value.error,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions xmtp_id/src/associations/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl SmartContractSignatureVerifier for MockSmartContractSignatureVerifier {
Ok(ValidationResponse {
is_valid: self.is_valid_signature,
block_number: Some(1),
error: None,
})
}
}
Expand Down
66 changes: 65 additions & 1 deletion xmtp_id/src/scw_verifier/chain_rpc_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
}
Expand All @@ -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},
Expand Down Expand Up @@ -159,6 +163,66 @@ pub mod tests {
}
}

pub struct AnvilMeta {
pub keys: Vec<SecretKey<Secp256k1>>,
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<Func, Fut>(fun: Func)
where
Func: FnOnce(
AnvilMeta,
Provider<Http>,
SignerMiddleware<Provider<Http>, LocalWallet>,
SmartContracts,
) -> Fut,
Fut: futures::Future<Output = ()>,
{
// 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::<Http>::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<Func, Fut>(fun: Func)
where
Expand Down
10 changes: 10 additions & 0 deletions xmtp_id/src/scw_verifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub trait SmartContractSignatureVerifier: Send + Sync + DynClone + 'static {
pub struct ValidationResponse {
pub is_valid: bool,
pub block_number: Option<u64>,
pub error: Option<String>,
}

dyn_clone::clone_trait_object!(SmartContractSignatureVerifier);
Expand Down Expand Up @@ -128,6 +129,15 @@ impl MultiSmartContractSignatureVerifier {
info!("No upgraded chain url for chain {id}, using default.");
};
});

if let Ok(url) = env::var("FOUNDRY_URL") {
codabrink marked this conversation as resolved.
Show resolved Hide resolved
info!("Adding foundry to the verifiers: {url}");
self.verifiers.insert(
"eip155:31337".to_string(),
Box::new(RpcSmartContractWalletVerifier::new(url)),
);
}

self
}

Expand Down
6 changes: 5 additions & 1 deletion xmtp_id/src/scw_verifier/remote_signature_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
130 changes: 55 additions & 75 deletions xmtp_mls/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,89 +695,69 @@ mod tests {
};
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::scw_verifier::tests::{with_docker_smart_contracts, CoinbaseSmartWallet};

with_smart_contracts(|anvil, _provider, client, smart_contracts| async move {
let key = anvil.keys()[0].clone();
let wallet: LocalWallet = key.clone().into();
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 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_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 scw_addr = scw_factory
.get_address(owners.clone(), nonce)
.await
.unwrap();

let contract_call = scw_factory.create_account(owners.clone(), nonce);
let contract_call = scw_factory.create_account(owners.clone(), nonce);

contract_call.send().await.unwrap().await.unwrap();
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<TestClient> = ClientBuilder::new(identity_strategy)
.store(store)
.local_client()
.await
.build()
.await
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 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
.unwrap();

assert!(response.is_valid);
assert!(response.block_number.is_some());
})
let api_client: Client<TestClient> = ClientBuilder::new(identity_strategy)
.store(store)
.local_client()
.await
.build()
.await
.unwrap();

let hash = H256::random().into();
codabrink marked this conversation as resolved.
Show resolved Hide resolved
let smart_wallet = CoinbaseSmartWallet::new(
scw_addr,
Arc::new(client.with_signer(wallet.clone().with_chain_id(anvil_meta.chain_id))),
);
let replay_safe_hash = smart_wallet.replay_safe_hash(hash).call().await.unwrap();
let account_id = AccountId::new_evm(anvil_meta.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();

assert!(valid_response.is_valid);
assert!(valid_response.block_number.is_some());
},
)
.await;
}
}
6 changes: 3 additions & 3 deletions xmtp_mls/src/storage/encrypted_store/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,17 @@ impl DbConnection {

pub fn find_dm_group(
&self,
dm_target_inbox_id: &str,
target_inbox_id: &str,
) -> Result<Option<StoredGroup>, 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<StoredGroup> = 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
);
}

Expand Down