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/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..55ab7d5c3d --- /dev/null +++ b/light-clients/ethereum-light-client/src/client.rs @@ -0,0 +1,1101 @@ +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, + }, + wasm::{ + client_state::ClientState as WasmClientState, + consensus_state::ConsensusState as WasmConsensusState, + }, + }, + }, + TryFromProto, +}; + +use crate::{ + consensus_state::TrustedConsensusState, + context::LightClientContext, + custom_query::{query_aggregate_public_keys, CustomQuery, VerificationContext}, + errors::Error, + eth_encoding::generate_commitment_key, + Config, +}; + +pub struct EthereumLightClient; + +impl IBCClient for EthereumLightClient { + type Error = Error; + + type CustomQuery = CustomQuery; + + type Header = Header; + + type Misbehaviour = Header; + + 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, + 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::valid(None)) + } + + fn update_state( + mut deps: DepsMut, + 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_misbeviour( + _deps: Deps, + _client_message: ics008_wasm_client::ClientMessage, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + fn check_for_misbehaviour( + _deps: Deps, + _client_message: ics008_wasm_client::ClientMessage, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + fn verify_upgrade_and_update_state( + _deps: Deps, + _upgrade_client_state: Self::ClientState, + _upgrade_consensus_state: Self::ConsensusState, + _proof_upgrade_client: Binary, + _proof_upgrade_consensus_state: Binary, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + fn check_substitute_and_update_state( + _deps: Deps, + ) -> Result { + Ok(ContractResult::valid(None)) + } + + 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(), update.clone()).unwrap(); + EthereumLightClient::update_state(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: 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(), 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(), 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(), 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..409f22dcfe 100644 --- a/light-clients/ethereum-light-client/src/contract.rs +++ b/light-clients/ethereum-light-client/src/contract.rs @@ -1,40 +1,11 @@ -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 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}; + +pub struct InstantiateMsg {} #[entry_point] pub fn instantiate( @@ -49,1060 +20,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..1d8ba5ffbd 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::Wasm(e), + ics008_wasm_client::Error::NotSpecCompilant(e) => Error::Wasm(e), + ics008_wasm_client::Error::ClientStateNotFound => Error::Wasm(format!("err:#?")), } } } 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(()) -}