diff --git a/docs/architecture/adr-003-handler-implementation.md b/docs/architecture/adr-003-handler-implementation.md new file mode 100644 index 0000000000..f0f5ada59a --- /dev/null +++ b/docs/architecture/adr-003-handler-implementation.md @@ -0,0 +1,679 @@ +# ADR 003: IBC protocol implementation + +## Changelog +* 2020-08-06: Initial proposal +* 2020-08-10: Rename Handler to Message Processor +* 2020-08-14: Revamp definition of chain-specific messages, readers and keepers + +## Reader + +> This section contains all the context one needs to understand the current state, and why there is a problem. It should be as succinct as possible and introduce the high level idea behind the solution. + +TODO + +## Decision + +In this ADR, we provide recommendations for implementing the IBC message processing logic within the `ibc-rs` crate. +Concepts are introduced in the order given by a topological sort of their dependencies on each other. + +### Events + +IBC message processors must be able to emit events which will then be broadcasted via the node's pub/sub mechanism, +and eventually picked up by the IBC relayer. + +A generic interface for events is provided below, where an event is represented +as a pair of an event type and a list of attributes. An attribute is simply a pair +of a key and a value, both represented as strings. + +Here is the [list of all IBB-related events][events], as seen by the relayer. +Because the structure of these events do not match the ones which are emitted by the IBC message processors, +each IBC submodule should defined its own event type and associated variants. + +[events]: https://github.com/informalsystems/ibc-rs/blob/bf84a73ef7b3d5e9a434c9af96165997382dcc9d/modules/src/events.rs#L15-L43 + +```rust +pub struct Attribute { + key: String, + value: String, +} + +impl Attribute { + pub fn new(key: String, value: String) -> Self; +} + +pub enum EventType { + Message, + Custom(String), +} + +pub struct Event { + typ: EventType, + attributes: Vec, +} + +impl Event { + pub fn new(typ: EventType, attrs: Vec<(String, String)>) -> Self; +} +``` + +### Logging + +IBC message processors must be able to log information for introspectability and ease of debugging. +A message processor can output multiple log records, which are expressed as a pair of a status and a +log line. The interface for emitting log records is described in the next section. + +```rust +pub enum LogStatus { + Success, + Info, + Warning, + Error, +} + +pub struct Log { + status: LogStatus, + body: String, +} + +impl Log { + fn success(msg: impl Display) -> Self; + fn info(msg: impl Display) -> Self; + fn warning(msg: impl Display) -> Self; + fn error(msg: impl Display) -> Self; +} +``` + +### Message processor output + +IBC message processors must be able to return arbitrary data, together with events and log records, as descibed above. +As a message processor may fail, it is necessary to keep track of errors. + +To this end, we introduce a type for the return value of a message processor: + +```rust +pub type HandlerResult = Result, E>; + +pub struct HandlerOutput { + pub result: T, + pub log: Vec, + pub events: Vec, +} +``` + +We introduce a builder interface to be used within the message processor implementation to incrementally build a `HandlerOutput` value. + +```rust +impl HandlerOutput { + pub fn builder() -> HandlerOutputBuilder { + HandlerOutputBuilder::new() + } +} + +pub struct HandlerOutputBuilder { + log: Vec, + events: Vec, + marker: PhantomData, +} + +impl HandlerOutputBuilder { + pub fn log(&mut self, log: impl Into); + pub fn emit(&mut self, event: impl Into); + pub fn with_result(self, result: T) -> HandlerOutput; +} +``` + +We provide below an example usage of the builder API: + +```rust +fn some_ibc_handler() -> HandlerResult { + let mut output = HandlerOutput::builder(); + + // ... + + output.log(Log::info("did something")) + + // ... + + output.log(Log::success("all good")); + output.emit(SomeEvent::AllGood); + + Ok(output.with_result(42)); +} +``` + +### IBC Submodule + +The various IBC messages and their processing logic, as described in the IBC specification, +are split into a collection of submodules, each pertaining to a specific aspect of +the IBC protocol, eg. client lifecycle management, connection lifecycle management, +packet relay, etc. + +In this section we propose a general approach to implement the message processors for a submodule. +As a running example we will use a dummy submodule that deals with connections, which should not +be mistaken for the actual ICS 003 Connection submodule. + +#### Events + +The events which may be emitted by the message processors of a submodule should be defined +as an enumeration, while a way of converting those into the generic `Event` type +defined in a previous section should be provided via the `From` trait. + +```rust +pub enum ConnectionEvent { + ConnectionOpenInit(ConnectionId), + ConnectionOpenTry(ConnectionId), +} + +impl From for Event { + fn from(ce: ConnectionEvent) -> Event { + match ce { + ConnectionEvent::ConnectionOpenInit(connection_id) => Event::new( + EventType::Custom("ConnectionOpenInit".to_string()), + vec![("connection_id".to_string(), connection_id.to_string())], + ), + ConnectionEvent::ConnectionOpenTry(connection_id) => Event::new( + EventType::Custom("ConnectionOpenTry".to_string()), + vec![("connection_id".to_string(), connection_id.to_string())], + ), + } + } +} +``` + +#### Reader + +A typical message processor will need to read data from the chain state at the current height, +via the private and provable stores. + +To avoid coupling between the message processor interface and the store API, we introduce an interface +for accessing this data. This interface, called a `Reader`, is shared between all message processors +in a submodule, as those typically access the same data. + +Having a high-level interface for this purpose helps avoiding coupling which makes +writing unit tests for the message processors easier, as one does not need to provide a concrete +store, or to mock one. + +```rust +pub trait ConnectionReader +{ + fn connection_end(&self, connection_id: &ConnectionId) -> Option; +} +``` + +A production implementation of this `Reader` would hold references to both the private and provable +store at the current height where the message processor executes, but we omit the actual implementation as +the store interfaces are yet to be defined, as is the general IBC top-level module machinery. + +A mock implementation of the `ConnectionReader` trait could look as follows: + +```rust +struct MockConnectionReader { + connection_id: ConnectionId, + connection_end: Option, + client_reader: MockClientReader, +} + +impl ConnectionReader for MockConnectionReader { + fn connection_end(&self, connection_id: &ConnectionId) -> Option { + if connection_id == &self.connection_id { + self.connection_end.clone() + } else { + None + } + } +} +``` + +#### Keeper + +Once a message processor executes successfully, some data will typically need to be persisted in the chain state +via the private/provable store interfaces. In the same vein as for the reader defined in the previous section, +a submodule should define a trait which provides operations to persist such data. +The same considerations w.r.t. to coupling and unit-testing apply here as well. + +```rust +pub trait ConnectionKeeper { + fn store_connection( + &mut self, + client_id: ConnectionId, + client_type: ConnectionType, + ) -> Result<(), Error>; + + fn add_connection_to_client( + &mut self, + client_id: ClientId, + connection_id: ConnectionId, + ) -> Result<(), Error>; +} +``` + +#### Submodule implementation + +We now come to the actual definition of a message processor for a submodule. + +We recommend each message processor to be defined within its own Rust module, named +after the message processor itself. For example, the "Create Client" message processor of ICS 002 would +be defined in `ibc_modules::ics02_client::handler::create_client`. + +##### Message type + +Each message processor must define a datatype which represent the message it can process. + +```rust +pub struct MsgConnectionOpenInit { + connection_id: ConnectionId, + client_id: ClientId, + counterparty: Counterparty, +} +``` + +##### Message processor implementation + +In this section we provide guidelines for implementating an actual message processor. + +We divide the message processor in two parts: processing and persistance. + +###### Processing + +The actual logic of the message processor is expressed as a pure function, typically named +`process`, which takes as arguments a `Reader` and the corresponding message, and returns +a `HandlerOutput`, where `T` is a concrete datatype and `E` is an error type which defines +all potential errors yielded by the message processors of the current submodule. + +```rust +pub struct ConnectionMsgProcessingResult { + connection_id: ConnectionId, + connection_end: ConnectionEnd, +} +``` + +The `process` function will typically read data via the `Reader`, perform checks and validation, construct new +datatypes, emit log records and events, and eventually return some data together with objects to be persisted. + +To this end, this `process` function will create and manipulate a `HandlerOutput` value like described in +the corresponding section. + +```rust +pub fn process( + reader: &dyn ConnectionReader, + msg: MsgConnectionOpenInit, +) -> HandlerResult +{ + let mut output = HandlerOutput::builder(); + + let MsgConnectionOpenInit { connection_id, client_id, counterparty, } = msg; + + if reader.connection_end(&connection_id).is_some() { + return Err(Kind::ConnectionAlreadyExists(connection_id).into()); + } + + output.log("success: no connection state found"); + + if reader.client_reader.client_state(&client_id).is_none() { + return Err(Kind::ClientForConnectionMissing(client_id).into()); + } + + output.log("success: client found"); + + output.emit(ConnectionEvent::ConnectionOpenInit(connection_id.clone())); + + Ok(output.with_result(ConnectionMsgProcessingResult { + connection_id, + client_id, + counterparty, + })) +} +``` + +###### Persistence + +If the `process` function specified above succeeds, the result value it yielded is then +passed to a function named `keep`, which is responsible for persisting the objects constructed +by the processing function. This `keep` function takes the submodule's `Keeper` and the result +type defined above, and performs side-effecting calls to the keeper's methods to persist the result. + +Below is given an implementation of the `keep` function for the "Create Connection" message processors: + +```rust +pub fn keep( + keeper: &mut dyn ConnectionKeeper, + result: ConnectionMsgProcessingResult, +) -> Result<(), Error> +{ + keeper.store_connection(result.connection_id.clone(), result.connection_end)?; + keeper.add_connection_to_client(result.client_id, result.connection_id)?; + + Ok(()) +} +``` + +##### Submodule dispatcher + +> This section is very much a work in progress, as further investigation into what +> a production-ready implementation of the `ctx` parameter of the top-level dispatcher +> is required. As such, implementors should feel free to disregard the recommendations +> below, and are encouraged to come up with amendments to this ADR to better capture +> the actual requirements. + +Each submodule is responsible for dispatching the messages it is given to the appropriate +message processing function and, if successful, pass the resulting data to the persistance +function defined in the previous section. + +To this end, the submodule should define an enumeration of all messages, in order +for the top-level submodule dispatcher to forward them to the appropriate processor. +Such a definition for the ICS 003 Connection submodule is given below. + +```rust +pub enum ConnectionMsg { + ConnectionOpenInit(MsgConnectionOpenInit), + ConnectionOpenTry(MsgConnectionOpenTry), + ... +} +``` +The actual implementation of a submodule dispatcher is quite straightforward and unlikely to vary +much in substance between submodules. We give an implementation for the ICS 003 Connection module below. + +```rust +pub fn dispatch(ctx: &mut Ctx, msg: Msg) -> Result, Error> +where + Ctx: ConnectionReader + ConnectionKeeper, +{ + match msg { + Msg::ConnectionOpenInit(msg) => { + let HandlerOutput { + result, + log, + events, + } = connection_open_init::process(ctx, msg)?; + + connection::keep(ctx, result)?; + + Ok(HandlerOutput::builder() + .with_log(log) + .with_events(events) + .with_result(())) + } + + Msg::ConnectionOpenTry(msg) => // omitted + } +} +``` + +In essence, a top-level dispatcher is a function of a message wrapped in the enumeration introduced above, +and a "context" which implements both the `Reader` and `Keeper` interfaces. + +### Dealing with chain-specific datatypes + +The ICS 002 Client submodule stands out from the other submodules as it needs +to deal with chain-specific datatypes, such as `Header`, `ClientState`, and +`ConsensusState`. + +To abstract over chain-specific datatypes, we introduce a trait which specifies +both which types we need to abstract over, and their interface. + +For the ICS 002 Client submodule, this trait looks as follow: + +```rust +pub trait ClientDef { + type Header: Header; + type ClientState: ClientState; + type ConsensusState: ConsensusState; +} +``` + +The `ClientDef` trait specifies three datatypes, and their corresponding interface, which is provided +via a trait defined in the same submodule. + +A production implementation of this interface would instantiate these types with the concrete +types used by the chain, eg. Tendermint datatypes. Each concrete datatype must be provided +with a `From` instance to lift it into its corresponding `Any...` enumeration. + +For the purpose of unit-testing, a mock implementation of the `ClientDef` trait could look as follows: + +```rust +struct MockHeader(u32); + +impl Header for MockHeader { + // omitted +} + +impl From for AnyHeader { + fn from(mh: MockHeader) -> Self { + Self::Mock(mh) + } +} + +struct MockClientState(u32); + +impl ClientState for MockClientState { + // omitted +} + +impl From for AnyClientState { + fn from(mcs: MockClientState) -> Self { + Self::Mock(mcs) + } +} + +struct MockConsensusState(u32); + +impl ConsensusState for MockConsensusState { + // omitted +} + +impl From for AnyConsensusState { + fn from(mcs: MockConsensusState) -> Self { + Self::Mock(mcs) + } +} + +struct MockClient; + +impl ClientDef for MockClient { + type Header = MockHeader; + type ClientState = MockClientState; + type ConsensusState = MockConsensusState; +} +``` + +Since the actual type of client can only be determined at runtime, we cannot encode +the type of client within the message itself. + +Because of some limitations of the Rust type system, namely the lack of proper support +for existential types, it is currently impossible to define `Reader` and `Keeper` traits +which are agnostic to the actual type of client being used. + +We could alternatively model all chain-specific datatypes as boxed trait objects (`Box`), +but this approach runs into a lot of limitations of trait objects, such as the inability to easily +require such trait objects to be Clonable, or Serializable, or to define an equality relation on them. +Some support for such functionality can be found in third-party libraries, but the overall experience +for the developer is too subpar. + +We thus settle on a different strategy: lifting chain-specific data into an `enum` over all +possible chain types. + +For example, to model a chain-specific `Header` type, we would define an enumeration in the following +way: + +```rust +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // TODO: Add Eq +pub enum AnyHeader { + Mock(mocks::MockHeader), + Tendermint(tendermint::header::Header), +} + +impl Header for AnyHeader { + fn height(&self) -> Height { + match self { + Self::Mock(header) => header.height(), + Self::Tendermint(header) => header.height(), + } + } + + fn client_type(&self) -> ClientType { + match self { + Self::Mock(header) => header.client_type(), + Self::Tendermint(header) => header.client_type(), + } + } +} +``` + +This enumeration dispatches method calls to the underlying datatype at runtime, while +hiding the latter, and is thus akin to a proper existential type without running +into any limitations of the Rust type system (`impl Header` bounds not being allowed +everywhere, `Header` not being able to be treated as a trait objects because of `Clone`, +`PartialEq` and `Serialize`, `Deserialize` bounds, etc.) + +Other chain-specific datatypes, such as `ClientState` and `ConsensusState` require their own +enumeration over all possible implementations. + +On top of that, we also need to lift the specific client definitions (`ClientDef` instances), +into their own enumeration, as follows: + +```rust +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AnyClient { + Mock(mocks::MockClient), + Tendermint(tendermint::TendermintClient), +} + +impl ClientDef for AnyClient { + type Header = AnyHeader; + type ClientState = AnyClientState; + type ConsensusState = AnyConsensusState; +} +``` + +Messages can now be defined generically over the `ClientDef` instance: + + +```rust +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MsgCreateClient { + pub client_id: ClientId, + pub client_type: ClientType, + pub consensus_state: CD::ConsensusState, +} + +pub struct MsgUpdateClient { + pub client_id: ClientId, + pub header: CD::Header, +} +``` + +The `Keeper` and `Reader` traits are defined for any client: + +```rust +pub trait ClientReader { + fn client_type(&self, client_id: &ClientId) -> Option; + fn client_state(&self, client_id: &ClientId) -> Option; + fn consensus_state(&self, client_id: &ClientId, height: Height) -> Option; +} + +pub trait ClientKeeper { + fn store_client_type( + &mut self, + client_id: ClientId, + client_type: ClientType, + ) -> Result<(), Error>; + + fn store_client_state( + &mut self, + client_id: ClientId, + client_state: AnyClientState, + ) -> Result<(), Error>; + + fn store_consensus_state( + &mut self, + client_id: ClientId, + consensus_state: AnyConsensusState, + ) -> Result<(), Error>; +} +``` + +This way, only one implementation of the `ClientReader` and `ClientKeeper` trait is required, +as it can delegate eg. the serialization of the underlying datatypes to the `Serialize` bound +of the `Any...` wrappper. + +Both the `process` and `keep` function are defined to take a message generic over +the actual client type: + +```rust +pub fn process( + ctx: &dyn ClientReader, + msg: MsgCreateClient, +) -> HandlerResult, Error>; + +pub fn keep( + keeper: &mut dyn ClientKeeper, + result: CreateClientResult, +) -> Result<(), Error>; +``` + +Same for the top-level dispatcher: + +```rust +pub fn dispatch(ctx: &mut Ctx, msg: ClientMsg) -> Result, Error> +where + Ctx: ClientReader + ClientKeeper; +``` + +With this boilerplate out of way, one can write tests using a mock client, and associated mock datatypes +in a fairly straightforward way, taking advantage of the `From` instance to lift concerete mock datatypes +into the `Any...` enumeration: + +```rust + #[test] + fn test_create_client_ok() { + let client_id: ClientId = "mockclient".parse().unwrap(); + + let reader = MockClientReader { + client_id: client_id.clone(), + client_type: None, + client_state: None, + consensus_state: None, + }; + + let msg = MsgCreateClient { + client_id, + client_type: ClientType::Tendermint, + consensus_state: MockConsensusState(42).into(), // lift into `AnyConsensusState` + }; + + let output = process(&reader, msg.clone()); + + match output { + Ok(HandlerOutput { + result, + events, + log, + }) => { + // snip + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } +``` + +## Status + +Proposed + +## Consequences + +> This section describes the consequences, after applying the decision. All consequences should be summarized here, not just the "positive" ones. + +### Positive + +### Negative + +### Neutral + +## References + +> Are there any relevant PR comments, issues that led up to this, or articles refernced for why we made the given design choice? If so link them here! + +* {reference link} diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 71758f5d8b..04aff3f55c 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -29,6 +29,7 @@ serde_json = "1" tracing = "0.1.13" prost = "0.6.1" bytes = "0.5.5" +dyn-clonable = "0.9.0" [dev-dependencies] tokio = { version = "0.2", features = ["macros"] } diff --git a/modules/src/handler.rs b/modules/src/handler.rs new file mode 100644 index 0000000000..05448f412f --- /dev/null +++ b/modules/src/handler.rs @@ -0,0 +1,95 @@ +use std::marker::PhantomData; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Attribute { + key: String, + value: String, +} + +impl Attribute { + pub fn new(key: String, value: String) -> Self { + Self { key, value } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EventType { + Message, + Custom(String), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Event { + tpe: EventType, + attributes: Vec, +} + +impl Event { + pub fn new(tpe: EventType, attrs: Vec<(String, String)>) -> Self { + Self { + tpe, + attributes: attrs + .into_iter() + .map(|(k, v)| Attribute::new(k, v)) + .collect(), + } + } +} + +pub type HandlerResult = Result, E>; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HandlerOutput { + pub result: T, + pub log: Vec, + pub events: Vec, +} + +impl HandlerOutput { + pub fn builder() -> HandlerOutputBuilder { + HandlerOutputBuilder::new() + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct HandlerOutputBuilder { + log: Vec, + events: Vec, + marker: PhantomData, +} + +impl HandlerOutputBuilder { + pub fn new() -> Self { + Self { + log: vec![], + events: vec![], + marker: PhantomData, + } + } + + pub fn with_log(mut self, log: impl Into>) -> Self { + self.log.append(&mut log.into()); + self + } + + pub fn log(&mut self, log: impl Into) { + self.log.push(log.into()); + } + + pub fn with_events(mut self, events: impl Into>) -> Self { + self.events.append(&mut events.into()); + self + } + + pub fn emit(&mut self, event: impl Into) { + self.events.push(event.into()); + } + + pub fn with_result(self, result: T) -> HandlerOutput { + HandlerOutput { + result, + log: self.log, + events: self.events, + } + } +} diff --git a/modules/src/ics02_client/client_def.rs b/modules/src/ics02_client/client_def.rs new file mode 100644 index 0000000000..592281dbc0 --- /dev/null +++ b/modules/src/ics02_client/client_def.rs @@ -0,0 +1,110 @@ +use serde_derive::{Deserialize, Serialize}; + +use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::header::Header; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics23_commitment::CommitmentRoot; +use crate::Height; + +use crate::ics02_client::mocks; +use crate::ics07_tendermint as tendermint; +use crate::ics07_tendermint::client_def::TendermintClient; + +pub trait ClientDef: Clone { + type Header: Header; + type ClientState: ClientState; + type ConsensusState: ConsensusState; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // TODO: Add Eq +#[allow(clippy::large_enum_variant)] +pub enum AnyHeader { + Mock(mocks::MockHeader), + Tendermint(tendermint::header::Header), +} + +impl Header for AnyHeader { + fn client_type(&self) -> ClientType { + match self { + Self::Mock(header) => header.client_type(), + Self::Tendermint(header) => header.client_type(), + } + } + + fn height(&self) -> Height { + match self { + Self::Mock(header) => header.height(), + Self::Tendermint(header) => header.height(), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AnyClientState { + Mock(mocks::MockClientState), + Tendermint(crate::ics07_tendermint::client_state::ClientState), +} + +impl ClientState for AnyClientState { + fn chain_id(&self) -> String { + todo!() + } + + fn client_type(&self) -> ClientType { + todo!() + } + + fn get_latest_height(&self) -> Height { + match self { + AnyClientState::Tendermint(tm_state) => tm_state.get_latest_height(), + AnyClientState::Mock(mock_state) => mock_state.get_latest_height(), + } + } + + fn is_frozen(&self) -> bool { + todo!() + } + + fn verify_client_consensus_state( + &self, + _root: &CommitmentRoot, + ) -> Result<(), Box> { + todo!() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AnyConsensusState { + Mock(mocks::MockConsensusState), + Tendermint(crate::ics07_tendermint::consensus_state::ConsensusState), +} + +impl ConsensusState for AnyConsensusState { + fn client_type(&self) -> ClientType { + todo!() + } + + fn height(&self) -> Height { + todo!() + } + + fn root(&self) -> &CommitmentRoot { + todo!() + } + + fn validate_basic(&self) -> Result<(), Box> { + todo!() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AnyClient { + Mock(mocks::MockClient), + Tendermint(TendermintClient), +} + +impl ClientDef for AnyClient { + type Header = AnyHeader; + type ClientState = AnyClientState; + type ConsensusState = AnyConsensusState; +} diff --git a/modules/src/ics02_client/client_type.rs b/modules/src/ics02_client/client_type.rs index 9837514d76..a9d1e7ab60 100644 --- a/modules/src/ics02_client/client_type.rs +++ b/modules/src/ics02_client/client_type.rs @@ -38,8 +38,8 @@ mod tests { let client_type = ClientType::from_str("tendermint"); match client_type { - Ok(ClientType::Tendermint) => assert!(true), - Err(_) => assert!(false, "parse failed"), + Ok(ClientType::Tendermint) => (), + _ => panic!("parse failed"), } } @@ -52,7 +52,7 @@ mod tests { format!("{}", err), "unknown client type: some-random-client-type" ), - _ => assert!(false, "parse didn't fail"), + _ => panic!("parse didn't fail"), } } } diff --git a/modules/src/ics02_client/error.rs b/modules/src/ics02_client/error.rs index 8bca154a27..7c1acb0b4b 100644 --- a/modules/src/ics02_client/error.rs +++ b/modules/src/ics02_client/error.rs @@ -1,12 +1,27 @@ use anomaly::{BoxError, Context}; use thiserror::Error; +use crate::ics24_host::identifier::ClientId; +use crate::Height; + pub type Error = anomaly::Error; -#[derive(Clone, Debug, Error)] +#[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum Kind { #[error("unknown client type")] UnknownClientType, + + #[error("client already exists: {0}")] + ClientAlreadyExists(ClientId), + + #[error("client not found: {0}")] + ClientNotFound(ClientId), + + #[error("consensus state not found at: {0} at height {1}")] + ConsensusStateNotFound(ClientId, Height), + + #[error("implementation specific")] + ImplementationSpecific, } impl Kind { diff --git a/modules/src/ics02_client/events.rs b/modules/src/ics02_client/events.rs index d2c6b364b6..e2ef2eb2a4 100644 --- a/modules/src/ics02_client/events.rs +++ b/modules/src/ics02_client/events.rs @@ -9,6 +9,7 @@ use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; use tendermint::block; +/// NewBlock event signals the committing & execution of a new block. // TODO - find a better place for NewBlock #[derive(Debug, Deserialize, Serialize, Clone)] pub struct NewBlock { @@ -27,6 +28,7 @@ impl From for IBCEvent { } } +/// CreateClient event signals the creation of a new on-chain client (IBC client). #[derive(Debug, Deserialize, Serialize, Clone)] pub struct CreateClient { pub height: block::Height, @@ -51,6 +53,7 @@ impl From for IBCEvent { } } +/// UpdateClient event signals a recent update of an on-chain client (IBC Client). #[derive(Debug, Deserialize, Serialize, Clone)] pub struct UpdateClient { pub height: block::Height, @@ -75,6 +78,8 @@ impl From for IBCEvent { } } +/// ClientMisbehavior event signals the update of an on-chain client (IBC Client) with evidence of +/// misbehavior. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ClientMisbehavior { pub height: block::Height, diff --git a/modules/src/ics02_client/handler.rs b/modules/src/ics02_client/handler.rs new file mode 100644 index 0000000000..cb80a63919 --- /dev/null +++ b/modules/src/ics02_client/handler.rs @@ -0,0 +1,102 @@ +#![allow(unused_imports)] + +use crate::handler::{Event, EventType, HandlerOutput}; +use crate::ics02_client::client_def::{AnyClient, AnyClientState, AnyConsensusState, ClientDef}; +use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::error::Error; +use crate::ics02_client::msgs::{MsgCreateAnyClient, MsgUpdateAnyClient}; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics24_host::identifier::ClientId; + +use crate::Height; + +pub mod create_client; +pub mod update_client; + +pub trait ClientReader { + fn client_type(&self, client_id: &ClientId) -> Option; + fn client_state(&self, client_id: &ClientId) -> Option; + fn consensus_state(&self, client_id: &ClientId, height: Height) -> Option; +} + +pub trait ClientKeeper { + fn store_client_type( + &mut self, + client_id: ClientId, + client_type: ClientType, + ) -> Result<(), Error>; + + fn store_client_state( + &mut self, + client_id: ClientId, + client_state: AnyClientState, + ) -> Result<(), Error>; + + fn store_consensus_state( + &mut self, + client_id: ClientId, + consensus_state: AnyConsensusState, + ) -> Result<(), Error>; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ClientEvent { + ClientCreated(ClientId), + ClientUpdated(ClientId), +} + +impl From for Event { + fn from(ce: ClientEvent) -> Event { + match ce { + ClientEvent::ClientCreated(client_id) => Event::new( + EventType::Custom("ClientCreated".to_string()), + vec![("client_id".to_string(), client_id.to_string())], + ), + ClientEvent::ClientUpdated(client_id) => Event::new( + EventType::Custom("ClientUpdated".to_string()), + vec![("client_id".to_string(), client_id.to_string())], + ), + } + } +} + +pub enum ClientMsg { + CreateClient(MsgCreateAnyClient), + UpdateClient(MsgUpdateAnyClient), +} + +pub fn dispatch(ctx: &mut Ctx, msg: ClientMsg) -> Result, Error> +where + Ctx: ClientReader + ClientKeeper, +{ + match msg { + ClientMsg::CreateClient(msg) => { + let HandlerOutput { + result, + log, + events, + } = create_client::process(ctx, msg)?; + + create_client::keep(ctx, result)?; + + Ok(HandlerOutput::builder() + .with_log(log) + .with_events(events) + .with_result(())) + } + ClientMsg::UpdateClient(msg) => { + let HandlerOutput { + result, + log, + events, + } = update_client::process(ctx, msg)?; + + update_client::keep(ctx, result)?; + + Ok(HandlerOutput::builder() + .with_log(log) + .with_events(events) + .with_result(())) + } + } +} diff --git a/modules/src/ics02_client/handler/create_client.rs b/modules/src/ics02_client/handler/create_client.rs new file mode 100644 index 0000000000..45f19200b9 --- /dev/null +++ b/modules/src/ics02_client/handler/create_client.rs @@ -0,0 +1,235 @@ +#![allow(unreachable_code, unused_variables)] + +use crate::handler::{HandlerOutput, HandlerResult}; +use crate::ics02_client::client_def::{AnyClient, ClientDef}; +use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::error::{Error, Kind}; +use crate::ics02_client::handler::{ClientEvent, ClientKeeper, ClientReader}; +use crate::ics02_client::msgs::MsgCreateAnyClient; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics24_host::identifier::ClientId; +use std::time::Duration; + +#[derive(Debug)] +pub struct CreateClientResult { + client_id: ClientId, + client_type: ClientType, + client_state: CD::ClientState, + consensus_state: CD::ConsensusState, +} + +pub fn process( + ctx: &dyn ClientReader, + msg: MsgCreateAnyClient, +) -> HandlerResult, Error> { + let mut output = HandlerOutput::builder(); + + let MsgCreateAnyClient { + client_id, + client_type, + client_state, + consensus_state, + } = msg; + + if ctx.client_state(&client_id).is_some() { + return Err(Kind::ClientAlreadyExists(client_id).into()); + } + + output.log("success: no client state found"); + + if ctx.client_type(&client_id).is_some() { + return Err(Kind::ClientAlreadyExists(client_id).into()); + } + + output.log("success: no client type found"); + + output.emit(ClientEvent::ClientCreated(client_id.clone())); + + Ok(output.with_result(CreateClientResult { + client_id, + client_type, + client_state, + consensus_state, + })) +} + +pub fn keep( + keeper: &mut dyn ClientKeeper, + result: CreateClientResult, +) -> Result<(), Error> { + keeper.store_client_type(result.client_id.clone(), result.client_type)?; + keeper.store_client_state(result.client_id.clone(), result.client_state)?; + keeper.store_consensus_state(result.client_id, result.consensus_state)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics02_client::header::Header; + use crate::ics02_client::mocks::*; + use crate::ics02_client::state::{ClientState, ConsensusState}; + use crate::ics07_tendermint::client_def::TendermintClient; + use crate::ics07_tendermint::header::test_util::get_dummy_header; + use crate::ics07_tendermint::msgs::create_client::MsgCreateClient; + use crate::ics23_commitment::CommitmentRoot; + use crate::Height; + use std::str::FromStr; + use thiserror::Error; + + #[test] + fn test_create_client_ok() { + let client_id: ClientId = "mockclient".parse().unwrap(); + + let reader = MockClientReader { + client_id: client_id.clone(), + client_type: None, + client_state: None, + consensus_state: None, + }; + + let msg = MsgCreateAnyClient { + client_id, + client_type: ClientType::Tendermint, + client_state: MockClientState(42).into(), + consensus_state: MockConsensusState(42).into(), + }; + + let output = process(&reader, msg.clone()); + + match output { + Ok(HandlerOutput { + result, + events, + log, + }) => { + assert_eq!(result.client_type, ClientType::Tendermint); + assert_eq!( + events, + vec![ClientEvent::ClientCreated(msg.client_id).into()] + ); + assert_eq!( + log, + vec![ + "success: no client state found".to_string(), + "success: no client type found".to_string() + ] + ); + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } + + #[test] + fn test_create_client_existing_client_type() { + let client_id: ClientId = "mockclient".parse().unwrap(); + + let reader = MockClientReader { + client_id: client_id.clone(), + client_type: Some(ClientType::Tendermint), + client_state: None, + consensus_state: None, + }; + + let msg = MsgCreateAnyClient { + client_id, + client_type: ClientType::Tendermint, + client_state: MockClientState(42).into(), + consensus_state: MockConsensusState(42).into(), + }; + + let output = process(&reader, msg.clone()); + + if let Err(err) = output { + assert_eq!(err.kind(), &Kind::ClientAlreadyExists(msg.client_id)); + } else { + panic!("expected an error"); + } + } + + #[test] + fn test_create_client_existing_client_state() { + let client_id: ClientId = "mockclient".parse().unwrap(); + + let reader = MockClientReader { + client_id: client_id.clone(), + client_type: None, + client_state: Some(MockClientState(0)), + consensus_state: None, + }; + + let msg = MsgCreateAnyClient { + client_id, + client_type: ClientType::Tendermint, + client_state: MockClientState(42).into(), + consensus_state: MockConsensusState(42).into(), + }; + + let output = process(&reader, msg.clone()); + + if let Err(err) = output { + assert_eq!(err.kind(), &Kind::ClientAlreadyExists(msg.client_id)); + } else { + panic!("expected an error"); + } + } + #[test] + fn test_tm_create_client_ok() { + use tendermint::account::Id as AccountId; + + let client_id: ClientId = "tendermint".parse().unwrap(); + + let reader = MockClientReader { + client_id: client_id.clone(), + client_type: None, + client_state: None, + consensus_state: None, + }; + + let ics_msg = MsgCreateClient { + client_id, + header: get_dummy_header(), + trusting_period: Duration::from_secs(64000), + unbonding_period: Duration::from_secs(128000), + max_clock_drift: Duration::from_millis(3000), + signer: AccountId::from_str("7C2BB42A8BE69791EC763E51F5A49BCD41E82237").unwrap(), + }; + + //let msg = ics_msg.pre_process(); + let msg = MsgCreateAnyClient { + client_id: ics_msg.client_id().clone(), + client_type: ics_msg.client_type(), + client_state: ics_msg.client_state(), + consensus_state: ics_msg.consensus_state(), + }; + + let output = process(&reader, msg.clone()); + + match output { + Ok(HandlerOutput { + result, + events, + log, + }) => { + assert_eq!(result.client_type, ClientType::Tendermint); + assert_eq!( + events, + vec![ClientEvent::ClientCreated(msg.client_id).into()] + ); + assert_eq!( + log, + vec![ + "success: no client state found".to_string(), + "success: no client type found".to_string() + ] + ); + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } +} diff --git a/modules/src/ics02_client/handler/update_client.rs b/modules/src/ics02_client/handler/update_client.rs new file mode 100644 index 0000000000..d548bfed9e --- /dev/null +++ b/modules/src/ics02_client/handler/update_client.rs @@ -0,0 +1,110 @@ +#![allow(unreachable_code, unused_variables)] + +use crate::handler::{HandlerOutput, HandlerResult}; +use crate::ics02_client::client_def::{AnyClient, ClientDef}; +use crate::ics02_client::error::{Error, Kind}; +use crate::ics02_client::handler::{ClientEvent, ClientKeeper, ClientReader}; +use crate::ics02_client::msgs::MsgUpdateAnyClient; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics24_host::identifier::ClientId; + +#[derive(Debug)] +pub struct UpdateClientResult { + client_id: ClientId, + client_state: CD::ClientState, + consensus_state: CD::ConsensusState, +} + +pub fn process( + ctx: &dyn ClientReader, + msg: MsgUpdateAnyClient, +) -> HandlerResult, Error> { + let mut output = HandlerOutput::builder(); + + let MsgUpdateAnyClient { client_id, header } = msg; + + let client_type = ctx + .client_type(&client_id) + .ok_or_else(|| Kind::ClientNotFound(client_id.clone()))?; + + let client_state = ctx + .client_state(&client_id) + .ok_or_else(|| Kind::ClientNotFound(client_id.clone()))?; + + let latest_height = client_state.get_latest_height(); + let consensus_state = ctx + .consensus_state(&client_id, latest_height) + .ok_or_else(|| Kind::ConsensusStateNotFound(client_id.clone(), latest_height))?; + + // Use client_state to validate the new header against the latest consensus_state. + // This function will return the new client_state (its latest_height changed) and a + // consensus_state obtained from header. These will be later persisted by the keeper. + // FIXME + // (new_client_state, new_consensus_state) = + // CD::check_validity_and_update_state(client_state, consensus_state, &header)?; + + output.emit(ClientEvent::ClientUpdated(client_id.clone())); + + Ok(output.with_result(UpdateClientResult { + client_id, + client_state, // new_client_state + consensus_state, // new_consensus_state + })) +} + +pub fn keep( + keeper: &mut dyn ClientKeeper, + result: UpdateClientResult, +) -> Result<(), Error> { + keeper.store_client_state(result.client_id.clone(), result.client_state)?; + keeper.store_consensus_state(result.client_id, result.consensus_state)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ics02_client::client_type::ClientType; + use crate::ics02_client::header::Header; + use crate::ics02_client::mocks::*; + use crate::ics02_client::state::{ClientState, ConsensusState}; + use crate::ics23_commitment::CommitmentRoot; + use crate::Height; + use thiserror::Error; + + #[test] + fn test_update_client_ok() { + let mock = MockClientReader { + client_id: "mockclient".parse().unwrap(), + client_type: Some(ClientType::Tendermint), + client_state: MockClientState(42).into(), + consensus_state: MockConsensusState(42).into(), + }; + + let msg = MsgUpdateAnyClient { + client_id: "mockclient".parse().unwrap(), + header: MockHeader(46).into(), + }; + + let output = process(&mock, msg.clone()); + + match output { + Ok(HandlerOutput { + result: _, + events, + log, + }) => { + // assert_eq!(result.client_state, MockClientState(0)); + assert_eq!( + events, + vec![ClientEvent::ClientUpdated(msg.client_id).into()] + ); + assert!(log.is_empty()); + } + Err(err) => { + panic!("unexpected error: {}", err); + } + } + } +} diff --git a/modules/src/ics02_client/header.rs b/modules/src/ics02_client/header.rs index bc54fd2aa6..255b70ed52 100644 --- a/modules/src/ics02_client/header.rs +++ b/modules/src/ics02_client/header.rs @@ -2,7 +2,8 @@ use super::client_type::ClientType; use crate::Height; /// Abstract of consensus state update information -pub trait Header { +#[dyn_clonable::clonable] +pub trait Header: Clone + std::fmt::Debug { /// The type of client (eg. Tendermint) fn client_type(&self) -> ClientType; diff --git a/modules/src/ics02_client/mocks.rs b/modules/src/ics02_client/mocks.rs new file mode 100644 index 0000000000..1900fc379f --- /dev/null +++ b/modules/src/ics02_client/mocks.rs @@ -0,0 +1,185 @@ +use crate::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader, ClientDef}; +use crate::ics02_client::client_type::ClientType; +use crate::ics02_client::error::Error; +use crate::ics02_client::handler::{ClientKeeper, ClientReader}; +use crate::ics02_client::header::Header; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics23_commitment::CommitmentRoot; +use crate::ics24_host::identifier::ClientId; +use crate::Height; + +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MockError {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MockHeader(pub u32); + +impl From for AnyHeader { + fn from(mh: MockHeader) -> Self { + Self::Mock(mh) + } +} + +impl Header for MockHeader { + fn client_type(&self) -> ClientType { + todo!() + } + + fn height(&self) -> Height { + todo!() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MockClientState(pub u32); + +impl From for AnyClientState { + fn from(mcs: MockClientState) -> Self { + Self::Mock(mcs) + } +} + +impl ClientState for MockClientState { + fn chain_id(&self) -> String { + todo!() + } + + fn client_type(&self) -> ClientType { + todo!() + } + + fn get_latest_height(&self) -> Height { + Height::from(self.0 as u64) + } + + fn is_frozen(&self) -> bool { + todo!() + } + + fn verify_client_consensus_state( + &self, + _root: &CommitmentRoot, + ) -> Result<(), Box> { + todo!() + } +} + +impl From for MockClientState { + fn from(cs: MockConsensusState) -> Self { + Self(cs.0) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct MockConsensusState(pub u32); + +impl From for AnyConsensusState { + fn from(mcs: MockConsensusState) -> Self { + Self::Mock(mcs) + } +} + +impl ConsensusState for MockConsensusState { + fn client_type(&self) -> ClientType { + todo!() + } + + fn height(&self) -> Height { + todo!() + } + + fn root(&self) -> &CommitmentRoot { + todo!() + } + + fn validate_basic(&self) -> Result<(), Box> { + todo!() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MockClient; + +impl ClientDef for MockClient { + type Header = MockHeader; + type ClientState = MockClientState; + type ConsensusState = MockConsensusState; +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MockClientContext { + reader: MockClientReader, + keeper: MockClientKeeper, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MockClientReader { + pub client_id: ClientId, + pub client_state: Option, + pub client_type: Option, + pub consensus_state: Option, +} + +impl ClientReader for MockClientReader { + fn client_type(&self, client_id: &ClientId) -> Option { + if client_id == &self.client_id { + self.client_type.clone() + } else { + None + } + } + + #[allow(trivial_casts)] + fn client_state(&self, client_id: &ClientId) -> Option { + if client_id == &self.client_id { + self.client_state.map(Into::into) + } else { + None + } + } + + #[allow(trivial_casts)] + fn consensus_state(&self, client_id: &ClientId, _height: Height) -> Option { + if client_id == &self.client_id { + self.consensus_state.map(Into::into) + } else { + None + } + } +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct MockClientKeeper { + pub client_state: Option, + pub client_type: Option, + pub consensus_state: Option, +} + +impl ClientKeeper for MockClientKeeper { + fn store_client_type( + &mut self, + _client_id: ClientId, + _client_type: ClientType, + ) -> Result<(), Error> { + todo!() + } + + fn store_client_state( + &mut self, + _client_id: ClientId, + _client_state: AnyClientState, + ) -> Result<(), Error> { + todo!() + } + + fn store_consensus_state( + &mut self, + _client_id: ClientId, + _consensus_state: AnyConsensusState, + ) -> Result<(), Error> { + todo!() + } +} diff --git a/modules/src/ics02_client/mod.rs b/modules/src/ics02_client/mod.rs index 81ad985e35..9f5caea073 100644 --- a/modules/src/ics02_client/mod.rs +++ b/modules/src/ics02_client/mod.rs @@ -1,9 +1,12 @@ //! ICS 02: IBC Client implementation -pub mod client; +pub mod client_def; pub mod client_type; pub mod error; pub mod events; +pub mod handler; pub mod header; +pub mod mocks; pub mod msgs; +pub mod raw; pub mod state; diff --git a/modules/src/ics02_client/msgs.rs b/modules/src/ics02_client/msgs.rs index a25d49ba4d..6ba0bda9be 100644 --- a/modules/src/ics02_client/msgs.rs +++ b/modules/src/ics02_client/msgs.rs @@ -1,25 +1,25 @@ -use super::client_type::ClientType; -use super::header::Header; -use super::state::ConsensusState; +//! These are definitions of messages that a relayer submits to a chain. Specific implementations of +//! these messages can be found, for instance, in ICS 07 for Tendermint-specific chains. A chain +//! handles these messages in two layers: first with the general ICS 02 client handler, which +//! subsequently calls into the chain-specific (e.g., ICS 07) client handler. See: +//! https://github.com/cosmos/ics/tree/master/spec/ics-002-client-semantics#create. + +use crate::ics02_client::client_def::ClientDef; +use crate::ics02_client::client_type::ClientType; use crate::ics24_host::identifier::ClientId; -use crate::tx_msg::Msg; -pub trait MsgUpdateClient -where - Self: Msg, -{ - type Header: Header; - fn client_id(&self) -> &ClientId; - fn header(&self) -> &Self::Header; +/// A type of message that triggers the creation of a new on-chain (IBC) client. +#[derive(Clone, Debug)] +pub struct MsgCreateAnyClient { + pub client_id: ClientId, + pub client_type: ClientType, + pub client_state: CD::ClientState, + pub consensus_state: CD::ConsensusState, } -pub trait MsgCreateClient -where - Self: Msg, -{ - type ConsensusState: ConsensusState; - - fn client_id(&self) -> &ClientId; - fn client_type(&self) -> ClientType; - fn consensus_state(&self) -> Self::ConsensusState; +/// A type of message that triggers the update of an on-chain (IBC) client with new headers. +#[derive(Clone, Debug)] +pub struct MsgUpdateAnyClient { + pub client_id: ClientId, + pub header: CD::Header, } diff --git a/modules/src/ics02_client/client.rs b/modules/src/ics02_client/raw.rs similarity index 100% rename from modules/src/ics02_client/client.rs rename to modules/src/ics02_client/raw.rs diff --git a/modules/src/ics02_client/state.rs b/modules/src/ics02_client/state.rs index d441112fcf..eede4f6ec1 100644 --- a/modules/src/ics02_client/state.rs +++ b/modules/src/ics02_client/state.rs @@ -1,12 +1,9 @@ use super::client_type::ClientType; - use crate::ics23_commitment::CommitmentRoot; -use crate::ics24_host::identifier::ClientId; use crate::Height; -pub trait ConsensusState { - type ValidationError: std::error::Error; - +#[dyn_clonable::clonable] +pub trait ConsensusState: Clone + std::fmt::Debug { /// Type of client associated with this consensus state (eg. Tendermint) fn client_type(&self) -> ClientType; @@ -17,14 +14,13 @@ pub trait ConsensusState { fn root(&self) -> &CommitmentRoot; /// Performs basic validation of the consensus state - fn validate_basic(&self) -> Result<(), Self::ValidationError>; + fn validate_basic(&self) -> Result<(), Box>; } -pub trait ClientState { - type ValidationError: std::error::Error; - +#[dyn_clonable::clonable] +pub trait ClientState: Clone + std::fmt::Debug { /// Client ID of this state - fn client_id(&self) -> ClientId; + fn chain_id(&self) -> String; /// Type of client associated with this state (eg. Tendermint) fn client_type(&self) -> ClientType; @@ -35,9 +31,11 @@ pub trait ClientState { /// Freeze status of the client fn is_frozen(&self) -> bool; - // TODO: It's unclear what this function is expected to achieve. Document this. + /// Verifies a proof of the consensus state of the specified client stored on the target machine. + /// FIXME: Definition is incomplete. + /// See https://github.com/cosmos/ics/tree/master/spec/ics-002-client-semantics#required-functions fn verify_client_consensus_state( &self, root: &CommitmentRoot, - ) -> Result<(), Self::ValidationError>; + ) -> Result<(), Box>; } diff --git a/modules/src/ics03_connection/msgs.rs b/modules/src/ics03_connection/msgs.rs index adeccf1ba3..1a5192e8b0 100644 --- a/modules/src/ics03_connection/msgs.rs +++ b/modules/src/ics03_connection/msgs.rs @@ -338,7 +338,7 @@ mod tests { counterparty_connection_id: "abcdefghijksdffjssdkflweldflsfladfsfwjkrekcmmsdfsdfjflddmnopqrstu" .to_string(), - ..default_con_params.clone() + ..default_con_params }, want_pass: false, }, @@ -484,7 +484,7 @@ mod tests { name: "Empty proof".to_string(), params: ConOpenTryParams { proof_init: CommitmentProof { ops: vec![] }, - ..default_con_params.clone() + ..default_con_params }, want_pass: false, }, @@ -584,7 +584,7 @@ mod tests { name: "Bad consensus height, height is 0".to_string(), params: ConOpenAckParams { consensus_height: 0, - ..default_con_params.clone() + ..default_con_params }, want_pass: false, }, @@ -658,7 +658,7 @@ mod tests { name: "Bad proof height, height is 0".to_string(), params: ConOpenConfirmParams { proof_height: 0, - ..default_con_params.clone() + ..default_con_params }, want_pass: false, }, diff --git a/modules/src/ics04_channel/channel.rs b/modules/src/ics04_channel/channel.rs index df7c633dff..321f5e7605 100644 --- a/modules/src/ics04_channel/channel.rs +++ b/modules/src/ics04_channel/channel.rs @@ -259,7 +259,7 @@ mod tests { let tests: Vec = vec![ Test { name: "Raw channel end with missing counterparty".to_string(), - params: empty_raw_channel_end.clone(), + params: empty_raw_channel_end, want_pass: false, }, Test { @@ -307,7 +307,7 @@ mod tests { }, Test { name: "Raw channel end with correct params".to_string(), - params: raw_channel_end.clone(), + params: raw_channel_end, want_pass: true, }, ] diff --git a/modules/src/ics04_channel/msgs.rs b/modules/src/ics04_channel/msgs.rs index 11229d3099..6b5f4d8b2b 100644 --- a/modules/src/ics04_channel/msgs.rs +++ b/modules/src/ics04_channel/msgs.rs @@ -662,7 +662,7 @@ mod tests { name: "Bad connection hops (conn id too short, must be 10 chars)".to_string(), params: OpenInitParams { connection_hops: vec!["conn124".to_string()].into_iter().collect(), - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -866,7 +866,7 @@ mod tests { name: "Correct counterparty channel identifier".to_string(), params: OpenTryParams { counterparty_channel_id: "channelid34".to_string(), - ..default_params.clone() + ..default_params }, want_pass: true, }, @@ -996,7 +996,7 @@ mod tests { name: "Bad proof height, height = 0".to_string(), params: OpenAckParams { proof_height: 0, - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -1111,7 +1111,7 @@ mod tests { name: "Bad proof height, height = 0".to_string(), params: OpenConfirmParams { proof_height: 0, - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -1213,7 +1213,7 @@ mod tests { name: "Bad channel, name too long".to_string(), params: CloseInitParams { channel_id: "abcdeasdfasdfasdfasdfasdfasdfasdfasdfdgasdfasdfasdfghijklmnopqrstu".to_string(), - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -1325,7 +1325,7 @@ mod tests { name: "Bad proof height, height = 0".to_string(), params: CloseConfirmParams { proof_height: 0, - ..default_params.clone() + ..default_params }, want_pass: false, }, diff --git a/modules/src/ics07_tendermint/client_def.rs b/modules/src/ics07_tendermint/client_def.rs new file mode 100644 index 0000000000..c7611315be --- /dev/null +++ b/modules/src/ics07_tendermint/client_def.rs @@ -0,0 +1,13 @@ +use crate::ics02_client::client_def::ClientDef; +use crate::ics07_tendermint::client_state::ClientState; +use crate::ics07_tendermint::consensus_state::ConsensusState; +use crate::ics07_tendermint::header::Header; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TendermintClient; + +impl ClientDef for TendermintClient { + type Header = Header; + type ClientState = ClientState; + type ConsensusState = ConsensusState; +} diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs index fa5f3fa6d5..e223867dea 100644 --- a/modules/src/ics07_tendermint/client_state.rs +++ b/modules/src/ics07_tendermint/client_state.rs @@ -1,29 +1,34 @@ use crate::ics02_client::client_type::ClientType; +use crate::ics07_tendermint::consensus_state::ConsensusState; +use crate::ics07_tendermint::error::{Error, Kind}; use crate::ics23_commitment::CommitmentRoot; -use crate::ics07_tendermint::error::{Error, Kind}; -use crate::ics07_tendermint::header::Header; -use crate::ics24_host::identifier::ClientId; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; -use tendermint::lite::Header as liteHeader; +use tendermint::block::Height; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ClientState { - id: ClientId, - trusting_period: Duration, - unbonding_period: Duration, - frozen_height: crate::Height, - latest_header: Header, + pub chain_id: String, + // pub trust_level: TrustLevel, + pub trusting_period: Duration, + pub unbonding_period: Duration, + pub max_clock_drift: Duration, + pub latest_height: crate::Height, + pub frozen_height: crate::Height, + //pub proof_specs: Specs } impl ClientState { pub fn new( - id: String, + chain_id: String, + // trust_level: TrustLevel, trusting_period: Duration, unbonding_period: Duration, - latest_header: Header, + max_clock_drift: Duration, + latest_height: crate::Height, frozen_height: crate::Height, + // proof_specs: Specs ) -> Result { // Basic validation of trusting period and unbonding period: each should be non-zero. if trusting_period <= Duration::new(0, 0) { @@ -43,32 +48,40 @@ impl ClientState { } // Basic validation for the frozen_height parameter. - if frozen_height != 0 { + if frozen_height != Height(0) { return Err(Kind::ValidationError .context("ClientState cannot be frozen at creation time") .into()); } - // Initially, no validation is needed for the `latest_header`. This has to be validated - // upon updating a client (see `update_client.rs` and fn - // `ClientState::verify_client_consensus_state`). + // Basic validation for the frozen_height parameter. + if latest_height <= Height(0) { + return Err(Kind::ValidationError + .context("ClientState latest height cannot be smaller than zero") + .into()); + } Ok(Self { // TODO: Consider adding a specific 'IdentifierError' Kind, akin to the one in ICS04. - id: id.parse().map_err(|e| Kind::ValidationError.context(e))?, + chain_id, trusting_period, unbonding_period, - latest_header, + max_clock_drift, frozen_height, + latest_height, }) } } -impl crate::ics02_client::state::ClientState for ClientState { - type ValidationError = Error; +impl From for ClientState { + fn from(_: ConsensusState) -> Self { + todo!() + } +} - fn client_id(&self) -> ClientId { - self.id.clone() +impl crate::ics02_client::state::ClientState for ClientState { + fn chain_id(&self) -> String { + self.chain_id.clone() } fn client_type(&self) -> ClientType { @@ -76,18 +89,18 @@ impl crate::ics02_client::state::ClientState for ClientState { } fn get_latest_height(&self) -> crate::Height { - self.latest_header.signed_header.header.height() + self.latest_height } fn is_frozen(&self) -> bool { // If 'frozen_height' is set to a non-zero value, then the client state is frozen. - self.frozen_height != 0 + self.frozen_height != Height(0) } fn verify_client_consensus_state( &self, _root: &CommitmentRoot, - ) -> Result<(), Self::ValidationError> { + ) -> Result<(), Box> { unimplemented!() } } @@ -95,10 +108,9 @@ impl crate::ics02_client::state::ClientState for ClientState { #[cfg(test)] mod tests { use crate::ics07_tendermint::client_state::ClientState; - use crate::ics07_tendermint::header::test_util::get_dummy_header; - use crate::ics07_tendermint::header::Header; use crate::test::test_serialization_roundtrip; use std::time::Duration; + use tendermint::block::Height; use tendermint_rpc::endpoint::abci_query::AbciQuery; #[test] @@ -123,17 +135,19 @@ mod tests { id: String, trusting_period: Duration, unbonding_period: Duration, - latest_header: Header, + max_clock_drift: Duration, + latest_height: crate::Height, frozen_height: crate::Height, } // Define a "default" set of parameters to reuse throughout these tests. let default_params: ClientStateParams = ClientStateParams { - id: "abcdefghijkl".to_string(), + id: "thisisthechainid".to_string(), trusting_period: Duration::from_secs(64000), unbonding_period: Duration::from_secs(128000), - latest_header: get_dummy_header(), - frozen_height: 0, + max_clock_drift: Duration::from_millis(3000), + latest_height: Height(10), + frozen_height: Height(0), }; struct Test { @@ -148,18 +162,10 @@ mod tests { params: default_params.clone(), want_pass: true, }, - Test { - name: "Invalid client id".to_string(), - params: ClientStateParams { - id: "9000".to_string(), - ..default_params.clone() - }, - want_pass: false, - }, Test { name: "Invalid frozen height parameter (should be 0)".to_string(), params: ClientStateParams { - frozen_height: 1, + frozen_height: Height(1), ..default_params.clone() }, want_pass: false, @@ -185,7 +191,7 @@ mod tests { params: ClientStateParams { trusting_period: Duration::from_secs(11), unbonding_period: Duration::from_secs(10), - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -200,7 +206,8 @@ mod tests { p.id, p.trusting_period, p.unbonding_period, - p.latest_header, + p.max_clock_drift, + p.latest_height, p.frozen_height, ); diff --git a/modules/src/ics07_tendermint/consensus_state.rs b/modules/src/ics07_tendermint/consensus_state.rs index dd4ba0ab80..1b6d0ab8bb 100644 --- a/modules/src/ics07_tendermint/consensus_state.rs +++ b/modules/src/ics07_tendermint/consensus_state.rs @@ -2,13 +2,14 @@ use crate::ics02_client::client_type::ClientType; use crate::ics23_commitment::CommitmentRoot; use serde_derive::{Deserialize, Serialize}; +use tendermint::Hash; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ConsensusState { - root: CommitmentRoot, - height: crate::Height, - timestamp: tendermint::time::Time, - validator_set: tendermint::validator::Set, + pub height: crate::Height, + pub timestamp: tendermint::time::Time, + pub root: CommitmentRoot, + pub next_validators_hash: Hash, } impl ConsensusState { @@ -16,20 +17,18 @@ impl ConsensusState { root: CommitmentRoot, height: crate::Height, timestamp: tendermint::time::Time, - validator_set: tendermint::validator::Set, + next_validators_hash: Hash, ) -> Self { Self { root, height, timestamp, - validator_set, + next_validators_hash, } } } impl crate::ics02_client::state::ConsensusState for ConsensusState { - type ValidationError = crate::ics07_tendermint::error::Error; - fn client_type(&self) -> ClientType { ClientType::Tendermint } @@ -42,7 +41,7 @@ impl crate::ics02_client::state::ConsensusState for ConsensusState { &self.root } - fn validate_basic(&self) -> Result<(), Self::ValidationError> { + fn validate_basic(&self) -> Result<(), Box> { unimplemented!() } } diff --git a/modules/src/ics07_tendermint/header.rs b/modules/src/ics07_tendermint/header.rs index ffb1085ca1..d86d13de67 100644 --- a/modules/src/ics07_tendermint/header.rs +++ b/modules/src/ics07_tendermint/header.rs @@ -5,14 +5,28 @@ use tendermint::block::signed_header::SignedHeader; use tendermint::validator::Set as ValidatorSet; use crate::ics02_client::client_type::ClientType; +use crate::ics07_tendermint::consensus_state::ConsensusState; +use crate::ics23_commitment::CommitmentRoot; use crate::Height; /// Tendermint consensus header #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Header { - pub signed_header: SignedHeader, - pub validator_set: ValidatorSet, - pub next_validator_set: ValidatorSet, + pub signed_header: SignedHeader, // contains the commitment root + pub validator_set: ValidatorSet, // the validator set that signed Header + pub trusted_height: Height, // the height of a trusted header seen by client less than or equal to Header + pub trusted_validator_set: ValidatorSet, // the last trusted validator set at trusted height +} + +impl Header { + pub(crate) fn consensus_state(&self) -> ConsensusState { + ConsensusState { + height: self.signed_header.header.height, + timestamp: self.signed_header.header.time, + root: CommitmentRoot::from_bytes(&self.signed_header.header.app_hash), + next_validators_hash: self.signed_header.header.next_validators_hash, + } + } } impl crate::ics02_client::header::Header for Header { @@ -21,9 +35,12 @@ impl crate::ics02_client::header::Header for Header { } fn height(&self) -> Height { - use tendermint::lite::types::Header; - self.signed_header.header.height() + self.signed_header.header.height } + + // fn consensus_state(&self) -> &dyn crate::ics02_client::state::ConsensusState { + // &self.consensus_state() + // } } #[cfg(test)] @@ -31,6 +48,7 @@ pub mod test_util { use crate::ics07_tendermint::header::Header; use subtle_encoding::hex; use tendermint::block::signed_header::SignedHeader; + use tendermint::block::Height; use tendermint::validator::Info as ValidatorInfo; use tendermint::validator::Set as ValidatorSet; use tendermint::{vote, PublicKey}; @@ -64,7 +82,8 @@ pub mod test_util { Header { signed_header: shdr, validator_set: vs.clone(), - next_validator_set: vs.clone(), + trusted_height: Height(9), + trusted_validator_set: vs, } } } diff --git a/modules/src/ics07_tendermint/mod.rs b/modules/src/ics07_tendermint/mod.rs index d384cb4bc0..c2b0fc503e 100644 --- a/modules/src/ics07_tendermint/mod.rs +++ b/modules/src/ics07_tendermint/mod.rs @@ -1,5 +1,6 @@ //! ICS 07: Tendermint Client +pub mod client_def; pub mod client_state; pub mod consensus_state; pub mod error; diff --git a/modules/src/ics07_tendermint/msgs/create_client.rs b/modules/src/ics07_tendermint/msgs/create_client.rs index 53e8935ad7..94489ef169 100644 --- a/modules/src/ics07_tendermint/msgs/create_client.rs +++ b/modules/src/ics07_tendermint/msgs/create_client.rs @@ -1,12 +1,12 @@ -use crate::ics02_client::client_type::ClientType; -use crate::ics07_tendermint::consensus_state::ConsensusState; use crate::ics07_tendermint::header::Header; -use crate::ics23_commitment::CommitmentRoot; use crate::ics24_host::identifier::ClientId; use crate::tx_msg::Msg; use std::time::Duration; +use crate::ics02_client::client_def::{AnyClientState, AnyConsensusState}; +use crate::ics02_client::client_type::ClientType; +use crate::ics07_tendermint::client_state::ClientState; use serde_derive::{Deserialize, Serialize}; use tendermint::account::Id as AccountId; @@ -14,36 +14,58 @@ pub const TYPE_MSG_CREATE_CLIENT: &str = "create_client"; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct MsgCreateClient { - client_id: ClientId, - header: Header, - trusting_period: Duration, - bonding_period: Duration, - signer: AccountId, + pub client_id: ClientId, + pub header: Header, + // trust_level: Fraction, + pub trusting_period: Duration, + pub unbonding_period: Duration, + pub max_clock_drift: Duration, + // proof_specs: ProofSpecs, + pub signer: AccountId, } impl MsgCreateClient { pub fn new( client_id: ClientId, header: Header, + // trust_level: Fraction, trusting_period: Duration, - bonding_period: Duration, + unbonding_period: Duration, + max_clock_drift: Duration, + // proof_specs: ProofSpecs, signer: AccountId, ) -> Self { Self { client_id, header, trusting_period, - bonding_period, + unbonding_period, + max_clock_drift, signer, } } - fn get_client_id(&self) -> &ClientId { + pub(crate) fn client_id(&self) -> &ClientId { &self.client_id } - fn get_header(&self) -> &Header { - &self.header + pub(crate) fn client_type(&self) -> ClientType { + ClientType::Tendermint + } + + pub(crate) fn consensus_state(&self) -> AnyConsensusState { + AnyConsensusState::Tendermint(self.header.consensus_state()) + } + + pub(crate) fn client_state(&self) -> AnyClientState { + AnyClientState::Tendermint(ClientState { + chain_id: self.header.signed_header.header.chain_id.to_string(), + trusting_period: self.trusting_period, + unbonding_period: self.unbonding_period, + max_clock_drift: self.max_clock_drift, + latest_height: self.header.signed_header.header.height, + frozen_height: 0.into(), + }) } } @@ -71,27 +93,3 @@ impl Msg for MsgCreateClient { vec![self.signer] } } - -impl crate::ics02_client::msgs::MsgCreateClient for MsgCreateClient { - type ConsensusState = ConsensusState; - - fn client_id(&self) -> &ClientId { - &self.client_id - } - - fn client_type(&self) -> ClientType { - ClientType::Tendermint - } - - fn consensus_state(&self) -> Self::ConsensusState { - let root = CommitmentRoot; // TODO - let header = &self.header.signed_header.header; - - ConsensusState::new( - root, - header.height.into(), - header.time, - self.header.validator_set.clone(), - ) - } -} diff --git a/modules/src/ics07_tendermint/msgs/update_client.rs b/modules/src/ics07_tendermint/msgs/update_client.rs index 5b5cb1350a..597e4d88f0 100644 --- a/modules/src/ics07_tendermint/msgs/update_client.rs +++ b/modules/src/ics07_tendermint/msgs/update_client.rs @@ -2,6 +2,7 @@ use crate::ics07_tendermint::header::Header; use crate::ics24_host::identifier::ClientId; use crate::tx_msg::Msg; +use crate::ics07_tendermint::consensus_state::ConsensusState; use serde_derive::{Deserialize, Serialize}; use tendermint::account::Id as AccountId; @@ -23,13 +24,17 @@ impl MsgUpdateClient { } } - fn get_client_id(&self) -> &ClientId { + fn client_id(&self) -> &ClientId { &self.client_id } - fn get_header(&self) -> &Header { + fn header(&self) -> &Header { &self.header } + + fn consensus_state(&self) -> ConsensusState { + self.header.consensus_state() + } } impl Msg for MsgUpdateClient { @@ -56,15 +61,3 @@ impl Msg for MsgUpdateClient { vec![self.signer] } } - -impl crate::ics02_client::msgs::MsgUpdateClient for MsgUpdateClient { - type Header = Header; - - fn client_id(&self) -> &ClientId { - &self.client_id - } - - fn header(&self) -> &Self::Header { - &self.header - } -} diff --git a/modules/src/ics23_commitment/mod.rs b/modules/src/ics23_commitment/mod.rs index 23251942ff..ced4f91e13 100644 --- a/modules/src/ics23_commitment/mod.rs +++ b/modules/src/ics23_commitment/mod.rs @@ -5,6 +5,12 @@ use tendermint::merkle::proof::Proof; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CommitmentRoot; +impl CommitmentRoot { + pub fn from_bytes(_bytes: &[u8]) -> Self { + // TODO + CommitmentRoot {} + } +} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CommitmentPath; diff --git a/modules/src/lib.rs b/modules/src/lib.rs index eef337d78d..618dda4c8e 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -20,6 +20,7 @@ //! - ICS 24: Host Requirements pub mod events; +pub mod handler; pub mod ics02_client; pub mod ics03_connection; pub mod ics04_channel; @@ -33,7 +34,7 @@ pub mod try_from_raw; pub mod tx_msg; /// Height of a block, same as in `tendermint` crate -pub type Height = tendermint::lite::Height; +pub type Height = tendermint::block::Height; #[cfg(test)] mod test; diff --git a/relayer-cli/src/commands/query/channel.rs b/relayer-cli/src/commands/query/channel.rs index 7c26452aaa..71b4609258 100644 --- a/relayer-cli/src/commands/query/channel.rs +++ b/relayer-cli/src/commands/query/channel.rs @@ -189,7 +189,7 @@ mod tests { name: "Bad channel, name too short".to_string(), params: QueryChannelEndCmd { channel_id: Some("chshort".to_string()), - ..default_params.clone() + ..default_params }, want_pass: false, }, diff --git a/relayer-cli/src/commands/query/client.rs b/relayer-cli/src/commands/query/client.rs index a951b6ba80..80aee8facc 100644 --- a/relayer-cli/src/commands/query/client.rs +++ b/relayer-cli/src/commands/query/client.rs @@ -348,7 +348,7 @@ mod tests { name: "No client id specified".to_string(), params: QueryClientStateCmd { client_id: None, - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -356,7 +356,7 @@ mod tests { name: "Bad client id, non-alpha".to_string(), params: QueryClientStateCmd { client_id: Some("p34".to_string()), - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -434,7 +434,7 @@ mod tests { name: "No client id specified".to_string(), params: QueryClientConnectionsCmd { client_id: None, - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -442,7 +442,7 @@ mod tests { name: "Bad client id, non-alpha".to_string(), params: QueryClientConnectionsCmd { client_id: Some("p34".to_string()), - ..default_params.clone() + ..default_params }, want_pass: false, }, diff --git a/relayer-cli/src/commands/query/connection.rs b/relayer-cli/src/commands/query/connection.rs index 74c46f0694..e03e6871d7 100644 --- a/relayer-cli/src/commands/query/connection.rs +++ b/relayer-cli/src/commands/query/connection.rs @@ -141,7 +141,7 @@ mod tests { name: "No connection id specified".to_string(), params: QueryConnectionEndCmd { connection_id: None, - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -149,7 +149,7 @@ mod tests { name: "Bad connection, non-alpha".to_string(), params: QueryConnectionEndCmd { connection_id: Some("conn01".to_string()), - ..default_params.clone() + ..default_params }, want_pass: false, }, @@ -157,7 +157,7 @@ mod tests { name: "Bad connection, name too short".to_string(), params: QueryConnectionEndCmd { connection_id: Some("connshort".to_string()), - ..default_params.clone() + ..default_params }, want_pass: false, }, diff --git a/relayer-cli/tests/acceptance.rs b/relayer-cli/tests/acceptance.rs index 2da38d04ca..9063229a18 100644 --- a/relayer-cli/tests/acceptance.rs +++ b/relayer-cli/tests/acceptance.rs @@ -27,7 +27,7 @@ use once_cell::sync::Lazy; /// the runner acquire a mutex when executing commands and inspecting /// exit statuses, serializing what would otherwise be multithreaded /// invocations as `cargo test` executes tests in parallel by default. -pub static RUNNER: Lazy = Lazy::new(|| CmdRunner::default()); +pub static RUNNER: Lazy = Lazy::new(CmdRunner::default); /// Use `Config::default()` value if no config or args #[test] diff --git a/relayer-cli/tests/integration.rs b/relayer-cli/tests/integration.rs index ac6efe2704..16de6a4001 100644 --- a/relayer-cli/tests/integration.rs +++ b/relayer-cli/tests/integration.rs @@ -63,7 +63,7 @@ fn query_connection_id() { assert_eq!(query.counterparty().connection_id(), "connectionidtwo"); assert_eq!( query.counterparty().prefix(), - &CommitmentPrefix::new("prefix".as_bytes().to_vec()) + &CommitmentPrefix::new(b"prefix".to_vec()) ); assert_eq!( query.versions(),