Skip to content

Commit

Permalink
Full stack SCW tests with external Anvil instance (#1141)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
codabrink and neekolas authored Oct 16, 2024
1 parent f4d22fb commit 6937f23
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 94 deletions.
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

0 comments on commit 6937f23

Please sign in to comment.