diff --git a/Cargo.lock b/Cargo.lock index 13a9705fa3..30b69b4066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3369,6 +3369,18 @@ dependencies = [ "unionlabs", ] +[[package]] +name = "cosmos-state-lens-light-client-types" +version = "0.1.0" +dependencies = [ + "alloy", + "hex-literal", + "protos", + "serde", + "thiserror", + "unionlabs", +] + [[package]] name = "cosmwasm-core" version = "2.1.4" @@ -4891,11 +4903,31 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "evm-in-cosmos-light-client" +version = "0.1.0" +dependencies = [ + "cometbls-light-client", + "cosmwasm-std 2.1.4", + "ethereum-light-client-types", + "evm-state-lens-light-client-types", + "evm-storage-verifier", + "ibc-union-light-client", + "ibc-union-msg", + "ibc-union-spec", + "ics23", + "rlp", + "serde", + "thiserror", + "unionlabs", +] + [[package]] name = "evm-state-lens-light-client-types" version = "0.1.0" dependencies = [ "alloy", + "bincode 2.0.0-rc.3", "protos", "serde", "thiserror", @@ -13049,6 +13081,27 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-bootstrap-module-state-lens-ics23-ics23" +version = "0.1.0" +dependencies = [ + "alloy", + "beacon-api", + "beacon-api-types", + "cometbft-rpc", + "cosmos-state-lens-light-client-types", + "ibc-union-spec", + "jsonrpsee", + "serde", + "serde_json", + "tendermint-light-client-types", + "tokio", + "tracing", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-bootstrap-module-tendermint" version = "0.1.0" @@ -13149,6 +13202,27 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-module-state-lens-ics23-ics23" +version = "0.1.0" +dependencies = [ + "alloy", + "cosmos-state-lens-light-client-types", + "futures", + "jsonrpsee", + "macros", + "protos", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-module-tendermint" version = "0.1.0" @@ -13296,6 +13370,35 @@ dependencies = [ "voyager-vm", ] +[[package]] +name = "voyager-client-update-plugin-state-lens-ics23-ics23" +version = "0.1.0" +dependencies = [ + "alloy", + "cometbft-rpc", + "cometbft-types", + "cosmos-state-lens-light-client-types", + "dashmap 5.5.3", + "enumorph", + "futures", + "ibc-union-spec", + "ics23", + "jsonrpsee", + "macros", + "num-bigint 0.4.6", + "prost 0.12.6", + "protos", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "unionlabs", + "voyager-message", + "voyager-vm", +] + [[package]] name = "voyager-client-update-plugin-tendermint" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 885e073d46..753749c514 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ members = [ "lib/linea-light-client-types", "lib/movement-light-client-types", "lib/state-lens/evm-light-client-types", + "lib/state-lens/cosmos-light-client-types", # these will all be re enabled and updated once ethereum-light-client is updated @@ -85,7 +86,7 @@ members = [ "cosmwasm/ibc-union/light-clients/arbitrum", "cosmwasm/ibc-union/light-clients/berachain", "cosmwasm/ibc-union/light-clients/cometbls", - # "cosmwasm/ibc-union/light-clients/evm-in-cosmos", + "cosmwasm/ibc-union/light-clients/evm-in-cosmos", # "cosmwasm/ibc-union/light-clients/scroll", "cosmwasm/ibc-union/light-clients/tendermint", # "cosmwasm/ibc-union/light-clients/linea", @@ -117,12 +118,14 @@ members = [ "voyager/modules/client/movement", "voyager/modules/client/tendermint", "voyager/modules/client/state-lens/evm", + "voyager/modules/client/state-lens/ics23-ics23", "voyager/modules/client-bootstrap/cometbls", "voyager/modules/client-bootstrap/ethereum", "voyager/modules/client-bootstrap/movement", "voyager/modules/client-bootstrap/tendermint", "voyager/modules/client-bootstrap/state-lens/evm", + "voyager/modules/client-bootstrap/state-lens/ics23-ics23", "voyager/modules/consensus/berachain", "voyager/modules/consensus/cometbls", @@ -136,6 +139,7 @@ members = [ "voyager/plugins/client-update/movement", "voyager/plugins/client-update/tendermint", "voyager/plugins/client-update/state-lens/evm", + "voyager/plugins/client-update/state-lens/ics23-ics23", "voyager/plugins/periodic-client-update", @@ -199,6 +203,7 @@ arbitrum-light-client-types = { path = "lib/arbitrum-light-client-types", defaul arbitrum-verifier = { path = "lib/arbitrum-verifier", default-features = false } cometbls-groth16-verifier = { path = "lib/cometbls-groth16-verifier", default-features = false } +cometbls-light-client = { path = "cosmwasm/ibc-union/light-clients/cometbls", default-features = false } cometbls-light-client-types = { path = "lib/cometbls-light-client-types", default-features = false } scroll-light-client-types = { path = "lib/scroll-light-client-types", default-features = false } @@ -213,7 +218,8 @@ scroll-api = { path = "lib/scroll-api", default-features = fal scroll-codec = { path = "lib/scroll-codec", default-features = false } scroll-rpc = { path = "lib/scroll-rpc", default-features = false } -evm-state-lens-light-client-types = { path = "lib/state-lens/evm-light-client-types", default-features = false } +cosmos-state-lens-light-client-types = { path = "lib/state-lens/cosmos-light-client-types", default-features = false } +evm-state-lens-light-client-types = { path = "lib/state-lens/evm-light-client-types", default-features = false } tendermint-light-client = { path = "cosmwasm/ibc-union/light-clients/tendermint", default-features = false } tendermint-light-client-types = { path = "lib/tendermint-light-client-types", default-features = false } diff --git a/cosmwasm/ibc-union/core/light-client-interface/Cargo.toml b/cosmwasm/ibc-union/core/light-client-interface/Cargo.toml index 8c48e9bbb2..cbd9219f82 100644 --- a/cosmwasm/ibc-union/core/light-client-interface/Cargo.toml +++ b/cosmwasm/ibc-union/core/light-client-interface/Cargo.toml @@ -6,9 +6,9 @@ repository.workspace = true version = "0.1.0" [dependencies] -cosmwasm-schema = { workspace = true, package = "cosmwasm-schema" } -cosmwasm-std = { workspace = true, package = "cosmwasm-std" } -cw-storage-plus = { workspace = true, package = "cw-storage-plus" } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } ibc-union-msg = { workspace = true } macros.workspace = true schemars = { workspace = true } diff --git a/cosmwasm/ibc-union/core/light-client-interface/src/lib.rs b/cosmwasm/ibc-union/core/light-client-interface/src/lib.rs index 160d1b86d8..c90c36ce8b 100644 --- a/cosmwasm/ibc-union/core/light-client-interface/src/lib.rs +++ b/cosmwasm/ibc-union/core/light-client-interface/src/lib.rs @@ -24,6 +24,7 @@ pub mod state; // These are only used for `key` calculation. We don't want this crate to depend on `ibc-union`. pub const CLIENT_STATES: Map = Map::new("client_states"); pub const CLIENT_CONSENSUS_STATES: Map<(u32, u64), Binary> = Map::new("client_consensus_states"); +const CLIENT_IMPLS: Map = Map::new("client_impls"); const QUERY_STORE: Item = Item::new("query_store"); // TODO: Add #[source] to all variants @@ -121,6 +122,47 @@ impl<'a, T: IbcClient> IbcClientCtx<'a, T> { height, ) } + + pub fn verify_membership( + &self, + client_id: u32, + height: u64, + path: Bytes, + storage_proof: Client::StorageProof, + value: Bytes, + ) -> Result<(), IbcClientError> { + let client_impl = client_impl(self.deps.querier.into_empty(), &self.ibc_host, client_id)?; + self.deps.querier.query_wasm_smart::<()>( + &client_impl, + &QueryMsg::VerifyMembership { + client_id, + height, + proof: storage_proof.encode_as::().into(), + path, + value, + }, + )?; + + Ok(()) + } +} + +fn client_impl( + querier: QuerierWrapper, + ibc_host: &Addr, + client_id: u32, +) -> Result> { + let addr = from_json::( + querier + .query_wasm_raw(ibc_host.to_string(), CLIENT_IMPLS.key(client_id).to_vec())? + .ok_or_else(|| { + IbcClientError::Std(StdError::generic_err(format!( + "unable to read client state of client {client_id}" + ))) + })?, + )?; + + Ok(addr) } pub trait IbcClient: Sized { @@ -136,7 +178,10 @@ pub trait IbcClient: Sized { /// a common encoding scheme for state lenses. When doing state lenses, client X will read the /// consensus state of client Y by assuming it's state is ethabi-encoded. type ConsensusState: Decode + Encode + Debug + 'static; - type StorageProof: Decode + Debug + 'static; + type StorageProof: Encode + + Decode + + Debug + + 'static; type Encoding: Encoding; fn verify_membership( diff --git a/cosmwasm/ibc-union/light-clients/cometbls/Cargo.toml b/cosmwasm/ibc-union/light-clients/cometbls/Cargo.toml index c92fccdb75..b331b3a87e 100644 --- a/cosmwasm/ibc-union/light-clients/cometbls/Cargo.toml +++ b/cosmwasm/ibc-union/light-clients/cometbls/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] cometbls-groth16-verifier = { workspace = true } -cometbls-light-client-types = { workspace = true, features = ["serde", "ethabi", "proto"] } +cometbls-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } cosmwasm-std = { workspace = true, features = ["abort", "iterator"] } ibc-union-light-client = { workspace = true } ibc-union-msg = { workspace = true } diff --git a/cosmwasm/ibc-union/light-clients/cometbls/src/client.rs b/cosmwasm/ibc-union/light-clients/cometbls/src/client.rs index e954c72984..ed7ba5e27c 100644 --- a/cosmwasm/ibc-union/light-clients/cometbls/src/client.rs +++ b/cosmwasm/ibc-union/light-clients/cometbls/src/client.rs @@ -9,7 +9,7 @@ use ibc_union_light_client::IbcClientCtx; use ibc_union_msg::lightclient::Status; use ics23::ibc_api::SDK_SPECS; use unionlabs::{ - encoding::Proto, + encoding::Bincode, ibc::core::{ client::height::Height, commitment::{merkle_proof::MerkleProof, merkle_root::MerkleRoot}, @@ -38,26 +38,33 @@ impl ibc_union_light_client::IbcClient for CometblsLightClient, - _height: u64, - _key: Vec, - _storage_proof: Self::StorageProof, - _value: Vec, + ctx: ibc_union_light_client::IbcClientCtx, + height: u64, + key: Vec, + storage_proof: Self::StorageProof, + value: Vec, ) -> Result<(), ibc_union_light_client::IbcClientError> { - // let consensus_state = ctx.read_self_consensus_state(height)?; - // Ok(ics23::ibc_api::verify_membership( - // &storage_proof, - // &SDK_SPECS, - // &consensus_state.app_hash, - // // FIXME: concat(contract, key) right? - // &[b"wasm".to_vec(), key], - // value, - // ) - // .map_err(Into::::into)?) - Ok(()) + let client_state = ctx.read_self_client_state()?; + let consensus_state = ctx.read_self_consensus_state(height)?; + Ok(ics23::ibc_api::verify_membership( + &storage_proof, + &SDK_SPECS, + &consensus_state.app_hash, + &[ + b"wasm".to_vec(), + 0x3u8 + .to_le_bytes() + .into_iter() + .chain(client_state.contract_address) + .chain(key) + .collect::>(), + ], + value, + ) + .map_err(Into::::into)?) } fn verify_non_membership( diff --git a/cosmwasm/ibc-union/light-clients/ethereum/src/contract.rs b/cosmwasm/ibc-union/light-clients/ethereum/src/contract.rs index 51d3b1332d..73ae64d57c 100644 --- a/cosmwasm/ibc-union/light-clients/ethereum/src/contract.rs +++ b/cosmwasm/ibc-union/light-clients/ethereum/src/contract.rs @@ -4,14 +4,13 @@ use cosmwasm_std::{ from_json, to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, }; -use ethereum_light_client_types::{ClientState, ConsensusState}; +use ethereum_light_client_types::ClientState; use ibc_union_light_client::{ msg::{InstantiateMsg, QueryMsg}, read_consensus_state, state::IBC_HOST, - IbcClientError, CLIENT_CONSENSUS_STATES, CLIENT_STATES, + IbcClientError, CLIENT_STATES, }; -use ibc_union_msg::module::IbcUnionMsg; use serde::{Deserialize, Serialize}; use unionlabs::{ encoding::{Bincode, EncodeAs, EthAbi}, diff --git a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/Cargo.toml b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/Cargo.toml index 2c2809eda0..7ad6fd1974 100644 --- a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/Cargo.toml +++ b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/Cargo.toml @@ -13,14 +13,19 @@ workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -cometbls-light-client-types = { workspace = true, features = ["proto"] } -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"] } +cometbls-light-client = { workspace = true, features = ["library"] } +cosmwasm-std = { workspace = true, features = ["abort"] } +ethereum-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } +evm-state-lens-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } +evm-storage-verifier = { workspace = true } +ibc-union-light-client = { workspace = true } +ibc-union-msg = { workspace = true } +ibc-union-spec = { workspace = true } +ics23 = { workspace = true } +rlp = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi", "stargate", "bincode"] } [features] default = [] diff --git a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/client.rs b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/client.rs index 3161d0e359..573a09fee4 100644 --- a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/client.rs +++ b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/client.rs @@ -1,51 +1,25 @@ -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 cometbls_light_client::client::CometblsLightClient; +use cosmwasm_std::Empty; +use ethereum_light_client_types::StorageProof; +use evm_state_lens_light_client_types::{ClientState, ConsensusState, Header}; +use ibc_union_light_client::IbcClient; +use ibc_union_msg::lightclient::Status; +use ibc_union_spec::ConsensusStatePath; 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::{ - ethereum::{self, storage_proof::StorageProof}, - evm_in_cosmos::{ - client_state::ClientState, consensus_state::ConsensusState, header::Header, - }, - wasm, - }, - }, - ics24::{ClientConsensusStatePath, Path}, + encoding::Bincode, + ethereum::{ibc_commitment_key, keccak256}, + primitives::H256, + uint::U256, }; use crate::errors::Error; -type WasmClientState = wasm::client_state::ClientState; -type WasmConsensusState = wasm::consensus_state::ConsensusState; -type WasmL1ConsensusState = wasm::consensus_state::ConsensusState< - cometbls_light_client_types::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 CustomQuery = Empty; type Header = Header; @@ -55,190 +29,214 @@ impl IbcClient for EvmInCosmosLightClient { type ConsensusState = ConsensusState; - type Encoding = Proto; + type StorageProof = StorageProof; + + type Encoding = Bincode; 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, storage_proof, value) - .map_err(Error::EthereumLightClient)? - } - StorageState::Empty => do_verify_non_membership(path, storage_root, storage_proof) - .map_err(Error::EthereumLightClient)?, - } + ctx: ibc_union_light_client::IbcClientCtx, + height: u64, + key: Vec, + storage_proof: Self::StorageProof, + value: Vec, + ) -> Result<(), ibc_union_light_client::IbcClientError> { + let consensus_state = ctx.read_self_consensus_state(height)?; + + verify_membership(key, consensus_state.storage_root, storage_proof, value)?; 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.parse().unwrap(), - 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)?; + fn verify_non_membership( + ctx: ibc_union_light_client::IbcClientCtx, + height: u64, + key: Vec, + storage_proof: Self::StorageProof, + ) -> Result<(), ibc_union_light_client::IbcClientError> { + let consensus_state = ctx.read_self_consensus_state(height)?; + + verify_non_membership(key, consensus_state.storage_root, storage_proof)?; + Ok(()) } - fn verify_misbehaviour( - _deps: Deps, - _env: Env, - _misbehaviour: Self::Misbehaviour, - ) -> Result<(), IbcClientError> { - Err(Error::Unimplemented.into()) + fn get_timestamp(consensus_state: &Self::ConsensusState) -> u64 { + consensus_state.timestamp + } + + fn get_latest_height(client_state: &Self::ClientState) -> u64 { + client_state.l2_latest_height + } + + fn status(_client_state: &Self::ClientState) -> Status { + // FIXME: expose the ctx to this call to allow threading this call to L1 + // client. generally, we want to thread if a client is an L2 so always + // provide the ctx? + // 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) + Status::Active + } + + fn verify_creation( + _client_state: &Self::ClientState, + _consensus_state: &Self::ConsensusState, + ) -> Result<(), ibc_union_light_client::IbcClientError> { + Ok(()) } - fn update_state( - mut deps: DepsMut, - _env: Env, + fn verify_header( + ctx: ibc_union_light_client::IbcClientCtx, header: Self::Header, - ) -> Result, IbcClientError> { - let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + ) -> Result< + (u64, Self::ClientState, Self::ConsensusState), + ibc_union_light_client::IbcClientError, + > { + let mut client_state = ctx.read_self_client_state()?; + + ctx.verify_membership::( + client_state.l1_client_id, + header.l1_height.height(), + ConsensusStatePath { + client_id: client_state.l2_client_id, + height: header.l2_height.height(), + } + .key() + .into_bytes(), + header.l2_consensus_state_proof.clone(), + keccak256(&header.l2_consensus_state).into(), + ) + .map_err(Error::L1Error)?; - let updated_height = Height { - revision_number: client_state.latest_height.revision_number, - revision_height: header.l1_height.revision_height, - }; + let l2_timestamp = extract_uint64( + &header.l2_consensus_state, + client_state.timestamp_offset as usize, + ); + + let l2_state_root = extract_bytes32( + &header.l2_consensus_state, + client_state.state_root_offset as usize, + ); - 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 l2_storage_root = extract_bytes32( + &header.l2_consensus_state, + client_state.storage_root_offset as usize, + ); + + if client_state.l2_latest_height < header.l2_height.height() { + client_state.l2_latest_height = header.l2_height.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, - }, + let consensus_state = ConsensusState { + timestamp: l2_timestamp, + state_root: l2_state_root, + storage_root: l2_storage_root, }; - 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) + Ok((header.l2_height.height(), client_state, consensus_state)) } - fn check_for_misbehaviour_on_misbehaviour( - _deps: Deps, + fn misbehaviour( + _ctx: ibc_union_light_client::IbcClientCtx, _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()) + ) -> Result> { + unimplemented!() } +} - fn migrate_client_store(_deps: DepsMut) -> Result<(), IbcClientError> { - Err(Error::Unimplemented.into()) - } +fn extract_uint64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes( + data[offset..offset + 8] + .try_into() + .expect("impossible; qed"), + ) +} - 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)?; +fn extract_bytes32(data: &[u8], offset: usize) -> H256 { + H256::new( + data[offset..offset + 32] + .try_into() + .expect("impossible; qed"), + ) +} - if l1_client_state.data.frozen_height != Height::default() { - return Ok(Status::Frozen); - } +pub fn verify_membership( + key: Vec, + storage_root: H256, + storage_proof: StorageProof, + value: Vec, +) -> Result<(), Error> { + check_commitment_key( + H256::try_from(&key).map_err(|_| Error::InvalidCommitmentKeyLength(key))?, + storage_proof.key, + )?; + + let value = H256::try_from(&value).map_err(|_| Error::InvalidCommitmentValueLength(value))?; + + let proof_value = H256::from(storage_proof.value.to_be_bytes()); + + if value != proof_value { + return Err(Error::StoredValueMismatch { + expected: value, + stored: proof_value, + }); + } - let Some(_) = read_consensus_state::(deps, &client_state.latest_height)? else { - return Ok(Status::Expired); - }; + evm_storage_verifier::verify_storage_proof( + storage_root, + storage_proof.key, + &rlp::encode(&storage_proof.value), + &storage_proof.proof, + ) + .map_err(Error::VerifyStorageProof) +} - Ok(Status::Active) - } +pub fn check_commitment_key(path: H256, key: U256) -> Result<(), Error> { + let expected_commitment_key = ibc_commitment_key(path); - fn export_metadata( - _deps: Deps, - _env: &Env, - ) -> Result, IbcClientError> { - Ok(Vec::new()) + if expected_commitment_key != key { + Err(Error::InvalidCommitmentKey { + expected: expected_commitment_key, + found: key, + }) + } else { + Ok(()) } +} - fn timestamp_at_height( - deps: Deps, - height: Height, - ) -> Result> { - Ok(read_consensus_state::(deps, &height)? - .ok_or(Error::ConsensusStateNotFound(height))? - .data - .timestamp) +pub fn verify_non_membership( + key: Vec, + storage_root: H256, + storage_proof: StorageProof, +) -> Result<(), Error> { + check_commitment_key( + H256::try_from(&key).map_err(|_| Error::InvalidCommitmentKeyLength(key))?, + storage_proof.key, + )?; + + if evm_storage_verifier::verify_storage_absence( + storage_root, + storage_proof.key, + &storage_proof.proof, + ) + .map_err(Error::VerifyStorageAbsence)? + { + Ok(()) + } else { + Err(Error::CounterpartyStorageNotNil) } } diff --git a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/contract.rs b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/contract.rs index cf14a15d1b..0c4bf8b08b 100644 --- a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/contract.rs +++ b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/contract.rs @@ -1,56 +1,36 @@ -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}, +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use ibc_union_light_client::{ + msg::{InstantiateMsg, QueryMsg}, + IbcClientError, }; -use crate::{client::EvmInCosmosLightClient, errors::Error}; +use crate::client::EvmInCosmosLightClient; -// 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] +#[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - mut deps: DepsMut>, - _env: Env, - _info: MessageInfo, + deps: DepsMut, + env: Env, + info: MessageInfo, msg: InstantiateMsg, -) -> Result { - let client_state = - ClientState::decode_as::(&msg.client_state).map_err(Error::ClientStateDecode)?; +) -> Result> { + ibc_union_light_client::instantiate(deps, env, info, msg) +} - 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()) +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + ibc_union_light_client::query::(deps, env, msg).map_err(Into::into) } -ics008_wasm_client::define_cosmwasm_light_client_contract!(EvmInCosmosLightClient, EvmInCosmos); +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MigrateMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + _deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result> { + Ok(Response::new()) +} diff --git a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/errors.rs b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/errors.rs index 9acfccf28f..3e29b15ca5 100644 --- a/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/errors.rs +++ b/cosmwasm/ibc-union/light-clients/evm-in-cosmos/src/errors.rs @@ -1,32 +1,15 @@ -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 cometbls_light_client::client::CometblsLightClient; +use cosmwasm_std::StdError; +use ibc_union_light_client::IbcClientError; +use unionlabs::{ibc::core::client::height::Height, primitives::H256, uint::U256}; use crate::client::EvmInCosmosLightClient; -#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[derive(thiserror::Error, Debug)] 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), @@ -36,8 +19,29 @@ pub enum Error { #[error("verify l2 membership error")] VerifyL2Membership(#[from] ics23::ibc_api::VerifyMembershipError), - #[error(transparent)] - EthereumLightClient(#[from] ethereum_light_client::errors::Error), + #[error("error while querying l1 state: {0}")] + L1Error(#[from] IbcClientError), + + #[error("commitment key must be 32 bytes but we got: {0:?}")] + InvalidCommitmentKeyLength(Vec), + + #[error("expected value ({expected}) and stored value ({stored}) don't match")] + StoredValueMismatch { expected: H256, stored: H256 }, + + #[error("verify storage proof error")] + VerifyStorageProof(#[source] evm_storage_verifier::error::Error), + + #[error("invalid commitment key, expected ({expected:#x}) but found ({found:#x})")] + InvalidCommitmentKey { expected: U256, found: U256 }, + + #[error("commitment value must be 32 bytes but we got: {0:?}")] + InvalidCommitmentValueLength(Vec), + + #[error("verify storage absence error")] + VerifyStorageAbsence(#[source] evm_storage_verifier::error::Error), + + #[error("counterparty storage not nil")] + CounterpartyStorageNotNil, } impl From for IbcClientError { @@ -45,3 +49,9 @@ impl From for IbcClientError { IbcClientError::ClientSpecific(value) } } + +impl From for StdError { + fn from(value: Error) -> Self { + StdError::generic_err(value.to_string()) + } +} diff --git a/cosmwasm/ibc-union/light-clients/tendermint/Cargo.toml b/cosmwasm/ibc-union/light-clients/tendermint/Cargo.toml index 1bd0bd67a4..eb84a9f9a8 100644 --- a/cosmwasm/ibc-union/light-clients/tendermint/Cargo.toml +++ b/cosmwasm/ibc-union/light-clients/tendermint/Cargo.toml @@ -20,7 +20,7 @@ cosmwasm-std = { workspace = true, features = ["abort", "cosmwasm_2_1", "iterato sha2 = { workspace = true } thiserror = { workspace = true } -cometbft-types = { workspace = true, features = ["proto", "hash"] } +cometbft-types = { workspace = true, features = ["hash"] } ibc-union-light-client = { workspace = true } ibc-union-msg = { workspace = true } ics23 = { workspace = true } diff --git a/cosmwasm/ibc-union/light-clients/tendermint/src/client.rs b/cosmwasm/ibc-union/light-clients/tendermint/src/client.rs index 6185008b46..3c6fb56b34 100644 --- a/cosmwasm/ibc-union/light-clients/tendermint/src/client.rs +++ b/cosmwasm/ibc-union/light-clients/tendermint/src/client.rs @@ -10,11 +10,11 @@ use tendermint_light_client_types::{ClientState, ConsensusState, Header}; use tendermint_verifier::types::{HostFns, SignatureVerifier}; use unionlabs::{ bounded::BoundedI64, - encoding::{DecodeAs, Proto}, + encoding::Bincode, google::protobuf::{duration::Duration, timestamp::Timestamp}, ibc::core::{ client::height::Height, - commitment::{merkle_path::MerklePath, merkle_proof::MerkleProof, merkle_root::MerkleRoot}, + commitment::{merkle_proof::MerkleProof, merkle_root::MerkleRoot}, }, primitives::{encoding::HexUnprefixed, H256}, }; @@ -45,30 +45,25 @@ impl IbcClient for TendermintLightClient { type StorageProof = MerkleProof; - type Encoding = Proto; + type Encoding = Bincode; fn verify_membership( - _ctx: IbcClientCtx, - _height: u64, - _key: Vec, - _storage_proof: Self::StorageProof, - _value: Vec, + ctx: IbcClientCtx, + height: u64, + key: Vec, + storage_proof: Self::StorageProof, + value: Vec, ) -> Result<(), IbcClientError> { - // let consensus_state = ctx.read_self_consensus_state(height)?; - // let path = MerklePath::decode_as::(&key).unwrap(); - - // ics23::ibc_api::verify_membership( - // &storage_proof, - // &SDK_SPECS, - // &consensus_state.root, - // &path - // .key_path - // .into_iter() - // .map(|s| s.into_bytes()) - // .collect::>(), - // value, - // ) - // .map_err(Error::VerifyMembership)?; + let client_state = ctx.read_self_client_state()?; + let consensus_state = ctx.read_self_consensus_state(height)?; + + verify_membership( + &client_state.contract_address, + &consensus_state.root, + key, + storage_proof, + value, + )?; Ok(()) } @@ -79,20 +74,15 @@ impl IbcClient for TendermintLightClient { key: Vec, storage_proof: Self::StorageProof, ) -> Result<(), IbcClientError> { + let client_state = ctx.read_self_client_state()?; let consensus_state = ctx.read_self_consensus_state(height)?; - let path = MerklePath::decode_as::(&key).unwrap(); - ics23::ibc_api::verify_non_membership( - &storage_proof, - &SDK_SPECS, + verify_non_membership( + &client_state.contract_address, &consensus_state.root, - &path - .key_path - .into_iter() - .map(|s| s.into_bytes()) - .collect::>(), - ) - .map_err(Error::VerifyMembership)?; + key, + storage_proof, + )?; Ok(()) } @@ -361,6 +351,54 @@ pub fn parse_revision_number(chain_id: &str) -> Option { .map(|height_str| height_str.parse().ok())? } +pub fn verify_membership( + contract_address: &H256, + root: &MerkleRoot, + key: Vec, + storage_proof: MerkleProof, + value: Vec, +) -> Result<(), Error> { + ics23::ibc_api::verify_membership( + &storage_proof, + &SDK_SPECS, + root, + &[ + b"wasm".to_vec(), + 0x3u8 + .to_le_bytes() + .into_iter() + .chain(*contract_address) + .chain(key) + .collect::>(), + ], + value, + ) + .map_err(Error::VerifyMembership) +} + +pub fn verify_non_membership( + contract_address: &H256, + root: &MerkleRoot, + key: Vec, + storage_proof: MerkleProof, +) -> Result<(), Error> { + ics23::ibc_api::verify_non_membership( + &storage_proof, + &SDK_SPECS, + root, + &[ + b"wasm".to_vec(), + 0x3u8 + .to_le_bytes() + .into_iter() + .chain(*contract_address) + .chain(key) + .collect::>(), + ], + ) + .map_err(Error::VerifyMembership) +} + // #[cfg(test)] // mod tests { // use std::fs; diff --git a/cosmwasm/ibc-union/light-clients/tendermint/src/contract.rs b/cosmwasm/ibc-union/light-clients/tendermint/src/contract.rs index 1bf9853c6e..890327fe83 100644 --- a/cosmwasm/ibc-union/light-clients/tendermint/src/contract.rs +++ b/cosmwasm/ibc-union/light-clients/tendermint/src/contract.rs @@ -25,6 +25,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pub struct MigrateMsg {} #[entry_point] -pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult { +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { Ok(Response::default()) } diff --git a/evm/contracts/clients/CosmosInCosmosClient.sol b/evm/contracts/clients/CosmosInCosmosClient.sol index 94322428e7..68e205512b 100644 --- a/evm/contracts/clients/CosmosInCosmosClient.sol +++ b/evm/contracts/clients/CosmosInCosmosClient.sol @@ -30,7 +30,8 @@ struct ClientState { string l2ChainId; uint32 l1ClientId; uint32 l2ClientId; - uint64 latestHeight; + uint64 l2LatestHeight; + bytes32 contractAddress; } struct ConsensusState { @@ -59,7 +60,8 @@ library CosmosInCosmosLib { clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + clientState.contractAddress ); } @@ -117,20 +119,20 @@ contract CosmosInCosmosClient is assembly { consensusState := consensusStateBytes.offset } - if (clientState.latestHeight == 0 || consensusState.timestamp == 0) { + if (clientState.l2LatestHeight == 0 || consensusState.timestamp == 0) { revert CosmosInCosmosLib.ErrInvalidInitialConsensusState(); } clientStates[clientId] = clientState; - consensusStates[clientId][clientState.latestHeight] = consensusState; + consensusStates[clientId][clientState.l2LatestHeight] = consensusState; // Normalize to nanosecond because ibc-go recvPacket expects nanos... - processedMoments[clientId][clientState.latestHeight] = ProcessedMoment({ + processedMoments[clientId][clientState.l2LatestHeight] = ProcessedMoment({ timestamp: block.timestamp * 1e9, height: block.number }); return ConsensusStateUpdate({ clientStateCommitment: clientState.commit(), consensusStateCommitment: consensusState.commit(), - height: clientState.latestHeight + height: clientState.l2LatestHeight }); } @@ -146,7 +148,7 @@ contract CosmosInCosmosClient is assembly { header := clientMessageBytes.offset } - ClientState memory clientState = clientStates[clientId]; + ClientState storage clientState = clientStates[clientId]; ILightClient l1Client = IBCStore(ibcHandler).getClient(clientState.l1ClientId); // L₂[H₂] ∈ L₁[H₁] @@ -160,7 +162,7 @@ contract CosmosInCosmosClient is clientState.l2ClientId, header.l2Height ) ), - abi.encodePacked(keccak256(abi.encode(header.l2ConsensusState))) + abi.encodePacked(keccak256(header.l2ConsensusState)) ) ) { revert CosmosInCosmosLib.ErrInvalidL1Proof(); @@ -172,8 +174,8 @@ contract CosmosInCosmosClient is l2ConsensusState := rawL2ConsensusState.offset } - if (header.l2Height > clientState.latestHeight) { - clientState.latestHeight = header.l2Height; + if (header.l2Height > clientState.l2LatestHeight) { + clientState.l2LatestHeight = header.l2Height; } // L₂[H₂] = S₂ @@ -214,12 +216,15 @@ contract CosmosInCosmosClient is if (isFrozenImpl(clientId)) { revert CosmosInCosmosLib.ErrClientFrozen(); } + bytes32 contractAddress = clientStates[clientId].contractAddress; bytes32 appHash = consensusStates[clientId][height].appHash; return ICS23Verifier.verifyMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), - path, + abi.encodePacked( + IBCStoreLib.COMMITMENT_PREFIX_PATH, contractAddress, path + ), value ); } @@ -233,12 +238,15 @@ contract CosmosInCosmosClient is if (isFrozenImpl(clientId)) { revert CosmosInCosmosLib.ErrClientFrozen(); } + bytes32 contractAddress = clientStates[clientId].contractAddress; bytes32 appHash = consensusStates[clientId][height].appHash; return ICS23Verifier.verifyNonMembership( appHash, proof, abi.encodePacked(IBCStoreLib.COMMITMENT_PREFIX), - path + abi.encodePacked( + IBCStoreLib.COMMITMENT_PREFIX_PATH, contractAddress, path + ) ); } @@ -265,7 +273,7 @@ contract CosmosInCosmosClient is function getLatestHeight( uint32 clientId ) external view override returns (uint64) { - return clientStates[clientId].latestHeight; + return clientStates[clientId].l2LatestHeight; } function isFrozen( diff --git a/evm/evm.nix b/evm/evm.nix index f57287aef1..1e55241121 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -761,6 +761,18 @@ _: { ); }) networks ) + // builtins.listToAttrs ( + builtins.map (args: { + name = "eth-upgrade-${args.network}-cosmos-lens-client"; + value = eth-upgrade ( + { + dry = false; + protocol = "CosmosInCosmosClient"; + } + // args + ); + }) networks + ) // builtins.listToAttrs ( builtins.map (args: { name = "eth-dryupgrade-${args.network}-ibc"; diff --git a/evm/scripts/Deploy.s.sol b/evm/scripts/Deploy.s.sol index e09042cbd0..6075ccc8b8 100644 --- a/evm/scripts/Deploy.s.sol +++ b/evm/scripts/Deploy.s.sol @@ -44,6 +44,7 @@ library LightClients { string constant NAMESPACE = "lightclients"; string constant COMETBLS = "cometbls"; string constant STATE_LENS_EVM = "state-lens/evm"; + string constant STATE_LENS_COSMOS = "state-lens/ics23-ics23"; function make( string memory lightClient @@ -125,6 +126,24 @@ abstract contract UnionScript is UnionBase { ); } + function deployCosmosLens( + IBCHandler handler, + address owner + ) internal returns (CosmosInCosmosClient) { + return CosmosInCosmosClient( + deploy( + LightClients.make(LightClients.STATE_LENS_COSMOS), + abi.encode( + address(new CosmosInCosmosClient()), + abi.encodeCall( + CosmosInCosmosClient.initialize, + (address(handler), owner) + ) + ) + ) + ); + } + function deployCometbls( IBCHandler handler, address owner @@ -213,6 +232,7 @@ abstract contract UnionScript is UnionBase { IBCHandler, CometblsClient, EvmInCosmosClient, + CosmosInCosmosClient, PingPong, UCS01Relay, UCS02NFT, @@ -222,6 +242,7 @@ abstract contract UnionScript is UnionBase { IBCHandler handler = deployIBCHandler(owner); CometblsClient cometblsClient = deployCometbls(handler, owner); EvmInCosmosClient evmLensClient = deployEvmLens(handler, owner); + CosmosInCosmosClient cosmosLensClient = deployCosmosLens(handler, owner); PingPong pingpong = deployUCS00(handler, owner, 100000000000000); UCS01Relay relay = deployUCS01(handler, owner); UCS02NFT nft = deployUCS02(handler, owner); @@ -230,6 +251,7 @@ abstract contract UnionScript is UnionBase { handler, cometblsClient, evmLensClient, + cosmosLensClient, pingpong, relay, nft, @@ -353,6 +375,48 @@ contract DeployUCS03 is UnionScript { } } +contract DeployCosmosLens is UnionScript { + using LibString for *; + + address immutable deployer; + address immutable sender; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + } + + function getDeployer() internal view override returns (Deployer) { + return Deployer(deployer); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address owner = vm.addr(privateKey); + + address handler = getDeployed(IBC.BASED); + + vm.startBroadcast(privateKey); + + CosmosInCosmosClient cosmosLensClient = + deployCosmosLens(IBCHandler(handler), owner); + + vm.stopBroadcast(); + + console.log("CosmosInCosmosClient: ", address(cosmosLensClient)); + } +} + contract DeployIBC is UnionScript { Deployer immutable deployer; @@ -372,6 +436,7 @@ contract DeployIBC is UnionScript { IBCHandler handler, CometblsClient cometblsClient, EvmInCosmosClient evmLensClient, + CosmosInCosmosClient cosmosLensClient, PingPong pingpong, UCS01Relay relay, UCS02NFT nft, @@ -379,6 +444,7 @@ contract DeployIBC is UnionScript { ) = deployIBC(vm.addr(privateKey)); handler.registerClient(LightClients.COMETBLS, cometblsClient); handler.registerClient(LightClients.STATE_LENS_EVM, evmLensClient); + handler.registerClient(LightClients.STATE_LENS_COSMOS, cosmosLensClient); vm.stopBroadcast(); @@ -387,6 +453,7 @@ contract DeployIBC is UnionScript { console.log("IBCHandler: ", address(handler)); console.log("CometblsClient: ", address(cometblsClient)); console.log("EvmInCosmosClient: ", address(evmLensClient)); + console.log("CosmosInCosmosClient: ", address(cosmosLensClient)); console.log("UCS00: ", address(pingpong)); console.log("UCS01: ", address(relay)); console.log("UCS02: ", address(nft)); @@ -412,6 +479,7 @@ contract DeployDeployerAndIBC is UnionScript { IBCHandler handler, CometblsClient cometblsClient, EvmInCosmosClient evmLensClient, + CosmosInCosmosClient cosmosLensClient, PingPong pingpong, UCS01Relay relay, UCS02NFT nft, @@ -419,6 +487,7 @@ contract DeployDeployerAndIBC is UnionScript { ) = deployIBC(vm.addr(privateKey)); handler.registerClient(LightClients.COMETBLS, cometblsClient); handler.registerClient(LightClients.STATE_LENS_EVM, evmLensClient); + handler.registerClient(LightClients.STATE_LENS_COSMOS, cosmosLensClient); vm.stopBroadcast(); @@ -427,6 +496,7 @@ contract DeployDeployerAndIBC is UnionScript { console.log("IBCHandler: ", address(handler)); console.log("CometblsClient: ", address(cometblsClient)); console.log("EvmInCosmosClient: ", address(evmLensClient)); + console.log("CosmosInCosmosClient: ", address(cosmosLensClient)); console.log("UCS00: ", address(pingpong)); console.log("UCS01: ", address(relay)); console.log("UCS02: ", address(nft)); @@ -469,6 +539,8 @@ contract GetDeployed is Script { getDeployed(LightClients.make(LightClients.COMETBLS)); address evmLensClient = getDeployed(LightClients.make(LightClients.STATE_LENS_EVM)); + address cosmosLensClient = + getDeployed(LightClients.make(LightClients.STATE_LENS_COSMOS)); address ucs00 = getDeployed(Protocols.make(Protocols.UCS00)); address ucs01 = getDeployed(Protocols.make(Protocols.UCS01)); address ucs02 = getDeployed(Protocols.make(Protocols.UCS02)); @@ -492,6 +564,13 @@ contract GetDeployed is Script { abi.encodePacked("EvmLensClient: ", evmLensClient.toHexString()) ) ); + console.log( + string( + abi.encodePacked( + "CosmosLensClient: ", cosmosLensClient.toHexString() + ) + ) + ); console.log(string(abi.encodePacked("UCS00: ", ucs00.toHexString()))); console.log(string(abi.encodePacked("UCS01: ", ucs01.toHexString()))); console.log(string(abi.encodePacked("UCS02: ", ucs02.toHexString()))); @@ -1003,3 +1082,44 @@ contract UpgradeEvmInCosmosClient is Script { vm.stopBroadcast(); } } + +contract UpgradeCosmosInCosmosClient is Script { + using LibString for *; + + address immutable deployer; + address immutable sender; + uint256 immutable privateKey; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + privateKey = vm.envUint("PRIVATE_KEY"); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + address cosmosLensClient = + getDeployed(LightClients.make(LightClients.STATE_LENS_COSMOS)); + console.log( + string( + abi.encodePacked( + "CosmosInCosmos: ", cosmosLensClient.toHexString() + ) + ) + ); + vm.startBroadcast(privateKey); + address newImplementation = address(new CosmosInCosmosClient()); + CosmosInCosmosClient(cosmosLensClient).upgradeToAndCall( + newImplementation, new bytes(0) + ); + vm.stopBroadcast(); + } +} diff --git a/evm/tests/src/02-client/CosmosInCosmosClient.t.sol b/evm/tests/src/02-client/CosmosInCosmosClient.t.sol index 275ced1c32..3697b61974 100644 --- a/evm/tests/src/02-client/CosmosInCosmosClient.t.sol +++ b/evm/tests/src/02-client/CosmosInCosmosClient.t.sol @@ -159,13 +159,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); // Encode the consensus state @@ -188,7 +190,7 @@ contract CosmosInCosmosClientTest is Test { ); uint64 latestHeight = client.getLatestHeight(clientId); assertEq( - latestHeight, clientState.latestHeight, "Latest height mismatch" + latestHeight, clientState.l2LatestHeight, "Latest height mismatch" ); // Verify consensus state @@ -214,13 +216,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 0 + l2LatestHeight: 0, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); // Encode the consensus state @@ -249,13 +253,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); ConsensusState memory consensusState = ConsensusState({ @@ -322,13 +328,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); ConsensusState memory consensusState = ConsensusState({ @@ -388,13 +396,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); ConsensusState memory consensusState = ConsensusState({ @@ -442,13 +452,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); ConsensusState memory consensusState = ConsensusState({ @@ -498,13 +510,15 @@ contract CosmosInCosmosClientTest is Test { l2ChainId: "test-chain", l1ClientId: 2, l2ClientId: 3, - latestHeight: 100 + l2LatestHeight: 100, + contractAddress: keccak256("test") }); bytes memory clientStateBytes = abi.encode( clientState.l2ChainId, clientState.l1ClientId, clientState.l2ClientId, - clientState.latestHeight + clientState.l2LatestHeight, + keccak256("test") ); ConsensusState memory consensusState = ConsensusState({ diff --git a/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs b/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs index 447c49d01b..90f284ca86 100644 --- a/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs +++ b/generated/rust/protos/src/union.ibc.lightclients.evmincosmos.v1.rs @@ -2,10 +2,10 @@ #[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(uint32, tag = "1")] + pub l1_client_id: u32, + #[prost(uint32, tag = "2")] + pub l2_client_id: u32, #[prost(uint64, tag = "3")] pub latest_slot: u64, /// Evm diff --git a/lib/state-lens/cosmos-light-client-types/Cargo.toml b/lib/state-lens/cosmos-light-client-types/Cargo.toml new file mode 100644 index 0000000000..5608d6494e --- /dev/null +++ b/lib/state-lens/cosmos-light-client-types/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +name = "cosmos-state-lens-light-client-types" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["sol-types"], optional = true } +protos = { workspace = true, optional = true, features = ["proto_full", "serde"] } +serde = { workspace = true, optional = true, features = ["derive"] } +thiserror = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi", "proto"] } + +[dev-dependencies] +hex-literal = { workspace = true } + +[features] +default = [] +ethabi = ["unionlabs/ethabi", "dep:alloy", "dep:protos"] +serde = ["dep:serde"] diff --git a/lib/state-lens/cosmos-light-client-types/src/client_state.rs b/lib/state-lens/cosmos-light-client-types/src/client_state.rs new file mode 100644 index 0000000000..4bc7148cca --- /dev/null +++ b/lib/state-lens/cosmos-light-client-types/src/client_state.rs @@ -0,0 +1,87 @@ +use unionlabs::primitives::H256; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ClientState { + /// l2 chain id + pub l2_chain_id: String, + /// l1 client id used to check the l2 inclusion proof against + pub l1_client_id: u32, + /// l2 client id + pub l2_client_id: u32, + /// l2 latest height + pub l2_latest_height: u64, + /// ibc contract that is running on l2 + pub contract_address: H256, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use core::str; + use std::string::FromUtf8Error; + + use alloy::sol_types::SolValue; + use unionlabs::{ + encoding::{Decode, Encode, EthAbi}, + TryFromEthAbiBytesErrorAlloy, + }; + + use crate::ClientState; + + alloy::sol! { + struct SolClientState { + string l2ChainId; + uint32 l1ClientId; + uint32 l2ClientId; + uint64 l2LatestHeight; + bytes32 contractAddress; + } + } + + impl Encode for ClientState { + fn encode(self) -> Vec { + SolClientState { + l2ChainId: self.l2_chain_id, + l1ClientId: self.l1_client_id, + l2ClientId: self.l2_client_id, + l2LatestHeight: self.l2_latest_height, + contractAddress: self.contract_address.into(), + } + .abi_encode_params() + } + } + + impl Decode for ClientState { + type Error = TryFromEthAbiBytesErrorAlloy; + + fn decode(bytes: &[u8]) -> Result { + let client_state = SolClientState::abi_decode_params(bytes, true)?; + + Ok(Self { + l2_chain_id: String::from_utf8(client_state.l2ChainId.into_bytes()) + .map_err(|err| TryFromEthAbiBytesErrorAlloy::Convert(Error::ChainId(err)))?, + l1_client_id: client_state.l1ClientId, + l2_client_id: client_state.l2ClientId, + l2_latest_height: client_state.l2LatestHeight, + contract_address: client_state.contractAddress.into(), + }) + } + } + + #[derive(Debug, Clone, PartialEq, thiserror::Error)] + pub enum Error { + #[error("invalid chain_id")] + ChainId(#[from] FromUtf8Error), + } + + #[cfg(test)] + mod test { + fn test_decode() { + // TODO(aeryz): impl + } + + fn test_encode() { + // TODO(aeryz): impl + } + } +} diff --git a/lib/state-lens/cosmos-light-client-types/src/consensus_state.rs b/lib/state-lens/cosmos-light-client-types/src/consensus_state.rs new file mode 100644 index 0000000000..ae68c99827 --- /dev/null +++ b/lib/state-lens/cosmos-light-client-types/src/consensus_state.rs @@ -0,0 +1,45 @@ +use unionlabs::primitives::H256; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ConsensusState { + /// Timestamp of the execution layer. + pub timestamp: u64, + /// App hash of the execution layer. + pub app_hash: H256, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use alloy::sol_types::SolValue; + use unionlabs::impl_ethabi_via_try_from_into; + + use super::*; + + impl_ethabi_via_try_from_into!(ConsensusState => SolConsensusState); + + alloy::sol! { + struct SolConsensusState { + uint64 timestamp; + bytes32 appHash; + } + } + + impl From for SolConsensusState { + fn from(value: ConsensusState) -> Self { + Self { + timestamp: value.timestamp, + appHash: value.app_hash.get().into(), + } + } + } + + impl From for ConsensusState { + fn from(value: SolConsensusState) -> Self { + Self { + timestamp: value.timestamp, + app_hash: H256::new(value.appHash.0), + } + } + } +} diff --git a/lib/state-lens/cosmos-light-client-types/src/header.rs b/lib/state-lens/cosmos-light-client-types/src/header.rs new file mode 100644 index 0000000000..bc7db748e9 --- /dev/null +++ b/lib/state-lens/cosmos-light-client-types/src/header.rs @@ -0,0 +1,125 @@ +use unionlabs::{ + ibc::core::{client::height::Height, commitment::merkle_proof::MerkleProof}, + primitives::Bytes, +}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Header { + pub l1_height: Height, + pub l2_height: Height, + pub l2_consensus_state_proof: MerkleProof, + pub l2_consensus_state: Bytes, +} + +#[cfg(feature = "ethabi")] +pub mod ethabi { + use alloy::sol_types::SolValue; + use unionlabs::{ + encoding::{Encode, EthAbi}, + union::ics23, + }; + + use crate::Header; + + impl Encode for Header { + fn encode(self) -> Vec { + Into::::into(self).abi_encode_params() + } + } + + alloy::sol! { + struct SolHeader { + uint64 l1Height; + uint64 l2Height; + bytes l2InclusionProof; + bytes l2ConsensusState; + } + } + + #[derive(Debug, Clone, PartialEq, thiserror::Error)] + pub enum Error {} + + impl From
for SolHeader { + fn from(value: Header) -> Self { + Self { + l1Height: value.l1_height.height(), + l2Height: value.l2_height.height(), + l2InclusionProof: encode_merkle_proof_for_evm(value.l2_consensus_state_proof) + .into(), + l2ConsensusState: value.l2_consensus_state.into(), + } + } + } + + // FIXME: deduplicate with voyager/module/client/cometbls, in unionlabs? + fn encode_merkle_proof_for_evm( + proof: unionlabs::ibc::core::commitment::merkle_proof::MerkleProof, + ) -> Vec { + alloy::sol! { + struct ExistenceProof { + bytes key; + bytes value; + bytes leafPrefix; + InnerOp[] path; + } + + struct NonExistenceProof { + bytes key; + ExistenceProof left; + ExistenceProof right; + } + + struct InnerOp { + bytes prefix; + bytes suffix; + } + + struct ProofSpec { + uint256 childSize; + uint256 minPrefixLength; + uint256 maxPrefixLength; + } + } + + let merkle_proof = ics23::merkle_proof::MerkleProof::try_from( + protos::ibc::core::commitment::v1::MerkleProof::from(proof), + ) + .unwrap(); + + let convert_inner_op = |i: unionlabs::union::ics23::inner_op::InnerOp| InnerOp { + prefix: i.prefix.into(), + suffix: i.suffix.into(), + }; + + let convert_existence_proof = + |e: unionlabs::union::ics23::existence_proof::ExistenceProof| ExistenceProof { + key: e.key.into(), + value: e.value.into(), + leafPrefix: e.leaf_prefix.into(), + path: e.path.into_iter().map(convert_inner_op).collect(), + }; + + let exist_default = || ics23::existence_proof::ExistenceProof { + key: vec![].into(), + value: vec![].into(), + leaf_prefix: vec![].into(), + path: vec![], + }; + + match merkle_proof { + ics23::merkle_proof::MerkleProof::Membership(a, b) => { + (convert_existence_proof(a), convert_existence_proof(b)).abi_encode_params() + } + ics23::merkle_proof::MerkleProof::NonMembership(a, b) => ( + NonExistenceProof { + key: a.key.into(), + left: convert_existence_proof(a.left.unwrap_or_else(exist_default)), + right: convert_existence_proof(a.right.unwrap_or_else(exist_default)), + }, + convert_existence_proof(b), + ) + .abi_encode_params(), + } + } +} diff --git a/lib/state-lens/cosmos-light-client-types/src/lib.rs b/lib/state-lens/cosmos-light-client-types/src/lib.rs new file mode 100644 index 0000000000..3d184a96ce --- /dev/null +++ b/lib/state-lens/cosmos-light-client-types/src/lib.rs @@ -0,0 +1,5 @@ +pub mod client_state; +pub mod consensus_state; +pub mod header; + +pub use crate::{client_state::ClientState, consensus_state::ConsensusState, header::Header}; diff --git a/lib/state-lens/evm-light-client-types/Cargo.toml b/lib/state-lens/evm-light-client-types/Cargo.toml index 5a7de0de49..7a956f5336 100644 --- a/lib/state-lens/evm-light-client-types/Cargo.toml +++ b/lib/state-lens/evm-light-client-types/Cargo.toml @@ -5,12 +5,14 @@ version = "0.1.0" [dependencies] alloy = { workspace = true, features = ["sol-types"], optional = true } +bincode = { workspace = true, features = ["alloc", "derive"], optional = true } protos = { workspace = true, optional = true, features = ["proto_full", "serde"] } serde = { workspace = true, optional = true, features = ["derive"] } thiserror = { workspace = true } unionlabs = { workspace = true, features = ["ethabi", "proto"] } [features] +bincode = ["dep:bincode", "unionlabs/bincode"] default = [] ethabi = ["unionlabs/ethabi", "dep:alloy", "dep:protos"] serde = ["dep:serde"] diff --git a/lib/state-lens/evm-light-client-types/src/client_state.rs b/lib/state-lens/evm-light-client-types/src/client_state.rs index 325dfdc6a5..3549f29dd4 100644 --- a/lib/state-lens/evm-light-client-types/src/client_state.rs +++ b/lib/state-lens/evm-light-client-types/src/client_state.rs @@ -1,5 +1,6 @@ #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] pub struct ClientState { /// l2 chain id pub l2_chain_id: String, diff --git a/lib/state-lens/evm-light-client-types/src/consensus_state.rs b/lib/state-lens/evm-light-client-types/src/consensus_state.rs index 0ceae7cafa..cac706fc4e 100644 --- a/lib/state-lens/evm-light-client-types/src/consensus_state.rs +++ b/lib/state-lens/evm-light-client-types/src/consensus_state.rs @@ -2,6 +2,7 @@ use unionlabs::primitives::H256; #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] pub struct ConsensusState { /// Timestamp of the execution layer. pub timestamp: u64, diff --git a/lib/state-lens/evm-light-client-types/src/header.rs b/lib/state-lens/evm-light-client-types/src/header.rs index bc7db748e9..edcd4cead7 100644 --- a/lib/state-lens/evm-light-client-types/src/header.rs +++ b/lib/state-lens/evm-light-client-types/src/header.rs @@ -5,6 +5,7 @@ use unionlabs::{ #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] pub struct Header { pub l1_height: Height, pub l2_height: Height, diff --git a/lib/tendermint-light-client-types/src/client_state.rs b/lib/tendermint-light-client-types/src/client_state.rs index 02a5551b88..925b5d1857 100644 --- a/lib/tendermint-light-client-types/src/client_state.rs +++ b/lib/tendermint-light-client-types/src/client_state.rs @@ -1,6 +1,6 @@ use unionlabs::{ cosmos::ics23::proof_spec::ProofSpec, google::protobuf::duration::Duration, - ibc::core::client::height::Height, + ibc::core::client::height::Height, primitives::H256, }; use crate::Fraction; @@ -18,13 +18,20 @@ pub struct ClientState { pub latest_height: Height, pub proof_specs: Vec, pub upgrade_path: Vec, + /// For clients that connect to the cosmwasm implementation of ibc-union, the contract address of the IBC host is required in order to verify storage proofs. For clients connecting to IBC classic, this field is not required and can be ignored during client creation and migration. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "H256::is_zero") + )] + pub contract_address: H256, } #[cfg(feature = "proto")] pub mod proto { use unionlabs::{ cosmos::ics23::proof_spec::TryFromProofSpecError, errors::MissingField, - google::protobuf::duration::DurationError, impl_proto_via_try_from_into, required, + google::protobuf::duration::DurationError, impl_proto_via_try_from_into, primitives::H256, + required, }; impl_proto_via_try_from_into!(ClientState => protos::ibc::lightclients::tendermint::v1::ClientState); @@ -95,6 +102,8 @@ pub mod proto { .collect::, _>>() .map_err(Error::ProofSpecs)?, upgrade_path: value.upgrade_path, + // contract address is not needed for native impl which uses proto + contract_address: H256::default(), }) } } @@ -149,6 +158,7 @@ mod tests { }] .to_vec(), upgrade_path: ["upgrade".to_owned(), "path".to_owned()].to_vec(), + contract_address: H256::default(), } } diff --git a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs b/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs deleted file mode 100644 index 1e80278bf9..0000000000 --- a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos.rs +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e8744d911e..0000000000 --- a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/client_state.rs +++ /dev/null @@ -1,53 +0,0 @@ -use core::fmt::Debug; - -use macros::model; - -use crate::{errors::InvalidLength, primitives::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, -} - -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(), - } - } -} - -#[derive(Debug, PartialEq, Clone, thiserror::Error)] -pub enum TryFromClientStateError { - #[error("invalid ibc contract address")] - IbcContractAddress(#[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)?, - }) - } -} 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 deleted file mode 100644 index d1e937fa73..0000000000 --- a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/consensus_state.rs +++ /dev/null @@ -1,51 +0,0 @@ -use macros::model; - -use crate::{errors::InvalidLength, primitives::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 deleted file mode 100644 index a774d82ea0..0000000000 --- a/lib/unionlabs/src/ibc/lightclients/evm_in_cosmos/header.rs +++ /dev/null @@ -1,74 +0,0 @@ -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, thiserror::Error)] -pub enum TryFromHeaderError { - #[error(transparent)] - MissingField(#[from] MissingField), - #[error("invalid l2_consensus_state")] - L2ConsensusState(#[source] TryFromConsensusStateError), - #[error("invalid l2_inclusion_proof")] - L2InclusionProof(#[source] TryFromMerkleProofError), - #[error("invalid account_proof")] - AccountProof(#[source] 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/voyager-core/src/lib.rs b/lib/voyager-core/src/lib.rs index 005f2cb730..621cdbaaba 100644 --- a/lib/voyager-core/src/lib.rs +++ b/lib/voyager-core/src/lib.rs @@ -120,6 +120,11 @@ impl ClientType { /// [CometBLS]: https://github.com/unionlabs/cometbls pub const STATE_LENS_EVM: &'static str = "state-lens/evm"; + /// Tendermint consensus specification, extracted from [CometBLS]. + /// + /// [CometBLS]: https://github.com/unionlabs/cometbls + pub const STATE_LENS_ICS23_ICS23: &'static str = "state-lens/ics23-ics23"; + // lots more to come - near, linea, polygon - stay tuned } diff --git a/lib/voyager-message/src/lib.rs b/lib/voyager-message/src/lib.rs index b997a8cb1e..16931a17c4 100644 --- a/lib/voyager-message/src/lib.rs +++ b/lib/voyager-message/src/lib.rs @@ -921,7 +921,7 @@ struct ParamsWithItemId<'a> { impl ToRpcParams for ParamsWithItemId<'_> { fn to_rpc_params(self) -> Result>, serde_json::Error> { - info!("to_rpc_params"); + debug!("to_rpc_params"); Ok(Some( RawValue::from_string(serde_json::to_string(&self)?).unwrap(), diff --git a/voyager/lens-config.json b/voyager/lens-config.json new file mode 100644 index 0000000000..8f685c1f86 --- /dev/null +++ b/voyager/lens-config.json @@ -0,0 +1,509 @@ +{ + "modules": { + "state": [ + { + "enabled": true, + "path": "./target/debug/voyager-state-module-cosmos-sdk-union", + "info": { + "chain_id": "union-devnet-1", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090", + "ibc_host_contract_address": "union14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s3e9fe2" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-state-module-cosmos-sdk-union", + "info": { + "chain_id": "elgafar-1", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660", + "ibc_host_contract_address": "stars1s0x3yq0pmltxq56f4yppgmd02ret3uj5k9ftj6ug9c7lc379sw7qv396zm" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-state-module-ethereum", + "info": { + "chain_id": "32382", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "eth_rpc_api": "http://localhost:8545" + } + } + ], + "proof": [ + { + "enabled": true, + "path": "./target/debug/voyager-proof-module-cosmos-sdk-union", + "info": { + "chain_id": "union-devnet-1", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090", + "ibc_host_contract_address": "union14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s3e9fe2" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-proof-module-cosmos-sdk-union", + "info": { + "chain_id": "elgafar-1", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660", + "ibc_host_contract_address": "stars1s0x3yq0pmltxq56f4yppgmd02ret3uj5k9ftj6ug9c7lc379sw7qv396zm" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-proof-module-ethereum", + "info": { + "chain_id": "32382", + "ibc_spec_id": "ibc-union" + }, + "config": { + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "eth_rpc_api": "http://localhost:8545" + } + } + ], + "consensus": [ + { + "enabled": true, + "path": "./target/debug/voyager-consensus-module-cometbls", + "info": { + "chain_id": "union-devnet-1", + "consensus_type": "cometbls" + }, + "config": { + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090", + "ibc_host_contract_address": "union14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s3e9fe2" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-consensus-module-tendermint", + "info": { + "chain_id": "elgafar-1", + "consensus_type": "tendermint" + }, + "config": { + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-consensus-module-ethereum", + "info": { + "chain_id": "32382", + "consensus_type": "ethereum" + }, + "config": { + "chain_spec": "minimal", + "eth_rpc_api": "http://localhost:8545", + "eth_beacon_rpc_api": "http://localhost:9596" + } + } + ], + "client": [ + { + "enabled": true, + "path": "./target/debug/voyager-client-module-state-lens-evm", + "info": { + "client_type": "state-lens/evm", + "consensus_type": "ethereum", + "ibc_interface": "ibc-cosmwasm", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-state-lens-ics23-ics23", + "info": { + "client_type": "state-lens/ics23-ics23", + "consensus_type": "tendermint", + "ibc_interface": "ibc-solidity", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-cometbls", + "info": { + "client_type": "cometbls", + "consensus_type": "cometbls", + "ibc_interface": "ibc-solidity", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-cometbls", + "info": { + "client_type": "cometbls", + "consensus_type": "cometbls", + "ibc_interface": "ibc-move/aptos", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-cometbls", + "info": { + "client_type": "cometbls", + "consensus_type": "cometbls", + "ibc_interface": "ibc-cosmwasm", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-cometbls", + "info": { + "client_type": "cometbls", + "consensus_type": "cometbls", + "ibc_interface": "ibc-go-v8/08-wasm", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-tendermint", + "info": { + "client_type": "tendermint", + "consensus_type": "tendermint", + "ibc_interface": "ibc-cosmwasm", + "ibc_spec_id": "ibc-union" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-module-ethereum", + "info": { + "client_type": "ethereum", + "consensus_type": "ethereum", + "ibc_interface": "ibc-cosmwasm", + "ibc_spec_id": "ibc-union" + }, + "config": { + "chain_spec": "minimal" + } + } + ], + "client_bootstrap": [ + { + "enabled": true, + "path": "./target/debug/voyager-client-bootstrap-module-state-lens-evm", + "info": { + "chain_id": "32382", + "client_type": "state-lens/evm" + }, + "config": { + "l1_client_id": 5, + "l2_client_id": 1, + "timestamp_offset": 0, + "state_root_offset": 32, + "storage_root_offset": 64 + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-bootstrap-module-state-lens-ics23-ics23", + "info": { + "chain_id": "elgafar-1", + "client_type": "state-lens/ics23-ics23" + }, + "config": { + "l1_client_id": 1, + "l2_client_id": 2, + "l2_contract_address": "stars1s0x3yq0pmltxq56f4yppgmd02ret3uj5k9ftj6ug9c7lc379sw7qv396zm" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-bootstrap-module-ethereum", + "info": { + "chain_id": "32382", + "client_type": "ethereum" + }, + "config": { + "chain_spec": "minimal", + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "eth_rpc_api": "http://localhost:8545", + "eth_beacon_rpc_api": "http://localhost:9596" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-bootstrap-module-cometbls", + "info": { + "chain_id": "union-devnet-1", + "client_type": "cometbls" + }, + "config": { + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090", + "ibc_host_contract_address": "union14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s3e9fe2" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-bootstrap-module-tendermint", + "info": { + "chain_id": "elgafar-1", + "client_type": "tendermint" + }, + "config": { + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660" + } + } + ] + }, + "plugins": [ + { + "enabled": true, + "path": "./target/debug/voyager-event-source-plugin-cosmos-sdk", + "config": { + "chain_id": "union-devnet-1", + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-event-source-plugin-cosmos-sdk", + "config": { + "chain_id": "elgafar-1", + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-event-source-plugin-ethereum", + "config": { + "chain_id": "32382", + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "eth_rpc_api": "http://localhost:8545", + "eth_beacon_rpc_api": "http://localhost:9596" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-transaction-plugin-cosmos-sdk", + "config": { + "chain_id": "union-devnet-1", + "ibc_host_contract_address": "union14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s3e9fe2", + "keyring": { + "name": "union-devnet", + "keys": [ + { + "type": "raw", + "name": "alice", + "key": "0xaa820fa947beb242032a41b6dc9a8b9c37d8f5fbcda0966b1ec80335b10a7d6f" + } + ] + }, + "gas_config": { + "gas_price": "1.0", + "gas_denom": "muno", + "gas_multiplier": "1.1", + "max_gas": 10000000 + }, + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-transaction-plugin-cosmos-sdk", + "config": { + "chain_id": "elgafar-1", + "ibc_host_contract_address": "stars1s0x3yq0pmltxq56f4yppgmd02ret3uj5k9ftj6ug9c7lc379sw7qv396zm", + "keyring": { + "name": "stars-testnet", + "keys": [ + { + "type": "raw", + "name": "alice", + "key": "0x089f6742fe5a3d9897695089ab5551a57af0bb503caedb471626f618f423eaa0" + } + ] + }, + "gas_config": { + "gas_price": "1.0", + "gas_denom": "ustars", + "gas_multiplier": "1.1", + "max_gas": 10000000 + }, + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-transaction-plugin-ethereum", + "config": { + "chain_id": "32382", + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "multicall_address": "0x9fd9D9528c8373D990a1380B9414bDE179007A35", + "keyring": { + "name": "ethereum-devnet", + "keys": [ + { + "type": "raw", + "name": "dev-key0", + "key": "0x4e9444a6efd6d42725a250b650a781da2737ea308c839eaccb0f7f3dbd2fea77" + }, + { + "type": "raw", + "name": "dev-key1", + "key": "0xd9c5dc47ed678fc3e63249953866d79e5cf48418e79d8eec1a985be7393ef3b9" + }, + { + "type": "raw", + "name": "eth-key-2", + "key": "0x0a917066d306f09670e47729bfd4384f4afcac98493c65b9733870a434d71f29" + }, + { + "type": "raw", + "name": "eth-key-3", + "key": "0xff45ae2a6c4899294e898a171e15de8a4d2557852378364f8684c6a1520ccd7d" + }, + { + "type": "raw", + "name": "eth-key-4", + "key": "0x27ae0c7b8d7c698e41b173265490a4c64b4e39ae78599166e003f868f12140fd" + } + ] + }, + "eth_rpc_api": "http://localhost:8545" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-plugin-transaction-batch", + "config": { + "chain_id": "union-devnet-1", + "client_configs": { + "min_batch_size": 1, + "max_batch_size": 4, + "max_wait_time": { + "secs": 1, + "nanos": 0 + } + } + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-plugin-transaction-batch", + "config": { + "chain_id": "elgafar-1", + "client_configs": { + "min_batch_size": 1, + "max_batch_size": 4, + "max_wait_time": { + "secs": 1, + "nanos": 0 + } + } + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-plugin-transaction-batch", + "config": { + "chain_id": "32382", + "client_configs": { + "min_batch_size": 1, + "max_batch_size": 5, + "max_wait_time": { + "secs": 1, + "nanos": 0 + } + } + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-update-plugin-cometbls", + "config": { + "chain_id": "union-devnet-1", + "ws_url": "http://localhost:26657", + "grpc_url": "http://localhost:9090", + "prover_endpoints": ["http://localhost:9999"] + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-update-plugin-state-lens-evm", + "config": { + "l0_client_id": 1, + "l1_client_id": 2, + "l1_chain_id": "union-devnet-1", + "l2_chain_id": "32382", + "l1_ws_url": "http://localhost:26657", + "l2_rpc_url": "http://localhost:8545" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-update-plugin-state-lens-ics23-ics23", + "config": { + "l0_client_id": 1, + "l1_client_id": 2, + "l1_chain_id": "union-devnet-1", + "l2_chain_id": "elgafar-1", + "l1_ws_url": "http://localhost:26657", + "l2_ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-update-plugin-tendermint", + "config": { + "chain_id": "elgafar-1", + "ws_url": "https://rpc.elgafar-1.stargaze.chain.kitchen", + "grpc_url": "http://grpc-1.elgafar-1.stargaze-apis.com:26660" + } + }, + { + "enabled": true, + "path": "./target/debug/voyager-client-update-plugin-ethereum", + "config": { + "chain_id": "32382", + "chain_spec": "minimal", + "ibc_handler_address": "0xed2af2ad7fe0d92011b26a2e5d1b4dc7d12a47c5", + "eth_rpc_api": "http://localhost:8545", + "eth_beacon_rpc_api": "http://localhost:9596" + } + } + ], + "voyager": { + "num_workers": 50, + "queue": { + "type": "pg-queue", + "database_url": "postgres://postgres:postgrespassword@127.0.0.1:5432/default", + "max_connections": 50, + "min_connections": 50 + }, + "optimizer_delay_milliseconds": 100 + } +} diff --git a/voyager/modules/client-bootstrap/state-lens/ics23-ics23/Cargo.toml b/voyager/modules/client-bootstrap/state-lens/ics23-ics23/Cargo.toml new file mode 100644 index 0000000000..ee53f58da9 --- /dev/null +++ b/voyager/modules/client-bootstrap/state-lens/ics23-ics23/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2021" +name = "voyager-client-bootstrap-module-state-lens-ics23-ics23" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +beacon-api = { workspace = true } +beacon-api-types = { workspace = true, features = ["serde"] } +cometbft-rpc = { workspace = true } +cosmos-state-lens-light-client-types = { workspace = true, features = ["serde"] } +ibc-union-spec.workspace = true +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tendermint-light-client-types = { workspace = true, features = ["serde"] } +tokio = { workspace = true } +tracing = { workspace = true } +unionlabs = { workspace = true } +voyager-message = { workspace = true } +voyager-vm = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/voyager/modules/client-bootstrap/state-lens/ics23-ics23/src/main.rs b/voyager/modules/client-bootstrap/state-lens/ics23-ics23/src/main.rs new file mode 100644 index 0000000000..0cfa84a79c --- /dev/null +++ b/voyager/modules/client-bootstrap/state-lens/ics23-ics23/src/main.rs @@ -0,0 +1,87 @@ +use cosmos_state_lens_light_client_types::{ClientState, ConsensusState}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tendermint_light_client_types::ConsensusState as TmConsensusState; +use tracing::instrument; +use unionlabs::{bech32::Bech32, ibc::core::client::height::Height, primitives::H256}; +use voyager_message::{ + core::{ChainId, ClientType, QueryHeight}, + into_value, + module::{ClientBootstrapModuleInfo, ClientBootstrapModuleServer}, + ClientBootstrapModule, ExtensionsExt, VoyagerClient, +}; +use voyager_vm::BoxDynError; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module { + pub l2_chain_id: ChainId, + pub l1_client_id: u32, + pub l2_client_id: u32, + pub l2_contract_address: H256, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub l1_client_id: u32, + pub l2_client_id: u32, + pub l2_contract_address: Bech32, +} + +impl ClientBootstrapModule for Module { + type Config = Config; + + async fn new( + config: Self::Config, + info: ClientBootstrapModuleInfo, + ) -> Result { + Ok(Self { + l2_chain_id: info.chain_id, + l1_client_id: config.l1_client_id, + l2_client_id: config.l2_client_id, + l2_contract_address: *config.l2_contract_address.data(), + }) + } +} + +#[async_trait] +impl ClientBootstrapModuleServer for Module { + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn self_client_state(&self, _: &Extensions, height: Height) -> RpcResult { + Ok(into_value(ClientState { + l1_client_id: self.l1_client_id, + l2_chain_id: self.l2_chain_id.to_string(), + l2_client_id: self.l2_client_id, + l2_latest_height: height.height(), + contract_address: self.l2_contract_address, + })) + } + + /// The consensus state on this chain at the specified `Height`. + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn self_consensus_state(&self, ext: &Extensions, height: Height) -> RpcResult { + let voy_client = ext.try_get::()?; + let state = voy_client + .self_consensus_state( + self.l2_chain_id.clone(), + ClientType::new(ClientType::TENDERMINT), + QueryHeight::Specific(height), + ) + .await? + .state; + let consensus_state = + serde_json::from_value::(state).expect("big trouble"); + Ok(into_value(&ConsensusState { + timestamp: consensus_state.timestamp.as_unix_nanos(), + app_hash: H256::new(*consensus_state.root.hash.get()), + })) + } +} diff --git a/voyager/modules/client-bootstrap/tendermint/src/main.rs b/voyager/modules/client-bootstrap/tendermint/src/main.rs index 28b7308352..3fc4b2c3f3 100644 --- a/voyager/modules/client-bootstrap/tendermint/src/main.rs +++ b/voyager/modules/client-bootstrap/tendermint/src/main.rs @@ -14,8 +14,11 @@ use serde_json::Value; use tendermint_light_client_types::{ClientState, ConsensusState, Fraction}; use tracing::{error, instrument}; use unionlabs::{ + bech32::Bech32, ibc::core::{client::height::Height, commitment::merkle_root::MerkleRoot}, - option_unwrap, result_unwrap, ErrorReporter, + option_unwrap, + primitives::H256, + result_unwrap, ErrorReporter, }; use voyager_message::{ core::{ChainId, ClientType}, @@ -36,12 +39,16 @@ pub struct Module { pub tm_client: cometbft_rpc::Client, pub chain_revision: u64, pub grpc_url: String, + + pub ibc_host_contract_address: H256, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub ws_url: String, pub grpc_url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ibc_host_contract_address: Option>, } impl ClientBootstrapModule for Module { @@ -76,6 +83,10 @@ impl ClientBootstrapModule for Module { chain_id: ChainId::new(chain_id), chain_revision, grpc_url: config.grpc_url, + ibc_host_contract_address: config + .ibc_host_contract_address + .map(|a| *a.data()) + .unwrap_or_default(), }) } } @@ -171,6 +182,7 @@ impl ClientBootstrapModuleServer for Module { ), proof_specs: SDK_SPECS.into(), upgrade_path: vec!["upgrade".into(), "upgradedIBCState".into()], + contract_address: self.ibc_host_contract_address, }) .unwrap()) } diff --git a/voyager/modules/client/cometbls/Cargo.toml b/voyager/modules/client/cometbls/Cargo.toml index e0a6585edf..010a811c8b 100644 --- a/voyager/modules/client/cometbls/Cargo.toml +++ b/voyager/modules/client/cometbls/Cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" alloy = { workspace = true, features = ["sol-types"] } ark-bn254 = { version = "0.4", default-features = false, features = ["curve"] } ark-serialize = "0.4.2" -cometbls-light-client-types = { workspace = true, features = ["proto", "ethabi", "serde"] } +cometbls-light-client-types = { workspace = true, features = ["proto", "ethabi", "serde", "bincode"] } jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } macros = { workspace = true } num-bigint = { workspace = true } @@ -16,7 +16,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -unionlabs = { workspace = true } +unionlabs = { workspace = true, features = ["bincode"] } voyager-message = { workspace = true } voyager-vm = { workspace = true } diff --git a/voyager/modules/client/cometbls/src/main.rs b/voyager/modules/client/cometbls/src/main.rs index bf4d836633..28b26b43b4 100644 --- a/voyager/modules/client/cometbls/src/main.rs +++ b/voyager/modules/client/cometbls/src/main.rs @@ -12,7 +12,7 @@ use serde_json::{json, Value}; use tracing::{debug, instrument}; use unionlabs::{ self, - encoding::{Bcs, DecodeAs, EncodeAs, EthAbi, Proto}, + encoding::{Bcs, Bincode, DecodeAs, EncodeAs, EthAbi, Proto}, google::protobuf::any::Any, ibc::lightclients::wasm, primitives::Bytes, @@ -157,11 +157,11 @@ impl Module { }) .map(|any| any.0.data) } - SupportedIbcInterface::IbcCosmwasm => ClientState::decode_as::(client_state) + SupportedIbcInterface::IbcCosmwasm => ClientState::decode_as::(client_state) .map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to decode client state: {}", ErrorReporter(err)), + format!("unable to decode client state: {err}"), None::<()>, ) }), @@ -267,7 +267,7 @@ impl ClientModuleServer for Module { })), )); } - Ok(cs.encode_as::()) + Ok(cs.encode_as::()) } SupportedIbcInterface::IbcGoV8_08Wasm => { let metadata = @@ -353,7 +353,7 @@ impl ClientModuleServer for Module { }) .map(|mut header| match self.ibc_interface { SupportedIbcInterface::IbcSolidity => Ok(header.encode_as::()), - SupportedIbcInterface::IbcCosmwasm => Ok(header.encode_as::()), + SupportedIbcInterface::IbcCosmwasm => Ok(header.encode_as::()), SupportedIbcInterface::IbcMoveAptos => { header.zero_knowledge_proof = reencode_zkp_for_move(&header.zero_knowledge_proof) @@ -389,7 +389,7 @@ impl ClientModuleServer for Module { }) .map(|proof| match self.ibc_interface { SupportedIbcInterface::IbcSolidity => encode_merkle_proof_for_evm(proof), - SupportedIbcInterface::IbcCosmwasm => proof.encode_as::(), + SupportedIbcInterface::IbcCosmwasm => proof.encode_as::(), SupportedIbcInterface::IbcMoveAptos => encode_merkle_proof_for_move( ics23::merkle_proof::MerkleProof::try_from( protos::ibc::core::commitment::v1::MerkleProof::from(proof), diff --git a/voyager/modules/client/movement/Cargo.toml b/voyager/modules/client/movement/Cargo.toml index 9f8a43cc5a..a95f2bc47a 100644 --- a/voyager/modules/client/movement/Cargo.toml +++ b/voyager/modules/client/movement/Cargo.toml @@ -12,5 +12,5 @@ serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -unionlabs = { workspace = true } +unionlabs = { workspace = true, features = ["bincode"] } voyager-message = { workspace = true } diff --git a/voyager/modules/client/state-lens/evm/Cargo.toml b/voyager/modules/client/state-lens/evm/Cargo.toml index 50dfaf3b16..33d0edd802 100644 --- a/voyager/modules/client/state-lens/evm/Cargo.toml +++ b/voyager/modules/client/state-lens/evm/Cargo.toml @@ -7,8 +7,8 @@ version = "0.1.0" beacon-api-types = { workspace = true, features = ["serde"] } chain-utils = { workspace = true } enumorph = { workspace = true } -ethereum-light-client-types = { workspace = true, features = ["serde", "ethabi"] } -evm-state-lens-light-client-types = { workspace = true, features = ["serde", "ethabi"] } +ethereum-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } +evm-state-lens-light-client-types = { workspace = true, features = ["serde", "ethabi", "bincode"] } futures = { workspace = true } jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } macros = { workspace = true } diff --git a/voyager/modules/client/state-lens/evm/src/main.rs b/voyager/modules/client/state-lens/evm/src/main.rs index 7044f690e2..903dbbc71e 100644 --- a/voyager/modules/client/state-lens/evm/src/main.rs +++ b/voyager/modules/client/state-lens/evm/src/main.rs @@ -10,10 +10,9 @@ use serde_json::{json, Value}; use tracing::instrument; use unionlabs::{ self, - encoding::{DecodeAs, EncodeAs, EthAbi}, + encoding::{Bincode, DecodeAs, EncodeAs, EthAbi}, ibc::core::client::height::Height, primitives::Bytes, - ErrorReporter, }; use voyager_message::{ core::{ @@ -31,8 +30,45 @@ async fn main() { Module::run().await } +#[derive(Debug, Clone, PartialEq, Copy, serde::Serialize, serde::Deserialize)] +#[serde(try_from = "String", into = "String")] +pub enum SupportedIbcInterface { + IbcSolidity, + IbcCosmwasm, +} + +impl TryFrom for SupportedIbcInterface { + // TODO: Better error type here + type Error = String; + + fn try_from(value: String) -> Result { + match &*value { + IbcInterface::IBC_SOLIDITY => Ok(SupportedIbcInterface::IbcSolidity), + IbcInterface::IBC_COSMWASM => Ok(SupportedIbcInterface::IbcCosmwasm), + _ => Err(format!("unsupported IBC interface: `{value}`")), + } + } +} + +impl SupportedIbcInterface { + fn as_str(&self) -> &'static str { + match self { + SupportedIbcInterface::IbcSolidity => IbcInterface::IBC_SOLIDITY, + SupportedIbcInterface::IbcCosmwasm => IbcInterface::IBC_COSMWASM, + } + } +} + +impl From for String { + fn from(value: SupportedIbcInterface) -> Self { + value.as_str().to_owned() + } +} + #[derive(Debug, Clone)] -pub struct Module {} +pub struct Module { + pub ibc_interface: SupportedIbcInterface, +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config {} @@ -43,8 +79,12 @@ impl ClientModule for Module { async fn new(_: Self::Config, info: ClientModuleInfo) -> Result { info.ensure_client_type(ClientType::STATE_LENS_EVM)?; info.ensure_consensus_type(ConsensusType::ETHEREUM)?; - info.ensure_ibc_interface(IbcInterface::IBC_SOLIDITY)?; - Ok(Self {}) + info.ensure_ibc_interface(IbcInterface::IBC_SOLIDITY) + .or(info.ensure_ibc_interface(IbcInterface::IBC_COSMWASM))?; + + Ok(Self { + ibc_interface: SupportedIbcInterface::try_from(info.ibc_interface.to_string())?, + }) } } @@ -56,20 +96,33 @@ impl Module { SelfConsensusState::decode_as::(consensus_state).map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to decode consensus state: {}", ErrorReporter(err)), + format!("unable to decode consensus state: {err}"), None::<()>, ) }) } - pub fn decode_client_state(client_state: &[u8]) -> RpcResult { - ::decode_as::(client_state).map_err(|err| { - ErrorObject::owned( - FATAL_JSONRPC_ERROR_CODE, - format!("unable to decode client state: {}", ErrorReporter(err)), - None::<()>, - ) - }) + pub fn decode_client_state(&self, client_state: &[u8]) -> RpcResult { + match self.ibc_interface { + SupportedIbcInterface::IbcSolidity => { + ::decode_as::(client_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode client state: {err}"), + None::<()>, + ) + }) + } + SupportedIbcInterface::IbcCosmwasm => { + ::decode_as::(client_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode client state: {err}"), + None::<()>, + ) + }) + } + } } pub fn make_height(revision_height: u64) -> Height { @@ -85,7 +138,7 @@ impl ClientModuleServer for Module { _: &Extensions, client_state: Bytes, ) -> RpcResult { - let cs = Module::decode_client_state(&client_state)?; + let cs = self.decode_client_state(&client_state)?; Ok(ClientStateMeta { chain_id: ChainId::new(cs.l2_chain_id.to_string()), @@ -108,7 +161,7 @@ impl ClientModuleServer for Module { #[instrument] async fn decode_client_state(&self, _: &Extensions, client_state: Bytes) -> RpcResult { - Ok(into_value(Module::decode_client_state(&client_state)?)) + Ok(into_value(self.decode_client_state(&client_state)?)) } #[instrument] @@ -144,11 +197,14 @@ impl ClientModuleServer for Module { .map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to deserialize client state: {}", ErrorReporter(err)), + format!("unable to deserialize client state: {err}"), None::<()>, ) }) - .map(|cs| cs.encode_as::()) + .map(|cs| match self.ibc_interface { + SupportedIbcInterface::IbcSolidity => cs.encode_as::(), + SupportedIbcInterface::IbcCosmwasm => cs.encode_as::(), + }) .map(Into::into) } @@ -162,10 +218,7 @@ impl ClientModuleServer for Module { .map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!( - "unable to deserialize consensus state: {}", - ErrorReporter(err) - ), + format!("unable to deserialize consensus state: {err}"), None::<()>, ) }) @@ -199,11 +252,14 @@ impl ClientModuleServer for Module { .map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to deserialize header: {}", ErrorReporter(err)), + format!("unable to deserialize header: {err}"), None::<()>, ) }) - .map(|header| header.encode_as::()) + .map(|header| match self.ibc_interface { + SupportedIbcInterface::IbcSolidity => header.encode_as::(), + SupportedIbcInterface::IbcCosmwasm => header.encode_as::(), + }) .map(Into::into) } @@ -212,12 +268,15 @@ impl ClientModuleServer for Module { let proof = serde_json::from_value::(proof).map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to deserialize proof: {}", ErrorReporter(err)), + format!("unable to deserialize proof: {err}"), None::<()>, ) })?; - // TODO: extract to unionlabs? this is MPT proofs encoding for EVM - // the solidity MPT verifier expects the proof RLP nodes to be serialized in sequence - Ok(proof.proof.into_iter().flatten().collect()) + match self.ibc_interface { + // TODO: extract to unionlabs? this is MPT proofs encoding for EVM + // the solidity MPT verifier expects the proof RLP nodes to be serialized in sequence + SupportedIbcInterface::IbcSolidity => Ok(proof.proof.into_iter().flatten().collect()), + SupportedIbcInterface::IbcCosmwasm => Ok(proof.encode_as::().into()), + } } } diff --git a/voyager/modules/client/state-lens/ics23-ics23/Cargo.toml b/voyager/modules/client/state-lens/ics23-ics23/Cargo.toml new file mode 100644 index 0000000000..1f1f3e42c7 --- /dev/null +++ b/voyager/modules/client/state-lens/ics23-ics23/Cargo.toml @@ -0,0 +1,21 @@ +[package] +edition = "2021" +name = "voyager-client-module-state-lens-ics23-ics23" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["sol-types"] } +cosmos-state-lens-light-client-types = { workspace = true, features = ["serde", "ethabi"] } +futures = { workspace = true } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +unionlabs = { workspace = true } +voyager-message = { workspace = true } +voyager-vm = { workspace = true } diff --git a/voyager/modules/client/state-lens/ics23-ics23/src/main.rs b/voyager/modules/client/state-lens/ics23-ics23/src/main.rs new file mode 100644 index 0000000000..2fa69b5a14 --- /dev/null +++ b/voyager/modules/client/state-lens/ics23-ics23/src/main.rs @@ -0,0 +1,295 @@ +use alloy::sol_types::SolValue as _; +use cosmos_state_lens_light_client_types::{ClientState, ConsensusState, Header}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + types::ErrorObject, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tracing::instrument; +use unionlabs::{ + self, + encoding::{DecodeAs, EncodeAs, EthAbi}, + ibc::core::client::height::Height, + primitives::Bytes, + union::ics23, + ErrorReporter, +}; +use voyager_message::{ + core::{ + ChainId, ClientStateMeta, ClientType, ConsensusStateMeta, ConsensusType, IbcInterface, + Timestamp, + }, + into_value, + module::{ClientModuleInfo, ClientModuleServer}, + ClientModule, FATAL_JSONRPC_ERROR_CODE, +}; +use voyager_vm::BoxDynError; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config {} + +impl ClientModule for Module { + type Config = Config; + + async fn new(_: Self::Config, info: ClientModuleInfo) -> Result { + info.ensure_client_type(ClientType::STATE_LENS_ICS23_ICS23)?; + info.ensure_consensus_type(ConsensusType::TENDERMINT)?; + info.ensure_ibc_interface(IbcInterface::IBC_SOLIDITY)?; + Ok(Self {}) + } +} + +type SelfConsensusState = ConsensusState; +type SelfClientState = ClientState; + +impl Module { + pub fn decode_consensus_state(consensus_state: &[u8]) -> RpcResult { + SelfConsensusState::decode_as::(consensus_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode consensus state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + } + + pub fn decode_client_state(client_state: &[u8]) -> RpcResult { + ::decode_as::(client_state).map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to decode client state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + } + + pub fn make_height(revision_height: u64) -> Height { + Height::new(revision_height) + } +} + +#[async_trait] +impl ClientModuleServer for Module { + #[instrument] + async fn decode_client_state_meta( + &self, + _: &Extensions, + client_state: Bytes, + ) -> RpcResult { + let cs = Module::decode_client_state(&client_state)?; + + Ok(ClientStateMeta { + chain_id: ChainId::new(cs.l2_chain_id.to_string()), + counterparty_height: Module::make_height(cs.l2_latest_height), + }) + } + + #[instrument] + async fn decode_consensus_state_meta( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + let cs = Module::decode_consensus_state(&consensus_state)?; + + Ok(ConsensusStateMeta { + timestamp_nanos: Timestamp::from_nanos(cs.timestamp), + }) + } + + #[instrument] + async fn decode_client_state(&self, _: &Extensions, client_state: Bytes) -> RpcResult { + Ok(into_value(Module::decode_client_state(&client_state)?)) + } + + #[instrument] + async fn decode_consensus_state( + &self, + _: &Extensions, + consensus_state: Bytes, + ) -> RpcResult { + Ok(into_value(Module::decode_consensus_state( + &consensus_state, + )?)) + } + + #[instrument] + async fn encode_client_state( + &self, + _: &Extensions, + client_state: Value, + metadata: Value, + ) -> RpcResult { + if !metadata.is_null() { + return Err(ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + "metadata was provided, but this client type does not require \ + metadata for client state encoding", + Some(json!({ + "provided_metadata": metadata, + })), + )); + } + + serde_json::from_value::(client_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize client state: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_consensus_state( + &self, + _: &Extensions, + consensus_state: Value, + ) -> RpcResult { + serde_json::from_value::(consensus_state) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!( + "unable to deserialize consensus state: {}", + ErrorReporter(err) + ), + None::<()>, + ) + }) + .map(|cs| cs.encode_as::()) + .map(Into::into) + } + + #[instrument(skip_all)] + async fn reencode_counterparty_client_state( + &self, + _: &Extensions, + _client_state: Bytes, + _client_type: ClientType, + ) -> RpcResult { + todo!() + } + + #[instrument(skip_all)] + async fn reencode_counterparty_consensus_state( + &self, + _: &Extensions, + _consensus_state: Bytes, + _client_type: ClientType, + ) -> RpcResult { + todo!() + } + + #[instrument] + async fn encode_header(&self, _: &Extensions, header: Value) -> RpcResult { + serde_json::from_value::
(header) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize header: {}", ErrorReporter(err)), + None::<()>, + ) + }) + .map(|header| header.encode_as::()) + .map(Into::into) + } + + #[instrument] + async fn encode_proof(&self, _: &Extensions, proof: Value) -> RpcResult { + let proof = serde_json::from_value::< + unionlabs::ibc::core::commitment::merkle_proof::MerkleProof, + >(proof) + .map_err(|err| { + ErrorObject::owned( + FATAL_JSONRPC_ERROR_CODE, + format!("unable to deserialize proof: {}", ErrorReporter(err)), + None::<()>, + ) + })?; + Ok(encode_merkle_proof_for_evm(proof).into()) + } +} + +fn encode_merkle_proof_for_evm( + proof: unionlabs::ibc::core::commitment::merkle_proof::MerkleProof, +) -> Vec { + alloy::sol! { + struct ExistenceProof { + bytes key; + bytes value; + bytes leafPrefix; + InnerOp[] path; + } + + struct NonExistenceProof { + bytes key; + ExistenceProof left; + ExistenceProof right; + } + + struct InnerOp { + bytes prefix; + bytes suffix; + } + + struct ProofSpec { + uint256 childSize; + uint256 minPrefixLength; + uint256 maxPrefixLength; + } + } + + let merkle_proof = ics23::merkle_proof::MerkleProof::try_from( + protos::ibc::core::commitment::v1::MerkleProof::from(proof), + ) + .unwrap(); + + let convert_inner_op = |i: unionlabs::union::ics23::inner_op::InnerOp| InnerOp { + prefix: i.prefix.into(), + suffix: i.suffix.into(), + }; + + let convert_existence_proof = + |e: unionlabs::union::ics23::existence_proof::ExistenceProof| ExistenceProof { + key: e.key.into(), + value: e.value.into(), + leafPrefix: e.leaf_prefix.into(), + path: e.path.into_iter().map(convert_inner_op).collect(), + }; + + let exist_default = || ics23::existence_proof::ExistenceProof { + key: vec![].into(), + value: vec![].into(), + leaf_prefix: vec![].into(), + path: vec![], + }; + + match merkle_proof { + ics23::merkle_proof::MerkleProof::Membership(a, b) => { + (convert_existence_proof(a), convert_existence_proof(b)).abi_encode_params() + } + ics23::merkle_proof::MerkleProof::NonMembership(a, b) => ( + NonExistenceProof { + key: a.key.into(), + left: convert_existence_proof(a.left.unwrap_or_else(exist_default)), + right: convert_existence_proof(a.right.unwrap_or_else(exist_default)), + }, + convert_existence_proof(b), + ) + .abi_encode_params(), + } +} diff --git a/voyager/modules/client/tendermint/Cargo.toml b/voyager/modules/client/tendermint/Cargo.toml index 6287ef9920..cf821b0c48 100644 --- a/voyager/modules/client/tendermint/Cargo.toml +++ b/voyager/modules/client/tendermint/Cargo.toml @@ -8,9 +8,9 @@ jsonrpsee = { workspace = true, features = ["macros", "serve macros = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tendermint-light-client-types = { workspace = true, features = ["proto", "serde", "ethabi"] } +tendermint-light-client-types = { workspace = true, features = ["proto", "serde", "ethabi", "bincode"] } tokio = { workspace = true } tracing = { workspace = true } -unionlabs = { workspace = true } +unionlabs = { workspace = true, features = ["bincode"] } voyager-message = { workspace = true } voyager-vm = { workspace = true } diff --git a/voyager/modules/client/tendermint/src/main.rs b/voyager/modules/client/tendermint/src/main.rs index 64593299d2..a88893cb35 100644 --- a/voyager/modules/client/tendermint/src/main.rs +++ b/voyager/modules/client/tendermint/src/main.rs @@ -10,7 +10,7 @@ use tendermint_light_client_types::{ClientState, ConsensusState, Header}; use tracing::{debug, instrument}; use unionlabs::{ self, - encoding::{DecodeAs, EncodeAs, EthAbi, Proto}, + encoding::{Bincode, DecodeAs, EncodeAs, EthAbi, Proto}, google::protobuf::any::Any, primitives::Bytes, ErrorReporter, @@ -127,11 +127,11 @@ impl Module { }) .map(|any| any.0) } - SupportedIbcInterface::IbcCosmwasm => ClientState::decode_as::(client_state) + SupportedIbcInterface::IbcCosmwasm => ClientState::decode_as::(client_state) .map_err(|err| { ErrorObject::owned( FATAL_JSONRPC_ERROR_CODE, - format!("unable to decode client state: {}", ErrorReporter(err)), + format!("unable to decode client state: {err}"), None::<()>, ) }), @@ -210,7 +210,7 @@ impl ClientModuleServer for Module { }) .map(|cs| match self.ibc_interface { SupportedIbcInterface::IbcGoV8Native => Any(cs).encode_as::().into(), - SupportedIbcInterface::IbcCosmwasm => cs.encode_as::().into(), + SupportedIbcInterface::IbcCosmwasm => cs.encode_as::().into(), }) } @@ -269,7 +269,7 @@ impl ClientModuleServer for Module { }) .map(|header| match self.ibc_interface { SupportedIbcInterface::IbcGoV8Native => Any(header).encode_as::().into(), - SupportedIbcInterface::IbcCosmwasm => header.encode_as::().into(), + SupportedIbcInterface::IbcCosmwasm => header.encode_as::().into(), }) } @@ -287,7 +287,7 @@ impl ClientModuleServer for Module { }) .map(|cs| match self.ibc_interface { SupportedIbcInterface::IbcGoV8Native => cs.encode_as::().into(), - SupportedIbcInterface::IbcCosmwasm => cs.encode_as::().into(), + SupportedIbcInterface::IbcCosmwasm => cs.encode_as::().into(), }) } } diff --git a/voyager/plugins/client-update/state-lens/ics23-ics23/Cargo.toml b/voyager/plugins/client-update/state-lens/ics23-ics23/Cargo.toml new file mode 100644 index 0000000000..755ba92509 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/ics23-ics23/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition = "2021" +name = "voyager-client-update-plugin-state-lens-ics23-ics23" +version = "0.1.0" + +[dependencies] +alloy = { workspace = true, features = ["rpc", "rpc-types", "transports", "transport-http", "transport-ws", "reqwest", "provider-ws"] } +cometbft-rpc = { workspace = true } +cometbft-types.workspace = true +cosmos-state-lens-light-client-types = { workspace = true, features = ["serde"] } +dashmap = { workspace = true } +enumorph = { workspace = true } +futures = { workspace = true } +ibc-union-spec.workspace = true +ics23 = { workspace = true } +jsonrpsee = { workspace = true, features = ["macros", "server", "tracing"] } +macros = { workspace = true } +num-bigint = { workspace = true } +prost = { workspace = true } +protos = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +unionlabs = { workspace = true } +voyager-message = { workspace = true } +voyager-vm = { workspace = true } diff --git a/voyager/plugins/client-update/state-lens/ics23-ics23/src/call.rs b/voyager/plugins/client-update/state-lens/ics23-ics23/src/call.rs new file mode 100644 index 0000000000..3d9897aafd --- /dev/null +++ b/voyager/plugins/client-update/state-lens/ics23-ics23/src/call.rs @@ -0,0 +1,25 @@ +use enumorph::Enumorph; +use macros::model; +use unionlabs::ibc::core::client::height::Height; +use voyager_message::core::ChainId; + +#[model] +#[derive(Enumorph)] +pub enum ModuleCall { + FetchUpdate(FetchUpdate), + FetchUpdateAfterL1Update(FetchUpdateAfterL1Update), +} + +#[model] +pub struct FetchUpdate { + pub counterparty_chain_id: ChainId, + pub update_from: Height, + pub update_to: Height, +} + +#[model] +pub struct FetchUpdateAfterL1Update { + pub counterparty_chain_id: ChainId, + pub update_from: Height, + pub update_to: Height, +} diff --git a/voyager/plugins/client-update/state-lens/ics23-ics23/src/callback.rs b/voyager/plugins/client-update/state-lens/ics23-ics23/src/callback.rs new file mode 100644 index 0000000000..a332e95f9a --- /dev/null +++ b/voyager/plugins/client-update/state-lens/ics23-ics23/src/callback.rs @@ -0,0 +1,6 @@ +use enumorph::Enumorph; +use macros::model; + +#[model] +#[derive(Enumorph)] +pub enum ModuleCallback {} diff --git a/voyager/plugins/client-update/state-lens/ics23-ics23/src/data.rs b/voyager/plugins/client-update/state-lens/ics23-ics23/src/data.rs new file mode 100644 index 0000000000..f52f66d6b6 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/ics23-ics23/src/data.rs @@ -0,0 +1,4 @@ +use macros::model; + +#[model] +pub enum ModuleData {} diff --git a/voyager/plugins/client-update/state-lens/ics23-ics23/src/main.rs b/voyager/plugins/client-update/state-lens/ics23-ics23/src/main.rs new file mode 100644 index 0000000000..1f417b9950 --- /dev/null +++ b/voyager/plugins/client-update/state-lens/ics23-ics23/src/main.rs @@ -0,0 +1,311 @@ +use std::{collections::VecDeque, fmt::Debug}; + +use call::FetchUpdateAfterL1Update; +use cosmos_state_lens_light_client_types::Header; +use ibc_union_spec::{ConsensusStatePath, IbcUnion}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + Extensions, +}; +use serde::{Deserialize, Serialize}; +use tracing::{debug, instrument}; +use unionlabs::ibc::core::commitment::merkle_proof::MerkleProof; +use voyager_message::{ + call::{Call, FetchUpdateHeaders, WaitForTrustedHeight}, + callback::AggregateMsgUpdateClientsFromOrderedHeaders, + core::{ChainId, ClientType, IbcSpec, QueryHeight}, + data::{Data, DecodedHeaderMeta, OrderedHeaders}, + hook::UpdateHook, + into_value, + module::{PluginInfo, PluginServer}, + DefaultCmd, ExtensionsExt, Plugin, PluginMessage, RawClientId, VoyagerClient, VoyagerMessage, +}; +use voyager_vm::{call, conc, data, pass::PassResult, promise, seq, BoxDynError, Op, Visit}; + +use crate::{ + call::{FetchUpdate, ModuleCall}, + callback::ModuleCallback, +}; + +pub mod call; +pub mod callback; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + Module::run().await +} + +#[derive(Debug, Clone)] +pub struct Module { + pub l0_client_id: u32, + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub l2_chain_id: ChainId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub l0_client_id: u32, + pub l1_client_id: u32, + pub l1_chain_id: ChainId, + pub l2_chain_id: ChainId, +} + +impl Plugin for Module { + type Call = ModuleCall; + type Callback = ModuleCallback; + + type Config = Config; + type Cmd = DefaultCmd; + + async fn new(config: Self::Config) -> Result { + Ok(Self { + l0_client_id: config.l0_client_id, + l1_client_id: config.l1_client_id, + l1_chain_id: config.l1_chain_id, + l2_chain_id: config.l2_chain_id, + }) + } + + fn info(config: Self::Config) -> PluginInfo { + PluginInfo { + name: plugin_name(&config.l2_chain_id), + interest_filter: UpdateHook::filter( + &config.l2_chain_id, + &ClientType::new(ClientType::STATE_LENS_ICS23_ICS23), + ), + } + } + + async fn cmd(_config: Self::Config, cmd: Self::Cmd) { + match cmd {} + } +} + +fn plugin_name(chain_id: &ChainId) -> String { + pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME"); + + format!("{PLUGIN_NAME}/{}", chain_id) +} + +impl Module { + fn plugin_name(&self) -> String { + plugin_name(&self.l2_chain_id) + } +} + +#[async_trait] +impl PluginServer for Module { + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn run_pass( + &self, + _: &Extensions, + msgs: Vec>, + ) -> RpcResult> { + Ok(PassResult { + optimize_further: vec![], + ready: msgs + .into_iter() + .map(|mut op| { + UpdateHook::new( + &self.l2_chain_id, + &ClientType::new(ClientType::STATE_LENS_ICS23_ICS23), + |fetch| { + Call::Plugin(PluginMessage::new( + self.plugin_name(), + ModuleCall::from(FetchUpdate { + counterparty_chain_id: fetch.counterparty_chain_id.clone(), + update_from: fetch.update_from, + update_to: fetch.update_to, + }), + )) + }, + ) + .visit_op(&mut op); + + op + }) + .enumerate() + .map(|(i, op)| (vec![i], op)) + .collect(), + }) + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn call(&self, ext: &Extensions, msg: ModuleCall) -> RpcResult> { + match msg { + ModuleCall::FetchUpdate(FetchUpdate { + counterparty_chain_id, + update_from, + update_to, + }) => { + let voy_client = ext.try_get::()?; + let l1_latest_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), true) + .await?; + let l2_consensus_state_proof = serde_json::from_value::( + voy_client + .query_ibc_proof( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + ConsensusStatePath { + client_id: self.l1_client_id, + height: update_to.height(), + }, + ) + .await + .expect("big trouble") + .proof, + ) + .expect("impossible"); + let l2_merkle_proof = unionlabs::union::ics23::merkle_proof::MerkleProof::try_from( + protos::ibc::core::commitment::v1::MerkleProof::from(l2_consensus_state_proof), + ) + .expect("impossible"); + let continuation = call(PluginMessage::new( + self.plugin_name(), + ModuleCall::from(FetchUpdateAfterL1Update { + counterparty_chain_id, + update_from, + update_to, + }), + )); + // If the L2 consensus proof exists on the L1, we don't have to update the L2 on the L1. + match l2_merkle_proof { + unionlabs::union::ics23::merkle_proof::MerkleProof::Membership(_, _) => { + Ok(continuation) + } + _ => Ok(conc([ + // Update the L2 (tm) client on L1 (union) and then dispatch the continuation + promise( + [call(FetchUpdateHeaders { + client_type: ClientType::new(ClientType::TENDERMINT), + chain_id: self.l2_chain_id.clone(), + counterparty_chain_id: self.l1_chain_id.clone(), + update_from, + update_to, + })], + [], + AggregateMsgUpdateClientsFromOrderedHeaders { + ibc_spec_id: IbcUnion::ID, + chain_id: self.l1_chain_id.clone(), + client_id: RawClientId::new(self.l1_client_id), + }, + ), + seq([ + call(WaitForTrustedHeight { + chain_id: self.l1_chain_id.clone(), + ibc_spec_id: IbcUnion::ID, + client_id: RawClientId::new(self.l1_client_id), + height: update_to, + finalized: true, + }), + continuation, + ]), + ])), + } + } + ModuleCall::FetchUpdateAfterL1Update(FetchUpdateAfterL1Update { + counterparty_chain_id, + .. + }) => { + let voy_client = ext.try_get::()?; + let l1_latest_height = voy_client + .query_latest_height(self.l1_chain_id.clone(), false) + .await?; + debug!("l1 latest height {}", l1_latest_height); + let l0_client_meta = voy_client + .client_meta::( + counterparty_chain_id.clone(), + QueryHeight::Latest, + self.l0_client_id, + ) + .await?; + let l1_client_meta = voy_client + .client_meta::( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + self.l1_client_id, + ) + .await?; + // The client has been updated to at least update_to + let update_to = l1_client_meta.counterparty_height; + debug!("l0 client meta {:#?}", l0_client_meta); + let l2_consensus_state_path = ConsensusStatePath { + client_id: self.l1_client_id, + height: update_to.height(), + }; + let l2_consensus_state = voy_client + .query_ibc_state( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + l2_consensus_state_path.clone(), + ) + .await? + .state; + debug!("l2 consensus state {:#?}", l2_consensus_state); + let l2_consensus_state_proof = serde_json::from_value::( + voy_client + .query_ibc_proof( + self.l1_chain_id.clone(), + QueryHeight::Specific(l1_latest_height), + l2_consensus_state_path, + ) + .await + .expect("big trouble") + .proof, + ) + .expect("impossible"); + debug!("l2 consensus state proof {:#?}", l2_consensus_state_proof); + // Dispatch an update for the L1 on the destination, then dispatch the L2 update on the destination + Ok(conc([ + promise( + [call(FetchUpdateHeaders { + client_type: ClientType::new(ClientType::COMETBLS), + chain_id: self.l1_chain_id.clone(), + counterparty_chain_id: counterparty_chain_id.clone(), + update_from: l0_client_meta.counterparty_height, + update_to: l1_latest_height, + })], + [], + AggregateMsgUpdateClientsFromOrderedHeaders { + ibc_spec_id: IbcUnion::ID, + chain_id: counterparty_chain_id.clone(), + client_id: RawClientId::new(self.l0_client_id), + }, + ), + seq([ + call(WaitForTrustedHeight { + chain_id: counterparty_chain_id, + ibc_spec_id: IbcUnion::ID, + client_id: RawClientId::new(self.l0_client_id), + height: l1_latest_height, + finalized: false, + }), + data(OrderedHeaders { + headers: vec![( + DecodedHeaderMeta { height: update_to }, + into_value(Header { + l1_height: l1_latest_height, + l2_height: update_to, + l2_consensus_state_proof, + l2_consensus_state, + }), + )], + }), + ]), + ])) + } + } + } + + #[instrument(skip_all, fields(chain_id = %self.l2_chain_id))] + async fn callback( + &self, + _: &Extensions, + callback: ModuleCallback, + _data: VecDeque, + ) -> RpcResult> { + match callback {} + } +} diff --git a/voyager/plugins/client-update/tendermint/src/main.rs b/voyager/plugins/client-update/tendermint/src/main.rs index 153666f962..cc81033b82 100644 --- a/voyager/plugins/client-update/tendermint/src/main.rs +++ b/voyager/plugins/client-update/tendermint/src/main.rs @@ -8,7 +8,10 @@ use jsonrpsee::{ use serde::{Deserialize, Serialize}; use tendermint_light_client_types::Header; use tracing::instrument; -use unionlabs::primitives::{encoding::HexUnprefixed, H160}; +use unionlabs::{ + ibc::core::client::height::Height, + primitives::{encoding::HexUnprefixed, H160}, +}; use voyager_message::{ call::Call, core::{ChainId, ClientType}, @@ -198,7 +201,10 @@ impl PluginServer for Module { untrusted_commit.signed_header.header.proposer_address, ), signed_header: untrusted_commit.signed_header, - trusted_height: update_from, + trusted_height: Height::new_with_revision( + self.chain_revision, + update_from.height(), + ), trusted_validators: mk_validator_set( trusted_validators.validators, trusted_commit.signed_header.header.proposer_address, diff --git a/voyager/plugins/event-source/ethereum/src/main.rs b/voyager/plugins/event-source/ethereum/src/main.rs index 671067a066..aa36262895 100644 --- a/voyager/plugins/event-source/ethereum/src/main.rs +++ b/voyager/plugins/event-source/ethereum/src/main.rs @@ -451,7 +451,7 @@ impl PluginServer for Module { client_id: raw_event.client_id, connection_id: raw_event.connection_id, counterparty_client_id: raw_event.counterparty_client_id, - counterparty_connection_id: raw_event.counterparty_client_id, + counterparty_connection_id: raw_event.counterparty_connection_id, } .into();