CCIP Read ISM (#2398)

### Description

CCIP Read ISM implementation,

- [x] Contracts
- [x] Relayer
- [x] Example HTTP backend

### Drive-by changes

_Are there any minor or drive-by changes also included?_

### Related issues

- Fixes #[2211]

### Backward compatibility

_Are these changes backward compatible?_

Yes, relayers will skip messages they can't handle.

_Are there any infrastructure implications, e.g. changes that would
prohibit deploying older commits using this infra tooling?_

Yes, relayers won't be able to submit messages that have a CCIP Read
ISM.

### Testing

_What kind of testing have these changes undergone?_

- [x] Manual
- [x] Unit Tests

### General approach

- [x] Get a basic [CCIP Read implementation
working](https://github.com/AlexBHarley/permissionless-chainlink-feeds/tree/main/contracts)
- [x] Build a [mock service that serves CCIP Read compatible
data](https://github.com/AlexBHarley/permissionless-chainlink-feeds/tree/main/apps/api)
- [x] Update the Relayer to support CCIP Read ISMs
pull/2532/head
Alex 1 year ago committed by GitHub
parent 637172bb96
commit da25b06254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 714
      rust/Cargo.lock
  2. 1
      rust/agents/relayer/Cargo.toml
  3. 14
      rust/agents/relayer/src/msg/metadata/base.rs
  4. 102
      rust/agents/relayer/src/msg/metadata/ccip_read.rs
  5. 2
      rust/agents/relayer/src/msg/metadata/mod.rs
  6. 83
      rust/chains/hyperlane-ethereum/abis/ICcipReadIsm.abi.json
  7. 109
      rust/chains/hyperlane-ethereum/src/ccip_read_ism.rs
  8. 11
      rust/chains/hyperlane-ethereum/src/lib.rs
  9. 25
      rust/hyperlane-base/src/settings/chains.rs
  10. 14
      rust/hyperlane-core/src/traits/ccip_read_ism.rs
  11. 2
      rust/hyperlane-core/src/traits/interchain_security_module.rs
  12. 2
      rust/hyperlane-core/src/traits/mod.rs
  13. 3
      solidity/contracts/interfaces/IInterchainSecurityModule.sol
  14. 28
      solidity/contracts/interfaces/isms/ICcipReadIsm.sol
  15. 32
      solidity/contracts/isms/ccip-read/AbstractCcipReadIsm.sol
  16. 1
      solidity/package.json
  17. 14
      yarn.lock

714
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -27,6 +27,7 @@ tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] }
tracing-futures.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
regex = "1.5"
hyperlane-core = { path = "../../hyperlane-core" }
hyperlane-base = { path = "../../hyperlane-base" }

@ -13,8 +13,8 @@ use hyperlane_base::{
};
use hyperlane_core::accumulator::merkle::Proof;
use hyperlane_core::{
AggregationIsm, Checkpoint, HyperlaneDomain, HyperlaneMessage, InterchainSecurityModule,
ModuleType, MultisigIsm, RoutingIsm, ValidatorAnnounce, H160, H256,
AggregationIsm, CcipReadIsm, Checkpoint, HyperlaneDomain, HyperlaneMessage,
InterchainSecurityModule, ModuleType, MultisigIsm, RoutingIsm, ValidatorAnnounce, H160, H256,
};
use crate::merkle_tree_builder::MerkleTreeBuilder;
@ -23,7 +23,8 @@ use crate::msg::metadata::multisig::{
MessageIdMultisigMetadataBuilder,
};
use crate::msg::metadata::{
AggregationIsmMetadataBuilder, NullMetadataBuilder, RoutingIsmMetadataBuilder,
AggregationIsmMetadataBuilder, CcipReadIsmMetadataBuilder, NullMetadataBuilder,
RoutingIsmMetadataBuilder,
};
#[derive(Debug, thiserror::Error)]
@ -87,6 +88,7 @@ impl MetadataBuilder for BaseMetadataBuilder {
ModuleType::Routing => Box::new(RoutingIsmMetadataBuilder::new(base)),
ModuleType::Aggregation => Box::new(AggregationIsmMetadataBuilder::new(base)),
ModuleType::Null => Box::new(NullMetadataBuilder::new()),
ModuleType::CcipRead => Box::new(CcipReadIsmMetadataBuilder::new(base)),
_ => return Err(MetadataBuilderError::UnsupportedModuleType(module_type).into()),
};
metadata_builder
@ -162,6 +164,12 @@ impl BaseMetadataBuilder {
.await
}
pub async fn build_ccip_read_ism(&self, address: H256) -> Result<Box<dyn CcipReadIsm>> {
self.destination_chain_setup
.build_ccip_read_ism(address, &self.metrics)
.await
}
pub async fn build_checkpoint_syncer(
&self,
validators: &[H256],

@ -0,0 +1,102 @@
use async_trait::async_trait;
use hyperlane_ethereum::OffchainLookup;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::ops::Deref;
use derive_new::new;
use eyre::Context;
use tracing::{info, instrument};
use super::{BaseMetadataBuilder, MetadataBuilder};
use ethers::abi::AbiDecode;
use ethers::core::utils::hex::decode as hex_decode;
use hyperlane_core::{HyperlaneMessage, RawHyperlaneMessage, H256};
use regex::Regex;
#[derive(Serialize, Deserialize)]
struct OffchainResponse {
data: String,
}
#[derive(Clone, Debug, new)]
pub struct CcipReadIsmMetadataBuilder {
base: BaseMetadataBuilder,
}
impl Deref for CcipReadIsmMetadataBuilder {
type Target = BaseMetadataBuilder;
fn deref(&self) -> &Self::Target {
&self.base
}
}
#[async_trait]
impl MetadataBuilder for CcipReadIsmMetadataBuilder {
#[instrument(err, skip(self))]
async fn build(
&self,
ism_address: H256,
message: &HyperlaneMessage,
) -> eyre::Result<Option<Vec<u8>>> {
const CTX: &str = "When fetching CcipRead metadata";
let ism = self.build_ccip_read_ism(ism_address).await.context(CTX)?;
let response = ism
.get_offchain_verify_info(RawHyperlaneMessage::from(message).to_vec())
.await;
let info: OffchainLookup = match response {
Ok(_) => {
info!("incorrectly configured getOffchainVerifyInfo, expected revert");
return Ok(None);
}
Err(raw_error) => {
let matching_regex = Regex::new(r"0x[[:xdigit:]]+")?;
if let Some(matching) = &matching_regex.captures(&raw_error.to_string()) {
OffchainLookup::decode(hex_decode(&matching[0][2..])?)?
} else {
info!("unable to parse custom error out of revert");
return Ok(None);
}
}
};
for url in info.urls.iter() {
let interpolated_url = url
.replace("{sender}", &info.sender.to_string())
.replace("{data}", &info.call_data.to_string());
let res = if !url.contains("{data}") {
let body = json!({
"data": info.call_data.to_string(),
"sender": info.sender.to_string(),
});
Client::new()
.post(interpolated_url)
.header("Content-Type", "application/json")
.json(&body)
.send()
.await?
} else {
reqwest::get(interpolated_url).await?
};
let json: Result<OffchainResponse, reqwest::Error> = res.json().await;
match json {
Ok(result) => {
// remove leading 0x which hex_decode doesn't like
let metadata = hex_decode(&result.data[2..])?;
return Ok(Some(metadata));
}
Err(_err) => {
// try the next URL
}
}
}
// No metadata endpoints or endpoints down
Ok(None)
}
}

@ -1,5 +1,6 @@
mod aggregation;
mod base;
mod ccip_read;
mod multisig;
mod null_metadata;
mod routing;
@ -7,5 +8,6 @@ mod routing;
use aggregation::AggregationIsmMetadataBuilder;
pub(crate) use base::BaseMetadataBuilder;
pub(crate) use base::MetadataBuilder;
use ccip_read::CcipReadIsmMetadataBuilder;
use null_metadata::NullMetadataBuilder;
use routing::RoutingIsmMetadataBuilder;

@ -0,0 +1,83 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "string[]",
"name": "urls",
"type": "string[]"
},
{
"internalType": "bytes",
"name": "callData",
"type": "bytes"
},
{
"internalType": "bytes4",
"name": "callbackFunction",
"type": "bytes4"
},
{
"internalType": "bytes",
"name": "extraData",
"type": "bytes"
}
],
"name": "OffchainLookup",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_message",
"type": "bytes"
}
],
"name": "getOffchainVerifyInfo",
"outputs": [],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "moduleType",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_metadata",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "_message",
"type": "bytes"
}
],
"name": "verify",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -0,0 +1,109 @@
#![allow(clippy::enum_variant_names)]
#![allow(missing_docs)]
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use ethers::providers::Middleware;
use tracing::instrument;
use hyperlane_core::{
CcipReadIsm, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract,
HyperlaneDomain, HyperlaneProvider, H256,
};
pub use crate::contracts::i_ccip_read_ism::{
ICcipReadIsm as EthereumCcipReadIsmInternal, OffchainLookup, ICCIPREADISM_ABI,
};
use crate::trait_builder::BuildableWithProvider;
use crate::EthereumProvider;
pub struct CcipReadIsmBuilder {}
#[async_trait]
impl BuildableWithProvider for CcipReadIsmBuilder {
type Output = Box<dyn CcipReadIsm>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
provider: M,
locator: &ContractLocator,
) -> Self::Output {
Box::new(EthereumCcipReadIsm::new(Arc::new(provider), locator))
}
}
/// A reference to an CcipReadIsm contract on some Ethereum chain
#[derive(Debug)]
pub struct EthereumCcipReadIsm<M>
where
M: Middleware,
{
contract: Arc<EthereumCcipReadIsmInternal<M>>,
domain: HyperlaneDomain,
}
impl<M> EthereumCcipReadIsm<M>
where
M: Middleware + 'static,
{
/// Create a reference to a mailbox at a specific Ethereum address on some
/// chain
pub fn new(provider: Arc<M>, locator: &ContractLocator) -> Self {
Self {
contract: Arc::new(EthereumCcipReadIsmInternal::new(locator.address, provider)),
domain: locator.domain.clone(),
}
}
}
impl<M> HyperlaneChain for EthereumCcipReadIsm<M>
where
M: Middleware + 'static,
{
fn domain(&self) -> &HyperlaneDomain {
&self.domain
}
fn provider(&self) -> Box<dyn HyperlaneProvider> {
Box::new(EthereumProvider::new(
self.contract.client(),
self.domain.clone(),
))
}
}
impl<M> HyperlaneContract for EthereumCcipReadIsm<M>
where
M: Middleware + 'static,
{
fn address(&self) -> H256 {
self.contract.address().into()
}
}
#[async_trait]
impl<M> CcipReadIsm for EthereumCcipReadIsm<M>
where
M: Middleware + 'static,
{
#[instrument(err)]
async fn get_offchain_verify_info(&self, message: Vec<u8>) -> ChainResult<()> {
self.contract
.get_offchain_verify_info(message.into())
.call()
.await?;
Ok(())
}
}
pub struct EthereumCcipReadIsmAbi;
impl HyperlaneAbi for EthereumCcipReadIsmAbi {
const SELECTOR_SIZE_BYTES: usize = 4;
fn fn_map() -> HashMap<Vec<u8>, &'static str> {
super::extract_fn_map(&ICCIPREADISM_ABI)
}
}

