Skip to content

Commit

Permalink
✨ ISM (#8)
Browse files Browse the repository at this point in the history
* 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

* Update release.yml

---------

Co-authored-by: JordyRo1 <[email protected]>
  • Loading branch information
EvolveArt and JordyRo1 authored May 15, 2024
1 parent 8c39481 commit 22a7911
Show file tree
Hide file tree
Showing 26 changed files with 1,322 additions and 107 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@ 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/ \;
- name: Generate checksums
working-directory: ${{ env.working-directory}}
run: |
cd filtered_artifacts
for file in *; do
sha256sum "$file" > "$file.sha256"
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 .
Expand All @@ -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
Expand All @@ -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
11 changes: 8 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
- 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
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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<ContractState> {
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)
}
}
176 changes: 176 additions & 0 deletions contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo
Original file line number Diff line number Diff line change
@@ -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<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
validators: LegacyMap<u32, EthAddress>,
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<ContractState> {
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<EthAddress> {
build_validators_span(self)
}

fn get_threshold(self: @ContractState) -> u32 {
self.threshold.read()
}

fn set_validators(ref self: ContractState, _validators: Span<EthAddress>) {
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<EthAddress>, 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<EthAddress> {
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()
}
}
Loading

0 comments on commit 22a7911

Please sign in to comment.