diff --git a/ibc-core/ics02-client/types/Cargo.toml b/ibc-core/ics02-client/types/Cargo.toml index bc11d449c..abc812cd4 100644 --- a/ibc-core/ics02-client/types/Cargo.toml +++ b/ibc-core/ics02-client/types/Cargo.toml @@ -40,6 +40,9 @@ tendermint = { workspace = true } parity-scale-codec = { workspace = true, optional = true } scale-info = { workspace = true, optional = true } +[dev-dependencies] +rstest = { workspace = true } + [features] default = [ "std" ] std = [ diff --git a/ibc-core/ics02-client/types/src/error.rs b/ibc-core/ics02-client/types/src/error.rs index 08c6d2114..decfbfe1e 100644 --- a/ibc-core/ics02-client/types/src/error.rs +++ b/ibc-core/ics02-client/types/src/error.rs @@ -105,6 +105,12 @@ pub enum ClientError { InvalidUpdateClientMessage, /// other error: `{description}` Other { description: String }, + /// invalid attribute key: `{attribute_key}` + InvalidAttributeKey { attribute_key: String }, + /// invalid attribute value: `{attribute_value}` + InvalidAttributeValue { attribute_value: String }, + /// Missing attribute key: `{attribute_key}` + MissingAttributeKey { attribute_key: String }, } impl From<&'static str> for ClientError { diff --git a/ibc-core/ics02-client/types/src/events.rs b/ibc-core/ics02-client/types/src/events.rs index d7c7545c1..a91e1b413 100644 --- a/ibc-core/ics02-client/types/src/events.rs +++ b/ibc-core/ics02-client/types/src/events.rs @@ -5,8 +5,9 @@ use ibc_primitives::prelude::*; use subtle_encoding::hex; use tendermint::abci; +use self::str::FromStr; +use crate::error::ClientError; use crate::height::Height; - /// Client event types pub const CREATE_CLIENT_EVENT: &str = "create_client"; pub const UPDATE_CLIENT_EVENT: &str = "update_client"; @@ -51,6 +52,37 @@ impl From for abci::EventAttribute { (CLIENT_ID_ATTRIBUTE_KEY, attr.client_id.as_str()).into() } } +impl TryFrom for ClientIdAttribute { + type Error = ClientError; + + fn try_from(value: abci::EventAttribute) -> Result { + if let Ok(key_str) = value.key_str() { + if key_str != CLIENT_ID_ATTRIBUTE_KEY { + return Err(ClientError::InvalidAttributeKey { + attribute_key: key_str.to_string(), + }); + } + } else { + return Err(ClientError::InvalidAttributeKey { + attribute_key: String::new(), + }); + } + + value + .value_str() + .map(|value| { + let client_id = + ClientId::from_str(value).map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: value.to_string(), + })?; + + Ok(ClientIdAttribute { client_id }) + }) + .map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: String::new(), + })? + } +} #[cfg_attr( feature = "parity-scale-codec", @@ -76,6 +108,39 @@ impl From for abci::EventAttribute { } } +impl TryFrom for ClientTypeAttribute { + type Error = ClientError; + + fn try_from(value: abci::EventAttribute) -> Result { + if let Ok(key_str) = value.key_str() { + if key_str != CLIENT_TYPE_ATTRIBUTE_KEY { + return Err(ClientError::InvalidAttributeKey { + attribute_key: key_str.to_string(), + }); + } + } else { + return Err(ClientError::InvalidAttributeKey { + attribute_key: String::new(), + }); + } + + value + .value_str() + .map(|value| { + let client_type = ClientType::from_str(value).map_err(|_| { + ClientError::InvalidAttributeValue { + attribute_value: value.to_string(), + } + })?; + + Ok(ClientTypeAttribute { client_type }) + }) + .map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: String::new(), + })? + } +} + #[cfg_attr( feature = "parity-scale-codec", derive( @@ -100,6 +165,36 @@ impl From for abci::EventAttribute { } } +impl TryFrom for ConsensusHeightAttribute { + type Error = ClientError; + + fn try_from(value: abci::EventAttribute) -> Result { + if let Ok(key_str) = value.key_str() { + if key_str != CONSENSUS_HEIGHT_ATTRIBUTE_KEY { + return Err(ClientError::InvalidAttributeKey { + attribute_key: key_str.to_string(), + }); + } + } else { + return Err(ClientError::InvalidAttributeKey { + attribute_key: String::new(), + }); + } + + value + .value_str() + .map(|value| { + let consensus_height = + Height::from_str(value).map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: value.to_string(), + })?; + Ok(ConsensusHeightAttribute { consensus_height }) + }) + .map_err(|_| ClientError::InvalidAttributeKey { + attribute_key: String::new(), + })? + } +} #[cfg_attr( feature = "parity-scale-codec", derive( @@ -129,6 +224,44 @@ impl From for abci::EventAttribute { } } +impl TryFrom for ConsensusHeightsAttribute { + type Error = ClientError; + + fn try_from(value: abci::EventAttribute) -> Result { + if let Ok(key_str) = value.key_str() { + if key_str != CONSENSUS_HEIGHTS_ATTRIBUTE_KEY { + return Err(ClientError::InvalidAttributeKey { + attribute_key: key_str.to_string(), + }); + } + } else { + return Err(ClientError::InvalidAttributeKey { + attribute_key: String::new(), + }); + } + + value + .value_str() + .map(|value| { + let consensus_heights: Vec = value + .split(',') + .map(|height_str| { + Height::from_str(height_str).map_err(|_| { + ClientError::InvalidAttributeValue { + attribute_value: height_str.to_string(), + } + }) + }) + .collect::, ClientError>>()?; + + Ok(ConsensusHeightsAttribute { consensus_heights }) + }) + .map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: String::new(), + })? + } +} + #[cfg_attr( feature = "parity-scale-codec", derive( @@ -159,6 +292,37 @@ impl From for abci::EventAttribute { .into() } } +impl TryFrom for HeaderAttribute { + type Error = ClientError; + + fn try_from(value: abci::EventAttribute) -> Result { + if let Ok(key_str) = value.key_str() { + if key_str != HEADER_ATTRIBUTE_KEY { + return Err(ClientError::InvalidAttributeKey { + attribute_key: key_str.to_string(), + }); + } + } else { + return Err(ClientError::InvalidAttributeKey { + attribute_key: String::new(), + }); + } + + value + .value_str() + .map(|value| { + let header = + hex::decode(value).map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: value.to_string(), + })?; + + Ok(HeaderAttribute { header }) + }) + .map_err(|_| ClientError::InvalidAttributeValue { + attribute_value: String::new(), + })? + } +} /// CreateClient event signals the creation of a new on-chain client (IBC client). #[cfg_attr( @@ -220,6 +384,79 @@ impl From for abci::Event { } } +impl TryFrom for CreateClient { + type Error = ClientError; + + fn try_from(value: abci::Event) -> Result { + if value.kind != CREATE_CLIENT_EVENT { + return Err(ClientError::Other { + description: "Error in parsing CreateClient event".to_string(), + }); + } + + value + .attributes + .iter() + .try_fold( + (None, None, None), + |(client_id, client_type, consensus_height): ( + Option, + Option, + Option, + ), + attribute| { + let key = + attribute + .key_str() + .map_err(|_| ClientError::InvalidAttributeKey { + attribute_key: String::new(), + })?; + + match key { + CLIENT_ID_ATTRIBUTE_KEY => Ok(( + Some(attribute.clone().try_into()?), + client_type, + consensus_height, + )), + CLIENT_TYPE_ATTRIBUTE_KEY => Ok(( + client_id, + Some(attribute.clone().try_into()?), + consensus_height, + )), + CONSENSUS_HEIGHT_ATTRIBUTE_KEY => { + Ok((client_id, client_type, Some(attribute.clone().try_into()?))) + } + _ => Ok((client_id, client_type, consensus_height)), + } + }, + ) + .and_then( + |(client_id, client_type, consensus_height): ( + Option, + Option, + Option, + )| { + let client_id = client_id.ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CLIENT_ID_ATTRIBUTE_KEY.to_string(), + })?; + let client_type = + client_type.ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CLIENT_TYPE_ATTRIBUTE_KEY.to_string(), + })?; + let consensus_height = + consensus_height.ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CONSENSUS_HEIGHT_ATTRIBUTE_KEY.to_string(), + })?; + + Ok(CreateClient::new( + client_id.client_id, + client_type.client_type, + consensus_height.consensus_height, + )) + }, + ) + } +} /// UpdateClient event signals a recent update of an on-chain client (IBC Client). #[cfg_attr( feature = "parity-scale-codec", @@ -305,7 +542,116 @@ impl From for abci::Event { } } } +impl TryFrom for UpdateClient { + type Error = ClientError; + + fn try_from(value: abci::Event) -> Result { + if value.kind != UPDATE_CLIENT_EVENT { + return Err(ClientError::Other { + description: "Error in parsing UpdateClient event".to_string(), + }); + } + type UpdateClientAttributes = ( + Option, + Option, + Option, + Option, + Option, + ); + + value + .attributes + .iter() + .try_fold( + (None, None, None, None, None), + |acc: UpdateClientAttributes, attribute| { + let key = + attribute + .key_str() + .map_err(|_| ClientError::InvalidAttributeKey { + attribute_key: String::new(), + })?; + + match key { + CLIENT_ID_ATTRIBUTE_KEY => Ok(( + Some(attribute.clone().try_into()?), + acc.1, + acc.2, + acc.3, + acc.4, + )), + CLIENT_TYPE_ATTRIBUTE_KEY => Ok(( + acc.0, + Some(attribute.clone().try_into()?), + acc.2, + acc.3, + acc.4, + )), + CONSENSUS_HEIGHT_ATTRIBUTE_KEY => Ok(( + acc.0, + acc.1, + Some(attribute.clone().try_into()?), + acc.3, + acc.4, + )), + CONSENSUS_HEIGHTS_ATTRIBUTE_KEY => Ok(( + acc.0, + acc.1, + acc.2, + Some(attribute.clone().try_into()?), + acc.4, + )), + HEADER_ATTRIBUTE_KEY => Ok(( + acc.0, + acc.1, + acc.2, + acc.3, + Some(attribute.clone().try_into()?), + )), + _ => Ok(acc), + } + }, + ) + .and_then( + |(client_id, client_type, consensus_height, consensus_heights, header)| { + let client_id = client_id + .ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CLIENT_ID_ATTRIBUTE_KEY.to_string(), + })? + .client_id; + let client_type = client_type + .ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CLIENT_TYPE_ATTRIBUTE_KEY.to_string(), + })? + .client_type; + let consensus_height = consensus_height + .ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CONSENSUS_HEIGHT_ATTRIBUTE_KEY.to_string(), + })? + .consensus_height; + let consensus_heights = consensus_heights + .ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: CONSENSUS_HEIGHTS_ATTRIBUTE_KEY.to_string(), + })? + .consensus_heights; + let header = header + .ok_or_else(|| ClientError::MissingAttributeKey { + attribute_key: HEADER_ATTRIBUTE_KEY.to_string(), + })? + .header; + + Ok(UpdateClient::new( + client_id, + client_type, + consensus_height, + consensus_heights, + header, + )) + }, + ) + } +} /// ClientMisbehaviour event signals the update of an on-chain client (IBC Client) with evidence of /// misbehaviour. #[cfg_attr( @@ -416,3 +762,132 @@ impl From for abci::Event { } } } + +#[cfg(test)] +mod tests { + use core::any::Any; + + use rstest::*; + + use super::*; + + #[rstest] + #[case( + abci::Event { + kind: CREATE_CLIENT_EVENT.to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_id", "07-tendermint-0")), + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + ], + }, + Ok(CreateClient::new( + ClientId::from_str("07-tendermint-0").expect("should parse"), + ClientType::from_str("07-tendermint").expect("should parse"), + Height::new(1, 10).unwrap(), + )), + )] + #[case( + abci::Event { + kind: "some_other_event".to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_id", "07-tendermint-0")), + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + ], + }, + Err(ClientError::Other { + description: "Error in parsing CreateClient event".to_string(), + }), + )] + #[case( + abci::Event { + kind: CREATE_CLIENT_EVENT.to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + ], + }, + Err(ClientError::MissingAttributeKey { + attribute_key: CLIENT_ID_ATTRIBUTE_KEY.to_string(), + }), + )] + fn test_create_client_try_from( + #[case] event: abci::Event, + #[case] expected: Result, + ) { + let result = CreateClient::try_from(event); + if expected.is_err() { + assert_eq!( + result.unwrap_err().type_id(), + expected.unwrap_err().type_id() + ); + } else { + assert_eq!(result.unwrap(), expected.unwrap()); + } + } + + #[rstest] + #[case( + abci::Event { + kind: UPDATE_CLIENT_EVENT.to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_id", "07-tendermint-0")), + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + abci::EventAttribute::from(("consensus_heights", "1-10,1-11")), + abci::EventAttribute::from(("header", "1234")), + ], + }, + Ok(UpdateClient::new( + ClientId::from_str("07-tendermint-0").expect("should parse"), + ClientType::from_str("07-tendermint").expect("should parse"), + Height::new(1, 10).unwrap(), + vec![Height::new(1, 10).unwrap(), Height::new(1, 11).unwrap()], + vec![0x12, 0x34], + )), + )] + #[case( + abci::Event { + kind: "some_other_event".to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_id", "07-tendermint-0")), + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + abci::EventAttribute::from(("consensus_heights", "1-10,1-11")), + abci::EventAttribute::from(("header", "1234")), + ], + }, + Err(ClientError::Other { + description: "Error in parsing UpdateClient event".to_string(), + }), + )] + #[case( + abci::Event { + kind: UPDATE_CLIENT_EVENT.to_owned(), + attributes: vec![ + abci::EventAttribute::from(("client_type", "07-tendermint")), + abci::EventAttribute::from(("consensus_height", "1-10")), + abci::EventAttribute::from(("consensus_heights", "1-10,1-11")), + abci::EventAttribute::from(("header", "1234")), + ], + }, + Err(ClientError::MissingAttributeKey { + attribute_key: CLIENT_ID_ATTRIBUTE_KEY.to_string(), + }), + )] + fn test_update_client_try_from( + #[case] event: abci::Event, + #[case] expected: Result, + ) { + let result = UpdateClient::try_from(event); + if expected.is_err() { + assert_eq!( + result.unwrap_err().type_id(), + expected.unwrap_err().type_id() + ); + } else { + assert_eq!(result.unwrap(), expected.unwrap()); + } + } +}