@ -10,9 +10,10 @@ use ethers::prelude::{abi, Lazy, Middleware};
#[cfg(not(doctest))]
pub use self::{
aggregation_ism::*, config::*, interchain_gas::*, interchain_security_module::*, mailbox::*,
multisig_ism::*, provider::*, routing_ism::*, rpc_clients::*, signers::*, singleton_signer::*,
trait_builder::*, validator_announce::*,
aggregation_ism::*, ccip_read_ism::*, config::*, config::*, interchain_gas::*,
interchain_gas::*, interchain_security_module::*, interchain_security_module::*, mailbox::*,
mailbox::*, multisig_ism::*, provider::*, routing_ism::*, rpc_clients::*, signers::*,
singleton_signer::*, trait_builder::*, validator_announce::*,
};
#[cfg(not(doctest))]
@ -45,6 +46,10 @@ mod multisig_ism;
#[cfg(not(doctest))]
mod routing_ism;
/// CcipReadIsm abi
#[cfg(not(doctest))]
mod ccip_read_ism;
/// ValidatorAnnounce abi
#[cfg(not(doctest))]
mod validator_announce;

@ -8,7 +8,7 @@ use ethers_prometheus::middleware::{
ChainInfo, ContractInfo, PrometheusMiddlewareConf, WalletInfo,
};
use hyperlane_core::{
config::*, AggregationIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain,
config::*, AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain,
HyperlaneDomainProtocol, HyperlaneProvider, HyperlaneSigner, Indexer, InterchainGasPaymaster,
InterchainGasPayment, InterchainSecurityModule, Mailbox, MessageIndexer, MultisigIsm,
RoutingIsm, ValidatorAnnounce, H160, H256,
@ -552,6 +552,29 @@ impl ChainConf {
.context(ctx)
}
/// Try to convert the chain setting into a CcipRead Ism contract
pub async fn build_ccip_read_ism(
&self,
address: H256,
metrics: &CoreMetrics,
) -> Result<Box<dyn CcipReadIsm>> {
let ctx = "Building CcipRead ISM";
let locator = ContractLocator {
domain: &self.domain,
address,
};
match &self.connection()? {
ChainConnectionConf::Ethereum(conf) => {
self.build_ethereum(conf, &locator, metrics, h_eth::CcipReadIsmBuilder {})
.await
}
ChainConnectionConf::Fuel(_) => todo!(),
}
.context(ctx)
}
async fn signer<S: BuildableWithSignerConf>(&self) -> Result<Option<S>> {
if let Some(conf) = &self.signer {
Ok(Some(conf.build::<S>().await?))

@ -0,0 +1,14 @@
use std::fmt::Debug;
use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::{ChainResult, HyperlaneContract};
/// Interface for the CcipReadIsm chain contract
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait CcipReadIsm: HyperlaneContract + Send + Sync + Debug {
/// Reverts with a custom error specifying how to query for offchain information
async fn get_offchain_verify_info(&self, message: Vec<u8>) -> ChainResult<()>;
}

@ -26,6 +26,8 @@ pub enum ModuleType {
MessageIdMultisig,
/// No metadata ISM (no metadata)
Null,
/// Ccip Read ISM (accepts offchain signature information)
CcipRead,
}
/// Interface for the InterchainSecurityModule chain contract. Allows abstraction over

@ -1,4 +1,5 @@
pub use aggregation_ism::*;
pub use ccip_read_ism::*;
pub use cursor::*;
pub use db::*;
pub use deployed::*;
@ -14,6 +15,7 @@ pub use signing::*;
pub use validator_announce::*;
mod aggregation_ism;
mod ccip_read_ism;
mod cursor;
mod db;
mod deployed;

@ -9,7 +9,8 @@ interface IInterchainSecurityModule {
LEGACY_MULTISIG,
MERKLE_ROOT_MULTISIG,
MESSAGE_ID_MULTISIG,
NULL // used with relayer carrying no metadata
NULL, // used with relayer carrying no metadata
CCIP_READ
}
/**

@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import {IInterchainSecurityModule} from "../IInterchainSecurityModule.sol";
interface ICcipReadIsm is IInterchainSecurityModule {
/// @dev https://eips.ethereum.org/EIPS/eip-3668
/// @param sender the address of the contract making the call, usually address(this)
/// @param urls the URLs to query for offchain data
/// @param callData context needed for offchain service to service request
/// @param callbackFunction function selector to call with offchain information
/// @param extraData additional passthrough information to call callbackFunction with
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
/**
* @notice Reverts with the data needed to query information offchain
* and be submitted via the origin mailbox
* @dev See https://eips.ethereum.org/EIPS/eip-3668 for more information
* @param _message data that will help construct the offchain query
*/
function getOffchainVerifyInfo(bytes calldata _message) external view;
}

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {ICcipReadIsm} from "../../interfaces/isms/ICcipReadIsm.sol";
import {IMailbox} from "../../interfaces/IMailbox.sol";
import {Message} from "../../libs/Message.sol";
import {AbstractMultisigIsm} from "../multisig/AbstractMultisigIsm.sol";
/**
* @title AbstractCcipReadIsm
* @notice An ISM that allows arbitrary payloads to be submitted and verified on chain
* @dev https://eips.ethereum.org/EIPS/eip-3668
* @dev The AbstractCcipReadIsm provided by Hyperlane is left intentially minimalist as
* the range of applications that could be supported by a CcipReadIsm are so broad. However
* there are few things to note when building a custom CcipReadIsm.
*
* 1. `getOffchainVerifyInfo` should revert with a `OffchainLookup` error, which encodes
* the data necessary to query for offchain information
* 2. For full CCIP Read specification compatibility, CcipReadIsm's should expose a function
* that in turn calls `process` on the configured Mailbox with the provided metadata and
* message. This functions selector should be provided as the `callbackFunction` payload
* for the OffchainLookup error
*/
abstract contract AbstractCcipReadIsm is ICcipReadIsm {
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.CCIP_READ);
}

@ -3,6 +3,7 @@
"description": "Core solidity contracts for Hyperlane",
"version": "1.4.2",
"dependencies": {
"@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "1.4.2",
"@openzeppelin/contracts": "^4.8.0",
"@openzeppelin/contracts-upgradeable": "^4.8.0"

@ -2847,6 +2847,19 @@ __metadata:
languageName: node
linkType: hard
"@eth-optimism/contracts@npm:^0.6.0":
version: 0.6.0
resolution: "@eth-optimism/contracts@npm:0.6.0"
dependencies:
"@eth-optimism/core-utils": 0.12.0
"@ethersproject/abstract-provider": ^5.7.0
"@ethersproject/abstract-signer": ^5.7.0
peerDependencies:
ethers: ^5
checksum: 52e9a6cc6ad9bf3ab085d3be501fa4c89e48865baa8aee01aff39c2b007b69600304c7e8f8f4e00d67396e48a0dbfe3a260437efd3a4d7216424cece52639870
languageName: node
linkType: hard
"@eth-optimism/core-utils@npm:0.12.0, @eth-optimism/core-utils@npm:^0.12.0":
version: 0.12.0
resolution: "@eth-optimism/core-utils@npm:0.12.0"
@ -3896,6 +3909,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hyperlane-xyz/core@workspace:solidity"
dependencies:
"@eth-optimism/contracts": ^0.6.0
"@hyperlane-xyz/utils": 1.4.2
"@nomiclabs/hardhat-ethers": ^2.2.1
"@nomiclabs/hardhat-waffle": ^2.0.3

Loading…
Cancel
Save