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