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
6 changes: 3 additions & 3 deletions dev/build_validation_service_local
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
docker build --platform=linux/amd64 -t xmtp/mls-validation-service:latest -f ./dev/validation_service/local.Dockerfile .
6 changes: 6 additions & 0 deletions dev/docker/anvil.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
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:
ANVIL_URL: "http://anvil:8545"

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

db:
image: postgres:13
Expand Down
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
4 changes: 2 additions & 2 deletions dev/validation_service/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
CMD ["mls-validation-service"]
3 changes: 3 additions & 0 deletions mls_validation_service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
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
4 changes: 4 additions & 0 deletions xmtp_id/src/associations/verified_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ impl VerifiedSignature {
signature_bytes.to_vec(),
))
} else {
tracing::error!(
"Smart contract wallet signature is invalid {:?}",
response.error
);
Err(SignatureError::Invalid)
}
}
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
11 changes: 11 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,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
}

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())
}
}
159 changes: 78 additions & 81 deletions xmtp_mls/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestClient> = 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<TestClient> = 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;
}
}
Loading