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 ISMspull/2532/head
parent
637172bb96
commit
da25b06254
File diff suppressed because it is too large
Load Diff
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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<()>; |
||||||
|
} |
@ -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); |
||||||
|
} |
Loading…
Reference in new issue