Make merkle proofs optional on multisig ISM (#2173)

### Description

Validators currently sign `(root, index)` checkpoints and during
verification, a `message` is passed as calldata, an `id()` is derived,
and a `proof` of `id()` at `index` in `root` is verified

This provides “all or nothing” censorship resistance guarantees because
a validator can only sign roots to allow any contained messages to be
processed.

We have considered alternatives where validators sign `message` directly
and we lose censorship resistance in exchange for eliminating merkle
proof verification gas costs.

However, if validators sign `(root, index, message)` tuples, we can skip
merkle proof verification on the destination chain while still
maintaining censorship resistance by providing two valid metadata
formats:

1. existing validator signatures and merkle proof verification of
inclusion
2. including merkle proof verification for pathway where validators are
censoring `message`

It’s worth noting the validator is required to index event data to
produce this new signature format. However, this does not require
historical indexing and new validators being spun up can simply begin
indexing from tip.

See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2187 for
validator changes
See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2248 for
relayer and e2e test changes

### Drive-by changes

Merkle index also optional

### Related issues

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2192

### Backward compatibility

- new ISM deployment is necessary (we could upgrade implementation in
theory)
- Validator and relayer upgrades

### Testing

Unit (fuzz) Tests, E2E tests
pull/2283/head
Yorke Rhodes 1 year ago committed by GitHub
parent 8aa7b62bea
commit 50f04db1fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .github/workflows/e2e.yml
  2. 3
      rust/Cargo.lock
  3. 1
      rust/Cargo.toml
  4. 1
      rust/agents/relayer/Cargo.toml
  5. 83
      rust/agents/relayer/src/msg/metadata/base.rs
  6. 1
      rust/agents/relayer/src/msg/metadata/mod.rs
  7. 185
      rust/agents/relayer/src/msg/metadata/multisig.rs
  8. 167
      rust/agents/relayer/src/msg/metadata/multisig/base.rs
  9. 69
      rust/agents/relayer/src/msg/metadata/multisig/legacy_multisig.rs
  10. 61
      rust/agents/relayer/src/msg/metadata/multisig/merkle_root_multisig.rs
  11. 61
      rust/agents/relayer/src/msg/metadata/multisig/message_id_multisig.rs
  12. 10
      rust/agents/relayer/src/msg/metadata/multisig/mod.rs
  13. 2
      rust/agents/relayer/src/relayer.rs
  14. 15
      rust/agents/validator/src/settings.rs
  15. 166
      rust/agents/validator/src/submit.rs
  16. 171
      rust/agents/validator/src/validator.rs
  17. 1
      rust/chains/hyperlane-ethereum/Cargo.toml
  18. 16
      rust/chains/hyperlane-ethereum/src/interchain_security_module.rs
  19. 66
      rust/chains/hyperlane-ethereum/src/mailbox.rs
  20. 15
      rust/chains/hyperlane-ethereum/src/provider.rs
  21. 12
      rust/chains/hyperlane-fuel/src/mailbox.rs
  22. 4
      rust/hyperlane-base/src/contract_sync/mod.rs
  23. 13
      rust/hyperlane-base/src/traits/checkpoint_syncer.rs
  24. 37
      rust/hyperlane-base/src/types/local_storage.rs
  25. 211
      rust/hyperlane-base/src/types/multisig.rs
  26. 42
      rust/hyperlane-base/src/types/s3_storage.rs
  27. 1
      rust/hyperlane-core/Cargo.toml
  28. 10
      rust/hyperlane-core/src/accumulator/incremental.rs
  29. 25
      rust/hyperlane-core/src/traits/interchain_security_module.rs
  30. 10
      rust/hyperlane-core/src/traits/mailbox.rs
  31. 80
      rust/hyperlane-core/src/types/checkpoint.rs
  32. 8
      rust/hyperlane-test/src/mocks/mailbox.rs
  33. 79
      rust/utils/run-locally/src/main.rs
  34. 89
      solidity/.gas-snapshot
  35. 3
      solidity/contracts/interfaces/IInterchainSecurityModule.sol
  36. 64
      solidity/contracts/isms/multisig/AbstractMerkleRootMultisigIsm.sol
  37. 54
      solidity/contracts/isms/multisig/AbstractMessageIdMultisigIsm.sol
  38. 88
      solidity/contracts/isms/multisig/AbstractMultisigIsm.sol
  39. 4
      solidity/contracts/isms/multisig/LegacyMultisigIsm.sol
  40. 63
      solidity/contracts/isms/multisig/StaticMultisigIsm.sol
  41. 16
      solidity/contracts/isms/multisig/StaticMultisigIsmFactory.sol
  42. 1
      solidity/contracts/isms/routing/AbstractRoutingIsm.sol
  43. 12
      solidity/contracts/libs/CheckpointLib.sol
  44. 50
      solidity/contracts/libs/LegacyCheckpointLib.sol
  45. 3
      solidity/contracts/libs/Merkle.sol
  46. 58
      solidity/contracts/libs/isms/MerkleRootMultisigIsmMetadata.sol
  47. 59
      solidity/contracts/libs/isms/MessageIdMultisigIsmMetadata.sol
  48. 4
      solidity/contracts/test/TestLegacyMultisigIsm.sol
  49. 2
      solidity/contracts/test/TestMultisigIsm.sol
  50. 136
      solidity/test/isms/MultisigIsm.t.sol
  51. 12
      typescript/infra/config/environments/test/multisigIsm.ts
  52. 15
      typescript/infra/hardhat.config.ts
  53. 63
      typescript/sdk/src/consts/environments/test.json
  54. 36
      typescript/sdk/src/consts/multisigIsm.ts
  55. 8
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  56. 96
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  57. 16
      typescript/sdk/src/ism/HyperlaneIsmFactoryDeployer.ts
  58. 7
      typescript/sdk/src/ism/contracts.ts
  59. 19
      typescript/sdk/src/ism/types.ts
  60. 2
      typescript/sdk/src/test/testUtils.ts

@ -38,6 +38,9 @@ jobs:
toolchain: stable
profile: minimal
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- name: rust cache
uses: Swatinem/rust-cache@v2
with:

3
rust/Cargo.lock generated

@ -2835,6 +2835,7 @@ dependencies = [
"config",
"convert_case 0.6.0",
"derive-new",
"derive_more",
"ethers-contract",
"ethers-core",
"ethers-providers",
@ -2870,6 +2871,7 @@ dependencies = [
"hex 0.4.3",
"hyperlane-core",
"num",
"num-traits",
"reqwest",
"serde",
"serde_json",
@ -4342,6 +4344,7 @@ dependencies = [
"async-trait",
"config",
"derive-new",
"derive_more",
"enum_dispatch",
"ethers",
"ethers-contract",

@ -31,6 +31,7 @@ async-trait = { version = "0.1" }
color-eyre = { version = "0.6" }
config = "~0.13.3"
derive-new = "0.5"
derive_more = "0.99"
enum_dispatch = "0.3"
ethers = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-02-10-01" }
ethers-contract = { git = "https://github.com/hyperlane-xyz/ethers-rs", tag = "2023-02-10-01", features = ["legacy"] }

@ -33,6 +33,7 @@ hyperlane-base = { path = "../../hyperlane-base" }
hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" }
num-derive.workspace = true
num-traits.workspace = true
derive_more.workspace = true
[dev-dependencies]
tokio-test = "0.4"

@ -5,39 +5,33 @@ use std::{collections::HashMap, fmt::Debug};
use async_trait::async_trait;
use derive_new::new;
use eyre::{Context, Result};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use tokio::sync::RwLock;
use tracing::{debug, instrument, warn};
use tracing::{debug, info, instrument, warn};
use hyperlane_base::{
ChainConf, CheckpointSyncer, CheckpointSyncerConf, CoreMetrics, MultisigCheckpointSyncer,
};
use hyperlane_core::accumulator::merkle::Proof;
use hyperlane_core::{
HyperlaneDomain, HyperlaneMessage, MultisigIsm, MultisigSignedCheckpoint, RoutingIsm,
Checkpoint, HyperlaneDomain, HyperlaneMessage, ModuleType, MultisigIsm, RoutingIsm,
ValidatorAnnounce, H160, H256,
};
use crate::merkle_tree_builder::MerkleTreeBuilder;
use crate::msg::metadata::{MultisigIsmMetadataBuilder, RoutingIsmMetadataBuilder};
use crate::msg::metadata::multisig::{
LegacyMultisigMetadataBuilder, MerkleRootMultisigMetadataBuilder,
MessageIdMultisigMetadataBuilder,
};
use crate::msg::metadata::RoutingIsmMetadataBuilder;
#[derive(Debug, thiserror::Error)]
pub enum MetadataBuilderError {
#[error("Unknown or invalid module type ({0})")]
UnsupportedModuleType(u8),
UnsupportedModuleType(ModuleType),
#[error("Exceeded max depth when building metadata ({0})")]
MaxDepthExceeded(u32),
}
#[derive(FromPrimitive, Clone, Debug)]
pub enum SupportedIsmTypes {
Routing = 1,
// Aggregation = 2,
LegacyMultisig = 3,
Multisig = 4,
}
#[async_trait]
pub trait MetadataBuilder: Send + Sync {
#[allow(clippy::async_yields_async)]
@ -71,7 +65,7 @@ impl Debug for BaseMetadataBuilder {
#[async_trait]
impl MetadataBuilder for BaseMetadataBuilder {
#[instrument(err, skip(self))]
#[instrument(err, skip(self), fields(domain=self.domain().name()))]
async fn build(
&self,
ism_address: H256,
@ -84,17 +78,16 @@ impl MetadataBuilder for BaseMetadataBuilder {
.await
.context(CTX)?;
let module_type = ism.module_type().await.context(CTX)?;
let supported_type = SupportedIsmTypes::from_u8(module_type)
.ok_or(MetadataBuilderError::UnsupportedModuleType(module_type))
.context(CTX)?;
let base = self.clone_with_incremented_depth()?;
let metadata_builder: Box<dyn MetadataBuilder> = match supported_type {
SupportedIsmTypes::Multisig => Box::new(MultisigIsmMetadataBuilder::new(base, false)),
SupportedIsmTypes::LegacyMultisig => {
Box::new(MultisigIsmMetadataBuilder::new(base, true))
let metadata_builder: Box<dyn MetadataBuilder> = match module_type {
ModuleType::LegacyMultisig => Box::new(LegacyMultisigMetadataBuilder::new(base)),
ModuleType::MerkleRootMultisig => {
Box::new(MerkleRootMultisigMetadataBuilder::new(base))
}
SupportedIsmTypes::Routing => Box::new(RoutingIsmMetadataBuilder::new(base)),
ModuleType::MessageIdMultisig => Box::new(MessageIdMultisigMetadataBuilder::new(base)),
ModuleType::Routing => Box::new(RoutingIsmMetadataBuilder::new(base)),
_ => return Err(MetadataBuilderError::UnsupportedModuleType(module_type).into()),
};
metadata_builder
.build(ism_address, message)
@ -118,35 +111,31 @@ impl BaseMetadataBuilder {
}
}
pub async fn get_proof(
&self,
message: &HyperlaneMessage,
checkpoint: MultisigSignedCheckpoint,
) -> Result<Proof> {
pub async fn get_proof(&self, nonce: u32, checkpoint: Checkpoint) -> Result<Option<Proof>> {
const CTX: &str = "When fetching message proof";
self.origin_prover_sync
let proof = self
.origin_prover_sync
.read()
.await
.get_proof(message.nonce, checkpoint.checkpoint.index)
.context(CTX)
.get_proof(nonce, checkpoint.index)
.context(CTX)?;
// checkpoint may be fraudulent if the root does not
// match the canonical root at the checkpoint's index
if proof.root() != checkpoint.root {
info!(
?checkpoint,
canonical_root = ?proof.root(),
"Could not fetch metadata: checkpoint root does not match canonical root from merkle proof"
);
Ok(None)
} else {
Ok(Some(proof))
}
}
pub async fn fetch_checkpoint(
&self,
validators: &Vec<H256>,
threshold: usize,
message: &HyperlaneMessage,
) -> Result<Option<MultisigSignedCheckpoint>> {
const CTX: &str = "When fetching checkpoint signatures";
let highest_known_nonce = self.origin_prover_sync.read().await.count() - 1;
let checkpoint_syncer = self
.build_checkpoint_syncer(validators)
.await
.context(CTX)?;
checkpoint_syncer
.fetch_checkpoint_in_range(validators, threshold, message.nonce, highest_known_nonce)
.await
.context(CTX)
pub async fn highest_known_nonce(&self) -> u32 {
self.origin_prover_sync.read().await.count() - 1
}
pub async fn build_routing_ism(&self, address: H256) -> Result<Box<dyn RoutingIsm>> {

@ -4,5 +4,4 @@ mod routing;
pub(crate) use base::BaseMetadataBuilder;
pub(crate) use base::MetadataBuilder;
use multisig::MultisigIsmMetadataBuilder;
use routing::RoutingIsmMetadataBuilder;

@ -1,185 +0,0 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
use async_trait::async_trait;
use derive_new::new;
use ethers::abi::Token;
use eyre::Context;
use tracing::{debug, info, instrument};
use hyperlane_core::accumulator::merkle::Proof;
use hyperlane_core::{
HyperlaneMessage, MultisigIsm, MultisigSignedCheckpoint, SignatureWithSigner, H256,
};
use super::base::MetadataBuilder;
use super::BaseMetadataBuilder;
#[derive(Clone, Debug, new)]
pub struct MultisigIsmMetadataBuilder {
base: BaseMetadataBuilder,
legacy: bool,
}
impl Deref for MultisigIsmMetadataBuilder {
type Target = BaseMetadataBuilder;
fn deref(&self) -> &Self::Target {
&self.base
}
}
#[async_trait]
impl MetadataBuilder for MultisigIsmMetadataBuilder {
#[instrument(err, skip(self))]
async fn build(
&self,
ism_address: H256,
message: &HyperlaneMessage,
) -> eyre::Result<Option<Vec<u8>>> {
const CTX: &str = "When fetching MultisigIsm metadata";
let multisig_ism = self.build_multisig_ism(ism_address).await.context(CTX)?;
let (validators, threshold) = multisig_ism
.validators_and_threshold(message)
.await
.context(CTX)?;
let Some(checkpoint) = self.fetch_checkpoint(&validators, threshold.into(), message)
.await.context(CTX)?
else {
if validators.is_empty() {
info!(
ism=%multisig_ism.address(),
chain=%self.base.domain().name(),
"Could not fetch metadata: No validator set for chain is configured on the recipient's ISM"
);
} else {
info!(
?validators, threshold, ism=%multisig_ism.address(),
"Could not fetch metadata: Unable to reach quorum"
);
}
return Ok(None);
};
// At this point we have a signed checkpoint with a quorum of validator
// signatures. But it may be a fraudulent checkpoint that doesn't
// match the canonical root at the checkpoint's index.
debug!(?checkpoint, "Found checkpoint with quorum");
let proof = self
.get_proof(message, checkpoint.clone())
.await
.context(CTX)?;
if checkpoint.checkpoint.root == proof.root() {
debug!(
?validators,
threshold,
?checkpoint,
?proof,
"Fetched metadata"
);
let metadata = self.format_metadata(&validators, threshold, &checkpoint, &proof);
Ok(Some(metadata))
} else {
info!(
?checkpoint,
canonical_root = ?proof.root(),
"Could not fetch metadata: Signed checkpoint does not match canonical root"
);
Ok(None)
}
}
}
impl MultisigIsmMetadataBuilder {
/// Returns the metadata needed by the contract's verify function
fn format_metadata(
&self,
validators: &[H256],
threshold: u8,
checkpoint: &MultisigSignedCheckpoint,
proof: &Proof,
) -> Vec<u8> {
assert_eq!(threshold as usize, checkpoint.signatures.len());
let root_bytes = checkpoint.checkpoint.root.to_fixed_bytes().into();
let index_bytes = checkpoint.checkpoint.index.to_be_bytes().into();
let proof_tokens: Vec<Token> = proof
.path
.iter()
.map(|x| Token::FixedBytes(x.to_fixed_bytes().into()))
.collect();
let mailbox_and_proof_bytes = ethers::abi::encode(&[
Token::FixedBytes(
checkpoint
.checkpoint
.mailbox_address
.to_fixed_bytes()
.into(),
),
Token::FixedArray(proof_tokens),
]);
// The ethers encoder likes to zero-pad non word-aligned byte arrays.
// Thus, we pack the signatures, which are not word-aligned, ourselves.
let signature_vecs: Vec<Vec<u8>> = order_signatures(validators, &checkpoint.signatures);
let signature_bytes = signature_vecs.concat();
if self.legacy {
let validator_tokens: Vec<Token> = validators
.iter()
.map(|x| Token::FixedBytes(x.to_fixed_bytes().into()))
.collect();
let validator_bytes = ethers::abi::encode(&[Token::FixedArray(validator_tokens)]);
[
root_bytes,
index_bytes,
mailbox_and_proof_bytes,
Vec::from([threshold]),
signature_bytes,
validator_bytes,
]
.concat()
} else {
[
root_bytes,
index_bytes,
mailbox_and_proof_bytes,
signature_bytes,
]
.concat()
}
}
}
/// Orders `signatures` by the signers according to the `desired_order`.
/// Returns a Vec of the signature raw bytes in the correct order.
/// Panics if any signers in `signatures` are not present in `desired_order`
fn order_signatures(desired_order: &[H256], signatures: &[SignatureWithSigner]) -> Vec<Vec<u8>> {
// Signer address => index to sort by
let ordering_map: HashMap<H256, usize> = desired_order
.iter()
.cloned()
.enumerate()
.map(|(index, a)| (a, index))
.collect();
// Create a tuple of (SignatureWithSigner, index to sort by)
let mut ordered_signatures = signatures
.iter()
.cloned()
.map(|s| {
let order_index = ordering_map.get(&H256::from(s.signer)).unwrap();
(s, *order_index)
})
.collect::<Vec<(SignatureWithSigner, usize)>>();
// Sort by the index
ordered_signatures.sort_by_key(|s| s.1);
// Now collect only the raw signature bytes
ordered_signatures
.iter()
.map(|s| s.0.signature.to_vec())
.collect()
}

@ -0,0 +1,167 @@
use std::collections::HashMap;
use std::fmt::Debug;
use async_trait::async_trait;
use derive_new::new;
use ethers::abi::Token;
use eyre::{Context, Result};
use hyperlane_base::MultisigCheckpointSyncer;
use hyperlane_core::accumulator::merkle::Proof;
use hyperlane_core::{Checkpoint, HyperlaneMessage, SignatureWithSigner, H256};
use strum::Display;
use tracing::{debug, info};
use crate::msg::metadata::BaseMetadataBuilder;
use crate::msg::metadata::MetadataBuilder;
#[derive(new)]
pub struct MultisigMetadata {
checkpoint: Checkpoint,
signatures: Vec<SignatureWithSigner>,
message_id: Option<H256>,
proof: Option<Proof>,
}
#[derive(Debug, Display, PartialEq, Eq, Clone)]
pub enum MetadataToken {
CheckpointRoot,
CheckpointIndex,
CheckpointMailbox,
MessageId,
MerkleProof,
Threshold,
Signatures,
Validators,
}
#[async_trait]
pub trait MultisigIsmMetadataBuilder: AsRef<BaseMetadataBuilder> + Send + Sync {
async fn fetch_metadata(
&self,
validators: &[H256],
threshold: u8,
message: &HyperlaneMessage,
checkpoint_syncer: &MultisigCheckpointSyncer,
) -> Result<Option<MultisigMetadata>>;
fn token_layout(&self) -> Vec<MetadataToken>;
fn format_metadata(
&self,
validators: &[H256],
threshold: u8,
metadata: MultisigMetadata,
) -> Vec<u8> {
let build_token = |token: &MetadataToken| match token {
MetadataToken::CheckpointRoot => metadata.checkpoint.root.to_fixed_bytes().into(),
MetadataToken::CheckpointIndex => metadata.checkpoint.index.to_be_bytes().into(),
MetadataToken::CheckpointMailbox => {
metadata.checkpoint.mailbox_address.to_fixed_bytes().into()
}
MetadataToken::MessageId => metadata.message_id.unwrap().to_fixed_bytes().into(),
MetadataToken::Threshold => Vec::from([threshold]),
MetadataToken::MerkleProof => {
let proof_tokens: Vec<Token> = metadata
.proof
.unwrap()
.path
.iter()
.map(|x| Token::FixedBytes(x.to_fixed_bytes().into()))
.collect();
ethers::abi::encode(&proof_tokens)
}
MetadataToken::Validators => {
let validator_tokens: Vec<Token> = validators
.iter()
.map(|x| Token::FixedBytes(x.to_fixed_bytes().into()))
.collect();
ethers::abi::encode(&[Token::FixedArray(validator_tokens)])
}
MetadataToken::Signatures => {
let ordered_signatures = order_signatures(validators, &metadata.signatures);
let threshold_signatures = &ordered_signatures[..threshold as usize];
threshold_signatures.concat()
}
};
self.token_layout().iter().flat_map(build_token).collect()
}
}
#[async_trait]
impl<T: MultisigIsmMetadataBuilder> MetadataBuilder for T {
#[allow(clippy::async_yields_async)]
async fn build(
&self,
ism_address: H256,
message: &HyperlaneMessage,
) -> Result<Option<Vec<u8>>> {
const CTX: &str = "When fetching MultisigIsm metadata";
let multisig_ism = self
.as_ref()
.build_multisig_ism(ism_address)
.await
.context(CTX)?;
let (validators, threshold) = multisig_ism
.validators_and_threshold(message)
.await
.context(CTX)?;
if validators.is_empty() {
info!("Could not fetch metadata: No validator set found for ISM");
return Ok(None);
}
let checkpoint_syncer = self
.as_ref()
.build_checkpoint_syncer(&validators)
.await
.context(CTX)?;
if let Some(metadata) = self
.fetch_metadata(&validators, threshold, message, &checkpoint_syncer)
.await
.context(CTX)?
{
debug!(?message, ?metadata.checkpoint, "Found checkpoint with quorum");
Ok(Some(self.format_metadata(&validators, threshold, metadata)))
} else {
info!(
?message, ?validators, threshold, ism=%multisig_ism.address(),
"Could not fetch metadata: Unable to reach quorum"
);
Ok(None)
}
}
}
/// Orders `signatures` by the signers according to the `desired_order`.
/// Returns a Vec of the signature raw bytes in the correct order.
/// Panics if any signers in `signatures` are not present in `desired_order`
fn order_signatures(desired_order: &[H256], signatures: &[SignatureWithSigner]) -> Vec<Vec<u8>> {
// Signer address => index to sort by
let ordering_map: HashMap<H256, usize> = desired_order
.iter()
.enumerate()
.map(|(index, a)| (*a, index))
.collect();
// Create a tuple of (SignatureWithSigner, index to sort by)
let mut ordered_signatures = signatures
.iter()
.cloned()
.map(|s| {
let order_index = ordering_map.get(&H256::from(s.signer)).unwrap();
(s, *order_index)
})
.collect::<Vec<_>>();
// Sort by the index
ordered_signatures.sort_by_key(|s| s.1);
// Now collect only the raw signature bytes
ordered_signatures
.into_iter()
.map(|s| s.0.signature.to_vec())
.collect()
}

@ -0,0 +1,69 @@
use std::fmt::Debug;
use async_trait::async_trait;
use derive_more::{AsRef, Deref};
use derive_new::new;
use eyre::{Context, Result};
use hyperlane_base::MultisigCheckpointSyncer;
use hyperlane_core::{HyperlaneMessage, H256};
use crate::msg::metadata::BaseMetadataBuilder;
use super::base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata};
#[derive(Debug, Clone, Deref, new, AsRef)]
pub struct LegacyMultisigMetadataBuilder(BaseMetadataBuilder);
#[async_trait]
impl MultisigIsmMetadataBuilder for LegacyMultisigMetadataBuilder {
fn token_layout(&self) -> Vec<MetadataToken> {
vec![
MetadataToken::CheckpointRoot,
MetadataToken::CheckpointIndex,
MetadataToken::CheckpointMailbox,
MetadataToken::MerkleProof,
MetadataToken::Threshold,
MetadataToken::Signatures,
MetadataToken::Validators,
]
}
async fn fetch_metadata(
&self,
validators: &[H256],
threshold: u8,
message: &HyperlaneMessage,
checkpoint_syncer: &MultisigCheckpointSyncer,
) -> Result<Option<MultisigMetadata>> {
const CTX: &str = "When fetching LegacyMultisig metadata";
let highest_nonce = self.highest_known_nonce().await;
let Some(quorum_checkpoint) = checkpoint_syncer
.legacy_fetch_checkpoint_in_range(
validators,
threshold as usize,
message.nonce,
highest_nonce,
)
.await
.context(CTX)?
else {
return Ok(None);
};
let Some(proof) = self
.get_proof(message.nonce, quorum_checkpoint.checkpoint)
.await
.context(CTX)?
else {
return Ok(None);
};
Ok(Some(MultisigMetadata::new(
quorum_checkpoint.checkpoint,
quorum_checkpoint.signatures,
None,
Some(proof),
)))
}
}

@ -0,0 +1,61 @@
use std::fmt::Debug;
use async_trait::async_trait;
use derive_more::{AsRef, Deref};
use derive_new::new;
use eyre::{Context, Result};
use hyperlane_base::MultisigCheckpointSyncer;
use hyperlane_core::{HyperlaneMessage, H256};
use crate::msg::metadata::BaseMetadataBuilder;
use super::base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata};
#[derive(Debug, Clone, Deref, new, AsRef)]
pub struct MerkleRootMultisigMetadataBuilder(BaseMetadataBuilder);
#[async_trait]
impl MultisigIsmMetadataBuilder for MerkleRootMultisigMetadataBuilder {
fn token_layout(&self) -> Vec<MetadataToken> {
vec![
MetadataToken::CheckpointMailbox,
MetadataToken::CheckpointIndex,
MetadataToken::MessageId,
MetadataToken::MerkleProof,
MetadataToken::Signatures,
]
}
async fn fetch_metadata(
&self,
validators: &[H256],
threshold: u8,
message: &HyperlaneMessage,
checkpoint_syncer: &MultisigCheckpointSyncer,
) -> Result<Option<MultisigMetadata>> {
const CTX: &str = "When fetching MerkleRootMultisig metadata";
let highest_nonce = self.highest_known_nonce().await;
let Some(quorum_checkpoint) = checkpoint_syncer
.fetch_checkpoint_in_range(validators, threshold as usize, message.nonce, highest_nonce)
.await
.context(CTX)?
else {
return Ok(None);
};
let Some(proof) = self
.get_proof(message.nonce, quorum_checkpoint.checkpoint.checkpoint)
.await
.context(CTX)?
else {
return Ok(None);
};
Ok(Some(MultisigMetadata::new(
quorum_checkpoint.checkpoint.checkpoint,
quorum_checkpoint.signatures,
Some(quorum_checkpoint.checkpoint.message_id),
Some(proof),
)))
}
}

@ -0,0 +1,61 @@
use std::fmt::Debug;
use async_trait::async_trait;
use derive_more::{AsRef, Deref};
use derive_new::new;
use eyre::{Context, Result};
use hyperlane_base::MultisigCheckpointSyncer;
use hyperlane_core::{HyperlaneMessage, H256};
use tracing::warn;
use crate::msg::metadata::BaseMetadataBuilder;
use super::base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata};
#[derive(Debug, Clone, Deref, new, AsRef)]
pub struct MessageIdMultisigMetadataBuilder(BaseMetadataBuilder);
#[async_trait]
impl MultisigIsmMetadataBuilder for MessageIdMultisigMetadataBuilder {
fn token_layout(&self) -> Vec<MetadataToken> {
vec![
MetadataToken::CheckpointMailbox,
MetadataToken::CheckpointRoot,
MetadataToken::Signatures,
]
}
async fn fetch_metadata(
&self,
validators: &[H256],
threshold: u8,
message: &HyperlaneMessage,
checkpoint_syncer: &MultisigCheckpointSyncer,
) -> Result<Option<MultisigMetadata>> {
const CTX: &str = "When fetching MessageIdMultisig metadata";
let Some(quorum_checkpoint) = checkpoint_syncer
.fetch_checkpoint(validators, threshold as usize, message.nonce)
.await
.context(CTX)?
else {
return Ok(None);
};
if quorum_checkpoint.checkpoint.message_id != message.id() {
warn!(
"Quorum checkpoint message id {} does not match message id {}",
quorum_checkpoint.checkpoint.message_id,
message.id()
);
return Ok(None);
}
Ok(Some(MultisigMetadata::new(
quorum_checkpoint.checkpoint.checkpoint,
quorum_checkpoint.signatures,
None,
None,
)))
}
}

@ -0,0 +1,10 @@
mod base;
mod legacy_multisig;
mod merkle_root_multisig;
mod message_id_multisig;
pub use base::{MetadataToken, MultisigIsmMetadataBuilder, MultisigMetadata};
pub use legacy_multisig::LegacyMultisigMetadataBuilder;
pub use merkle_root_multisig::MerkleRootMultisigMetadataBuilder;
pub use message_id_multisig::MessageIdMultisigMetadataBuilder;

@ -269,7 +269,7 @@ impl Relayer {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let contract_sync = self.message_syncs.get(origin).unwrap().clone();
let cursor = contract_sync
.forward_backward_message_sync_cursor(index_settings)
.forward_backward_message_sync_cursor(index_settings.chunk_size)
.await;
tokio::spawn(async move {
contract_sync

@ -1,5 +1,6 @@
//! Configuration
use std::path::PathBuf;
use std::time::Duration;
use eyre::{eyre, Context};
@ -13,6 +14,8 @@ use hyperlane_core::HyperlaneDomain;
decl_settings!(Validator,
Parsed {
/// Database path
db: PathBuf,
/// Chain to validate messages on
origin_chain: HyperlaneDomain,
/// The validator attestation signer
@ -25,6 +28,8 @@ decl_settings!(Validator,
interval: Duration,
},
Raw {
/// Database path (path on the fs)
db: Option<String>,
// Name of the chain to validate message on
originchainname: Option<String>,
/// The validator attestation signer
@ -82,6 +87,15 @@ impl FromRawConf<'_, RawValidatorSettings> for ValidatorSettings {
.take_err(&mut err, || cwp + "originchainname")
else { return Err(err) };
let db = raw
.db
.and_then(|r| r.parse().take_err(&mut err, || cwp + "db"))
.unwrap_or_else(|| {
std::env::current_dir()
.unwrap()
.join(format!("validator_db_{origin_chain_name}"))
});
let base = raw
.base
.parse_config_with_filter::<Settings>(
@ -99,6 +113,7 @@ impl FromRawConf<'_, RawValidatorSettings> for ValidatorSettings {
err.into_result()?;
Ok(Self {
base: base.unwrap(),
db,
origin_chain: origin_chain.unwrap(),
validator: validator.unwrap(),
checkpoint_syncer: checkpoint_syncer.unwrap(),

@ -1,26 +1,31 @@
use std::num::NonZeroU64;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::vec;
use eyre::Result;
use hyperlane_base::db::HyperlaneRocksDB;
use hyperlane_core::accumulator::incremental::IncrementalMerkle;
use prometheus::IntGauge;
use tokio::{task::JoinHandle, time::sleep};
use tokio::time::sleep;
use tracing::instrument;
use tracing::{debug, error, info, info_span, instrument::Instrumented, warn, Instrument};
use tracing::{debug, info};
use hyperlane_base::{CheckpointSyncer, CoreMetrics};
use hyperlane_core::{
Announcement, HyperlaneDomain, HyperlaneSigner, HyperlaneSignerExt, Mailbox, ValidatorAnnounce,
H256, U256,
Checkpoint, CheckpointWithMessageId, HyperlaneChain, HyperlaneContract, HyperlaneDomain,
HyperlaneSigner, HyperlaneSignerExt, Mailbox,
};
#[derive(Clone)]
pub(crate) struct ValidatorSubmitter {
interval: Duration,
reorg_period: Option<NonZeroU64>,
signer: Arc<dyn HyperlaneSigner>,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
checkpoint_syncer: Arc<dyn CheckpointSyncer>,
message_db: HyperlaneRocksDB,
metrics: ValidatorSubmitterMetrics,
}
@ -29,90 +34,105 @@ impl ValidatorSubmitter {
interval: Duration,
reorg_period: u64,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
signer: Arc<dyn HyperlaneSigner>,
checkpoint_syncer: Arc<dyn CheckpointSyncer>,
message_db: HyperlaneRocksDB,
metrics: ValidatorSubmitterMetrics,
) -> Self {
Self {
reorg_period: NonZeroU64::new(reorg_period),
interval,
mailbox,
validator_announce,
signer,
checkpoint_syncer,
message_db,
metrics,
}
}
pub(crate) fn spawn(self) -> Instrumented<JoinHandle<Result<()>>> {
let span = info_span!("ValidatorSubmitter");
tokio::spawn(async move { self.main_task().await }).instrument(span)
}
async fn announce_task(&self) -> Result<()> {
// Sign and post the validator announcement
let announcement = Announcement {
validator: self.signer.eth_address(),
pub(crate) fn checkpoint(&self, tree: &IncrementalMerkle) -> Checkpoint {
Checkpoint {
root: tree.root(),
index: tree.index(),
mailbox_address: self.mailbox.address(),
mailbox_domain: self.mailbox.domain().id(),
storage_location: self.checkpoint_syncer.announcement_location(),
};
let signed_announcement = self.signer.sign(announcement.clone()).await?;
self.checkpoint_syncer
.write_announcement(&signed_announcement)
.await?;
// Ensure that the validator has announced themselves before we enter
// the main validator submit loop. This is to avoid a situation in
// which the validator is signing checkpoints but has not announced
// their locations, which makes them functionally unusable.
let validators: [H256; 1] = [self.signer.eth_address().into()];
loop {
info!("Checking for validator announcement");
if let Some(locations) = self
.validator_announce
.get_announced_storage_locations(&validators)
.await?
.first()
}
}
#[instrument(err, skip(self), fields(domain=%self.mailbox.domain()))]
pub(crate) async fn checkpoint_submitter(
self,
mut tree: IncrementalMerkle,
target_checkpoint: Option<Checkpoint>,
) -> Result<()> {
let mut checkpoint_queue = vec![];
let mut reached_target = false;
while !reached_target {
let correctness_checkpoint = if let Some(c) = target_checkpoint {
c
} else {
// lag by reorg period to match message indexing
let latest_checkpoint = self.mailbox.latest_checkpoint(self.reorg_period).await?;
self.metrics
.latest_checkpoint_observed
.set(latest_checkpoint.index as i64);
latest_checkpoint
};
// ingest available messages from DB
while let Some(message) = self
.message_db
.retrieve_message_by_nonce(tree.count() as u32)?
{
if locations.contains(&self.checkpoint_syncer.announcement_location()) {
info!("Validator has announced signature storage location");
break;
}
info!("Validator has not announced signature storage location");
let balance_delta = self
.validator_announce
.announce_tokens_needed(signed_announcement.clone())
.await?;
if balance_delta > U256::zero() {
warn!(
tokens_needed=%balance_delta,
validator_address=?announcement.validator,
"Please send tokens to the validator address to announce",
debug!(index = message.nonce, "Ingesting leaf to tree");
let message_id = message.id();
tree.ingest(message_id);
let checkpoint = self.checkpoint(&tree);
checkpoint_queue.push(CheckpointWithMessageId {
checkpoint,
message_id,
});
// compare against every queued checkpoint to prevent ingesting past target
if checkpoint == correctness_checkpoint {
debug!(
index = checkpoint.index,
"Reached tree consistency, signing queued checkpoints"
);
} else {
let outcome = self
.validator_announce
.announce(signed_announcement.clone(), None)
.await?;
if !outcome.executed {
error!(
hash=?outcome.txid,
"Transaction attempting to announce validator reverted"
);
// drain and sign all checkpoints in the queue
for queued_checkpoint in checkpoint_queue.drain(..) {
let signed_checkpoint = self.signer.sign(queued_checkpoint).await?;
self.checkpoint_syncer
.write_checkpoint(&signed_checkpoint)
.await?;
info!(index = queued_checkpoint.index, "Signed checkpoint");
}
self.metrics
.latest_checkpoint_processed
.set(checkpoint.index as i64);
// break out of submitter loop if target checkpoint is reached
reached_target = target_checkpoint.is_some();
break;
}
}
sleep(self.interval).await;
}
Ok(())
}
#[instrument(err, skip(self), fields(domain=%self.mailbox.domain()))]
async fn main_task(self) -> Result<()> {
self.announce_task().await?;
// TODO: remove this once validator is tolerant of tasks exiting
loop {
sleep(Duration::from_secs(u64::MAX)).await;
}
}
pub(crate) async fn legacy_checkpoint_submitter(self) -> Result<()> {
// Ensure that the mailbox has > 0 messages before we enter the main
// validator submit loop. This is to avoid an underflow / reverted
// call when we invoke the `mailbox.latest_checkpoint()` method,
@ -131,7 +151,7 @@ impl ValidatorSubmitter {
if let Some(current_index) = current_index {
self.metrics
.latest_checkpoint_processed
.legacy_latest_checkpoint_processed
.set(current_index as i64);
}
@ -152,13 +172,12 @@ impl ValidatorSubmitter {
true
};
info!(current_index = current_index, "Starting Validator");
loop {
// Check the latest checkpoint
let latest_checkpoint = self.mailbox.latest_checkpoint(self.reorg_period).await?;
self.metrics
.latest_checkpoint_observed
.legacy_latest_checkpoint_observed
.set(latest_checkpoint.index as i64);
// Occasional info to make it clear to a validator operator whether things are
@ -191,10 +210,10 @@ impl ValidatorSubmitter {
current_index = Some(latest_checkpoint.index);
self.checkpoint_syncer
.write_checkpoint(&signed_checkpoint)
.legacy_write_checkpoint(&signed_checkpoint)
.await?;
self.metrics
.latest_checkpoint_processed
.legacy_latest_checkpoint_processed
.set(signed_checkpoint.value.index as i64);
}
@ -203,15 +222,24 @@ impl ValidatorSubmitter {
}
}
#[derive(Clone)]
pub(crate) struct ValidatorSubmitterMetrics {
latest_checkpoint_observed: IntGauge,
latest_checkpoint_processed: IntGauge,
legacy_latest_checkpoint_observed: IntGauge,
legacy_latest_checkpoint_processed: IntGauge,
}
impl ValidatorSubmitterMetrics {
pub fn new(metrics: &CoreMetrics, mailbox_chain: &HyperlaneDomain) -> Self {
let chain_name = mailbox_chain.name();
Self {
legacy_latest_checkpoint_observed: metrics
.latest_checkpoint()
.with_label_values(&["legacy_validator_observed", chain_name]),
legacy_latest_checkpoint_processed: metrics
.latest_checkpoint()
.with_label_values(&["legacy_validator_processed", chain_name]),
latest_checkpoint_observed: metrics
.latest_checkpoint()
.with_label_values(&["validator_observed", chain_name]),

@ -3,11 +3,22 @@ use std::time::Duration;
use async_trait::async_trait;
use eyre::Result;
use tokio::task::JoinHandle;
use tracing::instrument::Instrumented;
use hyperlane_base::db::HyperlaneRocksDB;
use hyperlane_base::MessageContractSync;
use hyperlane_core::accumulator::incremental::IncrementalMerkle;
use std::num::NonZeroU64;
use tokio::{task::JoinHandle, time::sleep};
use tracing::{error, info, info_span, instrument::Instrumented, warn, Instrument};
use hyperlane_base::{run_all, BaseAgent, CheckpointSyncer, CoreMetrics, HyperlaneAgentCore};
use hyperlane_core::{HyperlaneDomain, HyperlaneSigner, Mailbox, ValidatorAnnounce};
use hyperlane_base::{
db::DB, run_all, BaseAgent, CheckpointSyncer, ContractSyncMetrics, CoreMetrics,
HyperlaneAgentCore,
};
use hyperlane_core::{
Announcement, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneSigner,
HyperlaneSignerExt, Mailbox, ValidatorAnnounce, H256, U256,
};
use crate::{
settings::ValidatorSettings, submit::ValidatorSubmitter, submit::ValidatorSubmitterMetrics,
@ -18,6 +29,8 @@ use crate::{
pub struct Validator {
origin_chain: HyperlaneDomain,
core: HyperlaneAgentCore,
db: HyperlaneRocksDB,
message_sync: Arc<MessageContractSync>,
mailbox: Arc<dyn Mailbox>,
validator_announce: Arc<dyn ValidatorAnnounce>,
signer: Arc<dyn HyperlaneSigner>,
@ -42,6 +55,9 @@ impl BaseAgent for Validator {
where
Self: Sized,
{
let db = DB::from_path(&settings.db)?;
let msg_db = HyperlaneRocksDB::new(&settings.origin_chain, db);
let signer = settings
.validator
// Intentionally using hyperlane_ethereum for the validator's signer
@ -53,17 +69,30 @@ impl BaseAgent for Validator {
let mailbox = settings
.build_mailbox(&settings.origin_chain, &metrics)
.await?
.into();
.await?;
let validator_announce = settings
.build_validator_announce(&settings.origin_chain, &metrics)
.await?;
let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&metrics));
let message_sync = settings
.build_message_indexer(
&settings.origin_chain,
&metrics,
&contract_sync_metrics,
Arc::new(msg_db.clone()),
)
.await?
.into();
Ok(Self {
origin_chain: settings.origin_chain,
core,
mailbox,
db: msg_db,
mailbox: mailbox.into(),
message_sync,
validator_announce: validator_announce.into(),
signer,
reorg_period: settings.reorg_period,
@ -74,17 +103,139 @@ impl BaseAgent for Validator {
#[allow(clippy::async_yields_async)]
async fn run(&self) -> Instrumented<JoinHandle<Result<()>>> {
let submit = ValidatorSubmitter::new(
self.announce().await.expect("Failed to announce validator");
let mut tasks = vec![];
tasks.push(self.run_message_sync().await);
for checkpoint_sync_task in self.run_checkpoint_submitters().await {
tasks.push(checkpoint_sync_task);
}
run_all(tasks)
}
}
impl Validator {
async fn run_message_sync(&self) -> Instrumented<JoinHandle<Result<()>>> {
let index_settings = self.as_ref().settings.chains[self.origin_chain.name()]
.index
.clone();
let contract_sync = self.message_sync.clone();
let cursor = contract_sync
.forward_backward_message_sync_cursor(index_settings.chunk_size)
.await;
tokio::spawn(async move {
contract_sync
.clone()
.sync("dispatched_messages", cursor)
.await
})
.instrument(info_span!("MailboxMessageSyncer"))
}
async fn run_checkpoint_submitters(&self) -> Vec<Instrumented<JoinHandle<Result<()>>>> {
let submitter = ValidatorSubmitter::new(
self.interval,
self.reorg_period,
self.mailbox.clone(),
self.validator_announce.clone(),
self.signer.clone(),
self.checkpoint_syncer.clone(),
self.db.clone(),
ValidatorSubmitterMetrics::new(&self.core.metrics, &self.origin_chain),
);
run_all(vec![submit.spawn()])
let empty_tree = IncrementalMerkle::default();
let lag = NonZeroU64::new(self.reorg_period);
let tip_tree = self
.mailbox
.tree(lag)
.await
.expect("failed to get mailbox tree");
let backfill_target = submitter.checkpoint(&tip_tree);
let mut tasks = vec![];
let backfill_submitter = submitter.clone();
let legacy_submitter = submitter.clone();
tasks.push(
tokio::spawn(async move {
backfill_submitter
.checkpoint_submitter(empty_tree, Some(backfill_target))
.await
})
.instrument(info_span!("BackfillCheckpointSubmitter")),
);
tasks.push(
tokio::spawn(async move { submitter.checkpoint_submitter(tip_tree, None).await })
.instrument(info_span!("TipCheckpointSubmitter")),
);
tasks.push(
tokio::spawn(async move { legacy_submitter.legacy_checkpoint_submitter().await })
.instrument(info_span!("LegacyCheckpointSubmitter")),
);
tasks
}
async fn announce(&self) -> Result<()> {
// Sign and post the validator announcement
let announcement = Announcement {
validator: self.signer.eth_address(),
mailbox_address: self.mailbox.address(),
mailbox_domain: self.mailbox.domain().id(),
storage_location: self.checkpoint_syncer.announcement_location(),
};
let signed_announcement = self.signer.sign(announcement.clone()).await?;
self.checkpoint_syncer
.write_announcement(&signed_announcement)
.await?;
// Ensure that the validator has announced themselves before we enter
// the main validator submit loop. This is to avoid a situation in
// which the validator is signing checkpoints but has not announced
// their locations, which makes them functionally unusable.
let validators: [H256; 1] = [self.signer.eth_address().into()];
loop {
info!("Checking for validator announcement");
if let Some(locations) = self
.validator_announce
.get_announced_storage_locations(&validators)
.await?
.first()
{
if locations.contains(&self.checkpoint_syncer.announcement_location()) {
info!("Validator has announced signature storage location");
break;
}
info!("Validator has not announced signature storage location");
let balance_delta = self
.validator_announce
.announce_tokens_needed(signed_announcement.clone())
.await?;
if balance_delta > U256::zero() {
warn!(
tokens_needed=%balance_delta,
validator_address=?announcement.validator,
"Please send tokens to the validator address to announce",
);
} else {
let outcome = self
.validator_announce
.announce(signed_announcement.clone(), None)
.await?;
if !outcome.executed {
error!(
hash=?outcome.txid,
"Transaction attempting to announce validator reverted"
);
}
}
}
sleep(self.interval).await;
}
Ok(())
}
}

@ -27,6 +27,7 @@ url.workspace = true
hyperlane-core = { path = "../../hyperlane-core" }
ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] }
num-traits.workspace = true
[build-dependencies]
abigen = { path = "../../utils/abigen", features = ["ethers"] }

@ -6,12 +6,13 @@ use std::sync::Arc;
use async_trait::async_trait;
use ethers::providers::Middleware;
use tracing::instrument;
use tracing::{instrument, warn};
use hyperlane_core::{
ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain,
HyperlaneProvider, InterchainSecurityModule, H256,
HyperlaneProvider, InterchainSecurityModule, ModuleType, H256,
};
use num_traits::cast::FromPrimitive;
use crate::contracts::i_interchain_security_module::{
IInterchainSecurityModule as EthereumInterchainSecurityModuleInternal,
@ -96,9 +97,14 @@ where
M: Middleware + 'static,
{
#[instrument(err, ret)]
async fn module_type(&self) -> ChainResult<u8> {
let module_type = self.contract.module_type().call().await?;
Ok(module_type)
async fn module_type(&self) -> ChainResult<ModuleType> {
let module = self.contract.module_type().call().await?;
if let Some(module_type) = ModuleType::from_u8(module) {
Ok(module_type)
} else {
warn!(%module, "Unknown module type");
Ok(ModuleType::Unused)
}
}
}

@ -9,6 +9,8 @@ use async_trait::async_trait;
use ethers::abi::AbiEncode;
use ethers::prelude::Middleware;
use ethers_contract::builders::ContractCall;
use hyperlane_core::accumulator::incremental::IncrementalMerkle;
use hyperlane_core::accumulator::TREE_DEPTH;
use tracing::instrument;
use hyperlane_core::{
@ -24,6 +26,9 @@ use crate::trait_builder::BuildableWithProvider;
use crate::tx::{fill_tx_gas_params, report_tx};
use crate::EthereumProvider;
/// derived from `forge inspect Mailbox storage --pretty`
const MERKLE_TREE_CONTRACT_SLOT: u32 = 152;
impl<M> std::fmt::Display for EthereumMailboxInternal<M>
where
M: Middleware,
@ -333,6 +338,67 @@ where
})
}
#[instrument(level = "debug", err, ret, skip(self))]
#[allow(clippy::needless_range_loop)]
async fn tree(&self, lag: Option<NonZeroU64>) -> ChainResult<IncrementalMerkle> {
let lag = lag.map(|v| v.get()).unwrap_or(0).into();
// use consistent block for all storage slot or view calls to prevent
// race conditions where tree contents change between calls
let fixed_block_number = self
.provider
.get_block_number()
.await
.map_err(ChainCommunicationError::from_other)?
.saturating_sub(lag)
.into();
let expected_root = self
.contract
.root()
.block(fixed_block_number)
.call()
.await?
.into();
// TODO: migrate to single contract view call once mailbox is upgraded
// see https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2250
// let branch = self.contract.branch().block(block_number).call().await;
let mut branch = [H256::zero(); TREE_DEPTH];
for index in 0..TREE_DEPTH {
let slot = U256::from(MERKLE_TREE_CONTRACT_SLOT) + index;
let mut location = [0u8; 32];
slot.to_big_endian(&mut location);
branch[index] = self
.provider
.get_storage_at(
self.contract.address(),
location.into(),
Some(fixed_block_number),
)
.await
.map_err(ChainCommunicationError::from_other)?;
}
let count = self
.contract
.count()
.block(fixed_block_number)
.call()
.await? as usize;
let tree = IncrementalMerkle::new(branch, count);
// validate tree built from storage slot lookups matches expected
// result from root() view call at consistent block
assert_eq!(tree.root(), expected_root);
Ok(tree)
}
#[instrument(err, ret, skip(self))]
async fn default_ism(&self) -> ChainResult<H256> {
Ok(self.contract.default_ism().call().await?.into())

@ -103,6 +103,21 @@ where
}
}
impl<M> EthereumProvider<M>
where
M: Middleware + 'static,
{
#[instrument(err, skip(self))]
async fn get_storage_at(&self, address: H256, location: H256) -> ChainResult<H256> {
let storage = self
.provider
.get_storage_at(H160::from(address), location, None)
.await
.map_err(ChainCommunicationError::from_other)?;
Ok(storage)
}
}
/// Builder for hyperlane providers.
pub struct HyperlaneProviderBuilder {}

@ -7,9 +7,10 @@ use fuels::prelude::{Bech32ContractId, WalletUnlocked};
use tracing::instrument;
use hyperlane_core::{
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProvider, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, H256, U256,
accumulator::incremental::IncrementalMerkle, utils::fmt_bytes, ChainCommunicationError,
ChainResult, Checkpoint, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract,
HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexer, LogMeta, Mailbox,
TxCostEstimate, TxOutcome, H256, U256,
};
use crate::{
@ -79,6 +80,11 @@ impl Mailbox for FuelMailbox {
.map_err(ChainCommunicationError::from_other)
}
#[instrument(level = "debug", err, ret, skip(self))]
async fn tree(&self, lag: Option<NonZeroU64>) -> ChainResult<IncrementalMerkle> {
todo!()
}
#[instrument(level = "debug", err, ret, skip(self))]
async fn delivered(&self, id: H256) -> ChainResult<bool> {
todo!()

@ -138,13 +138,13 @@ impl MessageContractSync {
/// Returns a new cursor to be used for syncing dispatched messages from the indexer
pub async fn forward_backward_message_sync_cursor(
&self,
index_settings: IndexSettings,
chunk_size: u32,
) -> Box<dyn ContractSyncCursor<HyperlaneMessage>> {
Box::new(
ForwardBackwardMessageSyncCursor::new(
self.indexer.clone(),
self.db.clone(),
index_settings.chunk_size,
chunk_size,
)
.await
.unwrap(),

@ -3,7 +3,7 @@ use std::fmt::Debug;
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint};
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint, SignedCheckpointWithMessageId};
/// A generic trait to read/write Checkpoints offchain
#[async_trait]
@ -11,9 +11,16 @@ pub trait CheckpointSyncer: Debug + Send + Sync {
/// Read the highest index of this Syncer
async fn latest_index(&self) -> Result<Option<u32>>;
/// Attempt to fetch the signed checkpoint at this index
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>>;
async fn legacy_fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>>;
/// Attempt to fetch the signed (checkpoint, messageId) tuple at this index
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpointWithMessageId>>;
/// Write the signed checkpoint to this syncer
async fn write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()>;
async fn legacy_write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()>;
/// Write the signed (checkpoint, messageId) tuple to this syncer
async fn write_checkpoint(
&self,
signed_checkpoint: &SignedCheckpointWithMessageId,
) -> Result<()>;
/// Write the signed announcement to this syncer
async fn write_announcement(&self, signed_announcement: &SignedAnnouncement) -> Result<()>;
/// Return the announcement storage location for this syncer

@ -4,7 +4,7 @@ use async_trait::async_trait;
use eyre::{Context, Result};
use prometheus::IntGauge;
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint};
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint, SignedCheckpointWithMessageId};
use crate::traits::CheckpointSyncer;
@ -30,10 +30,14 @@ impl LocalStorage {
Ok(Self { path, latest_index })
}
fn checkpoint_file_path(&self, index: u32) -> PathBuf {
fn legacy_checkpoint_file_path(&self, index: u32) -> PathBuf {
self.path.join(format!("{}.json", index))
}
fn checkpoint_file_path(&self, index: u32) -> PathBuf {
self.path.join(format!("{}_with_id.json", index))
}
fn latest_index_file_path(&self) -> PathBuf {
self.path.join("index.json")
}
@ -71,8 +75,8 @@ impl CheckpointSyncer for LocalStorage {
}
}
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>> {
match tokio::fs::read(self.checkpoint_file_path(index)).await {
async fn legacy_fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>> {
match tokio::fs::read(self.legacy_checkpoint_file_path(index)).await {
Ok(data) => {
let checkpoint = serde_json::from_slice(&data)?;
Ok(Some(checkpoint))
@ -81,9 +85,17 @@ impl CheckpointSyncer for LocalStorage {
}
}
async fn write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()> {
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpointWithMessageId>> {
let Ok(data) = tokio::fs::read(self.checkpoint_file_path(index)).await else {
return Ok(None)
};
let checkpoint = serde_json::from_slice(&data)?;
Ok(Some(checkpoint))
}
async fn legacy_write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()> {
let serialized_checkpoint = serde_json::to_string_pretty(signed_checkpoint)?;
let path = self.checkpoint_file_path(signed_checkpoint.value.index);
let path = self.legacy_checkpoint_file_path(signed_checkpoint.value.index);
tokio::fs::write(&path, &serialized_checkpoint)
.await
.with_context(|| format!("Writing checkpoint to {path:?}"))?;
@ -100,6 +112,19 @@ impl CheckpointSyncer for LocalStorage {
Ok(())
}
async fn write_checkpoint(
&self,
signed_checkpoint: &SignedCheckpointWithMessageId,
) -> Result<()> {
let serialized_checkpoint = serde_json::to_string_pretty(signed_checkpoint)?;
let path = self.checkpoint_file_path(signed_checkpoint.value.index);
tokio::fs::write(&path, &serialized_checkpoint)
.await
.with_context(|| format!("Writing (checkpoint, messageId) to {path:?}"))?;
Ok(())
}
async fn write_announcement(&self, signed_announcement: &SignedAnnouncement) -> Result<()> {
let serialized_announcement = serde_json::to_string_pretty(signed_announcement)?;
let path = self.announcement_file_path();

@ -6,7 +6,10 @@ use ethers::prelude::Address;
use eyre::Result;
use tracing::{debug, instrument, trace};
use hyperlane_core::{MultisigSignedCheckpoint, SignedCheckpointWithSigner, H160, H256};
use hyperlane_core::{
Checkpoint, CheckpointWithMessageId, MultisigSignedCheckpoint, SignedCheckpointWithSigner,
H160, H256,
};
use crate::CheckpointSyncer;
@ -32,16 +35,16 @@ impl MultisigCheckpointSyncer {
///
/// Note it's possible to not find a quorum.
#[instrument(err, skip(self))]
pub async fn fetch_checkpoint_in_range(
pub async fn legacy_fetch_checkpoint_in_range(
&self,
validators: &Vec<H256>,
validators: &[H256],
threshold: usize,
minimum_index: u32,
maximum_index: u32,
) -> Result<Option<MultisigSignedCheckpoint>> {
) -> Result<Option<MultisigSignedCheckpoint<Checkpoint>>> {
// Get the latest_index from each validator's checkpoint syncer.
let mut latest_indices = Vec::with_capacity(validators.len());
for validator in validators.iter() {
for validator in validators {
let address = H160::from(*validator);
if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&address) {
// Gracefully handle errors getting the latest_index
@ -56,7 +59,10 @@ impl MultisigCheckpointSyncer {
}
}
}
debug!(latest_indices=?latest_indices, "Fetched latest indices from checkpoint syncers");
debug!(
?latest_indices,
"Fetched latest indices from checkpoint syncers"
);
if latest_indices.is_empty() {
debug!("No validators returned a latest index");
@ -76,8 +82,9 @@ impl MultisigCheckpointSyncer {
return Ok(None);
}
for index in (minimum_index..=start_index).rev() {
if let Ok(Some(checkpoint)) =
self.fetch_checkpoint(index, validators, threshold).await
if let Ok(Some(checkpoint)) = self
.legacy_fetch_checkpoint(index, validators, threshold)
.await
{
return Ok(Some(checkpoint));
}
@ -90,25 +97,28 @@ impl MultisigCheckpointSyncer {
/// Fetches a MultisigSignedCheckpoint if there is a quorum.
/// Returns Ok(None) if there is no quorum.
#[instrument(err, skip(self))]
async fn fetch_checkpoint(
pub async fn legacy_fetch_checkpoint(
&self,
index: u32,
validators: &Vec<H256>,
validators: &[H256],
threshold: usize,
) -> Result<Option<MultisigSignedCheckpoint>> {
) -> Result<Option<MultisigSignedCheckpoint<Checkpoint>>> {
// Keeps track of signed validator checkpoints for a particular root.
// In practice, it's likely that validators will all sign the same root for a
// particular index, but we'd like to be robust to this not being the case
let mut signed_checkpoints_per_root: HashMap<H256, Vec<SignedCheckpointWithSigner>> =
HashMap::new();
let mut signed_checkpoints_per_root: HashMap<
H256,
Vec<SignedCheckpointWithSigner<Checkpoint>>,
> = HashMap::new();
for validator in validators.iter() {
for validator in validators {
let addr = H160::from(*validator);
if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&addr) {
// Gracefully ignore an error fetching the checkpoint from a validator's
// checkpoint syncer, which can happen if the validator has not
// signed the checkpoint at `index`.
if let Ok(Some(signed_checkpoint)) = checkpoint_syncer.fetch_checkpoint(index).await
if let Ok(Some(signed_checkpoint)) =
checkpoint_syncer.legacy_fetch_checkpoint(index).await
{
// If the signed checkpoint is for a different index, ignore it
if signed_checkpoint.value.index != index {
@ -138,6 +148,173 @@ impl MultisigCheckpointSyncer {
};
let root = signed_checkpoint_with_signer.signed_checkpoint.value.root;
let signature_count = match signed_checkpoints_per_root.entry(root) {
Entry::Occupied(mut entry) => {
let vec = entry.get_mut();
vec.push(signed_checkpoint_with_signer);
vec.len()
}
Entry::Vacant(entry) => {
entry.insert(vec![signed_checkpoint_with_signer]);
1 // length of 1
}
};
debug!(
validator = format!("{validator:#x}"),
index,
root = format!("{root:#x}"),
signature_count,
"Found signed checkpoint"
);
// If we've hit a quorum, create a MultisigSignedCheckpoint
if signature_count >= threshold {
if let Some(signed_checkpoints) = signed_checkpoints_per_root.get(&root) {
let checkpoint =
MultisigSignedCheckpoint::try_from(signed_checkpoints)?;
debug!(?checkpoint, "Fetched multisig checkpoint");
return Ok(Some(checkpoint));
}
}
} else {
debug!(
validator = format!("{validator:#x}"),
index = index,
"Unable to find signed checkpoint"
);
}
} else {
debug!(%validator, "Unable to find checkpoint syncer");
continue;
}
}
Ok(None)
}
/// Attempts to get the latest checkpoint with a quorum of signatures among
/// validators.
///
/// First iterates through the `latest_index` of each validator's checkpoint
/// syncer, looking for the highest index that >= `threshold` validators
/// have returned.
///
/// Attempts to find a quorum of signed checkpoints from that index,
/// iterating backwards if unsuccessful, until the (optional) index is
/// reached.
///
/// Note it's possible to not find a quorum.
#[instrument(err, skip(self))]
pub async fn fetch_checkpoint_in_range(
&self,
validators: &[H256],
threshold: usize,
minimum_index: u32,
maximum_index: u32,
) -> Result<Option<MultisigSignedCheckpoint<CheckpointWithMessageId>>> {
// Get the latest_index from each validator's checkpoint syncer.
let mut latest_indices = Vec::with_capacity(validators.len());
for validator in validators {
let address = H160::from(*validator);
if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&address) {
// Gracefully handle errors getting the latest_index
match checkpoint_syncer.latest_index().await {
Ok(Some(index)) => {
trace!(?address, ?index, "Validator returned latest index");
latest_indices.push(index);
}
err => {
debug!(?address, ?err, "Failed to get latest index from validator");
}
}
}
}
debug!(
?latest_indices,
"Fetched latest indices from checkpoint syncers"
);
if latest_indices.is_empty() {
debug!("No validators returned a latest index");
return Ok(None);
}
// Sort in descending order. The n'th index will represent
// the highest index for which we (supposedly) have (n+1) signed checkpoints
latest_indices.sort_by(|a, b| b.cmp(a));
if let Some(&highest_quorum_index) = latest_indices.get(threshold - 1) {
// The highest viable checkpoint index is the minimum of the highest index
// we (supposedly) have a quorum for, and the maximum index for which we can
// generate a proof.
let start_index = highest_quorum_index.min(maximum_index);
if minimum_index > start_index {
debug!(%start_index, %highest_quorum_index, "Highest quorum index is below the minimum index");
return Ok(None);
}
for index in (minimum_index..=start_index).rev() {
if let Ok(Some(checkpoint)) =
self.fetch_checkpoint(validators, threshold, index).await
{
return Ok(Some(checkpoint));
}
}
}
debug!("No checkpoint found in range");
Ok(None)
}
/// Fetches a MultisigSignedCheckpointWithMessageId if there is a quorum.
/// Returns Ok(None) if there is no quorum.
#[instrument(err, skip(self))]
pub async fn fetch_checkpoint(
&self,
validators: &[H256],
threshold: usize,
index: u32,
) -> Result<Option<MultisigSignedCheckpoint<CheckpointWithMessageId>>> {
// Keeps track of signed validator checkpoints for a particular root.
// In practice, it's likely that validators will all sign the same root for a
// particular index, but we'd like to be robust to this not being the case
let mut signed_checkpoints_per_root: HashMap<
H256,
Vec<SignedCheckpointWithSigner<CheckpointWithMessageId>>,
> = HashMap::new();
for validator in validators.iter() {
let addr = H160::from(*validator);
if let Some(checkpoint_syncer) = self.checkpoint_syncers.get(&addr) {
// Gracefully ignore an error fetching the checkpoint from a validator's
// checkpoint syncer, which can happen if the validator has not
// signed the checkpoint at `index`.
if let Ok(Some(signed_checkpoint)) = checkpoint_syncer.fetch_checkpoint(index).await
{
// If the signed checkpoint is for a different index, ignore it
if signed_checkpoint.value.index != index {
debug!(
validator = format!("{:#x}", validator),
index = index,
checkpoint_index = signed_checkpoint.value.index,
"Checkpoint index mismatch"
);
continue;
}
// Ensure that the signature is actually by the validator
let signer = signed_checkpoint.recover()?;
if H256::from(signer) != *validator {
debug!(
validator = format!("{:#x}", validator),
index = index,
"Checkpoint signature mismatch"
);
continue;
}
// Insert the SignedCheckpointWithSigner into signed_checkpoints_per_root
let signed_checkpoint_with_signer =
SignedCheckpointWithSigner::<CheckpointWithMessageId> {
signer,
signed_checkpoint,
};
let root = signed_checkpoint_with_signer.signed_checkpoint.value.root;
let signature_count = match signed_checkpoints_per_root.entry(root) {
Entry::Occupied(mut entry) => {
let vec = entry.get_mut();
@ -160,7 +337,9 @@ impl MultisigCheckpointSyncer {
if signature_count >= threshold {
if let Some(signed_checkpoints) = signed_checkpoints_per_root.get(&root) {
let checkpoint =
MultisigSignedCheckpoint::try_from(signed_checkpoints)?;
MultisigSignedCheckpoint::<CheckpointWithMessageId>::try_from(
signed_checkpoints,
)?;
debug!(checkpoint=?checkpoint, "Fetched multisig checkpoint");
return Ok(Some(checkpoint));
}

@ -14,7 +14,7 @@ use rusoto_s3::{GetObjectError, GetObjectRequest, PutObjectRequest, S3Client, S3
use tokio::time::timeout;
use crate::settings::aws_credentials::AwsChainCredentialsProvider;
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint};
use hyperlane_core::{SignedAnnouncement, SignedCheckpoint, SignedCheckpointWithMessageId};
use crate::CheckpointSyncer;
@ -120,8 +120,12 @@ impl S3Storage {
})
}
fn legacy_checkpoint_key(index: u32) -> String {
format!("checkpoint_{index}.json")
}
fn checkpoint_key(index: u32) -> String {
format!("checkpoint_{}.json", index)
format!("checkpoint_{index}_with_id.json")
}
fn index_key() -> String {
@ -152,7 +156,15 @@ impl CheckpointSyncer for S3Storage {
ret
}
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>> {
async fn legacy_fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpoint>> {
self.anonymously_read_from_bucket(S3Storage::legacy_checkpoint_key(index))
.await?
.map(|data| serde_json::from_slice(&data))
.transpose()
.map_err(Into::into)
}
async fn fetch_checkpoint(&self, index: u32) -> Result<Option<SignedCheckpointWithMessageId>> {
self.anonymously_read_from_bucket(S3Storage::checkpoint_key(index))
.await?
.map(|data| serde_json::from_slice(&data))
@ -160,10 +172,10 @@ impl CheckpointSyncer for S3Storage {
.map_err(Into::into)
}
async fn write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()> {
async fn legacy_write_checkpoint(&self, signed_checkpoint: &SignedCheckpoint) -> Result<()> {
let serialized_checkpoint = serde_json::to_string_pretty(signed_checkpoint)?;
self.write_to_bucket(
S3Storage::checkpoint_key(signed_checkpoint.value.index),
S3Storage::legacy_checkpoint_key(signed_checkpoint.value.index),
&serialized_checkpoint,
)
.await?;
@ -176,17 +188,27 @@ impl CheckpointSyncer for S3Storage {
Ok(())
}
async fn write_checkpoint(
&self,
signed_checkpoint: &SignedCheckpointWithMessageId,
) -> Result<()> {
let serialized_checkpoint = serde_json::to_string_pretty(signed_checkpoint)?;
self.write_to_bucket(
S3Storage::checkpoint_key(signed_checkpoint.value.index),
&serialized_checkpoint,
)
.await?;
Ok(())
}
async fn write_announcement(&self, signed_announcement: &SignedAnnouncement) -> Result<()> {
let serialized_announcement = serde_json::to_string_pretty(signed_announcement)?;
self.write_to_bucket(S3Storage::announcement_key(), &serialized_announcement)
.await?;
Ok(())
}
fn announcement_location(&self) -> String {
let mut location: String = "s3://".to_owned();
location.push_str(self.bucket.as_ref());
location.push('/');
location.push_str(self.region.name());
location
format!("s3://{}/{}", self.bucket, self.region.name())
}
}

@ -33,6 +33,7 @@ thiserror.workspace = true
# version determined by ethers-rs
primitive-types = "*"
lazy_static = "*"
derive_more.workspace = true
[dev-dependencies]
config.workspace = true

@ -1,10 +1,12 @@
use derive_new::new;
use crate::accumulator::{
hash_concat,
merkle::{merkle_root_from_branch, Proof},
H256, TREE_DEPTH, ZERO_HASHES,
};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, new)]
/// An incremental merkle tree, modeled on the eth2 deposit contract
pub struct IncrementalMerkle {
branch: [H256; TREE_DEPTH],
@ -61,6 +63,12 @@ impl IncrementalMerkle {
self.count
}
/// Get the index
pub fn index(&self) -> u32 {
assert!(self.count > 0, "index is invalid when tree is empty");
self.count as u32 - 1
}
/// Get the leading-edge branch.
pub fn branch(&self) -> &[H256; TREE_DEPTH] {
&self.branch

@ -2,14 +2,35 @@ use std::fmt::Debug;
use async_trait::async_trait;
use auto_impl::auto_impl;
use num_derive::FromPrimitive;
use strum::Display;
use crate::{ChainResult, HyperlaneContract};
/// Enumeration of all known module types
#[derive(FromPrimitive, Clone, Debug, Default, Display, Copy, PartialEq, Eq)]
pub enum ModuleType {
/// INVALID ISM
#[default]
Unused,
/// Routing ISM (defers to another ISM)
Routing,
/// Aggregation ISM (aggregates multiple ISMs)
Aggregation,
/// Legacy ISM (validators in calldata, set commitment in storage)
LegacyMultisig,
/// Merkle Proof ISM (batching and censorship resistance)
MerkleRootMultisig,
/// Message ID ISM (cheapest multisig with no batching)
MessageIdMultisig,
}
/// Interface for the InterchainSecurityModule chain contract. Allows abstraction over
/// different chains
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait InterchainSecurityModule: HyperlaneContract + Send + Sync + Debug {
/// Returns the validator and threshold needed to verify message
async fn module_type(&self) -> ChainResult<u8>;
/// Returns the module type of the ISM compliant with the corresponding
/// metadata offchain fetching and onchain formatting standard.
async fn module_type(&self) -> ChainResult<ModuleType>;
}

@ -5,8 +5,8 @@ use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::{
traits::TxOutcome, utils::domain_hash, ChainResult, Checkpoint, HyperlaneContract,
HyperlaneMessage, TxCostEstimate, H256, U256,
accumulator::incremental::IncrementalMerkle, traits::TxOutcome, utils::domain_hash,
ChainResult, Checkpoint, HyperlaneContract, HyperlaneMessage, TxCostEstimate, H256, U256,
};
/// Interface for the Mailbox chain contract. Allows abstraction over different
@ -19,6 +19,12 @@ pub trait Mailbox: HyperlaneContract + Send + Sync + Debug {
domain_hash(self.address(), self.domain().id())
}
/// Return the incremental merkle tree in storage
///
/// - `lag` is how far behind the current block to query, if not specified
/// it will query at the latest block.
async fn tree(&self, lag: Option<NonZeroU64>) -> ChainResult<IncrementalMerkle>;
/// Gets the current leaf count of the merkle tree
///
/// - `lag` is how far behind the current block to query, if not specified

@ -1,14 +1,13 @@
use async_trait::async_trait;
use derive_more::Deref;
use ethers_core::types::{Address, Signature};
use serde::{Deserialize, Serialize};
use sha3::{digest::Update, Digest, Keccak256};
use std::fmt::{Debug, Formatter};
use std::fmt::Debug;
use crate::utils::{fmt_address_for_domain, fmt_domain};
use crate::{utils::domain_hash, Signable, SignedType, H256};
/// An Hyperlane checkpoint
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
pub struct Checkpoint {
/// The mailbox address
pub mailbox_address: H256,
@ -20,20 +19,16 @@ pub struct Checkpoint {
pub index: u32,
}
impl Debug for Checkpoint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Checkpoint {{ mailbox_address: {}, mailbox_domain: {}, root: {:?}, index: {} }}",
fmt_address_for_domain(self.mailbox_domain, self.mailbox_address),
fmt_domain(self.mailbox_domain),
self.root,
self.index
)
}
/// A Hyperlane (checkpoint, messageId) tuple
#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Deref)]
pub struct CheckpointWithMessageId {
/// existing Hyperlane checkpoint struct
#[deref]
pub checkpoint: Checkpoint,
/// hash of message emitted from mailbox checkpoint.index
pub message_id: H256,
}
#[async_trait]
impl Signable for Checkpoint {
/// A hash of the checkpoint contents.
/// The EIP-191 compliant version of this hash is signed by validators.
@ -51,16 +46,36 @@ impl Signable for Checkpoint {
}
}
/// A checkpoint that has been signed.
impl Signable for CheckpointWithMessageId {
/// A hash of the checkpoint contents.
/// The EIP-191 compliant version of this hash is signed by validators.
fn signing_hash(&self) -> H256 {
// sign:
// domain_hash(mailbox_address, mailbox_domain) || root || index (as u32) || message_id
H256::from_slice(
Keccak256::new()
.chain(domain_hash(self.mailbox_address, self.mailbox_domain))
.chain(self.root)
.chain(self.index.to_be_bytes())
.chain(self.message_id)
.finalize()
.as_slice(),
)
}
}
/// Signed checkpoint
pub type SignedCheckpoint = SignedType<Checkpoint>;
/// Signed (checkpoint, messageId) tuple
pub type SignedCheckpointWithMessageId = SignedType<CheckpointWithMessageId>;
/// An individual signed checkpoint with the recovered signer
#[derive(Clone, Debug)]
pub struct SignedCheckpointWithSigner {
pub struct SignedCheckpointWithSigner<T: Signable> {
/// The recovered signer
pub signer: Address,
/// The signed checkpoint
pub signed_checkpoint: SignedCheckpoint,
pub signed_checkpoint: SignedType<T>,
}
/// A signature and its signer.
@ -73,25 +88,14 @@ pub struct SignatureWithSigner {
}
/// A checkpoint and multiple signatures
#[derive(Clone)]
pub struct MultisigSignedCheckpoint {
#[derive(Clone, Debug)]
pub struct MultisigSignedCheckpoint<T> {
/// The checkpoint
pub checkpoint: Checkpoint,
pub checkpoint: T,
/// Signatures over the checkpoint. No ordering guarantees.
pub signatures: Vec<SignatureWithSigner>,
}
impl Debug for MultisigSignedCheckpoint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MultisigSignedCheckpoint {{ checkpoint: {:?}, signature_count: {} }}",
self.checkpoint,
self.signatures.len()
)
}
}
/// Error types for MultisigSignedCheckpoint
#[derive(Debug, thiserror::Error)]
pub enum MultisigSignedCheckpointError {
@ -103,12 +107,16 @@ pub enum MultisigSignedCheckpointError {
EmptySignatures(),
}
impl TryFrom<&Vec<SignedCheckpointWithSigner>> for MultisigSignedCheckpoint {
impl<T: Signable + Eq + Copy> TryFrom<&Vec<SignedCheckpointWithSigner<T>>>
for MultisigSignedCheckpoint<T>
{
type Error = MultisigSignedCheckpointError;
/// Given multiple signed checkpoints with their signer, creates a
/// MultisigSignedCheckpoint
fn try_from(signed_checkpoints: &Vec<SignedCheckpointWithSigner>) -> Result<Self, Self::Error> {
fn try_from(
signed_checkpoints: &Vec<SignedCheckpointWithSigner<T>>,
) -> Result<Self, Self::Error> {
if signed_checkpoints.is_empty() {
return Err(MultisigSignedCheckpointError::EmptySignatures());
}
@ -124,7 +132,7 @@ impl TryFrom<&Vec<SignedCheckpointWithSigner>> for MultisigSignedCheckpoint {
let signatures = signed_checkpoints
.iter()
.map(|c| SignatureWithSigner {
.map(|c: &SignedCheckpointWithSigner<T>| SignatureWithSigner {
signature: c.signed_checkpoint.signature,
signer: c.signer,
})

@ -5,7 +5,7 @@ use std::num::NonZeroU64;
use async_trait::async_trait;
use mockall::*;
use hyperlane_core::*;
use hyperlane_core::{accumulator::incremental::IncrementalMerkle, *};
mock! {
pub MailboxContract {
@ -28,6 +28,8 @@ mock! {
nonce: usize,
) -> ChainResult<Option<H256>> {}
pub fn _tree(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<IncrementalMerkle> {}
pub fn _count(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<u32> {}
pub fn _latest_checkpoint(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<Checkpoint> {}
@ -70,6 +72,10 @@ impl Mailbox for MockMailboxContract {
self._count(maybe_lag)
}
async fn tree(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<IncrementalMerkle> {
self._tree(maybe_lag)
}
async fn latest_checkpoint(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<Checkpoint> {
self._latest_checkpoint(maybe_lag)
}

@ -150,11 +150,14 @@ fn main() -> ExitCode {
fs::create_dir_all(&log_dir).expect("Failed to make log dir");
}
let build_log = concat_path(&log_dir, "build.log");
let hardhat_log = concat_path(&log_dir, "hardhat.stdout.log");
let anvil_log = concat_path(&log_dir, "anvil.stdout.log");
let checkpoints_dirs = (0..3).map(|_| tempdir().unwrap()).collect::<Vec<_>>();
let rocks_db_dir = tempdir().unwrap();
let relayer_db = concat_path(&rocks_db_dir, "relayer");
let validator_dbs = (0..3)
.map(|i| concat_path(&rocks_db_dir, format!("validator{i}")))
.collect::<Vec<_>>();
let common_env = hashmap! {
"RUST_BACKTRACE" => "full",
@ -195,6 +198,7 @@ fn main() -> ExitCode {
"HYP_BASE_CHAINS_TEST2_CONNECTION_TYPE" => "httpFallback",
"HYP_BASE_CHAINS_TEST3_CONNECTION_URL" => "http://127.0.0.1:8545",
"HYP_BASE_METRICS" => metrics_port,
"HYP_BASE_DB" => validator_dbs[i].to_str().unwrap(),
"HYP_VALIDATOR_ORIGINCHAINNAME" => originchainname,
"HYP_VALIDATOR_VALIDATOR_KEY" => VALIDATOR_KEYS[i],
"HYP_VALIDATOR_REORGPERIOD" => "0",
@ -228,6 +232,9 @@ fn main() -> ExitCode {
.join(", ")
);
println!("Relayer DB in {}", relayer_db.display());
(0..3).for_each(|i| {
println!("Validator {} DB in {}", i + 1, validator_dbs[i].display());
});
let build_cmd = {
let build_log = make_static(build_log.to_str().unwrap().into());
@ -293,16 +300,14 @@ fn main() -> ExitCode {
state.build_log = build_log;
state.log_all = log_all;
println!("Launching hardhat...");
let mut node = Command::new("yarn");
node.args(["hardhat", "node"])
.current_dir("../typescript/infra");
println!("Launching anvil...");
let mut node = Command::new("anvil");
if log_all {
// TODO: should we log this? It seems way too verbose to be useful
// node.stdout(Stdio::piped());
node.stdout(Stdio::null());
} else {
node.stdout(append_to(hardhat_log));
node.stdout(append_to(anvil_log));
}
let node = node.spawn().expect("Failed to start node");
state.node = Some(node);
@ -352,25 +357,7 @@ fn main() -> ExitCode {
state.watchers.push(scraper_stderr);
state.scraper = Some(scraper);
for (i, validator_env) in validator_envs.iter().enumerate() {
let (validator, validator_stdout, validator_stderr) = run_agent(
"validator",
&common_env
.clone()
.into_iter()
.chain(validator_env.clone())
.collect(),
&[],
make_static(format!("VAL{}", 1 + i)),
log_all,
&log_dir,
);
state.watchers.push(validator_stdout);
state.watchers.push(validator_stderr);
state.validators.push(validator);
}
// Send half the kathy messages before the relayer comes up
// Send half the kathy messages before starting the agents
let mut kathy = Command::new("yarn");
kathy
.arg("kathy")
@ -389,6 +376,24 @@ fn main() -> ExitCode {
state.watchers.push(kathy_stderr);
kathy.wait().unwrap();
for (i, validator_env) in validator_envs.iter().enumerate() {
let (validator, validator_stdout, validator_stderr) = run_agent(
"validator",
&common_env
.clone()
.into_iter()
.chain(validator_env.clone())
.collect(),
&[],
make_static(format!("VAL{}", 1 + i)),
log_all,
&log_dir,
);
state.watchers.push(validator_stdout);
state.watchers.push(validator_stderr);
state.validators.push(validator);
}
let (relayer, relayer_stdout, relayer_stderr) = run_agent(
"relayer",
&relayer_env.into_iter().chain(common_env.clone()).collect(),
@ -413,6 +418,7 @@ fn main() -> ExitCode {
&(kathy_messages / 2).to_string(),
"--timeout",
"1000",
"--mineforever",
])
.current_dir("../typescript/infra")
.stdout(Stdio::piped())
@ -426,36 +432,19 @@ fn main() -> ExitCode {
let loop_start = Instant::now();
// give things a chance to fully start.
sleep(Duration::from_secs(5));
let mut kathy_done = false;
while RUNNING.fetch_and(true, Ordering::Relaxed) {
if !kathy_done {
// check if kathy has finished
match state.kathy.as_mut().unwrap().try_wait().unwrap() {
Some(s) if s.success() => {
kathy_done = true;
}
Some(_) => {
return ExitCode::from(1);
}
None => {}
}
}
if ci_mode {
// for CI we have to look for the end condition.
let num_messages_expected = (kathy_messages / 2) as u32 * 2;
if kathy_done && termination_invariants_met(num_messages_expected).unwrap_or(false) {
if termination_invariants_met(num_messages_expected).unwrap_or(false) {
// end condition reached successfully
println!("Kathy completed successfully and agent metrics look healthy");
println!("Agent metrics look healthy");
break;
} else if (Instant::now() - loop_start).as_secs() > ci_mode_timeout {
// we ran out of time
eprintln!("CI timeout reached before queues emptied and or kathy finished.");
eprintln!("CI timeout reached before queues emptied");
return ExitCode::from(1);
}
} else if kathy_done {
// when not in CI mode, run until kathy finishes, which should only happen if a
// number of rounds is specified.
break;
}
sleep(Duration::from_secs(5));
}

@ -1,25 +1,33 @@
AggregationIsmTest:testModulesAndThreshold(uint8,uint8,bytes32) (runs: 256, μ: 1539280, ~: 1314952)
AggregationIsmTest:testVerify(uint8,uint8,bytes32) (runs: 256, μ: 1564112, ~: 1339075)
AggregationIsmTest:testVerifyIncorrectMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1564050, ~: 1339762)
AggregationIsmTest:testVerifyMissingMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1556826, ~: 1332121)
AggregationIsmTest:testVerifyNoMetadataRequired(uint8,uint8,uint8,bytes32) (runs: 256, μ: 1700671, ~: 1305241)
GasRouterTest:testDispatchWithGas(uint256) (runs: 256, μ: 413789, ~: 413789)
AggregationIsmTest:testModulesAndThreshold(uint8,uint8,bytes32) (runs: 256, μ: 1556428, ~: 1334785)
AggregationIsmTest:testVerify(uint8,uint8,bytes32) (runs: 256, μ: 1581057, ~: 1358996)
AggregationIsmTest:testVerifyIncorrectMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1580974, ~: 1359683)
AggregationIsmTest:testVerifyMissingMetadata(uint8,uint8,bytes32) (runs: 256, μ: 1573730, ~: 1351998)
AggregationIsmTest:testVerifyNoMetadataRequired(uint8,uint8,uint8,bytes32) (runs: 256, μ: 1722168, ~: 1325061)
DomainRoutingIsmTest:testRoute(uint32,bytes32) (runs: 256, μ: 435158, ~: 435236)
DomainRoutingIsmTest:testSet(uint32) (runs: 256, μ: 421157, ~: 421157)
DomainRoutingIsmTest:testSetManyViaFactory(uint8,uint32) (runs: 256, μ: 40359815, ~: 29407180)
DomainRoutingIsmTest:testSetNonOwner(uint32,address) (runs: 256, μ: 11298, ~: 11298)
DomainRoutingIsmTest:testVerify(uint32,bytes32) (runs: 256, μ: 441253, ~: 441331)
DomainRoutingIsmTest:testVerifyNoIsm(uint32,bytes32) (runs: 256, μ: 444212, ~: 444212)
GasRouterTest:testDispatchWithGas(uint256) (runs: 256, μ: 347585, ~: 347585)
GasRouterTest:testQuoteGasPayment(uint256) (runs: 256, μ: 85818, ~: 85818)
GasRouterTest:testSetDestinationGas(uint256) (runs: 256, μ: 73808, ~: 75985)
InterchainAccountRouterTest:testCallRemoteWithDefault(bytes32) (runs: 256, μ: 595193, ~: 595504)
InterchainAccountRouterTest:testCallRemoteWithOverrides(bytes32) (runs: 256, μ: 499986, ~: 500297)
InterchainAccountRouterTest:testCallRemoteWithDefault(bytes32) (runs: 256, μ: 555673, ~: 556140)
InterchainAccountRouterTest:testCallRemoteWithFailingDefaultIsm(bytes32) (runs: 256, μ: 601813, ~: 602824)
InterchainAccountRouterTest:testCallRemoteWithFailingIsmOverride(bytes32) (runs: 256, μ: 619172, ~: 620105)
InterchainAccountRouterTest:testCallRemoteWithOverrides(bytes32) (runs: 256, μ: 460466, ~: 460933)
InterchainAccountRouterTest:testCallRemoteWithoutDefaults(bytes32) (runs: 256, μ: 20418, ~: 20418)
InterchainAccountRouterTest:testConstructor() (gas: 2577062)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsm(bytes32,bytes32) (runs: 256, μ: 109207, ~: 109207)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmImmutable(bytes32,bytes32,bytes32,bytes32) (runs: 256, μ: 106970, ~: 106970)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmNonOwner(address,bytes32,bytes32) (runs: 256, μ: 20291, ~: 20291)
InterchainAccountRouterTest:testEnrollRemoteRouters(uint8,uint32,bytes32) (runs: 256, μ: 3961640, ~: 3383014)
InterchainAccountRouterTest:testGetLocalInterchainAccount(bytes32) (runs: 256, μ: 508305, ~: 508616)
InterchainAccountRouterTest:testGetRemoteInterchainAccount() (gas: 120231)
InterchainAccountRouterTest:testOverrideAndCallRemote(bytes32) (runs: 256, μ: 595216, ~: 595527)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmImmutable(bytes32,bytes32,bytes32,bytes32) (runs: 256, μ: 106926, ~: 106926)
InterchainAccountRouterTest:testEnrollRemoteRouterAndIsmNonOwner(address,bytes32,bytes32) (runs: 256, μ: 20313, ~: 20313)
InterchainAccountRouterTest:testEnrollRemoteRouters(uint8,uint32,bytes32) (runs: 256, μ: 4050183, ~: 3316983)
InterchainAccountRouterTest:testGetLocalInterchainAccount(bytes32) (runs: 256, μ: 468785, ~: 469252)
InterchainAccountRouterTest:testGetRemoteInterchainAccount() (gas: 120253)
InterchainAccountRouterTest:testOverrideAndCallRemote(bytes32) (runs: 256, μ: 555674, ~: 556141)
InterchainAccountRouterTest:testReceiveValue(uint256) (runs: 256, μ: 109672, ~: 109672)
InterchainAccountRouterTest:testSendValue(uint256) (runs: 256, μ: 524292, ~: 524292)
InterchainAccountRouterTest:testSingleCallRemoteWithDefault(bytes32) (runs: 256, μ: 595909, ~: 596220)
InterchainAccountRouterTest:testSendValue(uint256) (runs: 256, μ: 484762, ~: 484840)
InterchainAccountRouterTest:testSingleCallRemoteWithDefault(bytes32) (runs: 256, μ: 556324, ~: 556791)
InterchainGasPaymasterTest:testClaim() (gas: 90675)
InterchainGasPaymasterTest:testConstructorSetsBeneficiary() (gas: 7648)
InterchainGasPaymasterTest:testGetExchangeRateAndGasPrice() (gas: 41743)
@ -35,22 +43,25 @@ InterchainGasPaymasterTest:testSetBeneficiary() (gas: 18694)
InterchainGasPaymasterTest:testSetBeneficiaryRevertsIfNotOwner() (gas: 11033)
InterchainGasPaymasterTest:testSetGasOracle() (gas: 40459)
InterchainGasPaymasterTest:testSetGasOracleRevertsIfNotOwner() (gas: 13783)
InterchainQueryRouterTest:testCannotCallbackReverting() (gas: 1450639)
InterchainQueryRouterTest:testCannotQueryReverting() (gas: 1118311)
InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1481496, ~: 1481496)
InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1665219, ~: 1665219)
InterchainQueryRouterTest:testSingleQueryAddress(address) (runs: 256, μ: 1481533, ~: 1481533)
LiquidityLayerRouterTest:testCannotSendToRecipientWithoutHandle() (gas: 662979)
LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 545350)
LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 551460)
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 28663)
InterchainQueryRouterTest:testCannotCallbackReverting() (gas: 1372627)
InterchainQueryRouterTest:testCannotQueryReverting() (gas: 1094371)
InterchainQueryRouterTest:testQueryAddress(address) (runs: 256, μ: 1383752, ~: 1383830)
InterchainQueryRouterTest:testQueryUint256(uint256) (runs: 256, μ: 1566435, ~: 1567679)
InterchainQueryRouterTest:testSingleQueryAddress(address) (runs: 256, μ: 1383789, ~: 1383867)
LiquidityLayerRouterTest:testCannotSendToRecipientWithoutHandle() (gas: 646167)
LiquidityLayerRouterTest:testDispatchWithTokenTransfersMovesTokens() (gas: 513057)
LiquidityLayerRouterTest:testDispatchWithTokensCallsAdapter() (gas: 519167)
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithFailedTransferIn() (gas: 29596)
LiquidityLayerRouterTest:testDispatchWithTokensRevertsWithUnkownBridgeAdapter() (gas: 20663)
LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 781605)
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 596435)
LiquidityLayerRouterTest:testSendToRecipientWithoutHandleWhenSpecifyingNoMessage() (gas: 1197693)
LiquidityLayerRouterTest:testDispatchWithTokensTransfersOnDestination() (gas: 745139)
LiquidityLayerRouterTest:testProcessingRevertsIfBridgeAdapterReverts() (gas: 578662)
LiquidityLayerRouterTest:testSendToRecipientWithoutHandleWhenSpecifyingNoMessage() (gas: 1161102)
LiquidityLayerRouterTest:testSetLiquidityLayerAdapter() (gas: 23363)
MessagingTest:testSendMessage(string) (runs: 256, μ: 277766, ~: 296095)
MultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 335532, ~: 327631)
MerkleRootMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 336856, ~: 330762)
MerkleRootMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 339564, ~: 331597)
MessageIdMultisigIsmTest:testFailVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 312768, ~: 307238)
MessageIdMultisigIsmTest:testVerify(uint32,bytes32,bytes,uint8,uint8,bytes32) (runs: 256, μ: 314834, ~: 307097)
MessagingTest:testSendMessage(string) (runs: 256, μ: 263049, ~: 278203)
OverheadIgpTest:testDestinationGasAmount() (gas: 33814)
OverheadIgpTest:testDestinationGasAmountWhenOverheadNotSet() (gas: 7912)
OverheadIgpTest:testInnerIgpSet() (gas: 7632)
@ -61,9 +72,9 @@ OverheadIgpTest:testSetDestinationGasAmountsNotOwner() (gas: 12018)
PausableReentrancyGuardTest:testNonreentrant() (gas: 9628)
PausableReentrancyGuardTest:testNonreentrantNotPaused() (gas: 14163)
PausableReentrancyGuardTest:testPause() (gas: 13635)
PortalAdapterTest:testAdapter(uint256) (runs: 256, μ: 135467, ~: 135583)
PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 256, μ: 140406, ~: 140522)
PortalAdapterTest:testReceivingWorks(uint256) (runs: 256, μ: 229403, ~: 229520)
PortalAdapterTest:testAdapter(uint256) (runs: 256, μ: 135466, ~: 135583)
PortalAdapterTest:testReceivingRevertsWithoutTransferCompletion(uint256) (runs: 256, μ: 140405, ~: 140522)
PortalAdapterTest:testReceivingWorks(uint256) (runs: 256, μ: 229401, ~: 229513)
StorageGasOracleTest:testConstructorSetsOwnership() (gas: 7611)
StorageGasOracleTest:testGetExchangeRateAndGasPrice() (gas: 12456)
StorageGasOracleTest:testGetExchangeRateAndGasPriceUnknownDomain() (gas: 8064)
@ -71,10 +82,10 @@ StorageGasOracleTest:testSetRemoteGasData() (gas: 38836)
StorageGasOracleTest:testSetRemoteGasDataConfigs() (gas: 69238)
StorageGasOracleTest:testSetRemoteGasDataConfigsRevertsIfNotOwner() (gas: 12227)
StorageGasOracleTest:testSetRemoteGasDataRevertsIfNotOwner() (gas: 11275)
TestQuerySenderTest:testSendAddressQuery(address) (runs: 256, μ: 1055196, ~: 1055274)
TestQuerySenderTest:testSendAddressQueryRequiresGasPayment() (gas: 363337)
TestQuerySenderTest:testSendBytesQuery(uint256) (runs: 256, μ: 1688469, ~: 1688625)
TestQuerySenderTest:testSendBytesQueryRequiresGasPayment() (gas: 363358)
TestQuerySenderTest:testSendUint256Query(uint256) (runs: 256, μ: 1688544, ~: 1688700)
TestQuerySenderTest:testSendUint256QueryRequiresGasPayment() (gas: 363325)
TestQuerySenderTest:testSendAddressQuery(address) (runs: 256, μ: 957141, ~: 957608)
TestQuerySenderTest:testSendAddressQueryRequiresGasPayment() (gas: 329870)
TestQuerySenderTest:testSendBytesQuery(uint256) (runs: 256, μ: 1590463, ~: 1591085)
TestQuerySenderTest:testSendBytesQueryRequiresGasPayment() (gas: 329891)
TestQuerySenderTest:testSendUint256Query(uint256) (runs: 256, μ: 1590538, ~: 1591160)
TestQuerySenderTest:testSendUint256QueryRequiresGasPayment() (gas: 329858)
ValidatorAnnounceTest:testAnnounce() (gas: 245554)

@ -7,7 +7,8 @@ interface IInterchainSecurityModule {
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MULTISIG
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG
}
/**

@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol";
import {MerkleRootMultisigIsmMetadata} from "../../libs/isms/MerkleRootMultisigIsmMetadata.sol";
import {Message} from "../../libs/Message.sol";
import {MerkleLib} from "../../libs/Merkle.sol";
import {CheckpointLib} from "../../libs/CheckpointLib.sol";
/**
* @title MerkleRootMultisigIsm
* @notice Provides abstract logic for verifying signatures on a merkle root
* and a merkle proof of message inclusion in that root.
* @dev Implement and use if you want strong censorship resistance guarantees.
* @dev May be adapted in future to support batch message verification against a single root.
*/
abstract contract AbstractMerkleRootMultisigIsm is AbstractMultisigIsm {
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.MERKLE_ROOT_MULTISIG);
/**
* @inheritdoc AbstractMultisigIsm
*/
function digest(bytes calldata _metadata, bytes calldata _message)
internal
pure
override
returns (bytes32)
{
// We verify a merkle proof of (messageId, index) I to compute root J
bytes32 _root = MerkleLib.branchRoot(
Message.id(_message),
MerkleRootMultisigIsmMetadata.proof(_metadata),
Message.nonce(_message)
);
// We provide (messageId, index) J in metadata for digest derivation
return
CheckpointLib.digest(
Message.origin(_message),
MerkleRootMultisigIsmMetadata.originMailbox(_metadata),
_root,
MerkleRootMultisigIsmMetadata.index(_metadata),
MerkleRootMultisigIsmMetadata.messageId(_metadata)
);
}
/**
* @inheritdoc AbstractMultisigIsm
*/
function signatureAt(bytes calldata _metadata, uint256 _index)
internal
pure
virtual
override
returns (bytes memory signature)
{
return MerkleRootMultisigIsmMetadata.signatureAt(_metadata, _index);
}
}

@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol";
import {MessageIdMultisigIsmMetadata} from "../../libs/isms/MessageIdMultisigIsmMetadata.sol";
import {Message} from "../../libs/Message.sol";
import {CheckpointLib} from "../../libs/CheckpointLib.sol";
/**
* @title AbstractMessageIdMultisigIsm
* @notice Provides abstract logic for verifying signatures on a message ID.
* @dev Implement and use if you want fastest and cheapest security.
*/
abstract contract AbstractMessageIdMultisigIsm is AbstractMultisigIsm {
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.MESSAGE_ID_MULTISIG);
/**
* @inheritdoc AbstractMultisigIsm
*/
function digest(bytes calldata _metadata, bytes calldata _message)
internal
pure
override
returns (bytes32)
{
return
CheckpointLib.digest(
Message.origin(_message),
MessageIdMultisigIsmMetadata.originMailbox(_metadata),
MessageIdMultisigIsmMetadata.root(_metadata),
Message.nonce(_message),
Message.id(_message)
);
}
/**
* @inheritdoc AbstractMultisigIsm
*/
function signatureAt(bytes calldata _metadata, uint256 _index)
internal
pure
virtual
override
returns (bytes memory)
{
return MessageIdMultisigIsmMetadata.signatureAt(_metadata, _index);
}
}

@ -8,22 +8,17 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {IMultisigIsm} from "../../interfaces/isms/IMultisigIsm.sol";
import {Message} from "../../libs/Message.sol";
import {MultisigIsmMetadata} from "../../libs/isms/MultisigIsmMetadata.sol";
import {CheckpointLib} from "../../libs/CheckpointLib.sol";
import {MerkleLib} from "../../libs/Merkle.sol";
/**
* @title MultisigIsm
* @notice Manages per-domain m-of-n Validator sets that are used to verify
* interchain messages.
* @dev See ./AbstractMerkleRootMultisigIsm.sol and ./AbstractMessageIdMultisigIsm.sol
* for concrete implementations of `digest` and `signatureAt`.
* @dev See ./StaticMultisigIsm.sol for concrete implementations.
*/
abstract contract AbstractMultisigIsm is IMultisigIsm {
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.MULTISIG);
// ============ Virtual Functions ============
// ======= OVERRIDE THESE TO IMPLEMENT =======
@ -41,74 +36,55 @@ abstract contract AbstractMultisigIsm is IMultisigIsm {
virtual
returns (address[] memory, uint8);
// ============ Public Functions ============
/**
* @notice Requires that m-of-n validators verify a merkle root,
* and verifies a merkle proof of `_message` against that root.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @notice Returns the digest to be used for signature verification.
* @param _metadata ABI encoded module metadata
* @param _message Formatted Hyperlane message (see Message.sol).
* @return digest The digest to be signed by validators
*/
function verify(bytes calldata _metadata, bytes calldata _message)
public
function digest(bytes calldata _metadata, bytes calldata _message)
internal
view
returns (bool)
{
require(_verifyMerkleProof(_metadata, _message), "!merkle");
require(_verifyValidatorSignatures(_metadata, _message), "!sigs");
return true;
}
// ============ Internal Functions ============
virtual
returns (bytes32);
/**
* @notice Verifies the merkle proof of `_message` against the provided
* checkpoint.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
* @notice Returns the signature at a given index from the metadata.
* @param _metadata ABI encoded module metadata
* @param _index The index of the signature to return
* @return signature Packed encoding of signature (65 bytes)
*/
function _verifyMerkleProof(
bytes calldata _metadata,
bytes calldata _message
) internal pure returns (bool) {
// calculate the expected root based on the proof
bytes32 _calculatedRoot = MerkleLib.branchRoot(
Message.id(_message),
MultisigIsmMetadata.proof(_metadata),
Message.nonce(_message)
);
return _calculatedRoot == MultisigIsmMetadata.root(_metadata);
}
function signatureAt(bytes calldata _metadata, uint256 _index)
internal
pure
virtual
returns (bytes memory);
// ============ Public Functions ============
/**
* @notice Verifies that a quorum of the origin domain's validators signed
* the provided checkpoint.
* @param _metadata ABI encoded module metadata (see MultisigIsmMetadata.sol)
* @notice Requires that m-of-n validators verify a merkle root,
* and verifies a merkle proof of `_message` against that root.
* @param _metadata ABI encoded module metadata
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function _verifyValidatorSignatures(
bytes calldata _metadata,
bytes calldata _message
) internal view returns (bool) {
function verify(bytes calldata _metadata, bytes calldata _message)
public
view
returns (bool)
{
bytes32 _digest = digest(_metadata, _message);
(
address[] memory _validators,
uint8 _threshold
) = validatorsAndThreshold(_message);
require(_threshold > 0, "No MultisigISM threshold present for message");
bytes32 _digest = CheckpointLib.digest(
Message.origin(_message),
MultisigIsmMetadata.originMailbox(_metadata),
MultisigIsmMetadata.root(_metadata),
MultisigIsmMetadata.index(_metadata)
);
uint256 _validatorCount = _validators.length;
uint256 _validatorIndex = 0;
// Assumes that signatures are ordered by validator
for (uint256 i = 0; i < _threshold; ++i) {
address _signer = ECDSA.recover(
_digest,
MultisigIsmMetadata.signatureAt(_metadata, i)
);
address _signer = ECDSA.recover(_digest, signatureAt(_metadata, i));
// Loop through remaining validators until we find a match
while (
_validatorIndex < _validatorCount &&

@ -12,7 +12,7 @@ import {Message} from "../../libs/Message.sol";
import {IMultisigIsm} from "../../interfaces/isms/IMultisigIsm.sol";
import {LegacyMultisigIsmMetadata} from "../../libs/isms/LegacyMultisigIsmMetadata.sol";
import {MerkleLib} from "../../libs/Merkle.sol";
import {CheckpointLib} from "../../libs/CheckpointLib.sol";
import {LegacyCheckpointLib} from "../../libs/LegacyCheckpointLib.sol";
/**
* @title MultisigIsm
@ -331,7 +331,7 @@ contract LegacyMultisigIsm is IMultisigIsm, Ownable {
// non-zero computed commitment, and this check will fail
// as the commitment in storage will be zero.
require(_commitment == commitment[_origin], "!commitment");
_digest = CheckpointLib.digest(
_digest = LegacyCheckpointLib.digest(
_origin,
LegacyMultisigIsmMetadata.originMailbox(_metadata),
LegacyMultisigIsmMetadata.root(_metadata),

@ -1,33 +1,68 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {AbstractMultisigIsm} from "./AbstractMultisigIsm.sol";
import {MultisigIsmMetadata} from "../../libs/isms/MultisigIsmMetadata.sol";
import {AbstractMerkleRootMultisigIsm} from "./AbstractMerkleRootMultisigIsm.sol";
import {AbstractMessageIdMultisigIsm} from "./AbstractMessageIdMultisigIsm.sol";
import {MetaProxy} from "../../libs/MetaProxy.sol";
import {StaticMOfNAddressSetFactory} from "../../libs/StaticMOfNAddressSetFactory.sol";
/**
* @title StaticMultisigIsm
* @notice Manages per-domain m-of-n Validator sets that are used
* @title AbstractMetaProxyMultisigIsm
* @notice Manages per-domain m-of-n Validator set that is used
* to verify interchain messages.
*/
contract StaticMultisigIsm is AbstractMultisigIsm {
// ============ Public Functions ============
abstract contract AbstractMetaProxyMultisigIsm is AbstractMultisigIsm {
/**
* @notice Returns the set of validators responsible for verifying _message
* and the number of signatures required
* @dev Can change based on the content of _message
* @return validators The array of validator addresses
* @return threshold The number of validator signatures needed
* @inheritdoc AbstractMultisigIsm
*/
function validatorsAndThreshold(bytes calldata)
public
view
virtual
pure
override
returns (address[] memory, uint8)
{
return abi.decode(MetaProxy.metadata(), (address[], uint8));
}
}
// solhint-disable no-empty-blocks
/**
* @title StaticMerkleRootMultisigIsm
* @notice Manages per-domain m-of-n validator set that is used
* to verify interchain messages using a merkle root signature quorum
* and merkle proof of inclusion.
*/
contract StaticMerkleRootMultisigIsm is
AbstractMerkleRootMultisigIsm,
AbstractMetaProxyMultisigIsm
{
}
/**
* @title StaticMessageIdMultisigIsm
* @notice Manages per-domain m-of-n validator set that is used
* to verify interchain messages using a message ID signature quorum.
*/
contract StaticMessageIdMultisigIsm is
AbstractMessageIdMultisigIsm,
AbstractMetaProxyMultisigIsm
{
}
// solhint-enable no-empty-blocks
contract StaticMerkleRootMultisigIsmFactory is StaticMOfNAddressSetFactory {
function _deployImplementation() internal override returns (address) {
return address(new StaticMerkleRootMultisigIsm());
}
}
contract StaticMessageIdMultisigIsmFactory is StaticMOfNAddressSetFactory {
function _deployImplementation() internal override returns (address) {
return address(new StaticMessageIdMultisigIsm());
}
}

@ -1,16 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {StaticMultisigIsm} from "./StaticMultisigIsm.sol";
import {StaticMOfNAddressSetFactory} from "../../libs/StaticMOfNAddressSetFactory.sol";
contract StaticMultisigIsmFactory is StaticMOfNAddressSetFactory {
function _deployImplementation()
internal
virtual
override
returns (address)
{
return address(new StaticMultisigIsm());
}
}

@ -11,6 +11,7 @@ import {IRoutingIsm} from "../../interfaces/isms/IRoutingIsm.sol";
abstract contract AbstractRoutingIsm is IRoutingIsm {
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.ROUTING);

@ -4,18 +4,25 @@ pragma solidity >=0.8.0;
// ============ External Imports ============
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {LegacyCheckpointLib} from "./LegacyCheckpointLib.sol";
library CheckpointLib {
/**
* @notice Returns the digest validators are expected to sign when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @param _checkpointRoot The root of the checkpoint.
* @param _checkpointIndex The index of the checkpoint.
* @param _messageId The message ID of the checkpoint.
* @dev Message ID must match leaf content of checkpoint root at index.
* @return The digest of the checkpoint.
*/
function digest(
uint32 _origin,
bytes32 _originMailbox,
bytes32 _checkpointRoot,
uint32 _checkpointIndex
uint32 _checkpointIndex,
bytes32 _messageId
) internal pure returns (bytes32) {
bytes32 _domainHash = domainHash(_origin, _originMailbox);
return
@ -24,7 +31,8 @@ library CheckpointLib {
abi.encodePacked(
_domainHash,
_checkpointRoot,
_checkpointIndex
_checkpointIndex,
_messageId
)
)
);

@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ External Imports ============
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
// ============ Internal Imports ============
import {CheckpointLib} from "./CheckpointLib.sol";
library LegacyCheckpointLib {
/**
* @notice Returns the digest validators are expected to sign when signing legacy checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @return The digest of the legacy checkpoint.
*/
function digest(
uint32 _origin,
bytes32 _originMailbox,
bytes32 _checkpointRoot,
uint32 _checkpointIndex
) internal pure returns (bytes32) {
bytes32 _domainHash = domainHash(_origin, _originMailbox);
return
ECDSA.toEthSignedMessageHash(
keccak256(
abi.encodePacked(
_domainHash,
_checkpointRoot,
_checkpointIndex
)
)
);
}
/**
* @notice Returns the domain hash that validators are expected to use
* when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @return The domain hash.
*/
function domainHash(uint32 _origin, bytes32 _originMailbox)
internal
pure
returns (bytes32)
{
return CheckpointLib.domainHash(_origin, _originMailbox);
}
}

@ -124,13 +124,14 @@ library MerkleLib {
**/
function branchRoot(
bytes32 _item,
bytes32[TREE_DEPTH] memory _branch,
bytes32[TREE_DEPTH] memory _branch, // cheaper than calldata indexing
uint256 _index
) internal pure returns (bytes32 _current) {
_current = _item;
for (uint256 i = 0; i < TREE_DEPTH; i++) {
uint256 _ithBit = (_index >> i) & 0x01;
// cheaper than calldata indexing _branch[i*32:(i+1)*32];
bytes32 _next = _branch[i];
if (_ithBit == 1) {
_current = keccak256(abi.encodePacked(_next, _current));

@ -3,27 +3,35 @@ pragma solidity >=0.8.0;
/**
* Format of metadata:
* [ 0: 32] Merkle root
* [ 32: 36] Root index
* [ 36: 68] Origin mailbox address
* [ 0: 32] Origin mailbox address
* [ 32: 36] Signed checkpoint index
* [ 36: 68] Signed checkpoint message ID
* [ 68:1092] Merkle proof
* [1092:????] Validator signatures, 65 bytes each, length == Threshold
* [1092:????] Validator signatures (length := threshold * 65)
*/
library MultisigIsmMetadata {
uint256 private constant MERKLE_ROOT_OFFSET = 0;
uint256 private constant MERKLE_INDEX_OFFSET = 32;
uint256 private constant ORIGIN_MAILBOX_OFFSET = 36;
uint256 private constant MERKLE_PROOF_OFFSET = 68;
uint256 private constant SIGNATURES_OFFSET = 1092;
uint256 private constant SIGNATURE_LENGTH = 65;
library MerkleRootMultisigIsmMetadata {
uint8 private constant ORIGIN_MAILBOX_OFFSET = 0;
uint8 private constant CHECKPOINT_INDEX_OFFSET = 32;
uint8 private constant CHECKPOINT_MESSAGE_ID_OFFSET = 36;
uint8 private constant MERKLE_PROOF_OFFSET = 68;
uint16 private constant MERKLE_PROOF_LENGTH = 32 * 32;
uint16 private constant SIGNATURES_OFFSET = 1092;
uint8 private constant SIGNATURE_LENGTH = 65;
/**
* @notice Returns the merkle root of the signed checkpoint.
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Merkle root of the signed checkpoint
* @return Origin mailbox of the signed checkpoint as bytes32
*/
function root(bytes calldata _metadata) internal pure returns (bytes32) {
return bytes32(_metadata[MERKLE_ROOT_OFFSET:MERKLE_INDEX_OFFSET]);
function originMailbox(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return
bytes32(
_metadata[ORIGIN_MAILBOX_OFFSET:ORIGIN_MAILBOX_OFFSET + 32]
);
}
/**
@ -34,21 +42,28 @@ library MultisigIsmMetadata {
function index(bytes calldata _metadata) internal pure returns (uint32) {
return
uint32(
bytes4(_metadata[MERKLE_INDEX_OFFSET:ORIGIN_MAILBOX_OFFSET])
bytes4(
_metadata[CHECKPOINT_INDEX_OFFSET:CHECKPOINT_INDEX_OFFSET +
4]
)
);
}
/**
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @notice Returns the message ID of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Origin mailbox of the signed checkpoint as bytes32
* @return Message ID of the signed checkpoint
*/
function originMailbox(bytes calldata _metadata)
function messageId(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return bytes32(_metadata[ORIGIN_MAILBOX_OFFSET:MERKLE_PROOF_OFFSET]);
return
bytes32(
_metadata[CHECKPOINT_MESSAGE_ID_OFFSET:CHECKPOINT_MESSAGE_ID_OFFSET +
32]
);
}
/**
@ -65,7 +80,8 @@ library MultisigIsmMetadata {
{
return
abi.decode(
_metadata[MERKLE_PROOF_OFFSET:SIGNATURES_OFFSET],
_metadata[MERKLE_PROOF_OFFSET:MERKLE_PROOF_OFFSET +
MERKLE_PROOF_LENGTH],
(bytes32[32])
);
}

@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/**
* Format of metadata:
* [ 0: 32] Origin mailbox address
* [ 32: 64] Signed checkpoint root
* [ 64:????] Validator signatures (length := threshold * 65)
*/
library MessageIdMultisigIsmMetadata {
uint8 private constant ORIGIN_MAILBOX_OFFSET = 0;
uint8 private constant MERKLE_ROOT_OFFSET = 32;
uint8 private constant SIGNATURES_OFFSET = 64;
uint8 private constant SIGNATURE_LENGTH = 65;
/**
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Origin mailbox of the signed checkpoint as bytes32
*/
function originMailbox(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return
bytes32(
_metadata[ORIGIN_MAILBOX_OFFSET:ORIGIN_MAILBOX_OFFSET + 32]
);
}
/**
* @notice Returns the merkle root of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Merkle root of the signed checkpoint
*/
function root(bytes calldata _metadata) internal pure returns (bytes32) {
return bytes32(_metadata[MERKLE_ROOT_OFFSET:MERKLE_ROOT_OFFSET + 32]);
}
/**
* @notice Returns the validator ECDSA signature at `_index`.
* @dev Assumes signatures are sorted by validator
* @dev Assumes `_metadata` encodes `threshold` signatures.
* @dev Assumes `_index` is less than `threshold`
* @param _metadata ABI encoded Multisig ISM metadata.
* @param _index The index of the signature to return.
* @return The validator ECDSA signature at `_index`.
*/
function signatureAt(bytes calldata _metadata, uint256 _index)
internal
pure
returns (bytes calldata)
{
uint256 _start = SIGNATURES_OFFSET + (_index * SIGNATURE_LENGTH);
uint256 _end = _start + SIGNATURE_LENGTH;
return _metadata[_start:_end];
}
}

@ -3,7 +3,7 @@ pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {LegacyMultisigIsm} from "../isms/multisig/LegacyMultisigIsm.sol";
import {CheckpointLib} from "../libs/CheckpointLib.sol";
import {LegacyCheckpointLib} from "../libs/LegacyCheckpointLib.sol";
contract TestLegacyMultisigIsm is LegacyMultisigIsm {
function getDomainHash(uint32 _origin, bytes32 _originMailbox)
@ -11,6 +11,6 @@ contract TestLegacyMultisigIsm is LegacyMultisigIsm {
pure
returns (bytes32)
{
return CheckpointLib.domainHash(_origin, _originMailbox);
return LegacyCheckpointLib.domainHash(_origin, _originMailbox);
}
}

@ -7,7 +7,7 @@ import {IMultisigIsm} from "../interfaces/isms/IMultisigIsm.sol";
contract TestMultisigIsm is IMultisigIsm {
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.MULTISIG);
uint8(IInterchainSecurityModule.Types.MERKLE_ROOT_MULTISIG);
bool public accept;

@ -5,28 +5,61 @@ import "forge-std/Test.sol";
import {IMultisigIsm} from "../../contracts/interfaces/isms/IMultisigIsm.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {StaticMultisigIsmFactory} from "../../contracts/isms/multisig/StaticMultisigIsmFactory.sol";
import {StaticMerkleRootMultisigIsmFactory, StaticMessageIdMultisigIsmFactory} from "../../contracts/isms/multisig/StaticMultisigIsm.sol";
import {MerkleRootMultisigIsmMetadata} from "../../contracts/libs/isms/MerkleRootMultisigIsmMetadata.sol";
import {CheckpointLib} from "../../contracts/libs/CheckpointLib.sol";
import {StaticMOfNAddressSetFactory} from "../../contracts/libs/StaticMOfNAddressSetFactory.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {MOfNTestUtils} from "./IsmTestUtils.sol";
contract MultisigIsmTest is Test {
abstract contract AbstractMultisigIsmTest is Test {
using Message for bytes;
uint32 constant ORIGIN = 11;
StaticMultisigIsmFactory factory;
StaticMOfNAddressSetFactory factory;
IMultisigIsm ism;
TestMailbox mailbox;
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
factory = new StaticMultisigIsmFactory();
function metadataPrefix(bytes memory message)
internal
view
virtual
returns (bytes memory);
function getMetadata(
uint8 m,
uint8 n,
bytes32 seed,
bytes memory message
) internal returns (bytes memory) {
uint32 domain = mailbox.localDomain();
uint256[] memory keys = addValidators(m, n, seed);
uint256[] memory signers = MOfNTestUtils.choose(m, keys, seed);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
bytes32 checkpointRoot = mailbox.root();
uint32 checkpointIndex = uint32(mailbox.count() - 1);
bytes32 messageId = message.id();
bytes32 digest = CheckpointLib.digest(
domain,
mailboxAsBytes32,
checkpointRoot,
checkpointIndex,
messageId
);
bytes memory metadata = metadataPrefix(message);
for (uint256 i = 0; i < m; i++) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i], digest);
metadata = abi.encodePacked(metadata, r, s, v);
}
return metadata;
}
function addValidators(
uint8 m,
uint8 n,
bytes32 seed
) private returns (uint256[] memory) {
) internal returns (uint256[] memory) {
uint256[] memory keys = new uint256[](n);
address[] memory addresses = new address[](n);
for (uint256 i = 0; i < n; i++) {
@ -60,37 +93,21 @@ contract MultisigIsmTest is Test {
return message;
}
function getMetadata(
function testVerify(
uint32 destination,
bytes32 recipient,
bytes calldata body,
uint8 m,
uint8 n,
bytes32 seed
) private returns (bytes memory) {
uint32 domain = mailbox.localDomain();
uint256[] memory keys = addValidators(m, n, seed);
uint256[] memory signers = MOfNTestUtils.choose(m, keys, seed);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
bytes32 checkpointRoot = mailbox.root();
uint32 checkpointIndex = uint32(mailbox.count() - 1);
bytes memory metadata = abi.encodePacked(
checkpointRoot,
checkpointIndex,
mailboxAsBytes32,
mailbox.proof()
);
bytes32 digest = CheckpointLib.digest(
domain,
mailboxAsBytes32,
checkpointRoot,
checkpointIndex
);
for (uint256 i = 0; i < signers.length; i++) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i], digest);
metadata = abi.encodePacked(metadata, r, s, v);
}
return metadata;
) public {
vm.assume(0 < m && m <= n && n < 10);
bytes memory message = getMessage(destination, recipient, body);
bytes memory metadata = getMetadata(m, n, seed, message);
assertTrue(ism.verify(metadata, message));
}
function testVerify(
function testFailVerify(
uint32 destination,
bytes32 recipient,
bytes calldata body,
@ -100,7 +117,56 @@ contract MultisigIsmTest is Test {
) public {
vm.assume(0 < m && m <= n && n < 10);
bytes memory message = getMessage(destination, recipient, body);
bytes memory metadata = getMetadata(m, n, seed);
assertTrue(ism.verify(metadata, message));
bytes memory metadata = getMetadata(m, n, seed, message);
// changing single byte in metadata should fail signature verification
uint256 index = uint256(seed) % metadata.length;
metadata[index] = ~metadata[index];
assertFalse(ism.verify(metadata, message));
}
}
contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
using Message for bytes;
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
factory = new StaticMerkleRootMultisigIsmFactory();
}
function metadataPrefix(bytes memory message)
internal
view
override
returns (bytes memory)
{
uint32 checkpointIndex = uint32(mailbox.count() - 1);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return
abi.encodePacked(
mailboxAsBytes32,
checkpointIndex,
message.id(),
mailbox.proof()
);
}
}
contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest {
using Message for bytes;
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
factory = new StaticMessageIdMultisigIsmFactory();
}
function metadataPrefix(bytes memory)
internal
view
override
returns (bytes memory)
{
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return abi.encodePacked(mailboxAsBytes32, mailbox.root());
}
}

@ -4,18 +4,18 @@ import { ChainMap, ModuleType, MultisigIsmConfig } from '@hyperlane-xyz/sdk';
export const multisigIsm: ChainMap<MultisigIsmConfig> = {
// Validators are anvil accounts 4-6
test1: {
type: ModuleType.MULTISIG,
validators: ['0x15d34aaf54267db7d7c367839aaf71a00a2c6a65'],
type: ModuleType.LEGACY_MULTISIG,
validators: ['0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65'],
threshold: 1,
},
test2: {
type: ModuleType.MULTISIG,
validators: ['0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc'],
type: ModuleType.MERKLE_ROOT_MULTISIG,
validators: ['0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'],
threshold: 1,
},
test3: {
type: ModuleType.MULTISIG,
validators: ['0x976ea74026e726554db657fa54763abd0c3a0aa9'],
type: ModuleType.MESSAGE_ID_MULTISIG,
validators: ['0x976EA74026E726554dB657fA54763abd0C3a0aa9'],
threshold: 1,
},
};

@ -38,9 +38,10 @@ task('kathy', 'Dispatches random hyperlane messages')
'0',
)
.addParam('timeout', 'Time to wait between messages in ms.', '5000')
.addFlag('mineforever', 'Mine forever after sending messages')
.setAction(
async (
taskArgs: { messages: string; timeout: string },
taskArgs: { messages: string; timeout: string; mineforever: boolean },
hre: HardhatRuntimeEnvironment,
) => {
const timeout = Number.parseInt(taskArgs.timeout);
@ -59,6 +60,10 @@ task('kathy', 'Dispatches random hyperlane messages')
const recipient = await recipientF.deploy();
await recipient.deployTransaction.wait();
const isAutomine: boolean = await hre.network.provider.send(
'hardhat_getAutomine',
);
// Generate artificial traffic
let messages = Number.parseInt(taskArgs.messages) || 0;
const run_forever = messages === 0;
@ -81,6 +86,7 @@ task('kathy', 'Dispatches random hyperlane messages')
// so gas estimation may sometimes be incorrect. Just avoid
// estimation to avoid this.
gasLimit: 150_000,
gasPrice: 2_000_000_000,
},
);
console.log(
@ -89,6 +95,13 @@ task('kathy', 'Dispatches random hyperlane messages')
} on ${local} with nonce ${(await mailbox.count()) - 1}`,
);
console.log(await chainSummary(core, local));
console.log(await chainSummary(core, remote));
await sleep(timeout);
}
while (taskArgs.mineforever && isAutomine) {
await hre.network.provider.send('hardhat_mine', ['0x01']);
await sleep(timeout);
}
},

@ -1,38 +1,41 @@
{
"test1": {
"storageGasOracle": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
"validatorAnnounce": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"proxyAdmin": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
"mailbox": "0x0165878A594ca255338adfa4d48449f69242Eb8F",
"interchainGasPaymaster": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
"defaultIsmInterchainGasPaymaster": "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
"multisigIsm": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"multisigIsmFactory": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"aggregationIsmFactory": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
"routingIsmFactory": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
"storageGasOracle": "0x36C02dA8a0983159322a80FFE9F24b1acfF8B570",
"validatorAnnounce": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
"proxyAdmin": "0x5eb3Bc0a489C5A8288765d2336659EbCA68FCd00",
"mailbox": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1",
"interchainGasPaymaster": "0x1291Be112d480055DaFd8a610b7d1e203891C274",
"defaultIsmInterchainGasPaymaster": "0xb7278A61aa25c888815aFC32Ad3cC52fF24fE575",
"legacyMultisigIsm": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
"merkleRootMultisigIsm": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"messageIdMultisigIsm": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
"aggregationIsm": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
"routingIsm": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"
},
"test2": {
"storageGasOracle": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
"validatorAnnounce": "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
"proxyAdmin": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
"mailbox": "0x9A676e781A523b5d0C0e43731313A708CB607508",
"interchainGasPaymaster": "0x59b670e9fA9D0A427751Af201D676719a970857b",
"defaultIsmInterchainGasPaymaster": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
"multisigIsm": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
"multisigIsmFactory": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
"aggregationIsmFactory": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9",
"routingIsmFactory": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707"
"storageGasOracle": "0x2bdCC0de6bE1f7D2ee689a0342D76F52E8EFABa3",
"validatorAnnounce": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
"proxyAdmin": "0x82e01223d51Eb87e16A03E24687EDF0F294da6f1",
"mailbox": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042",
"interchainGasPaymaster": "0xc351628EB244ec633d5f21fBD6621e1a683B1181",
"defaultIsmInterchainGasPaymaster": "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc",
"legacyMultisigIsm": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"merkleRootMultisigIsm": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
"messageIdMultisigIsm": "0x0165878A594ca255338adfa4d48449f69242Eb8F",
"aggregationIsm": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
"routingIsm": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
},
"test3": {
"storageGasOracle": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
"validatorAnnounce": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
"proxyAdmin": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
"mailbox": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1",
"interchainGasPaymaster": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933",
"defaultIsmInterchainGasPaymaster": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690",
"multisigIsm": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
"multisigIsmFactory": "0x0165878A594ca255338adfa4d48449f69242Eb8F",
"aggregationIsmFactory": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"routingIsmFactory": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"
"storageGasOracle": "0x162A433068F51e18b7d13932F27e66a3f99E6890",
"validatorAnnounce": "0x9d4454B023096f34B160D6B654540c56A1F81688",
"proxyAdmin": "0xB0D4afd8879eD9F52b28595d31B441D079B2Ca07",
"mailbox": "0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf",
"interchainGasPaymaster": "0x1fA02b2d6A771842690194Cf62D91bdd92BfE28d",
"defaultIsmInterchainGasPaymaster": "0x04C89607413713Ec9775E14b954286519d836FEf",
"legacyMultisigIsm": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
"merkleRootMultisigIsm": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
"messageIdMultisigIsm": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
"aggregationIsm": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
"routingIsm": "0x9A676e781A523b5d0C0e43731313A708CB607508"
}
}

@ -4,7 +4,7 @@ import { ChainMap } from '../types';
export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
// ----------------- Mainnets -----------------
celo: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0x1f20274b1210046769d48174c2f0e7c25ca7d5c5', // abacus
@ -16,7 +16,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
ethereum: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0x4c327ccb881a7542be77500b2833dc84c839e7b7', // abacus
@ -28,7 +28,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
avalanche: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0xa7aa52623fe3d78c343008c95894be669e218b8d', // abacus
@ -40,7 +40,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
polygon: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0x59a001c3451e7f9f3b4759ea215382c1e9aa5fc1', // abacus
@ -52,7 +52,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
bsc: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0xcc84b1eb711e5076b2755cf4ad1d2b42c458a45e', // abacus
@ -64,7 +64,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
arbitrum: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0xbcb815f38d481a5eba4d7ac4c9e74d9d0fc2a7e7', // abacus
@ -76,7 +76,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
optimism: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 4,
validators: [
'0x9f2296d5cfc6b5176adc7716c7596898ded13d35', // abacus
@ -88,7 +88,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
moonbeam: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 3,
validators: [
'0x237243d32d10e3bdbbf8dbcccc98ad44c1c172ea', // abacus
@ -98,7 +98,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
gnosis: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 3,
validators: [
'0xd0529ec8df08d0d63c0f023786bfa81e4bb51fd6', // abacus
@ -109,7 +109,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
},
// ----------------- Testnets -----------------
alfajores: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0xe6072396568e73ce6803b12b7e04164e839f1e54',
@ -118,7 +118,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
fuji: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0x9fa19ead5ec76e437948b35e227511b106293c40',
@ -127,7 +127,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
mumbai: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0x0a664ea799447da6b15645cf8b9e82072a68343f',
@ -136,7 +136,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
bsctestnet: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0x23338c8714976dd4a57eaeff17cbd26d7e275c08',
@ -145,7 +145,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
goerli: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0xf43fbd072fd38e1121d4b3b0b8a35116bbb01ea9',
@ -154,7 +154,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
sepolia: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0xbc748ee311f5f2d1975d61cdf531755ce8ce3066',
@ -163,7 +163,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
moonbasealpha: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0x890c2aeac157c3f067f3e42b8afc797939c59a32',
@ -172,7 +172,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
optimismgoerli: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0xbb8d77eefbecc55db6e5a19b0fc3dc290776f189',
@ -181,7 +181,7 @@ export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = {
],
},
arbitrumgoerli: {
type: ModuleType.MULTISIG,
type: ModuleType.LEGACY_MULTISIG,
threshold: 2,
validators: [
'0xce798fa21e323f6b24d9838a10ffecdefdfc4f30',

@ -23,7 +23,7 @@ import {
function randomModuleType(): ModuleType {
const choices = [
ModuleType.AGGREGATION,
ModuleType.MULTISIG,
ModuleType.MERKLE_ROOT_MULTISIG,
ModuleType.ROUTING,
];
return choices[randomInt(choices.length)];
@ -33,7 +33,7 @@ const randomMultisigIsmConfig = (m: number, n: number): MultisigIsmConfig => {
const emptyArray = new Array<number>(n).fill(0);
const validators = emptyArray.map(() => randomAddress());
return {
type: ModuleType.MULTISIG,
type: ModuleType.MERKLE_ROOT_MULTISIG,
validators,
threshold: m,
};
@ -41,8 +41,8 @@ const randomMultisigIsmConfig = (m: number, n: number): MultisigIsmConfig => {
const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => {
const moduleType =
depth == maxDepth ? ModuleType.MULTISIG : randomModuleType();
if (moduleType === ModuleType.MULTISIG) {
depth == maxDepth ? ModuleType.MERKLE_ROOT_MULTISIG : randomModuleType();
if (moduleType === ModuleType.MERKLE_ROOT_MULTISIG) {
const n = randomInt(5, 1);
return randomMultisigIsmConfig(randomInt(n, 1), n);
} else if (moduleType === ModuleType.ROUTING) {

@ -6,6 +6,7 @@ import {
IInterchainSecurityModule__factory,
IMultisigIsm__factory,
IRoutingIsm__factory,
LegacyMultisigIsm__factory,
StaticAggregationIsm__factory,
StaticMOfNAddressSetFactory,
} from '@hyperlane-xyz/core';
@ -58,9 +59,17 @@ export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
return new HyperlaneIsmFactory(helper.contractsMap, helper.multiProvider);
}
async deploy(chain: ChainName, config: IsmConfig): Promise<DeployedIsm> {
if (config.type === ModuleType.MULTISIG) {
return this.deployMultisigIsm(chain, config);
async deploy(
chain: ChainName,
config: IsmConfig,
origin?: ChainName,
): Promise<DeployedIsm> {
if (
config.type === ModuleType.MERKLE_ROOT_MULTISIG ||
config.type === ModuleType.MESSAGE_ID_MULTISIG ||
config.type === ModuleType.LEGACY_MULTISIG
) {
return this.deployMultisigIsm(chain, config, origin);
} else if (config.type === ModuleType.ROUTING) {
return this.deployRoutingIsm(chain, config);
} else if (config.type === ModuleType.AGGREGATION) {
@ -70,16 +79,41 @@ export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
}
}
private async deployMultisigIsm(chain: ChainName, config: MultisigIsmConfig) {
private async deployMultisigIsm(
chain: ChainName,
config: MultisigIsmConfig,
origin?: ChainName,
) {
const signer = this.multiProvider.getSigner(chain);
const multisigIsmFactory = this.getContracts(chain).multisigIsmFactory;
const address = await this.deployMOfNFactory(
chain,
multisigIsmFactory,
config.validators,
config.threshold,
);
return StaticAggregationIsm__factory.connect(address, signer);
let address: string;
if (config.type === ModuleType.LEGACY_MULTISIG) {
if (process.env.E2E_CI_MODE !== 'true') {
throw new Error(
'Legacy multisig ISM is being deprecated, do not deploy',
);
}
const multisig = await new LegacyMultisigIsm__factory()
.connect(signer)
.deploy();
await this.multiProvider.handleTx(chain, multisig.deployTransaction);
const originDomain = this.multiProvider.getDomainId(origin!);
await multisig.enrollValidators([originDomain], [config.validators]);
await multisig.setThreshold(originDomain, config.threshold);
address = multisig.address;
} else {
const multisigIsmFactory =
config.type === ModuleType.MERKLE_ROOT_MULTISIG
? this.getContracts(chain).merkleRootMultisigIsmFactory
: this.getContracts(chain).messageIdMultisigIsmFactory;
address = await this.deployMOfNFactory(
chain,
multisigIsmFactory,
config.validators,
config.threshold,
);
}
return IMultisigIsm__factory.connect(address, signer);
}
private async deployRoutingIsm(chain: ChainName, config: RoutingIsmConfig) {
@ -87,7 +121,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
const routingIsmFactory = this.getContracts(chain).routingIsmFactory;
const isms: ChainMap<types.Address> = {};
for (const origin of Object.keys(config.domains)) {
const ism = await this.deploy(chain, config.domains[origin]);
const ism = await this.deploy(chain, config.domains[origin], origin);
isms[origin] = ism.address;
}
const domains = Object.keys(isms).map((chain) =>
@ -116,7 +150,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
);
await routingIsm.transferOwnership(config.owner);
const address = dispatchLogs[0].args['module'];
return DomainRoutingIsm__factory.connect(address, signer);
return IRoutingIsm__factory.connect(address, signer);
}
private async deployAggregationIsm(
@ -136,7 +170,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
addresses,
config.threshold,
);
return StaticAggregationIsm__factory.connect(address, signer);
return IAggregationIsm__factory.connect(address, signer);
}
private async deployMOfNFactory(
@ -184,8 +218,9 @@ export async function moduleCanCertainlyVerify(
try {
const moduleType = await module.moduleType();
if (
moduleType === ModuleType.MULTISIG ||
moduleType === ModuleType.LEGACY_MULTISIG
moduleType === ModuleType.MERKLE_ROOT_MULTISIG ||
moduleType === ModuleType.LEGACY_MULTISIG ||
moduleType === ModuleType.MESSAGE_ID_MULTISIG
) {
const multisigModule = IMultisigIsm__factory.connect(
moduleAddress,
@ -251,15 +286,30 @@ export async function moduleMatchesConfig(
if (actualType !== config.type) return false;
let matches = true;
switch (config.type) {
case ModuleType.MULTISIG: {
case ModuleType.MERKLE_ROOT_MULTISIG:
case ModuleType.MESSAGE_ID_MULTISIG: {
// A MultisigIsm matches if validators and threshold match the config
const expectedAddress = await contracts.multisigIsmFactory.getAddress(
config.validators.sort(),
config.threshold,
);
const expectedAddress =
await contracts.merkleRootMultisigIsmFactory.getAddress(
config.validators.sort(),
config.threshold,
);
matches = utils.eqAddress(expectedAddress, module.address);
break;
}
case ModuleType.LEGACY_MULTISIG: {
const multisigIsm = LegacyMultisigIsm__factory.connect(
moduleAddress,
provider,
);
const domain = multiProvider.getDomainId(chain);
const validators = await multisigIsm.validators(domain);
const threshold = await multisigIsm.threshold(domain);
matches =
config.validators.sort() == validators.sort() &&
config.threshold == threshold;
break;
}
case ModuleType.ROUTING: {
// A RoutingIsm matches if:
// 1. The set of domains in the config equals those on-chain
@ -338,7 +388,7 @@ export function collectValidators(
config: IsmConfig,
): Set<string> {
let validators: string[] = [];
if (config.type === ModuleType.MULTISIG) {
if (config.type === ModuleType.MERKLE_ROOT_MULTISIG) {
validators = config.validators;
} else if (config.type === ModuleType.ROUTING) {
if (Object.keys(config.domains).includes(origin)) {

@ -33,9 +33,14 @@ export class HyperlaneIsmFactoryDeployer extends HyperlaneDeployer<
async deployContracts(
chain: ChainName,
): Promise<HyperlaneContracts<IsmFactoryFactories>> {
const multisigIsmFactory = await this.deployContract(
const merkleRootMultisigIsmFactory = await this.deployContract(
chain,
'multisigIsmFactory',
'merkleRootMultisigIsmFactory',
[],
);
const messageIdMultisigIsmFactory = await this.deployContract(
chain,
'messageIdMultisigIsmFactory',
[],
);
const aggregationIsmFactory = await this.deployContract(
@ -48,6 +53,11 @@ export class HyperlaneIsmFactoryDeployer extends HyperlaneDeployer<
'routingIsmFactory',
[],
);
return { multisigIsmFactory, aggregationIsmFactory, routingIsmFactory };
return {
merkleRootMultisigIsmFactory,
messageIdMultisigIsmFactory,
aggregationIsmFactory,
routingIsmFactory,
};
}
}

@ -1,11 +1,14 @@
import {
DomainRoutingIsmFactory__factory,
StaticAggregationIsmFactory__factory,
StaticMultisigIsmFactory__factory,
StaticMerkleRootMultisigIsmFactory__factory,
StaticMessageIdMultisigIsmFactory__factory,
} from '@hyperlane-xyz/core';
export const ismFactoryFactories = {
multisigIsmFactory: new StaticMultisigIsmFactory__factory(),
merkleRootMultisigIsmFactory:
new StaticMerkleRootMultisigIsmFactory__factory(),
messageIdMultisigIsmFactory: new StaticMessageIdMultisigIsmFactory__factory(),
aggregationIsmFactory: new StaticAggregationIsmFactory__factory(),
routingIsmFactory: new DomainRoutingIsmFactory__factory(),
};

@ -1,27 +1,28 @@
import {
DomainRoutingIsm,
StaticAggregationIsm,
StaticMultisigIsm,
IAggregationIsm,
IMultisigIsm,
IRoutingIsm,
} from '@hyperlane-xyz/core';
import type { types } from '@hyperlane-xyz/utils';
import { ChainMap } from '../types';
export type DeployedIsm =
| StaticMultisigIsm
| StaticAggregationIsm
| DomainRoutingIsm;
export type DeployedIsm = IMultisigIsm | IAggregationIsm | IRoutingIsm;
export enum ModuleType {
UNUSED,
ROUTING,
AGGREGATION,
LEGACY_MULTISIG,
MULTISIG,
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG,
}
export type MultisigIsmConfig = {
type: ModuleType.MULTISIG;
type:
| ModuleType.LEGACY_MULTISIG
| ModuleType.MERKLE_ROOT_MULTISIG
| ModuleType.MESSAGE_ID_MULTISIG;
validators: Array<types.Address>;
threshold: number;
};

@ -72,7 +72,7 @@ const nonZeroAddress = ethers.constants.AddressZero.replace('00', '01');
// dummy config as TestInbox and TestOutbox do not use deployed ISM
export function testCoreConfig(chains: ChainName[]): ChainMap<CoreConfig> {
const multisigIsm: MultisigIsmConfig = {
type: ModuleType.MULTISIG,
type: ModuleType.MERKLE_ROOT_MULTISIG,
validators: [nonZeroAddress],
threshold: 1,
};

Loading…
Cancel
Save