diff --git a/Cargo.lock b/Cargo.lock index 1da07f2f39..426efd9689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3548,6 +3548,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "evm-in-cosmos-light-client" +version = "0.1.0" +dependencies = [ + "cosmwasm-std 1.5.2", + "ethereum-light-client", + "ics008-wasm-client", + "ics23", + "protos", + "thiserror", + "unionlabs", +] + [[package]] name = "expander" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index 225312a8d1..5525754d75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ members = [ "light-clients/berachain-light-client", "light-clients/cometbls-light-client", "light-clients/ethereum-light-client", + "light-clients/evm-in-cosmos-light-client", "light-clients/scroll-light-client", "light-clients/tendermint-light-client", "light-clients/linea-light-client", @@ -93,43 +94,44 @@ opt-level = 3 strip = true [workspace.dependencies] -arbitrum-verifier = { path = "lib/arbitrum-verifier", default-features = false } -beacon-api = { path = "lib/beacon-api", default-features = false } -block-message = { path = "lib/block-message", default-features = false } -chain-utils = { path = "lib/chain-utils", default-features = false } -cometbft-rpc = { path = "lib/cometbft-rpc", default-features = false } -cometbls-groth16-verifier = { path = "lib/cometbls-groth16-verifier", default-features = false } -contracts = { path = "generated/rust/contracts", default-features = false } -ethereum-light-client = { path = "light-clients/ethereum-light-client", default-features = false } -ethereum-verifier = { path = "lib/ethereum-verifier", default-features = false } -gnark-key-parser = { path = "lib/gnark-key-parser", default-features = false } -gnark-mimc = { path = "lib/gnark-mimc", default-features = false } -ibc-vm-rs = { path = "lib/ibc-vm-rs", default-features = false } -ics008-wasm-client = { path = "lib/ics-008-wasm-client", default-features = false } -ics23 = { path = "lib/ics23", default-features = false } -linea-verifier = { path = "lib/linea-verifier", default-features = false } -linea-zktrie = { path = "lib/linea-zktrie", default-features = false } -macros = { path = "lib/macros", default-features = false } -pg-queue = { path = "lib/pg-queue", default-features = false } -poseidon-rs = { path = "lib/poseidon-rs", default-features = false } -protos = { path = "generated/rust/protos", default-features = false } -queue-msg = { path = "lib/queue-msg", default-features = false } -queue-msg-macro = { path = "lib/queue-msg-macro", default-features = false } -relay-message = { path = "lib/relay-message", default-features = false } -scroll-api = { path = "lib/scroll-api", default-features = false } -scroll-codec = { path = "lib/scroll-codec", default-features = false } -scroll-rpc = { path = "lib/scroll-rpc", default-features = false } -scroll-verifier = { path = "lib/scroll-verifier", default-features = false } -serde-utils = { path = "lib/serde-utils", default-features = false } -ssz = { path = "lib/ssz", default-features = false } -ssz-derive = { path = "lib/ssz-derive", default-features = false } -tendermint-light-client = { path = "light-clients/tendermint-light-client", default-features = false } -tendermint-verifier = { path = "lib/tendermint-verifier", default-features = false } -token-factory-api = { path = "cosmwasm/token-factory-api", default-features = false } -ucs01-relay-api = { path = "cosmwasm/ucs01-relay-api", default-features = false } -unionlabs = { path = "lib/unionlabs", default-features = false } -voyager-message = { path = "lib/voyager-message", default-features = false } -zktrie = { path = "lib/zktrie-rs", default-features = false } +arbitrum-verifier = { path = "lib/arbitrum-verifier", default-features = false } +beacon-api = { path = "lib/beacon-api", default-features = false } +block-message = { path = "lib/block-message", default-features = false } +chain-utils = { path = "lib/chain-utils", default-features = false } +cometbft-rpc = { path = "lib/cometbft-rpc", default-features = false } +cometbls-groth16-verifier = { path = "lib/cometbls-groth16-verifier", default-features = false } +contracts = { path = "generated/rust/contracts", default-features = false } +ethereum-light-client = { path = "light-clients/ethereum-light-client", default-features = false } +ethereum-verifier = { path = "lib/ethereum-verifier", default-features = false } +evm-in-cosmos-light-client = { path = "light-clients/evm-in-cosmos-light-client", default-features = false } +gnark-key-parser = { path = "lib/gnark-key-parser", default-features = false } +gnark-mimc = { path = "lib/gnark-mimc", default-features = false } +ibc-vm-rs = { path = "lib/ibc-vm-rs", default-features = false } +ics008-wasm-client = { path = "lib/ics-008-wasm-client", default-features = false } +ics23 = { path = "lib/ics23", default-features = false } +linea-verifier = { path = "lib/linea-verifier", default-features = false } +linea-zktrie = { path = "lib/linea-zktrie", default-features = false } +macros = { path = "lib/macros", default-features = false } +pg-queue = { path = "lib/pg-queue", default-features = false } +poseidon-rs = { path = "lib/poseidon-rs", default-features = false } +protos = { path = "generated/rust/protos", default-features = false } +queue-msg = { path = "lib/queue-msg", default-features = false } +queue-msg-macro = { path = "lib/queue-msg-macro", default-features = false } +relay-message = { path = "lib/relay-message", default-features = false } +scroll-api = { path = "lib/scroll-api", default-features = false } +scroll-codec = { path = "lib/scroll-codec", default-features = false } +scroll-rpc = { path = "lib/scroll-rpc", default-features = false } +scroll-verifier = { path = "lib/scroll-verifier", default-features = false } +serde-utils = { path = "lib/serde-utils", default-features = false } +ssz = { path = "lib/ssz", default-features = false } +ssz-derive = { path = "lib/ssz-derive", default-features = false } +tendermint-light-client = { path = "light-clients/tendermint-light-client", default-features = false } +tendermint-verifier = { path = "lib/tendermint-verifier", default-features = false } +token-factory-api = { path = "cosmwasm/token-factory-api", default-features = false } +ucs01-relay-api = { path = "cosmwasm/ucs01-relay-api", default-features = false } +unionlabs = { path = "lib/unionlabs", default-features = false } +voyager-message = { path = "lib/voyager-message", default-features = false } +zktrie = { path = "lib/zktrie-rs", default-features = false } # external dependencies milagro_bls = { git = "https://github.com/Snowfork/milagro_bls", rev = "bc2b5b5e8d48b7e2e1bfaa56dc2d93e13cb32095", default-features = false } diff --git a/dictionary.txt b/dictionary.txt index ec6d3f6f37..51c5f32db5 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -482,6 +482,7 @@ ethevent ethkey evidencekeeper evidencetypes +evmincosmos evmos extldflags extralight diff --git a/flake.nix b/flake.nix index 0378422c72..899961ec90 100644 --- a/flake.nix +++ b/flake.nix @@ -263,6 +263,7 @@ ./light-clients/arbitrum-light-client/arbitrum-light-client.nix ./light-clients/linea-light-client/linea-light-client.nix ./light-clients/berachain-light-client/berachain-light-client.nix + ./light-clients/evm-in-cosmos-light-client/evm-in-cosmos-light-client.nix ./lib/cometbls-groth16-verifier/default.nix ./lib/linea-verifier/default.nix ./lib/linea-zktrie/default.nix diff --git a/generated/rust/protos/Cargo.toml b/generated/rust/protos/Cargo.toml index 9873504b70..4246b3babb 100644 --- a/generated/rust/protos/Cargo.toml +++ b/generated/rust/protos/Cargo.toml @@ -282,6 +282,7 @@ proto_full = [ "union+ibc+lightclients+berachain+v1", "union+ibc+lightclients+cometbls+v1", "union+ibc+lightclients+ethereum+v1", + "union+ibc+lightclients+evmincosmos+v1", "union+ibc+lightclients+linea+v1", "union+ibc+lightclients+scroll+v1", "union+ics23+v1", @@ -325,6 +326,11 @@ proto_full = [ "ibc+core+commitment+v1", ] "union+ibc+lightclients+ethereum+v1" = ["ibc+core+client+v1"] +"union+ibc+lightclients+evmincosmos+v1" = [ + "ibc+core+client+v1", + "ibc+core+commitment+v1", + "union+ibc+lightclients+ethereum+v1", +] "union+ibc+lightclients+linea+v1" = ["ibc+core+client+v1", "union+ibc+lightclients+ethereum+v1"] "union+ibc+lightclients+scroll+v1" = ["ibc+core+client+v1", "union+ibc+lightclients+ethereum+v1"] "union+ics23+v1" = [] diff --git a/generated/rust/protos/src/lib.rs b/generated/rust/protos/src/lib.rs index c507d2f05a..bd7fa1b287 100644 --- a/generated/rust/protos/src/lib.rs +++ b/generated/rust/protos/src/lib.rs @@ -883,6 +883,14 @@ pub mod union { // @@protoc_insertion_point(union.ibc.lightclients.ethereum.v1) } } + pub mod evmincosmos { + #[cfg(feature = "union+ibc+lightclients+evmincosmos+v1")] + // @@protoc_insertion_point(attribute:union.ibc.lightclients.evmincosmos.v1) + pub mod v1 { + include!("union.ibc.lightclients.evmincosmos.v1.rs"); + // @@protoc_insertion_point(union.ibc.lightclients.evmincosmos.v1) + } + } pub mod linea { #[cfg(feature = "union+ibc+lightclients+linea+v1")] // @@protoc_insertion_point(attribute:union.ibc.lightclients.linea.v1) diff --git a/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs b/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs new file mode 100644 index 0000000000..2648223d2b --- /dev/null +++ b/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs @@ -0,0 +1,67 @@ +// @generated +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ClientState { + #[prost(string, tag = "1")] + pub l1_client_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub l2_client_id: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub latest_slot: u64, + /// Evm + #[prost(bytes = "vec", tag = "5")] + pub ibc_commitment_slot: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "6")] + pub ibc_contract_address: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for ClientState { + const NAME: &'static str = "ClientState"; + const PACKAGE: &'static str = "union.ibc.lightclients.evmincosmos.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.evmincosmos.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConsensusState { + #[prost(bytes = "vec", tag = "1")] + pub evm_state_root: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub ibc_storage_root: ::prost::alloc::vec::Vec, + #[prost(uint64, tag = "3")] + pub timestamp: u64, +} +impl ::prost::Name for ConsensusState { + const NAME: &'static str = "ConsensusState"; + const PACKAGE: &'static str = "union.ibc.lightclients.evmincosmos.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.evmincosmos.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Header { + #[prost(message, optional, tag = "1")] + pub l1_height: + ::core::option::Option, + #[prost(uint64, tag = "2")] + pub l2_slot: u64, + #[prost(message, optional, tag = "3")] + pub l2_consensus_state: ::core::option::Option, + /// Proof of the l2 consensus state in the l1 client. + #[prost(message, optional, tag = "4")] + pub l2_inclusion_proof: ::core::option::Option< + super::super::super::super::super::ibc::core::commitment::v1::MerkleProof, + >, + /// Proof of the ibc contract in the evm state root. + #[prost(message, optional, tag = "5")] + pub account_proof: ::core::option::Option, +} +impl ::prost::Name for Header { + const NAME: &'static str = "Header"; + const PACKAGE: &'static str = "union.ibc.lightclients.evmincosmos.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("union.ibc.lightclients.evmincosmos.v1.{}", Self::NAME) + } +} +// @@protoc_insertion_point(module) diff --git a/hubble/src/chain_id_query.rs b/hubble/src/chain_id_query.rs index ca3e28b078..5ec5c48304 100644 --- a/hubble/src/chain_id_query.rs +++ b/hubble/src/chain_id_query.rs @@ -173,6 +173,9 @@ pub async fn tx(db: PgPool, indexers: Indexers) { cs.chain_id().to_string() } + WasmClientType::EvmInCosmos => { + todo!() + } }; datas.push(Data { diff --git a/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs b/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs index 47644c2ff8..89fd27375c 100644 --- a/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs +++ b/lib/unionlabs/src/cosmwasm/wasm/union/custom_query.rs @@ -2,7 +2,7 @@ use core::fmt::Debug; use cosmwasm_std::{Binary, Deps, QueryRequest}; -use crate::bls::BlsPublicKey; +use crate::{bls::BlsPublicKey, ibc::core::client::height::Height, ics24::Path}; #[derive(thiserror::Error, Debug, PartialEq, Clone)] pub enum Error { @@ -13,8 +13,11 @@ pub enum Error { AggregatePublicKeys(String), #[error("invalid public key is returned from `aggregate_public_key`")] InvalidAggregatePublicKey, - #[error("error while running `consensus_state` query ({0})")] - ConsensusState(String), + #[error("abci query for {path} failed: {err}")] + ABCI { + path: Path, + err: String, + }, } #[derive(serde::Serialize, serde::Deserialize, Clone)] @@ -73,8 +76,8 @@ use { crate::{ encoding::{Decode, DecodeAs, Proto}, google::protobuf::any::Any, - ibc::core::client::height::Height, - ics24::ClientConsensusStatePath, + ics24::{ClientConsensusStatePath, ClientStatePath}, + traits::Id, }, cosmwasm_std::{to_json_vec, ContractResult, Env, SystemResult}, prost::Message, @@ -83,20 +86,16 @@ use { #[allow(clippy::missing_panics_doc)] #[cfg(feature = "stargate")] -pub fn query_consensus_state( +pub fn query_ibc_abci( deps: Deps, env: &Env, - // TODO: Use ClientId here - client_id: String, - height: Height, + path: Path, ) -> Result where Any: Decode, { let query = protos::cosmos::base::tendermint::v1beta1::AbciQueryRequest { - data: ClientConsensusStatePath { client_id, height } - .to_string() - .into_bytes(), + data: path.clone().to_string().into_bytes(), path: "store/ibc/key".to_string(), height: env .block @@ -110,19 +109,63 @@ where path: "/cosmos.base.tendermint.v1beta1.Service/ABCIQuery".into(), data: query.encode_to_vec().into(), }) - .map_err(|e| Error::ConsensusState(format!("{e:?}")))?; + .map_err(|e| Error::ABCI { + path: path.clone(), + err: format!("{e:?}"), + })?; let abci_response_data = match deps.querier.raw_query(&raw) { - SystemResult::Err(system_err) => Err(Error::ConsensusState(format!( - "Querier system error: {system_err}" - ))), - SystemResult::Ok(ContractResult::Err(contract_err)) => Err(Error::ConsensusState(format!( - "Querier contract error: {contract_err}" - ))), + SystemResult::Err(system_err) => Err(Error::ABCI { + path: path.clone(), + err: format!("Querier system error: {system_err}"), + }), + SystemResult::Ok(ContractResult::Err(contract_err)) => Err(Error::ABCI { + path: path.clone(), + err: format!("Querier contract error: {contract_err}"), + }), SystemResult::Ok(ContractResult::Ok(value)) => Ok(value), }?; - let abci_response = AbciQueryResponse::decode(abci_response_data.as_ref()) - .map_err(|e| Error::ConsensusState(format!("{e:?}")))?; - let Any(value) = Any::::decode_as::(&abci_response.value) - .map_err(|e| Error::ConsensusState(format!("{e:?}")))?; + let abci_response = + AbciQueryResponse::decode(abci_response_data.as_ref()).map_err(|e| Error::ABCI { + path: path.clone(), + err: format!("AbciQueryResponse decoding: {e:?}"), + })?; + let Any(value) = + Any::::decode_as::(&abci_response.value).map_err(|e| Error::ABCI { + path, + err: format!("AnyProto decoding: {e:?}"), + })?; Ok(value) } + +#[allow(clippy::missing_panics_doc)] +#[cfg(feature = "stargate")] +pub fn query_consensus_state( + deps: Deps, + env: &Env, + // TODO: Use ClientId here + client_id: String, + height: Height, +) -> Result +where + Any: Decode, +{ + query_ibc_abci::( + deps, + env, + Path::ClientConsensusState(ClientConsensusStatePath { client_id, height }), + ) +} + +#[allow(clippy::missing_panics_doc)] +#[cfg(feature = "stargate")] +pub fn query_client_state( + deps: Deps, + env: &Env, + // TODO: Use ClientId here + client_id: String, +) -> Result +where + Any: Decode, +{ + query_ibc_abci::(deps, env, Path::ClientState(ClientStatePath { client_id })) +} diff --git a/lib/unionlabs/src/ibc/lightclients.rs b/lib/unionlabs/src/ibc/lightclients.rs index 1817834008..2715fba26f 100644 --- a/lib/unionlabs/src/ibc/lightclients.rs +++ b/lib/unionlabs/src/ibc/lightclients.rs @@ -2,6 +2,7 @@ pub mod arbitrum; pub mod berachain; pub mod cometbls; pub mod ethereum; +pub mod evm_in_cosmos; pub mod linea; pub mod scroll; pub mod tendermint; diff --git a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs new file mode 100644 index 0000000000..1e80278bf9 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs @@ -0,0 +1,3 @@ +pub mod client_state; +pub mod consensus_state; +pub mod header; diff --git a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/client_state.rs b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/client_state.rs new file mode 100644 index 0000000000..5bf0837cba --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/client_state.rs @@ -0,0 +1,59 @@ +use core::fmt::Debug; + +use macros::model; + +use crate::{errors::InvalidLength, hash::H160, uint::U256}; + +#[model(proto( + raw(protos::union::ibc::lightclients::evmincosmos::v1::ClientState), + into, + from +))] +pub struct ClientState { + // TODO: This should be ClientId + pub l1_client_id: String, + pub l2_client_id: String, + pub latest_slot: u64, + pub ibc_contract_address: H160, + pub ibc_commitment_slot: U256, +} + +impl From for protos::union::ibc::lightclients::evmincosmos::v1::ClientState { + fn from(value: ClientState) -> Self { + Self { + l1_client_id: value.l1_client_id, + l2_client_id: value.l2_client_id, + latest_slot: value.latest_slot, + ibc_contract_address: value.ibc_contract_address.into(), + ibc_commitment_slot: value.ibc_commitment_slot.to_be_bytes().into(), + } + } +} + +#[derive(Debug, PartialEq, Clone, thiserror::Error)] +pub enum TryFromClientStateError { + #[error("invalid ibc contract address")] + IbcContractAddress(#[source] InvalidLength), + #[error("invalid ibc commitment slot")] + IbcCommitmentSlot(#[source] InvalidLength), +} + +impl TryFrom for ClientState { + type Error = TryFromClientStateError; + + fn try_from( + value: protos::union::ibc::lightclients::evmincosmos::v1::ClientState, + ) -> Result { + Ok(Self { + l1_client_id: value.l1_client_id, + l2_client_id: value.l2_client_id, + latest_slot: value.latest_slot, + ibc_contract_address: value + .ibc_contract_address + .try_into() + .map_err(TryFromClientStateError::IbcContractAddress)?, + ibc_commitment_slot: U256::try_from_be_bytes(&value.ibc_commitment_slot) + .map_err(TryFromClientStateError::IbcCommitmentSlot)?, + }) + } +} diff --git a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/consensus_state.rs b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/consensus_state.rs new file mode 100644 index 0000000000..db6bf39a93 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/consensus_state.rs @@ -0,0 +1,51 @@ +use macros::model; + +use crate::{errors::InvalidLength, hash::H256}; + +#[model(proto( + raw(protos::union::ibc::lightclients::evmincosmos::v1::ConsensusState), + into, + from +))] +pub struct ConsensusState { + pub evm_state_root: H256, + pub ibc_storage_root: H256, + pub timestamp: u64, +} + +impl From for protos::union::ibc::lightclients::evmincosmos::v1::ConsensusState { + fn from(value: ConsensusState) -> Self { + Self { + evm_state_root: value.evm_state_root.into(), + ibc_storage_root: value.ibc_storage_root.into(), + timestamp: value.timestamp, + } + } +} + +#[derive(Debug, PartialEq, Clone, thiserror::Error)] +pub enum TryFromConsensusStateError { + #[error("invalid evm state root")] + EvmStateRoot(#[source] InvalidLength), + #[error("invalid ibc storage root")] + IbcStorageRoot(#[source] InvalidLength), +} + +impl TryFrom for ConsensusState { + type Error = TryFromConsensusStateError; + fn try_from( + value: protos::union::ibc::lightclients::evmincosmos::v1::ConsensusState, + ) -> Result { + Ok(Self { + evm_state_root: value + .evm_state_root + .try_into() + .map_err(TryFromConsensusStateError::EvmStateRoot)?, + ibc_storage_root: value + .ibc_storage_root + .try_into() + .map_err(TryFromConsensusStateError::IbcStorageRoot)?, + timestamp: value.timestamp, + }) + } +} diff --git a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/header.rs b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/header.rs new file mode 100644 index 0000000000..6a3fe4f650 --- /dev/null +++ b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/header.rs @@ -0,0 +1,70 @@ +use macros::model; + +use crate::{ + errors::{required, MissingField}, + ibc::{ + core::{ + client::height::Height, + commitment::merkle_proof::{MerkleProof, TryFromMerkleProofError}, + }, + lightclients::ethereum::{ + account_proof::{AccountProof, TryFromAccountProofError}, + consensus_state::TryFromConsensusStateError, + }, + }, +}; + +#[model(proto( + raw(protos::union::ibc::lightclients::evmincosmos::v1::Header), + into, + from +))] +pub struct Header { + pub l1_height: Height, + pub l2_slot: u64, + pub l2_consensus_state: crate::ibc::lightclients::ethereum::consensus_state::ConsensusState, + pub l2_inclusion_proof: MerkleProof, + pub account_proof: AccountProof, +} + +impl From
for protos::union::ibc::lightclients::evmincosmos::v1::Header { + fn from(value: Header) -> Self { + Self { + l1_height: Some(value.l1_height.into()), + l2_slot: value.l2_slot, + l2_consensus_state: Some(value.l2_consensus_state.into()), + l2_inclusion_proof: Some(value.l2_inclusion_proof.into()), + account_proof: Some(value.account_proof.into()), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum TryFromHeaderError { + MissingField(MissingField), + L2ConsensusState(TryFromConsensusStateError), + L2InclusionProof(TryFromMerkleProofError), + AccountProof(TryFromAccountProofError), +} + +impl TryFrom for Header { + type Error = TryFromHeaderError; + + fn try_from( + value: protos::union::ibc::lightclients::evmincosmos::v1::Header, + ) -> Result { + Ok(Self { + l1_height: required!(value.l1_height)?.into(), + l2_slot: value.l2_slot, + l2_consensus_state: required!(value.l2_consensus_state)? + .try_into() + .map_err(TryFromHeaderError::L2ConsensusState)?, + l2_inclusion_proof: required!(value.l2_inclusion_proof)? + .try_into() + .map_err(TryFromHeaderError::L2InclusionProof)?, + account_proof: required!(value.account_proof)? + .try_into() + .map_err(TryFromHeaderError::AccountProof)?, + }) + } +} diff --git a/lib/unionlabs/src/lib.rs b/lib/unionlabs/src/lib.rs index a98e706355..9e3f272c13 100644 --- a/lib/unionlabs/src/lib.rs +++ b/lib/unionlabs/src/lib.rs @@ -156,6 +156,7 @@ pub enum WasmClientType { Arbitrum, Linea, Berachain, + EvmInCosmos, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -204,6 +205,7 @@ impl FromStr for WasmClientType { "Arbitrum" => Ok(WasmClientType::Arbitrum), "Linea" => Ok(WasmClientType::Linea), "Berachain" => Ok(WasmClientType::Berachain), + "EvmInCosmos" => Ok(WasmClientType::EvmInCosmos), _ => Err(WasmClientTypeParseError::UnknownType(s.to_string())), } } @@ -220,6 +222,7 @@ impl Display for WasmClientType { Self::Arbitrum => write!(f, "Arbitrum"), Self::Linea => write!(f, "Linea"), Self::Berachain => write!(f, "Berachain"), + Self::EvmInCosmos => write!(f, "EvmInCosmos"), } } } diff --git a/light-clients/evm-in-cosmos-light-client/Cargo.toml b/light-clients/evm-in-cosmos-light-client/Cargo.toml new file mode 100644 index 0000000000..77d2a933b7 --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["Union Labs"] +edition = "2021" +license-file = { workspace = true } +name = "evm-in-cosmos-light-client" +publish = false +version = "0.1.0" + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true, features = ["abort"] } +ethereum-light-client = { workspace = true, features = ["mainnet", "library"] } +ics008-wasm-client = { workspace = true } +ics23 = { workspace = true } +protos = { workspace = true } +thiserror = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi", "stargate"] } + +[features] +default = [] +# enabling this feature disables exposing the entrypoints and setting `#[global_allocator]` +library = [] diff --git a/light-clients/evm-in-cosmos-light-client/evm-in-cosmos-light-client.nix b/light-clients/evm-in-cosmos-light-client/evm-in-cosmos-light-client.nix new file mode 100644 index 0000000000..41b55ba375 --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/evm-in-cosmos-light-client.nix @@ -0,0 +1,19 @@ +{ ... }: { + perSystem = { crane, lib, ensure-wasm-client-type, ... }: + let + workspace = (crane.buildWasmContract { + crateDirFromRoot = "light-clients/evm-in-cosmos-light-client"; + checks = [ + (file_path: '' + ${ensure-wasm-client-type { + inherit file_path; + type = "EvmInCosmos"; + }} + '') + ]; + }); + in + { + inherit (workspace) packages checks; + }; +} diff --git a/light-clients/evm-in-cosmos-light-client/src/client.rs b/light-clients/evm-in-cosmos-light-client/src/client.rs new file mode 100644 index 0000000000..68f8abe2ee --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/src/client.rs @@ -0,0 +1,252 @@ +use cosmwasm_std::{Deps, DepsMut, Env}; +use ethereum_light_client::client::{do_verify_membership, do_verify_non_membership}; +use ics008_wasm_client::{ + storage_utils::{ + read_client_state, read_consensus_state, save_consensus_state, update_client_state, + }, + IbcClient, IbcClientError, Status, StorageState, +}; +use ics23::ibc_api::SDK_SPECS; +use unionlabs::{ + cosmwasm::wasm::union::custom_query::{ + query_client_state, query_consensus_state, UnionCustomQuery, + }, + encoding::{DecodeAs, EncodeAs, Proto}, + ibc::{ + core::{ + client::{genesis_metadata::GenesisMetadata, height::Height}, + commitment::merkle_path::MerklePath, + }, + lightclients::{ + cometbls, + ethereum::{self, storage_proof::StorageProof}, + evm_in_cosmos::{ + client_state::ClientState, consensus_state::ConsensusState, header::Header, + }, + wasm, + }, + }, + ics24::{ClientConsensusStatePath, Path}, +}; + +use crate::errors::Error; + +type WasmClientState = wasm::client_state::ClientState; +type WasmConsensusState = wasm::consensus_state::ConsensusState; +type WasmL1ConsensusState = + wasm::consensus_state::ConsensusState; +type WasmL1ClientState = wasm::client_state::ClientState; +type WasmL2ConsensusState = + wasm::consensus_state::ConsensusState; + +pub struct EvmInCosmosLightClient; + +impl IbcClient for EvmInCosmosLightClient { + type Error = Error; + + type CustomQuery = UnionCustomQuery; + + type Header = Header; + + type Misbehaviour = Header; + + type ClientState = ClientState; + + type ConsensusState = ConsensusState; + + type Encoding = Proto; + + fn verify_membership( + deps: Deps, + height: Height, + _delay_time_period: u64, + _delay_block_period: u64, + proof: Vec, + mut path: MerklePath, + value: ics008_wasm_client::StorageState, + ) -> Result<(), IbcClientError> { + let consensus_state: WasmConsensusState = + read_consensus_state(deps, &height)?.ok_or(Error::ConsensusStateNotFound(height))?; + let client_state: WasmClientState = read_client_state(deps)?; + + let path = path.key_path.pop().ok_or(Error::EmptyIbcPath)?; + + // This storage root is verified during the header update, so we don't need to verify it again. + let storage_root = consensus_state.data.ibc_storage_root; + + let storage_proof = + StorageProof::decode_as::(&proof).map_err(Error::StorageProofDecode)?; + + match value { + StorageState::Occupied(value) => do_verify_membership( + path, + storage_root, + client_state.data.ibc_commitment_slot, + storage_proof, + value, + ) + .map_err(Error::EthereumLightClient)?, + StorageState::Empty => do_verify_non_membership( + path, + storage_root, + client_state.data.ibc_commitment_slot, + storage_proof, + ) + .map_err(Error::EthereumLightClient)?, + } + + Ok(()) + } + + fn verify_header( + deps: Deps, + env: Env, + header: Self::Header, + ) -> Result<(), IbcClientError> { + let client_state: WasmClientState = read_client_state(deps)?; + let l1_consensus_state = query_consensus_state::( + deps, + &env, + client_state.data.l1_client_id.clone(), + header.l1_height, + ) + .map_err(Error::CustomQuery)?; + let client_consensus_state_path = Path::ClientConsensusState(ClientConsensusStatePath { + client_id: client_state.data.l2_client_id, + height: Height { + revision_number: 0, + revision_height: header.l2_slot, + }, + }); + // The ethereum consensus state is stored in proto-encoded wasm-wrapped form. + let normalized_l2_consensus_state = WasmL2ConsensusState { + data: header.l2_consensus_state, + }; + // Verify inclusion of the ethereum consensus state against union. + ics23::ibc_api::verify_membership( + &header.l2_inclusion_proof, + &SDK_SPECS, + &l1_consensus_state.data.app_hash, + &[ + b"ibc".to_vec(), + client_consensus_state_path.to_string().into_bytes(), + ], + normalized_l2_consensus_state.encode_as::(), + ) + .map_err(Error::VerifyL2Membership)?; + Ok(()) + } + + fn verify_misbehaviour( + _deps: Deps, + _env: Env, + _misbehaviour: Self::Misbehaviour, + ) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn update_state( + mut deps: DepsMut, + _env: Env, + header: Self::Header, + ) -> Result, IbcClientError> { + let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + + let updated_height = Height { + revision_number: client_state.latest_height.revision_number, + revision_height: header.l1_height.revision_height, + }; + + if client_state.latest_height < header.l1_height { + client_state.data.latest_slot = updated_height.revision_height; + update_client_state::( + deps.branch(), + client_state, + updated_height.revision_height, + ); + } + + let consensus_state = WasmConsensusState { + data: ConsensusState { + evm_state_root: header.l2_consensus_state.state_root, + ibc_storage_root: header.l2_consensus_state.storage_root, + timestamp: header.l2_consensus_state.timestamp, + }, + }; + save_consensus_state::(deps, consensus_state, &updated_height); + Ok(vec![updated_height]) + } + + fn update_state_on_misbehaviour( + _deps: DepsMut, + _env: Env, + _client_message: Vec, + ) -> Result<(), IbcClientError> { + panic!("impossible; misbehavior check is done on the l1 light client.") + } + + fn check_for_misbehaviour_on_header( + _deps: Deps, + _header: Self::Header, + ) -> Result> { + Ok(false) + } + + fn check_for_misbehaviour_on_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result> { + Err(Error::Unimplemented.into()) + } + + fn verify_upgrade_and_update_state( + _deps: DepsMut, + _upgrade_client_state: Self::ClientState, + _upgrade_consensus_state: Self::ConsensusState, + _proof_upgrade_client: Vec, + _proof_upgrade_consensus_state: Vec, + ) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn migrate_client_store(_deps: DepsMut) -> Result<(), IbcClientError> { + Err(Error::Unimplemented.into()) + } + + fn status(deps: Deps, env: &Env) -> Result> { + let client_state: WasmClientState = read_client_state(deps)?; + let l1_client_state = query_client_state::( + deps, + env, + client_state.data.l1_client_id.clone(), + ) + .map_err(Error::CustomQuery)?; + + if l1_client_state.data.frozen_height != Height::default() { + return Ok(Status::Frozen); + } + + let Some(_) = read_consensus_state::(deps, &client_state.latest_height)? else { + return Ok(Status::Expired); + }; + + Ok(Status::Active) + } + + fn export_metadata( + _deps: Deps, + _env: &Env, + ) -> Result, IbcClientError> { + Ok(Vec::new()) + } + + fn timestamp_at_height( + deps: Deps, + height: Height, + ) -> Result> { + Ok(read_consensus_state::(deps, &height)? + .ok_or(Error::ConsensusStateNotFound(height))? + .data + .timestamp) + } +} diff --git a/light-clients/evm-in-cosmos-light-client/src/contract.rs b/light-clients/evm-in-cosmos-light-client/src/contract.rs new file mode 100644 index 0000000000..cf14a15d1b --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/src/contract.rs @@ -0,0 +1,56 @@ +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; +use ics008_wasm_client::{ + storage_utils::{save_proto_client_state, save_proto_consensus_state}, + CustomQueryOf, InstantiateMsg, +}; +use protos::ibc::lightclients::wasm::v1::{ + ClientState as ProtoClientState, ConsensusState as ProtoConsensusState, +}; +use unionlabs::{ + encoding::{DecodeAs, Proto}, + ibc::{core::client::height::Height, lightclients::evm_in_cosmos::client_state::ClientState}, +}; + +use crate::{client::EvmInCosmosLightClient, errors::Error}; + +// NOTE(aeryz): the fact that the host module forces the light clients to store and use the wasm wrapping +// in the client state makes this code kinda messy. But this is going to be resolved in the future versions +// of IBC (probably v9). When that feature is implemented, we can move this to the ics008 macro. +#[entry_point] +pub fn instantiate( + mut deps: DepsMut>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let client_state = + ClientState::decode_as::(&msg.client_state).map_err(Error::ClientStateDecode)?; + + save_proto_consensus_state::( + deps.branch(), + ProtoConsensusState { + data: msg.consensus_state.into(), + }, + &Height { + revision_number: 0, + revision_height: client_state.latest_slot, + }, + ); + save_proto_client_state::( + deps, + ProtoClientState { + data: msg.client_state.into(), + checksum: msg.checksum.into(), + latest_height: Some( + Height { + revision_number: 0, + revision_height: client_state.latest_slot, + } + .into(), + ), + }, + ); + Ok(Response::default()) +} + +ics008_wasm_client::define_cosmwasm_light_client_contract!(EvmInCosmosLightClient, EvmInCosmos); diff --git a/light-clients/evm-in-cosmos-light-client/src/errors.rs b/light-clients/evm-in-cosmos-light-client/src/errors.rs new file mode 100644 index 0000000000..9acfccf28f --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/src/errors.rs @@ -0,0 +1,47 @@ +use ethereum_light_client::errors::CanonicalizeStoredValueError; +use ics008_wasm_client::IbcClientError; +use unionlabs::{ + encoding::{DecodeErrorOf, Proto}, + ibc::{ + core::client::height::Height, + lightclients::{ethereum::storage_proof::StorageProof, evm_in_cosmos}, + }, +}; + +use crate::client::EvmInCosmosLightClient; + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +pub enum Error { + #[error("unimplemented feature")] + Unimplemented, + + #[error("unable to decode storage proof")] + StorageProofDecode(#[source] DecodeErrorOf), + + #[error("unable to decode client state")] + ClientStateDecode(#[source] DecodeErrorOf), + + #[error(transparent)] + CanonicalizeStoredValue(#[from] CanonicalizeStoredValueError), + + #[error("custom query error")] + CustomQuery(#[from] unionlabs::cosmwasm::wasm::union::custom_query::Error), + + #[error("consensus state not found at height {0}")] + ConsensusStateNotFound(Height), + + #[error("IBC path is empty")] + EmptyIbcPath, + + #[error("verify l2 membership error")] + VerifyL2Membership(#[from] ics23::ibc_api::VerifyMembershipError), + + #[error(transparent)] + EthereumLightClient(#[from] ethereum_light_client::errors::Error), +} + +impl From for IbcClientError { + fn from(value: Error) -> Self { + IbcClientError::ClientSpecific(value) + } +} diff --git a/light-clients/evm-in-cosmos-light-client/src/lib.rs b/light-clients/evm-in-cosmos-light-client/src/lib.rs new file mode 100644 index 0000000000..1b07a37a4d --- /dev/null +++ b/light-clients/evm-in-cosmos-light-client/src/lib.rs @@ -0,0 +1,4 @@ +pub mod client; +#[cfg(any(test, not(feature = "library")))] +pub mod contract; +pub mod errors; diff --git a/uniond/proto/union/ibc/lightclients/evmincosmos/v1/evmincosmos.proto b/uniond/proto/union/ibc/lightclients/evmincosmos/v1/evmincosmos.proto new file mode 100644 index 0000000000..44fa77bb4c --- /dev/null +++ b/uniond/proto/union/ibc/lightclients/evmincosmos/v1/evmincosmos.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; +package union.ibc.lightclients.evmincosmos.v1; + +option go_package = "union/ibc/lightclients/evmincosmos"; +import "ibc/core/client/v1/client.proto"; +import "union/ibc/lightclients/ethereum/v1/ethereum.proto"; +import "ibc/core/commitment/v1/commitment.proto"; + +message ClientState { + string l1_client_id = 1; + string l2_client_id = 2; + uint64 latest_slot = 3; + + // Evm + bytes ibc_commitment_slot = 5; + bytes ibc_contract_address = 6; +} + +message ConsensusState { + bytes evm_state_root = 1; + bytes ibc_storage_root = 2; + uint64 timestamp = 3; +} + +message Header { + .ibc.core.client.v1.Height l1_height = 1; + uint64 l2_slot = 2; + .union.ibc.lightclients.ethereum.v1.ConsensusState l2_consensus_state = 3; + // Proof of the l2 consensus state in the l1 client. + .ibc.core.commitment.v1.MerkleProof l2_inclusion_proof = 4; + // Proof of the ibc contract in the evm state root. + .union.ibc.lightclients.ethereum.v1.AccountProof account_proof = 5; +}