diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index b3b678a..4b75385 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -20,4 +20,10 @@ snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v sort-module-level-items = true [[target.starknet-contract]] +# Enable Sierra codegen. +sierra = true + +# Enable CASM codegen. casm = true +# Emit Python-powered hints in order to run compiled CASM class with legacy Cairo VM. +casm-add-pythonic-hints = true diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 320a914..a0a052d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1767,6 +1767,8 @@ dependencies = [ "cainome", "ethers", "eyre", + "futures", + "k256", "rstest", "serde", "serde_json", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 96f31e5..08eb6f4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -42,12 +42,16 @@ cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.9", fea ethers = "2.0.7" serde = { version = "1.0.162", default-features = false, features = ["derive"] } serde_json = "1.0.96" +futures = "0.3.30" # utilities thiserror = { version = "1.0.37" } anyhow = { version = "1.0.71", features = ["backtrace"] } eyre = { version = "0.6.8" } +# crypto +k256 = { version = "0.13.1", default-features = false, features = ["ecdsa"] } + rstest = "0.18.2" [build-dependencies] diff --git a/rust/tests/contracts/strk/deploy.rs b/rust/tests/contracts/strk/deploy.rs index 344c58f..da510b5 100644 --- a/rust/tests/contracts/strk/deploy.rs +++ b/rust/tests/contracts/strk/deploy.rs @@ -1,13 +1,19 @@ -use super::{hook::Hook, types::Codes, StarknetAccount}; +use super::{ + hook::Hook, + ism::Ism, + types::{Codes, CoreDeployments}, + StarknetAccount, +}; pub fn deploy_core( - owner: StarknetAccount, - deployer: StarknetAccount, + owner: &StarknetAccount, + deployer: &StarknetAccount, codes: &Codes, domain: u32, - default_ism: String, + default_ism: Ism, default_hook: Hook, required_hook: Hook, -) { +) -> eyre::Result { // deploy mailbox + Ok(CoreDeployments::default()) } diff --git a/rust/tests/contracts/strk/hook.rs b/rust/tests/contracts/strk/hook.rs index d16c32f..14f7965 100644 --- a/rust/tests/contracts/strk/hook.rs +++ b/rust/tests/contracts/strk/hook.rs @@ -11,6 +11,7 @@ pub enum Hook { }, Merkle {}, + Igp(u32), Pausable {}, diff --git a/rust/tests/contracts/strk/ism.rs b/rust/tests/contracts/strk/ism.rs new file mode 100644 index 0000000..1f458b9 --- /dev/null +++ b/rust/tests/contracts/strk/ism.rs @@ -0,0 +1,173 @@ +use cainome::cairo_serde::ContractAddress; +use futures::{stream::FuturesUnordered, StreamExt}; +use starknet::{accounts::Account, core::types::FieldElement, macros::felt}; + +use super::bind::multisig_ism::messageid_multisig_ism; +use super::bind::routing::domain_routing_ism; +use crate::validator::{self, TestValidators}; + +use super::{deploy_contract, types::Codes, StarknetAccount}; + +#[derive(Clone)] +pub enum Ism { + Routing(Vec<(u32, Self)>), + + Multisig { + validators: validator::TestValidators, + }, + + Aggregate { + isms: Vec, + threshold: u8, + }, + + #[allow(dead_code)] + Mock, +} + +impl Ism { + pub fn routing(isms: Vec<(u32, Self)>) -> Self { + Self::Routing(isms) + } + + pub fn multisig(validators: validator::TestValidators) -> Self { + Self::Multisig { validators } + } +} + +impl Ism { + async fn deploy_mock(codes: &Codes, deployer: &StarknetAccount) -> eyre::Result { + let res = deploy_contract(codes.test_mock_ism, vec![], felt!("0"), deployer).await; + Ok(res.0) + } + + async fn deploy_multisig( + codes: &Codes, + set: validator::TestValidators, + owner: &StarknetAccount, + deployer: &StarknetAccount, + ) -> eyre::Result { + let res = deploy_contract( + codes.ism_multisig, + vec![owner.address()], + felt!("0"), + deployer, + ) + .await; + + let contract = messageid_multisig_ism::new(res.0, owner); + contract + .set_validators( + &set.validators + .iter() + .map(|v| v.eth_addr()) + .collect::>(), + ) + .send() + .await?; + + Ok(res.0) + } + + async fn deploy_routing( + codes: &Codes, + isms: Vec<(u32, Self)>, + owner: &StarknetAccount, + deployer: &StarknetAccount, + ) -> eyre::Result { + let res = deploy_contract( + codes.ism_routing, + vec![owner.address()], + felt!("0"), + deployer, + ) + .await; + + let futures = FuturesUnordered::new(); + + for i in isms.iter() { + let future = async move { + ::clone(&i.1) + .deploy(codes, owner, deployer) + .await + }; + futures.push(future); + } + + let results = futures.collect::>().await; + + let modules: Vec<_> = results + .iter() + .map(|a| ContractAddress(*a.as_ref().unwrap())) + .collect(); + + let contract = domain_routing_ism::new(res.0, owner); + contract + .initialize(&isms.iter().map(|i| i.0).collect::>(), &modules) + .send() + .await?; + + Ok(res.0) + } + + async fn deploy_aggregate( + codes: &Codes, + isms: Vec, + threshold: u8, + owner: &StarknetAccount, + deployer: &StarknetAccount, + ) -> eyre::Result { + let futures = FuturesUnordered::new(); + + for i in isms.iter() { + let future = async move { + ::clone(&i) + .deploy(codes, owner, deployer) + .await + }; + futures.push(future); + } + + let results = futures.collect::>().await; + + let ism_addrs: Vec<_> = results.iter().map(|a| *a.as_ref().unwrap()).collect(); + + let res = deploy_contract(codes.ism_aggregate, ism_addrs, felt!("0"), deployer).await; + + Ok(res.0) + } + + pub async fn deploy( + self, + codes: &Codes, + owner: &StarknetAccount, + deployer: &StarknetAccount, + ) -> eyre::Result { + match self { + Self::Mock => Self::deploy_mock(codes, deployer).await, + Self::Multisig { validators: set } => { + Self::deploy_multisig(codes, set, owner, deployer).await + } + Self::Aggregate { isms, threshold } => { + Self::deploy_aggregate(codes, isms, threshold, owner, deployer).await + } + Self::Routing(isms) => Self::deploy_routing(codes, isms, owner, deployer).await, + } + } +} + +pub fn prepare_routing_ism(info: Vec<(u32, TestValidators)>) -> Ism { + let mut isms = vec![]; + + for (domain, set) in info { + isms.push(( + domain, + Ism::Aggregate { + isms: vec![Ism::multisig(set)], + threshold: 1, + }, + )); + } + + Ism::routing(isms) +} diff --git a/rust/tests/contracts/strk/mod.rs b/rust/tests/contracts/strk/mod.rs index b4763a1..8b6c8d8 100644 --- a/rust/tests/contracts/strk/mod.rs +++ b/rust/tests/contracts/strk/mod.rs @@ -1,6 +1,7 @@ mod bind; mod deploy; mod hook; +mod ism; mod setup; mod types; mod utils; diff --git a/rust/tests/contracts/strk/setup.rs b/rust/tests/contracts/strk/setup.rs index 13930f6..eb38106 100644 --- a/rust/tests/contracts/strk/setup.rs +++ b/rust/tests/contracts/strk/setup.rs @@ -7,6 +7,7 @@ use crate::validator::TestValidators; use super::{ deploy_core, get_dev_account, hook::Hook, + ism::prepare_routing_ism, types::{Codes, CoreDeployments}, StarknetAccount, }; diff --git a/rust/tests/contracts/strk/types.rs b/rust/tests/contracts/strk/types.rs index 9c141b8..d565c61 100644 --- a/rust/tests/contracts/strk/types.rs +++ b/rust/tests/contracts/strk/types.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; -use starknet::{accounts::SingleOwnerAccount, providers::AnyProvider, signers::LocalWallet}; +use starknet::{ + accounts::SingleOwnerAccount, core::types::FieldElement, providers::AnyProvider, + signers::LocalWallet, +}; pub type StarknetAccount = SingleOwnerAccount; @@ -15,32 +18,32 @@ impl FromIterator<(String, u64)> for CodesMap { #[derive(serde::Serialize, serde::Deserialize)] pub struct Codes { - pub mailbox: String, + pub mailbox: FieldElement, #[serde(rename = "validator_announce")] - pub va: String, + pub va: FieldElement, - pub hook_aggregate: String, - pub hook_merkle: String, - pub hook_pausable: String, - pub hook_routing: String, - pub hook_routing_custom: String, - pub hook_routing_fallback: String, + pub hook_aggregate: FieldElement, + pub hook_merkle: FieldElement, + pub hook_pausable: FieldElement, + pub hook_routing: FieldElement, + pub hook_routing_custom: FieldElement, + pub hook_routing_fallback: FieldElement, - pub igp: String, - pub igp_oracle: String, + pub igp: FieldElement, + pub igp_oracle: FieldElement, - pub ism_aggregate: String, - pub ism_multisig: String, - pub ism_routing: String, + pub ism_aggregate: FieldElement, + pub ism_multisig: FieldElement, + pub ism_routing: FieldElement, - pub test_mock_hook: String, - pub test_mock_ism: String, - pub test_mock_msg_receiver: String, + pub test_mock_hook: FieldElement, + pub test_mock_ism: FieldElement, + pub test_mock_msg_receiver: FieldElement, - pub warp_strk20: String, - pub warp_native: String, + pub warp_strk20: FieldElement, + pub warp_native: FieldElement, - pub strk20_base: String, + pub strk20_base: FieldElement, } impl TryFrom for Codes { @@ -55,13 +58,13 @@ impl TryFrom for Codes { } } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Default, serde::Serialize, serde::Deserialize)] pub struct CoreDeployments { - pub mailbox: String, - pub default_ism: String, - pub default_hook: String, - pub required_hook: String, - pub msg_receiver: String, + pub mailbox: FieldElement, + pub default_ism: FieldElement, + pub default_hook: FieldElement, + pub required_hook: FieldElement, + pub msg_receiver: FieldElement, } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/rust/tests/contracts/strk/utils.rs b/rust/tests/contracts/strk/utils.rs index 03b2c57..4ce59a8 100644 --- a/rust/tests/contracts/strk/utils.rs +++ b/rust/tests/contracts/strk/utils.rs @@ -1,8 +1,9 @@ use starknet::{ accounts::SingleOwnerAccount, - core::types::FieldElement, + contract::ContractFactory, + core::types::{BlockId, BlockTag, FieldElement, InvokeTransactionResult}, providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient, Url}, - signers::LocalWallet, + signers::{LocalWallet, SigningKey}, }; use super::StarknetAccount; @@ -28,12 +29,27 @@ const KATANA_CHAIN_ID: u32 = 82743958523457; /// Returns a pre-funded account for a local katana chain. pub fn get_dev_account(index: u32) -> StarknetAccount { - let (address, private_key) = KATANA_PREFUNDED_ACCOUNTS + let (address, private_key) = *KATANA_PREFUNDED_ACCOUNTS .get(index as usize) .expect("Invalid index"); - let signer = LocalWallet::from_signing_key(private_key); - build_single_owner_account(KATANA_RPC_URL, signer, address, false, KATANA_CHAIN_ID) + let signer = LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + FieldElement::from_hex_be(&private_key).unwrap(), + )); + + let mut account = build_single_owner_account( + &Url::parse(KATANA_RPC_URL).expect("Invalid rpc url"), + signer, + &FieldElement::from_hex_be(address).unwrap(), + false, + KATANA_CHAIN_ID, + ); + + // `SingleOwnerAccount` defaults to checking nonce and estimating fees against the latest + // block. Optionally change the target block to pending with the following line: + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + account } /// Creates a single owner account for a given signer and account address. @@ -65,7 +81,25 @@ pub fn build_single_owner_account( rpc_client, signer, *account_address, - chain_id, + chain_id.into(), execution_encoding, ) } + +/// Deploys a contract with the given class hash, constructor calldata, and salt. +/// Returns the deployed address and the transaction result. +pub async fn deploy_contract( + class_hash: FieldElement, + constructor_calldata: Vec, + salt: FieldElement, + deployer: &StarknetAccount, +) -> (FieldElement, InvokeTransactionResult) { + let contract_factory = ContractFactory::new(class_hash, deployer); + + let deployment = contract_factory.deploy(constructor_calldata, salt, false); + + ( + deployment.deployed_address(), + deployment.send().await.expect("Failed to deploy contract"), + ) +} diff --git a/rust/tests/validator.rs b/rust/tests/validator.rs index 5368c0e..cb17dd3 100644 --- a/rust/tests/validator.rs +++ b/rust/tests/validator.rs @@ -1,9 +1,11 @@ +use cainome::cairo_serde::EthAddress; use ethers::types::{Address, H160}; use ethers::utils::hex::FromHex; use k256::{ ecdsa::{RecoveryId, SigningKey, VerifyingKey}, elliptic_curve::rand_core::OsRng, }; +use starknet::core::types::FieldElement; #[derive(Clone)] pub struct TestValidator { @@ -29,34 +31,11 @@ impl TestValidator { Self { priv_key, pub_key } } - fn pub_key_to_binary(&self) -> HexBinary { - self.pub_key - .to_encoded_point(true) - .as_bytes() - .to_vec() - .into() - } - - pub fn addr(&self, hrp: &str) -> String { - bech32_encode( - hrp, - pub_to_addr(self.pub_key_to_binary()).unwrap().as_slice(), + pub fn eth_addr(&self) -> EthAddress { + EthAddress( + FieldElement::from_byte_slice_be(&self.pub_key.to_encoded_point(false).as_bytes()) + .unwrap(), ) - .unwrap() - .into() - } - - pub fn to_val(&self, domain: u32) -> ValidatorSet { - ValidatorSet { - domain, - validator: eth_addr(self.pub_key.to_encoded_point(false).as_bytes().into()).unwrap(), - } - } - - pub fn sign(&self, digest: [u8; 32]) -> (Binary, RecoveryId) { - let (sign, recov_id) = self.priv_key.sign_prehash_recoverable(&digest).unwrap(); - - (Binary(sign.to_bytes().to_vec()), recov_id) } } @@ -98,107 +77,4 @@ impl TestValidators { threshold, } } - - pub fn to_set(&self) -> Vec { - self.validators - .iter() - .map(|v| v.to_val(self.domain)) - .collect::>() - } - - pub fn sign(&self, num: u8, digest: [u8; 32]) -> Vec { - let num = num as usize; - assert!(self.validators.len() >= num); - - let signatures = &self.validators[0..num] - .iter() - .map(|v| { - let (mut signature, recov_id) = v.sign(digest); - signature.0.extend(vec![recov_id.to_byte() + 27]); - signature.into() - }) - .collect::>(); - - signatures.clone() - } - - pub fn make_metadata( - &self, - origin_merkle_tree: Address, - merkle_root: [u8; 32], - merkle_index: u32, - message_id: [u8; 32], - is_passed: bool, - ) -> eyre::Result { - let mut addr = [0u8; 32]; - addr[32 - origin_merkle_tree.0.len()..].copy_from_slice(&origin_merkle_tree.0); - - let multisig_hash = hpl_ism_multisig::multisig_hash( - hpl_ism_multisig::domain_hash(self.domain, addr.to_vec().into())?.to_vec(), - merkle_root.to_vec(), - merkle_index, - message_id.to_vec(), - )?; - - let hashed_message = eth_hash(multisig_hash)?; - - let signatures = if is_passed { - self.sign(self.threshold, hashed_message.as_slice().try_into()?) - } else { - self.sign(self.threshold - 1, hashed_message.as_slice().try_into()?) - }; - - Ok(MessageIdMultisigIsmMetadata { - origin_merkle_tree: origin_merkle_tree.0.to_vec().into(), - merkle_root: merkle_root.to_vec().into(), - merkle_index: merkle_index.to_be_bytes().to_vec().into(), - signatures, - }) - } -} - -#[test] -fn test_validator() { - let owner = addr("owner"); - let validators = TestValidators::new(2, 5, 3); - - let mut deps = mock_dependencies(); - - hpl_ownable::initialize(deps.as_mut().storage, &owner).unwrap(); - - hpl_ism_multisig::contract::execute( - deps.as_mut(), - mock_env(), - mock_info(owner.as_str(), &[]), - hpl_interface::ism::multisig::ExecuteMsg::SetValidators { - domain: validators.domain, - threshold: validators.threshold, - validators: validators - .to_set() - .iter() - .map(|v| v.validator.clone()) - .collect(), - }, - ) - .unwrap(); - - let mut message: Message = gen_bz(100).into(); - message.origin_domain = validators.domain; - - let message_id = message.id(); - - let metadata = validators - .make_metadata( - H160::from_slice(gen_bz(20).as_slice()), - gen_bz(32).as_slice().try_into().unwrap(), - 0, - message_id.as_slice().try_into().unwrap(), - true, - ) - .unwrap(); - - let res = - hpl_ism_multisig::query::verify_message(deps.as_ref(), metadata.into(), message.into()) - .unwrap(); - assert!(res.verified); }