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 testspull/2283/head
parent
8aa7b62bea
commit
50f04db1fa
@ -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; |
@ -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); |
||||
} |
||||
} |
@ -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()); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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]; |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue