From d32ec0e1c7ec1ebd0ef1c0b9c02fa6fba3e21dd6 Mon Sep 17 00:00:00 2001 From: Jordy Romuald <87231934+JordyRo1@users.noreply.github.com> Date: Wed, 15 May 2024 14:52:21 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20ISM=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init: mailbox * feat: mailbox client + fix mailbox/message * fix: msg.value and view function * feat: router * feat: add nonce getter * fix + tests * feat:docs * Ism integration * feat: validator announce * feat: mock ism * fix: typo * fix: fmt * fix: remove test_multisig * fix: opp * fix: mailbox review * fix: comment multisig test * fix: fmt * corrections + tests * refactor * fix: fmt * fix: working dir * fix: ci working directory * fix: typo * fix: typo * fix: release dir --------- Co-authored-by: 0xevolve --- .github/workflows/release.yml | 15 +- .github/workflows/test.yml | 11 +- .gitignore => contracts/.gitignore | 0 Scarb.lock => contracts/Scarb.lock | 0 Scarb.toml => contracts/Scarb.toml | 0 .../src}/contracts/client/mailboxclient.cairo | 13 ++ .../src}/contracts/client/router.cairo | 0 .../multisig/merkleroot_multisig_ism.cairo | 94 ++++++++ .../multisig/messageid_multisig_ism.cairo | 176 ++++++++++++++ .../isms/multisig/validator_announce.cairo | 191 +++++++++++++++ .../isms/routing/domain_routing_ism.cairo | 151 ++++++++++++ .../src/contracts/libs/checkpoint_lib.cairo | 50 ++++ .../src}/contracts/libs/message.cairo | 0 .../multisig/message_id_ism_metadata.cairo | 39 ++++ .../src}/contracts/mailbox.cairo | 0 contracts/src/contracts/mocks/ism.cairo | 42 ++++ .../contracts/mocks/message_recipient.cairo | 0 {src => contracts/src}/interfaces.cairo | 88 ++++++- contracts/src/lib.cairo | 38 +++ contracts/src/tests/setup.cairo | 146 ++++++++++++ .../src}/tests/test_mailbox.cairo | 0 contracts/src/tests/test_multisig.cairo | 217 ++++++++++++++++++ {src => contracts/src}/utils/keccak256.cairo | 0 contracts/src/utils/store_arrays.cairo | 71 ++++++ src/lib.cairo | 23 -- src/tests/setup.cairo | 66 ------ 26 files changed, 1323 insertions(+), 108 deletions(-) rename .gitignore => contracts/.gitignore (100%) rename Scarb.lock => contracts/Scarb.lock (100%) rename Scarb.toml => contracts/Scarb.toml (100%) rename {src => contracts/src}/contracts/client/mailboxclient.cairo (92%) rename {src => contracts/src}/contracts/client/router.cairo (100%) create mode 100644 contracts/src/contracts/isms/multisig/merkleroot_multisig_ism.cairo create mode 100644 contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo create mode 100644 contracts/src/contracts/isms/multisig/validator_announce.cairo create mode 100644 contracts/src/contracts/isms/routing/domain_routing_ism.cairo create mode 100644 contracts/src/contracts/libs/checkpoint_lib.cairo rename {src => contracts/src}/contracts/libs/message.cairo (100%) create mode 100644 contracts/src/contracts/libs/multisig/message_id_ism_metadata.cairo rename {src => contracts/src}/contracts/mailbox.cairo (100%) create mode 100644 contracts/src/contracts/mocks/ism.cairo rename {src => contracts/src}/contracts/mocks/message_recipient.cairo (100%) rename {src => contracts/src}/interfaces.cairo (70%) create mode 100644 contracts/src/lib.cairo create mode 100644 contracts/src/tests/setup.cairo rename {src => contracts/src}/tests/test_mailbox.cairo (100%) create mode 100644 contracts/src/tests/test_multisig.cairo rename {src => contracts/src}/utils/keccak256.cairo (100%) create mode 100644 contracts/src/utils/store_arrays.cairo delete mode 100644 src/lib.cairo delete mode 100644 src/tests/setup.cairo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcc7e9e..2a14d4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,21 @@ jobs: pull-requests: write name: artifact runs-on: ubuntu-latest + env: + working-directory: ./contracts steps: - uses: actions/checkout@v4 - uses: software-mansion/setup-scarb@v1 - name: Build contracts + working-directory: ${{ env.working-directory}} run: scarb build - name: Archive contracts + working-directory: ${{ env.working-directory}} run: | mkdir -p filtered_artifacts - find ./target/dev -type f -name '*.contract_class.json' -exec cp {} filtered_artifacts/ \; + find ./contracts/target/dev -type f -name '*.contract_class.json' -exec cp {} filtered_artifacts/ \; - name: Generate checksums + working-directory: ${{ env.working-directory}} run: | cd filtered_artifacts for file in *; do @@ -29,6 +34,7 @@ jobs: md5sum "$file" > "$file.md5" done - name: Build artifact zip + working-directory: ${{ env.working-directory}} run: | cd filtered_artifacts zip -r ../hyperlane-starknet-${{ github.ref_name }}.zip . @@ -37,6 +43,7 @@ jobs: md5sum hyperlane-starknet-${{ github.ref_name }}.zip > hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5 - name: Find zip files + working-directory: ${{ env.working-directory}} run: | find ./filtered_artifacts -type f -name '*.zip' -exec echo "::set-output name=zip_files::{}" \; id: find_zip_files @@ -45,8 +52,8 @@ jobs: uses: softprops/action-gh-release@v1 with: files: | - hyperlane-starknet-${{ github.ref_name }}.zip - hyperlane-starknet-${{ github.ref_name }}.CHECKSUM - hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5 + ./contracts/hyperlane-starknet-${{ github.ref_name }}.zip + ./contracts/hyperlane-starknet-${{ github.ref_name }}.CHECKSUM + ./contracts/hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 356e114..8d503ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,10 +5,15 @@ on: jobs: check: runs-on: ubuntu-latest + env: + working-directory: ./contracts steps: - uses: actions/checkout@v3 - uses: software-mansion/setup-scarb@v1 - uses: foundry-rs/setup-snfoundry@v3 - - run: scarb fmt --check - - run: scarb build - - run: snforge test \ No newline at end of file + - working-directory: ${{ env.working-directory}} + run: scarb fmt --check + - working-directory: ${{ env.working-directory}} + run: scarb build + - working-directory: ${{ env.working-directory}} + run: snforge test \ No newline at end of file diff --git a/.gitignore b/contracts/.gitignore similarity index 100% rename from .gitignore rename to contracts/.gitignore diff --git a/Scarb.lock b/contracts/Scarb.lock similarity index 100% rename from Scarb.lock rename to contracts/Scarb.lock diff --git a/Scarb.toml b/contracts/Scarb.toml similarity index 100% rename from Scarb.toml rename to contracts/Scarb.toml diff --git a/src/contracts/client/mailboxclient.cairo b/contracts/src/contracts/client/mailboxclient.cairo similarity index 92% rename from src/contracts/client/mailboxclient.cairo rename to contracts/src/contracts/client/mailboxclient.cairo index 1d4f671..d6a7e58 100644 --- a/src/contracts/client/mailboxclient.cairo +++ b/contracts/src/contracts/client/mailboxclient.cairo @@ -68,6 +68,19 @@ mod mailboxclient { self.interchain_security_module.write(_module); } + fn get_local_domain(self: @ContractState) -> u32 { + self.local_domain.read() + } + + fn get_hook(self: @ContractState) -> ContractAddress { + self.hook.read() + } + + fn get_interchain_security_module(self: @ContractState) -> ContractAddress { + self.interchain_security_module.read() + } + + fn _MailboxClient_initialize( ref self: ContractState, _hook: ContractAddress, diff --git a/src/contracts/client/router.cairo b/contracts/src/contracts/client/router.cairo similarity index 100% rename from src/contracts/client/router.cairo rename to contracts/src/contracts/client/router.cairo diff --git a/contracts/src/contracts/isms/multisig/merkleroot_multisig_ism.cairo b/contracts/src/contracts/isms/multisig/merkleroot_multisig_ism.cairo new file mode 100644 index 0000000..974c7bc --- /dev/null +++ b/contracts/src/contracts/isms/multisig/merkleroot_multisig_ism.cairo @@ -0,0 +1,94 @@ +#[starknet::contract] +pub mod merkleroot_multisig_ism { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + + + use core::ecdsa::check_ecdsa_signature; + use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait}; + use hyperlane_starknet::interfaces::{ + IMultisigIsm, IMultisigIsmDispatcher, IMultisigIsmDispatcherTrait, ModuleType, + IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, + }; + + use starknet::ContractAddress; + #[storage] + struct Storage {} + + mod Errors { + pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present'; + pub const VERIFICATION_FAILED_THRESHOLD_NOT_REACHED: felt252 = 'Verify failed, < threshold'; + } + + #[abi(embed_v0)] + impl IMerklerootMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MERKLE_ROOT_MULTISIG(starknet::get_contract_address()) + } + + fn verify( + self: @ContractState, + _metadata: Bytes, + _message: Message, + _validator_configuration: ContractAddress + ) -> bool { + let digest = digest(_metadata.clone(), _message.clone()); + let validator_configuration = IMultisigIsmDispatcher { + contract_address: _validator_configuration + }; + let (validators, threshold) = validator_configuration + .validators_and_threshold(_message); + assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE); + let validator_count = validators.len(); + let mut unmatched_signatures = 0; + let mut matched_signatures = 0; + let mut i = 0; + + // for each couple (sig_s, sig_r) extracted from the metadata + loop { + if (i == threshold) { + break (); + } + let (signature_r, signature_s) = get_signature_at(_metadata.clone(), i); + + // we loop on the validators list public key in order to find a match + let mut cur_idx = 0; + let is_signer_in_list = loop { + if (cur_idx == validators.len()) { + break false; + } + let signer = *validators.at(cur_idx); + if check_ecdsa_signature( + digest, signer.try_into().unwrap(), signature_r, signature_s + ) { + // we found a match + break true; + } + cur_idx += 1; + }; + if (!is_signer_in_list) { + unmatched_signatures += 1; + } else { + matched_signatures += 1; + } + assert( + unmatched_signatures < validator_count - threshold, + Errors::VERIFICATION_FAILED_THRESHOLD_NOT_REACHED + ); + i += 1; + }; + assert( + matched_signatures >= threshold, Errors::VERIFICATION_FAILED_THRESHOLD_NOT_REACHED + ); + true + } + } + + fn digest(_metadata: Bytes, _message: Message) -> felt252 { + return 0; + } + + fn get_signature_at(_metadata: Bytes, index: u32) -> (felt252, felt252) { + (0, 0) + } +} diff --git a/contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo b/contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo new file mode 100644 index 0000000..4cd2b3a --- /dev/null +++ b/contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo @@ -0,0 +1,176 @@ +#[starknet::contract] +pub mod messageid_multisig_ism { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use core::ecdsa::check_ecdsa_signature; + use hyperlane_starknet::contracts::libs::checkpoint_lib::checkpoint_lib::CheckpointLib; + use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait}; + use hyperlane_starknet::contracts::libs::multisig::message_id_ism_metadata::message_id_ism_metadata::MessageIdIsmMetadata; + use hyperlane_starknet::interfaces::{ + ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, + }; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::ContractAddress; + use starknet::EthAddress; + use starknet::eth_signature::is_eth_signature_valid; + use starknet::secp256_trait::{Signature, signature_from_vrs}; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + #[storage] + struct Storage { + validators: LegacyMap, + threshold: u32, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + mod Errors { + pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present'; + pub const NO_MATCH_FOR_SIGNATURE: felt252 = 'No match for given signature'; + pub const EMPTY_METADATA: felt252 = 'Empty metadata'; + pub const VALIDATOR_ADDRESS_CANNOT_BE_NULL: felt252 = 'Validator address cannot be 0'; + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + + #[constructor] + fn constructor(ref self: ContractState, _owner: ContractAddress) { + self.ownable.initializer(_owner); + } + + #[abi(embed_v0)] + impl IMessageidMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MESSAGE_ID_MULTISIG(starknet::get_contract_address()) + } + + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + assert(_metadata.clone().data().len() > 0, Errors::EMPTY_METADATA); + let digest = digest(_metadata.clone(), _message.clone()); + let (validators, threshold) = self.validators_and_threshold(_message); + assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE); + let mut matched_signatures = 0; + let mut i = 0; + + // for each couple (sig_s, sig_r) extracted from the metadata + loop { + if (i == threshold) { + break (); + } + let signature = get_signature_at(_metadata.clone(), i); + // we loop on the validators list public key in order to find a match + let mut cur_idx = 0; + let is_signer_in_list = loop { + if (cur_idx == validators.len()) { + break false; + } + let signer = *validators.at(cur_idx); + if bool_is_eth_signature_valid(digest, signature, signer) { + // we found a match + break true; + } + cur_idx += 1; + }; + assert(is_signer_in_list, Errors::NO_MATCH_FOR_SIGNATURE); + i += 1; + }; + true + } + fn get_validators(self: @ContractState) -> Span { + build_validators_span(self) + } + + fn get_threshold(self: @ContractState) -> u32 { + self.threshold.read() + } + + fn set_validators(ref self: ContractState, _validators: Span) { + self.ownable.assert_only_owner(); + let mut cur_idx = 0; + + loop { + if (cur_idx == _validators.len()) { + break (); + } + let validator = *_validators.at(cur_idx); + assert( + validator != 0.try_into().unwrap(), Errors::VALIDATOR_ADDRESS_CANNOT_BE_NULL + ); + self.validators.write(cur_idx.into(), validator); + cur_idx += 1; + } + } + + fn set_threshold(ref self: ContractState, _threshold: u32) { + self.ownable.assert_only_owner(); + self.threshold.write(_threshold); + } + + fn validators_and_threshold( + self: @ContractState, _message: Message + ) -> (Span, u32) { + // USER CONTRACT DEFINITION HERE + // USER CAN SPECIFY VALIDATORS SELECTION CONDITIONS + let threshold = self.threshold.read(); + (build_validators_span(self), threshold) + } + } + + fn digest(_metadata: Bytes, _message: Message) -> u256 { + let origin_merkle_tree_hook = MessageIdIsmMetadata::origin_merkle_tree_hook( + _metadata.clone() + ); + let root = MessageIdIsmMetadata::root(_metadata.clone()); + let index = MessageIdIsmMetadata::index(_metadata.clone()); + CheckpointLib::digest( + _message.origin, + origin_merkle_tree_hook.into(), + root.into(), + index, + MessageTrait::format_message(_message) + ) + } + + fn get_signature_at(_metadata: Bytes, _index: u32) -> Signature { + let (v, r, s) = MessageIdIsmMetadata::signature_at(_metadata, _index); + signature_from_vrs(v.into(), r, s) + } + + fn bool_is_eth_signature_valid( + msg_hash: u256, signature: Signature, signer: EthAddress + ) -> bool { + match is_eth_signature_valid(msg_hash, signature, signer) { + Result::Ok(()) => true, + Result::Err(_) => false + } + } + + fn build_validators_span(self: @ContractState) -> Span { + let mut validators = ArrayTrait::new(); + let mut cur_idx = 0; + loop { + let validator = self.validators.read(cur_idx); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + cur_idx += 1; + }; + validators.span() + } +} diff --git a/contracts/src/contracts/isms/multisig/validator_announce.cairo b/contracts/src/contracts/isms/multisig/validator_announce.cairo new file mode 100644 index 0000000..4d91469 --- /dev/null +++ b/contracts/src/contracts/isms/multisig/validator_announce.cairo @@ -0,0 +1,191 @@ +#[starknet::contract] +pub mod validator_announce { + use alexandria_bytes::{Bytes, BytesTrait}; + use core::keccak::keccak_u256s_be_inputs; + use hyperlane_starknet::contracts::libs::checkpoint_lib::checkpoint_lib::{ + HYPERLANE_ANNOUNCEMENT, ETH_SIGNED_MESSAGE + }; + use hyperlane_starknet::interfaces::IValidatorAnnounce; + use hyperlane_starknet::interfaces::{IMailboxClientDispatcher, IMailboxClientDispatcherTrait}; + use hyperlane_starknet::utils::keccak256::reverse_endianness; + use hyperlane_starknet::utils::store_arrays::StoreFelt252Array; + + use starknet::ContractAddress; + use starknet::EthAddress; + use starknet::eth_signature::is_eth_signature_valid; + use starknet::secp256_trait::{Signature, signature_from_vrs}; + + #[storage] + struct Storage { + mailboxclient: ContractAddress, + storage_location: LegacyMap::>, + replay_protection: LegacyMap::, + validators: LegacyMap::, + } + + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ValidatorAnnouncement: ValidatorAnnouncement + } + + #[derive(starknet::Event, Drop)] + pub struct ValidatorAnnouncement { + pub validator: EthAddress, + pub storage_location: felt252 + } + + pub mod Errors { + pub const REPLAY_PROTECTION_ERROR: felt252 = 'Announce already occured'; + pub const WRONG_SIGNER: felt252 = 'Wrong signer'; + } + + #[constructor] + fn constructor(ref self: ContractState, _mailbox_client: ContractAddress) { + self.mailboxclient.write(_mailbox_client); + } + + #[abi(embed_v0)] + impl IValidatorAnnonceImpl of IValidatorAnnounce { + fn announce( + ref self: ContractState, + _validator: EthAddress, + _storage_location: felt252, + _signature: Bytes + ) -> bool { + let felt252_validator: felt252 = _validator.into(); + let mut input: Array = array![ + felt252_validator.into(), _storage_location.into(), + ]; + let replay_id = keccak_hash(input.span()); + assert(!self.replay_protection.read(replay_id), Errors::REPLAY_PROTECTION_ERROR); + let announcement_digest = self.get_announcement_digest(_storage_location); + let signature: Signature = convert_to_signature(_signature); + assert( + bool_is_eth_signature_valid(announcement_digest, signature, _validator), + Errors::WRONG_SIGNER + ); + match find_validators_index(@self, _validator) { + Option::Some(_) => {}, + Option::None(()) => { + let last_validator = find_last_validator(@self); + self.validators.write(last_validator, _validator); + } + }; + let mut storage_locations = self.storage_location.read(_validator); + storage_locations.append(_storage_location); + self + .emit( + ValidatorAnnouncement { + validator: _validator, storage_location: _storage_location + } + ); + true + } + + fn get_announced_storage_locations( + self: @ContractState, mut _validators: Span + ) -> Span> { + let mut metadata = array![]; + loop { + match _validators.pop_front() { + Option::Some(validator) => { + let validator_metadata = self.storage_location.read(*validator); + metadata.append(validator_metadata.span()) + }, + Option::None(()) => { break (); } + } + }; + metadata.span() + } + + fn get_announced_validators(self: @ContractState) -> Span { + build_validators_array(self) + } + fn get_announcement_digest(self: @ContractState, _storage_location: felt252) -> u256 { + let domain_hash = domain_hash(self); + let mut input: Array = array![ + ETH_SIGNED_MESSAGE.into(), domain_hash.into(), _storage_location.into(), + ]; + let hash = keccak_u256s_be_inputs(input.span()); + reverse_endianness(hash) + } + } + + + fn convert_to_signature(_signature: Bytes) -> Signature { + let (_, r) = _signature.read_u256(0); + let (_, s) = _signature.read_u256(32); + let (_, v) = _signature.read_u256(64); + signature_from_vrs(v.try_into().unwrap(), r, s) + } + fn keccak_hash(_input: Span) -> u256 { + let hash = keccak_u256s_be_inputs(_input); + reverse_endianness(hash) + } + + + fn domain_hash(self: @ContractState) -> u256 { + let mailboxclient = IMailboxClientDispatcher { + contract_address: self.mailboxclient.read() + }; + let mailboxclient_address: felt252 = self.mailboxclient.read().try_into().unwrap(); + let mut input: Array = array![ + mailboxclient.get_local_domain().into(), + mailboxclient_address.try_into().unwrap(), + HYPERLANE_ANNOUNCEMENT.into() + ]; + let hash = keccak_u256s_be_inputs(input.span()); + reverse_endianness(hash) + } + + + fn bool_is_eth_signature_valid( + msg_hash: u256, signature: Signature, signer: EthAddress + ) -> bool { + match is_eth_signature_valid(msg_hash, signature, signer) { + Result::Ok(()) => true, + Result::Err(_) => false + } + } + + fn find_validators_index(self: @ContractState, _validator: EthAddress) -> Option { + let mut current_validator: EthAddress = 0.try_into().unwrap(); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == _validator { + break Option::Some(current_validator); + } else if next_validator == 0.try_into().unwrap() { + break Option::None(()); + } + current_validator = next_validator; + } + } + + fn find_last_validator(self: @ContractState) -> EthAddress { + let mut current_validator = self.validators.read(0.try_into().unwrap()); + loop { + let next_validator = self.validators.read(current_validator); + if next_validator == 0.try_into().unwrap() { + break current_validator; + } + current_validator = next_validator; + } + } + + fn build_validators_array(self: @ContractState) -> Span { + let mut index = 0.try_into().unwrap(); + let mut validators = array![]; + loop { + let validator = self.validators.read(index); + if (validator == 0.try_into().unwrap()) { + break (); + } + validators.append(validator); + index = validator; + }; + + validators.span() + } +} diff --git a/contracts/src/contracts/isms/routing/domain_routing_ism.cairo b/contracts/src/contracts/isms/routing/domain_routing_ism.cairo new file mode 100644 index 0000000..146d291 --- /dev/null +++ b/contracts/src/contracts/isms/routing/domain_routing_ism.cairo @@ -0,0 +1,151 @@ +#[starknet::contract] +pub mod domain_routing_ism { + use core::panic_with_felt252; + use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait}; + use hyperlane_starknet::interfaces::IDomainRoutingIsm; + + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + + use starknet::{ContractAddress, contract_address_const}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + type Domain = u32; + type Index = u32; + #[storage] + struct Storage { + modules: LegacyMap, + domains: LegacyMap, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + mod Errors { + pub const LENGTH_MISMATCH: felt252 = 'Length mismatch'; + pub const ORIGIN_NOT_FOUND: felt252 = 'Origin not found'; + pub const MODULE_CANNOT_BE_ZERO: felt252 = 'Module cannot be zero'; + pub const DOMAIN_NOT_FOUND: felt252 = 'Domain not found'; + } + + #[constructor] + fn constructor(ref self: ContractState, _owner: ContractAddress) { + self.ownable.initializer(_owner); + } + + #[abi(embed_v0)] + impl IDomainRoutingIsmImpl of IDomainRoutingIsm { + fn initialize( + ref self: ContractState, _domains: Span, _modules: Span + ) { + self.ownable.assert_only_owner(); + assert(_domains.len() == _modules.len(), Errors::LENGTH_MISMATCH); + let mut cur_idx = 0; + loop { + if (cur_idx == _domains.len()) { + break (); + } + _set(ref self, *_domains.at(cur_idx), *_modules.at(cur_idx)); + cur_idx += 1; + } + } + + fn set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + self.ownable.assert_only_owner(); + assert(_module != contract_address_const::<0>(), Errors::MODULE_CANNOT_BE_ZERO); + _set(ref self, _domain, _module); + } + + fn remove(ref self: ContractState, _domain: u32) { + self.ownable.assert_only_owner(); + _remove(ref self, _domain); + } + + fn domains(self: @ContractState) -> Span { + let mut current_domain = self.domains.read(0); + let mut domains = array![]; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + break (); + } + domains.append(current_domain); + current_domain = next_domain; + }; + domains.span() + } + + fn module(self: @ContractState, _origin: u32) -> ContractAddress { + let module = self.modules.read(_origin); + assert(module != contract_address_const::<0>(), Errors::ORIGIN_NOT_FOUND); + module + } + + fn route(self: @ContractState, _message: Message) -> ContractAddress { + self.modules.read(_message.origin) + } + } + + fn find_last_domain(self: @ContractState) -> u32 { + let mut current_domain = self.domains.read(0); + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == 0 { + break current_domain; + } + current_domain = next_domain; + } + } + + fn find_domain_index(self: @ContractState, _domain: u32) -> Option { + let mut current_domain = 0; + loop { + let next_domain = self.domains.read(current_domain); + if next_domain == _domain { + break Option::Some(current_domain); + } else if next_domain == 0 { + break Option::None(()); + } + current_domain = next_domain; + } + } + + fn _remove(ref self: ContractState, _domain: u32) { + let domain_index = match find_domain_index(@self, _domain) { + Option::Some(index) => index, + Option::None(()) => { + panic_with_felt252(Errors::DOMAIN_NOT_FOUND); + 0 + } + }; + let next_domain = self.domains.read(_domain); + self.domains.write(domain_index, next_domain); + } + + fn _set(ref self: ContractState, _domain: u32, _module: ContractAddress) { + match find_domain_index(@self, _domain) { + Option::Some(_) => {}, + Option::None(()) => { + let latest_domain = find_last_domain(@self); + self.domains.write(latest_domain, _domain); + } + } + self.modules.write(_domain, _module); + } +} diff --git a/contracts/src/contracts/libs/checkpoint_lib.cairo b/contracts/src/contracts/libs/checkpoint_lib.cairo new file mode 100644 index 0000000..e44f1bd --- /dev/null +++ b/contracts/src/contracts/libs/checkpoint_lib.cairo @@ -0,0 +1,50 @@ +pub mod checkpoint_lib { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use core::keccak::keccak_u256s_be_inputs; + use hyperlane_starknet::contracts::libs::message::Message; + use hyperlane_starknet::utils::keccak256::reverse_endianness; + + + pub trait CheckpointLib { + fn digest( + _origin: u32, + _origin_merkle_tree_hook: u256, + _checkpoint_root: u256, + _checkpoint_index: u32, + _message_id: u256 + ) -> u256; + fn domain_hash(_origin: u32, _origin_merkle_tree_hook: u256) -> u256; + } + const HYPERLANE: felt252 = 'HYPERLANE'; + pub const ETH_SIGNED_MESSAGE: felt252 = '\x19Ethereum Signed Message:\n'; + pub const HYPERLANE_ANNOUNCEMENT: felt252 = 'HYPERLANE_ANNOUNCEMENT'; + + impl CheckpointLibImpl of CheckpointLib { + fn digest( + _origin: u32, + _origin_merkle_tree_hook: u256, + _checkpoint_root: u256, + _checkpoint_index: u32, + _message_id: u256 + ) -> u256 { + let domain_hash = CheckpointLib::domain_hash(_origin, _origin_merkle_tree_hook); + let mut input: Array = array![ + ETH_SIGNED_MESSAGE.into(), + domain_hash.into(), + _checkpoint_root.into(), + _checkpoint_index.into(), + _message_id.into(), + ]; + let hash = keccak_u256s_be_inputs(input.span()); + reverse_endianness(hash) + } + + fn domain_hash(_origin: u32, _origin_merkle_tree_hook: u256) -> u256 { + let mut input: Array = array![ + _origin.into(), _origin_merkle_tree_hook.into(), HYPERLANE.into() + ]; + let hash = keccak_u256s_be_inputs(input.span()); + reverse_endianness(hash) + } + } +} diff --git a/src/contracts/libs/message.cairo b/contracts/src/contracts/libs/message.cairo similarity index 100% rename from src/contracts/libs/message.cairo rename to contracts/src/contracts/libs/message.cairo diff --git a/contracts/src/contracts/libs/multisig/message_id_ism_metadata.cairo b/contracts/src/contracts/libs/multisig/message_id_ism_metadata.cairo new file mode 100644 index 0000000..9f93bea --- /dev/null +++ b/contracts/src/contracts/libs/multisig/message_id_ism_metadata.cairo @@ -0,0 +1,39 @@ +pub mod message_id_ism_metadata { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + + + pub trait MessageIdIsmMetadata { + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256; + fn root(_metadata: Bytes) -> u256; + fn index(_metadata: Bytes) -> u32; + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256); + } + pub const ORIGIN_MERKLE_TREE_HOOK_OFFSET: u32 = 0; + pub const ROOT_OFFSET: u32 = 32; + pub const INDEX_OFFSET: u32 = 64; + pub const SIGNATURE_OFFSET: u32 = 80; + impl MessagIdIsmMetadataImpl of MessageIdIsmMetadata { + fn origin_merkle_tree_hook(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(ORIGIN_MERKLE_TREE_HOOK_OFFSET); + felt + } + + fn root(_metadata: Bytes) -> u256 { + let (_, felt) = _metadata.read_u256(ROOT_OFFSET); + felt + } + + fn index(_metadata: Bytes) -> u32 { + let (_, felt) = _metadata.read_u32(INDEX_OFFSET); + felt + } + + fn signature_at(_metadata: Bytes, _index: u32) -> (u8, u256, u256) { + // the first signer index is 0 + let (index_r, r) = _metadata.read_u256(SIGNATURE_OFFSET + 80 * _index); + let (index_s, s) = _metadata.read_u256(index_r); + let (_, v) = _metadata.read_u8(index_s); + (v, r, s) + } + } +} diff --git a/src/contracts/mailbox.cairo b/contracts/src/contracts/mailbox.cairo similarity index 100% rename from src/contracts/mailbox.cairo rename to contracts/src/contracts/mailbox.cairo diff --git a/contracts/src/contracts/mocks/ism.cairo b/contracts/src/contracts/mocks/ism.cairo new file mode 100644 index 0000000..77482d5 --- /dev/null +++ b/contracts/src/contracts/mocks/ism.cairo @@ -0,0 +1,42 @@ +#[starknet::contract] +pub mod ism { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait}; + use hyperlane_starknet::interfaces::{ + IInterchainSecurityModule, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, ModuleType + }; + use starknet::ContractAddress; + use starknet::EthAddress; + + #[storage] + struct Storage {} + #[abi(embed_v0)] + impl IMessageidMultisigIsmImpl of IInterchainSecurityModule { + fn module_type(self: @ContractState) -> ModuleType { + ModuleType::MESSAGE_ID_MULTISIG(starknet::get_contract_address()) + } + + fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool { + true + } + + fn validators_and_threshold( + self: @ContractState, _message: Message + ) -> (Span, u32) { + (array![].span(), 0) + } + + fn get_validators(self: @ContractState) -> Span { + array![].span() + } + + fn get_threshold(self: @ContractState) -> u32 { + 0 + } + + fn set_validators(ref self: ContractState, _validators: Span) {} + + fn set_threshold(ref self: ContractState, _threshold: u32) {} + } +} diff --git a/src/contracts/mocks/message_recipient.cairo b/contracts/src/contracts/mocks/message_recipient.cairo similarity index 100% rename from src/contracts/mocks/message_recipient.cairo rename to contracts/src/contracts/mocks/message_recipient.cairo diff --git a/src/interfaces.cairo b/contracts/src/interfaces.cairo similarity index 70% rename from src/interfaces.cairo rename to contracts/src/interfaces.cairo index 12ed7ab..b87b1f7 100644 --- a/src/interfaces.cairo +++ b/contracts/src/interfaces.cairo @@ -1,7 +1,8 @@ use alexandria_bytes::Bytes; +use core::array::ArrayTrait; use hyperlane_starknet::contracts::libs::message::Message; use starknet::ContractAddress; - +use starknet::EthAddress; #[derive(Serde)] pub enum Types { UNUSED, @@ -18,18 +19,19 @@ pub enum Types { } -#[derive(Serde)] +#[derive(Serde, Drop, PartialEq)] pub enum ModuleType { - UNUSED, - ROUTING, - AGGREGATION, - LEGACY_MULTISIG, - MERKLE_ROOT_MULTISIG, - MESSAGE_ID_MULTISIG, + UNUSED: ContractAddress, + ROUTING: ContractAddress, + AGGREGATION: ContractAddress, + LEGACY_MULTISIG: ContractAddress, + MERKLE_ROOT_MULTISIG: ContractAddress, + MESSAGE_ID_MULTISIG: ContractAddress, NULL, // used with relayer carrying no metadata - CCIP_READ, + CCIP_READ: ContractAddress, } + #[starknet::interface] pub trait IMailbox { fn initializer( @@ -102,12 +104,24 @@ pub trait IInterchainSecurityModule { /// * `_metadata` - Off-chain metadata provided by a relayer, specific to the security model encoded by /// the module (e.g. validator signatures) /// * `_message` - Hyperlane encoded interchain message - fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; + fn verify(self: @TContractState, _metadata: Bytes, _message: Message,) -> bool; + + fn validators_and_threshold( + self: @TContractState, _message: Message + ) -> (Span, u32); + + fn get_validators(self: @TContractState) -> Span; + + fn get_threshold(self: @TContractState) -> u32; + + fn set_validators(ref self: TContractState, _validators: Span); + + fn set_threshold(ref self: TContractState, _threshold: u32); } #[starknet::interface] pub trait ISpecifiesInterchainSecurityModule { - fn interchain_security_module(self: @TContractState) -> ContractAddress; + fn interchain_security_module(self: @TContractState) -> ModuleType; } @@ -147,6 +161,12 @@ pub trait IMailboxClient { _interchain_security_module: ContractAddress, ); + fn get_hook(self: @TContractState) -> ContractAddress; + + fn get_local_domain(self: @TContractState) -> u32; + + fn get_interchain_security_module(self: @TContractState) -> ContractAddress; + fn _is_latest_dispatched(self: @TContractState, _id: u256) -> bool; fn _is_delivered(self: @TContractState, _id: u256) -> bool; @@ -186,7 +206,6 @@ pub trait IInterchainGasPaymaster { ) -> u256; } - #[starknet::interface] pub trait IRouter { fn routers(self: @TContractState, _domain: u32) -> ContractAddress; @@ -204,3 +223,48 @@ pub trait IRouter { fn handle(self: @TContractState, _origin: u32, _sender: ContractAddress, _message: Message); } + +#[starknet::interface] +pub trait IDefaultFallbackRoutingIsm { + /// Returns an enum that represents the type of security model encoded by this ISM. + /// Relayers infer how to fetch and format metadata. + fn module_type(self: @TContractState) -> ModuleType; + + fn route(self: @TContractState, _message: Message) -> ContractAddress; + + fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; +} + +#[starknet::interface] +pub trait IDomainRoutingIsm { + fn initialize(ref self: TContractState, _domains: Span, _modules: Span); + + fn set(ref self: TContractState, _domain: u32, _module: ContractAddress); + + fn remove(ref self: TContractState, _domain: u32); + + fn domains(self: @TContractState) -> Span; + + fn module(self: @TContractState, _origin: u32) -> ContractAddress; + + fn route(self: @TContractState, _message: Message) -> ContractAddress; +} + + +#[starknet::interface] +pub trait IValidatorAnnounce { + fn get_announced_validators(self: @TContractState) -> Span; + + fn get_announced_storage_locations( + self: @TContractState, _validators: Span + ) -> Span>; + + fn announce( + ref self: TContractState, + _validator: EthAddress, + _storage_location: felt252, + _signature: Bytes + ) -> bool; + + fn get_announcement_digest(self: @TContractState, _storage_location: felt252) -> u256; +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo new file mode 100644 index 0000000..f80fa41 --- /dev/null +++ b/contracts/src/lib.cairo @@ -0,0 +1,38 @@ +mod interfaces; +mod contracts { + pub mod mailbox; + pub mod libs { + pub mod checkpoint_lib; + pub mod message; + pub mod multisig { + pub mod message_id_ism_metadata; + } + } + pub mod client { + pub mod mailboxclient; + pub mod router; + } + pub mod mocks { + pub mod ism; + pub mod message_recipient; + } + pub mod isms { + pub mod multisig { + pub mod messageid_multisig_ism; + pub mod validator_announce; + } + pub mod routing { + pub mod domain_routing_ism; + } + } +} +mod utils { + pub mod keccak256; + pub mod store_arrays; +} + +#[cfg(test)] +mod tests { + pub mod setup; + pub mod test_mailbox; +} diff --git a/contracts/src/tests/setup.cairo b/contracts/src/tests/setup.cairo new file mode 100644 index 0000000..3e167f1 --- /dev/null +++ b/contracts/src/tests/setup.cairo @@ -0,0 +1,146 @@ +use core::result::ResultTrait; +use hyperlane_starknet::contracts::mocks::message_recipient::message_recipient; +use hyperlane_starknet::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait, IInterchainSecurityModule, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IValidatorAnnounceDispatcher, IValidatorAnnounceDispatcherTrait, IMailboxClientDispatcher, + IMailboxClientDispatcherTrait +}; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn +}; +use starknet::secp256_trait::Signature; + +use starknet::{ContractAddress, contract_address_const, EthAddress}; + +pub const LOCAL_DOMAIN: u32 = 534352; +pub const DESTINATION_DOMAIN: u32 = 9841001; + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +pub fn NEW_OWNER() -> ContractAddress { + contract_address_const::<'NEW_OWNER'>() +} + +pub fn DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'DEFAULT_ISM'>() +} + +pub fn DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'DEFAULT_HOOK'>() +} + +pub fn REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'REQUIRED_HOOK'>() +} + +pub fn NEW_DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_ISM'>() +} + +pub fn NEW_DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_HOOK'>() +} + +pub fn NEW_REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'NEW_REQUIRED_HOOK'>() +} + +pub fn RECIPIENT_ADDRESS() -> ContractAddress { + contract_address_const::<'RECIPIENT_ADDRESS'>() +} + +pub fn VALIDATOR_ADDRESS() -> EthAddress { + 'VALIDATOR_ADDRESS'.try_into().unwrap() +} + +pub fn VALIDATOR_PUBLIC_KEY() -> u256 { + 'VALIDATOR_PUBLIC_KEY' +} + +pub fn setup() -> (IMailboxDispatcher, EventSpy) { + let mailbox_class = declare("mailbox").unwrap(); + let (mailbox_addr, _) = mailbox_class + .deploy(@array![LOCAL_DOMAIN.into(), OWNER().into()]) + .unwrap(); + let mut spy = spy_events(SpyOn::One(mailbox_addr)); + (IMailboxDispatcher { contract_address: mailbox_addr }, spy) +} + +pub fn mock_setup() -> IMessageRecipientDispatcher { + let message_recipient_class = declare("message_recipient").unwrap(); + + let (message_recipient_addr, _) = message_recipient_class.deploy(@array![]).unwrap(); + IMessageRecipientDispatcher { contract_address: message_recipient_addr } +} + +pub fn setup_messageid_multisig_ism() -> IInterchainSecurityModuleDispatcher { + let messageid_multisig_class = declare("messageid_multisig_ism").unwrap(); + + let (messageid_multisig_addr, _) = messageid_multisig_class.deploy(@array![]).unwrap(); + IInterchainSecurityModuleDispatcher { contract_address: messageid_multisig_addr } +} + +pub fn setup_mailbox_client() -> IMailboxClientDispatcher { + let (mailbox, _) = setup(); + let mailboxclient_class = declare("mailboxclient").unwrap(); + let (mailboxclient_addr, _) = mailboxclient_class + .deploy(@array![mailbox.contract_address.into(), OWNER().into()]) + .unwrap(); + IMailboxClientDispatcher { contract_address: mailboxclient_addr } +} + + +pub fn setup_validator_announce() -> IValidatorAnnounceDispatcher { + let validator_announce_class = declare("validator_announce").unwrap(); + let mailboxclient = setup_mailbox_client(); + let (validator_announce_addr, _) = validator_announce_class + .deploy(@array![mailboxclient.contract_address.into()]) + .unwrap(); + IValidatorAnnounceDispatcher { contract_address: validator_announce_addr } +} + + +// Configuration from the main cairo repo: https://github.com/starkware-libs/cairo/blob/main/corelib/src/test/secp256k1_test.cairo +pub fn get_message_and_signature(y_parity: bool) -> (u256, Array, Array) { + let msg_hash = 0xfbff8940be2153ce000c0e1933bf32e179c60f53c45f56b4ac84b2e90f1f6214; + let validators_array: Array = array![ + 0x2cb1a91F2F23D6eC7FD22d2f7996f55B71EB32dc.try_into().unwrap(), + 0x0fb1A81BcefDEc06154279219F227938D00B1c12.try_into().unwrap(), + 0xF650b555CFDEfF61d225058e26326266E69660c2.try_into().unwrap(), + 0x03aC66d13dc1B5b10fc363fC32f324ca947CDac1.try_into().unwrap(), + 0x5711B186cdCAFD9E7aa1f78c0A0c30d3C7A2Af77.try_into().unwrap() + ]; + let signatures = array![ + Signature { + r: 0xb994fec0137776002d05dcf847bbba338285f1210c9ca7829109578ac876519f, + s: 0x0a42bb91f22ef042ca82fdcf8c8a5846e0debbce509dc2a0ce28a988dcbe4a16, + y_parity + }, + Signature { + r: 0xf81a5dd3f871ad2d27a3b538e73663d723f8263fb3d289514346d43d000175f5, + s: 0x083df770623e9ae52a7bb154473961e24664bb003bdfdba6100fb5e540875ce1, + y_parity + }, + Signature { + r: 0x76b194f951f94492ca582dab63dc413b9ac1ca9992c22bc2186439e9ab8fdd3c, + s: 0x62a6a6f402edaa53e9bdc715070a61edb0d98d4e14e182f60bdd4ae932b40b29, + y_parity + }, + Signature { + r: 0x35932eefd85897d868aaacd4ba7aee81a2384e42ba062133f6d37fdfebf94ad4, + s: 0x78cce49db96ee27c3f461800388ac95101476605baa64a194b7dd4d56d2d4a4d, + y_parity + }, + Signature { + r: 0x6b38d4353d69396e91c57542254348d16459d448ab887574e9476a6ff76d49a1, + s: 0x3527627295bde423d7d799afef22affac4f00c70a5b651ad14c8879aeb9b6e03, + y_parity + } + ]; + + (msg_hash, validators_array, signatures) +} diff --git a/src/tests/test_mailbox.cairo b/contracts/src/tests/test_mailbox.cairo similarity index 100% rename from src/tests/test_mailbox.cairo rename to contracts/src/tests/test_mailbox.cairo diff --git a/contracts/src/tests/test_multisig.cairo b/contracts/src/tests/test_multisig.cairo new file mode 100644 index 0000000..8caf13d --- /dev/null +++ b/contracts/src/tests/test_multisig.cairo @@ -0,0 +1,217 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use alexandria_data_structures::array_ext::ArrayTraitExt; +use core::array::ArrayTrait; +use core::array::SpanTrait; +use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use hyperlane_starknet::contracts::libs::multisig::message_id_ism_metadata::message_id_ism_metadata::MessageIdIsmMetadata; +use hyperlane_starknet::contracts::mailbox::mailbox; +use hyperlane_starknet::interfaces::IMessageRecipientDispatcherTrait; +use hyperlane_starknet::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, ModuleType, + IInterchainSecurityModuleDispatcher, IInterchainSecurityModuleDispatcherTrait, + IInterchainSecurityModule +}; +use hyperlane_starknet::tests::setup::{ + setup, mock_setup, setup_messageid_multisig_ism, OWNER, NEW_OWNER, VALIDATOR_ADDRESS, + VALIDATOR_PUBLIC_KEY, setup_validator_announce, get_message_and_signature, LOCAL_DOMAIN, + DESTINATION_DOMAIN, RECIPIENT_ADDRESS +}; +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget, stop_prank}; +use starknet::eth_address::EthAddress; +use starknet::secp256_trait::Signature; +#[test] +fn test_set_validators() { + let new_validators = array![VALIDATOR_ADDRESS()].span(); + let validators = setup_messageid_multisig_ism(); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + validators.set_validators(new_validators); + let validators_span = validators.get_validators(); + assert(validators_span == new_validators, 'wrong validator address def'); +} + + +#[test] +fn test_set_threshold() { + let new_threshold = 3; + let validators = setup_messageid_multisig_ism(); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + validators.set_threshold(new_threshold); + assert(validators.get_threshold() == new_threshold, 'wrong validator threshold'); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_validators_fails_if_caller_not_owner() { + let new_validators = array![VALIDATOR_ADDRESS(),].span(); + let validators = setup_messageid_multisig_ism(); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER()); + validators.set_validators(new_validators); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_threshold_fails_if_caller_not_owner() { + let new_threshold = 3; + let validators = setup_messageid_multisig_ism(); + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER()); + validators.set_threshold(new_threshold); +} + + +#[test] +fn test_message_id_ism_metadata() { + let origin_merkle_tree_hook = array![ + // origin_merkle_tree_hook + 0x02030405060708091011121314151623, 0x16151413121110090807060504030201 + ]; + let root = array![0x01000304050607080910111213141516, 0x01020304050607080920111213141516,]; + let index = array![0x00000013000000000000000000000000]; + let index_u32 = 0x13; + let signature_1 = array![ + 0x09020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x01020304050607080910000000000000, + 0x02010304050607080910111213141516, + 0x03000000000000000000000000000000 + ]; + let signature_2 = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000, + 0x02000000000000000000000000000000 + ]; + let signature_3 = array![ + 0x01020304050607080910111213141516, + 0x13092450000011115450564500700000, + 0x01020304050607080910000000000000, + 0x01020304050607080910111213141516, + 0x02000000000000000000000000000000 + ]; + let signature_1_v = 0x3; + let signature_2_v = 0x2; + let signature_3_v = 0x2; + let mut metadata = origin_merkle_tree_hook.concat(@root); + metadata = metadata.concat(@index); + metadata = metadata.concat(@signature_1); + metadata = metadata.concat(@signature_2); + metadata = metadata.concat(@signature_3); + let bytes_metadata = BytesTrait::new(496, metadata); + assert( + MessageIdIsmMetadata::origin_merkle_tree_hook( + bytes_metadata.clone() + ) == u256 { low: *origin_merkle_tree_hook.at(1), high: *origin_merkle_tree_hook.at(0) }, + 'wrong merkle tree hook' + ); + assert( + MessageIdIsmMetadata::root( + bytes_metadata.clone() + ) == u256 { low: *root.at(1), high: *root.at(0) }, + 'wrong root' + ); + assert(MessageIdIsmMetadata::index(bytes_metadata.clone()) == index_u32, 'wrong index'); + assert( + MessageIdIsmMetadata::signature_at( + bytes_metadata.clone(), 0 + ) == ( + signature_1_v, + u256 { low: *signature_1.at(1), high: *signature_1.at(0) }, + u256 { low: *signature_1.at(3), high: *signature_1.at(2) } + ), + 'wrong signature 1' + ); + assert( + MessageIdIsmMetadata::signature_at( + bytes_metadata.clone(), 1 + ) == ( + signature_2_v, + u256 { low: *signature_2.at(1), high: *signature_2.at(0) }, + u256 { low: *signature_2.at(3), high: *signature_2.at(2) } + ), + 'wrong signature 2' + ); + assert( + MessageIdIsmMetadata::signature_at( + bytes_metadata.clone(), 2 + ) == ( + signature_3_v, + u256 { low: *signature_3.at(1), high: *signature_3.at(0) }, + u256 { low: *signature_3.at(3), high: *signature_3.at(2) } + ), + 'wrong signature 3' + ); +} + + +#[test] +fn test_message_id_multisig_module_type() { + let messageid = setup_messageid_multisig_ism(); + assert( + messageid.module_type() == ModuleType::MESSAGE_ID_MULTISIG(messageid.contract_address), + 'Wrong module type' + ); +} + + +#[test] +fn test_message_id_multisig_verify() { + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let messageid = setup_messageid_multisig_ism(); + let (msg_hash, validators_address, signatures) = get_message_and_signature(false); + let validators = setup_messageid_multisig_ism(); + let metadata = array![ + 0x01020304050607080910111213141516, + 0x16151413121110090807060504030201, + 0x01020304050607080910111213141516, + 0x01020304050607080920111213141516, + 0x00000010000000000000000000000000, + *signatures.at(0).r.high, + *signatures.at(0).r.low, + *signatures.at(0).s.high, + *signatures.at(0).s.low, + *signatures.at(1).r.high, + *signatures.at(1).r.low, + *signatures.at(1).s.high, + *signatures.at(1).s.low, + *signatures.at(2).r.high, + *signatures.at(2).r.low, + *signatures.at(2).s.high, + *signatures.at(2).s.low, + *signatures.at(3).r.high, + *signatures.at(3).r.low, + *signatures.at(3).s.high, + *signatures.at(3).s.low, + 0x00000010000000000000000000000000 + ]; + let ownable = IOwnableDispatcher { contract_address: validators.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + validators.set_validators(validators_address.span()); + validators.set_threshold(3); + let bytes_metadata = BytesTrait::new(496, metadata); + assert( + messageid.verify(bytes_metadata, message, validators.contract_address) == true, + 'verification failed' + ); +} diff --git a/src/utils/keccak256.cairo b/contracts/src/utils/keccak256.cairo similarity index 100% rename from src/utils/keccak256.cairo rename to contracts/src/utils/keccak256.cairo diff --git a/contracts/src/utils/store_arrays.cairo b/contracts/src/utils/store_arrays.cairo new file mode 100644 index 0000000..e8eb6d0 --- /dev/null +++ b/contracts/src/utils/store_arrays.cairo @@ -0,0 +1,71 @@ +use starknet::storage_access::{Store, StorageBaseAddress,}; +// ************************************************************************* +// IMPORTS +// ************************************************************************* + +// Code from Satoru +// Core lib imports. +use starknet::{ContractAddress, SyscallResult,}; + + +pub impl StoreFelt252Array of Store> { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult> { + StoreFelt252Array::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreFelt252Array::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = array![]; + + // Read the stored array's length. If the length is superior to 255, the read will fail. + let len: u8 = Store::::read_at_offset(address_domain, base, offset).unwrap(); + offset += 1; + + // Sequentially read all stored elements and append them to the array. + let exit = len + offset; + loop { + if offset >= exit { + break; + } + + let value = Store::::read_at_offset(address_domain, base, offset).unwrap(); + arr.append(value); + offset += Store::::size(); + }; + + // Return the array. + Result::Ok(arr) + } + + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8, mut value: Array + ) -> SyscallResult<()> { + // // Store the length of the array in the first storage slot. + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len); + offset += 1; + + // Store the array elements sequentially + loop { + match value.pop_front() { + Option::Some(element) => { + Store::::write_at_offset(address_domain, base, offset, element) + .unwrap(); + offset += Store::::size(); + }, + Option::None(_) => { break Result::Ok(()); } + }; + } + } + + fn size() -> u8 { + 1 + } +} diff --git a/src/lib.cairo b/src/lib.cairo deleted file mode 100644 index 71d0b62..0000000 --- a/src/lib.cairo +++ /dev/null @@ -1,23 +0,0 @@ -mod interfaces; -mod contracts { - pub mod mailbox; - pub mod libs { - pub mod message; - } - pub mod client { - pub mod mailboxclient; - pub mod router; - } - pub mod mocks { - pub mod message_recipient; - } -} -mod utils { - pub mod keccak256; -} - -#[cfg(test)] -mod tests { - pub mod setup; - pub mod test_mailbox; -} diff --git a/src/tests/setup.cairo b/src/tests/setup.cairo deleted file mode 100644 index 0ef29fd..0000000 --- a/src/tests/setup.cairo +++ /dev/null @@ -1,66 +0,0 @@ -use core::result::ResultTrait; -use hyperlane_starknet::contracts::mocks::message_recipient::message_recipient; -use hyperlane_starknet::interfaces::{ - IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, - IMessageRecipientDispatcherTrait -}; -use snforge_std::{ - declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn -}; - -use starknet::{ContractAddress, contract_address_const}; - -pub const LOCAL_DOMAIN: u32 = 534352; -pub const DESTINATION_DOMAIN: u32 = 9841001; - -pub fn OWNER() -> ContractAddress { - contract_address_const::<'OWNER'>() -} - -pub fn NEW_OWNER() -> ContractAddress { - contract_address_const::<'NEW_OWNER'>() -} - -pub fn DEFAULT_ISM() -> ContractAddress { - contract_address_const::<'DEFAULT_ISM'>() -} - -pub fn DEFAULT_HOOK() -> ContractAddress { - contract_address_const::<'DEFAULT_HOOK'>() -} - -pub fn REQUIRED_HOOK() -> ContractAddress { - contract_address_const::<'REQUIRED_HOOK'>() -} - -pub fn NEW_DEFAULT_ISM() -> ContractAddress { - contract_address_const::<'NEW_DEFAULT_ISM'>() -} - -pub fn NEW_DEFAULT_HOOK() -> ContractAddress { - contract_address_const::<'NEW_DEFAULT_HOOK'>() -} - -pub fn NEW_REQUIRED_HOOK() -> ContractAddress { - contract_address_const::<'NEW_REQUIRED_HOOK'>() -} - -pub fn RECIPIENT_ADDRESS() -> ContractAddress { - contract_address_const::<'RECIPIENT_ADDRESS'>() -} - -pub fn setup() -> (IMailboxDispatcher, EventSpy) { - let mailbox_class = declare("mailbox").unwrap(); - let (mailbox_addr, _) = mailbox_class - .deploy(@array![LOCAL_DOMAIN.into(), OWNER().into()]) - .unwrap(); - let mut spy = spy_events(SpyOn::One(mailbox_addr)); - (IMailboxDispatcher { contract_address: mailbox_addr }, spy) -} - -pub fn mock_setup() -> IMessageRecipientDispatcher { - let message_recipient_class = declare("message_recipient").unwrap(); - - let (message_recipient_addr, _) = message_recipient_class.deploy(@array![]).unwrap(); - IMessageRecipientDispatcher { contract_address: message_recipient_addr } -}