diff --git a/Cargo.lock b/Cargo.lock index 4d9f79266a..e881f2954b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,6 +986,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "hex", + "ics008-wasm-client", "prost", "protos", "schemars", @@ -995,7 +996,6 @@ dependencies = [ "sha3", "thiserror", "unionlabs", - "wasm-light-client-types", ] [[package]] @@ -1768,6 +1768,7 @@ dependencies = [ "hex", "hex-literal", "ibc", + "ics008-wasm-client", "prost", "protos", "rlp", @@ -1779,7 +1780,6 @@ dependencies = [ "thiserror", "tiny-keccak", "unionlabs", - "wasm-light-client-types", ] [[package]] @@ -2846,6 +2846,19 @@ dependencies = [ "tendermint-proto 0.29.1", ] +[[package]] +name = "ics008-wasm-client" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "ibc", + "prost", + "protos", + "serde", + "serde_json", + "unionlabs", +] + [[package]] name = "ics23" version = "0.9.0" @@ -6001,18 +6014,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "wasm-light-client-types" -version = "0.1.0" -dependencies = [ - "cosmwasm-std", - "ibc", - "prost", - "protos", - "serde", - "unionlabs", -] - [[package]] name = "web-sys" version = "0.3.64" diff --git a/Cargo.toml b/Cargo.toml index cfc71a7e54..be3a9ea0ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "lib/*/fuzz", "light-clients/ethereum-light-client", "light-clients/cometbls-light-client", - "light-clients/wasm-light-client-types", "generated/rust", "generated/contracts", "unionvisor", @@ -45,7 +44,7 @@ contracts = { path = "generated/contracts", default-features = false } serde-utils = { path = "lib/serde-utils", default-features = false } ethereum-verifier = { path = "lib/ethereum-verifier", default-features = false } cometbls-groth16-verifier = { path = "lib/cometbls-groth16-verifier", default-features = false } -wasm-light-client-types = { path = "light-clients/wasm-light-client-types", default-features = false } +ics008-wasm-client = { path = "lib/ics-008-wasm-client", default-features = false } protos = { path = "generated/rust", default-features = false } unionlabs = { path = "lib/unionlabs", default-features = false } beacon-api = { path = "lib/beacon-api", default-features = false } diff --git a/light-clients/wasm-light-client-types/Cargo.toml b/lib/ics-008-wasm-client/Cargo.toml similarity index 88% rename from light-clients/wasm-light-client-types/Cargo.toml rename to lib/ics-008-wasm-client/Cargo.toml index bd108caec9..8783a3ea6e 100644 --- a/light-clients/wasm-light-client-types/Cargo.toml +++ b/lib/ics-008-wasm-client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wasm-light-client-types" +name = "ics008-wasm-client" version = "0.1.0" authors = ["Union Labs"] edition = "2021" @@ -13,3 +13,6 @@ prost = { version = "0.11", default-features = false } protos = { workspace = true, default-features = false, features = ["proto_full", "std"] } serde = { version = "1.0", default-features = false, features = ["derive"] } unionlabs = { workspace = true, default-features = false } + +[dev-dependencies] +serde_json = "1.0.0" diff --git a/lib/ics-008-wasm-client/src/ibc_client.rs b/lib/ics-008-wasm-client/src/ibc_client.rs new file mode 100644 index 0000000000..873527d4c8 --- /dev/null +++ b/lib/ics-008-wasm-client/src/ibc_client.rs @@ -0,0 +1,202 @@ +use core::fmt::Debug; + +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo}; +use unionlabs::{ + ibc::{ + core::client::height::Height, + lightclients::wasm::{client_state::ClientState, consensus_state::ConsensusState}, + }, + Proto, TryFromProto, TryFromProtoBytesError, TryFromProtoErrorOf, +}; + +use crate::{ + msg::{ClientMessage, ContractResult, ExecuteMsg, MerklePath, QueryMsg, QueryResponse}, + Error, +}; + +pub enum StorageState { + Occupied(Vec), + Empty, +} + +pub trait IbcClient { + type Error: From>> + From; + type CustomQuery: cosmwasm_std::CustomQuery; + // TODO(aeryz): see #583 + type Header: TryFromProto; + // TODO(aeryz): see #583, #588 + type Misbehaviour; + type ClientState: TryFromProto; + type ConsensusState: TryFromProto; + + fn execute( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: ExecuteMsg, + ) -> Result + where + // NOTE(aeryz): unfortunately bounding to `Debug` in associated type creates a + // recursion in the compiler, see this issue: https://github.com/rust-lang/rust/issues/87755 + ::Proto: prost::Message + Default, + TryFromProtoErrorOf: Debug, + ::Proto: prost::Message + Default, + TryFromProtoErrorOf: Debug, + { + match msg { + ExecuteMsg::VerifyMembership { + height, + delay_time_period, + delay_block_period, + proof, + path, + value, + } => Self::verify_membership( + deps.as_ref(), + height, + delay_time_period, + delay_block_period, + proof, + path, + StorageState::Occupied(value.0), + ), + ExecuteMsg::VerifyNonMembership { + height, + delay_time_period, + delay_block_period, + proof, + path, + } => Self::verify_membership( + deps.as_ref(), + height, + delay_time_period, + delay_block_period, + proof, + path, + StorageState::Empty, + ), + ExecuteMsg::VerifyClientMessage { client_message } => match client_message { + ClientMessage::Header(header) => { + let header = Self::Header::try_from_proto_bytes(&header.data)?; + Self::verify_header(deps.as_ref(), env, header) + } + ClientMessage::Misbehaviour(_misbehaviour) => { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + }, + ExecuteMsg::UpdateState { client_message } => match client_message { + ClientMessage::Header(header) => { + let header = Self::Header::try_from_proto_bytes(&header.data)?; + Self::update_state(deps, env, header) + } + ClientMessage::Misbehaviour(_) => Err(Error::UnexpectedCallDataFromHostModule( + "`UpdateState` cannot be called with `Misbehaviour`".to_string(), + ) + .into()), + }, + ExecuteMsg::UpdateStateOnMisbehaviour { client_message } => { + Self::update_state_on_misbehaviour(deps, client_message) + } + ExecuteMsg::CheckForMisbehaviour { client_message } => match client_message { + ClientMessage::Header(header) => { + let header = Self::Header::try_from_proto_bytes(&header.data)?; + Self::verify_header(deps.as_ref(), env, header) + } + ClientMessage::Misbehaviour(_) => { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + }, + ExecuteMsg::VerifyUpgradeAndUpdateState { + upgrade_client_state, + upgrade_consensus_state, + proof_upgrade_client, + proof_upgrade_consensus_state, + } => Self::verify_upgrade_and_update_state( + deps, + <_>::try_from_proto(upgrade_client_state) + .map_err(|err| Error::Decode(format!("{err:?}")))?, + <_>::try_from_proto(upgrade_consensus_state) + .map_err(|err| Error::Decode(format!("{err:?}")))?, + proof_upgrade_client, + proof_upgrade_consensus_state, + ), + ExecuteMsg::CheckSubstituteAndUpdateState {} => { + Self::check_substitute_and_update_state(deps.as_ref()) + } + } + } + + fn query( + deps: Deps, + env: Env, + msg: QueryMsg, + ) -> Result { + match msg { + QueryMsg::Status {} => Self::status(deps, &env), + QueryMsg::ExportMetadata {} => Self::export_metadata(deps, &env), + } + } + + #[allow(clippy::too_many_arguments)] + fn verify_membership( + deps: Deps, + height: Height, + delay_time_period: u64, + delay_block_period: u64, + proof: Binary, + path: MerklePath, + value: StorageState, + ) -> Result; + + fn verify_header( + deps: Deps, + env: Env, + header: Self::Header, + ) -> Result; + + fn verify_misbehaviour( + deps: Deps, + misbehaviour: Self::Misbehaviour, + ) -> Result; + + fn update_state( + deps: DepsMut, + env: Env, + header: Self::Header, + ) -> Result; + + // TODO(aeryz): make this client message generic over the underlying types + fn update_state_on_misbehaviour( + deps: DepsMut, + client_message: ClientMessage, + ) -> Result; + + fn check_for_misbehaviour_on_header( + deps: Deps, + header: Self::Header, + ) -> Result; + + fn check_for_misbehaviour_on_misbehaviour( + deps: Deps, + misbehaviour: Self::Misbehaviour, + ) -> Result; + + fn verify_upgrade_and_update_state( + deps: DepsMut, + upgrade_client_state: ClientState, + upgrade_consensus_state: ConsensusState, + proof_upgrade_client: Binary, + proof_upgrade_consensus_state: Binary, + ) -> Result; + + fn check_substitute_and_update_state( + deps: Deps, + ) -> Result; + + fn status(deps: Deps, env: &Env) -> Result; + + fn export_metadata( + deps: Deps, + env: &Env, + ) -> Result; +} diff --git a/lib/ics-008-wasm-client/src/lib.rs b/lib/ics-008-wasm-client/src/lib.rs new file mode 100644 index 0000000000..d026d39d73 --- /dev/null +++ b/lib/ics-008-wasm-client/src/lib.rs @@ -0,0 +1,15 @@ +#![recursion_limit = "512"] + +mod ibc_client; +mod msg; +pub mod storage_utils; + +pub use ibc_client::*; +pub use msg::*; + +#[derive(Debug)] +pub enum Error { + Decode(String), + UnexpectedCallDataFromHostModule(String), + ClientStateNotFound, +} diff --git a/light-clients/wasm-light-client-types/src/msg.rs b/lib/ics-008-wasm-client/src/msg.rs similarity index 77% rename from light-clients/wasm-light-client-types/src/msg.rs rename to lib/ics-008-wasm-client/src/msg.rs index 24f387d970..8814a8e0c6 100644 --- a/light-clients/wasm-light-client-types/src/msg.rs +++ b/lib/ics-008-wasm-client/src/msg.rs @@ -14,9 +14,10 @@ pub struct MerklePath { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct ClientMessage { - pub header: Option
, - pub misbehaviour: Option, +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum ClientMessage { + Header(Header), + Misbehaviour(Misbehaviour), } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -96,18 +97,17 @@ pub enum ExecuteMsg { }, CheckSubstituteAndUpdateState {}, - - ExportMetadata {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields, rename_all = "snake_case")] pub enum QueryMsg { Status {}, + ExportMetadata {}, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct StatusResponse { +pub struct QueryResponse { pub status: String, pub genesis_metadata: Vec, } @@ -129,11 +129,40 @@ impl Display for Status { } } -impl From for StatusResponse { +impl From for QueryResponse { fn from(value: Status) -> Self { - StatusResponse { + QueryResponse { status: value.to_string(), genesis_metadata: Vec::new(), } } } + +#[cfg(test)] +mod tests { + use protos::ibc::lightclients::wasm::v1::Header; + + use crate::{ClientMessage, ExecuteMsg}; + + #[test] + fn execute_msg_snake_case_encoded() { + let msg = ExecuteMsg::CheckSubstituteAndUpdateState {}; + assert_eq!( + serde_json::to_string(&msg).unwrap(), + r#"{"check_substitute_and_update_state":{}}"# + ) + } + + #[test] + fn client_msg_snake_case_encoded() { + let msg = ClientMessage::Header(Header { + data: vec![], + height: None, + }); + + assert_eq!( + serde_json::to_string(&msg).unwrap(), + r#"{"header":{"data":"","height":null}}"# + ) + } +} diff --git a/light-clients/cometbls-light-client/src/state.rs b/lib/ics-008-wasm-client/src/storage_utils.rs similarity index 54% rename from light-clients/cometbls-light-client/src/state.rs rename to lib/ics-008-wasm-client/src/storage_utils.rs index da3da26d8f..4f7251978e 100644 --- a/light-clients/cometbls-light-client/src/state.rs +++ b/lib/ics-008-wasm-client/src/storage_utils.rs @@ -1,20 +1,18 @@ -use cosmwasm_std::{Deps, DepsMut}; +use core::fmt::Debug; + +use cosmwasm_std::{CustomQuery, Deps, DepsMut}; use unionlabs::{ - ibc::{ - core::client::height::Height, - google::protobuf::any::Any, - lightclients::{cometbls, wasm}, - }, - IntoProto, TryFromProto, + ibc::{core::client::height::Height, google::protobuf::any::Any, lightclients::wasm}, + IntoProto, Proto, TryFromProto, TryFromProtoErrorOf, }; -use crate::errors::Error; +use crate::Error; // Client state that is stored by the host pub const HOST_CLIENT_STATE_KEY: &str = "clientState"; pub const HOST_CONSENSUS_STATES_KEY: &str = "consensusStates"; -fn consensus_db_key(height: &Height) -> String { +pub fn consensus_db_key(height: &Height) -> String { format!( "{}/{}-{}", HOST_CONSENSUS_STATES_KEY, height.revision_number, height.revision_height @@ -28,10 +26,15 @@ fn consensus_db_key(height: &Height) -> String { /// - value: (PROTO_ENCODED_WASM_CLIENT_STATE) /// - code_id: Code ID of this contract's code /// - latest_height: Latest height that the state is updated -/// - data: Contract defined raw bytes, which we use as protobuf encoded ethereum client state. -pub fn read_client_state( - deps: Deps, -) -> Result, Error> { +/// - data: Contract defined raw bytes, which we use as protobuf encoded concrete client state. +pub fn read_client_state( + deps: Deps, +) -> Result, Error> +where + CS: TryFromProto + Debug, + ::Proto: prost::Message + Default, + TryFromProtoErrorOf: Debug, +{ let any_state = deps .storage .get(HOST_CLIENT_STATE_KEY.as_bytes()) @@ -39,11 +42,7 @@ pub fn read_client_state( Any::try_from_proto_bytes(any_state.as_slice()) .map(|any| any.0) - .map_err(|err| { - Error::decode(format!( - "when decoding raw bytes to any in `read_client_state`: {err:#?}" - )) - }) + .map_err(|err| Error::Decode(format!("error reading the client state: {err:#?}"))) } /// Reads the consensus state at a specific height from the host. @@ -53,24 +52,26 @@ pub fn read_client_state( /// - type_url: WASM_CONSENSUS_STATE_TYPE_URL /// - value: (PROTO_ENCODED_WASM_CLIENT_STATE) /// - timestamp: Time of this consensus state. -/// - data: Contract defined raw bytes, which we use as protobuf encoded ethereum consensus state. -pub fn read_consensus_state( - deps: Deps, +/// - data: Contract defined raw bytes, which we use as protobuf encoded concrete consensus state. +pub fn read_consensus_state( + deps: Deps, height: &Height, -) -> Result< - Option>, - Error, -> { +) -> Result>, Error> +where + CS: TryFromProto + Debug, + ::Proto: prost::Message + Default, + TryFromProtoErrorOf: Debug, +{ deps.storage .get(consensus_db_key(height).as_bytes()) .map(|bytes| Any::try_from_proto_bytes(&bytes).map(|any| any.0)) .transpose() - .map_err(|err| Error::decode(format!("error reading consensus state: {err:#?}"))) + .map_err(|err| Error::Decode(format!("error reading consensus state: {err:#?}"))) } -pub fn save_wasm_client_state( - deps: DepsMut, - wasm_client_state: wasm::client_state::ClientState, +pub fn save_client_state( + deps: DepsMut, + wasm_client_state: wasm::client_state::ClientState, ) { let any_state = Any(wasm_client_state); deps.storage.set( @@ -80,26 +81,22 @@ pub fn save_wasm_client_state( } /// Update the client state on the host store. -pub fn update_client_state( - deps: DepsMut, - mut wasm_client_state: wasm::client_state::ClientState, - // new_client_state: ethereum::client_state::ClientState, +pub fn update_client_state( + deps: DepsMut, + mut wasm_client_state: wasm::client_state::ClientState, latest_execution_height: u64, ) { - // wasm_client_state.data = new_client_state; wasm_client_state.latest_height = Height { revision_number: 0, revision_height: latest_execution_height, }; - save_wasm_client_state(deps, wasm_client_state); + save_client_state(deps, wasm_client_state); } -pub fn save_wasm_consensus_state( - deps: DepsMut, - wasm_consensus_state: wasm::consensus_state::ConsensusState< - cometbls::consensus_state::ConsensusState, - >, +pub fn save_consensus_state( + deps: DepsMut, + wasm_consensus_state: wasm::consensus_state::ConsensusState, height: &Height, ) { deps.storage.set( @@ -107,15 +104,3 @@ pub fn save_wasm_consensus_state( &Any(wasm_consensus_state).into_proto_bytes(), ); } - -/// Save new consensus state at height `consensus_state.slot` to the host store. -pub fn save_consensus_state( - deps: DepsMut, - wasm_consensus_state: wasm::consensus_state::ConsensusState< - cometbls::consensus_state::ConsensusState, - >, - height: Height, -) -> Result<(), Error> { - save_wasm_consensus_state(deps, wasm_consensus_state, &height); - Ok(()) -} diff --git a/light-clients/cometbls-light-client/Cargo.toml b/light-clients/cometbls-light-client/Cargo.toml index a4a3a7d89d..8b6a628e88 100644 --- a/light-clients/cometbls-light-client/Cargo.toml +++ b/light-clients/cometbls-light-client/Cargo.toml @@ -23,7 +23,7 @@ thiserror = { version = "1.0.26", default-features = false } protos = { workspace = true, default-features = false, features = ["proto_full", "std"] } cometbls-groth16-verifier = { workspace = true, default-features = false } -wasm-light-client-types.workspace = true +ics008-wasm-client.workspace = true unionlabs = { workspace = true, default-features = false } [dev-dependencies] diff --git a/light-clients/cometbls-light-client/src/bin/schema.rs b/light-clients/cometbls-light-client/src/bin/schema.rs deleted file mode 100644 index f328e4d9d0..0000000000 --- a/light-clients/cometbls-light-client/src/bin/schema.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/light-clients/cometbls-light-client/src/client.rs b/light-clients/cometbls-light-client/src/client.rs new file mode 100644 index 0000000000..c1d0b625a4 --- /dev/null +++ b/light-clients/cometbls-light-client/src/client.rs @@ -0,0 +1,311 @@ +use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env}; +use ics008_wasm_client::{ + storage_utils::{ + read_client_state, read_consensus_state, save_client_state, save_consensus_state, + }, + ContractResult, IbcClient, MerklePath, Status, StorageState, +}; +use prost::Message; +use unionlabs::{ + ibc::{ + core::{client::height::Height, commitment::merkle_root::MerkleRoot}, + google::protobuf::timestamp::Timestamp, + lightclients::cometbls::{ + client_state::ClientState, consensus_state::ConsensusState, header::Header, + }, + }, + tendermint::types::commit::Commit, +}; + +use crate::{errors::Error, zkp_verifier::verify_zkp}; + +type WasmClientState = unionlabs::ibc::lightclients::wasm::client_state::ClientState; +type WasmConsensusState = + unionlabs::ibc::lightclients::wasm::consensus_state::ConsensusState; + +pub struct CometblsLightClient; + +impl IbcClient for CometblsLightClient { + type Error = Error; + + type CustomQuery = Empty; + + type Header = Header; + + // TODO(aeryz): Change this to appropriate misbehavior type when it is implemented + type Misbehaviour = (); + + type ClientState = ClientState; + + type ConsensusState = ConsensusState; + + fn verify_membership( + _deps: Deps, + _height: Height, + _delay_time_period: u64, + _delay_block_period: u64, + _proof: Binary, + _path: MerklePath, + _value: StorageState, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + fn verify_header( + deps: Deps, + env: Env, + header: Self::Header, + ) -> Result { + let client_state: WasmClientState = read_client_state(deps)?; + let consensus_state: WasmConsensusState = + read_consensus_state(deps, &header.trusted_height)?.ok_or( + Error::ConsensusStateNotFound( + header.trusted_height.revision_number, + header.trusted_height.revision_height, + ), + )?; + + let untrusted_height_number = header.signed_header.commit.height.inner() as u64; + let trusted_height_number = header.trusted_height.revision_number; + + if untrusted_height_number <= trusted_height_number { + return Err(Error::InvalidHeader( + "header height <= consensus state height".into(), + )); + } + + let trusted_timestamp = consensus_state.timestamp; + let untrusted_timestamp = header.signed_header.header.time.seconds; + + if untrusted_timestamp.inner() as u64 <= trusted_timestamp { + return Err(Error::InvalidHeader( + "header time <= consensus state time".into(), + )); + } + + let current_time: Timestamp = env + .block + .time + .try_into() + .map_err(|_| Error::InvalidHeader("timestamp conversion failed".into()))?; + + if current_time + .duration_since(&header.signed_header.header.time) + .ok_or(Error::DurationAdditionOverflow)? + > client_state.data.trusting_period + { + return Err(Error::InvalidHeader("header expired".into())); + } + + let max_clock_drift = + current_time.seconds.inner() + client_state.data.max_clock_drift.seconds().inner(); + + if untrusted_timestamp.inner() >= max_clock_drift { + return Err(Error::InvalidHeader("header back to the future".into())); + } + + let trusted_validators_hash = consensus_state.data.next_validators_hash; + + let untrusted_validators_hash = if untrusted_height_number == trusted_height_number + 1 { + trusted_validators_hash.clone() + } else { + header.untrusted_validator_set_root + }; + + let expected_block_hash = header + .signed_header + .header + .calculate_merkle_root() + .ok_or(Error::UnableToCalculateMerkleRoot)?; + + if header.signed_header.commit.block_id.hash.0.as_slice() != expected_block_hash { + return Err(Error::InvalidHeader( + "commit.block_id.hash != header.root()".into(), + )); + } + + let signed_vote = canonical_vote( + &header.signed_header.commit, + header.signed_header.header.chain_id.clone(), + expected_block_hash, + ) + .encode_length_delimited_to_vec(); + + if !verify_zkp( + &trusted_validators_hash.0, + &untrusted_validators_hash.0, + &signed_vote, + &header.zero_knowledge_proof, + ) { + return Err(Error::InvalidZKP); + } + + Ok(ContractResult::valid(None)) + } + + fn verify_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + fn update_state( + mut deps: DepsMut, + _env: Env, + header: Self::Header, + ) -> Result { + let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + let mut consensus_state: WasmConsensusState = + read_consensus_state(deps.as_ref(), &header.trusted_height)?.ok_or( + Error::ConsensusStateNotFound( + header.trusted_height.revision_number, + header.trusted_height.revision_height, + ), + )?; + + let untrusted_height = Height::new( + header.trusted_height.revision_number, + header.signed_header.commit.height.inner() as u64, + ); + + if untrusted_height > client_state.latest_height { + client_state.latest_height = untrusted_height; + } + + consensus_state.data.root = MerkleRoot { + hash: header.signed_header.header.app_hash, + }; + + let untrusted_height_number = header.signed_header.commit.height.inner() as u64; + let trusted_height_number = header.trusted_height.revision_number; + + let untrusted_validators_hash = if untrusted_height_number == trusted_height_number + 1 { + consensus_state.data.next_validators_hash.clone() + } else { + header.untrusted_validator_set_root + }; + + consensus_state.data.next_validators_hash = untrusted_validators_hash; + consensus_state.timestamp = header.signed_header.header.time.seconds.inner() as u64; + + save_client_state(deps.branch(), client_state); + save_consensus_state(deps, consensus_state, &untrusted_height); + + Ok(ContractResult::valid(None)) + } + + fn update_state_on_misbehaviour( + _deps: DepsMut, + _client_message: ics008_wasm_client::ClientMessage, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn check_for_misbehaviour_on_header( + _deps: Deps, + _header: Self::Header, + ) -> Result { + // TODO(aeryz): Leaving this as success for us to be able to update the client. See: #588. + Ok(ContractResult::valid(None)) + } + + fn check_for_misbehaviour_on_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn verify_upgrade_and_update_state( + _deps: DepsMut, + _upgrade_client_state: WasmClientState, + _upgrade_consensus_state: WasmConsensusState, + _proof_upgrade_client: Binary, + _proof_upgrade_consensus_state: Binary, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn check_substitute_and_update_state( + _deps: Deps, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn status( + deps: Deps, + env: &cosmwasm_std::Env, + ) -> Result { + let client_state: WasmClientState = read_client_state(deps)?; + + // TODO(aeryz): make client state optional + if client_state.data.frozen_height.revision_height == 0 { + return Ok(Status::Frozen.into()); + } + + let Some(consensus_state) = read_consensus_state::( + deps, + &client_state.latest_height, + )? + else { + return Ok(Status::Expired.into()); + }; + + if is_client_expired( + consensus_state.timestamp, + client_state + .data + .trusting_period + .seconds() + .inner() + // NOTE: trusting_period *should* be strictly positive; enforce this somehow? + .try_into() + .unwrap_or_default(), + env.block.time.seconds(), + ) { + return Ok(Status::Expired.into()); + } + + Ok(Status::Active.into()) + } + + fn export_metadata( + _deps: Deps, + _env: &cosmwasm_std::Env, + ) -> Result { + Ok(ics008_wasm_client::QueryResponse { + status: String::new(), + genesis_metadata: vec![], + }) + } +} + +fn is_client_expired( + consensus_state_timestamp: u64, + trusting_period: u64, + current_block_time: u64, +) -> bool { + consensus_state_timestamp + trusting_period < current_block_time +} + +fn canonical_vote( + commit: &Commit, + chain_id: String, + expected_block_hash: [u8; 32], +) -> protos::tendermint::types::CanonicalVote { + protos::tendermint::types::CanonicalVote { + r#type: protos::tendermint::types::SignedMsgType::Precommit as i32, + height: commit.height.inner(), + round: commit.round.inner() as i64, + // TODO(aeryz): Implement BlockId to proto::CanonicalBlockId + block_id: Some(protos::tendermint::types::CanonicalBlockId { + hash: expected_block_hash.to_vec(), + part_set_header: Some(protos::tendermint::types::CanonicalPartSetHeader { + total: commit.block_id.part_set_header.total, + hash: commit.block_id.part_set_header.hash.0.to_vec(), + }), + }), + chain_id, + } +} diff --git a/light-clients/cometbls-light-client/src/contract.rs b/light-clients/cometbls-light-client/src/contract.rs index 32b5d74cd9..2aab909b31 100644 --- a/light-clients/cometbls-light-client/src/contract.rs +++ b/light-clients/cometbls-light-client/src/contract.rs @@ -1,30 +1,13 @@ use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - StdError, -}; -use prost::Message; -use protos::tendermint::types::{CanonicalBlockId, CanonicalPartSetHeader, SignedMsgType}; -use unionlabs::{ - ibc::{ - core::{client::height::Height, commitment::merkle_root::MerkleRoot}, - google::protobuf::timestamp::Timestamp, - lightclients::cometbls::header::Header, - }, - tendermint::types::commit::Commit, - TryFromProto, -}; -use wasm_light_client_types::msg::{ - ClientMessage, ContractResult, MerklePath, Status, StatusResponse, + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; +use ics008_wasm_client::{ExecuteMsg, IbcClient, QueryMsg}; +use serde::{Deserialize, Serialize}; -use crate::{ - errors::Error, - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, - state::{ - read_client_state, read_consensus_state, save_consensus_state, save_wasm_client_state, - }, - zkp_verifier::verify_zkp, -}; +use crate::{client::CometblsLightClient, errors::Error}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct InstantiateMsg {} #[entry_point] pub fn instantiate( @@ -40,230 +23,17 @@ pub fn instantiate( pub fn execute( deps: DepsMut, env: Env, - _info: MessageInfo, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { - let result = match msg { - ExecuteMsg::VerifyMembership { - height, - delay_time_period, - delay_block_period, - proof, - path, - value, - } => verify_membership( - deps.as_ref(), - height, - delay_time_period, - delay_block_period, - proof, - path, - value, - ), - ExecuteMsg::UpdateState { - client_message: ClientMessage { header, .. }, - } => { - if let Some(header) = header { - let header = Header::try_from_proto_bytes(&header.data).map_err(|err| { - Error::decode(format!( - "when converting proto header to header in update: {err:#?}" - )) - })?; - update_header(deps, env, header) - } else { - Err(StdError::not_found("Not implemented").into()) - } - } - _ => Ok(ContractResult::valid(None)), - }?; + let result = CometblsLightClient::execute(deps, env, info, msg)?; Ok(Response::default().set_data(result.encode()?)) } -/// Verifies if the `value` is committed at `path` in the counterparty light client. -pub fn verify_membership( - _deps: Deps, - _height: Height, - _delay_time_period: u64, - _delay_block_period: u64, - _proof: Binary, - _path: MerklePath, - _value: Binary, -) -> Result { - // TODO: #514 - Ok(ContractResult::valid(None)) -} - -pub fn update_header(mut deps: DepsMut, env: Env, header: Header) -> Result { - let mut client_state = read_client_state(deps.as_ref())?; - let mut consensus_state = read_consensus_state(deps.as_ref(), &header.trusted_height)?.ok_or( - Error::ConsensusStateNotFound( - header.trusted_height.revision_number, - header.trusted_height.revision_height, - ), - )?; - - let untrusted_height_number = header.signed_header.commit.height.inner() as u64; - let trusted_height_number = header.trusted_height.revision_number; - - if untrusted_height_number <= trusted_height_number { - return Err(Error::InvalidHeader( - "header height <= consensus state height".into(), - )); - } - - let trusted_timestamp = consensus_state.timestamp; - let untrusted_timestamp = header.signed_header.header.time.seconds; - - if untrusted_timestamp.inner() as u64 <= trusted_timestamp { - return Err(Error::InvalidHeader( - "header time <= consensus state time".into(), - )); - } - - let current_time: Timestamp = env - .block - .time - .try_into() - .map_err(|_| Error::InvalidHeader("timestamp conversion failed".into()))?; - - if current_time - .duration_since(&header.signed_header.header.time) - .ok_or(Error::DurationAdditionOverflow)? - > client_state.data.trusting_period - { - return Err(Error::InvalidHeader("header expired".into())); - } - - let max_clock_drift = - current_time.seconds.inner() + client_state.data.max_clock_drift.seconds().inner(); - - if untrusted_timestamp.inner() >= max_clock_drift { - return Err(Error::InvalidHeader("header back to the future".into())); - } - - let trusted_validators_hash = consensus_state.data.next_validators_hash.clone(); - - let untrusted_validators_hash = if untrusted_height_number == trusted_height_number + 1 { - trusted_validators_hash.clone() - } else { - header.untrusted_validator_set_root - }; - - let expected_block_hash = header - .signed_header - .header - .calculate_merkle_root() - .ok_or(Error::UnableToCalculateMerkleRoot)?; - - if header.signed_header.commit.block_id.hash.0.as_slice() != expected_block_hash { - return Err(Error::InvalidHeader( - "commit.block_id.hash != header.root()".into(), - )); - } - - let signed_vote = canonical_vote( - &header.signed_header.commit, - header.signed_header.header.chain_id.clone(), - expected_block_hash, - ) - .encode_length_delimited_to_vec(); - - if !verify_zkp( - &trusted_validators_hash.0, - &untrusted_validators_hash.0, - &signed_vote, - &header.zero_knowledge_proof, - ) { - return Err(Error::InvalidZKP); - } - - let untrusted_height = Height::new( - header.trusted_height.revision_number, - header.signed_header.commit.height.inner() as u64, - ); - - if untrusted_height > client_state.latest_height { - client_state.latest_height = untrusted_height; - } - - consensus_state.data.root = MerkleRoot { - hash: header.signed_header.header.app_hash, - }; - - consensus_state.data.next_validators_hash = untrusted_validators_hash; - consensus_state.timestamp = header.signed_header.header.time.seconds.inner() as u64; - - save_wasm_client_state(deps.branch(), client_state); - save_consensus_state(deps, consensus_state, untrusted_height)?; - - Ok(ContractResult::valid(None)) -} - -fn canonical_vote( - commit: &Commit, - chain_id: String, - expected_block_hash: [u8; 32], -) -> protos::tendermint::types::CanonicalVote { - protos::tendermint::types::CanonicalVote { - r#type: SignedMsgType::Precommit as i32, - height: commit.height.inner(), - round: commit.round.inner() as i64, - // TODO(aeryz): Implement BlockId to proto::CanonicalBlockId - block_id: Some(CanonicalBlockId { - hash: expected_block_hash.to_vec(), - part_set_header: Some(CanonicalPartSetHeader { - total: commit.block_id.part_set_header.total, - hash: commit.block_id.part_set_header.hash.0.to_vec(), - }), - }), - chain_id, - } -} - #[entry_point] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - let response = match msg { - QueryMsg::Status {} => query_status(deps, &env)?, - }; + let response = CometblsLightClient::query(deps, env, msg)?; to_binary(&response).map_err(Into::into) } - -fn query_status(deps: Deps, env: &Env) -> Result { - let client_state = read_client_state(deps)?; - - // TODO(aeryz): make client state optional - if client_state.data.frozen_height.revision_height == 0 { - return Ok(Status::Frozen.into()); - } - - let Some(consensus_state) = read_consensus_state(deps, &client_state.latest_height)? else { - return Ok(Status::Expired.into()); - }; - - if is_client_expired( - consensus_state.timestamp, - client_state - .data - .trusting_period - .seconds() - .inner() - // NOTE: trusting_period *should* be strictly positive; enforce this somehow? - .try_into() - .unwrap_or_default(), - env.block.time.seconds(), - ) { - return Ok(Status::Expired.into()); - } - - Ok(Status::Active.into()) -} - -fn is_client_expired( - consensus_state_timestamp: u64, - trusting_period: u64, - current_block_time: u64, -) -> bool { - consensus_state_timestamp + trusting_period < current_block_time -} diff --git a/light-clients/cometbls-light-client/src/errors.rs b/light-clients/cometbls-light-client/src/errors.rs index 95498ff45b..86ef599edd 100644 --- a/light-clients/cometbls-light-client/src/errors.rs +++ b/light-clients/cometbls-light-client/src/errors.rs @@ -1,5 +1,8 @@ use cosmwasm_std::StdError; use thiserror::Error as ThisError; +use unionlabs::{ + ibc::lightclients::cometbls::header::Header, TryFromProtoBytesError, TryFromProtoErrorOf, +}; #[derive(ThisError, Debug, PartialEq)] pub enum Error { @@ -96,6 +99,9 @@ pub enum Error { #[error("Custom query: {0}")] CustomQuery(String), + + #[error("Wasm client error: {0}")] + Wasm(String), } impl Error { @@ -123,10 +129,18 @@ impl Error { } } -impl From for Error { - fn from(error: wasm_light_client_types::Error) -> Self { +impl From>> for Error { + fn from(value: TryFromProtoBytesError>) -> Self { + Self::DecodeError(format!("{:?}", value)) + } +} + +impl From for Error { + fn from(error: ics008_wasm_client::Error) -> Self { match error { - wasm_light_client_types::Error::Decode(e) => Error::DecodeError(e), + ics008_wasm_client::Error::Decode(e) => Error::DecodeError(e), + ics008_wasm_client::Error::UnexpectedCallDataFromHostModule(e) => Error::Wasm(e), + ics008_wasm_client::Error::ClientStateNotFound => Error::Wasm(format!("{error:#?}")), } } } diff --git a/light-clients/cometbls-light-client/src/lib.rs b/light-clients/cometbls-light-client/src/lib.rs index b3a96d7068..b9eb16eb16 100644 --- a/light-clients/cometbls-light-client/src/lib.rs +++ b/light-clients/cometbls-light-client/src/lib.rs @@ -1,6 +1,4 @@ +pub mod client; pub mod contract; pub mod errors; -pub mod msg; -pub mod state; -pub mod types; pub mod zkp_verifier; diff --git a/light-clients/cometbls-light-client/src/msg.rs b/light-clients/cometbls-light-client/src/msg.rs deleted file mode 100644 index 8d638cf4e8..0000000000 --- a/light-clients/cometbls-light-client/src/msg.rs +++ /dev/null @@ -1,5 +0,0 @@ -use cosmwasm_schema::cw_serde; -pub use wasm_light_client_types::msg::{ExecuteMsg, QueryMsg}; - -#[cw_serde] -pub struct InstantiateMsg {} diff --git a/light-clients/cometbls-light-client/src/types.rs b/light-clients/cometbls-light-client/src/types.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/light-clients/cometbls-light-client/src/types.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/light-clients/ethereum-light-client/Cargo.toml b/light-clients/ethereum-light-client/Cargo.toml index f6e13bba90..91d2196f2b 100644 --- a/light-clients/ethereum-light-client/Cargo.toml +++ b/light-clients/ethereum-light-client/Cargo.toml @@ -27,8 +27,8 @@ sha3 = { version = "0.10.8", default-features = false } thiserror = { version = "1.0.26", default-features = false } tiny-keccak = { version = "2.0.2", default-features = false, features = ["keccak"] } +ics008-wasm-client = { workspace = true } unionlabs = { workspace = true, default-features = false } -wasm-light-client-types.workspace = true [dev-dependencies] base64 = "0.21" diff --git a/light-clients/ethereum-light-client/src/bin/schema.rs b/light-clients/ethereum-light-client/src/bin/schema.rs deleted file mode 100644 index f328e4d9d0..0000000000 --- a/light-clients/ethereum-light-client/src/bin/schema.rs +++ /dev/null @@ -1 +0,0 @@ -fn main() {} diff --git a/light-clients/ethereum-light-client/src/client.rs b/light-clients/ethereum-light-client/src/client.rs new file mode 100644 index 0000000000..396267758b --- /dev/null +++ b/light-clients/ethereum-light-client/src/client.rs @@ -0,0 +1,1104 @@ +use std::str::FromStr; + +use cosmwasm_std::{Binary, Deps, DepsMut, Env}; +use ethabi::ethereum_types::U256 as ethabi_U256; +use ethereum_verifier::{ + compute_sync_committee_period_at_slot, compute_timestamp_at_slot, primitives::Slot, + validate_light_client_update, verify_account_storage_root, verify_storage_absence, + verify_storage_proof, +}; +use ibc::core::ics24_host::Path; +use ics008_wasm_client::{ + storage_utils::{ + read_client_state, read_consensus_state, save_consensus_state, update_client_state, + }, + ContractResult, IbcClient, QueryResponse, Status, StorageState, +}; +use sha3::Digest; +use unionlabs::{ + ethereum::H256, + ibc::{ + core::client::height::Height, + lightclients::ethereum::{ + client_state::ClientState, consensus_state::ConsensusState, header::Header, + proof::Proof, storage_proof::StorageProof, + }, + }, + TryFromProto, +}; + +use crate::{ + consensus_state::TrustedConsensusState, + context::LightClientContext, + custom_query::{query_aggregate_public_keys, CustomQuery, VerificationContext}, + errors::Error, + eth_encoding::generate_commitment_key, + Config, +}; + +type WasmClientState = unionlabs::ibc::lightclients::wasm::client_state::ClientState; +type WasmConsensusState = + unionlabs::ibc::lightclients::wasm::consensus_state::ConsensusState; + +pub struct EthereumLightClient; + +impl IbcClient for EthereumLightClient { + type Error = Error; + + type CustomQuery = CustomQuery; + + type Header = Header; + + // TODO(aeryz): See #588 + type Misbehaviour = (); + + type ClientState = ClientState; + + type ConsensusState = ConsensusState; + + fn verify_membership( + deps: Deps, + height: Height, + _delay_time_period: u64, + _delay_block_period: u64, + proof: Binary, + path: ics008_wasm_client::MerklePath, + value: ics008_wasm_client::StorageState, + ) -> Result { + let consensus_state: WasmConsensusState = read_consensus_state(deps, &height)?.ok_or( + Error::ConsensusStateNotFound(height.revision_number, height.revision_height), + )?; + let client_state: WasmClientState = read_client_state(deps)?; + + let path = Path::from_str( + path.key_path + .last() + .ok_or(Error::InvalidPath("path is empty".into()))?, + ) + .map_err(|e| Error::InvalidPath(e.to_string()))?; + + // This storage root is verified during the header update, so we don't need to verify it again. + let storage_root = consensus_state.data.storage_root; + + let storage_proof = { + let mut proofs = StorageProof::try_from_proto_bytes(&proof.0) + .map_err(|e| Error::decode(format!("when decoding storage proof: {e:#?}")))? + .proofs; + if proofs.len() > 1 { + return Err(Error::BatchingProofsNotSupported); + } + proofs.pop().ok_or(Error::EmptyProof)? + }; + + match value { + StorageState::Occupied(value) => do_verify_membership( + path, + storage_root, + client_state.data.counterparty_commitment_slot, + storage_proof, + value, + )?, + StorageState::Empty => do_verify_non_membership( + path, + storage_root, + client_state.data.counterparty_commitment_slot, + storage_proof, + )?, + } + + Ok(ContractResult::valid(None)) + } + + fn verify_header( + deps: Deps, + _env: Env, + header: Self::Header, + ) -> Result { + let trusted_sync_committee = header.trusted_sync_committee; + let wasm_consensus_state = + read_consensus_state(deps, &trusted_sync_committee.trusted_height)?.ok_or( + Error::ConsensusStateNotFound( + trusted_sync_committee.trusted_height.revision_number, + trusted_sync_committee.trusted_height.revision_height, + ), + )?; + + let aggregate_public_key = query_aggregate_public_keys( + deps, + trusted_sync_committee.sync_committee.pubkeys.clone().into(), + )?; + + let trusted_consensus_state = TrustedConsensusState::new( + wasm_consensus_state.data, + trusted_sync_committee.sync_committee, + aggregate_public_key, + trusted_sync_committee.is_next, + )?; + + let wasm_client_state = read_client_state(deps)?; + let ctx = LightClientContext::new(&wasm_client_state.data, trusted_consensus_state); + + validate_light_client_update::, VerificationContext>( + &ctx, + header.consensus_update.clone(), + (header.timestamp - wasm_client_state.data.genesis_time) + / wasm_client_state.data.seconds_per_slot + + wasm_client_state.data.fork_parameters.genesis_slot, + wasm_client_state.data.genesis_validators_root.clone(), + VerificationContext { deps }, + ) + .map_err(|e| Error::Verification(e.to_string()))?; + + let proof_data = header + .account_update + .proofs + .get(0) + .ok_or(Error::EmptyProof)?; + + verify_account_storage_root( + header.consensus_update.attested_header.execution.state_root, + &proof_data + .key + .as_slice() + .try_into() + .map_err(|_| Error::InvalidProofFormat)?, + &proof_data.proof, + proof_data + .value + .as_slice() + .try_into() + .map_err(|_| Error::InvalidProofFormat)?, + ) + .map_err(|e| Error::Verification(e.to_string()))?; + + Ok(ContractResult::valid(None)) + } + + fn verify_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn update_state( + mut deps: DepsMut, + _env: Env, + header: Self::Header, + ) -> Result { + let trusted_sync_committee = header.trusted_sync_committee; + let trusted_height = trusted_sync_committee.trusted_height; + + let mut consensus_state: WasmConsensusState = + read_consensus_state(deps.as_ref(), &trusted_sync_committee.trusted_height)?.ok_or( + Error::ConsensusStateNotFound( + trusted_sync_committee.trusted_height.revision_number, + trusted_sync_committee.trusted_height.revision_height, + ), + )?; + + let mut client_state: WasmClientState = read_client_state(deps.as_ref())?; + let consensus_update = header.consensus_update; + let account_update = header.account_update; + + let store_period = + compute_sync_committee_period_at_slot::(consensus_state.data.slot); + let update_finalized_period = compute_sync_committee_period_at_slot::( + consensus_update.attested_header.beacon.slot, + ); + + match consensus_state.data.next_sync_committee { + None if update_finalized_period != store_period => { + return Err(Error::StorePeriodMustBeEqualToFinalizedPeriod) + } + None => { + consensus_state.data.next_sync_committee = consensus_update + .next_sync_committee + .map(|c| c.aggregate_pubkey); + } + Some(ref next_sync_committee) if update_finalized_period == store_period + 1 => { + consensus_state.data.current_sync_committee = next_sync_committee.clone(); + consensus_state.data.next_sync_committee = consensus_update + .next_sync_committee + .map(|c| c.aggregate_pubkey); + } + _ => {} + } + + if consensus_update.attested_header.beacon.slot > consensus_state.data.slot { + consensus_state.data.slot = consensus_update.attested_header.beacon.slot; + // NOTE(aeryz): we don't use `optimistic_header` + } + + // We implemented the spec until this point. We apply our updates now. + let storage_root = account_update + .proofs + .get(0) + .ok_or(Error::EmptyProof)? + .value + .as_slice() + .try_into() + .map_err(|_| Error::InvalidProofFormat)?; + consensus_state.data.storage_root = storage_root; + + consensus_state.timestamp = compute_timestamp_at_slot::( + client_state.data.genesis_time, + consensus_update.finalized_header.beacon.slot, + ); + + if client_state.data.latest_slot < consensus_update.attested_header.beacon.slot { + client_state.data.latest_slot = consensus_update.attested_header.beacon.slot; + } + + // Some updates can be only for updating the sync committee, therefore the execution number can be + // smaller. We don't want to save a new state if this is the case. + let updated_execution_height = core::cmp::max( + trusted_height.revision_height, + consensus_update.attested_header.execution.block_number, + ); + + update_client_state( + deps.branch(), + client_state, + // wasm_client_state.data, + updated_execution_height, + ); + save_consensus_state( + deps, + consensus_state, + // wasm_consensus_state.data, + &Height { + revision_number: trusted_height.revision_number, + revision_height: updated_execution_height, + }, + ); + + Ok(ContractResult::valid(None)) + } + + fn update_state_on_misbehaviour( + _deps: DepsMut, + _client_message: ics008_wasm_client::ClientMessage, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + fn check_for_misbehaviour_on_header( + _deps: Deps, + _header: Self::Header, + ) -> Result { + // TODO(aeryz): Leaving this as success for us to be able to update the client. See: #588. + Ok(ContractResult::valid(None)) + } + + fn check_for_misbehaviour_on_misbehaviour( + _deps: Deps, + _misbehaviour: Self::Misbehaviour, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn verify_upgrade_and_update_state( + _deps: DepsMut, + _upgrade_client_state: WasmClientState, + _upgrade_consensus_state: WasmConsensusState, + _proof_upgrade_client: Binary, + _proof_upgrade_consensus_state: Binary, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn check_substitute_and_update_state( + _deps: Deps, + ) -> Result { + Ok(ContractResult::invalid("Not implemented".to_string())) + } + + fn status(deps: Deps, env: &Env) -> Result { + let client_state: WasmClientState = read_client_state(deps)?; + + if client_state.data.frozen_height.is_some() { + return Ok(Status::Frozen.into()); + } + + let Some(consensus_state) = + read_consensus_state::(deps, &client_state.latest_height)? + else { + return Ok(Status::Expired.into()); + }; + + if is_client_expired( + consensus_state.timestamp, + client_state.data.trusting_period, + env.block.time.seconds(), + ) { + return Ok(Status::Expired.into()); + } + + Ok(Status::Active.into()) + } + + fn export_metadata( + _deps: Deps, + _env: &Env, + ) -> Result { + Ok(QueryResponse { + status: String::new(), + genesis_metadata: vec![], + }) + } +} + +fn do_verify_membership( + path: Path, + storage_root: H256, + counterparty_commitment_slot: Slot, + storage_proof: Proof, + value: Vec, +) -> Result<(), Error> { + check_commitment_key(path, counterparty_commitment_slot, &storage_proof.key)?; + + // We store the hash of the data, not the data itself to the commitments map. + let expected_value_hash = sha3::Keccak256::new().chain_update(value).finalize(); + + let expected_value = ethabi_U256::from_big_endian(&expected_value_hash); + + let proof_value = ethabi_U256::from_big_endian(storage_proof.value.as_slice()); + + if expected_value != proof_value { + return Err(Error::stored_value_mismatch( + expected_value_hash, + storage_proof.value.as_slice(), + )); + } + + verify_storage_proof( + storage_root, + &storage_proof.key, + &rlp::encode(&storage_proof.value.as_slice()), + &storage_proof.proof, + ) + .map_err(|e| Error::Verification(e.to_string())) +} + +/// Verifies that no value is committed at `path` in the counterparty light client's storage. +fn do_verify_non_membership( + path: Path, + storage_root: H256, + counterparty_commitment_slot: Slot, + storage_proof: Proof, +) -> Result<(), Error> { + check_commitment_key(path, counterparty_commitment_slot, &storage_proof.key)?; + + if verify_storage_absence(storage_root, &storage_proof.key, &storage_proof.proof) + .map_err(|e| Error::Verification(e.to_string()))? + { + Ok(()) + } else { + Err(Error::CounterpartyStorageNotNil) + } +} + +fn check_commitment_key( + path: Path, + counterparty_commitment_slot: Slot, + key: &[u8], +) -> Result<(), Error> { + let expected_commitment_key = + generate_commitment_key(path.to_string(), counterparty_commitment_slot); + + // Data MUST be stored to the commitment path that is defined in ICS23. + if expected_commitment_key != key { + Err(Error::invalid_commitment_key(expected_commitment_key, key)) + } else { + Ok(()) + } +} + +fn is_client_expired( + consensus_state_timestamp: u64, + trusting_period: u64, + current_block_time: u64, +) -> bool { + consensus_state_timestamp + trusting_period < current_block_time +} + +#[cfg(test)] +mod test { + use std::marker::PhantomData; + + use cosmwasm_std::{ + testing::{mock_env, MockApi, MockQuerier, MockQuerierCustomHandlerResult, MockStorage}, + OwnedDeps, SystemResult, Timestamp, + }; + use ethereum_verifier::crypto::{eth_aggregate_public_keys, fast_aggregate_verify}; + use hex_literal::hex; + use ibc::{ + core::{ + ics02_client::client_type::ClientType, + ics24_host::{ + identifier::{ClientId, ConnectionId}, + path::{ClientConsensusStatePath, ClientStatePath, ConnectionPath}, + }, + }, + Height as IbcHeight, + }; + use ics008_wasm_client::storage_utils::save_client_state; + use prost::Message; + use unionlabs::{ + bls::BlsPublicKey, + ethereum_consts_traits::Minimal, + ibc::{ + core::commitment::merkle_root::MerkleRoot, + google::protobuf::duration::Duration, + lightclients::{cometbls, ethereum, tendermint::fraction::Fraction}, + }, + IntoProto, + }; + + use super::*; + + /// These values are obtained by uploading a dummy contract with the necessary types to the devnet and + /// reading the values by `eth_getProof` RPC call. + const CLIENT_STATE_PROOF_KEY: &[u8] = + &hex!("b35cad2b263a62faaae30d8b3f51201fea5501d2df17d59a3eef2751403e684f"); + const CLIENT_STATE_PROOF_VALUE: &[u8] = + &hex!("272c7c82ac0f0adbfe4ae30614165bf3b94d49754ce8c1955cc255dcc829b5"); + const CLIENT_STATE_PROOF: [&[u8]; 2] = [ + &hex!("f871808080a0b9f6e8d11cf768b8034f04b8b2ab45bb5ca792e1c6e3929cf8222a885631ffac808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0d1797d071b81705da736e39e75f1186c8e529ba339f7a7d12a9b4fafe33e43cc80"), + &hex!("f842a03a8c7f353aebdcd6b56a67cd1b5829681a3c6e1695282161ab3faa6c3666d4c3a09f272c7c82ac0f0adbfe4ae30614165bf3b94d49754ce8c1955cc255dcc829b5") + ]; + /// Storage root of the contract at the time that this proof is obtained. + const CLIENT_STATE_STORAGE_ROOT: H256 = H256(hex!( + "5634f342b966b609cdd8d2f7ed43bb94702c9e83d4e974b08a3c2b8205fd85e3" + )); + const CLIENT_STATE_WASM_CODE_ID: &[u8] = + &hex!("B41F9EE164A6520C269F8928A1F3264A6F983F27478CB3A2251B77A65E0CEFBF"); + + const CONSENSUS_STATE_PROOF_KEY: &[u8] = + &hex!("9f22934f38bf5512b9c33ed55f71525c5d129895aad5585a2624f6c756c1c101"); + const CONSENSUS_STATE_PROOF_VALUE: &[u8] = + &hex!("504adb89d4e609110eebf79183a10b9a4788a797d973c0ba0504e7a97fc1daa6"); + const CONSENSUS_STATE_PROOF: [&[u8]; 2] = [ + &hex!("f871808080a0b9f6e8d11cf768b8034f04b8b2ab45bb5ca792e1c6e3929cf8222a885631ffac808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0d1797d071b81705da736e39e75f1186c8e529ba339f7a7d12a9b4fafe33e43cc80"), + &hex!("f843a036210c27d08bc29676360b820acc6de648bb730808a3a7d36a960f6869ac4a3aa1a0504adb89d4e609110eebf79183a10b9a4788a797d973c0ba0504e7a97fc1daa6") + ]; + /// Storage root of the contract at the time that this proof is obtained. + const CONSENSUS_STATE_STORAGE_ROOT: H256 = H256(hex!( + "5634f342b966b609cdd8d2f7ed43bb94702c9e83d4e974b08a3c2b8205fd85e3" + )); + const CONSENSUS_STATE_CONTRACT_MERKLE_ROOT: H256 = H256(hex!( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + )); + const CONSENSUS_STATE_NEXT_VALIDATORS_HASH: H256 = H256(hex!( + "B41F9EE164A6520C269F8928A1F3264A6F983F27478CB3A2251B77A65E0CEFBF" + )); + + const CONNECTION_END_PROOF_KEY: &[u8] = + &hex!("8e80b902df24e0c324c454fcd01ae0c92966a3f6fe4d1809e7fb75043b6549db"); + const CONNECTION_END_PROOF_VALUE: &[u8] = + &hex!("9ac95d1087518963f797142524b3c6c273bb74297c076c00b02ed129bcb4cfc0"); + const CONNECTION_END_PROOF: [&[u8]; 2] = [ + &hex!("f871808080a01c44ba4a3ade71a6b527cb53c3f2dd91606f91cd119fd74e85208b1d13096739808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0771904c17414dbc0741f3d1fce0d2709d4f73418020b9b4961e4cb3ec6f46ac280"), + &hex!("f843a0320fddcfabb459601044296253eed7d7cb53d9a8a3e46b1f7db5115be261c419a1a09ac95d1087518963f797142524b3c6c273bb74297c076c00b02ed129bcb4cfc0") + ]; + /// Storage root of the contract at the time that this proof is obtained. + const CONNECTION_END_STORAGE_ROOT: H256 = H256(hex!( + "78c3bf305b31e5f903d623b0b0023bfa764208429d3ecc0f8e61df44b643981d" + )); + + const NON_MEMBERSHIP_STORAGE_ROOT: H256 = H256(hex!( + "9e352a10c5a38c301ee06c22a90f0971b679985b2ca6dd66aca224bd7a9957c1" + )); + const NON_MEMBERSHIP_PROOF_KEY: &[u8] = + &hex!("b35cad2b263a62faaae30d8b3f51201fea5501d2df17d59a3eef2751403e684f"); + const NON_MEMBERSHIP_PROOF: [&[u8]; 1] = [ + &hex!("f838a120df6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c79594be68fc2d8249eb60bfcf0e71d5a0d2f2e292c4ed"), + ]; + + const WASM_CLIENT_ID_PREFIX: &str = "08-wasm"; + const ETHEREUM_CLIENT_ID_PREFIX: &str = "10-ethereum"; + const IBC_KEY_PREFIX: &str = "ibc"; + const INITIAL_CONSENSUS_STATE_HEIGHT: Height = Height { + revision_number: 0, + revision_height: 1328, + }; + + #[test] + fn query_status_returns_active() { + let mut deps = OwnedDeps::<_, _, _, CustomQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), + custom_query_type: PhantomData, + }; + + let wasm_client_state = + serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); + + let wasm_consensus_state = + serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); + + save_client_state( + deps.as_mut(), + ::try_from_proto(wasm_client_state).unwrap(), + ); + + save_consensus_state( + deps.as_mut(), + ::try_from_proto(wasm_consensus_state).unwrap(), + &INITIAL_CONSENSUS_STATE_HEIGHT, + ); + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(0); + + assert_eq!( + EthereumLightClient::status(deps.as_ref(), &env), + Ok(Status::Active.into()) + ); + } + + #[test] + fn query_status_returns_frozen() { + let mut deps = OwnedDeps::<_, _, _, CustomQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), + custom_query_type: PhantomData, + }; + + let mut wasm_client_state = ::try_from_proto( + serde_json::from_str(include_str!("./test/client_state.json")).unwrap(), + ) + .unwrap(); + + wasm_client_state.data.frozen_height = Some(Height { + revision_number: 1, + revision_height: 1, + }); + + save_client_state(deps.as_mut(), wasm_client_state); + + assert_eq!( + EthereumLightClient::status(deps.as_ref(), &mock_env()), + Ok(Status::Frozen.into()) + ); + } + + #[test] + fn query_status_returns_expired() { + let mut deps = OwnedDeps::<_, _, _, CustomQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), + custom_query_type: PhantomData, + }; + + let mut wasm_client_state = ::try_from_proto( + serde_json::from_str(include_str!("./test/client_state.json")).unwrap(), + ) + .unwrap(); + + save_client_state(deps.as_mut(), wasm_client_state.clone()); + + // Client returns expired here because it cannot find the consensus state + assert_eq!( + EthereumLightClient::status(deps.as_ref(), &mock_env()), + Ok(Status::Expired.into()) + ); + + let wasm_consensus_state = ::try_from_proto( + serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(), + ) + .unwrap(); + + save_consensus_state( + deps.as_mut(), + wasm_consensus_state.clone(), + &INITIAL_CONSENSUS_STATE_HEIGHT, + ); + + wasm_client_state.data.trusting_period = 10; + save_client_state(deps.as_mut(), wasm_client_state.clone()); + let mut env = mock_env(); + + env.block.time = Timestamp::from_seconds( + wasm_client_state.data.trusting_period + wasm_consensus_state.timestamp + 1, + ); + assert_eq!( + EthereumLightClient::status(deps.as_ref(), &env), + Ok(Status::Expired.into()) + ); + + env.block.time = Timestamp::from_seconds( + wasm_client_state.data.trusting_period + wasm_consensus_state.timestamp, + ); + assert_eq!( + EthereumLightClient::status(deps.as_ref(), &env), + Ok(Status::Active.into()) + ) + } + + #[test] + fn verify_and_update_header_works_with_good_data() { + let mut deps = OwnedDeps::<_, _, _, CustomQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), + custom_query_type: PhantomData, + }; + + let wasm_client_state = + serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); + + let wasm_consensus_state = + serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); + + save_client_state( + deps.as_mut(), + ::try_from_proto(wasm_client_state).unwrap(), + ); + save_consensus_state( + deps.as_mut(), + ::try_from_proto(wasm_consensus_state).unwrap(), + &INITIAL_CONSENSUS_STATE_HEIGHT, + ); + + let updates = &[ + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/sync_committee_update_1.json")).unwrap(), + ) + .unwrap(), + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/finality_update_1.json")).unwrap(), + ) + .unwrap(), + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/sync_committee_update_2.json")).unwrap(), + ) + .unwrap(), + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/finality_update_2.json")).unwrap(), + ) + .unwrap(), + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/finality_update_3.json")).unwrap(), + ) + .unwrap(), + ethereum::header::Header::::try_from_proto( + serde_json::from_str(include_str!("./test/finality_update_4.json")).unwrap(), + ) + .unwrap(), + ]; + + for update in updates { + EthereumLightClient::verify_header(deps.as_ref(), mock_env(), update.clone()).unwrap(); + EthereumLightClient::update_state(deps.as_mut(), mock_env(), update.clone()).unwrap(); + // Consensus state is saved to the updated height. + if update.consensus_update.attested_header.beacon.slot + > update.trusted_sync_committee.trusted_height.revision_height + { + // It's a finality update + let wasm_consensus_state: WasmConsensusState = read_consensus_state( + deps.as_ref(), + &Height { + revision_number: 0, + revision_height: update.consensus_update.attested_header.beacon.slot, + }, + ) + .unwrap() + .unwrap(); + // Slot is updated. + assert_eq!( + wasm_consensus_state.data.slot, + update.consensus_update.attested_header.beacon.slot + ); + // Storage root is updated. + assert_eq!( + wasm_consensus_state.data.storage_root.into_bytes(), + update.account_update.proofs[0].value, + ); + // Latest slot is updated. + // TODO(aeryz): Add cases for `store_period == update_period` and `update_period == store_period + 1` + let wasm_client_state: WasmClientState = read_client_state(deps.as_ref()).unwrap(); + assert_eq!( + wasm_client_state.data.latest_slot, + update.consensus_update.attested_header.beacon.slot + ); + } else { + // It's a sync committee update + let updated_height = core::cmp::max( + update.trusted_sync_committee.trusted_height.revision_height, + update.consensus_update.attested_header.beacon.slot, + ); + let wasm_consensus_state: WasmConsensusState = read_consensus_state( + deps.as_ref(), + &Height { + revision_number: 0, + revision_height: updated_height, + }, + ) + .unwrap() + .unwrap(); + + assert_eq!( + wasm_consensus_state.data.next_sync_committee.unwrap(), + update + .consensus_update + .next_sync_committee + .clone() + .unwrap() + .aggregate_pubkey + ); + } + } + } + + fn custom_query_handler(query: &CustomQuery) -> MockQuerierCustomHandlerResult { + match query { + CustomQuery::AggregateVerify { + public_keys, + message, + signature, + } => { + let pubkeys: Vec = public_keys + .iter() + .map(|pk| pk.0.clone().try_into().unwrap()) + .collect(); + + let res = fast_aggregate_verify( + pubkeys.iter().collect::>().as_slice(), + message.as_ref(), + &signature.0.clone().try_into().unwrap(), + ); + + SystemResult::Ok(cosmwasm_std::ContractResult::Ok::( + serde_json::to_vec(&res.is_ok()).unwrap().into(), + )) + } + CustomQuery::Aggregate { public_keys } => { + let pubkey = eth_aggregate_public_keys( + public_keys + .iter() + .map(|pk| pk.as_ref().try_into().unwrap()) + .collect::>() + .as_slice(), + ) + .unwrap(); + + SystemResult::Ok(cosmwasm_std::ContractResult::Ok::( + serde_json::to_vec(&Binary(pubkey.into())).unwrap().into(), + )) + } + } + } + + fn prepare_for_fail_tests() -> ( + OwnedDeps, CustomQuery>, + ethereum::header::Header, + ) { + let mut deps = OwnedDeps::<_, _, _, CustomQuery> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), + custom_query_type: PhantomData, + }; + + let wasm_client_state: protos::ibc::lightclients::wasm::v1::ClientState = + serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); + + let wasm_consensus_state: protos::ibc::lightclients::wasm::v1::ConsensusState = + serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); + + save_client_state( + deps.as_mut(), + ::try_from_proto(wasm_client_state).unwrap(), + ); + save_consensus_state( + deps.as_mut(), + ::try_from_proto(wasm_consensus_state).unwrap(), + &INITIAL_CONSENSUS_STATE_HEIGHT, + ); + + let update = + serde_json::from_str::( + include_str!("./test/sync_committee_update_1.json"), + ) + .unwrap(); + + (deps, update.try_into().unwrap()) + } + + #[test] + fn verify_header_fails_when_sync_committee_aggregate_pubkey_is_incorrect() { + let (deps, mut update) = prepare_for_fail_tests(); + + let mut pubkey = update + .trusted_sync_committee + .sync_committee + .aggregate_pubkey + .clone(); + pubkey.0[0] += 1; + update + .trusted_sync_committee + .sync_committee + .aggregate_pubkey = pubkey; + assert!(EthereumLightClient::verify_header(deps.as_ref(), mock_env(), update).is_err()); + } + + #[test] + fn verify_header_fails_when_finalized_header_execution_branch_merkle_is_invalid() { + let (deps, mut update) = prepare_for_fail_tests(); + update.consensus_update.finalized_header.execution_branch[0].0[0] += 1; + assert!(EthereumLightClient::verify_header(deps.as_ref(), mock_env(), update).is_err()); + } + + #[test] + fn verify_header_fails_when_finality_branch_merkle_is_invalid() { + let (deps, mut update) = prepare_for_fail_tests(); + update.consensus_update.finality_branch[0].0[0] += 1; + assert!(EthereumLightClient::verify_header(deps.as_ref(), mock_env(), update).is_err()); + } + + #[test] + fn membership_verification_works_for_client_state() { + let proof = Proof { + key: CLIENT_STATE_PROOF_KEY.into(), + value: CLIENT_STATE_PROOF_VALUE.into(), + proof: CLIENT_STATE_PROOF.into_iter().map(Into::into).collect(), + }; + + let storage_root = CLIENT_STATE_STORAGE_ROOT.clone(); + + let client_state = cometbls::client_state::ClientState { + chain_id: "ibc-0".to_string(), + trust_level: Fraction { + numerator: 1, + denominator: 3, + }, + trusting_period: Duration::new(1814400, 0).unwrap(), + unbonding_period: Duration::new(1814400, 0).unwrap(), + max_clock_drift: Duration::new(40, 0).unwrap(), + frozen_height: Height { + revision_number: 0, + revision_height: 0, + }, + }; + + let wasm_client_state = protos::ibc::lightclients::wasm::v1::ClientState { + data: client_state.into_proto_bytes(), + code_id: CLIENT_STATE_WASM_CODE_ID.into(), + latest_height: Some(protos::ibc::core::client::v1::Height { + revision_number: 0, + revision_height: 1, + }), + }; + + let any_client_state = protos::google::protobuf::Any { + type_url: "/ibc.lightclients.wasm.v1.ClientState".into(), + value: wasm_client_state.encode_to_vec(), + }; + + do_verify_membership( + ClientStatePath::new( + &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), + ) + .into(), + storage_root, + 3, + proof, + any_client_state.encode_to_vec(), + ) + .expect("Membership verification of client state failed"); + } + + #[test] + fn membership_verification_works_for_consensus_state() { + let proof = Proof { + key: CONSENSUS_STATE_PROOF_KEY.into(), + value: CONSENSUS_STATE_PROOF_VALUE.into(), + proof: CONSENSUS_STATE_PROOF.into_iter().map(Into::into).collect(), + }; + + let storage_root = CONSENSUS_STATE_STORAGE_ROOT.clone(); + + let consensus_state = cometbls::consensus_state::ConsensusState { + root: MerkleRoot { + hash: CONSENSUS_STATE_CONTRACT_MERKLE_ROOT.clone(), + }, + next_validators_hash: CONSENSUS_STATE_NEXT_VALIDATORS_HASH.clone(), + }; + + let wasm_consensus_state = protos::ibc::lightclients::wasm::v1::ConsensusState { + data: consensus_state.into_proto_bytes(), + timestamp: 1684400046, + }; + + let any_consensus_state = protos::google::protobuf::Any { + type_url: "/ibc.lightclients.wasm.v1.ConsensusState".into(), + value: wasm_consensus_state.encode_to_vec(), + }; + + do_verify_membership( + ClientConsensusStatePath::new( + &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), + &IbcHeight::new(0, 1).unwrap(), + ) + .into(), + storage_root, + 3, + proof, + any_consensus_state.encode_to_vec(), + ) + .expect("Membership verification of consensus state failed"); + } + + fn prepare_connection_end() -> ( + Proof, + H256, + protos::ibc::core::connection::v1::ConnectionEnd, + ) { + let proof = Proof { + key: CONNECTION_END_PROOF_KEY.into(), + value: CONNECTION_END_PROOF_VALUE.into(), + proof: CONNECTION_END_PROOF.into_iter().map(Into::into).collect(), + }; + + let storage_root = CONNECTION_END_STORAGE_ROOT.clone(); + + let connection_end = protos::ibc::core::connection::v1::ConnectionEnd { + client_id: format!("{ETHEREUM_CLIENT_ID_PREFIX}-0"), + versions: vec![protos::ibc::core::connection::v1::Version { + identifier: "1".into(), + features: vec!["ORDER_ORDERED".into(), "ORDER_UNORDERED".into()], + }], + state: 1, + counterparty: Some(protos::ibc::core::connection::v1::Counterparty { + client_id: format!("{WASM_CLIENT_ID_PREFIX}-0"), + connection_id: Default::default(), + prefix: Some(protos::ibc::core::commitment::v1::MerklePrefix { + key_prefix: IBC_KEY_PREFIX.as_bytes().to_vec(), + }), + }), + delay_period: 0, + }; + + (proof, storage_root, connection_end) + } + + #[test] + fn membership_verification_works_for_connection_end() { + let (proof, storage_root, connection_end) = prepare_connection_end(); + + do_verify_membership( + ConnectionPath::new(&ConnectionId::new(0)).into(), + storage_root, + 3, + proof, + connection_end.encode_to_vec(), + ) + .expect("Membership verification of connection end failed"); + } + + #[test] + fn membership_verification_fails_for_incorrect_proofs() { + let (mut proof, storage_root, connection_end) = prepare_connection_end(); + + let proofs = vec![ + { + let mut proof = proof.clone(); + proof.value[10] = u8::MAX - proof.value[10]; // Makes sure that produced value is always valid and different + proof + }, + { + let mut proof = proof.clone(); + proof.key[5] = u8::MAX - proof.key[5]; + proof + }, + { + proof.proof[0][10] = u8::MAX - proof.proof[0][10]; + proof + }, + ]; + + for proof in proofs { + assert!(do_verify_membership( + ConnectionPath::new(&ConnectionId::new(0)).into(), + storage_root.clone(), + 3, + proof, + connection_end.encode_to_vec(), + ) + .is_err()); + } + } + + #[test] + fn membership_verification_fails_for_incorrect_storage_root() { + let (proof, mut storage_root, connection_end) = prepare_connection_end(); + + storage_root.0[10] = u8::MAX - storage_root.0[10]; + + assert!(do_verify_membership( + ConnectionPath::new(&ConnectionId::new(0)).into(), + storage_root, + 3, + proof, + connection_end.encode_to_vec(), + ) + .is_err()); + } + + #[test] + fn membership_verification_fails_for_incorrect_data() { + let (proof, storage_root, mut connection_end) = prepare_connection_end(); + + connection_end.client_id = "incorrect-client-id".into(); + + assert!(do_verify_membership( + ConnectionPath::new(&ConnectionId::new(0)).into(), + storage_root, + 3, + proof, + connection_end.encode_to_vec(), + ) + .is_err()); + } + + #[test] + fn non_membership_verification_works() { + let proof = Proof { + key: NON_MEMBERSHIP_PROOF_KEY.into(), + value: vec![0x0], + proof: NON_MEMBERSHIP_PROOF.into_iter().map(Into::into).collect(), + }; + + let storage_root = NON_MEMBERSHIP_STORAGE_ROOT.clone(); + + do_verify_non_membership( + ClientStatePath::new( + &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), + ) + .into(), + storage_root, + 3, + proof, + ) + .expect("Membership verification of client state failed"); + } + + #[test] + fn non_membership_verification_fails_when_value_not_empty() { + let (proof, storage_root, _) = prepare_connection_end(); + + assert_eq!( + do_verify_non_membership( + ConnectionPath::new(&ConnectionId::new(0)).into(), + storage_root, + 3, + proof, + ), + Err(Error::CounterpartyStorageNotNil) + ); + } +} diff --git a/light-clients/ethereum-light-client/src/contract.rs b/light-clients/ethereum-light-client/src/contract.rs index 7fd6eb8525..a9c98ffc41 100644 --- a/light-clients/ethereum-light-client/src/contract.rs +++ b/light-clients/ethereum-light-client/src/contract.rs @@ -1,40 +1,13 @@ -use std::str::FromStr; - use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - StdError, -}; -use ethabi::ethereum_types::U256 as ethabi_U256; -use ethereum_verifier::{ - compute_sync_committee_period_at_slot, compute_timestamp_at_slot, primitives::Slot, - validate_light_client_update, verify_account_storage_root, verify_storage_absence, - verify_storage_proof, -}; -use ibc::core::ics24_host::Path; -use sha3::Digest; -use unionlabs::{ - ethereum::H256, - ethereum_consts_traits::ChainSpec, - ibc::{ - core::client::height::Height, - lightclients::ethereum::{header::Header, proof::Proof, storage_proof::StorageProof}, - }, - TryFromProto, -}; -use wasm_light_client_types::msg::{ - ClientMessage, ContractResult, MerklePath, Status, StatusResponse, + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; +use ics008_wasm_client::{ExecuteMsg, IbcClient, QueryMsg}; +use serde::{Deserialize, Serialize}; -use crate::{ - consensus_state::TrustedConsensusState, - context::LightClientContext, - custom_query::{query_aggregate_public_keys, CustomQuery, VerificationContext}, - errors::Error, - eth_encoding::generate_commitment_key, - msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StorageState}, - state::{read_client_state, read_consensus_state, save_consensus_state, update_client_state}, - Config, -}; +use crate::{client::EthereumLightClient, custom_query::CustomQuery, errors::Error}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct InstantiateMsg {} #[entry_point] pub fn instantiate( @@ -49,1060 +22,17 @@ pub fn instantiate( #[entry_point] pub fn execute( deps: DepsMut, - _env: Env, - _info: MessageInfo, + env: Env, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { - let result = match msg { - ExecuteMsg::VerifyMembership { - height, - delay_time_period, - delay_block_period, - proof, - path, - value, - } => verify_membership( - deps.as_ref(), - height, - delay_time_period, - delay_block_period, - proof, - path, - StorageState::Occupied(value.0), - ), - ExecuteMsg::VerifyNonMembership { - height, - delay_time_period, - delay_block_period, - proof, - path, - } => verify_membership( - deps.as_ref(), - height, - delay_time_period, - delay_block_period, - proof, - path, - StorageState::Empty, - ), - ExecuteMsg::VerifyClientMessage { - client_message: ClientMessage { header, .. }, - } => { - if let Some(header) = header { - let header = - Header::::try_from_proto_bytes(&header.data).map_err(|err| { - Error::decode(format!( - "when converting proto header to header in update: {err:#?}" - )) - })?; - verify_header(deps.as_ref(), header) - } else { - Err(StdError::not_found("Not implemented").into()) - } - } - ExecuteMsg::UpdateState { - client_message: ClientMessage { header, .. }, - } => { - if let Some(header) = header { - let header = - Header::::try_from_proto_bytes(&header.data).map_err(|err| { - Error::decode(format!( - "when converting proto header to header in update: {err:#?}" - )) - })?; - update_header(deps, header) - } else { - Err(StdError::not_found("Not implemented").into()) - } - } - _ => Ok(ContractResult::valid(None)), - }?; - + let result = EthereumLightClient::execute(deps, env, info, msg)?; Ok(Response::default().set_data(result.encode()?)) } -/// Verifies if the `value` is committed at `path` in the counterparty light client. -pub fn verify_membership( - deps: Deps, - height: Height, - _delay_time_period: u64, - _delay_block_period: u64, - proof: Binary, - path: MerklePath, - value: StorageState, -) -> Result { - let consensus_state = read_consensus_state(deps, &height)?.ok_or( - Error::ConsensusStateNotFound(height.revision_number, height.revision_height), - )?; - let client_state = read_client_state(deps)?; - - let path = Path::from_str( - path.key_path - .last() - .ok_or(Error::InvalidPath("path is empty".into()))?, - ) - .map_err(|e| Error::InvalidPath(e.to_string()))?; - - // This storage root is verified during the header update, so we don't need to verify it again. - let storage_root = consensus_state.data.storage_root; - - let storage_proof = { - let mut proofs = StorageProof::try_from_proto_bytes(&proof.0) - .map_err(|e| Error::decode(format!("when decoding storage proof: {e:#?}")))? - .proofs; - if proofs.len() > 1 { - return Err(Error::BatchingProofsNotSupported); - } - proofs.pop().ok_or(Error::EmptyProof)? - }; - - match value { - StorageState::Occupied(value) => do_verify_membership( - path, - storage_root, - client_state.data.counterparty_commitment_slot, - storage_proof, - value, - )?, - StorageState::Empty => do_verify_non_membership( - path, - storage_root, - client_state.data.counterparty_commitment_slot, - storage_proof, - )?, - } - - Ok(ContractResult::valid(None)) -} - -pub fn do_verify_membership( - path: Path, - storage_root: H256, - counterparty_commitment_slot: Slot, - storage_proof: Proof, - value: Vec, -) -> Result<(), Error> { - check_commitment_key(path, counterparty_commitment_slot, &storage_proof.key)?; - - // We store the hash of the data, not the data itself to the commitments map. - let expected_value_hash = sha3::Keccak256::new().chain_update(value).finalize(); - - let expected_value = ethabi_U256::from_big_endian(&expected_value_hash); - - let proof_value = ethabi_U256::from_big_endian(storage_proof.value.as_slice()); - - if expected_value != proof_value { - return Err(Error::stored_value_mismatch( - expected_value_hash, - storage_proof.value.as_slice(), - )); - } - - verify_storage_proof( - storage_root, - &storage_proof.key, - &rlp::encode(&storage_proof.value.as_slice()), - &storage_proof.proof, - ) - .map_err(|e| Error::Verification(e.to_string())) -} - -/// Verifies that no value is committed at `path` in the counterparty light client's storage. -pub fn do_verify_non_membership( - path: Path, - storage_root: H256, - counterparty_commitment_slot: Slot, - storage_proof: Proof, -) -> Result<(), Error> { - check_commitment_key(path, counterparty_commitment_slot, &storage_proof.key)?; - - if verify_storage_absence(storage_root, &storage_proof.key, &storage_proof.proof) - .map_err(|e| Error::Verification(e.to_string()))? - { - Ok(()) - } else { - Err(Error::CounterpartyStorageNotNil) - } -} - -fn check_commitment_key( - path: Path, - counterparty_commitment_slot: Slot, - key: &[u8], -) -> Result<(), Error> { - let expected_commitment_key = - generate_commitment_key(path.to_string(), counterparty_commitment_slot); - - // Data MUST be stored to the commitment path that is defined in ICS23. - if expected_commitment_key != key { - Err(Error::invalid_commitment_key(expected_commitment_key, key)) - } else { - Ok(()) - } -} - -pub fn update_header( - mut deps: DepsMut, - header: Header, -) -> Result { - let trusted_sync_committee = header.trusted_sync_committee; - let trusted_height = trusted_sync_committee.trusted_height; - - let mut consensus_state = - read_consensus_state(deps.as_ref(), &trusted_sync_committee.trusted_height)?.ok_or( - Error::ConsensusStateNotFound( - trusted_sync_committee.trusted_height.revision_number, - trusted_sync_committee.trusted_height.revision_height, - ), - )?; - - let mut client_state = read_client_state(deps.as_ref())?; - let consensus_update = header.consensus_update; - let account_update = header.account_update; - - let store_period = compute_sync_committee_period_at_slot::(consensus_state.data.slot); - let update_finalized_period = - compute_sync_committee_period_at_slot::(consensus_update.attested_header.beacon.slot); - - match consensus_state.data.next_sync_committee { - None if update_finalized_period != store_period => { - return Err(Error::StorePeriodMustBeEqualToFinalizedPeriod) - } - None => { - consensus_state.data.next_sync_committee = consensus_update - .next_sync_committee - .map(|c| c.aggregate_pubkey); - } - Some(ref next_sync_committee) if update_finalized_period == store_period + 1 => { - consensus_state.data.current_sync_committee = next_sync_committee.clone(); - consensus_state.data.next_sync_committee = consensus_update - .next_sync_committee - .map(|c| c.aggregate_pubkey); - } - _ => {} - } - - if consensus_update.attested_header.beacon.slot > consensus_state.data.slot { - consensus_state.data.slot = consensus_update.attested_header.beacon.slot; - // NOTE(aeryz): we don't use `optimistic_header` - } - - // We implemented the spec until this point. We apply our updates now. - let storage_root = account_update - .proofs - .get(0) - .ok_or(Error::EmptyProof)? - .value - .as_slice() - .try_into() - .map_err(|_| Error::InvalidProofFormat)?; - consensus_state.data.storage_root = storage_root; - - consensus_state.timestamp = compute_timestamp_at_slot::( - client_state.data.genesis_time, - consensus_update.finalized_header.beacon.slot, - ); - - if client_state.data.latest_slot < consensus_update.attested_header.beacon.slot { - client_state.data.latest_slot = consensus_update.attested_header.beacon.slot; - } - - // Some updates can be only for updating the sync committee, therefore the execution number can be - // smaller. We don't want to save a new state if this is the case. - let updated_execution_height = core::cmp::max( - trusted_height.revision_height, - consensus_update.attested_header.execution.block_number, - ); - - update_client_state( - deps.branch(), - client_state, - // wasm_client_state.data, - updated_execution_height, - ); - save_consensus_state( - deps, - consensus_state, - // wasm_consensus_state.data, - updated_execution_height, - )?; - - Ok(ContractResult::valid(None)) -} - -pub fn verify_header( - deps: Deps, - header: Header, -) -> Result { - let trusted_sync_committee = header.trusted_sync_committee; - let wasm_consensus_state = read_consensus_state(deps, &trusted_sync_committee.trusted_height)? - .ok_or(Error::ConsensusStateNotFound( - trusted_sync_committee.trusted_height.revision_number, - trusted_sync_committee.trusted_height.revision_height, - ))?; - - let aggregate_public_key = query_aggregate_public_keys( - deps, - trusted_sync_committee.sync_committee.pubkeys.clone().into(), - )?; - - let trusted_consensus_state = TrustedConsensusState::new( - wasm_consensus_state.data, - trusted_sync_committee.sync_committee, - aggregate_public_key, - trusted_sync_committee.is_next, - )?; - - let wasm_client_state = read_client_state(deps)?; - let ctx = LightClientContext::new(&wasm_client_state.data, trusted_consensus_state); - - validate_light_client_update::, VerificationContext>( - &ctx, - header.consensus_update.clone(), - (header.timestamp - wasm_client_state.data.genesis_time) - / wasm_client_state.data.seconds_per_slot - + wasm_client_state.data.fork_parameters.genesis_slot, - wasm_client_state.data.genesis_validators_root.clone(), - VerificationContext { deps }, - ) - .map_err(|e| Error::Verification(e.to_string()))?; - - let proof_data = header - .account_update - .proofs - .get(0) - .ok_or(Error::EmptyProof)?; - - verify_account_storage_root( - header.consensus_update.attested_header.execution.state_root, - &proof_data - .key - .as_slice() - .try_into() - .map_err(|_| Error::InvalidProofFormat)?, - &proof_data.proof, - proof_data - .value - .as_slice() - .try_into() - .map_err(|_| Error::InvalidProofFormat)?, - ) - .map_err(|e| Error::Verification(e.to_string()))?; - - Ok(ContractResult::valid(None)) -} - #[entry_point] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - let response = match msg { - QueryMsg::Status {} => query_status(deps, &env)?, - }; + let response = EthereumLightClient::query(deps, env, msg)?; to_binary(&response).map_err(Into::into) } - -fn query_status(deps: Deps, env: &Env) -> Result { - let client_state = read_client_state(deps)?; - - if client_state.data.frozen_height.is_some() { - return Ok(Status::Frozen.into()); - } - - let Some(consensus_state) = read_consensus_state(deps, &client_state.latest_height)? else { - return Ok(Status::Expired.into()); - }; - - if is_client_expired( - consensus_state.timestamp, - client_state.data.trusting_period, - env.block.time.seconds(), - ) { - return Ok(Status::Expired.into()); - } - - Ok(Status::Active.into()) -} - -fn is_client_expired( - consensus_state_timestamp: u64, - trusting_period: u64, - current_block_time: u64, -) -> bool { - consensus_state_timestamp + trusting_period < current_block_time -} - -#[cfg(test)] -mod test { - use std::marker::PhantomData; - - use cosmwasm_std::{ - testing::{mock_env, MockApi, MockQuerier, MockQuerierCustomHandlerResult, MockStorage}, - OwnedDeps, SystemResult, Timestamp, - }; - use ethereum_verifier::crypto::{eth_aggregate_public_keys, fast_aggregate_verify}; - use hex_literal::hex; - use ibc::{ - core::{ - ics02_client::client_type::ClientType, - ics24_host::{ - identifier::{ClientId, ConnectionId}, - path::{ClientConsensusStatePath, ClientStatePath, ConnectionPath}, - }, - }, - Height as IbcHeight, - }; - use prost::Message; - use unionlabs::{ - bls::BlsPublicKey, - ethereum_consts_traits::Minimal, - ibc::{ - core::commitment::merkle_root::MerkleRoot, - google::protobuf::duration::Duration, - lightclients::{cometbls, ethereum, tendermint::fraction::Fraction, wasm}, - }, - IntoProto, - }; - - use super::*; - use crate::state::{save_wasm_client_state, save_wasm_consensus_state}; - - /// These values are obtained by uploading a dummy contract with the necessary types to the devnet and - /// reading the values by `eth_getProof` RPC call. - const CLIENT_STATE_PROOF_KEY: &[u8] = - &hex!("b35cad2b263a62faaae30d8b3f51201fea5501d2df17d59a3eef2751403e684f"); - const CLIENT_STATE_PROOF_VALUE: &[u8] = - &hex!("272c7c82ac0f0adbfe4ae30614165bf3b94d49754ce8c1955cc255dcc829b5"); - const CLIENT_STATE_PROOF: [&[u8]; 2] = [ - &hex!("f871808080a0b9f6e8d11cf768b8034f04b8b2ab45bb5ca792e1c6e3929cf8222a885631ffac808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0d1797d071b81705da736e39e75f1186c8e529ba339f7a7d12a9b4fafe33e43cc80"), - &hex!("f842a03a8c7f353aebdcd6b56a67cd1b5829681a3c6e1695282161ab3faa6c3666d4c3a09f272c7c82ac0f0adbfe4ae30614165bf3b94d49754ce8c1955cc255dcc829b5") - ]; - /// Storage root of the contract at the time that this proof is obtained. - const CLIENT_STATE_STORAGE_ROOT: H256 = H256(hex!( - "5634f342b966b609cdd8d2f7ed43bb94702c9e83d4e974b08a3c2b8205fd85e3" - )); - const CLIENT_STATE_WASM_CODE_ID: &[u8] = - &hex!("B41F9EE164A6520C269F8928A1F3264A6F983F27478CB3A2251B77A65E0CEFBF"); - - const CONSENSUS_STATE_PROOF_KEY: &[u8] = - &hex!("9f22934f38bf5512b9c33ed55f71525c5d129895aad5585a2624f6c756c1c101"); - const CONSENSUS_STATE_PROOF_VALUE: &[u8] = - &hex!("504adb89d4e609110eebf79183a10b9a4788a797d973c0ba0504e7a97fc1daa6"); - const CONSENSUS_STATE_PROOF: [&[u8]; 2] = [ - &hex!("f871808080a0b9f6e8d11cf768b8034f04b8b2ab45bb5ca792e1c6e3929cf8222a885631ffac808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0d1797d071b81705da736e39e75f1186c8e529ba339f7a7d12a9b4fafe33e43cc80"), - &hex!("f843a036210c27d08bc29676360b820acc6de648bb730808a3a7d36a960f6869ac4a3aa1a0504adb89d4e609110eebf79183a10b9a4788a797d973c0ba0504e7a97fc1daa6") - ]; - /// Storage root of the contract at the time that this proof is obtained. - const CONSENSUS_STATE_STORAGE_ROOT: H256 = H256(hex!( - "5634f342b966b609cdd8d2f7ed43bb94702c9e83d4e974b08a3c2b8205fd85e3" - )); - const CONSENSUS_STATE_CONTRACT_MERKLE_ROOT: H256 = H256(hex!( - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - )); - const CONSENSUS_STATE_NEXT_VALIDATORS_HASH: H256 = H256(hex!( - "B41F9EE164A6520C269F8928A1F3264A6F983F27478CB3A2251B77A65E0CEFBF" - )); - - const CONNECTION_END_PROOF_KEY: &[u8] = - &hex!("8e80b902df24e0c324c454fcd01ae0c92966a3f6fe4d1809e7fb75043b6549db"); - const CONNECTION_END_PROOF_VALUE: &[u8] = - &hex!("9ac95d1087518963f797142524b3c6c273bb74297c076c00b02ed129bcb4cfc0"); - const CONNECTION_END_PROOF: [&[u8]; 2] = [ - &hex!("f871808080a01c44ba4a3ade71a6b527cb53c3f2dd91606f91cd119fd74e85208b1d13096739808080808080808080a0f7202a06e8dc011d3123f907597f51546fe03542551af2c9c54d21ba0fbafc7280a0771904c17414dbc0741f3d1fce0d2709d4f73418020b9b4961e4cb3ec6f46ac280"), - &hex!("f843a0320fddcfabb459601044296253eed7d7cb53d9a8a3e46b1f7db5115be261c419a1a09ac95d1087518963f797142524b3c6c273bb74297c076c00b02ed129bcb4cfc0") - ]; - /// Storage root of the contract at the time that this proof is obtained. - const CONNECTION_END_STORAGE_ROOT: H256 = H256(hex!( - "78c3bf305b31e5f903d623b0b0023bfa764208429d3ecc0f8e61df44b643981d" - )); - - const NON_MEMBERSHIP_STORAGE_ROOT: H256 = H256(hex!( - "9e352a10c5a38c301ee06c22a90f0971b679985b2ca6dd66aca224bd7a9957c1" - )); - const NON_MEMBERSHIP_PROOF_KEY: &[u8] = - &hex!("b35cad2b263a62faaae30d8b3f51201fea5501d2df17d59a3eef2751403e684f"); - const NON_MEMBERSHIP_PROOF: [&[u8]; 1] = [ - &hex!("f838a120df6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c79594be68fc2d8249eb60bfcf0e71d5a0d2f2e292c4ed"), - ]; - - const WASM_CLIENT_ID_PREFIX: &str = "08-wasm"; - const ETHEREUM_CLIENT_ID_PREFIX: &str = "10-ethereum"; - const IBC_KEY_PREFIX: &str = "ibc"; - const INITIAL_CONSENSUS_STATE_HEIGHT: Height = Height { - revision_number: 0, - revision_height: 1328, - }; - - #[test] - fn query_status_returns_active() { - let mut deps = OwnedDeps::<_, _, _, CustomQuery> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), - custom_query_type: PhantomData, - }; - - let wasm_client_state = - serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); - - let wasm_consensus_state = - serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); - - save_wasm_client_state( - deps.as_mut(), - <_>::try_from_proto(wasm_client_state).unwrap(), - ); - - save_wasm_consensus_state( - deps.as_mut(), - <_>::try_from_proto(wasm_consensus_state).unwrap(), - &INITIAL_CONSENSUS_STATE_HEIGHT, - ); - - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(0); - - assert_eq!(query_status(deps.as_ref(), &env), Ok(Status::Active.into())); - } - - #[test] - fn query_status_returns_frozen() { - let mut deps = OwnedDeps::<_, _, _, CustomQuery> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), - custom_query_type: PhantomData, - }; - - let mut wasm_client_state = - >::try_from_proto( - serde_json::from_str(include_str!("./test/client_state.json")).unwrap(), - ) - .unwrap(); - - wasm_client_state.data.frozen_height = Some(Height { - revision_number: 1, - revision_height: 1, - }); - - save_wasm_client_state(deps.as_mut(), wasm_client_state); - - assert_eq!( - query_status(deps.as_ref(), &mock_env()), - Ok(Status::Frozen.into()) - ); - } - - #[test] - fn query_status_returns_expired() { - let mut deps = OwnedDeps::<_, _, _, CustomQuery> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), - custom_query_type: PhantomData, - }; - - let mut wasm_client_state = - >::try_from_proto( - serde_json::from_str(include_str!("./test/client_state.json")).unwrap(), - ) - .unwrap(); - - save_wasm_client_state(deps.as_mut(), wasm_client_state.clone()); - - // Client returns expired here because it cannot find the consensus state - assert_eq!( - query_status(deps.as_ref(), &mock_env()), - Ok(Status::Expired.into()) - ); - - let wasm_consensus_state = >::try_from_proto( - serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(), - ) - .unwrap(); - - save_wasm_consensus_state( - deps.as_mut(), - wasm_consensus_state.clone(), - &INITIAL_CONSENSUS_STATE_HEIGHT, - ); - - wasm_client_state.data.trusting_period = 10; - save_wasm_client_state(deps.as_mut(), wasm_client_state.clone()); - let mut env = mock_env(); - - env.block.time = Timestamp::from_seconds( - wasm_client_state.data.trusting_period + wasm_consensus_state.timestamp + 1, - ); - assert_eq!( - query_status(deps.as_ref(), &env), - Ok(Status::Expired.into()) - ); - - env.block.time = Timestamp::from_seconds( - wasm_client_state.data.trusting_period + wasm_consensus_state.timestamp, - ); - assert_eq!(query_status(deps.as_ref(), &env), Ok(Status::Active.into())) - } - - #[test] - fn verify_and_update_header_works_with_good_data() { - let mut deps = OwnedDeps::<_, _, _, CustomQuery> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), - custom_query_type: PhantomData, - }; - - let wasm_client_state = - serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); - - let wasm_consensus_state = - serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); - - save_wasm_client_state( - deps.as_mut(), - <_>::try_from_proto(wasm_client_state).unwrap(), - ); - save_wasm_consensus_state( - deps.as_mut(), - <_>::try_from_proto(wasm_consensus_state).unwrap(), - &INITIAL_CONSENSUS_STATE_HEIGHT, - ); - - let updates = &[ - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/sync_committee_update_1.json")).unwrap(), - ) - .unwrap(), - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/finality_update_1.json")).unwrap(), - ) - .unwrap(), - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/sync_committee_update_2.json")).unwrap(), - ) - .unwrap(), - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/finality_update_2.json")).unwrap(), - ) - .unwrap(), - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/finality_update_3.json")).unwrap(), - ) - .unwrap(), - ethereum::header::Header::::try_from_proto( - serde_json::from_str(include_str!("./test/finality_update_4.json")).unwrap(), - ) - .unwrap(), - ]; - - for update in updates { - verify_header(deps.as_ref(), update.clone()).unwrap(); - update_header(deps.as_mut(), update.clone()).unwrap(); - // Consensus state is saved to the updated height. - if update.consensus_update.attested_header.beacon.slot - > update.trusted_sync_committee.trusted_height.revision_height - { - // It's a finality update - let wasm_consensus_state = read_consensus_state( - deps.as_ref(), - &Height { - revision_number: 0, - revision_height: update.consensus_update.attested_header.beacon.slot, - }, - ) - .unwrap() - .unwrap(); - // Slot is updated. - assert_eq!( - wasm_consensus_state.data.slot, - update.consensus_update.attested_header.beacon.slot - ); - // Storage root is updated. - assert_eq!( - wasm_consensus_state.data.storage_root.into_bytes(), - update.account_update.proofs[0].value, - ); - // Latest slot is updated. - // TODO(aeryz): Add cases for `store_period == update_period` and `update_period == store_period + 1` - let wasm_client_state = read_client_state(deps.as_ref()).unwrap(); - assert_eq!( - wasm_client_state.data.latest_slot, - update.consensus_update.attested_header.beacon.slot - ); - } else { - // It's a sync committee update - let updated_height = core::cmp::max( - update.trusted_sync_committee.trusted_height.revision_height, - update.consensus_update.attested_header.beacon.slot, - ); - let wasm_consensus_state = read_consensus_state( - deps.as_ref(), - &Height { - revision_number: 0, - revision_height: updated_height, - }, - ) - .unwrap() - .unwrap(); - - assert_eq!( - wasm_consensus_state.data.next_sync_committee.unwrap(), - update - .consensus_update - .next_sync_committee - .clone() - .unwrap() - .aggregate_pubkey - ); - } - } - } - - fn custom_query_handler(query: &CustomQuery) -> MockQuerierCustomHandlerResult { - match query { - CustomQuery::AggregateVerify { - public_keys, - message, - signature, - } => { - let pubkeys: Vec = public_keys - .iter() - .map(|pk| pk.0.clone().try_into().unwrap()) - .collect(); - - let res = fast_aggregate_verify( - pubkeys.iter().collect::>().as_slice(), - message.as_ref(), - &signature.0.clone().try_into().unwrap(), - ); - - SystemResult::Ok(cosmwasm_std::ContractResult::Ok::( - serde_json::to_vec(&res.is_ok()).unwrap().into(), - )) - } - CustomQuery::Aggregate { public_keys } => { - let pubkey = eth_aggregate_public_keys( - public_keys - .iter() - .map(|pk| pk.as_ref().try_into().unwrap()) - .collect::>() - .as_slice(), - ) - .unwrap(); - - SystemResult::Ok(cosmwasm_std::ContractResult::Ok::( - serde_json::to_vec(&Binary(pubkey.into())).unwrap().into(), - )) - } - } - } - - fn prepare_for_fail_tests() -> ( - OwnedDeps, CustomQuery>, - ethereum::header::Header, - ) { - let mut deps = OwnedDeps::<_, _, _, CustomQuery> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]).with_custom_handler(custom_query_handler), - custom_query_type: PhantomData, - }; - - let wasm_client_state: protos::ibc::lightclients::wasm::v1::ClientState = - serde_json::from_str(include_str!("./test/client_state.json")).unwrap(); - - let wasm_consensus_state: protos::ibc::lightclients::wasm::v1::ConsensusState = - serde_json::from_str(include_str!("./test/consensus_state.json")).unwrap(); - - save_wasm_client_state(deps.as_mut(), wasm_client_state.try_into().unwrap()); - save_wasm_consensus_state( - deps.as_mut(), - wasm_consensus_state.try_into().unwrap(), - &INITIAL_CONSENSUS_STATE_HEIGHT, - ); - - let update = - serde_json::from_str::( - include_str!("./test/sync_committee_update_1.json"), - ) - .unwrap(); - - (deps, update.try_into().unwrap()) - } - - #[test] - fn verify_header_fails_when_sync_committee_aggregate_pubkey_is_incorrect() { - let (deps, mut update) = prepare_for_fail_tests(); - - let mut pubkey = update - .trusted_sync_committee - .sync_committee - .aggregate_pubkey - .clone(); - pubkey.0[0] += 1; - update - .trusted_sync_committee - .sync_committee - .aggregate_pubkey = pubkey; - assert!(verify_header(deps.as_ref(), update).is_err()); - } - - #[test] - fn verify_header_fails_when_finalized_header_execution_branch_merkle_is_invalid() { - let (deps, mut update) = prepare_for_fail_tests(); - update.consensus_update.finalized_header.execution_branch[0].0[0] += 1; - assert!(verify_header(deps.as_ref(), update).is_err()); - } - - #[test] - fn verify_header_fails_when_finality_branch_merkle_is_invalid() { - let (deps, mut update) = prepare_for_fail_tests(); - update.consensus_update.finality_branch[0].0[0] += 1; - assert!(verify_header(deps.as_ref(), update).is_err()); - } - - #[test] - fn membership_verification_works_for_client_state() { - let proof = Proof { - key: CLIENT_STATE_PROOF_KEY.into(), - value: CLIENT_STATE_PROOF_VALUE.into(), - proof: CLIENT_STATE_PROOF.into_iter().map(Into::into).collect(), - }; - - let storage_root = CLIENT_STATE_STORAGE_ROOT.clone(); - - let client_state = cometbls::client_state::ClientState { - chain_id: "ibc-0".to_string(), - trust_level: Fraction { - numerator: 1, - denominator: 3, - }, - trusting_period: Duration::new(1814400, 0).unwrap(), - unbonding_period: Duration::new(1814400, 0).unwrap(), - max_clock_drift: Duration::new(40, 0).unwrap(), - frozen_height: Height { - revision_number: 0, - revision_height: 0, - }, - }; - - let wasm_client_state = protos::ibc::lightclients::wasm::v1::ClientState { - data: client_state.into_proto_bytes(), - code_id: CLIENT_STATE_WASM_CODE_ID.into(), - latest_height: Some(protos::ibc::core::client::v1::Height { - revision_number: 0, - revision_height: 1, - }), - }; - - let any_client_state = protos::google::protobuf::Any { - type_url: "/ibc.lightclients.wasm.v1.ClientState".into(), - value: wasm_client_state.encode_to_vec(), - }; - - do_verify_membership( - ClientStatePath::new( - &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), - ) - .into(), - storage_root, - 3, - proof, - any_client_state.encode_to_vec(), - ) - .expect("Membership verification of client state failed"); - } - - #[test] - fn membership_verification_works_for_consensus_state() { - let proof = Proof { - key: CONSENSUS_STATE_PROOF_KEY.into(), - value: CONSENSUS_STATE_PROOF_VALUE.into(), - proof: CONSENSUS_STATE_PROOF.into_iter().map(Into::into).collect(), - }; - - let storage_root = CONSENSUS_STATE_STORAGE_ROOT.clone(); - - let consensus_state = cometbls::consensus_state::ConsensusState { - root: MerkleRoot { - hash: CONSENSUS_STATE_CONTRACT_MERKLE_ROOT.clone(), - }, - next_validators_hash: CONSENSUS_STATE_NEXT_VALIDATORS_HASH.clone(), - }; - - let wasm_consensus_state = protos::ibc::lightclients::wasm::v1::ConsensusState { - data: consensus_state.into_proto_bytes(), - timestamp: 1684400046, - }; - - let any_consensus_state = protos::google::protobuf::Any { - type_url: "/ibc.lightclients.wasm.v1.ConsensusState".into(), - value: wasm_consensus_state.encode_to_vec(), - }; - - do_verify_membership( - ClientConsensusStatePath::new( - &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), - &IbcHeight::new(0, 1).unwrap(), - ) - .into(), - storage_root, - 3, - proof, - any_consensus_state.encode_to_vec(), - ) - .expect("Membership verification of consensus state failed"); - } - - fn prepare_connection_end() -> ( - Proof, - H256, - protos::ibc::core::connection::v1::ConnectionEnd, - ) { - let proof = Proof { - key: CONNECTION_END_PROOF_KEY.into(), - value: CONNECTION_END_PROOF_VALUE.into(), - proof: CONNECTION_END_PROOF.into_iter().map(Into::into).collect(), - }; - - let storage_root = CONNECTION_END_STORAGE_ROOT.clone(); - - let connection_end = protos::ibc::core::connection::v1::ConnectionEnd { - client_id: format!("{ETHEREUM_CLIENT_ID_PREFIX}-0"), - versions: vec![protos::ibc::core::connection::v1::Version { - identifier: "1".into(), - features: vec!["ORDER_ORDERED".into(), "ORDER_UNORDERED".into()], - }], - state: 1, - counterparty: Some(protos::ibc::core::connection::v1::Counterparty { - client_id: format!("{WASM_CLIENT_ID_PREFIX}-0"), - connection_id: Default::default(), - prefix: Some(protos::ibc::core::commitment::v1::MerklePrefix { - key_prefix: IBC_KEY_PREFIX.as_bytes().to_vec(), - }), - }), - delay_period: 0, - }; - - (proof, storage_root, connection_end) - } - - #[test] - fn membership_verification_works_for_connection_end() { - let (proof, storage_root, connection_end) = prepare_connection_end(); - - do_verify_membership( - ConnectionPath::new(&ConnectionId::new(0)).into(), - storage_root, - 3, - proof, - connection_end.encode_to_vec(), - ) - .expect("Membership verification of connection end failed"); - } - - #[test] - fn membership_verification_fails_for_incorrect_proofs() { - let (mut proof, storage_root, connection_end) = prepare_connection_end(); - - let proofs = vec![ - { - let mut proof = proof.clone(); - proof.value[10] = u8::MAX - proof.value[10]; // Makes sure that produced value is always valid and different - proof - }, - { - let mut proof = proof.clone(); - proof.key[5] = u8::MAX - proof.key[5]; - proof - }, - { - proof.proof[0][10] = u8::MAX - proof.proof[0][10]; - proof - }, - ]; - - for proof in proofs { - assert!(do_verify_membership( - ConnectionPath::new(&ConnectionId::new(0)).into(), - storage_root.clone(), - 3, - proof, - connection_end.encode_to_vec(), - ) - .is_err()); - } - } - - #[test] - fn membership_verification_fails_for_incorrect_storage_root() { - let (proof, mut storage_root, connection_end) = prepare_connection_end(); - - storage_root.0[10] = u8::MAX - storage_root.0[10]; - - assert!(do_verify_membership( - ConnectionPath::new(&ConnectionId::new(0)).into(), - storage_root, - 3, - proof, - connection_end.encode_to_vec(), - ) - .is_err()); - } - - #[test] - fn membership_verification_fails_for_incorrect_data() { - let (proof, storage_root, mut connection_end) = prepare_connection_end(); - - connection_end.client_id = "incorrect-client-id".into(); - - assert!(do_verify_membership( - ConnectionPath::new(&ConnectionId::new(0)).into(), - storage_root, - 3, - proof, - connection_end.encode_to_vec(), - ) - .is_err()); - } - - #[test] - fn non_membership_verification_works() { - let proof = Proof { - key: NON_MEMBERSHIP_PROOF_KEY.into(), - value: vec![0x0], - proof: NON_MEMBERSHIP_PROOF.into_iter().map(Into::into).collect(), - }; - - let storage_root = NON_MEMBERSHIP_STORAGE_ROOT.clone(); - - do_verify_non_membership( - ClientStatePath::new( - &ClientId::new(ClientType::new(ETHEREUM_CLIENT_ID_PREFIX.into()), 0).unwrap(), - ) - .into(), - storage_root, - 3, - proof, - ) - .expect("Membership verification of client state failed"); - } - - #[test] - fn non_membership_verification_fails_when_value_not_empty() { - let (proof, storage_root, _) = prepare_connection_end(); - - assert_eq!( - do_verify_non_membership( - ConnectionPath::new(&ConnectionId::new(0)).into(), - storage_root, - 3, - proof, - ), - Err(Error::CounterpartyStorageNotNil) - ); - } -} diff --git a/light-clients/ethereum-light-client/src/errors.rs b/light-clients/ethereum-light-client/src/errors.rs index c291580380..5746f7565a 100644 --- a/light-clients/ethereum-light-client/src/errors.rs +++ b/light-clients/ethereum-light-client/src/errors.rs @@ -1,5 +1,10 @@ use cosmwasm_std::StdError; use thiserror::Error as ThisError; +use unionlabs::{ + ibc::lightclients::ethereum::header::Header, TryFromProtoBytesError, TryFromProtoErrorOf, +}; + +use crate::Config; #[derive(ThisError, Debug, PartialEq)] pub enum Error { @@ -87,6 +92,9 @@ pub enum Error { #[error("Custom query: {0}")] CustomQuery(String), + + #[error("Wasm client error: {0}")] + Wasm(String), } impl Error { @@ -114,10 +122,18 @@ impl Error { } } -impl From for Error { - fn from(error: wasm_light_client_types::Error) -> Self { +impl From>>> for Error { + fn from(value: TryFromProtoBytesError>>) -> Self { + Self::DecodeError(format!("{:?}", value)) + } +} + +impl From for Error { + fn from(error: ics008_wasm_client::Error) -> Self { match error { - wasm_light_client_types::Error::Decode(e) => Error::DecodeError(e), + ics008_wasm_client::Error::Decode(e) => Error::DecodeError(e), + ics008_wasm_client::Error::UnexpectedCallDataFromHostModule(e) => Error::Wasm(e), + ics008_wasm_client::Error::ClientStateNotFound => Error::Wasm(format!("{error:#?}")), } } } diff --git a/light-clients/ethereum-light-client/src/header.rs b/light-clients/ethereum-light-client/src/header.rs deleted file mode 100644 index 03f871345b..0000000000 --- a/light-clients/ethereum-light-client/src/header.rs +++ /dev/null @@ -1,7 +0,0 @@ -use unionlabs::{ethereum_consts_traits::ChainSpec, ibc::lightclients::ethereum::header::Header}; - -// REVIEW: Unused? -#[derive(serde::Serialize, serde::Deserialize)] -pub enum ClientMessage { - Header(Header), -} diff --git a/light-clients/ethereum-light-client/src/lib.rs b/light-clients/ethereum-light-client/src/lib.rs index 370e0d04e8..ceafa41a35 100644 --- a/light-clients/ethereum-light-client/src/lib.rs +++ b/light-clients/ethereum-light-client/src/lib.rs @@ -1,12 +1,10 @@ +pub mod client; pub mod consensus_state; pub mod context; pub mod contract; pub mod custom_query; pub mod errors; pub mod eth_encoding; -pub mod header; -pub mod msg; -pub mod state; #[cfg(feature = "mainnet")] pub use unionlabs::ethereum_consts_traits::Mainnet as Config; diff --git a/light-clients/ethereum-light-client/src/msg.rs b/light-clients/ethereum-light-client/src/msg.rs deleted file mode 100644 index 116ffaf030..0000000000 --- a/light-clients/ethereum-light-client/src/msg.rs +++ /dev/null @@ -1,10 +0,0 @@ -use cosmwasm_schema::cw_serde; -pub use wasm_light_client_types::msg::{ExecuteMsg, QueryMsg}; - -#[cw_serde] -pub struct InstantiateMsg {} - -pub enum StorageState { - Occupied(Vec), - Empty, -} diff --git a/light-clients/ethereum-light-client/src/state.rs b/light-clients/ethereum-light-client/src/state.rs deleted file mode 100644 index 9c4a6b3ce3..0000000000 --- a/light-clients/ethereum-light-client/src/state.rs +++ /dev/null @@ -1,129 +0,0 @@ -use cosmwasm_std::{Deps, DepsMut}; -use unionlabs::{ - ibc::{ - core::client::height::Height, - google::protobuf::any::Any, - lightclients::{ethereum, wasm}, - }, - IntoProto, TryFromProto, -}; - -use crate::{custom_query::CustomQuery, errors::Error}; - -// Client state that is stored by the host -pub const HOST_CLIENT_STATE_KEY: &str = "clientState"; -pub const HOST_CONSENSUS_STATES_KEY: &str = "consensusStates"; - -fn consensus_db_key(height: &Height) -> String { - format!( - "{}/{}-{}", - HOST_CONSENSUS_STATES_KEY, height.revision_number, height.revision_height - ) -} - -/// Reads the client state from the host. -/// -/// The host stores the client state with 'HOST_CLIENT_STATE_KEY' key in the following format: -/// - type_url: WASM_CLIENT_STATE_TYPE_URL -/// - value: (PROTO_ENCODED_WASM_CLIENT_STATE) -/// - code_id: Code ID of this contract's code -/// - latest_height: Latest height that the state is updated -/// - data: Contract defined raw bytes, which we use as protobuf encoded ethereum client state. -pub fn read_client_state( - deps: Deps, -) -> Result, Error> { - let any_state = deps - .storage - .get(HOST_CLIENT_STATE_KEY.as_bytes()) - .ok_or(Error::ClientStateNotFound)?; - - Any::try_from_proto_bytes(any_state.as_slice()) - .map(|any| any.0) - .map_err(|err| { - Error::decode(format!( - "when decoding raw bytes to any in `read_client_state`: {err:#?}" - )) - }) -} - -/// Reads the consensus state at a specific height from the host. -/// -/// The host stores the consensus state with 'HOST_CONSENSUS_STATES_KEY/REVISION_NUMBER-REVISION_HEIGHT' -/// key in the following format: -/// - type_url: WASM_CONSENSUS_STATE_TYPE_URL -/// - value: (PROTO_ENCODED_WASM_CLIENT_STATE) -/// - timestamp: Time of this consensus state. -/// - data: Contract defined raw bytes, which we use as protobuf encoded ethereum consensus state. -pub fn read_consensus_state( - deps: Deps, - height: &Height, -) -> Result< - Option>, - Error, -> { - deps.storage - .get(consensus_db_key(height).as_bytes()) - .map(|bytes| Any::try_from_proto_bytes(&bytes).map(|any| any.0)) - .transpose() - .map_err(|err| Error::decode(format!("error reading consensus state: {err:#?}"))) -} - -pub fn save_wasm_client_state( - deps: DepsMut, - wasm_client_state: wasm::client_state::ClientState, -) { - let any_state = Any(wasm_client_state); - deps.storage.set( - HOST_CLIENT_STATE_KEY.as_bytes(), - any_state.into_proto_bytes().as_slice(), - ); -} - -/// Update the client state on the host store. -pub fn update_client_state( - deps: DepsMut, - mut wasm_client_state: wasm::client_state::ClientState, - // new_client_state: ethereum::client_state::ClientState, - latest_execution_height: u64, -) { - // wasm_client_state.data = new_client_state; - wasm_client_state.latest_height = Height { - revision_number: 0, - revision_height: latest_execution_height, - }; - - save_wasm_client_state(deps, wasm_client_state); -} - -pub fn save_wasm_consensus_state( - deps: DepsMut, - wasm_consensus_state: wasm::consensus_state::ConsensusState< - ethereum::consensus_state::ConsensusState, - >, - height: &Height, -) { - deps.storage.set( - consensus_db_key(height).as_bytes(), - &Any(wasm_consensus_state).into_proto_bytes(), - ); -} - -/// Save new consensus state at height `consensus_state.slot` to the host store. -pub fn save_consensus_state( - deps: DepsMut, - wasm_consensus_state: wasm::consensus_state::ConsensusState< - ethereum::consensus_state::ConsensusState, - >, - // new_consensus_state: ethereum::consensus_state::ConsensusState, - execution_height: u64, -) -> Result<(), Error> { - let height = Height { - revision_number: 0, - revision_height: execution_height, - }; - // REVIEW: Is the timestamp set properly somewhere else? - // wasm_consensus_state.timestamp = new_consensus_state.timestamp; - // wasm_consensus_state.data = new_consensus_state; - save_wasm_consensus_state(deps, wasm_consensus_state, &height); - Ok(()) -} diff --git a/light-clients/wasm-light-client-types/src/lib.rs b/light-clients/wasm-light-client-types/src/lib.rs deleted file mode 100644 index ede8d1c324..0000000000 --- a/light-clients/wasm-light-client-types/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -use prost::Message; -use protos::{ - google::protobuf::Any, - ibc::lightclients::wasm::v1::{ - ClientState as RawWasmClientState, ConsensusState as RawWasmConsensusState, - }, -}; - -pub mod msg; - -pub enum Error { - Decode(String), -} - -impl Error { - pub fn decode>(msg: S) -> Error { - Error::Decode(msg.into()) - } -} - -pub fn decode_client_state_to_concrete_state( - state: &[u8], -) -> Result { - let any_state = Any::decode(state) - .map_err(|_| Error::decode("when decoding raw bytes to any in `verify_membership`"))?; - - let wasm_client_state = - RawWasmClientState::decode(any_state.value.as_slice()).map_err(|_| { - Error::decode("when decoding any value to wasm client state in `verify_membership`") - })?; - - let any_state = Any::decode(wasm_client_state.data.as_slice()).map_err(|_| { - Error::decode("when decoding wasm client state to tm client state in `verify_membership`") - })?; - - T::decode(any_state.value.as_slice()).map_err(|_| { - Error::decode("when decoding any state to tm client state in `verify_membership`") - }) -} - -pub fn decode_consensus_state_to_concrete_state( - state: &[u8], -) -> Result { - let any_state = Any::decode(state) - .map_err(|_| Error::decode("when decoding raw bytes to any in `verify_membership`"))?; - - let wasm_consensus_state = - RawWasmConsensusState::decode(any_state.value.as_slice()).map_err(|_| { - Error::decode("when decoding any value to wasm client state in `verify_membership`") - })?; - - let any_state = Any::decode(wasm_consensus_state.data.as_slice()).map_err(|_| { - Error::decode("when decoding wasm client state to tm client state in `verify_membership`") - })?; - - T::decode(any_state.value.as_slice()).map_err(|_| { - Error::decode("when decoding any state to tm client state in `verify_membership`") - }) -}